github-router 0.3.250 → 0.3.255

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.
Files changed (94) hide show
  1. package/dist/attribution-settings-CegtE0TY.js +2347 -0
  2. package/dist/attribution-settings-CegtE0TY.js.map +1 -0
  3. package/dist/auth-CP4q2CjO.js +44 -0
  4. package/dist/auth-CP4q2CjO.js.map +1 -0
  5. package/dist/browser-ext/manifest.json +1 -1
  6. package/dist/check-usage-6PB5IUFb.js +43 -0
  7. package/dist/check-usage-6PB5IUFb.js.map +1 -0
  8. package/dist/claude-BiZxoNWq.js +593 -0
  9. package/dist/claude-BiZxoNWq.js.map +1 -0
  10. package/dist/client-CQMfroGV.js +413 -0
  11. package/dist/client-CQMfroGV.js.map +1 -0
  12. package/dist/codex-ST21X3WF.js +85 -0
  13. package/dist/codex-ST21X3WF.js.map +1 -0
  14. package/dist/debug-CjmE9sgG.js +81 -0
  15. package/dist/debug-CjmE9sgG.js.map +1 -0
  16. package/dist/engine-DNCX0Hfd.js +2 -0
  17. package/dist/{lifecycle-DHZsVN8j.js → exec-DTO2oin0.js} +3 -130
  18. package/dist/exec-DTO2oin0.js.map +1 -0
  19. package/dist/file-log-reporter-IFQcL5IK.js +158 -0
  20. package/dist/file-log-reporter-IFQcL5IK.js.map +1 -0
  21. package/dist/gate-discovery-CepmbZnc.js +635 -0
  22. package/dist/gate-discovery-CepmbZnc.js.map +1 -0
  23. package/dist/get-copilot-usage-DD1N0Xlh.js +11 -0
  24. package/dist/get-copilot-usage-DD1N0Xlh.js.map +1 -0
  25. package/dist/hook-mcp-client-DBvm9608.js +136 -0
  26. package/dist/hook-mcp-client-DBvm9608.js.map +1 -0
  27. package/dist/internal-artifact-open-D2cQ2QCw.js +319 -0
  28. package/dist/internal-artifact-open-D2cQ2QCw.js.map +1 -0
  29. package/dist/internal-first-mate-guard-7v2CtMA6.js +2 -0
  30. package/dist/internal-first-mate-guard-La0tIjTU.js +126 -0
  31. package/dist/internal-first-mate-guard-La0tIjTU.js.map +1 -0
  32. package/dist/internal-plan-review-ipnT3bhj.js +114 -0
  33. package/dist/internal-plan-review-ipnT3bhj.js.map +1 -0
  34. package/dist/internal-prompt-submit-DOj75_hh.js +121 -0
  35. package/dist/internal-prompt-submit-DOj75_hh.js.map +1 -0
  36. package/dist/internal-session-bind-DwK2iGW0.js +133 -0
  37. package/dist/internal-session-bind-DwK2iGW0.js.map +1 -0
  38. package/dist/internal-stop-hook-CNPGJm63.js +212 -0
  39. package/dist/internal-stop-hook-CNPGJm63.js.map +1 -0
  40. package/dist/internal-stop-review-AUcFssrW.js +122 -0
  41. package/dist/internal-stop-review-AUcFssrW.js.map +1 -0
  42. package/dist/internal-worker-guard-Lu8VHj5k.js +75 -0
  43. package/dist/internal-worker-guard-Lu8VHj5k.js.map +1 -0
  44. package/dist/internal-workspace-header-BRMz0Yql.js +24 -0
  45. package/dist/internal-workspace-header-BRMz0Yql.js.map +1 -0
  46. package/dist/lifecycle-Byw5s1G8.js +134 -0
  47. package/dist/lifecycle-Byw5s1G8.js.map +1 -0
  48. package/dist/lifecycle-D80CaWie.js +2 -0
  49. package/dist/{lifecycle-CUf1UDrH.js → lifecycle-DD88ka3f.js} +3 -3
  50. package/dist/{lifecycle-CUf1UDrH.js.map → lifecycle-DD88ka3f.js.map} +1 -1
  51. package/dist/lifecycle-DzqCqDlM.js +2 -0
  52. package/dist/main.js +43 -12904
  53. package/dist/main.js.map +1 -1
  54. package/dist/mcp-workspace-header-CGJbNeHb.js +21 -0
  55. package/dist/mcp-workspace-header-CGJbNeHb.js.map +1 -0
  56. package/dist/models-B6UnGyRo.js +158 -0
  57. package/dist/models-B6UnGyRo.js.map +1 -0
  58. package/dist/orchestration-hRU4TdWr.js +39 -0
  59. package/dist/orchestration-hRU4TdWr.js.map +1 -0
  60. package/dist/{paths-BjTMI_xK.js → paths-C1cLZ7Ga.js} +52 -20
  61. package/dist/paths-C1cLZ7Ga.js.map +1 -0
  62. package/dist/paths-CwKwc2pi.js +2 -0
  63. package/dist/{peer-mcp-personas-BvommiSI.js → peer-mcp-personas-BDet588n.js} +163 -2392
  64. package/dist/peer-mcp-personas-BDet588n.js.map +1 -0
  65. package/dist/plan-review-hook-C8x0uKPf.js +159 -0
  66. package/dist/plan-review-hook-C8x0uKPf.js.map +1 -0
  67. package/dist/prompt-submit-hook-BrkkLAAB.js +153 -0
  68. package/dist/prompt-submit-hook-BrkkLAAB.js.map +1 -0
  69. package/dist/provision-DSkPa5hY.js +656 -0
  70. package/dist/provision-DSkPa5hY.js.map +1 -0
  71. package/dist/serve-sa_7c-yD.js +1511 -0
  72. package/dist/serve-sa_7c-yD.js.map +1 -0
  73. package/dist/server-setup-DcO9qvN3.js +4776 -0
  74. package/dist/server-setup-DcO9qvN3.js.map +1 -0
  75. package/dist/start-siGxuZhO.js +129 -0
  76. package/dist/start-siGxuZhO.js.map +1 -0
  77. package/dist/stop-gate-hook-DQc2KV7B.js +686 -0
  78. package/dist/stop-gate-hook-DQc2KV7B.js.map +1 -0
  79. package/dist/stop-gate-policy-CHwWbuF_.js +268 -0
  80. package/dist/stop-gate-policy-CHwWbuF_.js.map +1 -0
  81. package/dist/token-R2ocFC3M.js +1054 -0
  82. package/dist/token-R2ocFC3M.js.map +1 -0
  83. package/dist/version-_Q1WpsQp.js +32 -0
  84. package/dist/version-_Q1WpsQp.js.map +1 -0
  85. package/dist/worker-dispatch-Bj1uYyG9.js +222 -0
  86. package/dist/worker-dispatch-Bj1uYyG9.js.map +1 -0
  87. package/package.json +1 -1
  88. package/dist/engine-dOYoMQis.js +0 -2
  89. package/dist/lifecycle-0rXrwjWk.js +0 -2
  90. package/dist/lifecycle-DHZsVN8j.js.map +0 -1
  91. package/dist/lifecycle-hzPwXc3I.js +0 -2
  92. package/dist/paths-BjTMI_xK.js.map +0 -1
  93. package/dist/paths-D3XgmTcQ.js +0 -2
  94. package/dist/peer-mcp-personas-BvommiSI.js.map +0 -1
@@ -1,19 +1,26 @@
1
- import { t as PATHS } from "./paths-BjTMI_xK.js";
2
- import { d as resolveExecutable, f as runCommandCapture, l as parseBoolEnv, m as runManagedExeCapture, n as isPidAlive, o as trackChild, r as registerColbertExitHandlers, t as getColbertInstanceUuid, u as parseIntEnv } from "./lifecycle-DHZsVN8j.js";
3
- import { i as registerExitHandlers$1, n as getInstanceUuid, r as recordWorkerRepo, t as WorktreeRegistry } from "./lifecycle-CUf1UDrH.js";
1
+ import { t as getPackageVersion } from "./version-_Q1WpsQp.js";
2
+ import { t as PATHS } from "./paths-C1cLZ7Ga.js";
3
+ import { C as copilotHeaders, E as githubAgentHeaders, O as state, S as copilotBaseUrl, T as githubAgentGraphQLHeaders, _ as withTransientRetry, b as GITHUB_API_BASE_URL, d as resolveModel, f as sleep$2, g as fetchWithTransientRetry, h as isAllowedCopilotHost, i as tryRefreshAndRetry, v as HTTPError, w as copilotVersion, x as GITHUB_GRAPHQL_URL } from "./token-R2ocFC3M.js";
4
+ import { a as resolveExecutable, c as runManagedExeCapture, i as parseIntEnv, r as parseBoolEnv } from "./exec-DTO2oin0.js";
5
+ import { i as registerExitHandlers$1, n as getInstanceUuid, r as recordWorkerRepo, t as WorktreeRegistry } from "./lifecycle-DD88ka3f.js";
6
+ import { t as MCP_WORKSPACE_HEADER } from "./mcp-workspace-header-CGJbNeHb.js";
7
+ import { n as ArtifactError, r as applyInsecureTls, t as ArtifactClient } from "./client-CQMfroGV.js";
8
+ import { n as isPidAlive, o as trackChild, r as registerColbertExitHandlers, t as getColbertInstanceUuid } from "./lifecycle-Byw5s1G8.js";
9
+ import { h as runGateChecks, m as sealedGateIds, p as resolveSealedGate } from "./stop-gate-hook-DQc2KV7B.js";
10
+ import { t as liveExec } from "./orchestration-hRU4TdWr.js";
4
11
  import { createRequire } from "node:module";
5
12
  import consola from "consola";
6
- import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
7
- import fs, { chmod, mkdir, open, readFile, rename, rm, stat, symlink, writeFile } from "node:fs/promises";
8
- import * as os$1 from "node:os";
9
- import os, { homedir, platform, tmpdir } from "node:os";
13
+ import { chmodSync, closeSync, constants, cpSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
10
14
  import * as path$1 from "node:path";
11
15
  import path, { dirname, join } from "node:path";
16
+ import { fileURLToPath } from "node:url";
17
+ import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
18
+ import fs$1, { chmod, mkdir, open, readFile, rename, rm, stat, symlink, writeFile } from "node:fs/promises";
19
+ import * as os$1 from "node:os";
20
+ import os, { homedir, platform } from "node:os";
12
21
  import process$1 from "node:process";
13
22
  import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
14
- import { chmodSync, closeSync, constants, cpSync, existsSync, mkdirSync, openSync, promises, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
15
- import { fileURLToPath } from "node:url";
16
- import { Agent, ProxyAgent } from "undici";
23
+ import { ProxyAgent } from "undici";
17
24
  import { z } from "zod";
18
25
  import { AsyncLocalStorage } from "node:async_hooks";
19
26
  import { performance } from "node:perf_hooks";
@@ -33,914 +40,6 @@ import "diff";
33
40
  //#region \0rolldown/runtime.js
34
41
  var __require = /* #__PURE__ */ (() => createRequire(import.meta.url))();
35
42
  //#endregion
36
- //#region src/lib/state.ts
37
- const state = {
38
- accountType: "enterprise",
39
- manualApprove: false,
40
- rateLimitWait: false,
41
- showToken: false,
42
- extendedBetas: false,
43
- browseEnabled: false,
44
- fleetEnabled: false,
45
- agentsEnabled: false,
46
- powerBrowseEnabled: false,
47
- humanlikeForce: "auto",
48
- sessionId: randomUUID(),
49
- machineId: randomBytes(32).toString("hex")
50
- };
51
- //#endregion
52
- //#region src/lib/api-config.ts
53
- const standardHeaders = () => ({
54
- "content-type": "application/json",
55
- accept: "application/json"
56
- });
57
- const DEFAULT_COPILOT_VERSION = "0.43.2026033101";
58
- function copilotVersion(state) {
59
- return state.copilotVersion ?? DEFAULT_COPILOT_VERSION;
60
- }
61
- const API_VERSION = "2026-01-09";
62
- const copilotBaseUrl = (state) => state.copilotApiUrl ?? "https://api.githubcopilot.com";
63
- const copilotHeaders = (state, vision = false, integrationId = "vscode-chat") => {
64
- const version = copilotVersion(state);
65
- const headers = {
66
- Authorization: `Bearer ${state.copilotToken}`,
67
- "content-type": standardHeaders()["content-type"],
68
- "copilot-integration-id": integrationId,
69
- "editor-version": `vscode/${state.vsCodeVersion}`,
70
- "editor-plugin-version": `copilot-chat/${version}`,
71
- "user-agent": `GitHubCopilotChat/${version}`,
72
- "openai-intent": "conversation-panel",
73
- "x-interaction-type": "conversation-panel",
74
- "x-github-api-version": API_VERSION,
75
- "x-request-id": randomUUID(),
76
- "x-vscode-user-agent-library-version": "electron-fetch",
77
- "VScode-SessionId": state.sessionId,
78
- "VScode-MachineId": state.machineId
79
- };
80
- if (vision) headers["copilot-vision-request"] = "true";
81
- return headers;
82
- };
83
- const GITHUB_API_BASE_URL = process.env.GITHUB_API_URL ?? "https://api.github.com";
84
- const githubHeaders = (state) => ({
85
- ...standardHeaders(),
86
- authorization: `token ${state.githubToken}`,
87
- "editor-version": `vscode/${state.vsCodeVersion}`,
88
- "editor-plugin-version": `copilot-chat/${copilotVersion(state)}`,
89
- "user-agent": `GitHubCopilotChat/${copilotVersion(state)}`,
90
- "x-github-api-version": API_VERSION,
91
- "x-vscode-user-agent-library-version": "electron-fetch"
92
- });
93
- const GITHUB_BASE_URL = "https://github.com";
94
- const GITHUB_CLIENT_ID = "Iv1.b507a08c87ecfe98";
95
- const GITHUB_APP_SCOPES = ["read:user"].join(" ");
96
- const GITHUB_AGENT_CLIENT_ID = "178c6fc778ccc68e1d6a";
97
- const GITHUB_AGENT_SCOPES = [
98
- "repo",
99
- "workflow",
100
- "read:org"
101
- ].join(" ");
102
- const GITHUB_GRAPHQL_URL = process.env.GITHUB_GRAPHQL_URL ?? "https://api.github.com/graphql";
103
- const GITHUB_REST_API_VERSION = "2022-11-28";
104
- const githubAgentHeaders = (state) => ({
105
- ...standardHeaders(),
106
- authorization: `token ${state.githubAgentToken}`,
107
- "x-github-api-version": GITHUB_REST_API_VERSION,
108
- "user-agent": "github-router-first-mate"
109
- });
110
- const githubAgentGraphQLHeaders = (state, features) => ({
111
- ...githubAgentHeaders(state),
112
- ...features ? { "GraphQL-Features": features } : {}
113
- });
114
- //#endregion
115
- //#region src/lib/error.ts
116
- var HTTPError = class extends Error {
117
- response;
118
- constructor(message, response) {
119
- super(message);
120
- this.response = response;
121
- }
122
- };
123
- async function forwardError(c, error) {
124
- consola.error(`Error occurred at ${c.req.path}:`, error);
125
- if (error instanceof HTTPError) {
126
- const errorText = await error.response.text().catch(() => "");
127
- let errorJson;
128
- try {
129
- errorJson = JSON.parse(errorText);
130
- } catch {
131
- errorJson = void 0;
132
- }
133
- if (isContextOverflow(error.response.status, errorJson, errorText)) {
134
- const upstream = resolveErrorMessage(errorJson, errorText);
135
- consola.error("HTTP error (mapped to overflow):", errorJson ?? errorText);
136
- return c.json({
137
- type: "error",
138
- error: {
139
- type: "invalid_request_error",
140
- message: `prompt is too long: ${upstream}`
141
- }
142
- }, 400);
143
- }
144
- const responseStatus = error.response.status === 401 ? 503 : error.response.status;
145
- if (isAnthropicError(errorJson)) {
146
- consola.error("HTTP error:", errorJson);
147
- return c.json(errorJson, responseStatus);
148
- }
149
- const message = resolveErrorMessage(errorJson, errorText);
150
- consola.error("HTTP error:", errorJson ?? errorText);
151
- return c.json({
152
- type: "error",
153
- error: {
154
- type: resolveErrorType(responseStatus),
155
- message
156
- }
157
- }, responseStatus);
158
- }
159
- return c.json({
160
- type: "error",
161
- error: {
162
- type: "api_error",
163
- message: error instanceof Error ? error.message : String(error)
164
- }
165
- }, 500);
166
- }
167
- function resolveErrorMessage(errorJson, fallback) {
168
- if (typeof errorJson !== "object" || errorJson === null) return fallback;
169
- const errorRecord = errorJson;
170
- if (errorRecord.message !== void 0) return String(errorRecord.message);
171
- if (typeof errorRecord.error === "object" && errorRecord.error !== null) {
172
- const nestedRecord = errorRecord.error;
173
- if (nestedRecord.message !== void 0) return String(nestedRecord.message);
174
- }
175
- return fallback;
176
- }
177
- /**
178
- * Check if a parsed JSON body is already in Anthropic error format:
179
- * { type: "error", error: { type: "...", message: "..." } }
180
- */
181
- function isAnthropicError(json) {
182
- if (typeof json !== "object" || json === null) return false;
183
- const record = json;
184
- if (record.type !== "error") return false;
185
- if (typeof record.error !== "object" || record.error === null) return false;
186
- const inner = record.error;
187
- return typeof inner.type === "string" && typeof inner.message === "string";
188
- }
189
- const CONTEXT_OVERFLOW_SUBSTRINGS = [
190
- "prompt is too long",
191
- "context_length_exceeded",
192
- "context length exceeded",
193
- "input is too long",
194
- "maximum context length",
195
- "too many tokens"
196
- ];
197
- /**
198
- * Detect upstream context-overflow errors so we can remap them to a 400
199
- * "prompt is too long" shape that triggers Claude Code self-compaction.
200
- *
201
- * Always remaps 413 (treated as a hard payload-size signal regardless of
202
- * body wording). Remaps 400 only when the error text contains one of the
203
- * known overflow substrings — a regular 400 (e.g. "model not found") must
204
- * NOT remap.
205
- */
206
- function isContextOverflow(status, errorJson, errorText) {
207
- if (status === 413) return true;
208
- if (status !== 400) return false;
209
- const haystack = (errorText + " " + (typeof errorJson === "object" && errorJson !== null ? JSON.stringify(errorJson) : "")).toLowerCase();
210
- return CONTEXT_OVERFLOW_SUBSTRINGS.some((s) => haystack.includes(s));
211
- }
212
- /**
213
- * Map HTTP status to Anthropic error type.
214
- *
215
- * Note: a 401 from upstream is remapped to 503 in `forwardError` BEFORE
216
- * this function is called (no-401 invariant — see comment there). The
217
- * 401 → "authentication_error" mapping below is preserved for
218
- * defensive coverage in case any code path calls `resolveErrorType`
219
- * directly with an unsanitized status.
220
- */
221
- function resolveErrorType(status) {
222
- if (status === 400) return "invalid_request_error";
223
- if (status === 401) return "authentication_error";
224
- if (status === 403) return "permission_error";
225
- if (status === 404) return "not_found_error";
226
- if (status === 429) return "rate_limit_error";
227
- if (status === 503) return "overloaded_error";
228
- if (status === 529) return "overloaded_error";
229
- return "api_error";
230
- }
231
- //#endregion
232
- //#region src/lib/upstream-retry.ts
233
- /**
234
- * Shared transient-failure retry for upstream calls (`src/lib/upstream-retry.ts`).
235
- *
236
- * Retries ONLY on transient conditions — never on success, never on a
237
- * deterministic 4xx (400/401/403/404…), so a malformed request or auth
238
- * failure fails fast instead of being hammered. 401 is intentionally NOT
239
- * retried here: it stays with the existing token-refresh path so the
240
- * 401→503 forwardError invariant is preserved.
241
- *
242
- * Retryable:
243
- * - HTTP 429 + 5xx (500/502/503/504) — the "upstream is sick" class.
244
- * - network errors (ECONNRESET / "fetch failed" / "terminated" / EPIPE …).
245
- * - upstream TIMEOUT aborts: an AbortError thrown while the CALLER'S
246
- * `signal` is NOT aborted (a user cancel aborts the caller's signal
247
- * and is rethrown immediately, never retried).
248
- *
249
- * Exponential backoff + FULL jitter, capped, honoring `Retry-After`. The
250
- * inter-attempt sleep is abortable so a user cancel during backoff
251
- * returns promptly. Bounded `attempts` keep a single call from holding an
252
- * inflight slot indefinitely (robust AND fast).
253
- *
254
- * Streaming note: the user-facing passthrough and the worker loop must
255
- * only retry in the PRE-FIRST-BYTE window — a retry after bytes have
256
- * streamed would duplicate output. Callers there pass a `doFetch` that
257
- * has not yet handed its body to the consumer.
258
- */
259
- const DEFAULT_RETRY_STATUSES = [
260
- 429,
261
- 500,
262
- 502,
263
- 503,
264
- 504
265
- ];
266
- function parseRetryAfter(headerValue) {
267
- if (!headerValue) return void 0;
268
- const secs = Number(headerValue);
269
- if (Number.isFinite(secs)) return Math.max(0, secs * 1e3);
270
- const dateMs = Date.parse(headerValue);
271
- if (Number.isFinite(dateMs)) return Math.max(0, dateMs - Date.now());
272
- }
273
- /**
274
- * A thrown fetch error that is worth retrying — network resets, broken
275
- * pipes, DNS hiccups, and timeout aborts. NOTE: the caller must already
276
- * have ruled out a user cancel (caller signal aborted) before calling
277
- * this, since a timeout abort and a user-cancel abort look identical.
278
- */
279
- function isTransientNetworkError(err) {
280
- const e = err;
281
- if (!e) return false;
282
- if (e.name === "AbortError" || e.name === "TimeoutError") return true;
283
- const msg = (e.message ?? "").toLowerCase();
284
- if (msg.includes("terminated") || msg.includes("fetch failed") || msg.includes("network") || msg.includes("socket") || msg.includes("econnreset") || msg.includes("etimedout") || msg.includes("econnrefused") || msg.includes("epipe") || msg.includes("enotfound")) return true;
285
- const code = e.code ?? e.cause?.code;
286
- return code !== void 0 && [
287
- "ECONNRESET",
288
- "ETIMEDOUT",
289
- "ECONNREFUSED",
290
- "EPIPE",
291
- "ENOTFOUND"
292
- ].includes(code);
293
- }
294
- /** Sleep that resolves early (does not reject) when `signal` aborts — the
295
- * retry loop re-checks `signal.aborted` at the top and throws there. */
296
- function abortableSleep(ms, signal) {
297
- if (ms <= 0) return Promise.resolve();
298
- return new Promise((resolve) => {
299
- let settled = false;
300
- const done = () => {
301
- if (settled) return;
302
- settled = true;
303
- clearTimeout(timer);
304
- signal?.removeEventListener("abort", done);
305
- resolve();
306
- };
307
- const timer = setTimeout(done, ms);
308
- if (signal) {
309
- if (signal.aborted) {
310
- done();
311
- return;
312
- }
313
- signal.addEventListener("abort", done, { once: true });
314
- }
315
- });
316
- }
317
- /**
318
- * Run `doFetch` with bounded transient-failure retries. `doFetch` is a
319
- * thunk so each attempt issues a FRESH request (bodies can't be replayed
320
- * from a consumed stream). Returns the final `Response` — which may still
321
- * carry a retryable status if all attempts are exhausted (the caller
322
- * handles that as it would a single-shot failure).
323
- */
324
- async function fetchWithTransientRetry(doFetch, opts = {}) {
325
- const attempts = Math.max(1, opts.attempts ?? 3);
326
- const retryStatuses = opts.retryStatuses ?? DEFAULT_RETRY_STATUSES;
327
- const baseDelayMs = opts.baseDelayMs ?? 250;
328
- const maxDelayMs = opts.maxDelayMs ?? 4e3;
329
- const { signal, label } = opts;
330
- for (let attempt = 1;; attempt++) {
331
- if (signal?.aborted) throw new DOMException("This operation was aborted", "AbortError");
332
- let res;
333
- let caught;
334
- try {
335
- res = await doFetch(attempt);
336
- } catch (err) {
337
- caught = err;
338
- }
339
- if (res && !retryStatuses.includes(res.status)) return res;
340
- if (caught !== void 0) {
341
- if (signal?.aborted) throw caught;
342
- if (!isTransientNetworkError(caught)) throw caught;
343
- }
344
- if (attempt >= attempts) {
345
- if (res) return res;
346
- throw caught;
347
- }
348
- const retryAfterMs = res ? parseRetryAfter(res.headers.get("retry-after")) : void 0;
349
- if (res?.body) try {
350
- await res.body.cancel();
351
- } catch {}
352
- const expCap = Math.min(maxDelayMs, baseDelayMs * 2 ** (attempt - 1));
353
- const delay = Math.min(maxDelayMs, retryAfterMs ?? Math.round(Math.random() * expCap));
354
- if (label) {
355
- const why = res ? `HTTP ${res.status}` : caught?.name ?? "error";
356
- consola.debug(`[upstream-retry] ${label}: attempt ${attempt}/${attempts} failed (${why}); retrying in ${delay}ms`);
357
- }
358
- await abortableSleep(delay, signal);
359
- }
360
- }
361
- /** Extract an HTTP status from a thrown error (HTTPError carries
362
- * `.response.status`; others may carry `.status`/`.statusCode`; last
363
- * resort parses `"HTTP <code>"` from the message). */
364
- function errorStatus(err) {
365
- const e = err;
366
- for (const v of [
367
- e?.status,
368
- e?.statusCode,
369
- e?.response?.status
370
- ]) if (typeof v === "number") return v;
371
- const m = /\bHTTP (\d{3})\b/.exec(e?.message ?? "");
372
- return m ? Number(m[1]) : void 0;
373
- }
374
- /**
375
- * Generic transient-retry for a non-`Response`-returning call (e.g. the
376
- * Copilot service clients, which throw `HTTPError` on non-OK and throw on
377
- * network errors). Retries `fn()` when it throws a transient error — an
378
- * `HTTPError`-like with a retryable status (429/5xx) OR a transient
379
- * network/timeout error — using the same backoff + abort semantics as
380
- * `fetchWithTransientRetry`. Never retries a deterministic 4xx (incl.
381
- * 401), a non-transient throw, or a user cancel.
382
- */
383
- async function withTransientRetry(fn, opts = {}) {
384
- const attempts = Math.max(1, opts.attempts ?? 3);
385
- const retryStatuses = opts.retryStatuses ?? DEFAULT_RETRY_STATUSES;
386
- const baseDelayMs = opts.baseDelayMs ?? 250;
387
- const maxDelayMs = opts.maxDelayMs ?? 4e3;
388
- const { signal, label } = opts;
389
- for (let attempt = 1;; attempt++) {
390
- if (signal?.aborted) throw new DOMException("This operation was aborted", "AbortError");
391
- try {
392
- return await fn(attempt);
393
- } catch (err) {
394
- if (signal?.aborted) throw err;
395
- const status = errorStatus(err);
396
- if (!(status !== void 0 && retryStatuses.includes(status) || isTransientNetworkError(err)) || attempt >= attempts) throw err;
397
- const retryAfterMs = parseRetryAfter(err?.response?.headers?.get?.("retry-after") ?? null);
398
- const expCap = Math.min(maxDelayMs, baseDelayMs * 2 ** (attempt - 1));
399
- const delay = Math.min(maxDelayMs, retryAfterMs ?? Math.round(Math.random() * expCap));
400
- if (label) consola.debug(`[upstream-retry] ${label}: attempt ${attempt}/${attempts} threw (${status !== void 0 ? `HTTP ${status}` : err?.name ?? "error"}); retrying in ${delay}ms`);
401
- await abortableSleep(delay, signal);
402
- }
403
- }
404
- }
405
- //#endregion
406
- //#region src/services/github/get-copilot-token.ts
407
- /**
408
- * Allowlist of hosts the router will trust as the Copilot API base URL.
409
- * Anything else returned in `endpoints.api` (e.g. via a tampered or
410
- * misconfigured token-exchange response) is rejected — otherwise a
411
- * malicious value would receive the long-lived GitHub PAT we send to
412
- * `/mcp` for web search (see `src/services/copilot/web-search.ts`).
413
- */
414
- const COPILOT_HOST_ALLOWLIST = [
415
- "api.githubcopilot.com",
416
- "api.individual.githubcopilot.com",
417
- "api.business.githubcopilot.com",
418
- "api.enterprise.githubcopilot.com"
419
- ];
420
- /**
421
- * True iff `rawUrl` is an HTTPS URL whose host is a trusted Copilot API host.
422
- * Exported so every path that sends a long-lived token to a discovered
423
- * Copilot host (web search, first-mate CAPI session logs) shares one allowlist.
424
- */
425
- function isAllowedCopilotHost(rawUrl) {
426
- let parsed;
427
- try {
428
- parsed = new URL(rawUrl);
429
- } catch {
430
- return false;
431
- }
432
- if (parsed.protocol !== "https:") return false;
433
- return COPILOT_HOST_ALLOWLIST.includes(parsed.hostname);
434
- }
435
- const getCopilotToken = async () => {
436
- const response = await fetchWithTransientRetry(() => fetch(`${GITHUB_API_BASE_URL}/copilot_internal/v2/token`, { headers: githubHeaders(state) }), { label: "/copilot_internal/v2/token" });
437
- if (!response.ok) throw new HTTPError("Failed to get Copilot token", response);
438
- const data = await response.json();
439
- if (data.endpoints?.api) if (isAllowedCopilotHost(data.endpoints.api)) state.copilotApiUrl = data.endpoints.api;
440
- else consola.warn(`Refusing to honor Copilot API endpoint "${data.endpoints.api}" from the token-exchange response — not in allowlist (${COPILOT_HOST_ALLOWLIST.join(", ")}). ` + (state.copilotApiUrl ? `Keeping existing override "${state.copilotApiUrl}".` : `Falling back to the default api.githubcopilot.com.`));
441
- return data;
442
- };
443
- //#endregion
444
- //#region src/services/github/get-device-code.ts
445
- const DEFAULT_DEVICE_APP = {
446
- clientId: GITHUB_CLIENT_ID,
447
- scope: GITHUB_APP_SCOPES
448
- };
449
- async function getDeviceCode(app = DEFAULT_DEVICE_APP) {
450
- const response = await fetchWithTransientRetry(() => fetch(`${GITHUB_BASE_URL}/login/device/code`, {
451
- method: "POST",
452
- headers: standardHeaders(),
453
- body: JSON.stringify({
454
- client_id: app.clientId,
455
- scope: app.scope
456
- })
457
- }), { label: "/login/device/code" });
458
- if (!response.ok) throw new HTTPError("Failed to get device code", response);
459
- return await response.json();
460
- }
461
- //#endregion
462
- //#region src/services/github/get-user.ts
463
- async function getGitHubUser() {
464
- const response = await fetchWithTransientRetry(() => fetch(`${GITHUB_API_BASE_URL}/user`, { headers: {
465
- authorization: `token ${state.githubToken}`,
466
- ...standardHeaders()
467
- } }), { label: "/user" });
468
- if (!response.ok) throw new HTTPError("Failed to get GitHub user", response);
469
- return await response.json();
470
- }
471
- //#endregion
472
- //#region src/services/copilot/get-models.ts
473
- const getModels = async () => {
474
- const response = await fetchWithTransientRetry(() => tryRefreshAndRetry(() => fetch(`${copilotBaseUrl(state)}/models`, { headers: copilotHeaders(state) }), "/models"), { label: "/models" });
475
- if (!response.ok) throw new HTTPError("Failed to get models", response);
476
- return await response.json();
477
- };
478
- //#endregion
479
- //#region src/services/get-copilot-version.ts
480
- const FALLBACK$1 = "0.43.2026033101";
481
- async function getCopilotChatVersion() {
482
- const controller = new AbortController();
483
- const timeout = setTimeout(() => {
484
- controller.abort();
485
- }, 5e3);
486
- try {
487
- const response = await fetch("https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery", {
488
- method: "POST",
489
- headers: {
490
- "Content-Type": "application/json",
491
- Accept: "application/json;api-version=7.1-preview.1"
492
- },
493
- body: JSON.stringify({
494
- filters: [{ criteria: [{
495
- filterType: 7,
496
- value: "GitHub.copilot-chat"
497
- }] }],
498
- flags: 914
499
- }),
500
- signal: controller.signal
501
- });
502
- if (!response.ok) return FALLBACK$1;
503
- return (await response.json())?.results?.[0]?.extensions?.[0]?.versions?.[0]?.version ?? FALLBACK$1;
504
- } catch {
505
- return FALLBACK$1;
506
- } finally {
507
- clearTimeout(timeout);
508
- }
509
- }
510
- //#endregion
511
- //#region src/services/get-vscode-version.ts
512
- const FALLBACK = "1.104.3";
513
- async function getVSCodeVersion() {
514
- const controller = new AbortController();
515
- const timeout = setTimeout(() => {
516
- controller.abort();
517
- }, 5e3);
518
- try {
519
- const match = (await (await fetch("https://aur.archlinux.org/cgit/aur.git/plain/PKGBUILD?h=visual-studio-code-bin", { signal: controller.signal })).text()).match(/pkgver=([0-9.]+)/);
520
- if (match) return match[1];
521
- return FALLBACK;
522
- } catch {
523
- return FALLBACK;
524
- } finally {
525
- clearTimeout(timeout);
526
- }
527
- }
528
- await getVSCodeVersion();
529
- //#endregion
530
- //#region src/lib/utils.ts
531
- const sleep$3 = (ms) => new Promise((resolve) => {
532
- setTimeout(resolve, ms);
533
- });
534
- const isNullish = (value) => value === null || value === void 0;
535
- /**
536
- * Beta prefixes VS Code Copilot Chat v0.43 actually sends.
537
- * Default mode — makes proxy traffic indistinguishable from VS Code.
538
- */
539
- const VSCODE_BETA_PREFIXES = [
540
- "interleaved-thinking-",
541
- "context-management-",
542
- "advanced-tool-use-"
543
- ];
544
- /**
545
- * Extended beta prefixes for Claude CLI compatibility.
546
- * Enabled via --extended-betas flag. Includes all betas confirmed
547
- * to work with the Copilot API.
548
- *
549
- * Notably absent (Copilot 400s on these — verified live):
550
- * context-1m-, skills-, files-api-, code-execution-, output-128k-,
551
- * advisor-tool- (see EXPLICITLY_STRIPPED_BETA_PREFIXES below).
552
- * 1M context is unlocked by selecting `claude-opus-4.7-1m-internal`
553
- * as the model id, not via a beta header.
554
- *
555
- * Empirical verification (2026-05-11 against api.enterprise.githubcopilot.com):
556
- * task-budgets-2026-03-13 → 200 ACCEPTED (cost-ceiling leverage)
557
- * token-efficient-tools-2026-03-28 → 200 ACCEPTED (per-tool token saving)
558
- * summarize-connector-text-2026-03-13 → 200 (Anthropic-internal feature flag,
559
- * won't fire for non-ant users; allowlisted defensively for ant edge case)
560
- * afk-mode-2026-01-31 → 200 (Anthropic-internal feature flag)
561
- * cli-internal-2026-02-09 → 200 (USER_TYPE=ant only)
562
- * oauth-2025-04-20 → 200 (Files-API path; Files-API itself
563
- * is not supportable via Copilot, but the header alone is harmless)
564
- * prompt-caching-scope-2026-01-05 → 200 even with body cache_control.scope
565
- * stripped (already covered by `prompt-caching-` prefix above)
566
- */
567
- const EXTENDED_BETA_PREFIXES = [
568
- ...VSCODE_BETA_PREFIXES,
569
- "claude-code-",
570
- "effort-",
571
- "prompt-caching-",
572
- "computer-use-",
573
- "pdfs-",
574
- "max-tokens-",
575
- "token-counting-",
576
- "compact-",
577
- "structured-outputs-",
578
- "fast-mode-",
579
- "mcp-client-",
580
- "mcp-servers-",
581
- "redact-thinking-",
582
- "web-search-",
583
- "task-budgets-",
584
- "token-efficient-tools-",
585
- "summarize-connector-text-",
586
- "afk-mode-",
587
- "cli-internal-",
588
- "oauth-"
589
- ];
590
- /**
591
- * Beta prefixes the proxy explicitly STRIPS even from the extended
592
- * allowlist (and even if a future leverage mode broadens the allowlist
593
- * further). Defensive layer: today's allowlist-only filter would already
594
- * drop these because they're not in any allowlist, but keeping an
595
- * explicit deny-list catches future changes that broaden allow rules
596
- * (e.g. a hypothetical pattern-based mode that lets `claude-*` through).
597
- *
598
- * Empirical (2026-05-11): Copilot returns HTTP 400
599
- * `unsupported beta header(s): advisor-tool-2026-03-01`
600
- * on every request that includes `advisor-tool-`. Stripping it is the
601
- * difference between a working request (no ADVISOR semantics) and a
602
- * fully-failed request. Document upstream limitation in CLAUDE.md.
603
- */
604
- const EXPLICITLY_STRIPPED_BETA_PREFIXES = ["advisor-tool-"];
605
- /**
606
- * Filter an `anthropic-beta` header value, keeping only beta flags
607
- * in the active whitelist AND not in the explicit-strip list.
608
- * Uses extended prefixes when --extended-betas is enabled, VS Code-only
609
- * prefixes otherwise. Returns the filtered comma-separated string,
610
- * or undefined if nothing remains.
611
- */
612
- function filterBetaHeader(value) {
613
- const prefixes = state.extendedBetas ? EXTENDED_BETA_PREFIXES : VSCODE_BETA_PREFIXES;
614
- return value.split(",").map((v) => v.trim()).filter((v) => v && prefixes.some((prefix) => v.startsWith(prefix)) && !EXPLICITLY_STRIPPED_BETA_PREFIXES.some((p) => v.startsWith(p))).join(",") || void 0;
615
- }
616
- /**
617
- * Normalize a model ID for fuzzy comparison: lowercase, replace dots with
618
- * dashes, insert dash at letter→digit boundaries, and collapse repeated
619
- * dashes. E.g. "gpt5.3-codex" → "gpt-5-3-codex", "GPT-5.3-Codex" → "gpt-5-3-codex".
620
- */
621
- function normalizeModelId$1(id) {
622
- return id.toLowerCase().replace(/\./g, "-").replace(/([a-z])(\d)/g, "$1-$2").replace(/-{2,}/g, "-");
623
- }
624
- /**
625
- * Resolve a model name to the best available variant in the Copilot model list.
626
- *
627
- * Resolution cascade:
628
- * 0. `[1m]` literal-bracket suffix: strip, delegate, warn if downgraded.
629
- * Bracketed slug must never reach Copilot (400s on it). See cc-backup
630
- * `src/utils/context.ts:35-40` for Claude Code's 1M unlock mechanism.
631
- * 1. Exact match
632
- * 2. Case-insensitive match
633
- * 3. Family preference (opus→1m, codex→highest version)
634
- * 4. Normalized match (dots→dashes, letter-digit boundaries)
635
- * 5. Anthropic dated-slug retry: if the input matches `claude-...-YYYYMMDD`,
636
- * strip the date and re-run the cascade once. Family-guarded so non-claude
637
- * 8-digit suffixes can't be mis-stripped; runs after Steps 1-4 so explicit
638
- * version pinning (a dated catalog id matched at Step 1) always wins.
639
- * 6. Return as-is with a warning
640
- */
641
- /**
642
- * `[1m]`-downgrade warnings we've already emitted this process. `resolveModel`
643
- * is called many times per launch (env setup, model validation, the banner,
644
- * and — under `serve` — on every model-picker `/models` poll), so the downgrade
645
- * notice is deduped to once per model id to avoid log spam.
646
- */
647
- const warnedOneMDowngrade = /* @__PURE__ */ new Set();
648
- function resolveModel(modelId) {
649
- const models = state.models?.data;
650
- if (!models) return modelId;
651
- const oneMMatch = modelId.match(/^(.*)\[1m\]$/i);
652
- if (oneMMatch) {
653
- const stripped = oneMMatch[1];
654
- const resolved = resolveModel(stripped);
655
- if (!(/-1m(?:$|-)/.test(resolved) || (models.find((m) => m.id === resolved)?.capabilities?.limits?.max_context_window_tokens ?? 0) >= 1e6) && !warnedOneMDowngrade.has(modelId)) {
656
- warnedOneMDowngrade.add(modelId);
657
- consola.warn(`Model "${modelId}" requested 1M context but no 1M backend is in Copilot's catalog for this tier/family; downgrading upstream to "${resolved}" (200K). Claude Code's local context accounting will still assume 1M — expect premature auto-compact. Drop the [1m] suffix (or unset CLAUDE_CODE_DISABLE_1M_CONTEXT if you set it) to silence.`);
658
- }
659
- return resolved;
660
- }
661
- if (models.some((m) => m.id === modelId)) return modelId;
662
- const lower = modelId.toLowerCase();
663
- const ciMatch = models.find((m) => m.id.toLowerCase() === lower);
664
- if (ciMatch) return ciMatch.id;
665
- if (lower.includes("opus")) {
666
- const oneMs = models.filter((m) => m.id.includes("opus") && /-1m(?:$|-)/.test(m.id));
667
- const versionMatch = lower.match(/opus-(\d+)(?:[.-](\d+))?/);
668
- const requestedVersion = versionMatch ? versionMatch[2] ? `${versionMatch[1]}.${versionMatch[2]}` : versionMatch[1] : void 0;
669
- const oneM = (requestedVersion ? oneMs.find((m) => m.id.includes(`opus-${requestedVersion}-`)) : void 0) ?? (requestedVersion ? void 0 : oneMs[0]);
670
- if (oneM) return oneM.id;
671
- }
672
- if (lower.includes("codex")) {
673
- const codexModels = models.filter((m) => m.id.includes("codex") && !m.id.includes("mini"));
674
- if (codexModels.length > 0) {
675
- codexModels.sort((a, b) => b.id.localeCompare(a.id));
676
- return codexModels[0].id;
677
- }
678
- }
679
- const normalized = normalizeModelId$1(modelId);
680
- const normMatch = models.find((m) => normalizeModelId$1(m.id) === normalized);
681
- if (normMatch) return normMatch.id;
682
- const dateStripped = modelId.replace(/^(claude-[\w.-]+)-20\d{6}$/i, "$1");
683
- if (dateStripped !== modelId) {
684
- const retried = resolveModel(dateStripped);
685
- if (retried !== dateStripped || models.some((m) => m.id === dateStripped)) {
686
- consola.info(`Resolved Anthropic dated slug "${modelId}" → "${retried}" (stripped -YYYYMMDD; pass an explicit catalog id to pin a snapshot)`);
687
- return retried;
688
- }
689
- }
690
- if (lower.startsWith("claude-")) {
691
- const matchSonnet = /(?:^|-)sonnet(?:-|$)/.test(lower);
692
- const matchHaiku = /(?:^|-)haiku(?:-|$)/.test(lower);
693
- if (matchSonnet || matchHaiku) {
694
- const family = matchSonnet ? "sonnet" : "haiku";
695
- const familyMembers = models.filter((m) => new RegExp(`(?:^|-)${family}(?:-|$|\\.)`).test(m.id));
696
- if (familyMembers.length > 0) {
697
- familyMembers.sort((a, b) => b.id.localeCompare(a.id, void 0, { numeric: true }));
698
- const best = familyMembers[0].id;
699
- consola.info(`Model "${modelId}" not in Copilot catalog; falling back to highest available "${best}" (legacy ${family} slug). Pin a current catalog id to silence.`);
700
- return best;
701
- }
702
- }
703
- }
704
- consola.warn(`Model "${modelId}" not found in Copilot model list. Available: ${models.map((m) => m.id).join(", ")}`);
705
- return modelId;
706
- }
707
- /**
708
- * Resolve a codex model ID, falling back to the best available codex model.
709
- * Used by the codex subcommand for model selection.
710
- */
711
- function resolveCodexModel(modelId) {
712
- const resolved = resolveModel(modelId);
713
- const models = state.models?.data;
714
- if (!models) return resolved;
715
- if (models.some((m) => m.id === resolved)) return resolved;
716
- const candidates = models.filter((m) => {
717
- const endpoints = m.supported_endpoints ?? [];
718
- if (m.id.includes("mini") || m.id.includes("nano")) return false;
719
- return endpoints.length === 0 || endpoints.includes("/responses");
720
- });
721
- if (candidates.length > 0) {
722
- candidates.sort((a, b) => {
723
- const aCodex = a.id.includes("codex") ? 1 : 0;
724
- const bCodex = b.id.includes("codex") ? 1 : 0;
725
- if (aCodex !== bCodex) return bCodex - aCodex;
726
- return b.id.localeCompare(a.id);
727
- });
728
- const best = candidates[0].id;
729
- consola.warn(`Model "${modelId}" not available, using "${best}" instead`);
730
- return best;
731
- }
732
- return resolved;
733
- }
734
- async function cacheModels() {
735
- const models = await getModels();
736
- state.models = models;
737
- }
738
- const cacheVSCodeVersion = async () => {
739
- const response = await getVSCodeVersion();
740
- state.vsCodeVersion = response;
741
- consola.info(`Using VSCode version: ${response}`);
742
- };
743
- const cacheCopilotVersion = async () => {
744
- const version = await getCopilotChatVersion();
745
- state.copilotVersion = version;
746
- consola.info(`Using Copilot Chat version: ${version}`);
747
- };
748
- //#endregion
749
- //#region src/services/github/poll-access-token.ts
750
- async function pollAccessToken(deviceCode, clientId = GITHUB_CLIENT_ID) {
751
- const sleepDuration = (deviceCode.interval + 1) * 1e3;
752
- consola.debug(`Polling access token with interval of ${sleepDuration}ms`);
753
- const expiresAt = Date.now() + deviceCode.expires_in * 1e3;
754
- while (Date.now() < expiresAt) {
755
- const response = await fetch(`${GITHUB_BASE_URL}/login/oauth/access_token`, {
756
- method: "POST",
757
- headers: standardHeaders(),
758
- body: JSON.stringify({
759
- client_id: clientId,
760
- device_code: deviceCode.device_code,
761
- grant_type: "urn:ietf:params:oauth:grant-type:device_code"
762
- })
763
- });
764
- if (!response.ok) {
765
- consola.error("Failed to poll access token:", await response.text());
766
- if (Date.now() >= expiresAt) break;
767
- await sleep$3(sleepDuration);
768
- continue;
769
- }
770
- const json = await response.json();
771
- consola.debug("Polling access token response:", json);
772
- const { access_token } = json;
773
- if (access_token) return access_token;
774
- if (Date.now() >= expiresAt) break;
775
- await sleep$3(sleepDuration);
776
- }
777
- throw new Error("Device code expired. Please run auth again.");
778
- }
779
- //#endregion
780
- //#region src/lib/token.ts
781
- const readGithubToken = () => fs.readFile(PATHS.GITHUB_TOKEN_PATH, "utf8");
782
- const writeGithubToken = (token) => fs.writeFile(PATHS.GITHUB_TOKEN_PATH, token);
783
- const readGithubAgentToken = () => fs.readFile(PATHS.GITHUB_AGENT_TOKEN_PATH, "utf8");
784
- const writeGithubAgentToken = (token) => fs.writeFile(PATHS.GITHUB_AGENT_TOKEN_PATH, token, { mode: 384 });
785
- const setupCopilotToken = async () => {
786
- const { token, refresh_in } = await getCopilotToken();
787
- state.copilotToken = token;
788
- consola.debug("GitHub Copilot Token fetched successfully!");
789
- if (state.showToken) consola.info("Copilot token:", token);
790
- const refreshInterval = Math.max((refresh_in - 60) * 1e3, 1e3);
791
- setInterval(() => {
792
- refreshCopilotToken("interval");
793
- }, refreshInterval);
794
- };
795
- let inflightRefresh;
796
- let lastRefreshSuccess = 0;
797
- let lastRefreshFailure = 0;
798
- const REFRESH_SUCCESS_COOLDOWN_MS = 3e4;
799
- const REFRESH_FAILURE_COOLDOWN_MS = 5e3;
800
- async function refreshCopilotToken(reason) {
801
- if (inflightRefresh) return inflightRefresh;
802
- if (reason === "401-retry") {
803
- const now = Date.now();
804
- if (now - lastRefreshSuccess < REFRESH_SUCCESS_COOLDOWN_MS) {
805
- consola.debug(`refreshCopilotToken(${reason}) skipped: prior success within ${REFRESH_SUCCESS_COOLDOWN_MS}ms`);
806
- return;
807
- }
808
- if (now - lastRefreshFailure < REFRESH_FAILURE_COOLDOWN_MS) {
809
- consola.debug(`refreshCopilotToken(${reason}) skipped: prior failure within ${REFRESH_FAILURE_COOLDOWN_MS}ms`);
810
- return;
811
- }
812
- }
813
- inflightRefresh = (async () => {
814
- consola.debug(`Refreshing Copilot token (reason=${reason})`);
815
- try {
816
- const { token } = await getCopilotToken();
817
- state.copilotToken = token;
818
- lastRefreshSuccess = Date.now();
819
- consola.debug("Copilot token refreshed");
820
- if (state.showToken) consola.info("Refreshed Copilot token:", token);
821
- } catch (error) {
822
- lastRefreshFailure = Date.now();
823
- consola.error(`Failed to refresh Copilot token (reason=${reason}):`, error);
824
- } finally {
825
- inflightRefresh = void 0;
826
- }
827
- })();
828
- return inflightRefresh;
829
- }
830
- /**
831
- * Try `request()`. If it returns a 401, refresh the Copilot token (subject
832
- * to the single-flight + refresh-storm-protection of `refreshCopilotToken`)
833
- * and retry once. After one retry, propagate whatever the second attempt
834
- * returned — the caller's existing 401-handling path is preserved.
835
- *
836
- * The `request` callback is responsible for capturing `state.copilotToken`
837
- * locally before any await; this helper does NOT re-build the request
838
- * itself, just re-invokes the callback after a refresh.
839
- */
840
- async function tryRefreshAndRetry(request, routePath) {
841
- const first = await request();
842
- if (first.status !== 401) return first;
843
- consola.warn(`${routePath}: upstream returned 401, attempting one token refresh + retry`);
844
- await refreshCopilotToken("401-retry");
845
- return request();
846
- }
847
- async function setupGitHubToken(options) {
848
- try {
849
- const githubToken = await readGithubToken();
850
- if (githubToken && !options?.force) {
851
- state.githubToken = githubToken;
852
- if (state.showToken) consola.info("GitHub token:", githubToken);
853
- await logUser();
854
- return;
855
- }
856
- consola.info("Not logged in, getting new access token");
857
- const response = await getDeviceCode();
858
- consola.debug("Device code response:", response);
859
- consola.info(`Please enter the code "${response.user_code}" in ${response.verification_uri}`);
860
- const token = await pollAccessToken(response);
861
- await writeGithubToken(token);
862
- state.githubToken = token;
863
- if (state.showToken) consola.info("GitHub token:", token);
864
- await logUser();
865
- } catch (error) {
866
- if (error instanceof HTTPError) {
867
- consola.error("Failed to get GitHub token:", await error.response.json());
868
- throw error;
869
- }
870
- consola.error("Failed to get GitHub token:", error);
871
- throw error;
872
- }
873
- }
874
- async function logUser() {
875
- const user = await getGitHubUser();
876
- consola.info(`Logged in as ${user.login}`);
877
- }
878
- /**
879
- * Set up the SECOND, write-capable GitHub token used by the first-mate
880
- * agent-orchestration surface (`--agents`). Mirrors `setupGitHubToken`
881
- * but authenticates against the GitHub CLI's OAuth client
882
- * (`GITHUB_AGENT_CLIENT_ID`) requesting `repo workflow read:org`, and
883
- * stores the result apart at `PATHS.GITHUB_AGENT_TOKEN_PATH`. The Copilot
884
- * App token (`state.githubToken`) is left completely untouched — this is
885
- * a distinct identity for a distinct capability.
886
- *
887
- * Long-lived (device-flow user token) → no refresh loop; a later 401 is
888
- * surfaced to the caller as a revoked grant to re-run the login. Called
889
- * once from `setupAndServe` when `state.agentsEnabled` is true.
890
- */
891
- async function setupGitHubAgentToken(options) {
892
- try {
893
- const existing = (await readGithubAgentToken().catch(() => "")).trim();
894
- if (existing && !options?.force) {
895
- state.githubAgentToken = existing;
896
- if (state.showToken) consola.info("GitHub agent token:", existing);
897
- await warnIfAgentScopesInsufficient();
898
- return;
899
- }
900
- consola.info("Agent mode (--agents): a second GitHub login is required for a write-capable token (repo, workflow, read:org).");
901
- const response = await getDeviceCode({
902
- clientId: GITHUB_AGENT_CLIENT_ID,
903
- scope: GITHUB_AGENT_SCOPES
904
- });
905
- consola.debug("Agent device code response:", response);
906
- consola.info(`Please enter the code "${response.user_code}" in ${response.verification_uri} to authorize github-router's cloud-agent orchestration to act on your repositories.`);
907
- const token = await pollAccessToken(response, GITHUB_AGENT_CLIENT_ID);
908
- await writeGithubAgentToken(token);
909
- state.githubAgentToken = token;
910
- if (state.showToken) consola.info("GitHub agent token:", token);
911
- await warnIfAgentScopesInsufficient();
912
- } catch (error) {
913
- if (error instanceof HTTPError) {
914
- consola.error("Failed to get GitHub agent token:", await error.response.json());
915
- throw error;
916
- }
917
- consola.error("Failed to get GitHub agent token:", error);
918
- throw error;
919
- }
920
- }
921
- /**
922
- * Best-effort check that the agent token actually carries the scopes we
923
- * asked for. The GitHub CLI OAuth client is a classic OAuth App, so the
924
- * granted scopes are echoed in the `x-oauth-scopes` response header on
925
- * any authenticated call. Warn loudly (not fatal) if `repo`/`workflow`
926
- * are missing so the failure is diagnosable at login rather than at the
927
- * first write 403.
928
- */
929
- async function warnIfAgentScopesInsufficient() {
930
- try {
931
- const res = await fetch(`${GITHUB_API_BASE_URL}/user`, { headers: githubAgentHeaders(state) });
932
- if (res.status === 401) {
933
- consola.warn("GitHub agent token was rejected (401) — the grant may have been revoked. Re-run with --agents to log in again.");
934
- return;
935
- }
936
- const scopes = (res.headers.get("x-oauth-scopes") ?? "").split(",").map((s) => s.trim()).filter(Boolean);
937
- const missing = ["repo", "workflow"].filter((s) => !scopes.includes(s));
938
- if (missing.length > 0) consola.warn(`GitHub agent token is missing scope(s): ${missing.join(", ")}. The first-mate surface needs 'repo' + 'workflow' to create issues, assign cloud agents, and dispatch workflows. Re-run the agent login and grant the requested scopes.`);
939
- } catch (err) {
940
- consola.debug("Agent token scope check skipped:", err);
941
- }
942
- }
943
- //#endregion
944
43
  //#region src/lib/update-lock.ts
945
44
  /**
946
45
  * Cross-process install lock for the auto-update / self-update paths.
@@ -996,32 +95,6 @@ async function tryCreateLock(p) {
996
95
  }
997
96
  }
998
97
  //#endregion
999
- //#region src/lib/version.ts
1000
- /**
1001
- * Read this binary's published version from package.json at runtime.
1002
- *
1003
- * Done at runtime (not baked at build time) because release.yml builds
1004
- * BEFORE `npm version patch` bumps the version — a build-time inline
1005
- * would always ship the pre-bump value. The npm tarball ships package.json
1006
- * alongside `dist/`, so a sibling-up lookup from import.meta.url resolves
1007
- * cleanly in both dev (`src/lib/`) and bundled (`dist/`) layouts.
1008
- *
1009
- * Returns `"unknown"` if package.json can't be located or parsed —
1010
- * never throws, so the CLI never fails to start over version reporting.
1011
- */
1012
- function getPackageVersion() {
1013
- try {
1014
- const here = dirname(fileURLToPath(import.meta.url));
1015
- const candidates = [join(here, "..", "..", "package.json"), join(here, "..", "package.json")];
1016
- for (const path of candidates) try {
1017
- const raw = readFileSync(path, "utf8");
1018
- const parsed = JSON.parse(raw);
1019
- if (typeof parsed.version === "string" && (parsed.name === "github-router" || parsed.name === "@animeshkundu/github-router")) return parsed.version;
1020
- } catch {}
1021
- } catch {}
1022
- return "unknown";
1023
- }
1024
- //#endregion
1025
98
  //#region src/lib/port.ts
1026
99
  const DEFAULT_PORT = 8787;
1027
100
  const DEFAULT_CLAUDE_MODEL_FALLBACKS = [
@@ -1183,432 +256,6 @@ function collapsePathKeys(env) {
1183
256
  return env;
1184
257
  }
1185
258
  //#endregion
1186
- //#region src/lib/mcp-workspace-header.ts
1187
- /** Header the per-session MCP headersHelper emits, carrying the calling Claude
1188
- * session's working directory. The /mcp handler uses it as the default
1189
- * `workspace` for repo-scoped tools so a machine-wide `serve` targets the
1190
- * active repo, not the proxy launch cwd. */
1191
- const MCP_WORKSPACE_HEADER = "X-GH-Workspace";
1192
- /** stdout JSON the headersHelper prints; Claude merges it into the connection
1193
- * headers. `cwd` is the helper's own process cwd = the session's project dir. */
1194
- function buildWorkspaceHeaderJson(cwd) {
1195
- return JSON.stringify({ [MCP_WORKSPACE_HEADER]: cwd });
1196
- }
1197
- /** Command string Claude runs as the headersHelper. Mirrors
1198
- * buildPromptSubmitHookCommand's quoting (src/lib/orchestration/prompt-submit-hook.ts). */
1199
- function buildWorkspaceHeaderHelperCommand(execPath, scriptPath) {
1200
- const q = (s) => `"${s}"`;
1201
- return scriptPath && scriptPath !== execPath ? `${q(execPath)} ${q(scriptPath)} internal-workspace-header` : `${q(execPath)} internal-workspace-header`;
1202
- }
1203
- //#endregion
1204
- //#region src/lib/insecure-tls.ts
1205
- /**
1206
- * Runtime-aware "skip TLS verification" for a self-signed direct-HTTPS instance.
1207
- * The mechanism differs by runtime and getting it wrong is silent: the BUILT
1208
- * proxy runs under Node (bin shebang `#!/usr/bin/env node`), whose fetch (undici)
1209
- * IGNORES a per-request `tls` option — only a `dispatcher` relaxes verification.
1210
- * `bun run dev` and the test suite run under Bun, whose fetch honors `tls`. So we
1211
- * detect the runtime and attach the field it actually understands.
1212
- *
1213
- * Shared by the fleet control-plane client and the artifact-review client — both
1214
- * talk to a self-signed ai-or-die instance on loopback. Per-request scope (not a
1215
- * global TLS posture change) keeps Copilot upstream verification untouched.
1216
- */
1217
- const IS_BUN$1 = typeof globalThis.Bun !== "undefined";
1218
- let sharedInsecureDispatcher;
1219
- function insecureDispatcher() {
1220
- return sharedInsecureDispatcher ??= new Agent({ connect: { rejectUnauthorized: false } });
1221
- }
1222
- /**
1223
- * Attach the runtime-correct TLS-verification-off mechanism to a fetch init for a
1224
- * single self-signed direct-HTTPS instance: Bun → `tls`, Node → an undici
1225
- * `dispatcher`. Exported so BOTH runtime branches are unit-testable under one
1226
- * interpreter (the untested Node branch is exactly what shipped broken).
1227
- */
1228
- function applyInsecureTls(init, isBun = IS_BUN$1) {
1229
- if (isBun) init.tls = { rejectUnauthorized: false };
1230
- else init.dispatcher = insecureDispatcher();
1231
- }
1232
- //#endregion
1233
- //#region src/lib/artifact/client.ts
1234
- var ArtifactError = class extends Error {
1235
- code;
1236
- retryable;
1237
- status;
1238
- detail;
1239
- constructor(args) {
1240
- super(args.message);
1241
- this.name = "ArtifactError";
1242
- this.code = args.code;
1243
- this.retryable = args.retryable;
1244
- this.status = args.status;
1245
- this.detail = args.detail;
1246
- }
1247
- };
1248
- const AWAIT_DEFAULT_TIMEOUT_MS = 25e3;
1249
- const AWAIT_ABORT_MARGIN_MS = 5e3;
1250
- const DEFAULT_RETRY_BASE_MS = 250;
1251
- const TRANSIENT_RETRIES = 2;
1252
- const DEFAULT_RETRYABLE_CODES = /* @__PURE__ */ new Set(["UNREACHABLE", "TIMEOUT"]);
1253
- const AWAIT_RETRYABLE_CODES = /* @__PURE__ */ new Set(["UNREACHABLE"]);
1254
- var ArtifactClient = class {
1255
- baseUrl;
1256
- token;
1257
- sessionId;
1258
- fetchFn;
1259
- insecureTLS;
1260
- retryBaseMs;
1261
- constructor(options) {
1262
- this.baseUrl = options.baseUrl.replace(/\/+$/, "");
1263
- this.token = options.token;
1264
- this.sessionId = options.sessionId;
1265
- this.fetchFn = options.fetchFn ?? globalThis.fetch.bind(globalThis);
1266
- this.insecureTLS = options.insecureTLS ?? false;
1267
- this.retryBaseMs = options.retryBaseMs ?? DEFAULT_RETRY_BASE_MS;
1268
- }
1269
- /**
1270
- * Open (or replace) the review for this session. `mode` is advisory metadata
1271
- * forwarded to the server (forward-compat: ignored if unknown); the actual
1272
- * interactivity comes from the served HTML's `data-aod-*` markup. Retried on a
1273
- * transient failure with a STABLE idempotency key so a retry-after-success does
1274
- * not double-open.
1275
- */
1276
- open(file, opts = {}) {
1277
- const idempotencyKey = opts.idempotencyKey ?? randomUUID();
1278
- const body = { file };
1279
- if (opts.mode) body.mode = opts.mode;
1280
- return this.withRetry(() => this.requestOnce({
1281
- method: "POST",
1282
- pathname: this.path("/open"),
1283
- body,
1284
- signal: opts.signal,
1285
- idempotencyKey
1286
- }), TRANSIENT_RETRIES, opts.signal);
1287
- }
1288
- /**
1289
- * Replace the current review's content. Exactly one of `file` | `html` (the
1290
- * caller-facing tool enforces that). `html` is written by the SERVER to the
1291
- * review's existing sandboxed file, so `html` with no existing review is an
1292
- * INVALID_REQUEST from the server. Retried with a stable idempotency key.
1293
- */
1294
- update(opts) {
1295
- const idempotencyKey = opts.idempotencyKey ?? randomUUID();
1296
- const body = {};
1297
- if (opts.file !== void 0) body.file = opts.file;
1298
- if (opts.html !== void 0) body.html = opts.html;
1299
- return this.withRetry(() => this.requestOnce({
1300
- method: "POST",
1301
- pathname: this.path("/update"),
1302
- body,
1303
- signal: opts.signal,
1304
- idempotencyKey,
1305
- allowEmptyJson: true
1306
- }), TRANSIENT_RETRIES, opts.signal);
1307
- }
1308
- /** Force a reload from disk (no content change). Single-shot (idempotent, cheap). */
1309
- refresh(signal) {
1310
- return this.requestOnce({
1311
- method: "POST",
1312
- pathname: this.path("/refresh"),
1313
- signal,
1314
- allowEmptyJson: true
1315
- });
1316
- }
1317
- /** Hide the panel UI while keeping the review alive. Single-shot; server idempotent. */
1318
- dismiss(signal) {
1319
- return this.requestOnce({
1320
- method: "POST",
1321
- pathname: this.path("/dismiss"),
1322
- signal,
1323
- allowEmptyJson: true
1324
- });
1325
- }
1326
- /**
1327
- * Typed drain (contract v2.2 §1/§2). Long-holds up to the server cap, returns
1328
- * events with `id > cursor` plus the new high-water `cursor`. Idempotent by
1329
- * (cursor, event.id): re-calling with the same cursor replays the same window
1330
- * from the server's bounded buffer, so a lost cursor (compaction) is
1331
- * recoverable. Retried on a transient network failure (the cursor makes retry
1332
- * safe — no double-consumption).
1333
- */
1334
- awaitEvents(opts = {}) {
1335
- const serverTimeoutMs = typeof opts.timeoutMs === "number" && Number.isFinite(opts.timeoutMs) && opts.timeoutMs > 0 ? opts.timeoutMs : AWAIT_DEFAULT_TIMEOUT_MS;
1336
- const clientTimeoutMs = serverTimeoutMs + AWAIT_ABORT_MARGIN_MS;
1337
- return this.withRetry(() => this.requestOnce({
1338
- method: "GET",
1339
- pathname: this.path("/await"),
1340
- query: {
1341
- cursor: opts.cursor,
1342
- timeoutMs: String(serverTimeoutMs)
1343
- },
1344
- signal: opts.signal,
1345
- timeoutMsHint: clientTimeoutMs
1346
- }), TRANSIENT_RETRIES, opts.signal, AWAIT_RETRYABLE_CODES);
1347
- }
1348
- /** Free-text agent->human reply. SINGLE-SHOT (not retried): a retry-after-success
1349
- * would render a duplicate chat bubble server-side. */
1350
- agentReply(text, signal) {
1351
- return this.requestOnce({
1352
- method: "POST",
1353
- pathname: this.path("/agent-reply"),
1354
- body: { text },
1355
- signal,
1356
- allowEmptyJson: true
1357
- });
1358
- }
1359
- /**
1360
- * End the review. Retried with a stable idempotency key; a NOT_FOUND (the
1361
- * review is already ended — whether by a prior call or by a retry landing after
1362
- * the first attempt succeeded) is mapped to a successful `{ ok, status:"ended" }`
1363
- * rather than surfaced as a 404 (contract v2.2 §1.1).
1364
- */
1365
- async end(signal) {
1366
- const idempotencyKey = randomUUID();
1367
- try {
1368
- return await this.withRetry(() => this.requestOnce({
1369
- method: "POST",
1370
- pathname: this.path("/end"),
1371
- signal,
1372
- idempotencyKey,
1373
- allowEmptyJson: true
1374
- }), TRANSIENT_RETRIES, signal);
1375
- } catch (err) {
1376
- if (err instanceof ArtifactError && err.code === "NOT_FOUND") return {
1377
- ok: true,
1378
- status: "ended"
1379
- };
1380
- throw err;
1381
- }
1382
- }
1383
- /**
1384
- * FROZEN legacy long-poll (contract v2.2 §1). Old payload shape,
1385
- * comment-equivalent only. New agents use `awaitEvents`. Single request per
1386
- * call (the caller-facing tool owns the bounded re-poll budget).
1387
- */
1388
- poll(timeoutMsHint, signal) {
1389
- return this.requestOnce({
1390
- method: "GET",
1391
- pathname: this.path("/poll"),
1392
- signal,
1393
- timeoutMsHint
1394
- });
1395
- }
1396
- path(suffix) {
1397
- return `/api/artifact/${encodeURIComponent(this.sessionId)}${suffix}`;
1398
- }
1399
- async withRetry(fn, retries, signal, retryableCodes = DEFAULT_RETRYABLE_CODES) {
1400
- let attempt = 0;
1401
- for (;;) try {
1402
- return await fn();
1403
- } catch (err) {
1404
- attempt += 1;
1405
- if (!(err instanceof ArtifactError && err.retryable && retryableCodes.has(err.code)) || attempt > retries || signal?.aborted) throw err;
1406
- const base = this.retryBaseMs;
1407
- await sleep$2(base <= 0 ? 0 : Math.round(base * 2 ** (attempt - 1) * (.5 + Math.random() * .5)), signal);
1408
- }
1409
- }
1410
- async requestOnce(o) {
1411
- let url;
1412
- try {
1413
- url = new URL(o.pathname, `${this.baseUrl}/`);
1414
- } catch (err) {
1415
- throw new ArtifactError({
1416
- code: "UNREACHABLE",
1417
- message: "artifact API base URL is invalid",
1418
- retryable: false,
1419
- detail: err
1420
- });
1421
- }
1422
- if (o.query) {
1423
- for (const [key, value] of Object.entries(o.query)) if (value !== void 0) url.searchParams.set(key, value);
1424
- }
1425
- const timeout = combineSignalAndTimeout(o.signal, o.timeoutMsHint);
1426
- try {
1427
- const headers = { Authorization: `Bearer ${this.token}` };
1428
- if (o.body !== void 0) headers["Content-Type"] = "application/json";
1429
- if (o.idempotencyKey) headers["Idempotency-Key"] = o.idempotencyKey;
1430
- const init = {
1431
- method: o.method,
1432
- headers,
1433
- body: o.body === void 0 ? void 0 : JSON.stringify(o.body),
1434
- redirect: "error",
1435
- signal: timeout.signal
1436
- };
1437
- if (this.insecureTLS) applyInsecureTls(init);
1438
- const response = await this.fetchFn(url.toString(), init);
1439
- if (!response.ok) throw await mapHttpError$1(response);
1440
- let text;
1441
- try {
1442
- text = await response.text();
1443
- } catch (err) {
1444
- if (isAbortLike$2(err)) throw mapNetworkError$1(err);
1445
- throw new ArtifactError({
1446
- code: "INVALID_RESPONSE",
1447
- message: "artifact API response body could not be read",
1448
- retryable: false,
1449
- detail: err
1450
- });
1451
- }
1452
- if (!text && o.allowEmptyJson) return {};
1453
- try {
1454
- return JSON.parse(text);
1455
- } catch (err) {
1456
- throw new ArtifactError({
1457
- code: "INVALID_RESPONSE",
1458
- message: "artifact API returned a non-JSON response",
1459
- retryable: false,
1460
- detail: err
1461
- });
1462
- }
1463
- } catch (err) {
1464
- if (err instanceof ArtifactError) throw err;
1465
- throw mapNetworkError$1(err);
1466
- } finally {
1467
- timeout.cleanup();
1468
- }
1469
- }
1470
- };
1471
- function sleep$2(ms, signal) {
1472
- if (ms <= 0) return Promise.resolve();
1473
- return new Promise((resolve, reject) => {
1474
- const onAbort = () => {
1475
- clearTimeout(timer);
1476
- reject(new DOMException("retry backoff aborted", "AbortError"));
1477
- };
1478
- const timer = setTimeout(() => {
1479
- signal?.removeEventListener("abort", onAbort);
1480
- resolve();
1481
- }, ms);
1482
- if (signal?.aborted) {
1483
- onAbort();
1484
- return;
1485
- }
1486
- signal?.addEventListener("abort", onAbort, { once: true });
1487
- });
1488
- }
1489
- function combineSignalAndTimeout(signal, timeoutMsHint) {
1490
- const timeoutMs = typeof timeoutMsHint === "number" && Number.isFinite(timeoutMsHint) && timeoutMsHint > 0 ? timeoutMsHint : void 0;
1491
- if (timeoutMs === void 0) return {
1492
- signal,
1493
- cleanup: () => {}
1494
- };
1495
- const controller = new AbortController();
1496
- const abortFromCaller = () => {
1497
- try {
1498
- controller.abort(signal?.reason);
1499
- } catch {
1500
- controller.abort();
1501
- }
1502
- };
1503
- if (signal?.aborted) abortFromCaller();
1504
- signal?.addEventListener("abort", abortFromCaller, { once: true });
1505
- const timer = setTimeout(() => {
1506
- try {
1507
- controller.abort(new DOMException("artifact API request timed out", "TimeoutError"));
1508
- } catch {
1509
- controller.abort();
1510
- }
1511
- }, timeoutMs);
1512
- return {
1513
- signal: controller.signal,
1514
- cleanup: () => {
1515
- clearTimeout(timer);
1516
- signal?.removeEventListener("abort", abortFromCaller);
1517
- }
1518
- };
1519
- }
1520
- async function mapHttpError$1(response) {
1521
- const detail = await readErrorDetail$1(response);
1522
- const upstreamMessage = detailToMessage$1(detail);
1523
- const suffix = upstreamMessage ? `: ${upstreamMessage}` : "";
1524
- if (response.status === 401 || response.status === 403) return new ArtifactError({
1525
- code: "AUTH_FAILED",
1526
- message: `artifact API authentication failed (${response.status})${suffix}`,
1527
- retryable: false,
1528
- status: response.status,
1529
- detail
1530
- });
1531
- if (response.status === 404) return new ArtifactError({
1532
- code: "NOT_FOUND",
1533
- message: `artifact session or resource not found (404)${suffix}`,
1534
- retryable: false,
1535
- status: response.status,
1536
- detail
1537
- });
1538
- if (response.status === 408 || response.status === 504) return new ArtifactError({
1539
- code: "TIMEOUT",
1540
- message: `artifact API request timed out (${response.status})${suffix}`,
1541
- retryable: true,
1542
- status: response.status,
1543
- detail
1544
- });
1545
- if (response.status === 400 && detailToCode(detail)?.toUpperCase() === "INVALID_REQUEST") return new ArtifactError({
1546
- code: "INVALID_REQUEST",
1547
- message: `artifact API rejected the request (400)${suffix}`,
1548
- retryable: false,
1549
- status: response.status,
1550
- detail
1551
- });
1552
- return new ArtifactError({
1553
- code: "UPSTREAM_ERROR",
1554
- message: `artifact API returned HTTP ${response.status}${suffix}`,
1555
- retryable: response.status === 429 || response.status >= 500,
1556
- status: response.status,
1557
- detail
1558
- });
1559
- }
1560
- function mapNetworkError$1(err) {
1561
- if (isAbortLike$2(err)) return new ArtifactError({
1562
- code: "TIMEOUT",
1563
- message: "artifact API request timed out or was aborted",
1564
- retryable: true,
1565
- detail: err
1566
- });
1567
- return new ArtifactError({
1568
- code: "UNREACHABLE",
1569
- message: `artifact API unreachable: ${err instanceof Error ? err.message : String(err)}`,
1570
- retryable: true,
1571
- detail: err
1572
- });
1573
- }
1574
- async function readErrorDetail$1(response) {
1575
- const text = await response.text().catch(() => "");
1576
- if (!text) return void 0;
1577
- try {
1578
- return JSON.parse(text);
1579
- } catch {
1580
- return text;
1581
- }
1582
- }
1583
- function detailToMessage$1(detail) {
1584
- if (typeof detail === "string") return detail;
1585
- if (typeof detail !== "object" || detail === null) return void 0;
1586
- const record = detail;
1587
- const error = record.error;
1588
- if (typeof error === "string") return error;
1589
- if (typeof error === "object" && error !== null) {
1590
- const errorRecord = error;
1591
- if (typeof errorRecord.message === "string") return errorRecord.message;
1592
- if (typeof errorRecord.code === "string") return errorRecord.code;
1593
- }
1594
- if (typeof record.message === "string") return record.message;
1595
- }
1596
- /** Extract a machine `code` from an upstream error body (`{error:{code}}` or
1597
- * `{code}`) so a tagged 400 can be classified (INVALID_REQUEST). */
1598
- function detailToCode(detail) {
1599
- if (typeof detail !== "object" || detail === null) return void 0;
1600
- const record = detail;
1601
- const error = record.error;
1602
- if (typeof error === "object" && error !== null) {
1603
- const code = error.code;
1604
- if (typeof code === "string") return code;
1605
- }
1606
- if (typeof record.code === "string") return record.code;
1607
- }
1608
- function isAbortLike$2(err) {
1609
- return err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError");
1610
- }
1611
- //#endregion
1612
259
  //#region src/lib/artifact/tools.ts
1613
260
  const ARTIFACT_GROUP = "peers";
1614
261
  const ARTIFACT_POLL_TOOL_BUDGET_MS = 5e4;
@@ -2632,13 +1279,13 @@ function defaultFleetConfigPath() {
2632
1279
  async function loadFleetRegistryConfig(configPath = defaultFleetConfigPath()) {
2633
1280
  let stat;
2634
1281
  try {
2635
- stat = await fs.stat(configPath);
1282
+ stat = await fs$1.stat(configPath);
2636
1283
  } catch (err) {
2637
1284
  if (isNodeErrorCode(err, "ENOENT")) return { instances: [] };
2638
1285
  throw err;
2639
1286
  }
2640
1287
  if (process.platform !== "win32" && (stat.mode & 63) !== 0) console.warn(`[fleet] Registry file ${configPath} is group/other-readable; it contains bearer / tunnel credentials. Consider chmod 600.`);
2641
- const raw = await fs.readFile(configPath, "utf8");
1288
+ const raw = await fs$1.readFile(configPath, "utf8");
2642
1289
  if (raw.trim() === "") return { instances: [] };
2643
1290
  const parsed = JSON.parse(raw);
2644
1291
  if (!isObject(parsed)) throw new FleetRegistryError("INVALID_CONFIG", "fleet registry must be a JSON object");
@@ -2891,7 +1538,7 @@ function nonEmptyString(raw) {
2891
1538
  * validation; never throws — a missing/unreadable/oversized/malformed file
2892
1539
  * yields `[]` (discovery is best-effort and must never break static fleet use).
2893
1540
  */
2894
- async function readMeshPeers(readFileFn = (p) => fs.readFile(p, "utf8")) {
1541
+ async function readMeshPeers(readFileFn = (p) => fs$1.readFile(p, "utf8")) {
2895
1542
  if (meshDiscoveryDisabled()) return [];
2896
1543
  let raw;
2897
1544
  try {
@@ -2978,7 +1625,7 @@ function validEgressToken(raw) {
2978
1625
  */
2979
1626
  async function readMeshEgress(options = {}) {
2980
1627
  if (meshDiscoveryDisabled()) return void 0;
2981
- const readFileFn = options.readFileFn ?? ((p) => fs.readFile(p, "utf8"));
1628
+ const readFileFn = options.readFileFn ?? ((p) => fs$1.readFile(p, "utf8"));
2982
1629
  const now = options.now ?? (() => Date.now());
2983
1630
  const pidAlive = options.pidAlive ?? defaultPidAlive;
2984
1631
  let raw;
@@ -5559,7 +4206,7 @@ function isLease(value) {
5559
4206
  async function readLeaseFile(file) {
5560
4207
  let raw;
5561
4208
  try {
5562
- raw = await fs.readFile(file, "utf8");
4209
+ raw = await fs$1.readFile(file, "utf8");
5563
4210
  } catch (err) {
5564
4211
  if (err.code === "ENOENT") return void 0;
5565
4212
  throw err;
@@ -5572,40 +4219,40 @@ async function readLeaseFile(file) {
5572
4219
  }
5573
4220
  }
5574
4221
  async function writeLeaseAtomic(file, lease) {
5575
- await fs.mkdir(path.dirname(file), { recursive: true });
4222
+ await fs$1.mkdir(path.dirname(file), { recursive: true });
5576
4223
  const tmp = `${file}.tmp.${process.pid}.${randomBytes(4).toString("hex")}`;
5577
4224
  try {
5578
- await fs.writeFile(tmp, `${JSON.stringify(lease, null, 2)}\n`, { mode: 384 });
5579
- await fs.rename(tmp, file);
4225
+ await fs$1.writeFile(tmp, `${JSON.stringify(lease, null, 2)}\n`, { mode: 384 });
4226
+ await fs$1.rename(tmp, file);
5580
4227
  } catch (err) {
5581
- await fs.unlink(tmp).catch(() => {});
4228
+ await fs$1.unlink(tmp).catch(() => {});
5582
4229
  throw err;
5583
4230
  }
5584
4231
  }
5585
4232
  const LEASE_LOCK_MAX_WAIT_MS = 1e4;
5586
4233
  async function withLeaseLock(file, fn, hooks) {
5587
4234
  const lockPath = `${file}.lock`;
5588
- await fs.mkdir(path.dirname(lockPath), { recursive: true });
4235
+ await fs$1.mkdir(path.dirname(lockPath), { recursive: true });
5589
4236
  const ownerToken = `${process.pid}-${randomBytes(8).toString("hex")}`;
5590
4237
  const staging = `${lockPath}.acq.${ownerToken}`;
5591
- await fs.writeFile(staging, ownerToken, { mode: 384 });
4238
+ await fs$1.writeFile(staging, ownerToken, { mode: 384 });
5592
4239
  const start = Date.now();
5593
4240
  try {
5594
4241
  for (;;) try {
5595
- await fs.link(staging, lockPath);
4242
+ await fs$1.link(staging, lockPath);
5596
4243
  break;
5597
4244
  } catch (err) {
5598
4245
  if (err.code !== "EEXIST") throw err;
5599
4246
  let observed;
5600
4247
  try {
5601
- observed = (await fs.readFile(lockPath, "utf8")).trim();
5602
- const st = await fs.stat(lockPath);
4248
+ observed = (await fs$1.readFile(lockPath, "utf8")).trim();
4249
+ const st = await fs$1.stat(lockPath);
5603
4250
  if (Date.now() - st.mtimeMs > 1e4) {
5604
4251
  if (hooks?.onObservedStale) await hooks.onObservedStale();
5605
4252
  const breaker = `${lockPath}.stale.${ownerToken}`;
5606
4253
  let moved = false;
5607
4254
  try {
5608
- await fs.rename(lockPath, breaker);
4255
+ await fs$1.rename(lockPath, breaker);
5609
4256
  moved = true;
5610
4257
  } catch (e) {
5611
4258
  if (e.code !== "ENOENT") throw e;
@@ -5613,13 +4260,13 @@ async function withLeaseLock(file, fn, hooks) {
5613
4260
  if (moved) {
5614
4261
  let movedToken;
5615
4262
  try {
5616
- movedToken = (await fs.readFile(breaker, "utf8")).trim();
4263
+ movedToken = (await fs$1.readFile(breaker, "utf8")).trim();
5617
4264
  } catch {
5618
4265
  movedToken = void 0;
5619
4266
  }
5620
- if (movedToken === observed) await fs.unlink(breaker).catch(() => {});
5621
- else await fs.rename(breaker, lockPath).catch(async () => {
5622
- await fs.unlink(breaker).catch(() => {});
4267
+ if (movedToken === observed) await fs$1.unlink(breaker).catch(() => {});
4268
+ else await fs$1.rename(breaker, lockPath).catch(async () => {
4269
+ await fs$1.unlink(breaker).catch(() => {});
5623
4270
  });
5624
4271
  }
5625
4272
  continue;
@@ -5629,11 +4276,11 @@ async function withLeaseLock(file, fn, hooks) {
5629
4276
  await new Promise((r) => setTimeout(r, 15));
5630
4277
  }
5631
4278
  } finally {
5632
- await fs.unlink(staging).catch(() => {});
4279
+ await fs$1.unlink(staging).catch(() => {});
5633
4280
  }
5634
4281
  const verifyOwner = async () => {
5635
4282
  try {
5636
- return (await fs.readFile(lockPath, "utf8")).trim() === ownerToken;
4283
+ return (await fs$1.readFile(lockPath, "utf8")).trim() === ownerToken;
5637
4284
  } catch {
5638
4285
  return false;
5639
4286
  }
@@ -5641,7 +4288,7 @@ async function withLeaseLock(file, fn, hooks) {
5641
4288
  try {
5642
4289
  return await fn();
5643
4290
  } finally {
5644
- if (await verifyOwner()) await fs.unlink(lockPath).catch(() => {});
4291
+ if (await verifyOwner()) await fs$1.unlink(lockPath).catch(() => {});
5645
4292
  }
5646
4293
  }
5647
4294
  /**
@@ -5798,15 +4445,15 @@ function sleep$1(ms) {
5798
4445
  return new Promise((resolve) => setTimeout(resolve, ms));
5799
4446
  }
5800
4447
  async function writeJsonSecure(target, value) {
5801
- await fs.mkdir(path.dirname(target), { recursive: true });
4448
+ await fs$1.mkdir(path.dirname(target), { recursive: true });
5802
4449
  const tmp = `${target}.tmp.${process.pid}.${randomBytes(4).toString("hex")}`;
5803
4450
  try {
5804
- await fs.writeFile(tmp, `${JSON.stringify(value, null, 2)}\n`, { mode: 384 });
5805
- await fs.chmod(tmp, 384).catch(() => {});
5806
- await fs.rename(tmp, target);
5807
- await fs.chmod(target, 384).catch(() => {});
4451
+ await fs$1.writeFile(tmp, `${JSON.stringify(value, null, 2)}\n`, { mode: 384 });
4452
+ await fs$1.chmod(tmp, 384).catch(() => {});
4453
+ await fs$1.rename(tmp, target);
4454
+ await fs$1.chmod(target, 384).catch(() => {});
5808
4455
  } catch (err) {
5809
- await fs.unlink(tmp).catch(() => {});
4456
+ await fs$1.unlink(tmp).catch(() => {});
5810
4457
  throw err;
5811
4458
  }
5812
4459
  }
@@ -5815,11 +4462,11 @@ const LOCK_RETRY_MS = 25;
5815
4462
  const LOCK_MAX_WAIT_MS = 1e4;
5816
4463
  async function withFileLock(target, fn) {
5817
4464
  const lockPath = `${target}.lock`;
5818
- await fs.mkdir(path.dirname(lockPath), { recursive: true });
4465
+ await fs$1.mkdir(path.dirname(lockPath), { recursive: true });
5819
4466
  const ownerToken = `${process.pid}-${randomBytes(8).toString("hex")}`;
5820
4467
  const start = Date.now();
5821
4468
  for (;;) try {
5822
- const fh = await fs.open(lockPath, "wx");
4469
+ const fh = await fs$1.open(lockPath, "wx");
5823
4470
  await fh.writeFile(ownerToken);
5824
4471
  await fh.close();
5825
4472
  break;
@@ -5828,14 +4475,14 @@ async function withFileLock(target, fn) {
5828
4475
  if (!(code === "EEXIST" || code === "EPERM" && process.platform === "win32")) throw err;
5829
4476
  let observedToken;
5830
4477
  try {
5831
- observedToken = (await fs.readFile(lockPath, "utf8")).trim();
5832
- const st = await fs.stat(lockPath);
4478
+ observedToken = (await fs$1.readFile(lockPath, "utf8")).trim();
4479
+ const st = await fs$1.stat(lockPath);
5833
4480
  if (Date.now() - st.mtimeMs > LOCK_TTL_MS) {
5834
4481
  const stealPath = `${lockPath}.steal.${ownerToken}`;
5835
4482
  try {
5836
- await fs.rename(lockPath, stealPath);
5837
- if ((await fs.readFile(stealPath, "utf8").catch(() => "")).trim() === observedToken) await fs.unlink(stealPath).catch(() => {});
5838
- else await fs.rename(stealPath, lockPath).catch(() => fs.unlink(stealPath).catch(() => {}));
4483
+ await fs$1.rename(lockPath, stealPath);
4484
+ if ((await fs$1.readFile(stealPath, "utf8").catch(() => "")).trim() === observedToken) await fs$1.unlink(stealPath).catch(() => {});
4485
+ else await fs$1.rename(stealPath, lockPath).catch(() => fs$1.unlink(stealPath).catch(() => {}));
5839
4486
  } catch {}
5840
4487
  continue;
5841
4488
  }
@@ -5845,7 +4492,7 @@ async function withFileLock(target, fn) {
5845
4492
  }
5846
4493
  const verifyOwner = async () => {
5847
4494
  try {
5848
- return (await fs.readFile(lockPath, "utf8")).trim() === ownerToken;
4495
+ return (await fs$1.readFile(lockPath, "utf8")).trim() === ownerToken;
5849
4496
  } catch {
5850
4497
  return false;
5851
4498
  }
@@ -5853,7 +4500,7 @@ async function withFileLock(target, fn) {
5853
4500
  try {
5854
4501
  return await fn(verifyOwner);
5855
4502
  } finally {
5856
- if (await verifyOwner()) await fs.unlink(lockPath).catch(() => {});
4503
+ if (await verifyOwner()) await fs$1.unlink(lockPath).catch(() => {});
5857
4504
  }
5858
4505
  }
5859
4506
  const writeChains = /* @__PURE__ */ new Map();
@@ -5870,7 +4517,7 @@ const OCC_MAX_ATTEMPTS = 16;
5870
4517
  const OCC_BACKOFF_MS = 10;
5871
4518
  async function readRaw(file) {
5872
4519
  try {
5873
- return await fs.readFile(file, "utf8");
4520
+ return await fs$1.readFile(file, "utf8");
5874
4521
  } catch (err) {
5875
4522
  if (err.code === "ENOENT") return void 0;
5876
4523
  throw err;
@@ -6013,7 +4660,7 @@ function parseDecisions(raw) {
6013
4660
  async function readDecisionsFile() {
6014
4661
  let raw;
6015
4662
  try {
6016
- raw = await fs.readFile(decisionsPath(), "utf8");
4663
+ raw = await fs$1.readFile(decisionsPath(), "utf8");
6017
4664
  } catch (err) {
6018
4665
  if (err.code !== "ENOENT") consola.debug("first-mate decisions read skipped:", err);
6019
4666
  raw = void 0;
@@ -6617,7 +5264,7 @@ function parseLedgerRaw(raw) {
6617
5264
  async function readRepoLedgerWithRev(repo) {
6618
5265
  let raw;
6619
5266
  try {
6620
- raw = await fs.readFile(repoLedgerPath(repo), "utf8");
5267
+ raw = await fs$1.readFile(repoLedgerPath(repo), "utf8");
6621
5268
  } catch (err) {
6622
5269
  if (err.code !== "ENOENT") consola.debug("first-mate ledger read skipped:", err);
6623
5270
  return {
@@ -6751,7 +5398,7 @@ function repoKey(repo) {
6751
5398
  async function readMissions() {
6752
5399
  let raw;
6753
5400
  try {
6754
- raw = await fs.readFile(registryPath(), "utf8");
5401
+ raw = await fs$1.readFile(registryPath(), "utf8");
6755
5402
  } catch (err) {
6756
5403
  if (err.code !== "ENOENT") consola.debug("first-mate registry read skipped:", err);
6757
5404
  raw = void 0;
@@ -7109,7 +5756,7 @@ var Outbox = class {
7109
5756
  async read() {
7110
5757
  let raw;
7111
5758
  try {
7112
- raw = await fs.readFile(this.file, "utf8");
5759
+ raw = await fs$1.readFile(this.file, "utf8");
7113
5760
  } catch (err) {
7114
5761
  if (err.code === "ENOENT") return [];
7115
5762
  throw err;
@@ -7124,17 +5771,17 @@ var Outbox = class {
7124
5771
  }
7125
5772
  }
7126
5773
  async write(entries) {
7127
- await fs.mkdir(path.dirname(this.file), { recursive: true });
5774
+ await fs$1.mkdir(path.dirname(this.file), { recursive: true });
7128
5775
  const tmp = `${this.file}.tmp.${process.pid}.${randomBytes(4).toString("hex")}`;
7129
5776
  const value = {
7130
5777
  version: OUTBOX_VERSION,
7131
5778
  entries
7132
5779
  };
7133
5780
  try {
7134
- await fs.writeFile(tmp, `${JSON.stringify(value, null, 2)}\n`, { mode: 384 });
7135
- await fs.rename(tmp, this.file);
5781
+ await fs$1.writeFile(tmp, `${JSON.stringify(value, null, 2)}\n`, { mode: 384 });
5782
+ await fs$1.rename(tmp, this.file);
7136
5783
  } catch (err) {
7137
- await fs.unlink(tmp).catch(() => {});
5784
+ await fs$1.unlink(tmp).catch(() => {});
7138
5785
  throw err;
7139
5786
  }
7140
5787
  }
@@ -7462,9 +6109,9 @@ function sanitizeSegment(value) {
7462
6109
  }
7463
6110
  async function writeDecisionPacketHtml(packetId, html) {
7464
6111
  const dir = path.join(PATHS.FIRST_MATE_DIR, "packets");
7465
- await fs.mkdir(dir, { recursive: true });
6112
+ await fs$1.mkdir(dir, { recursive: true });
7466
6113
  const target = path.join(dir, `${sanitizeSegment(packetId)}.html`);
7467
- await fs.writeFile(target, html, { mode: 384 });
6114
+ await fs$1.writeFile(target, html, { mode: 384 });
7468
6115
  return target;
7469
6116
  }
7470
6117
  function agentRepo(repo) {
@@ -11204,7 +9851,7 @@ const WIN_RENAME_TRANSIENT = /* @__PURE__ */ new Set([
11204
9851
  */
11205
9852
  async function claimByRename(from, to) {
11206
9853
  for (let attempt = 0;; attempt++) try {
11207
- await fs.rename(from, to);
9854
+ await fs$1.rename(from, to);
11208
9855
  return true;
11209
9856
  } catch (err) {
11210
9857
  const code = err.code;
@@ -11241,8 +9888,8 @@ var AnswerInbox = class {
11241
9888
  }))];
11242
9889
  if (lines.length === 0) return 0;
11243
9890
  const run = this.chain.then(async () => {
11244
- await fs.mkdir(path.dirname(this.file), { recursive: true });
11245
- await fs.appendFile(this.file, `${lines.map((l) => JSON.stringify(l)).join("\n")}\n`, { mode: 384 });
9891
+ await fs$1.mkdir(path.dirname(this.file), { recursive: true });
9892
+ await fs$1.appendFile(this.file, `${lines.map((l) => JSON.stringify(l)).join("\n")}\n`, { mode: 384 });
11246
9893
  });
11247
9894
  this.chain = run.then(() => void 0, () => void 0);
11248
9895
  await run;
@@ -11283,7 +9930,7 @@ var AnswerInbox = class {
11283
9930
  const claimed = [];
11284
9931
  let siblings;
11285
9932
  try {
11286
- siblings = await fs.readdir(dir);
9933
+ siblings = await fs$1.readdir(dir);
11287
9934
  } catch {
11288
9935
  siblings = [];
11289
9936
  }
@@ -11294,7 +9941,7 @@ var AnswerInbox = class {
11294
9941
  const claim = `${orphan}.claim.${process.pid}.${randomBytes(4).toString("hex")}`;
11295
9942
  if (!await claimByRename(orphan, claim)) continue;
11296
9943
  try {
11297
- this.mergeLines(await fs.readFile(claim, "utf8"), out);
9944
+ this.mergeLines(await fs$1.readFile(claim, "utf8"), out);
11298
9945
  claimed.push(claim);
11299
9946
  } catch (err) {
11300
9947
  if (err.code !== "ENOENT") consola.warn(`first-mate: deferring unreadable answer claim ${claim} for retry:`, err);
@@ -11302,7 +9949,7 @@ var AnswerInbox = class {
11302
9949
  }
11303
9950
  const target = `${this.file}.draining.${process.pid}.${randomBytes(4).toString("hex")}`;
11304
9951
  if (await claimByRename(this.file, target)) try {
11305
- this.mergeLines(await fs.readFile(target, "utf8"), out);
9952
+ this.mergeLines(await fs$1.readFile(target, "utf8"), out);
11306
9953
  claimed.push(target);
11307
9954
  } catch (err) {
11308
9955
  if (err.code !== "ENOENT") consola.warn(`first-mate: deferring unreadable inbox claim ${target} for retry:`, err);
@@ -11310,7 +9957,7 @@ var AnswerInbox = class {
11310
9957
  for (const p of claimed) this.inflight.add(p);
11311
9958
  const ack = async () => {
11312
9959
  for (const p of claimed) {
11313
- await fs.unlink(p).catch(() => {});
9960
+ await fs$1.unlink(p).catch(() => {});
11314
9961
  this.inflight.delete(p);
11315
9962
  }
11316
9963
  };
@@ -11441,8 +10088,8 @@ var Tier1Shadow = class {
11441
10088
  this.judge = opts.judge ?? defaultTier1Judge;
11442
10089
  }
11443
10090
  async append(record) {
11444
- await fs.mkdir(path.dirname(this.file), { recursive: true });
11445
- await fs.appendFile(this.file, `${JSON.stringify(record)}\n`, { mode: 384 });
10091
+ await fs$1.mkdir(path.dirname(this.file), { recursive: true });
10092
+ await fs$1.appendFile(this.file, `${JSON.stringify(record)}\n`, { mode: 384 });
11446
10093
  }
11447
10094
  /**
11448
10095
  * Run the Tier1 judge for one request and log its would-be verdict. Returns
@@ -11719,7 +10366,7 @@ function parseStrategies(raw) {
11719
10366
  async function readStrategiesFile() {
11720
10367
  let raw;
11721
10368
  try {
11722
- raw = await fs.readFile(strategiesPath(), "utf8");
10369
+ raw = await fs$1.readFile(strategiesPath(), "utf8");
11723
10370
  } catch (err) {
11724
10371
  if (err.code !== "ENOENT") consola.debug("first-mate strategies read skipped:", err);
11725
10372
  raw = void 0;
@@ -16605,7 +15252,7 @@ function metaPath(workspace) {
16605
15252
  /** Read the sidecar metadata for a workspace (null if none yet). */
16606
15253
  async function readColbertMeta(workspace) {
16607
15254
  try {
16608
- const raw = await fs.readFile(metaPath(workspace), "utf8");
15255
+ const raw = await fs$1.readFile(metaPath(workspace), "utf8");
16609
15256
  const parsed = JSON.parse(raw);
16610
15257
  if (parsed && typeof parsed === "object" && typeof parsed.status === "string") return parsed;
16611
15258
  return null;
@@ -16630,14 +15277,14 @@ async function writeColbertMeta(meta) {
16630
15277
  return next;
16631
15278
  }
16632
15279
  async function writeColbertMetaUnchained(meta) {
16633
- await fs.mkdir(PATHS.COLBERT_META_DIR, { recursive: true });
15280
+ await fs$1.mkdir(PATHS.COLBERT_META_DIR, { recursive: true });
16634
15281
  const dest = metaPath(meta.workspace);
16635
15282
  const tmp = `${dest}.${process$1.pid}.${Math.random().toString(16).slice(2, 10)}.tmp`;
16636
15283
  try {
16637
- await fs.writeFile(tmp, JSON.stringify(meta, null, 2));
16638
- await fs.rename(tmp, dest);
15284
+ await fs$1.writeFile(tmp, JSON.stringify(meta, null, 2));
15285
+ await fs$1.rename(tmp, dest);
16639
15286
  } catch (err) {
16640
- await fs.rm(tmp, { force: true }).catch(() => {});
15287
+ await fs$1.rm(tmp, { force: true }).catch(() => {});
16641
15288
  throw err;
16642
15289
  }
16643
15290
  }
@@ -16653,7 +15300,7 @@ async function colbertProjectDir(workspace) {
16653
15300
  const indicesDir = PATHS.COLBERT_INDICES_DIR;
16654
15301
  let names;
16655
15302
  try {
16656
- names = await fs.readdir(indicesDir);
15303
+ names = await fs$1.readdir(indicesDir);
16657
15304
  } catch {
16658
15305
  return null;
16659
15306
  }
@@ -16663,7 +15310,7 @@ async function colbertProjectDir(workspace) {
16663
15310
  const dir = path.join(indicesDir, name);
16664
15311
  let proj;
16665
15312
  try {
16666
- proj = JSON.parse(await fs.readFile(path.join(dir, "project.json"), "utf8"));
15313
+ proj = JSON.parse(await fs$1.readFile(path.join(dir, "project.json"), "utf8"));
16667
15314
  } catch {
16668
15315
  continue;
16669
15316
  }
@@ -16814,7 +15461,7 @@ function indexDirSignature(workspace) {
16814
15461
  */
16815
15462
  async function realpathForCompare(p) {
16816
15463
  try {
16817
- return canonicalForCompare(await fs.realpath(p));
15464
+ return canonicalForCompare(await fs$1.realpath(p));
16818
15465
  } catch {
16819
15466
  return canonicalForCompare(p);
16820
15467
  }
@@ -17129,12 +15776,12 @@ async function runSemanticSearch(opts) {
17129
15776
  async function quarantineProjectDir(projectDir) {
17130
15777
  const quarantine = `${projectDir}.corrupt-${process$1.pid}-${randomBytes(4).toString("hex")}`;
17131
15778
  try {
17132
- await fs.rename(projectDir, quarantine);
15779
+ await fs$1.rename(projectDir, quarantine);
17133
15780
  } catch (err) {
17134
15781
  consola.debug("colbert: corrupt index quarantine rename failed:", err);
17135
15782
  return false;
17136
15783
  }
17137
- const removal = fs.rm(quarantine, {
15784
+ const removal = fs$1.rm(quarantine, {
17138
15785
  recursive: true,
17139
15786
  force: true
17140
15787
  }).catch((err) => {
@@ -18177,6 +16824,18 @@ function appBrowserMcpDir() {
18177
16824
  return dir;
18178
16825
  }
18179
16826
  /**
16827
+ * Memo for the interpreter probe below. The probe shells out to
16828
+ * `where`/`which`, which measured ~196ms per spawn on Windows, and it ran on
16829
+ * every browser tool call (twice, before the preflight was deduplicated).
16830
+ *
16831
+ * Only a SUCCESSFUL resolution is cached. `process.execPath` is the
16832
+ * fall-through when the probe fails, and caching that would pin a degraded
16833
+ * interpreter for the life of the process — including across a `node` install
16834
+ * that happens while the proxy is running. The path of an existing `node` on
16835
+ * PATH, by contrast, does not change mid-process in any way that matters here.
16836
+ */
16837
+ let cachedBridgeInterpreter;
16838
+ /**
18180
16839
  * Pick a runtime interpreter for the bridge. The bridge uses Node's
18181
16840
  * binary-stdin framing for native messaging which Bun handles
18182
16841
  * differently (Bun closes the bridge prematurely as soon as anything
@@ -18185,6 +16844,7 @@ function appBrowserMcpDir() {
18185
16844
  * binary) only if node isn't on PATH.
18186
16845
  */
18187
16846
  function resolveBridgeInterpreter() {
16847
+ if (cachedBridgeInterpreter !== void 0) return cachedBridgeInterpreter;
18188
16848
  const probeCmd = platform() === "win32" ? "where" : "which";
18189
16849
  try {
18190
16850
  const out = execFileSync(probeCmd, ["node"], {
@@ -18196,7 +16856,10 @@ function resolveBridgeInterpreter() {
18196
16856
  timeout: 2e3,
18197
16857
  windowsHide: true
18198
16858
  }).toString().trim().split(/\r?\n/)[0];
18199
- if (out) return out;
16859
+ if (out) {
16860
+ cachedBridgeInterpreter = out;
16861
+ return out;
16862
+ }
18200
16863
  } catch {}
18201
16864
  return process$1.execPath;
18202
16865
  }
@@ -18484,8 +17147,7 @@ async function probeHealth(port, token, timeoutMs = 500) {
18484
17147
  }
18485
17148
  function bridgeBundleExists() {
18486
17149
  try {
18487
- readFileSync(bridgeBundlePath());
18488
- return true;
17150
+ return statSync(bridgeBundlePath()).isFile();
18489
17151
  } catch {
18490
17152
  return false;
18491
17153
  }
@@ -18610,9 +17272,41 @@ let _inFlightReady;
18610
17272
  * Always 0 in production (only incremented when imported by tests).
18611
17273
  */
18612
17274
  let __implInvocationsForTests = 0;
17275
+ /**
17276
+ * Short-lived cache of a READY result.
17277
+ *
17278
+ * The single-flight above only collapses CONCURRENT callers — it clears in
17279
+ * `.finally()`, so sequential calls each re-run the full impl. Every browser
17280
+ * tool call runs the check twice (once in `browserPreflight` before slot
17281
+ * acquisition, once in `dispatchBrowserTool`, which is deliberate so internal
17282
+ * compound dispatches stay fail-closed), and each run re-probes health and
17283
+ * re-installs the native-messaging host — spawning `where`/`which` (~196ms on
17284
+ * Windows) and `reg.exe` (~26ms) per detected browser, synchronously.
17285
+ *
17286
+ * Only a READY result is cached, and only briefly:
17287
+ *
17288
+ * - Caching `install_required` would freeze a recoverable state — the user
17289
+ * loading the extension, or installing a browser, would not take effect
17290
+ * until restart. Those paths must stay self-healing.
17291
+ * - The TTL is deliberately short. These checks also REPAIR drift (a deleted
17292
+ * manifest, a cleared registry key), so the window in which that repair is
17293
+ * skipped has to stay small. A few seconds collapses the double-check and
17294
+ * a burst of calls without turning a self-healing probe into a one-shot.
17295
+ */
17296
+ const READY_CACHE_TTL_MS = 5e3;
17297
+ let _readyCache;
18613
17298
  async function ensureBridgeReady() {
17299
+ const cached = _readyCache;
17300
+ if (cached && Date.now() - cached.at < READY_CACHE_TTL_MS) return cached.value;
18614
17301
  if (_inFlightReady) return _inFlightReady;
18615
- _inFlightReady = _ensureBridgeReadyImpl().finally(() => {
17302
+ _inFlightReady = _ensureBridgeReadyImpl().then((result) => {
17303
+ if (!result.install_required) _readyCache = {
17304
+ at: Date.now(),
17305
+ value: result
17306
+ };
17307
+ else _readyCache = void 0;
17308
+ return result;
17309
+ }).finally(() => {
18616
17310
  _inFlightReady = void 0;
18617
17311
  });
18618
17312
  return _inFlightReady;
@@ -19239,7 +17933,7 @@ function logAudit$1(record) {
19239
17933
  try {
19240
17934
  const fs = await import("node:fs/promises");
19241
17935
  const path = await import("node:path");
19242
- const { PATHS } = await import("./paths-D3XgmTcQ.js");
17936
+ const { PATHS } = await import("./paths-CwKwc2pi.js");
19243
17937
  const dir = path.join(PATHS.APP_DIR, "browser-mcp");
19244
17938
  await fs.mkdir(dir, { recursive: true });
19245
17939
  const line = JSON.stringify({
@@ -20137,11 +18831,12 @@ const createChatCompletions = async (payload, modelHeaders, callerSignal, retryT
20137
18831
  }
20138
18832
  const claudeModels = state.models?.data.filter((m) => m.id.startsWith("claude")).map((m) => m.id).join(", ") ?? "(models not loaded)";
20139
18833
  consola.error(`Copilot rejected model "${payload.model}": ${response.status} ${errorBody} (available Claude models: ${claudeModels})`);
20140
- throw new HTTPError("Failed to create chat completions", new Response(errorBody, {
18834
+ const reconstructed = new Response(errorBody, {
20141
18835
  status: response.status,
20142
18836
  statusText: response.statusText,
20143
18837
  headers: response.headers
20144
- }));
18838
+ });
18839
+ throw new HTTPError("Failed to create chat completions", reconstructed);
20145
18840
  }
20146
18841
  if (payload.stream) return events(response);
20147
18842
  const cappedResult = await readResponseBodyCapped(response, "/v1/chat/completions", MAX_RESPONSE_BODY_BYTES);
@@ -26670,11 +25365,12 @@ async function createMessages(body, extraHeaders, callerSignal, retryTransient =
26670
25365
  errorBody = "(could not read error body)";
26671
25366
  }
26672
25367
  consola.error(`Copilot /v1/messages error: ${response.status} ${errorBody}`);
26673
- throw new HTTPError("Copilot messages request failed", new Response(errorBody, {
25368
+ const reconstructed = new Response(errorBody, {
26674
25369
  status: response.status,
26675
25370
  statusText: response.statusText,
26676
25371
  headers: response.headers
26677
- }));
25372
+ });
25373
+ throw new HTTPError("Copilot messages request failed", reconstructed);
26678
25374
  }
26679
25375
  return response;
26680
25376
  }
@@ -26717,11 +25413,12 @@ async function countTokens(body, extraHeaders, callerSignal, retryTransient = fa
26717
25413
  errorBody = "(could not read error body)";
26718
25414
  }
26719
25415
  consola.error(`Copilot count_tokens error: ${response.status} ${errorBody}`);
26720
- throw new HTTPError("Copilot count_tokens request failed", new Response(errorBody, {
25416
+ const reconstructed = new Response(errorBody, {
26721
25417
  status: response.status,
26722
25418
  statusText: response.statusText,
26723
25419
  headers: response.headers
26724
- }));
25420
+ });
25421
+ throw new HTTPError("Copilot count_tokens request failed", reconstructed);
26725
25422
  }
26726
25423
  return response;
26727
25424
  }
@@ -28894,7 +27591,7 @@ async function throttleSearch() {
28894
27591
  const waitMs = 1e3 - (now - searchTimestamps[0]);
28895
27592
  if (waitMs > 0) {
28896
27593
  consola.debug(`Web search rate limited, waiting ${waitMs}ms`);
28897
- await sleep$3(waitMs);
27594
+ await sleep$2(waitMs);
28898
27595
  }
28899
27596
  }
28900
27597
  searchTimestamps.push(Date.now());
@@ -30900,7 +29597,7 @@ async function sweepAgedWorkerDiffs(opts = {}) {
30900
29597
  }
30901
29598
  let entries;
30902
29599
  try {
30903
- entries = await fs.readdir(PATHS.WORKER_DIFFS_DIR);
29600
+ entries = await fs$1.readdir(PATHS.WORKER_DIFFS_DIR);
30904
29601
  } catch {
30905
29602
  return;
30906
29603
  }
@@ -30909,8 +29606,8 @@ async function sweepAgedWorkerDiffs(opts = {}) {
30909
29606
  if (!WORKER_DIFF_NAME_RE.test(name)) continue;
30910
29607
  const full = path.join(PATHS.WORKER_DIFFS_DIR, name);
30911
29608
  try {
30912
- if (now - (await fs.stat(full)).mtimeMs < AGE_SWEEP_MS$1) continue;
30913
- await fs.rm(full, { force: true }).catch(() => {});
29609
+ if (now - (await fs$1.stat(full)).mtimeMs < AGE_SWEEP_MS$1) continue;
29610
+ await fs$1.rm(full, { force: true }).catch(() => {});
30914
29611
  } catch {}
30915
29612
  }
30916
29613
  }
@@ -30923,13 +29620,13 @@ async function sweepAgedWorkerDiffs(opts = {}) {
30923
29620
  */
30924
29621
  async function saveOverflowResult(text) {
30925
29622
  await sweepAgedWorkerDiffs({ throttle: true });
30926
- await fs.mkdir(PATHS.WORKER_DIFFS_DIR, { recursive: true });
29623
+ await fs$1.mkdir(PATHS.WORKER_DIFFS_DIR, { recursive: true });
30927
29624
  let lastErr;
30928
29625
  for (let attempt = 0; attempt < 5; attempt++) {
30929
29626
  const name = `${process$1.pid}-${randomBytes(4).toString("hex")}.txt`;
30930
29627
  const outPath = path.join(PATHS.WORKER_DIFFS_DIR, name);
30931
29628
  try {
30932
- await fs.writeFile(outPath, text, {
29629
+ await fs$1.writeFile(outPath, text, {
30933
29630
  mode: 384,
30934
29631
  flag: "wx"
30935
29632
  });
@@ -31123,7 +29820,7 @@ async function findRepoRoot(workspaceAbs) {
31123
29820
  async function sweepAgedWorktrees(parent) {
31124
29821
  let entries;
31125
29822
  try {
31126
- entries = await fs.readdir(parent);
29823
+ entries = await fs$1.readdir(parent);
31127
29824
  } catch (err) {
31128
29825
  if (err.code === "ENOENT") return;
31129
29826
  return;
@@ -31133,7 +29830,7 @@ async function sweepAgedWorktrees(parent) {
31133
29830
  if (!WORKTREE_DIR_NAME_RE.test(name)) continue;
31134
29831
  const full = path.join(parent, name);
31135
29832
  try {
31136
- const ageMs = now - (await fs.stat(full)).mtimeMs;
29833
+ const ageMs = now - (await fs$1.stat(full)).mtimeMs;
31137
29834
  if (ageMs < AGE_SWEEP_MTIME_FLOOR_MS) continue;
31138
29835
  if (ageMs < AGE_SWEEP_MS) continue;
31139
29836
  await execFileP("git", [
@@ -31142,7 +29839,7 @@ async function sweepAgedWorktrees(parent) {
31142
29839
  "--force",
31143
29840
  full
31144
29841
  ], { timeout: 1e4 }).catch(() => {});
31145
- await fs.rm(full, {
29842
+ await fs$1.rm(full, {
31146
29843
  recursive: true,
31147
29844
  force: true
31148
29845
  }).catch(() => {});
@@ -31161,11 +29858,11 @@ async function sweepAgedWorktrees(parent) {
31161
29858
  async function createWorktree(workspaceAbs, opts) {
31162
29859
  const { repoRoot, gitCommonDir } = await findRepoRoot(workspaceAbs);
31163
29860
  const parent = path.join(gitCommonDir, "worker-worktrees");
31164
- await fs.mkdir(parent, { recursive: true });
29861
+ await fs$1.mkdir(parent, { recursive: true });
31165
29862
  await sweepAgedWorktrees(parent);
31166
29863
  let existing = [];
31167
29864
  try {
31168
- existing = await fs.readdir(parent);
29865
+ existing = await fs$1.readdir(parent);
31169
29866
  } catch {}
31170
29867
  if (existing.filter((n) => WORKTREE_DIR_NAME_RE.test(n)).length >= QUOTA_PER_REPO) throw new Error(`worker-agent worktree: per-repo quota exceeded (>=${QUOTA_PER_REPO} entries under ${parent}); abort, investigate, then prune manually or wait for the age sweep`);
31171
29868
  const suffix = randomBytes(4).toString("hex");
@@ -31213,9 +29910,9 @@ async function createWorktree(workspaceAbs, opts) {
31213
29910
  for (const rel of files) {
31214
29911
  const src = path.join(repoRoot, rel);
31215
29912
  const dst = path.join(dir, rel);
31216
- await fs.mkdir(path.dirname(dst), { recursive: true });
29913
+ await fs$1.mkdir(path.dirname(dst), { recursive: true });
31217
29914
  try {
31218
- await fs.copyFile(src, dst);
29915
+ await fs$1.copyFile(src, dst);
31219
29916
  } catch (err) {
31220
29917
  if (err.code === "ENOENT") continue;
31221
29918
  throw err;
@@ -31334,13 +30031,13 @@ async function saveOverflowPatch(dir) {
31334
30031
  "HEAD"
31335
30032
  ], { maxBuffer: 268435456 });
31336
30033
  await sweepAgedWorkerDiffs({ throttle: true });
31337
- await fs.mkdir(PATHS.WORKER_DIFFS_DIR, { recursive: true });
30034
+ await fs$1.mkdir(PATHS.WORKER_DIFFS_DIR, { recursive: true });
31338
30035
  let lastErr;
31339
30036
  for (let attempt = 0; attempt < 5; attempt++) {
31340
30037
  const name = `${process$1.pid}-${randomBytes(4).toString("hex")}.patch`;
31341
30038
  const patchPath = path.join(PATHS.WORKER_DIFFS_DIR, name);
31342
30039
  try {
31343
- await fs.writeFile(patchPath, patch.stdout, {
30040
+ await fs$1.writeFile(patchPath, patch.stdout, {
31344
30041
  mode: 384,
31345
30042
  flag: "wx"
31346
30043
  });
@@ -32404,93 +31101,6 @@ function selectChampion(orchestrated, baseline, canonicalGateIds, tiePolicy) {
32404
31101
  };
32405
31102
  }
32406
31103
  //#endregion
32407
- //#region src/lib/orchestration/gate-runner.ts
32408
- async function runGateChecks(checks, cwd, exec) {
32409
- const results = await Promise.all(checks.map(async (c) => {
32410
- try {
32411
- const r = await exec({
32412
- command: c.command,
32413
- cwd
32414
- });
32415
- return {
32416
- id: c.id,
32417
- passed: r.exitCode === 0
32418
- };
32419
- } catch {
32420
- return {
32421
- id: c.id,
32422
- passed: false
32423
- };
32424
- }
32425
- }));
32426
- const passed = /* @__PURE__ */ new Set();
32427
- const ran = /* @__PURE__ */ new Set();
32428
- for (const r of results) {
32429
- ran.add(r.id);
32430
- if (r.passed) passed.add(r.id);
32431
- }
32432
- return {
32433
- passed,
32434
- ran
32435
- };
32436
- }
32437
- //#endregion
32438
- //#region src/lib/orchestration/gate-registry.ts
32439
- /**
32440
- * Built-in sealed gates. Commands follow this repo's TS/Bun conventions (the
32441
- * `bun run <script>` indirection means a repo without that script simply fails
32442
- * the check, which the selector treats as not-passed rather than a crash). New
32443
- * ecosystems get a new sealed id here, never a caller-supplied command.
32444
- */
32445
- const SEALED_GATES = {
32446
- "default-ci": [
32447
- {
32448
- id: "typecheck",
32449
- command: "bun run typecheck"
32450
- },
32451
- {
32452
- id: "test",
32453
- command: "bun test"
32454
- },
32455
- {
32456
- id: "lint",
32457
- command: "bun run lint"
32458
- }
32459
- ],
32460
- "typecheck-test": [{
32461
- id: "typecheck",
32462
- command: "bun run typecheck"
32463
- }, {
32464
- id: "test",
32465
- command: "bun test"
32466
- }],
32467
- "typecheck-only": [{
32468
- id: "typecheck",
32469
- command: "bun run typecheck"
32470
- }]
32471
- };
32472
- /** The set of sealed gate ids, used as the kernel's `knownGateIds` so the IR
32473
- * verifier rejects an executable gate that references an unregistered id. */
32474
- function sealedGateIds() {
32475
- return new Set(Object.keys(SEALED_GATES));
32476
- }
32477
- /**
32478
- * Resolve a sealed gate by id. Returns a DEFENSIVE CLONE (fresh objects) so a
32479
- * caller can never mutate the registry's command set. `undefined` for an
32480
- * unknown id, which `run_workflow` rejects before executing anything.
32481
- */
32482
- function resolveSealedGate(gateId) {
32483
- const checks = SEALED_GATES[gateId];
32484
- if (!checks) return void 0;
32485
- return {
32486
- id: gateId,
32487
- checks: checks.map((c) => ({
32488
- id: c.id,
32489
- command: c.command
32490
- }))
32491
- };
32492
- }
32493
- //#endregion
32494
31104
  //#region src/lib/orchestration/verify.ts
32495
31105
  /**
32496
31106
  * Static verifier for the workflow IR. This is what `decompose`'s output is
@@ -32982,186 +31592,6 @@ function makeRunner(deps, ctx) {
32982
31592
  } };
32983
31593
  }
32984
31594
  //#endregion
32985
- //#region src/lib/orchestration/gate-immutability.ts
32986
- /** Patterns that weaken a gate regardless of language (test exclusivity / focus
32987
- * idioms shared across JS test runners; kept in COMMON since several languages
32988
- * reuse them). */
32989
- const COMMON_PATTERNS = [{
32990
- name: "skipped-test",
32991
- re: /(\.\s*skip\s*\(|\bxit\s*\(|\bxdescribe\s*\(|\.\s*only\s*\()/
32992
- }];
32993
- /** Language-specific weakening patterns, selected by the added line's file type. */
32994
- const LANG_PATTERNS = {
32995
- ts: [
32996
- {
32997
- name: "ts-suppression",
32998
- re: /@ts-(ignore|nocheck|expect-error)\b/
32999
- },
33000
- {
33001
- name: "any-cast",
33002
- re: /\bas\s+any\b|:\s*any\b/
33003
- },
33004
- {
33005
- name: "eslint-disable",
33006
- re: /eslint-disable\b/
33007
- }
33008
- ],
33009
- py: [
33010
- {
33011
- name: "py-type-ignore",
33012
- re: /#\s*type:\s*ignore\b/
33013
- },
33014
- {
33015
- name: "py-noqa",
33016
- re: /#\s*noqa\b/
33017
- },
33018
- {
33019
- name: "py-skip",
33020
- re: /@(pytest\.mark\.skip|unittest\.skip)\b/
33021
- }
33022
- ],
33023
- go: [{
33024
- name: "go-skip",
33025
- re: /\bt\.Skip\s*\(/
33026
- }, {
33027
- name: "go-nolint",
33028
- re: /\/\/\s*nolint\b/
33029
- }],
33030
- rust: [{
33031
- name: "rust-ignore",
33032
- re: /#\[\s*ignore\b/
33033
- }, {
33034
- name: "rust-allow",
33035
- re: /#\[\s*allow\s*\(/
33036
- }]
33037
- };
33038
- /** Map a file path to a language key for `LANG_PATTERNS` (null → COMMON only). */
33039
- function langForFile(file) {
33040
- if (!file) return null;
33041
- switch (file.slice(file.lastIndexOf(".")).toLowerCase()) {
33042
- case ".ts":
33043
- case ".tsx":
33044
- case ".mts":
33045
- case ".cts":
33046
- case ".js":
33047
- case ".jsx":
33048
- case ".mjs":
33049
- case ".cjs": return "ts";
33050
- case ".py":
33051
- case ".pyi": return "py";
33052
- case ".go": return "go";
33053
- case ".rs": return "rust";
33054
- default: return null;
33055
- }
33056
- }
33057
- /** Union of COMMON + every language — the pre-header default (no file known yet),
33058
- * so a header-less diff is matched permissively, exactly as before this became
33059
- * language-scoped. Once a real file header appears, the set narrows to that
33060
- * file's language (or COMMON only for an unknown extension → fail open). */
33061
- const ALL_PATTERNS = [...COMMON_PATTERNS, ...Object.values(LANG_PATTERNS).flat()];
33062
- /** A `diff --git a/x b/x` or `+++ b/x` header → the current file path. */
33063
- function fileFromHeader(line) {
33064
- const git = /^diff --git a\/.+ b\/(.+)$/.exec(line);
33065
- if (git) return git[1];
33066
- const plus = /^\+\+\+ b\/(.+)$/.exec(line);
33067
- if (plus) return plus[1];
33068
- }
33069
- function detectGateWeakening(diff) {
33070
- const findings = [];
33071
- let file;
33072
- let patterns = ALL_PATTERNS;
33073
- for (const raw of diff.split("\n")) {
33074
- const headerFile = fileFromHeader(raw);
33075
- if (headerFile !== void 0) {
33076
- file = headerFile;
33077
- const lang = langForFile(file);
33078
- patterns = lang ? [...COMMON_PATTERNS, ...LANG_PATTERNS[lang]] : COMMON_PATTERNS;
33079
- continue;
33080
- }
33081
- if (!raw.startsWith("+") || raw.startsWith("+++")) continue;
33082
- const added = raw.slice(1);
33083
- for (const p of patterns) if (p.re.test(added)) findings.push(file === void 0 ? {
33084
- pattern: p.name,
33085
- line: added.trim()
33086
- } : {
33087
- pattern: p.name,
33088
- line: added.trim(),
33089
- file
33090
- });
33091
- }
33092
- return {
33093
- weakened: findings.length > 0,
33094
- findings
33095
- };
33096
- }
33097
- //#endregion
33098
- //#region src/lib/orchestration/stop-gate.ts
33099
- /**
33100
- * The Phase-0 structural-gate decision — the cheap, CERTAIN floor win. It blocks
33101
- * marking work "done" when any canonical gate fails OR the diff weakens a gate.
33102
- * A non-model check that fires every run and never ships broken or gate-gamed
33103
- * code; per the floor analysis it is the only component that raises the
33104
- * worst-case floor BY CONSTRUCTION (the orchestration on top is a conditional
33105
- * bet).
33106
- *
33107
- * This is the logic behind the spawned-session Stop-hook AND the kernel runner's
33108
- * pre-accept check. The process exec and the diff are inputs, so the decision is
33109
- * pure + unit-testable; the hook script that runs `git diff` + invokes this is a
33110
- * thin wrapper.
33111
- */
33112
- async function evaluateStopGate(input) {
33113
- const gate = await runGateChecks(input.checks, input.cwd, input.exec);
33114
- const weak = detectGateWeakening(input.diff);
33115
- const failedChecks = input.checks.map((c) => c.id).filter((id) => !gate.passed.has(id));
33116
- const block = failedChecks.length > 0 || weak.weakened;
33117
- const parts = [];
33118
- if (failedChecks.length > 0) parts.push(`failing gates: ${failedChecks.join(", ")}`);
33119
- if (weak.weakened) {
33120
- const pats = [...new Set(weak.findings.map((f) => f.pattern))].join(", ");
33121
- parts.push(`gate-weakening in the diff: ${pats}`);
33122
- }
33123
- return {
33124
- block,
33125
- reason: block ? parts.join("; ") : "all canonical gates pass; no gate-weakening in the diff",
33126
- failedChecks,
33127
- weakening: weak.findings
33128
- };
33129
- }
33130
- //#endregion
33131
- //#region src/lib/orchestration/live-exec.ts
33132
- /**
33133
- * Live `ExecFn` for the gate runner — runs the kernel's SEALED gate command in a
33134
- * workspace via the project's Windows-safe exec helper (`runCommandCapture` →
33135
- * `buildExecInvocation`, which handles cmd.exe quoting on Windows). Only the
33136
- * kernel's sealed gate commands flow through here (tests/types/lint/build),
33137
- * never a producer-authored string, so a simple whitespace split into argv is
33138
- * safe for these fixed commands. A command that can't run (spawn error / killed)
33139
- * counts as a non-zero exit, never a throw — the gate runner treats it as
33140
- * not-passed.
33141
- *
33142
- * This is the one piece of the gate engine that touches a real subprocess; it is
33143
- * model-free and cross-platform, so it is exercised directly in CI (unlike the
33144
- * worker/worktree/model adapters, which need the gated E2E harness).
33145
- */
33146
- /** Per-command wall-clock cap so a hung gate command (watch-mode test, a process
33147
- * waiting on stdin, a stale lockfile) is tree-killed instead of hanging the
33148
- * caller forever. Generous (a real typecheck/test/lint can take minutes) but
33149
- * bounded; override with GH_ROUTER_GATE_CMD_TIMEOUT_MS. A timeout kills the
33150
- * command (code null) which the gate runner treats as not-passed. */
33151
- const CMD_TIMEOUT_MS = parseIntEnv(process.env.GH_ROUTER_GATE_CMD_TIMEOUT_MS) ?? 6e5;
33152
- const liveExec = async ({ command, cwd }) => {
33153
- const argv = command.trim().split(/\s+/).filter(Boolean);
33154
- if (argv.length === 0) return { exitCode: 1 };
33155
- try {
33156
- return { exitCode: (await runCommandCapture(argv, {
33157
- cwd,
33158
- timeoutMs: CMD_TIMEOUT_MS
33159
- })).code ?? 1 };
33160
- } catch {
33161
- return { exitCode: 1 };
33162
- }
33163
- };
33164
- //#endregion
33165
31595
  //#region src/lib/orchestration/runner-live.ts
33166
31596
  /**
33167
31597
  * The LIVE adapter for the kernel's NodeRunner primitives. buildLiveRunner wires
@@ -33264,665 +31694,6 @@ function buildLiveRunner(ctx, prim) {
33264
31694
  };
33265
31695
  }
33266
31696
  //#endregion
33267
- //#region src/lib/orchestration/stop-gate-policy.ts
33268
- /**
33269
- * Pure + thin-IO policy helpers for the consent-gated, top-level-only structural
33270
- * Stop-gate. Split out of `stop-gate-hook.ts` so the security-critical decisions
33271
- * (is this a subagent? is this repo trusted? which checks regressed?) are small,
33272
- * named, and unit-testable.
33273
- *
33274
- * Verified against the official Claude Code hooks reference
33275
- * (https://code.claude.com/docs/en/hooks), 2026-06:
33276
- * - `Stop` fires for the MAIN agent; subagents fire `SubagentStop`, and a
33277
- * `Stop`-registered hook is converted to `SubagentStop` in a subagent
33278
- * context — so the hook command CAN run for a subagent. The payload carries
33279
- * `agent_id` / `agent_type` ONLY inside a subagent/teammate context, so that
33280
- * is the reliable top-level-vs-sub discriminator (NOT an env var, which the
33281
- * proxy cannot set on Claude-spawned subagents).
33282
- * - `UserPromptSubmit` fires only for human prompts to the main agent.
33283
- * - `stop_hook_active` is NOT in the current documented payload, so the
33284
- * per-prompt block budget — not that flag — must be the termination guard.
33285
- */
33286
- /**
33287
- * True when the hook is firing inside a subagent / teammate context (NOT the
33288
- * top-level user session). Claude Code adds `agent_id` + `agent_type` to the
33289
- * payload only there, so their presence is the discriminator. The Stop-gate and
33290
- * the prompt-steer hook both stand down when this is true, scoping them to the
33291
- * top-level session.
33292
- */
33293
- function isSubagentContext(payload) {
33294
- const present = (v) => v !== void 0 && v !== null;
33295
- return present(payload?.agent_type) || present(payload?.agent_id);
33296
- }
33297
- /** Stable trust dir (NOT the per-launch mirror — trust must persist). */
33298
- function trustDir() {
33299
- return path.join(PATHS.APP_DIR, "stop-gate", "trust");
33300
- }
33301
- /** Resolve the git repo root for `cwd`, falling back to `cwd` when not a repo. */
33302
- async function repoRoot(cwd) {
33303
- const top = (await runCommandCapture([
33304
- "git",
33305
- "rev-parse",
33306
- "--show-toplevel"
33307
- ], {
33308
- cwd,
33309
- timeoutMs: 5e3
33310
- }).catch(() => void 0))?.stdout?.trim();
33311
- return top && top.length > 0 ? top : cwd;
33312
- }
33313
- function trustFileFor(root) {
33314
- const key = createHash("sha256").update(path.resolve(root)).digest("hex").slice(0, 32);
33315
- return path.join(trustDir(), key);
33316
- }
33317
- /**
33318
- * A stable identity for the repo at `root`: the first (root) commit SHA. It
33319
- * survives normal history growth but differs across distinct repositories, so a
33320
- * DIFFERENT repo later appearing at the same filesystem path is not silently
33321
- * trusted (codex review #2). Empty string when unavailable (no git / no commits)
33322
- * — trust then falls back to path-only, the best we can do.
33323
- */
33324
- async function repoFingerprint(root) {
33325
- return (await runCommandCapture([
33326
- "git",
33327
- "rev-list",
33328
- "--max-parents=0",
33329
- "HEAD"
33330
- ], {
33331
- cwd: root,
33332
- timeoutMs: 5e3
33333
- }).catch(() => void 0))?.stdout?.split(/\r?\n/).map((s) => s.trim()).filter(Boolean)[0] ?? "";
33334
- }
33335
- /**
33336
- * True iff the user has consented to run the gate in this repo AND the repo's
33337
- * identity still matches what was trusted. The trust file stores `root\nfp\n`;
33338
- * a present fingerprint is verified against the live one (deny on mismatch, and
33339
- * deny if we pinned one but can't recompute it — fail closed). A legacy file
33340
- * with no fingerprint is path-only trust.
33341
- */
33342
- async function isRepoTrusted(cwd) {
33343
- const root = await repoRoot(cwd);
33344
- let stored;
33345
- try {
33346
- stored = await promises.readFile(trustFileFor(root), "utf8");
33347
- } catch {
33348
- return false;
33349
- }
33350
- const storedFp = (stored.split(/\r?\n/)[1] ?? "").trim();
33351
- if (storedFp.length === 0) return true;
33352
- const currentFp = await repoFingerprint(root);
33353
- if (currentFp.length === 0) return false;
33354
- return currentFp === storedFp;
33355
- }
33356
- /** Record consent for this repo (consent once → automatic thereafter), pinning
33357
- * the repo's root-commit fingerprint so a later repo swap at the same path is
33358
- * not auto-trusted. */
33359
- async function trustRepo(cwd) {
33360
- const root = await repoRoot(cwd);
33361
- const fp = await repoFingerprint(root);
33362
- await promises.mkdir(trustDir(), { recursive: true });
33363
- await promises.writeFile(trustFileFor(root), `${root}\n${fp}\n`, { mode: 384 });
33364
- return root;
33365
- }
33366
- /**
33367
- * Repo-aware gate enable: `GH_ROUTER_DISABLE_STOP_GATE` force-off wins;
33368
- * `GH_ROUTER_ENABLE_STOP_GATE` force-on next; otherwise default to OFF unless the
33369
- * repo is trusted. This is the load-bearing security gate — the default is OFF,
33370
- * so an untrusted repo's scripts are never auto-run.
33371
- */
33372
- async function stopGateEnabledForRepo(cwd, env = process.env) {
33373
- if (parseBoolEnv(env.GH_ROUTER_DISABLE_STOP_GATE) === true) return false;
33374
- if (parseBoolEnv(env.GH_ROUTER_ENABLE_STOP_GATE) === true) return true;
33375
- return isRepoTrusted(cwd);
33376
- }
33377
- /**
33378
- * Given the current failed checks and the recorded baseline, return the checks
33379
- * that REGRESSED (failing now, not failing at baseline). A null baseline (first
33380
- * eval) yields an empty regression set — nothing is blamed on the agent yet.
33381
- */
33382
- function regressions(currentFailed, baseline) {
33383
- if (baseline === null) return [];
33384
- return currentFailed.filter((id) => !baseline.has(id));
33385
- }
33386
- /** File-backed `BaselineStore` under `stateDir`, keyed by sha256(session_id). */
33387
- function fileBaselineStore(stateDir) {
33388
- const fileFor = (sid) => path.join(stateDir, `baseline-${createHash("sha256").update(sid).digest("hex").slice(0, 32)}`);
33389
- return {
33390
- async get(sid) {
33391
- try {
33392
- const raw = await promises.readFile(fileFor(sid), "utf8");
33393
- const arr = JSON.parse(raw);
33394
- if (Array.isArray(arr)) return new Set(arr.filter((x) => typeof x === "string"));
33395
- return /* @__PURE__ */ new Set();
33396
- } catch {
33397
- return null;
33398
- }
33399
- },
33400
- async set(sid, failed) {
33401
- await promises.mkdir(stateDir, { recursive: true });
33402
- await promises.writeFile(fileFor(sid), JSON.stringify([...failed]), { mode: 384 });
33403
- }
33404
- };
33405
- }
33406
- function fileReviewDebounce(stateDir) {
33407
- const fileFor = (sid) => path.join(stateDir, `review-hash-${createHash("sha256").update(sid).digest("hex").slice(0, 32)}`);
33408
- const readLast = async (sid) => {
33409
- try {
33410
- return (await promises.readFile(fileFor(sid), "utf8")).trim();
33411
- } catch {
33412
- return "";
33413
- }
33414
- };
33415
- return {
33416
- async shouldReview(sid, diffHash) {
33417
- if (diffHash.length === 0) return false;
33418
- return await readLast(sid) !== diffHash;
33419
- },
33420
- async markReviewed(sid, diffHash) {
33421
- await promises.mkdir(stateDir, { recursive: true });
33422
- await promises.writeFile(fileFor(sid), diffHash, { mode: 384 });
33423
- }
33424
- };
33425
- }
33426
- function fileFindingsStore(stateDir) {
33427
- const fileFor = (sid) => path.join(stateDir, `findings-${createHash("sha256").update(sid).digest("hex").slice(0, 32)}`);
33428
- return {
33429
- async read(sid) {
33430
- try {
33431
- const raw = await promises.readFile(fileFor(sid), "utf8");
33432
- return raw.length > 0 ? raw : null;
33433
- } catch {
33434
- return null;
33435
- }
33436
- },
33437
- async write(sid, findings) {
33438
- await promises.mkdir(stateDir, { recursive: true });
33439
- const tmp = `${fileFor(sid)}.${process.pid}.tmp`;
33440
- await promises.writeFile(tmp, findings, { mode: 384 });
33441
- await promises.rename(tmp, fileFor(sid));
33442
- },
33443
- async clear(sid) {
33444
- await promises.unlink(fileFor(sid)).catch(() => {});
33445
- }
33446
- };
33447
- }
33448
- /**
33449
- * The single canonical state dir for the advisory-review layer (hook V2): the
33450
- * Stop hook's review debounce, the background review's findings file, and the
33451
- * UserPromptSubmit hook's last-user-prompt store all live here, keyed by
33452
- * sha256(session_id). One dir so the three independent subcommand processes
33453
- * (`internal-stop-hook`, `internal-stop-review`, `internal-prompt-submit`)
33454
- * agree on where to read/write without threading a path through env. Distinct
33455
- * from the deterministic gate's `gh-router-stopgate*` dirs (block budget +
33456
- * baseline) so the advisory layer can be wiped independently.
33457
- */
33458
- function stopReviewStateDir() {
33459
- return path.join(tmpdir(), "gh-router-stop-review");
33460
- }
33461
- function fileLastPromptStore(stateDir) {
33462
- const fileFor = (sid) => path.join(stateDir, `last-prompt-${createHash("sha256").update(sid).digest("hex").slice(0, 32)}`);
33463
- return {
33464
- async read(sid) {
33465
- try {
33466
- const raw = await promises.readFile(fileFor(sid), "utf8");
33467
- return raw.length > 0 ? raw : null;
33468
- } catch {
33469
- return null;
33470
- }
33471
- },
33472
- async write(sid, prompt) {
33473
- await promises.mkdir(stateDir, { recursive: true });
33474
- const tmp = `${fileFor(sid)}.${process.pid}.tmp`;
33475
- await promises.writeFile(tmp, prompt, { mode: 384 });
33476
- await promises.rename(tmp, fileFor(sid));
33477
- }
33478
- };
33479
- }
33480
- //#endregion
33481
- //#region src/lib/orchestration/stop-gate-hook.ts
33482
- /**
33483
- * The Phase-0 structural-gate Stop-hook glue. `runStopGateForLaunch` resolves a
33484
- * SEALED gate by id and evaluates it against the working-tree diff, returning the
33485
- * block decision a spawned-session Stop hook (or a manual check) acts on.
33486
- *
33487
- * Safety posture (deliberately different from the kernel): a Stop hook that
33488
- * blocks on a MISCONFIGURATION would wedge the user's session (they could not
33489
- * stop). So this fails OPEN on config problems (unknown gate id) and blocks ONLY
33490
- * on a genuine red gate or a gate-weakening diff. The kernel, by contrast, fails
33491
- * CLOSED to the baseline. Both are correct for their context.
33492
- *
33493
- * The sealed gate + diff + exec are inputs, so the decision is pure and
33494
- * unit-testable; the thin live wrapper (capture `git diff`, spawn the checks,
33495
- * map the result to an exit code) is the only part that needs a real process.
33496
- */
33497
- async function runStopGateForLaunch(input) {
33498
- const gate = resolveSealedGate(input.gateId);
33499
- if (!gate) return {
33500
- block: false,
33501
- reason: `stop-gate: unknown gateId "${input.gateId}" (not blocking)`,
33502
- failedChecks: [],
33503
- weakening: []
33504
- };
33505
- return evaluateStopGate({
33506
- checks: gate.checks,
33507
- cwd: input.workspace,
33508
- exec: input.exec,
33509
- diff: input.diff
33510
- });
33511
- }
33512
- /**
33513
- * The launch-stable baseline key for the dynamic gate path: keyed by the
33514
- * workdir + the descriptor's check set + a per-launch token, NOT the session id
33515
- * and NOT HEAD. The LAUNCHER generates the token (passed to the child via env),
33516
- * computes this key, and seeds the baseline (the set of checks already failing at
33517
- * launch, BEFORE the agent mutates the tree); the runtime hook recomputes the
33518
- * same key (reading the token from its env) and reads that baseline, so an
33519
- * agent-introduced failure is a REGRESSION rather than being silently adopted as
33520
- * the baseline at first stop. The per-launch token isolates concurrent sessions
33521
- * on the same repo (they don't clobber each other's baseline). HEAD is
33522
- * deliberately NOT in the key: an agent that COMMITS its work mid-session must
33523
- * still be judged against the pre-session baseline (a HEAD-keyed baseline would
33524
- * vanish on the commit — the exact moment it matters). A changed check set (the
33525
- * agent edited the harness) yields a different key → the launch baseline no
33526
- * longer matches and the hook degrades to first-stop baselining.
33527
- */
33528
- function launchBaselineKey(workdir, descriptorKey, token) {
33529
- return JSON.stringify([
33530
- "launch",
33531
- path.resolve(workdir),
33532
- descriptorKey,
33533
- token ?? ""
33534
- ]);
33535
- }
33536
- /**
33537
- * Run the resolved checks ONCE at launch (pre-mutation) and record the failing
33538
- * set as the baseline under `launchBaselineKey`. Best-effort: any failure is
33539
- * swallowed (the hook then degrades to first-stop baselining). The launcher MUST
33540
- * pass the SAME check set the hook will run (same `includeTests`) so a
33541
- * pre-existing test failure isn't later miscounted as a regression.
33542
- *
33543
- * NOTE: capture is best-effort pre-mutation — the launcher fires it before the
33544
- * child can edit, and in practice the agent does not mutate until well after a
33545
- * user prompt, so the checks see the clean tree. If the agent somehow mutates
33546
- * before this completes, the worst case is the legacy first-stop baseline (no
33547
- * regression, never a wedge).
33548
- */
33549
- async function captureLaunchBaseline(input) {
33550
- if (input.checks.length === 0) return;
33551
- try {
33552
- const gate = await runGateChecks(input.checks, input.workdir, input.exec);
33553
- const failed = input.checks.map((c) => c.id).filter((id) => !gate.passed.has(id));
33554
- await input.baseline.set(launchBaselineKey(input.workdir, input.descriptorKey, input.token), failed);
33555
- } catch {}
33556
- }
33557
- /**
33558
- * The advisory background review (hook V2) is ON by default whenever the Stop
33559
- * gate runs; it is the cross-lab accountability layer. Opt out with
33560
- * `GH_ROUTER_DISABLE_STOP_REVIEW=1` to keep the deterministic gate but drop the
33561
- * LLM review. (Disabling the whole gate with `GH_ROUTER_DISABLE_STOP_GATE=1`
33562
- * also drops the review, since the review only ever fires from the gate's green
33563
- * path.)
33564
- */
33565
- function stopReviewEnabled(env = process.env) {
33566
- return parseBoolEnv(env.GH_ROUTER_DISABLE_STOP_REVIEW) !== true;
33567
- }
33568
- /** The sealed gate the Stop hook runs, overridable via `GH_ROUTER_STOP_GATE_ID`
33569
- * (must be a registered sealed id; the live wrapper falls open on an unknown
33570
- * id). Defaults to `default-ci`. */
33571
- function stopGateId(env = process.env) {
33572
- const v = (env.GH_ROUTER_STOP_GATE_ID ?? "").trim();
33573
- return v.length > 0 ? v : "default-ci";
33574
- }
33575
- /**
33576
- * PLAN-MODE env fallback. When the session is in plan mode, plan/memory scratch
33577
- * edits should not trip the gate-WEAKENING scan (a plan/memory markdown file may
33578
- * legitimately quote `it.skip` / `as any` while describing work). The launcher
33579
- * sets `GH_ROUTER_STOP_GATE_PLAN_MODE` for a plan-mode session; the payload may
33580
- * ALSO carry a forward-compat `plan_mode` field (see `decideStopHook`). Either
33581
- * signal being present activates the scoping — the executable gate STILL runs
33582
- * and non-plan hunks are STILL scanned.
33583
- */
33584
- function stopGatePlanMode(env = process.env) {
33585
- return parseBoolEnv(env.GH_ROUTER_STOP_GATE_PLAN_MODE) === true;
33586
- }
33587
- /**
33588
- * Is a unified-diff file path confined to a `plans/` or `memory/` directory?
33589
- * Windows-safe (normalizes `\` → `/`). Matches only when a DIRECTORY segment of
33590
- * the path is exactly `plans` or `memory`, so `plans/x.md`, `memory/y.md`, and
33591
- * `a/plans/z` match while the filenames `docs/plans.md` / `myplans/x` do not.
33592
- */
33593
- function isPlanMemoryDiffPath(p) {
33594
- const segs = p.replace(/\\/g, "/").split("/");
33595
- for (let i = 0; i < segs.length - 1; i++) if (segs[i] === "plans" || segs[i] === "memory") return true;
33596
- return false;
33597
- }
33598
- /**
33599
- * Drop the file sections of a unified `git diff` whose NEW path (the `b/…` side)
33600
- * is confined to `plans/` or `memory/` — used for the gate-WEAKENING scan when
33601
- * plan mode is active. Sections for other files (e.g. `src/…`) are preserved
33602
- * verbatim, so a MIXED diff keeps its non-plan hunks and `src/` weakening is
33603
- * still caught. The executable gate does not read the diff, so filtering it here
33604
- * never affects which checks run. A diff with no `diff --git` sections (or an
33605
- * empty one) is returned unchanged.
33606
- */
33607
- function stripPlanMemoryDiffHunks(diff) {
33608
- if (diff.length === 0) return diff;
33609
- const out = [];
33610
- let dropping = false;
33611
- for (const line of diff.split("\n")) {
33612
- if (line.startsWith("diff --git ")) {
33613
- const bPath = /^diff --git a\/.+ b\/(.+)$/.exec(line)?.[1];
33614
- dropping = bPath !== void 0 && isPlanMemoryDiffPath(bPath);
33615
- if (!dropping) out.push(line);
33616
- continue;
33617
- }
33618
- if (!dropping) out.push(line);
33619
- }
33620
- return out.join("\n");
33621
- }
33622
- /**
33623
- * C3: whether the structural Stop-gate is disabled for THIS launch. True when the
33624
- * `GH_ROUTER_DISABLE_STOP_GATE` env is set OR the `--no-stop-gate` launcher flag was
33625
- * passed (`args["no-stop-gate"] === true`). A DRIVEN session sets the flag so a
33626
- * blocking Stop hook never hangs its turn-end (~10 min) waiting on the fleet driver.
33627
- * Pure so it is unit-testable without the live launch path.
33628
- */
33629
- function stopGateDisabled(args, env = process.env) {
33630
- return parseBoolEnv(env.GH_ROUTER_DISABLE_STOP_GATE) === true || args["no-stop-gate"] === true;
33631
- }
33632
- /** True when a settings `Stop` entry already registers `command` (so the merge
33633
- * is idempotent across re-launches). */
33634
- function entryHasCommand(entry, command) {
33635
- if (!entry || typeof entry !== "object") return false;
33636
- const hooks = entry.hooks;
33637
- if (!Array.isArray(hooks)) return false;
33638
- return hooks.some((h) => h && typeof h === "object" && h.command === command);
33639
- }
33640
- /**
33641
- * Idempotently merge a hook running `command` for `event` (default `Stop`) into
33642
- * an existing Claude Code settings object WITHOUT clobbering other hook events or
33643
- * other entries. Returns a new object (never mutates the input). Re-running the
33644
- * launcher with the same command+event does not duplicate the hook.
33645
- */
33646
- function mergeStopHookIntoSettings(existing, command, event = "Stop", timeoutSec, matcher) {
33647
- const base = existing && typeof existing === "object" ? { ...existing } : {};
33648
- const hooks = base.hooks && typeof base.hooks === "object" ? { ...base.hooks } : {};
33649
- const arr = Array.isArray(hooks[event]) ? [...hooks[event]] : [];
33650
- if (!arr.some((e) => entryHasCommand(e, command))) {
33651
- const hook = {
33652
- type: "command",
33653
- command
33654
- };
33655
- if (typeof timeoutSec === "number" && Number.isFinite(timeoutSec) && timeoutSec > 0) hook.timeout = timeoutSec;
33656
- arr.push(matcher ? {
33657
- matcher,
33658
- hooks: [hook]
33659
- } : { hooks: [hook] });
33660
- }
33661
- hooks[event] = arr;
33662
- base.hooks = hooks;
33663
- return base;
33664
- }
33665
- async function decideStopHook(input) {
33666
- const maxBlocks = input.maxBlocks ?? 2;
33667
- let payload = {};
33668
- let parsed = false;
33669
- try {
33670
- const p = JSON.parse(input.stdin);
33671
- if (p && typeof p === "object") {
33672
- payload = p;
33673
- parsed = true;
33674
- }
33675
- } catch {}
33676
- if (!parsed) return { exitCode: 0 };
33677
- if (isSubagentContext(payload)) return { exitCode: 0 };
33678
- const sessionId = typeof payload.session_id === "string" && payload.session_id.length > 0 ? payload.session_id : "";
33679
- if (!sessionId) return { exitCode: 0 };
33680
- const cwdRaw = typeof payload.cwd === "string" && payload.cwd.length > 0 ? payload.cwd : input.fallbackCwd;
33681
- let cwd = cwdRaw;
33682
- try {
33683
- cwd = await promises.realpath(cwdRaw);
33684
- } catch {}
33685
- let enabled;
33686
- try {
33687
- enabled = await input.isEnabledForRepo(cwd);
33688
- } catch {
33689
- return { exitCode: 0 };
33690
- }
33691
- if (!enabled) return { exitCode: 0 };
33692
- let priorBlocks;
33693
- try {
33694
- priorBlocks = await input.budget.count(sessionId);
33695
- } catch {
33696
- return { exitCode: 0 };
33697
- }
33698
- if (priorBlocks >= maxBlocks) return {
33699
- exitCode: 0,
33700
- stderr: `structural gate: reached the ${maxBlocks}-block limit for this prompt; allowing the stop WITHOUT re-running the checks. If the failures from the last block are unfixed, they are shipping — run the checks manually to verify.`
33701
- };
33702
- let resolvedKey = input.gateId;
33703
- let dynamicBaselineKey;
33704
- const planMode = input.planMode === true || payload.plan_mode === true;
33705
- const scanDiff = (diff) => planMode ? stripPlanMemoryDiffHunks(diff) : diff;
33706
- const capturedDiff = async (workdir) => {
33707
- try {
33708
- return {
33709
- diff: await input.captureDiff(workdir),
33710
- captured: true
33711
- };
33712
- } catch {
33713
- return {
33714
- diff: "",
33715
- captured: false
33716
- };
33717
- }
33718
- };
33719
- const runGate = async () => {
33720
- if (input.resolveChecks) {
33721
- const resolved = await input.resolveChecks(cwd).catch(() => null);
33722
- if (!resolved || resolved.checks.length === 0) return null;
33723
- resolvedKey = resolved.descriptorKey;
33724
- dynamicBaselineKey = resolved.baselineKey;
33725
- const workdir = resolved.workdir || cwd;
33726
- const { diff, captured } = await capturedDiff(workdir);
33727
- if (captured && diff.length === 0) return {
33728
- kind: "no-diff",
33729
- diff
33730
- };
33731
- const result = await evaluateStopGate({
33732
- checks: resolved.checks,
33733
- cwd: workdir,
33734
- exec: input.exec,
33735
- diff: captured ? scanDiff(diff) : ""
33736
- });
33737
- return {
33738
- kind: "evaluated",
33739
- failedChecks: [...result.failedChecks],
33740
- weakeningPatterns: [...new Set(result.weakening.map((w) => w.pattern))],
33741
- diff
33742
- };
33743
- }
33744
- const { diff, captured } = await capturedDiff(cwd);
33745
- if (captured && diff.length === 0) return {
33746
- kind: "no-diff",
33747
- diff
33748
- };
33749
- const result = await runStopGateForLaunch({
33750
- workspace: cwd,
33751
- gateId: input.gateId,
33752
- exec: input.exec,
33753
- diff: captured ? scanDiff(diff) : ""
33754
- });
33755
- return {
33756
- kind: "evaluated",
33757
- failedChecks: [...result.failedChecks],
33758
- weakeningPatterns: [...new Set(result.weakening.map((w) => w.pattern))],
33759
- diff
33760
- };
33761
- };
33762
- const timeoutMs = input.timeoutMs ?? 3e5;
33763
- let timer;
33764
- const raced = await Promise.race([runGate(), new Promise((resolve) => {
33765
- timer = setTimeout(() => resolve("timeout"), timeoutMs);
33766
- })]);
33767
- if (timer) clearTimeout(timer);
33768
- if (raced === "timeout") return { exitCode: 0 };
33769
- if (raced === null) return { exitCode: 0 };
33770
- if (raced.kind === "no-diff") return { exitCode: 0 };
33771
- const baselineKey = dynamicBaselineKey ?? JSON.stringify([
33772
- sessionId,
33773
- cwd,
33774
- resolvedKey
33775
- ]);
33776
- const recorded = await input.baseline.get(baselineKey).catch(() => null);
33777
- if (recorded === null) await input.baseline.set(baselineKey, raced.failedChecks).catch(() => {});
33778
- const regressed = regressions(raced.failedChecks, recorded);
33779
- const weakened = raced.weakeningPatterns.length > 0;
33780
- if (regressed.length === 0 && !weakened) {
33781
- await maybeSpawnReview(input, sessionId, cwd, raced.diff);
33782
- return { exitCode: 0 };
33783
- }
33784
- try {
33785
- await input.budget.record(sessionId);
33786
- } catch {
33787
- return { exitCode: 0 };
33788
- }
33789
- const parts = [];
33790
- if (regressed.length > 0) parts.push(`regressed gates: ${regressed.join(", ")}`);
33791
- if (weakened) parts.push(`gate-weakening in the diff: ${raced.weakeningPatterns.join(", ")}`);
33792
- return {
33793
- exitCode: 2,
33794
- stderr: `structural gate failed (block ${priorBlocks + 1}/${maxBlocks}): ${parts.join("; ")}. Fix the failing checks and revert any gate-weakening (no new .skip / as any / lint-disable) before finishing.`
33795
- };
33796
- }
33797
- /**
33798
- * The advisory-review side-effect on a GREEN stop: debounce by diff hash, then
33799
- * fire the detached background reviewer. ADVISORY-ONLY — it returns void, never
33800
- * throws (every step is swallowed), and the caller does not await its result for
33801
- * the exit decision. A no-op when the review layer isn't wired (no debounce /
33802
- * spawn injected, e.g. GH_ROUTER_DISABLE_STOP_REVIEW) or the diff is empty.
33803
- *
33804
- * `markReviewed` runs BEFORE the spawn so a crashing spawn still records the
33805
- * debounce (an identical tree won't re-trigger on the next stop). The review is
33806
- * gated on the diff CHANGING since the last review — without it, every stop of
33807
- * an unchanged tree would re-spend a background gpt-5.6-sol review.
33808
- *
33809
- * The whole body is bounded by a short timeout (the stores are local temp files
33810
- * that complete in well under a millisecond in practice, so the timeout never
33811
- * fires normally — but if the debounce read/write ever stalled, the stop must
33812
- * still proceed promptly; the advisory layer never delays a clean stop).
33813
- */
33814
- const REVIEW_SIDE_EFFECT_BUDGET_MS = 2e3;
33815
- async function maybeSpawnReview(input, sessionId, cwd, diff) {
33816
- if (!input.reviewDebounce || !input.spawnReview) return;
33817
- if (diff.trim().length === 0) return;
33818
- let timer;
33819
- try {
33820
- const work = (async () => {
33821
- const diffHash = createHash("sha256").update(diff).digest("hex");
33822
- if (!await input.reviewDebounce.shouldReview(sessionId, diffHash)) return;
33823
- await input.reviewDebounce.markReviewed(sessionId, diffHash);
33824
- input.spawnReview({
33825
- sessionId,
33826
- cwd,
33827
- diff,
33828
- diffHash
33829
- });
33830
- })();
33831
- await Promise.race([work, new Promise((resolve) => {
33832
- timer = setTimeout(resolve, REVIEW_SIDE_EFFECT_BUDGET_MS);
33833
- })]);
33834
- } catch {} finally {
33835
- if (timer) clearTimeout(timer);
33836
- }
33837
- }
33838
- /**
33839
- * A file-backed `BlockBudget` under `stateDir`, keyed by a hash of the session id
33840
- * (so a session id is never written verbatim to a predictable path). Best-effort:
33841
- * a read miss counts as 0; `record` increments. A write/read error propagates so
33842
- * `decideStopHook` stands down (it can't guarantee termination without the
33843
- * budget).
33844
- */
33845
- function fileBlockBudget(stateDir) {
33846
- const fileFor = (sid) => path.join(stateDir, `block-${createHash("sha256").update(sid).digest("hex").slice(0, 32)}`);
33847
- const readCount = async (sid) => {
33848
- try {
33849
- const raw = await promises.readFile(fileFor(sid), "utf8");
33850
- const n = Number.parseInt(raw.trim(), 10);
33851
- return Number.isFinite(n) && n > 0 ? n : 0;
33852
- } catch {
33853
- return 0;
33854
- }
33855
- };
33856
- return {
33857
- count: readCount,
33858
- async record(sid) {
33859
- const next = await readCount(sid) + 1;
33860
- await promises.mkdir(stateDir, { recursive: true });
33861
- await promises.writeFile(fileFor(sid), String(next), { mode: 384 });
33862
- },
33863
- async reset(sid) {
33864
- await promises.unlink(fileFor(sid)).catch(() => {});
33865
- }
33866
- };
33867
- }
33868
- /**
33869
- * Build the shell command string Claude Code runs for the Stop hook. Invokes the
33870
- * running github-router via its node/bun binary so it works regardless of PATH.
33871
- * Pure (takes the binary + script paths) so the quoting is unit-testable; the
33872
- * cross-platform firing is verified by the gated E2E.
33873
- */
33874
- function buildStopHookCommand(execPath, scriptPath) {
33875
- const q = (s) => `"${s}"`;
33876
- if (scriptPath && scriptPath !== execPath) return `${q(execPath)} ${q(scriptPath)} internal-stop-hook`;
33877
- return `${q(execPath)} internal-stop-hook`;
33878
- }
33879
- /**
33880
- * Build the shell command Claude Code runs for the SessionStart/SessionEnd hooks
33881
- * (registered only when github-router runs inside an ai-or-die Terminal tab). The
33882
- * sidecar path is baked in as a literal `--out` arg — NOT passed via env — so it
33883
- * survives `AIORDIE_CLAUDE_BIND` being stripped from the child's environment and a
33884
- * nested `github-router claude` can't inherit it. Pure (binary + script + out
33885
- * paths) for unit-testable quoting; the live firing is verified end-to-end.
33886
- */
33887
- function buildSessionBindHookCommand(execPath, scriptPath, outPath) {
33888
- const q = (s) => `"${s}"`;
33889
- return `${scriptPath && scriptPath !== execPath ? `${q(execPath)} ${q(scriptPath)}` : q(execPath)} internal-session-bind --out ${q(outPath)}`;
33890
- }
33891
- /** Command for the `internal-artifact-open` hook (no args — token comes from the
33892
- * mirror creds file, plan from the plans dir; nothing secret in argv). */
33893
- function buildArtifactOpenHookCommand(execPath, scriptPath) {
33894
- const q = (s) => `"${s}"`;
33895
- return `${scriptPath && scriptPath !== execPath ? `${q(execPath)} ${q(scriptPath)}` : q(execPath)} internal-artifact-open`;
33896
- }
33897
- /**
33898
- * Read-merge-atomic-write the Stop hook into a Claude Code `settings.json` file
33899
- * (the mirrored one). A MISSING file (ENOENT) starts from `{}`; any OTHER read or
33900
- * parse error THROWS (the caller's try/catch warns and continues) rather than
33901
- * overwriting a file we couldn't understand with our defaults. Preserves every
33902
- * other setting, is idempotent, and uses temp+rename so Claude Code's mtime
33903
- * watcher never sees a half-written file. Returns the merged object.
33904
- */
33905
- async function injectStopHookIntoSettingsFile(settingsPath, command, event = "Stop", timeoutSec, matcher) {
33906
- let existing = {};
33907
- let raw;
33908
- try {
33909
- raw = await promises.readFile(settingsPath, "utf8");
33910
- } catch (err) {
33911
- if (err.code !== "ENOENT") throw err;
33912
- raw = void 0;
33913
- }
33914
- if (raw !== void 0) {
33915
- const parsed = JSON.parse(raw);
33916
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) existing = parsed;
33917
- else throw new Error(`settings.json at ${settingsPath} is not a JSON object; refusing to overwrite`);
33918
- }
33919
- const merged = mergeStopHookIntoSettings(existing, command, event, timeoutSec, matcher);
33920
- const tmp = `${settingsPath}.${process.pid}.tmp`;
33921
- await promises.writeFile(tmp, `${JSON.stringify(merged, null, 2)}\n`, { mode: 384 });
33922
- await promises.rename(tmp, settingsPath);
33923
- return merged;
33924
- }
33925
- //#endregion
33926
31697
  //#region src/lib/orchestration/attest.ts
33927
31698
  function isNonEmptyString(v) {
33928
31699
  return typeof v === "string" && v.length > 0;
@@ -35921,6 +33692,6 @@ function enumerateInjectedMcpToolNames(groupKeys, opts = {}) {
35921
33692
  return [...new Set(names)];
35922
33693
  }
35923
33694
  //#endregion
35924
- export { searchWeb as $, DEFAULT_CLAUDE_MODEL_FALLBACKS as $t, trustRepo as A, state as An, getTokenCount as At, TEST_DEFAULT_MODEL as B, hasSupportedBrowserInstalled as Bt, fileLastPromptStore as C, fetchWithTransientRetry as Cn, scoutModel as Ct, repoRoot as D, copilotBaseUrl as Dn, shimDefaultsToXhigh as Dt, repoFingerprint as E, GITHUB_API_BASE_URL as En, workerToolsEnabled as Et, EXPLORE_DEFAULT_MODEL as F, createChatCompletions as Ft, buildEnv as G, CONDENSED_OPERATING_SEQUENCE as Gt, resolveModeDefaults as H, extractTarGzMember as Ht, EXPLORE_DEFAULT_THINKING as I, MAX_RESPONSE_BODY_BYTES as It, toolbeltEnabled as J, ArtifactClient as Jt, availableToolCommands as K, DEFINITION_OF_GREATNESS as Kt, IMPLEMENT_DEFAULT_MODEL as L, readResponseBodyCapped as Lt, resolveSealedGate as M, resolveMcpToolTimeoutMs as Mt, BROWSE_DEFAULT_MODEL as N, pickEndpoint as Nt, stopGateEnabledForRepo as O, copilotHeaders as On, countTokens as Ot, DEFAULT_MODEL as P, createResponses as Pt, assetFor as Q, toolbeltPathOverride as Qt, PLAN_DEFAULT_MODEL as R, parseJsonOrDiagnose as Rt, fileFindingsStore as S, getGitHubUser as Sn, reviewerModel as St, isSubagentContext as T, forwardError as Tn, standInToolEnabled as Tt, resolveWorkerRunOpts as U, extractZipMember as Ut, appendPlanReminder as V, provisionAndIndexColbert as Vt, runWorkerAgent as W, warmTreeSitterPool as Wt, vscodeRipgrepPath as X, buildWorkspaceHeaderJson as Xt, toolbeltSkipSet as Y, buildWorkspaceHeaderHelperCommand as Yt, TOOLBELT_TOOLS$1 as Z, collapsePathKeys as Zt, stopGateDisabled as _, isNullish as _n, browserCompoundToolsEnabled as _t, buildPeerAwarenessSnippet as a, generateRandomPort as an, buildAnthropicErrorEvent as at, stopReviewEnabled as b, sleep$3 as bn, geminiAvailable as bt, personasFor as c, withInstallLock as cn, logStreamError as ct, buildStopHookCommand as d, setupGitHubToken as dn, handleMcpDelete as dt, DEFAULT_CODEX_MODEL as en, ADVISOR_INTERNAL_TOOL_NAME as et, captureLaunchBaseline as f, tryRefreshAndRetry as fn, handleMcpPost as ft, launchBaselineKey as g, filterBetaHeader as gn, browseAgentEnabled as gt, injectStopHookIntoSettingsFile as h, cacheVSCodeVersion as hn, brainstormModel as ht, buildAgentPrompt as i, UPSTREAM_INACTIVITY_TIMEOUT_MS as in, isAdvisorRequested as it, liveExec as j, assembleResponsesPayload as jt, stopReviewStateDir as k, githubHeaders as kn, createMessages as kt, buildArtifactOpenHookCommand as l, setupCopilotToken as ln, readIteratorWithTimeout as lt, fileBlockBudget as m, cacheModels as mn, artifactToolsEnabled as mt, MCP_GROUPS as n, DEFAULT_PORT as nn, buildAdvisorStream as nt, buildPeerAwarenessSummary as o, pickClaudeDefault as on, buildOpenAIErrorEvent as ot, decideStopHook as p, cacheCopilotVersion as pn, agentToolsEnabled as pt, buildToolbeltAwareness as q, shouldUseInsecureTls as qt, assertMcpToolSurfaceConsistent as r, UPSTREAM_FETCH_TIMEOUT_MS as rn, injectAdvisorTool as rt, enumerateInjectedMcpToolNames as s, getPackageVersion as sn, isControllerClosedError as st, GROUP_META as t, DEFAULT_CODEX_MODEL_FALLBACKS as tn, ADVISOR_TOOL_INSTRUCTIONS as tt, buildSessionBindHookCommand as u, setupGitHubAgentToken as un, relayAnthropicStream as ut, stopGateId as v, resolveCodexModel as vn, browserToolsEnabled as vt, fileReviewDebounce as w, HTTPError as wn, scribeModel as wt, fileBaselineStore as x, getModels as xn, nativeSubagentModel as xt, stopGatePlanMode as y, resolveModel as yn, fleetToolsEnabled as yt, REVIEW_DEFAULT_MODEL as z, provisionBrowserAssets as zt };
33695
+ export { scoutModel as $, ADVISOR_INTERNAL_TOOL_NAME as A, UPSTREAM_FETCH_TIMEOUT_MS as At, relayAnthropicStream as B, buildToolbeltAwareness as C, shouldUseInsecureTls as Ct, TOOLBELT_TOOLS$1 as D, DEFAULT_CODEX_MODEL as Dt, vscodeRipgrepPath as E, DEFAULT_CLAUDE_MODEL_FALLBACKS as Et, buildAnthropicErrorEvent as F, brainstormModel as G, handleMcpPost as H, buildOpenAIErrorEvent as I, browserToolsEnabled as J, browseAgentEnabled as K, isControllerClosedError as L, buildAdvisorStream as M, generateRandomPort as Mt, injectAdvisorTool as N, pickClaudeDefault as Nt, assetFor as O, DEFAULT_CODEX_MODEL_FALLBACKS as Ot, isAdvisorRequested as P, withInstallLock as Pt, reviewerModel as Q, logStreamError as R, availableToolCommands as S, DEFINITION_OF_GREATNESS as St, toolbeltSkipSet as T, toolbeltPathOverride as Tt, agentToolsEnabled as U, handleMcpDelete as V, artifactToolsEnabled as W, geminiAvailable as X, fleetToolsEnabled as Y, nativeSubagentModel as Z, appendPlanReminder as _, provisionAndIndexColbert as _t, buildPeerAwarenessSnippet as a, createMessages as at, runWorkerAgent as b, warmTreeSitterPool as bt, personasFor as c, resolveMcpToolTimeoutMs as ct, EXPLORE_DEFAULT_MODEL as d, createChatCompletions as dt, scribeModel as et, EXPLORE_DEFAULT_THINKING as f, MAX_RESPONSE_BODY_BYTES as ft, TEST_DEFAULT_MODEL as g, hasSupportedBrowserInstalled as gt, REVIEW_DEFAULT_MODEL as h, provisionBrowserAssets as ht, buildAgentPrompt as i, countTokens as it, ADVISOR_TOOL_INSTRUCTIONS as j, UPSTREAM_INACTIVITY_TIMEOUT_MS as jt, searchWeb as k, DEFAULT_PORT as kt, BROWSE_DEFAULT_MODEL as l, pickEndpoint as lt, PLAN_DEFAULT_MODEL as m, parseJsonOrDiagnose as mt, MCP_GROUPS as n, workerToolsEnabled as nt, buildPeerAwarenessSummary as o, getTokenCount as ot, IMPLEMENT_DEFAULT_MODEL as p, readResponseBodyCapped as pt, browserCompoundToolsEnabled as q, assertMcpToolSurfaceConsistent as r, shimDefaultsToXhigh as rt, enumerateInjectedMcpToolNames as s, assembleResponsesPayload as st, GROUP_META as t, standInToolEnabled as tt, DEFAULT_MODEL as u, createResponses as ut, resolveModeDefaults as v, extractTarGzMember as vt, toolbeltEnabled as w, collapsePathKeys as wt, buildEnv as x, CONDENSED_OPERATING_SEQUENCE as xt, resolveWorkerRunOpts as y, extractZipMember as yt, readIteratorWithTimeout as z };
35925
33696
 
35926
- //# sourceMappingURL=peer-mcp-personas-BvommiSI.js.map
33697
+ //# sourceMappingURL=peer-mcp-personas-BDet588n.js.map