github-router 0.3.137 → 0.3.150

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
- import { t as PATHS } from "./paths-Cn5OzmYL.js";
2
- import { d as runCommandCapture, l as parseBoolEnv, n as isPidAlive, o as trackChild, p as runManagedExeCapture, r as registerColbertExitHandlers, t as getColbertInstanceUuid, u as resolveExecutable } from "./lifecycle-Cqe8OQVX.js";
3
- import { i as registerExitHandlers, n as getInstanceUuid, r as recordWorkerRepo, t as WorktreeRegistry } from "./lifecycle-CeVDX6av.js";
1
+ import { t as PATHS } from "./paths-D0tJ_tms.js";
2
+ import { d as runCommandCapture, l as parseBoolEnv, n as isPidAlive, o as trackChild, p as runManagedExeCapture, r as registerColbertExitHandlers, t as getColbertInstanceUuid, u as resolveExecutable } from "./lifecycle-DyEXZu2z.js";
3
+ import { i as registerExitHandlers, n as getInstanceUuid, r as recordWorkerRepo, t as WorktreeRegistry } from "./lifecycle-DGvk4z63.js";
4
4
  import { createRequire } from "node:module";
5
5
  import consola from "consola";
6
6
  import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
@@ -13,7 +13,7 @@ import process$1 from "node:process";
13
13
  import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
14
14
  import { chmodSync, closeSync, cpSync, existsSync, mkdirSync, openSync, promises, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
15
15
  import { fileURLToPath } from "node:url";
16
- import { Agent } from "undici";
16
+ import { Agent, ProxyAgent } from "undici";
17
17
  import { performance } from "node:perf_hooks";
18
18
  import { createInterface } from "node:readline";
19
19
  import Parser from "web-tree-sitter";
@@ -42,6 +42,7 @@ const state = {
42
42
  extendedBetas: false,
43
43
  browseEnabled: false,
44
44
  fleetEnabled: false,
45
+ agentsEnabled: false,
45
46
  powerBrowseEnabled: false,
46
47
  humanlikeForce: "auto",
47
48
  sessionId: randomUUID(),
@@ -93,6 +94,24 @@ const githubHeaders = (state$1) => ({
93
94
  const GITHUB_BASE_URL = "https://github.com";
94
95
  const GITHUB_CLIENT_ID = "Iv1.b507a08c87ecfe98";
95
96
  const GITHUB_APP_SCOPES = ["read:user"].join(" ");
97
+ const GITHUB_AGENT_CLIENT_ID = "178c6fc778ccc68e1d6a";
98
+ const GITHUB_AGENT_SCOPES = [
99
+ "repo",
100
+ "workflow",
101
+ "read:org"
102
+ ].join(" ");
103
+ const GITHUB_GRAPHQL_URL = process.env.GITHUB_GRAPHQL_URL ?? "https://api.github.com/graphql";
104
+ const GITHUB_REST_API_VERSION = "2022-11-28";
105
+ const githubAgentHeaders = (state$1) => ({
106
+ ...standardHeaders(),
107
+ authorization: `token ${state$1.githubAgentToken}`,
108
+ "x-github-api-version": GITHUB_REST_API_VERSION,
109
+ "user-agent": "github-router-first-mate"
110
+ });
111
+ const githubAgentGraphQLHeaders = (state$1, features) => ({
112
+ ...githubAgentHeaders(state$1),
113
+ ...features ? { "GraphQL-Features": features } : {}
114
+ });
96
115
 
97
116
  //#endregion
98
117
  //#region src/lib/error.ts
@@ -376,6 +395,11 @@ const COPILOT_HOST_ALLOWLIST = [
376
395
  "api.business.githubcopilot.com",
377
396
  "api.enterprise.githubcopilot.com"
378
397
  ];
398
+ /**
399
+ * True iff `rawUrl` is an HTTPS URL whose host is a trusted Copilot API host.
400
+ * Exported so every path that sends a long-lived token to a discovered
401
+ * Copilot host (web search, first-mate CAPI session logs) shares one allowlist.
402
+ */
379
403
  function isAllowedCopilotHost(rawUrl) {
380
404
  let parsed;
381
405
  try {
@@ -397,13 +421,17 @@ const getCopilotToken = async () => {
397
421
 
398
422
  //#endregion
399
423
  //#region src/services/github/get-device-code.ts
400
- async function getDeviceCode() {
424
+ const DEFAULT_DEVICE_APP = {
425
+ clientId: GITHUB_CLIENT_ID,
426
+ scope: GITHUB_APP_SCOPES
427
+ };
428
+ async function getDeviceCode(app = DEFAULT_DEVICE_APP) {
401
429
  const response = await fetchWithTransientRetry(() => fetch(`${GITHUB_BASE_URL}/login/device/code`, {
402
430
  method: "POST",
403
431
  headers: standardHeaders(),
404
432
  body: JSON.stringify({
405
- client_id: GITHUB_CLIENT_ID,
406
- scope: GITHUB_APP_SCOPES
433
+ client_id: app.clientId,
434
+ scope: app.scope
407
435
  })
408
436
  }), { label: "/login/device/code" });
409
437
  if (!response.ok) throw new HTTPError("Failed to get device code", response);
@@ -693,7 +721,7 @@ const cacheCopilotVersion = async () => {
693
721
 
694
722
  //#endregion
695
723
  //#region src/services/github/poll-access-token.ts
696
- async function pollAccessToken(deviceCode) {
724
+ async function pollAccessToken(deviceCode, clientId = GITHUB_CLIENT_ID) {
697
725
  const sleepDuration = (deviceCode.interval + 1) * 1e3;
698
726
  consola.debug(`Polling access token with interval of ${sleepDuration}ms`);
699
727
  const expiresAt = Date.now() + deviceCode.expires_in * 1e3;
@@ -702,7 +730,7 @@ async function pollAccessToken(deviceCode) {
702
730
  method: "POST",
703
731
  headers: standardHeaders(),
704
732
  body: JSON.stringify({
705
- client_id: GITHUB_CLIENT_ID,
733
+ client_id: clientId,
706
734
  device_code: deviceCode.device_code,
707
735
  grant_type: "urn:ietf:params:oauth:grant-type:device_code"
708
736
  })
@@ -727,6 +755,8 @@ async function pollAccessToken(deviceCode) {
727
755
  //#region src/lib/token.ts
728
756
  const readGithubToken = () => fs.readFile(PATHS.GITHUB_TOKEN_PATH, "utf8");
729
757
  const writeGithubToken = (token) => fs.writeFile(PATHS.GITHUB_TOKEN_PATH, token);
758
+ const readGithubAgentToken = () => fs.readFile(PATHS.GITHUB_AGENT_TOKEN_PATH, "utf8");
759
+ const writeGithubAgentToken = (token) => fs.writeFile(PATHS.GITHUB_AGENT_TOKEN_PATH, token, { mode: 384 });
730
760
  const setupCopilotToken = async () => {
731
761
  const { token, refresh_in } = await getCopilotToken();
732
762
  state.copilotToken = token;
@@ -820,6 +850,71 @@ async function logUser() {
820
850
  const user = await getGitHubUser();
821
851
  consola.info(`Logged in as ${user.login}`);
822
852
  }
853
+ /**
854
+ * Set up the SECOND, write-capable GitHub token used by the first-mate
855
+ * agent-orchestration surface (`--agents`). Mirrors `setupGitHubToken`
856
+ * but authenticates against the GitHub CLI's OAuth client
857
+ * (`GITHUB_AGENT_CLIENT_ID`) requesting `repo workflow read:org`, and
858
+ * stores the result apart at `PATHS.GITHUB_AGENT_TOKEN_PATH`. The Copilot
859
+ * App token (`state.githubToken`) is left completely untouched — this is
860
+ * a distinct identity for a distinct capability.
861
+ *
862
+ * Long-lived (device-flow user token) → no refresh loop; a later 401 is
863
+ * surfaced to the caller as a revoked grant to re-run the login. Called
864
+ * once from `setupAndServe` when `state.agentsEnabled` is true.
865
+ */
866
+ async function setupGitHubAgentToken(options) {
867
+ try {
868
+ const existing = (await readGithubAgentToken().catch(() => "")).trim();
869
+ if (existing && !options?.force) {
870
+ state.githubAgentToken = existing;
871
+ if (state.showToken) consola.info("GitHub agent token:", existing);
872
+ await warnIfAgentScopesInsufficient();
873
+ return;
874
+ }
875
+ consola.info("Agent mode (--agents): a second GitHub login is required for a write-capable token (repo, workflow, read:org).");
876
+ const response = await getDeviceCode({
877
+ clientId: GITHUB_AGENT_CLIENT_ID,
878
+ scope: GITHUB_AGENT_SCOPES
879
+ });
880
+ consola.debug("Agent device code response:", response);
881
+ 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.`);
882
+ const token = await pollAccessToken(response, GITHUB_AGENT_CLIENT_ID);
883
+ await writeGithubAgentToken(token);
884
+ state.githubAgentToken = token;
885
+ if (state.showToken) consola.info("GitHub agent token:", token);
886
+ await warnIfAgentScopesInsufficient();
887
+ } catch (error) {
888
+ if (error instanceof HTTPError) {
889
+ consola.error("Failed to get GitHub agent token:", await error.response.json());
890
+ throw error;
891
+ }
892
+ consola.error("Failed to get GitHub agent token:", error);
893
+ throw error;
894
+ }
895
+ }
896
+ /**
897
+ * Best-effort check that the agent token actually carries the scopes we
898
+ * asked for. The GitHub CLI OAuth client is a classic OAuth App, so the
899
+ * granted scopes are echoed in the `x-oauth-scopes` response header on
900
+ * any authenticated call. Warn loudly (not fatal) if `repo`/`workflow`
901
+ * are missing so the failure is diagnosable at login rather than at the
902
+ * first write 403.
903
+ */
904
+ async function warnIfAgentScopesInsufficient() {
905
+ try {
906
+ const res = await fetch(`${GITHUB_API_BASE_URL}/user`, { headers: githubAgentHeaders(state) });
907
+ if (res.status === 401) {
908
+ consola.warn("GitHub agent token was rejected (401) — the grant may have been revoked. Re-run with --agents to log in again.");
909
+ return;
910
+ }
911
+ const scopes = (res.headers.get("x-oauth-scopes") ?? "").split(",").map((s) => s.trim()).filter(Boolean);
912
+ const missing = ["repo", "workflow"].filter((s) => !scopes.includes(s));
913
+ 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.`);
914
+ } catch (err) {
915
+ consola.debug("Agent token scope check skipped:", err);
916
+ }
917
+ }
823
918
 
824
919
  //#endregion
825
920
  //#region src/lib/update-lock.ts
@@ -1049,7 +1144,7 @@ function collapsePathKeys(env) {
1049
1144
 
1050
1145
  //#endregion
1051
1146
  //#region src/lib/insecure-tls.ts
1052
- const IS_BUN = typeof globalThis.Bun !== "undefined";
1147
+ const IS_BUN$1 = typeof globalThis.Bun !== "undefined";
1053
1148
  let sharedInsecureDispatcher;
1054
1149
  function insecureDispatcher() {
1055
1150
  return sharedInsecureDispatcher ??= new Agent({ connect: { rejectUnauthorized: false } });
@@ -1060,7 +1155,7 @@ function insecureDispatcher() {
1060
1155
  * `dispatcher`. Exported so BOTH runtime branches are unit-testable under one
1061
1156
  * interpreter (the untested Node branch is exactly what shipped broken).
1062
1157
  */
1063
- function applyInsecureTls(init, isBun = IS_BUN) {
1158
+ function applyInsecureTls(init, isBun = IS_BUN$1) {
1064
1159
  if (isBun) init.tls = { rejectUnauthorized: false };
1065
1160
  else init.dispatcher = insecureDispatcher();
1066
1161
  }
@@ -1282,40 +1377,40 @@ function tool(toolNameHttp, description, inputSchema, handler) {
1282
1377
  try {
1283
1378
  return await handler(args, signal);
1284
1379
  } catch (err) {
1285
- return errorResult$1(err);
1380
+ return errorResult$2(err);
1286
1381
  }
1287
1382
  }
1288
1383
  };
1289
1384
  }
1290
1385
  const ARTIFACT_TOOLS = Object.freeze([
1291
- tool("artifact_open", "Open a workspace file in ai-or-die's Artifact review panel for human review. Only works inside an ai-or-die tab-backed Claude session.", objectSchema$1({ file: stringProp$1("Workspace-relative or absolute file path to show in the Artifact panel.") }, ["file"]), async (args, signal) => {
1386
+ tool("artifact_open", "Open a workspace file in ai-or-die's Artifact review panel for human review. Only works inside an ai-or-die tab-backed Claude session.", objectSchema$2({ file: stringProp$2("Workspace-relative or absolute file path to show in the Artifact panel.") }, ["file"]), async (args, signal) => {
1292
1387
  const env = readArtifactEnv();
1293
1388
  if (!env) return missingEnvResult();
1294
- const file = requiredString$1(args, "file");
1295
- return ok$1({
1389
+ const file = requiredString$2(args, "file");
1390
+ return ok$2({
1296
1391
  viewUrl: (await clientFromEnv(env).open(file, signal)).viewUrl,
1297
1392
  next_step: "Tell the user to review at the Artifact panel, then call artifact_poll."
1298
1393
  });
1299
1394
  }),
1300
- tool("artifact_poll", "Wait for human Artifact review feedback from ai-or-die and return the prompts/layout warnings/DOM snapshot for the agent to act on. Only works inside an ai-or-die tab-backed Claude session.", objectSchema$1({}, []), async (_args, signal) => {
1395
+ tool("artifact_poll", "Wait for human Artifact review feedback from ai-or-die and return the prompts/layout warnings/DOM snapshot for the agent to act on. Only works inside an ai-or-die tab-backed Claude session.", objectSchema$2({}, []), async (_args, signal) => {
1301
1396
  const env = readArtifactEnv();
1302
1397
  if (!env) return missingEnvResult();
1303
- return ok$1(formatPollResponse(await pollUntilReady(clientFromEnv(env), signal)));
1398
+ return ok$2(formatPollResponse(await pollUntilReady(clientFromEnv(env), signal)));
1304
1399
  }),
1305
- tool("artifact_reply", "Send the agent's reply back to the ai-or-die Artifact review panel after applying or responding to human feedback. Only works inside an ai-or-die tab-backed Claude session.", objectSchema$1({ text: stringProp$1("Agent reply text to deliver to the human Artifact review panel.") }, ["text"]), async (args, signal) => {
1400
+ tool("artifact_reply", "Send the agent's reply back to the ai-or-die Artifact review panel after applying or responding to human feedback. Only works inside an ai-or-die tab-backed Claude session.", objectSchema$2({ text: stringProp$2("Agent reply text to deliver to the human Artifact review panel.") }, ["text"]), async (args, signal) => {
1306
1401
  const env = readArtifactEnv();
1307
1402
  if (!env) return missingEnvResult();
1308
- const text = requiredString$1(args, "text");
1309
- return ok$1({
1403
+ const text = requiredString$2(args, "text");
1404
+ return ok$2({
1310
1405
  ok: true,
1311
1406
  ...await clientFromEnv(env).agentReply(text, signal),
1312
1407
  next_step: "Wait for further human review, or continue if the review loop is complete."
1313
1408
  });
1314
1409
  }),
1315
- tool("artifact_end", "End/close the ai-or-die Artifact review panel when the review loop is complete. Only works inside an ai-or-die tab-backed Claude session.", objectSchema$1({}, []), async (_args, signal) => {
1410
+ tool("artifact_end", "End/close the ai-or-die Artifact review panel when the review loop is complete. Only works inside an ai-or-die tab-backed Claude session.", objectSchema$2({}, []), async (_args, signal) => {
1316
1411
  const env = readArtifactEnv();
1317
1412
  if (!env) return missingEnvResult();
1318
- return ok$1({
1413
+ return ok$2({
1319
1414
  ok: true,
1320
1415
  ...await clientFromEnv(env).end(signal),
1321
1416
  next_step: "Artifact review loop ended."
@@ -1401,7 +1496,7 @@ function isWaitingStatus(status) {
1401
1496
  const normalized = status.toLowerCase();
1402
1497
  return normalized === "waiting" || normalized === "pending" || normalized === "open" || normalized === "idle" || normalized === "timeout" || normalized === "no_feedback";
1403
1498
  }
1404
- function requiredString$1(args, key) {
1499
+ function requiredString$2(args, key) {
1405
1500
  const value = args[key];
1406
1501
  if (typeof value !== "string" || value.trim() === "") throw new ArtifactToolInputError("INVALID_ARGUMENT", `arguments.${key} is required and must be a non-empty string`);
1407
1502
  return value;
@@ -1415,15 +1510,15 @@ var ArtifactToolInputError = class extends Error {
1415
1510
  }
1416
1511
  };
1417
1512
  function missingEnvResult() {
1418
- return jsonResult$1({ error: {
1513
+ return jsonResult$2({ error: {
1419
1514
  code: "NOT_IN_AIORDIE_TAB",
1420
1515
  message: "artifact tools only work inside an ai-or-die tab-backed Claude session. Missing AIORDIE_BASE_URL, AIORDIE_TOKEN, or AIORDIE_SESSION_ID."
1421
1516
  } }, true);
1422
1517
  }
1423
- function ok$1(value) {
1424
- return jsonResult$1(value, false);
1518
+ function ok$2(value) {
1519
+ return jsonResult$2(value, false);
1425
1520
  }
1426
- function jsonResult$1(value, isError) {
1521
+ function jsonResult$2(value, isError) {
1427
1522
  return {
1428
1523
  content: [{
1429
1524
  type: "text",
@@ -1432,19 +1527,19 @@ function jsonResult$1(value, isError) {
1432
1527
  ...isError ? { isError: true } : {}
1433
1528
  };
1434
1529
  }
1435
- function errorResult$1(err) {
1436
- if (err instanceof ArtifactError) return jsonResult$1({ error: definedObject$1({
1530
+ function errorResult$2(err) {
1531
+ if (err instanceof ArtifactError) return jsonResult$2({ error: definedObject$1({
1437
1532
  code: err.code,
1438
1533
  message: err.message,
1439
1534
  retryable: err.retryable,
1440
1535
  status: err.status
1441
1536
  }) }, true);
1442
- return jsonResult$1({ error: {
1443
- code: errorCode$1(err),
1537
+ return jsonResult$2({ error: {
1538
+ code: errorCode$2(err),
1444
1539
  message: err instanceof Error ? err.message : String(err)
1445
1540
  } }, true);
1446
1541
  }
1447
- function errorCode$1(err) {
1542
+ function errorCode$2(err) {
1448
1543
  if (typeof err === "object" && err !== null && "code" in err) {
1449
1544
  const code = err.code;
1450
1545
  if (typeof code === "string") return code;
@@ -1456,7 +1551,7 @@ function definedObject$1(input) {
1456
1551
  for (const [key, value] of Object.entries(input)) if (value !== void 0) result[key] = value;
1457
1552
  return result;
1458
1553
  }
1459
- function objectSchema$1(properties, required) {
1554
+ function objectSchema$2(properties, required) {
1460
1555
  return {
1461
1556
  type: "object",
1462
1557
  required,
@@ -1464,13 +1559,44 @@ function objectSchema$1(properties, required) {
1464
1559
  properties
1465
1560
  };
1466
1561
  }
1467
- function stringProp$1(description) {
1562
+ function stringProp$2(description) {
1468
1563
  return {
1469
1564
  type: "string",
1470
1565
  description
1471
1566
  };
1472
1567
  }
1473
1568
 
1569
+ //#endregion
1570
+ //#region src/lib/fleet/mesh-egress-agent.ts
1571
+ const IS_BUN = typeof globalThis.Bun !== "undefined";
1572
+ /** Thrown when a mesh request is attempted under Bun (no Bearer-proxy support). */
1573
+ var MeshEgressUnsupportedRuntimeError = class extends Error {
1574
+ constructor() {
1575
+ super("mesh egress routing is unsupported under Bun (dev runtime): Bun's fetch cannot send a Proxy-Authorization Bearer header. Run the built Node binary (dist/main.js) to drive mesh peers.");
1576
+ this.name = "MeshEgressUnsupportedRuntimeError";
1577
+ }
1578
+ };
1579
+ /**
1580
+ * Attach the egress-proxy mechanism to a fetch init for a single mesh peer request.
1581
+ * Node → a NEW undici `ProxyAgent` dispatcher whose CONNECT carries the Bearer
1582
+ * `Proxy-Authorization`. A new agent per request is intentional: the credential can
1583
+ * rotate when the sidecar restarts, so a cached agent could pin a stale token, and a
1584
+ * per-request agent avoids stashing the credential in longer-lived state. The caller
1585
+ * MUST `close()` the returned agent after the request (it holds a socket pool).
1586
+ *
1587
+ * `isBun` is injectable so BOTH branches are unit-testable under one interpreter.
1588
+ * Under Bun this THROWS {@link MeshEgressUnsupportedRuntimeError} (fail closed).
1589
+ */
1590
+ function applyMeshEgressProxy(init, meshProxy, isBun = IS_BUN) {
1591
+ if (isBun) throw new MeshEgressUnsupportedRuntimeError();
1592
+ const agent = new ProxyAgent({
1593
+ uri: meshProxy.url,
1594
+ headers: { "Proxy-Authorization": meshProxy.authHeader }
1595
+ });
1596
+ init.dispatcher = agent;
1597
+ return agent;
1598
+ }
1599
+
1474
1600
  //#endregion
1475
1601
  //#region src/lib/fleet/tunnel-auth.ts
1476
1602
  var TunnelAuthError = class extends Error {
@@ -1659,17 +1785,17 @@ function createTunnelTokenProvider(runner = realDevtunnelRunner()) {
1659
1785
  async getToken(cfg) {
1660
1786
  const key = cfg.tunnelId;
1661
1787
  const now = Date.now();
1662
- const cached$1 = cache.get(key);
1663
- if (cached$1 && cached$1.expMs > now) {
1664
- const comfortablyFresh = cached$1.expMs - now > REFRESH_MARGIN_MS;
1665
- const recentlyMinted = now - cached$1.mintedAt < MIN_REMINT_INTERVAL_MS;
1666
- if (comfortablyFresh || recentlyMinted) return cached$1.token;
1788
+ const cached$2 = cache.get(key);
1789
+ if (cached$2 && cached$2.expMs > now) {
1790
+ const comfortablyFresh = cached$2.expMs - now > REFRESH_MARGIN_MS;
1791
+ const recentlyMinted = now - cached$2.mintedAt < MIN_REMINT_INTERVAL_MS;
1792
+ if (comfortablyFresh || recentlyMinted) return cached$2.token;
1667
1793
  }
1668
1794
  const inf = inflight.get(key);
1669
1795
  if (inf) return inf;
1670
1796
  const bo = backoff.get(key);
1671
1797
  if (bo && now < bo.until) {
1672
- if (cached$1 && cached$1.expMs > now) return cached$1.token;
1798
+ if (cached$2 && cached$2.expMs > now) return cached$2.token;
1673
1799
  throw bo.err;
1674
1800
  }
1675
1801
  const p = mintOnce(cfg);
@@ -1722,6 +1848,8 @@ var FleetClient = class {
1722
1848
  getTunnelToken;
1723
1849
  onTunnelAuthInvalidate;
1724
1850
  insecureTLS;
1851
+ meshProxy;
1852
+ applyMeshEgress;
1725
1853
  constructor(options) {
1726
1854
  this.baseUrl = options.url.replace(/\/+$/, "");
1727
1855
  this.origin = new URL(this.baseUrl).origin;
@@ -1733,6 +1861,8 @@ var FleetClient = class {
1733
1861
  this.getTunnelToken = options.getTunnelToken;
1734
1862
  this.onTunnelAuthInvalidate = options.onTunnelAuthInvalidate;
1735
1863
  this.insecureTLS = options.insecureTLS === true;
1864
+ this.meshProxy = options.meshProxy;
1865
+ this.applyMeshEgress = options.applyMeshEgress ?? applyMeshEgressProxy;
1736
1866
  }
1737
1867
  capabilities(signal) {
1738
1868
  return this.request("GET", "/api/control/capabilities", void 0, void 0, signal);
@@ -1810,6 +1940,11 @@ var FleetClient = class {
1810
1940
  message: "fleet request URL origin did not match the registered instance origin",
1811
1941
  retryable: false
1812
1942
  });
1943
+ if (this.auth.type === "mesh" && this.meshProxy === void 0) throw new FleetError({
1944
+ code: "MESH_UNCONFIGURED",
1945
+ message: "mesh egress unconfigured or stale: no live loopback egress proxy for this tailnet peer. Start (or restart) the local ai-or-die `--mesh` sidecar so it publishes a fresh mesh/egress.json; this host cannot reach a `.ts.net` peer directly.",
1946
+ retryable: true
1947
+ });
1813
1948
  const devtunnelHost = isDevtunnelHost(url.hostname);
1814
1949
  const tunnelEligible = this.getTunnelToken !== void 0 && devtunnelHost && url.protocol === "https:";
1815
1950
  for (let attempt = 0; attempt < 2; attempt++) {
@@ -1828,6 +1963,7 @@ var FleetClient = class {
1828
1963
  ...body === void 0 ? {} : { "Content-Type": "application/json" }
1829
1964
  };
1830
1965
  let response;
1966
+ let meshAgent;
1831
1967
  try {
1832
1968
  const init = {
1833
1969
  method,
@@ -1837,8 +1973,17 @@ var FleetClient = class {
1837
1973
  signal
1838
1974
  };
1839
1975
  if (this.insecureTLS) applyInsecureTls(init);
1976
+ if (this.auth.type === "mesh") {
1977
+ if (this.meshProxy === void 0) throw new FleetError({
1978
+ code: "MESH_UNCONFIGURED",
1979
+ message: "mesh egress unconfigured or stale: refusing a direct fetch to a tailnet peer.",
1980
+ retryable: true
1981
+ });
1982
+ meshAgent = this.applyMeshEgress(init, this.meshProxy);
1983
+ }
1840
1984
  response = await this.fetchFn(url.toString(), init);
1841
1985
  } catch (err) {
1986
+ await closeQuietly(meshAgent);
1842
1987
  if (canRetry && method === "GET") {
1843
1988
  this.onTunnelAuthInvalidate();
1844
1989
  continue;
@@ -1846,13 +1991,18 @@ var FleetClient = class {
1846
1991
  throw this.auth.type === "mesh" ? mapMeshUnreachable(err) : mapNetworkError(err, devtunnelHost);
1847
1992
  }
1848
1993
  if (!response.ok) {
1994
+ await closeQuietly(meshAgent);
1849
1995
  if ((response.status === 401 || response.status === 403) && canRetry) {
1850
1996
  this.onTunnelAuthInvalidate();
1851
1997
  continue;
1852
1998
  }
1853
1999
  throw await mapHttpError(response, url.toString());
1854
2000
  }
1855
- return await response.json();
2001
+ try {
2002
+ return await response.json();
2003
+ } finally {
2004
+ await closeQuietly(meshAgent);
2005
+ }
1856
2006
  }
1857
2007
  throw new FleetError({
1858
2008
  code: "AUTH_FAILED",
@@ -1976,19 +2126,31 @@ function detailToSearchString(detail) {
1976
2126
  }
1977
2127
  }
1978
2128
  function mapMeshUnreachable(err) {
2129
+ if (err instanceof FleetError) return err;
2130
+ if (err instanceof MeshEgressUnsupportedRuntimeError) return new FleetError({
2131
+ code: "MESH_UNCONFIGURED",
2132
+ message: err.message,
2133
+ retryable: false
2134
+ });
1979
2135
  if (isAbortLike$1(err)) return new FleetError({
1980
2136
  code: "TIMEOUT",
1981
2137
  message: "fleet mesh peer request timed out or was aborted",
1982
- retryable: true,
1983
- detail: err
2138
+ retryable: true
1984
2139
  });
1985
2140
  return new FleetError({
1986
2141
  code: "TAILNET_UNREACHABLE",
1987
- message: `fleet mesh peer unreachable: ${err instanceof Error ? err.message : String(err)} — the peer is a tailnet node but the request did not land. A mesh ACL drops blocked traffic SILENTLY, so the likely cause is the \`tag:aiordie\` ACL (verify the peer permits this node), not a dead instance; also confirm the peer's mesh sidecar is up and serving HTTPS on the tailnet.`,
1988
- retryable: true,
1989
- detail: err
2142
+ message: `fleet mesh peer unreachable${meshErrorClass(err)} — the request went through the local egress proxy but did not land on the peer. A mesh ACL drops blocked traffic SILENTLY, so the likely cause is the \`tag:aiordie\` ACL (verify the peer permits this node), not a dead instance; also confirm the peer's mesh sidecar is up and serving HTTPS on the tailnet, and that the local egress sidecar is still running.`,
2143
+ retryable: true
1990
2144
  });
1991
2145
  }
2146
+ function meshErrorClass(err) {
2147
+ if (typeof err !== "object" || err === null) return "";
2148
+ const record = err;
2149
+ const parts = [];
2150
+ if (typeof record.name === "string" && record.name !== "" && record.name !== "Error") parts.push(record.name);
2151
+ if (typeof record.code === "string" && record.code !== "") parts.push(record.code);
2152
+ return parts.length === 0 ? "" : ` (${parts.join(" ")})`;
2153
+ }
1992
2154
  function mapNetworkError(err, devtunnelHost = false) {
1993
2155
  if (isAbortLike$1(err)) return new FleetError({
1994
2156
  code: "TIMEOUT",
@@ -2028,6 +2190,13 @@ function detailToMessage(detail) {
2028
2190
  function isAbortLike$1(err) {
2029
2191
  return err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError");
2030
2192
  }
2193
+ async function closeQuietly(agent) {
2194
+ if (agent === void 0) return;
2195
+ try {
2196
+ if (typeof agent.destroy === "function") await agent.destroy();
2197
+ else if (typeof agent.close === "function") await agent.close();
2198
+ } catch {}
2199
+ }
2031
2200
 
2032
2201
  //#endregion
2033
2202
  //#region src/lib/fleet/registry.ts
@@ -2266,6 +2435,9 @@ function isNodeErrorCode(err, code) {
2266
2435
  */
2267
2436
  const DISCOVERY_CACHE_TTL_MS = 5e3;
2268
2437
  const PEERS_JSON_MAX_BYTES = 256 * 1024;
2438
+ const EGRESS_TTL_MS = 12e4;
2439
+ const EGRESS_FUTURE_SKEW_MS = 5e3;
2440
+ const EGRESS_JSON_MAX_BYTES = 64 * 1024;
2269
2441
  /** The ai-or-die app data dir (NOT github-router's) — mirrors MeshManager's `base`. */
2270
2442
  function aiordieAppDir() {
2271
2443
  if (process.platform === "win32") {
@@ -2279,6 +2451,11 @@ function meshPeersFilePath() {
2279
2451
  if (override && override.trim() !== "") return override.trim();
2280
2452
  return nodePath.join(aiordieAppDir(), "mesh", "peers.json");
2281
2453
  }
2454
+ function meshEgressFilePath() {
2455
+ const override = process.env.GH_ROUTER_FLEET_EGRESS_FILE;
2456
+ if (override && override.trim() !== "") return override.trim();
2457
+ return nodePath.join(aiordieAppDir(), "mesh", "egress.json");
2458
+ }
2282
2459
  function meshDiscoveryDisabled() {
2283
2460
  return process.env.GH_ROUTER_FLEET_DISCOVERY === "0";
2284
2461
  }
@@ -2347,16 +2524,104 @@ function toInfo(instance) {
2347
2524
  url: instance.url
2348
2525
  };
2349
2526
  }
2527
+ function defaultPidAlive(pid) {
2528
+ try {
2529
+ process.kill(pid, 0);
2530
+ return true;
2531
+ } catch (err) {
2532
+ return typeof err === "object" && err !== null && err.code === "EPERM";
2533
+ }
2534
+ }
2535
+ function validLoopbackHttpUrl(raw) {
2536
+ if (typeof raw !== "string" || raw.trim() === "") return void 0;
2537
+ let url;
2538
+ try {
2539
+ url = new URL(raw.trim());
2540
+ } catch {
2541
+ return;
2542
+ }
2543
+ if (url.protocol !== "http:") return void 0;
2544
+ if (url.username !== "" || url.password !== "") return void 0;
2545
+ if (url.pathname !== "" && url.pathname !== "/" || url.search !== "" || url.hash !== "") return void 0;
2546
+ if (url.port === "") return void 0;
2547
+ const host = url.hostname.replace(/^\[/, "").replace(/\]$/, "").toLowerCase();
2548
+ if (host !== "127.0.0.1" && host !== "::1") return void 0;
2549
+ return `http://${host === "::1" ? "[::1]" : host}:${url.port}`;
2550
+ }
2551
+ function validEgressToken(raw) {
2552
+ if (typeof raw !== "string" || raw === "") return void 0;
2553
+ if (/[\s\u0000-\u001f\u007f\u0080-\u009f]/.test(raw)) return void 0;
2554
+ return raw;
2555
+ }
2556
+ /**
2557
+ * Read + validate the sidecar's `mesh/egress.json` into a {@link FleetMeshProxy}.
2558
+ * Best-effort like {@link readMeshPeers}: NEVER throws — a missing / unreadable /
2559
+ * oversized / malformed / stale / dead-pid file yields `undefined`. The returned
2560
+ * `authHeader` is a credential: it lives only here and in the ProxyAgent header,
2561
+ * never in `FleetInstanceInfo` / logs / errors.
2562
+ */
2563
+ async function readMeshEgress(options = {}) {
2564
+ if (meshDiscoveryDisabled()) return void 0;
2565
+ const readFileFn = options.readFileFn ?? ((p) => fs.readFile(p, "utf8"));
2566
+ const now = options.now ?? (() => Date.now());
2567
+ const pidAlive = options.pidAlive ?? defaultPidAlive;
2568
+ let raw;
2569
+ try {
2570
+ raw = await readFileFn(meshEgressFilePath());
2571
+ } catch {
2572
+ return;
2573
+ }
2574
+ if (typeof raw !== "string") return void 0;
2575
+ if (Buffer.byteLength(raw, "utf8") > EGRESS_JSON_MAX_BYTES) return void 0;
2576
+ let parsed;
2577
+ try {
2578
+ parsed = JSON.parse(raw);
2579
+ } catch {
2580
+ return;
2581
+ }
2582
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
2583
+ if (parsed.version !== 1) return void 0;
2584
+ const url = validLoopbackHttpUrl(parsed.url);
2585
+ if (url === void 0) return void 0;
2586
+ const token = validEgressToken(parsed.token);
2587
+ if (token === void 0) return void 0;
2588
+ if (typeof parsed.pid !== "number" || !Number.isInteger(parsed.pid) || parsed.pid <= 0) return void 0;
2589
+ let alive;
2590
+ try {
2591
+ alive = pidAlive(parsed.pid);
2592
+ } catch {
2593
+ return;
2594
+ }
2595
+ if (!alive) return void 0;
2596
+ if (typeof parsed.updatedAt !== "number" || !Number.isFinite(parsed.updatedAt)) return void 0;
2597
+ let age;
2598
+ try {
2599
+ age = now() - parsed.updatedAt;
2600
+ } catch {
2601
+ return;
2602
+ }
2603
+ if (!Number.isFinite(age)) return void 0;
2604
+ if (age > EGRESS_TTL_MS || age < -EGRESS_FUTURE_SKEW_MS) return void 0;
2605
+ return {
2606
+ url,
2607
+ authHeader: `Bearer ${token}`
2608
+ };
2609
+ }
2350
2610
  /**
2351
2611
  * Registry that merges a static `fleet.json` registry with mesh discovery. Static
2352
2612
  * ALWAYS wins on an id collision (a discovered peer sharing a static id is dropped
2353
2613
  * — never overwrites a configured instance, never merges auth across sources).
2354
2614
  * Discovery is cached briefly so a fan-out doesn't re-read the file per call, and a
2355
2615
  * failed discovery read leaves the static set untouched.
2616
+ *
2617
+ * The mesh egress proxy (from `egress.json`) is read in the SAME cached window and
2618
+ * attached to EVERY discovered mesh peer — the egress is per-conductor/self, shared
2619
+ * by all peers on this tailnet. Static bearer instances never carry one.
2356
2620
  */
2357
2621
  var MergedFleetRegistry = class {
2358
2622
  staticRegistry;
2359
2623
  discover;
2624
+ discoverEgress;
2360
2625
  cache;
2361
2626
  inflight;
2362
2627
  ttlMs;
@@ -2364,6 +2629,7 @@ var MergedFleetRegistry = class {
2364
2629
  constructor(options = {}) {
2365
2630
  this.staticRegistry = options.staticRegistry ?? new FleetRegistry();
2366
2631
  this.discover = options.discover ?? (() => readMeshPeers());
2632
+ this.discoverEgress = options.discoverEgress ?? (() => readMeshEgress());
2367
2633
  this.ttlMs = options.ttlMs ?? DISCOVERY_CACHE_TTL_MS;
2368
2634
  this.now = options.now ?? (() => Date.now());
2369
2635
  }
@@ -2372,17 +2638,18 @@ var MergedFleetRegistry = class {
2372
2638
  if (this.cache && now - this.cache.at < this.ttlMs) return this.cache.peers;
2373
2639
  if (this.inflight) return this.inflight;
2374
2640
  this.inflight = (async () => {
2375
- let peers;
2376
- try {
2377
- peers = await this.discover();
2378
- } catch {
2379
- peers = [];
2380
- }
2641
+ const [peersResult, egressResult] = await Promise.allSettled([this.discover(), this.discoverEgress()]);
2642
+ const peers = peersResult.status === "fulfilled" ? peersResult.value : [];
2643
+ const egress = egressResult.status === "fulfilled" ? egressResult.value : void 0;
2644
+ const withEgress = egress === void 0 ? peers : peers.map((peer) => peer.auth.type === "mesh" ? {
2645
+ ...peer,
2646
+ meshProxy: egress
2647
+ } : peer);
2381
2648
  this.cache = {
2382
2649
  at: this.now(),
2383
- peers
2650
+ peers: withEgress
2384
2651
  };
2385
- return peers;
2652
+ return withEgress;
2386
2653
  })().finally(() => {
2387
2654
  this.inflight = void 0;
2388
2655
  });
@@ -2445,6 +2712,12 @@ function createFleetTools(options = {}) {
2445
2712
  return defaultRegistry;
2446
2713
  }
2447
2714
  function clientFor(instance) {
2715
+ if (instance.auth.type === "mesh") return options.createClient ? options.createClient(instance) : new FleetClient({
2716
+ url: instance.url,
2717
+ auth: instance.auth,
2718
+ fetchFn: options.fetchFn,
2719
+ meshProxy: instance.meshProxy
2720
+ });
2448
2721
  const key = `${instance.id}\0${instance.url}\0${instance.auth.type}\0${instance.token}\0${instance.tunnelId ?? ""}\0${instance.tunnelToken ?? ""}\0${instance.insecureTLS === true ? "1" : "0"}`;
2449
2722
  const existing = clients.get(key);
2450
2723
  if (existing) return existing;
@@ -2460,8 +2733,8 @@ function createFleetTools(options = {}) {
2460
2733
  }
2461
2734
  async function getInstanceCapabilities(instance, signal) {
2462
2735
  const now = Date.now();
2463
- const cached$1 = capabilitiesCache.get(instance.id);
2464
- if (cached$1 && now - cached$1.at < CAPABILITIES_CACHE_TTL_MS) return cached$1.caps;
2736
+ const cached$2 = capabilitiesCache.get(instance.id);
2737
+ if (cached$2 && now - cached$2.at < CAPABILITIES_CACHE_TTL_MS) return cached$2.caps;
2465
2738
  try {
2466
2739
  const response = await clientFor(instance).capabilities(signal);
2467
2740
  const caps = new Set(response.capabilities);
@@ -2486,10 +2759,10 @@ function createFleetTools(options = {}) {
2486
2759
  return getRegistry().resolveInstance(arg);
2487
2760
  }
2488
2761
  async function resolveSession(args) {
2489
- const globalId = requiredString(args, "sessionId");
2762
+ const globalId = requiredString$1(args, "sessionId");
2490
2763
  const decoded = decodeSessionId(globalId);
2491
2764
  const instance = await resolve(decoded.instanceId);
2492
- const explicitInstance = optionalString(args, "instance");
2765
+ const explicitInstance = optionalString$1(args, "instance");
2493
2766
  if (explicitInstance !== void 0) {
2494
2767
  const explicit = await resolve(explicitInstance);
2495
2768
  if (explicit.id !== decoded.instanceId) throw new FleetToolInputError("INSTANCE_MISMATCH", `sessionId is for instance ${JSON.stringify(decoded.instanceId)} but arguments.instance resolved to ${JSON.stringify(explicit.id)}`);
@@ -2503,8 +2776,8 @@ function createFleetTools(options = {}) {
2503
2776
  async function probeInstance(info) {
2504
2777
  const cacheKey = `${info.id}\0${info.url}`;
2505
2778
  const now = Date.now();
2506
- const cached$1 = instanceProbeCache.get(cacheKey);
2507
- if (cached$1 && now - cached$1.at < INSTANCE_PROBE_CACHE_TTL_MS) return cached$1.result;
2779
+ const cached$2 = instanceProbeCache.get(cacheKey);
2780
+ if (cached$2 && now - cached$2.at < INSTANCE_PROBE_CACHE_TTL_MS) return cached$2.result;
2508
2781
  for (let attempt = 0; attempt <= INSTANCE_PROBE_RATE_LIMIT_MAX_RETRIES; attempt++) {
2509
2782
  const timeout = createProbeTimeout();
2510
2783
  try {
@@ -2557,69 +2830,69 @@ function createFleetTools(options = {}) {
2557
2830
  try {
2558
2831
  return await handler(args, signal);
2559
2832
  } catch (err) {
2560
- return errorResult(err);
2833
+ return errorResult$1(err);
2561
2834
  }
2562
2835
  }
2563
2836
  };
2564
2837
  }
2565
2838
  return Object.freeze([
2566
- tool$1("list_instances", "List registered remote ai-or-die instances in the fleet registry. Tokens are never returned.", objectSchema({}, []), async () => {
2567
- return ok({ instances: await mapWithConcurrency(await getRegistry().listInstances(), fleetFanoutConcurrency(LIST_INSTANCES_FANOUT_CONCURRENCY), (instance) => probeInstance(instance)) });
2839
+ tool$1("list_instances", "List registered remote ai-or-die instances in the fleet registry. Tokens are never returned.", objectSchema$1({}, []), async () => {
2840
+ return ok$1({ instances: await mapWithConcurrency(await getRegistry().listInstances(), fleetFanoutConcurrency(LIST_INSTANCES_FANOUT_CONCURRENCY), (instance) => probeInstance(instance)) });
2568
2841
  }),
2569
- tool$1("list_sessions", "List sessions on one fleet instance, returning globally-addressable session ids.", objectSchema({ instance: stringProp("Instance id or label. Defaults to the registry default, or the sole instance.") }, []), async (args, signal) => {
2570
- const instance = await resolve(optionalString(args, "instance"));
2842
+ tool$1("list_sessions", "List sessions on one fleet instance, returning globally-addressable session ids.", objectSchema$1({ instance: stringProp$1("Instance id or label. Defaults to the registry default, or the sole instance.") }, []), async (args, signal) => {
2843
+ const instance = await resolve(optionalString$1(args, "instance"));
2571
2844
  const response = await clientFor(instance).listSessions(signal);
2572
- return ok({
2845
+ return ok$1({
2573
2846
  resolvedInstance: publicInstance(instance),
2574
2847
  sessions: response.sessions.map((session) => globalizeSession(instance.id, session))
2575
2848
  });
2576
2849
  }),
2577
- tool$1("read_session", "Read recent text output from an addressed fleet session.", objectSchema({
2578
- sessionId: stringProp("Global session id in the form instanceId:localSessionId."),
2579
- instance: stringProp("Optional instance id/label; when supplied it must agree with sessionId."),
2580
- lines: numberProp("Number of recent lines to read."),
2581
- format: stringProp("Reserved for future formatting; results are JSON text today.")
2850
+ tool$1("read_session", "Read recent text output from an addressed fleet session.", objectSchema$1({
2851
+ sessionId: stringProp$1("Global session id in the form instanceId:localSessionId."),
2852
+ instance: stringProp$1("Optional instance id/label; when supplied it must agree with sessionId."),
2853
+ lines: numberProp$1("Number of recent lines to read."),
2854
+ format: stringProp$1("Reserved for future formatting; results are JSON text today.")
2582
2855
  }, ["sessionId"]), async (args, signal) => {
2583
2856
  const { instance, localId, globalId } = await resolveSession(args);
2584
- const lines = optionalNumber(args, "lines");
2857
+ const lines = optionalNumber$1(args, "lines");
2585
2858
  const response = await clientFor(instance).readSession(localId, lines, signal);
2586
- return ok({
2859
+ return ok$1({
2587
2860
  resolvedInstance: publicInstance(instance),
2588
2861
  ...response,
2589
2862
  sessionId: globalId
2590
2863
  });
2591
2864
  }),
2592
- tool$1("session_status", "Fetch lifecycle and interaction status for an addressed fleet session.", objectSchema({
2593
- sessionId: stringProp("Global session id in the form instanceId:localSessionId."),
2594
- instance: stringProp("Optional instance id/label; when supplied it must agree with sessionId.")
2865
+ tool$1("session_status", "Fetch lifecycle and interaction status for an addressed fleet session.", objectSchema$1({
2866
+ sessionId: stringProp$1("Global session id in the form instanceId:localSessionId."),
2867
+ instance: stringProp$1("Optional instance id/label; when supplied it must agree with sessionId.")
2595
2868
  }, ["sessionId"]), async (args, signal) => {
2596
2869
  const { instance, localId, globalId } = await resolveSession(args);
2597
2870
  const response = await clientFor(instance).status(localId, signal);
2598
- return ok({
2871
+ return ok$1({
2599
2872
  resolvedInstance: publicInstance(instance),
2600
2873
  ...response,
2601
2874
  sessionId: globalId
2602
2875
  });
2603
2876
  }),
2604
- tool$1("send_message", "Send a message to a fleet session. isError reflects DELIVERY ONLY: it is true only when the message could not be delivered to the session (transport/precondition failure). A delivered message whose confirmation did not arrive within awaitMs is NOT an error — it returns delivered:true with confirmationPending/confirmationTimedOut, because a long turn legitimately outruns awaitMs. Recommended pattern: send with awaitMs:0 for a fast delivery ack that never blocks on confirmation, then call await_turn (filtered to this sessionId) to observe the session's actual turn completion. The idempotencyKey makes a retried send safe (a retry never re-types the message).", objectSchema({
2605
- sessionId: stringProp("Global session id in the form instanceId:localSessionId."),
2606
- instance: stringProp("Optional instance id/label; when supplied it must agree with sessionId."),
2607
- message: stringProp("Message text to deliver to the session."),
2608
- idempotencyKey: stringProp("Optional caller idempotency key; AUTO-GENERATED when omitted, so you normally never pass it. Supply your OWN stable key only when you will retry the SAME send and need the upstream to dedupe it."),
2609
- awaitMs: numberProp("Optional best-effort confirmation wait (ms) — NOT a deadline. Prefer awaitMs:0 plus await_turn; a turn that outruns awaitMs returns confirmationPending, not an error.")
2877
+ tool$1("send_message", "Send a message to a fleet session. isError reflects DELIVERY ONLY: it is true only when the message could not be delivered to the session (transport/precondition failure). A delivered message whose confirmation did not arrive within awaitMs is NOT an error — it returns delivered:true with confirmationPending/confirmationTimedOut, because a long turn legitimately outruns awaitMs. Recommended pattern: send with awaitMs:0 for a fast delivery ack that never blocks on confirmation, then call await_turn (filtered to this sessionId) to observe the session's actual turn completion. The idempotencyKey makes a retried send safe (a retry never re-types the message).", objectSchema$1({
2878
+ sessionId: stringProp$1("Global session id in the form instanceId:localSessionId."),
2879
+ instance: stringProp$1("Optional instance id/label; when supplied it must agree with sessionId."),
2880
+ message: stringProp$1("Message text to deliver to the session."),
2881
+ idempotencyKey: stringProp$1("Optional caller idempotency key; AUTO-GENERATED when omitted, so you normally never pass it. Supply your OWN stable key only when you will retry the SAME send and need the upstream to dedupe it."),
2882
+ awaitMs: numberProp$1("Optional best-effort confirmation wait (ms) — NOT a deadline. Prefer awaitMs:0 plus await_turn; a turn that outruns awaitMs returns confirmationPending, not an error.")
2610
2883
  }, ["sessionId", "message"]), async (args, signal) => {
2611
2884
  const { instance, localId, globalId } = await resolveSession(args);
2612
- const awaitMs = optionalNumber(args, "awaitMs");
2885
+ const awaitMs = optionalNumber$1(args, "awaitMs");
2613
2886
  const response = await clientFor(instance).sendMessage(localId, {
2614
- message: requiredString(args, "message"),
2615
- idempotencyKey: optionalString(args, "idempotencyKey") ?? randomUUID(),
2887
+ message: requiredString$1(args, "message"),
2888
+ idempotencyKey: optionalString$1(args, "idempotencyKey") ?? randomUUID(),
2616
2889
  ...awaitMs === void 0 ? {} : { awaitMs }
2617
2890
  }, signal);
2618
2891
  const delivered = !(response.delivered === false || response.delivery?.status === "failed" || response.delivery?.status === "error");
2619
2892
  const confirmed = delivered && response.confirmed === true;
2620
2893
  const confirmationTimedOut = delivered && !confirmed && (awaitMs !== void 0 && awaitMs > 0 || response.confirmationTimedOut === true);
2621
2894
  const isError = !delivered;
2622
- return jsonResult({
2895
+ return jsonResult$1({
2623
2896
  resolvedInstance: publicInstance(instance),
2624
2897
  sessionId: globalId,
2625
2898
  ...response,
@@ -2632,111 +2905,111 @@ function createFleetTools(options = {}) {
2632
2905
  ...isError ? { message: "message was not delivered to the session by the upstream instance" } : confirmationTimedOut ? { message: "delivered; turn completion not confirmed in the await window. Use await_turn filtered to this sessionId to observe completion (the idempotencyKey makes a retried send safe)." } : {}
2633
2906
  }, isError);
2634
2907
  }),
2635
- tool$1("send_keys", "Send key input to a fleet session.", objectSchema({
2636
- sessionId: stringProp("Global session id in the form instanceId:localSessionId."),
2637
- instance: stringProp("Optional instance id/label; when supplied it must agree with sessionId."),
2638
- keys: stringProp("Key sequence to send."),
2639
- idempotencyKey: stringProp("Optional caller idempotency key; auto-generated when omitted."),
2908
+ tool$1("send_keys", "Send key input to a fleet session.", objectSchema$1({
2909
+ sessionId: stringProp$1("Global session id in the form instanceId:localSessionId."),
2910
+ instance: stringProp$1("Optional instance id/label; when supplied it must agree with sessionId."),
2911
+ keys: stringProp$1("Key sequence to send."),
2912
+ idempotencyKey: stringProp$1("Optional caller idempotency key; auto-generated when omitted."),
2640
2913
  raw: booleanProp("Pass keys through as raw input when the instance supports it.")
2641
2914
  }, ["sessionId", "keys"]), async (args, signal) => {
2642
2915
  const { instance, localId, globalId } = await resolveSession(args);
2643
2916
  const raw = optionalBoolean(args, "raw");
2644
2917
  const response = await clientFor(instance).sendKeys(localId, {
2645
- keys: requiredString(args, "keys"),
2646
- idempotencyKey: optionalString(args, "idempotencyKey") ?? randomUUID(),
2918
+ keys: requiredString$1(args, "keys"),
2919
+ idempotencyKey: optionalString$1(args, "idempotencyKey") ?? randomUUID(),
2647
2920
  ...raw === void 0 ? {} : { raw }
2648
2921
  }, signal);
2649
- return ok({
2922
+ return ok$1({
2650
2923
  resolvedInstance: publicInstance(instance),
2651
2924
  sessionId: globalId,
2652
2925
  ...response
2653
2926
  });
2654
2927
  }),
2655
- tool$1("respond", "Answer an awaited prompt in a fleet session by choice, option value, or explicit key override.", objectSchema({
2656
- sessionId: stringProp("Global session id in the form instanceId:localSessionId."),
2657
- instance: stringProp("Optional instance id/label; when supplied it must agree with sessionId."),
2658
- choice: stringProp("Named or numbered choice to select."),
2659
- optionValue: stringProp("Exact option value to select."),
2660
- keys: stringProp("Explicit key override to send instead of a mapped choice."),
2661
- idempotencyKey: stringProp("Optional caller idempotency key; auto-generated when omitted.")
2928
+ tool$1("respond", "Answer an awaited prompt in a fleet session by choice, option value, or explicit key override.", objectSchema$1({
2929
+ sessionId: stringProp$1("Global session id in the form instanceId:localSessionId."),
2930
+ instance: stringProp$1("Optional instance id/label; when supplied it must agree with sessionId."),
2931
+ choice: stringProp$1("Named or numbered choice to select."),
2932
+ optionValue: stringProp$1("Exact option value to select."),
2933
+ keys: stringProp$1("Explicit key override to send instead of a mapped choice."),
2934
+ idempotencyKey: stringProp$1("Optional caller idempotency key; auto-generated when omitted.")
2662
2935
  }, ["sessionId"]), async (args, signal) => {
2663
2936
  const { instance, localId, globalId } = await resolveSession(args);
2664
2937
  const input = definedObject({
2665
- choice: optionalString(args, "choice"),
2666
- optionValue: optionalString(args, "optionValue"),
2667
- keys: optionalString(args, "keys"),
2668
- idempotencyKey: optionalString(args, "idempotencyKey") ?? randomUUID()
2938
+ choice: optionalString$1(args, "choice"),
2939
+ optionValue: optionalString$1(args, "optionValue"),
2940
+ keys: optionalString$1(args, "keys"),
2941
+ idempotencyKey: optionalString$1(args, "idempotencyKey") ?? randomUUID()
2669
2942
  });
2670
2943
  const response = await clientFor(instance).respond(localId, input, signal);
2671
- return ok({
2944
+ return ok$1({
2672
2945
  resolvedInstance: publicInstance(instance),
2673
2946
  sessionId: globalId,
2674
2947
  ...response
2675
2948
  });
2676
2949
  }),
2677
- tool$1("create_session", "Create a new session on a specific fleet instance. The instance argument is required; no default is used.", objectSchema({
2678
- instance: stringProp("Required instance id or label. Create never uses the registry default."),
2679
- agent: stringProp("Agent/runtime to create on the instance."),
2680
- name: stringProp("Optional display name for the session."),
2681
- workingDir: stringProp("Optional working directory on the remote instance."),
2682
- idempotencyKey: stringProp("Optional caller idempotency key; auto-generated when omitted."),
2950
+ tool$1("create_session", "Create a new session on a specific fleet instance. The instance argument is required; no default is used.", objectSchema$1({
2951
+ instance: stringProp$1("Required instance id or label. Create never uses the registry default."),
2952
+ agent: stringProp$1("Agent/runtime to create on the instance."),
2953
+ name: stringProp$1("Optional display name for the session."),
2954
+ workingDir: stringProp$1("Optional working directory on the remote instance."),
2955
+ idempotencyKey: stringProp$1("Optional caller idempotency key; auto-generated when omitted."),
2683
2956
  start: booleanProp("Whether the remote instance should start the session immediately."),
2684
- readyTimeoutMs: numberProp("F17: bounded ms to wait for the agent to become driveable before returning. The response carries ready/bound/blocker."),
2685
- permissionMode: stringProp("F10 (claude only): permission mode the launched agent starts in — one of plan | acceptEdits | default | bypassPermissions. Rejected with BAD_REQUEST if unknown or if agentArgs also sets it."),
2957
+ readyTimeoutMs: numberProp$1("F17: bounded ms to wait for the agent to become driveable before returning. The response carries ready/bound/blocker."),
2958
+ permissionMode: stringProp$1("F10 (claude only): permission mode the launched agent starts in — one of plan | acceptEdits | default | bypassPermissions. Rejected with BAD_REQUEST if unknown or if agentArgs also sets it."),
2686
2959
  agentArgs: arrayProp("F10 (claude only): extra launcher args appended after the github-router prefix. Must NOT include --permission-mode or --dangerously-skip-permissions (use permissionMode) — rejected with BAD_REQUEST.")
2687
2960
  }, ["instance", "agent"]), async (args, signal) => {
2688
- const instance = await resolve(requiredString(args, "instance"));
2689
- const agent = requiredString(args, "agent");
2690
- const idempotencyKey = optionalString(args, "idempotencyKey") ?? randomUUID();
2691
- const permissionMode = optionalString(args, "permissionMode");
2961
+ const instance = await resolve(requiredString$1(args, "instance"));
2962
+ const agent = requiredString$1(args, "agent");
2963
+ const idempotencyKey = optionalString$1(args, "idempotencyKey") ?? randomUUID();
2964
+ const permissionMode = optionalString$1(args, "permissionMode");
2692
2965
  const agentArgs = optionalStringArray(args, "agentArgs");
2693
2966
  if (permissionMode !== void 0) await assertCapability(instance, "permission_mode", "permissionMode", signal);
2694
2967
  if (agentArgs !== void 0) await assertCapability(instance, "agent_args", "agentArgs", signal);
2695
2968
  const response = await clientFor(instance).createSession(definedObject({
2696
2969
  agent,
2697
- name: optionalString(args, "name"),
2698
- workingDir: optionalString(args, "workingDir"),
2970
+ name: optionalString$1(args, "name"),
2971
+ workingDir: optionalString$1(args, "workingDir"),
2699
2972
  start: optionalBoolean(args, "start"),
2700
- readyTimeoutMs: optionalNumber(args, "readyTimeoutMs"),
2973
+ readyTimeoutMs: optionalNumber$1(args, "readyTimeoutMs"),
2701
2974
  permissionMode,
2702
2975
  agentArgs,
2703
2976
  idempotencyKey
2704
2977
  }), signal);
2705
2978
  const localSessionId = typeof response.sessionId === "string" ? response.sessionId : "";
2706
- return ok({
2979
+ return ok$1({
2707
2980
  resolvedInstance: publicInstance(instance),
2708
2981
  ...response,
2709
2982
  sessionId: localSessionId ? encodeSessionId(instance.id, localSessionId) : response.sessionId
2710
2983
  });
2711
2984
  }),
2712
- tool$1("stop_session", "Stop a fleet session.", objectSchema({
2713
- sessionId: stringProp("Global session id in the form instanceId:localSessionId."),
2714
- instance: stringProp("Optional instance id/label; when supplied it must agree with sessionId."),
2715
- idempotencyKey: stringProp("Optional caller idempotency key; auto-generated when omitted."),
2716
- mode: stringProp("Optional stop mode understood by the remote instance.")
2985
+ tool$1("stop_session", "Stop a fleet session.", objectSchema$1({
2986
+ sessionId: stringProp$1("Global session id in the form instanceId:localSessionId."),
2987
+ instance: stringProp$1("Optional instance id/label; when supplied it must agree with sessionId."),
2988
+ idempotencyKey: stringProp$1("Optional caller idempotency key; auto-generated when omitted."),
2989
+ mode: stringProp$1("Optional stop mode understood by the remote instance.")
2717
2990
  }, ["sessionId"]), async (args, signal) => {
2718
2991
  const { instance, localId, globalId } = await resolveSession(args);
2719
- const idempotencyKey = optionalString(args, "idempotencyKey") ?? randomUUID();
2992
+ const idempotencyKey = optionalString$1(args, "idempotencyKey") ?? randomUUID();
2720
2993
  const response = await clientFor(instance).stopSession(localId, definedObject({
2721
- mode: optionalString(args, "mode"),
2994
+ mode: optionalString$1(args, "mode"),
2722
2995
  idempotencyKey
2723
2996
  }), signal);
2724
- return ok({
2997
+ return ok$1({
2725
2998
  resolvedInstance: publicInstance(instance),
2726
2999
  sessionId: globalId,
2727
3000
  ...response
2728
3001
  });
2729
3002
  }),
2730
- tool$1("await_turn", "Long-poll session events across fleet instances. The server owns per-target opaque cursors, so callers do not pass cursor tokens. Distinct concurrent watchers over the same instance set should pass a distinct watcherId so they do not share a cursor.", objectSchema({
3003
+ tool$1("await_turn", "Long-poll session events across fleet instances. The server owns per-target opaque cursors, so callers do not pass cursor tokens. Distinct concurrent watchers over the same instance set should pass a distinct watcherId so they do not share a cursor.", objectSchema$1({
2731
3004
  instances: arrayProp("Instance ids or labels to poll. Omit with sessionIds to target those session instances; omit both to poll every registered instance."),
2732
3005
  sessionIds: arrayProp("Global session ids to filter to."),
2733
- timeoutMs: numberProp("Long-poll timeout per instance in milliseconds."),
3006
+ timeoutMs: numberProp$1("Long-poll timeout per instance in milliseconds."),
2734
3007
  kinds: arrayProp("Optional event kinds to filter to."),
2735
- watcherId: stringProp("Optional stable id for this watcher. Use a distinct value for concurrent watchers over the same target set to keep cursors isolated.")
3008
+ watcherId: stringProp$1("Optional stable id for this watcher. Use a distinct value for concurrent watchers over the same target set to keep cursors isolated.")
2736
3009
  }, []), async (args, signal) => {
2737
3010
  const target = await resolveAwaitTarget(args, getRegistry());
2738
- const cursorByInstance = takeAwaitTurnCursorMap(awaitTurnCursorKey(optionalString(args, "watcherId")));
2739
- const timeoutMs = optionalNumber(args, "timeoutMs");
3011
+ const cursorByInstance = takeAwaitTurnCursorMap(awaitTurnCursorKey(optionalString$1(args, "watcherId")));
3012
+ const timeoutMs = optionalNumber$1(args, "timeoutMs");
2740
3013
  const kinds = optionalStringArray(args, "kinds");
2741
3014
  const results = await mapWithConcurrency(target.instances, fleetFanoutConcurrency(AWAIT_TURN_FANOUT_CONCURRENCY), async (instance) => {
2742
3015
  const deadline = createAwaitTurnDeadline(timeoutMs, awaitTurnDeadlineSlackMs);
@@ -2779,7 +3052,7 @@ function createFleetTools(options = {}) {
2779
3052
  instance: publicInstance(instance),
2780
3053
  ...gap
2781
3054
  })));
2782
- return ok({
3055
+ return ok$1({
2783
3056
  resolvedInstances: target.instances.map(publicInstance),
2784
3057
  events: events$1,
2785
3058
  gaps,
@@ -2791,53 +3064,53 @@ function createFleetTools(options = {}) {
2791
3064
  ...errors.length > 0 ? { errors } : {}
2792
3065
  });
2793
3066
  }),
2794
- tool$1("read_file", "Read a file from one fleet instance via its existing /api/files/content endpoint.", objectSchema({
2795
- instance: stringProp("Instance id or label. Defaults to the registry default, or the sole instance."),
2796
- path: stringProp("Remote file path to read.")
3067
+ tool$1("read_file", "Read a file from one fleet instance via its existing /api/files/content endpoint.", objectSchema$1({
3068
+ instance: stringProp$1("Instance id or label. Defaults to the registry default, or the sole instance."),
3069
+ path: stringProp$1("Remote file path to read.")
2797
3070
  }, ["path"]), async (args, signal) => {
2798
- const instance = await resolve(optionalString(args, "instance"));
2799
- const response = await clientFor(instance).readFile(requiredString(args, "path"), signal);
2800
- return ok({
3071
+ const instance = await resolve(optionalString$1(args, "instance"));
3072
+ const response = await clientFor(instance).readFile(requiredString$1(args, "path"), signal);
3073
+ return ok$1({
2801
3074
  resolvedInstance: publicInstance(instance),
2802
3075
  ...response
2803
3076
  });
2804
3077
  }),
2805
- tool$1("list_dir", "List a directory on one fleet instance via its existing /api/files endpoint.", objectSchema({
2806
- instance: stringProp("Instance id or label. Defaults to the registry default, or the sole instance."),
2807
- path: stringProp("Remote directory path to list.")
3078
+ tool$1("list_dir", "List a directory on one fleet instance via its existing /api/files endpoint.", objectSchema$1({
3079
+ instance: stringProp$1("Instance id or label. Defaults to the registry default, or the sole instance."),
3080
+ path: stringProp$1("Remote directory path to list.")
2808
3081
  }, ["path"]), async (args, signal) => {
2809
- const instance = await resolve(optionalString(args, "instance"));
2810
- const response = await clientFor(instance).listDir(requiredString(args, "path"), signal);
2811
- return ok({
3082
+ const instance = await resolve(optionalString$1(args, "instance"));
3083
+ const response = await clientFor(instance).listDir(requiredString$1(args, "path"), signal);
3084
+ return ok$1({
2812
3085
  resolvedInstance: publicInstance(instance),
2813
3086
  ...response
2814
3087
  });
2815
3088
  }),
2816
- tool$1("search", "Search files on one fleet instance via its existing /api/search endpoint.", objectSchema({
2817
- instance: stringProp("Instance id or label. Defaults to the registry default, or the sole instance."),
2818
- query: stringProp("Search query."),
2819
- path: stringProp("Optional path scope.")
3089
+ tool$1("search", "Search files on one fleet instance via its existing /api/search endpoint.", objectSchema$1({
3090
+ instance: stringProp$1("Instance id or label. Defaults to the registry default, or the sole instance."),
3091
+ query: stringProp$1("Search query."),
3092
+ path: stringProp$1("Optional path scope.")
2820
3093
  }, ["query"]), async (args, signal) => {
2821
- const instance = await resolve(optionalString(args, "instance"));
2822
- const response = await clientFor(instance).search(requiredString(args, "query"), optionalString(args, "path"), signal);
2823
- return ok({
3094
+ const instance = await resolve(optionalString$1(args, "instance"));
3095
+ const response = await clientFor(instance).search(requiredString$1(args, "query"), optionalString$1(args, "path"), signal);
3096
+ return ok$1({
2824
3097
  resolvedInstance: publicInstance(instance),
2825
3098
  ...response
2826
3099
  });
2827
3100
  }),
2828
- tool$1("git_show", "Read a file/revision through one fleet instance's existing /api/files/git-show endpoint.", objectSchema({
2829
- instance: stringProp("Instance id or label. Defaults to the registry default, or the sole instance."),
2830
- path: stringProp("Remote repository path or file path for git-show."),
2831
- ref: stringProp("Optional git ref/revision."),
2832
- rev: stringProp("Optional git revision alias."),
2833
- commit: stringProp("Optional commit id.")
3101
+ tool$1("git_show", "Read a file/revision through one fleet instance's existing /api/files/git-show endpoint.", objectSchema$1({
3102
+ instance: stringProp$1("Instance id or label. Defaults to the registry default, or the sole instance."),
3103
+ path: stringProp$1("Remote repository path or file path for git-show."),
3104
+ ref: stringProp$1("Optional git ref/revision."),
3105
+ rev: stringProp$1("Optional git revision alias."),
3106
+ commit: stringProp$1("Optional commit id.")
2834
3107
  }, ["path"]), async (args, signal) => {
2835
- const instance = await resolve(optionalString(args, "instance"));
3108
+ const instance = await resolve(optionalString$1(args, "instance"));
2836
3109
  const response = await clientFor(instance).gitShow({
2837
3110
  ...args,
2838
3111
  instance: void 0
2839
3112
  }, signal);
2840
- return ok({
3113
+ return ok$1({
2841
3114
  resolvedInstance: publicInstance(instance),
2842
3115
  ...response
2843
3116
  });
@@ -2930,6 +3203,8 @@ function fleetProbeHint(code) {
2930
3203
  case "RELAY_ERROR": return "tunnel relay returned an error; the host may be down, restarting, or under load";
2931
3204
  case "TIMEOUT": return "no response before the probe deadline; the host may be slow or the tunnel may have no host";
2932
3205
  case "UNREACHABLE": return "could not connect (DNS or connection failure); check the instance url";
3206
+ case "MESH_UNCONFIGURED": return "mesh egress unconfigured or stale; (re)start the local ai-or-die --mesh sidecar so it publishes a fresh mesh/egress.json";
3207
+ case "TAILNET_UNREACHABLE": return "reached the local egress proxy but the request did not land on the tailnet peer; likely the tag:aiordie ACL, or the peer's sidecar is down";
2933
3208
  default: return;
2934
3209
  }
2935
3210
  }
@@ -2944,170 +3219,3299 @@ function isFleetErrorCode(code) {
2944
3219
  case "NO_HOST":
2945
3220
  case "RELAY_ERROR":
2946
3221
  case "BAD_REQUEST":
2947
- case "RATE_LIMITED": return true;
3222
+ case "RATE_LIMITED":
3223
+ case "TAILNET_UNREACHABLE":
3224
+ case "MESH_UNCONFIGURED": return true;
2948
3225
  default: return false;
2949
3226
  }
2950
3227
  }
2951
- async function resolveAwaitTarget(args, registry) {
2952
- const instanceArgs = optionalStringArray(args, "instances");
2953
- const sessionIdArgs = optionalStringArray(args, "sessionIds");
2954
- const localSessionIdsByInstance = /* @__PURE__ */ new Map();
2955
- for (const sessionId of sessionIdArgs ?? []) {
2956
- const decoded = decodeSessionId(sessionId);
2957
- const existing = localSessionIdsByInstance.get(decoded.instanceId) ?? [];
2958
- existing.push(decoded.localId);
2959
- localSessionIdsByInstance.set(decoded.instanceId, existing);
3228
+ async function resolveAwaitTarget(args, registry) {
3229
+ const instanceArgs = optionalStringArray(args, "instances");
3230
+ const sessionIdArgs = optionalStringArray(args, "sessionIds");
3231
+ const localSessionIdsByInstance = /* @__PURE__ */ new Map();
3232
+ for (const sessionId of sessionIdArgs ?? []) {
3233
+ const decoded = decodeSessionId(sessionId);
3234
+ const existing = localSessionIdsByInstance.get(decoded.instanceId) ?? [];
3235
+ existing.push(decoded.localId);
3236
+ localSessionIdsByInstance.set(decoded.instanceId, existing);
3237
+ }
3238
+ let instances;
3239
+ if (instanceArgs !== void 0 && instanceArgs.length > 0) {
3240
+ instances = uniqueInstances(await Promise.all(instanceArgs.map((arg) => registry.resolveInstance(arg))));
3241
+ const ids = new Set(instances.map((instance) => instance.id));
3242
+ for (const instanceId of localSessionIdsByInstance.keys()) if (!ids.has(instanceId)) throw new FleetToolInputError("INSTANCE_MISMATCH", `sessionIds include instance ${JSON.stringify(instanceId)} which is not in arguments.instances`);
3243
+ } else if (localSessionIdsByInstance.size > 0) instances = uniqueInstances(await Promise.all([...localSessionIdsByInstance.keys()].map((instanceId) => registry.resolveInstance(instanceId))));
3244
+ else {
3245
+ const infos = await registry.listInstances();
3246
+ if (infos.length === 0) throw new FleetRegistryError("INSTANCE_REQUIRED", "await_turn requires at least one registered fleet instance");
3247
+ instances = uniqueInstances(await Promise.all(infos.map((info) => registry.resolveInstance(info.id))));
3248
+ }
3249
+ return {
3250
+ instances,
3251
+ localSessionIdsByInstance
3252
+ };
3253
+ }
3254
+ function globalizeSession(instanceId, session) {
3255
+ return {
3256
+ ...session,
3257
+ sessionId: encodeSessionId(instanceId, session.sessionId)
3258
+ };
3259
+ }
3260
+ function stampEvent(instance, event) {
3261
+ return {
3262
+ ...event,
3263
+ instance: publicInstance(instance),
3264
+ ...typeof event.sessionId === "string" ? { sessionId: encodeSessionId(instance.id, event.sessionId) } : {}
3265
+ };
3266
+ }
3267
+ function eventAtMs(value) {
3268
+ if (typeof value === "number" && Number.isFinite(value)) return value;
3269
+ if (typeof value === "string") {
3270
+ const parsed = Date.parse(value);
3271
+ if (!Number.isNaN(parsed)) return parsed;
3272
+ }
3273
+ return 0;
3274
+ }
3275
+ function compareStampedEvents(a, b) {
3276
+ const atA = eventAtMs(a.at);
3277
+ const atB = eventAtMs(b.at);
3278
+ if (atA !== atB) return atA - atB;
3279
+ return (typeof a.seq === "number" ? a.seq : 0) - (typeof b.seq === "number" ? b.seq : 0);
3280
+ }
3281
+ const MAX_WATCHER_ID_LEN = 200;
3282
+ const MAX_AWAIT_TURN_CURSOR_KEYS = 1024;
3283
+ function awaitTurnCursorKey(watcherId) {
3284
+ const id = watcherId ?? "default";
3285
+ return id.length > MAX_WATCHER_ID_LEN ? id.slice(0, MAX_WATCHER_ID_LEN) : id;
3286
+ }
3287
+ function takeAwaitTurnCursorMap(clientKey) {
3288
+ const existing = awaitTurnCursors.get(clientKey);
3289
+ if (existing) {
3290
+ awaitTurnCursors.delete(clientKey);
3291
+ awaitTurnCursors.set(clientKey, existing);
3292
+ return existing;
3293
+ }
3294
+ const created = /* @__PURE__ */ new Map();
3295
+ awaitTurnCursors.set(clientKey, created);
3296
+ while (awaitTurnCursors.size > MAX_AWAIT_TURN_CURSOR_KEYS) {
3297
+ const oldest = awaitTurnCursors.keys().next().value;
3298
+ if (oldest === void 0) break;
3299
+ awaitTurnCursors.delete(oldest);
3300
+ }
3301
+ return created;
3302
+ }
3303
+ function isAwaitTurnSuccess(result) {
3304
+ return result.ok;
3305
+ }
3306
+ function isAwaitTurnFailure(result) {
3307
+ return !result.ok;
3308
+ }
3309
+ function failedProbeResult(info, code) {
3310
+ const hint = fleetProbeHint(code);
3311
+ return {
3312
+ id: info.id,
3313
+ label: info.label,
3314
+ reachable: false,
3315
+ error: code,
3316
+ ...hint ? { hint } : {}
3317
+ };
3318
+ }
3319
+ async function mapWithConcurrency(items, limit, fn) {
3320
+ const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 1;
3321
+ const concurrency = Math.max(1, Math.min(items.length || 1, safeLimit));
3322
+ const results = new Array(items.length);
3323
+ let nextIndex = 0;
3324
+ async function worker() {
3325
+ while (nextIndex < items.length) {
3326
+ const index = nextIndex++;
3327
+ results[index] = await fn(items[index], index);
3328
+ }
3329
+ }
3330
+ await Promise.all(Array.from({ length: concurrency }, () => worker()));
3331
+ return results;
3332
+ }
3333
+ function fleetFanoutConcurrency(defaultLimit) {
3334
+ const raw = process.env[FLEET_FANOUT_CONCURRENCY_ENV];
3335
+ const parsed = raw === void 0 ? NaN : Number.parseInt(raw, 10);
3336
+ if (Number.isFinite(parsed) && parsed > 0) return Math.floor(parsed);
3337
+ return defaultLimit;
3338
+ }
3339
+ function probeRateLimitBackoffMs(attempt) {
3340
+ return Math.min(INSTANCE_PROBE_RATE_LIMIT_BACKOFF_BASE_MS * 2 ** attempt, INSTANCE_PROBE_RATE_LIMIT_BACKOFF_MAX_MS);
3341
+ }
3342
+ function nonNegativeNumberOrDefault(value, fallback) {
3343
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : fallback;
3344
+ }
3345
+ async function delay(ms) {
3346
+ if (ms <= 0) return;
3347
+ await new Promise((resolve) => setTimeout(resolve, ms));
3348
+ }
3349
+ function isAbortLike(err) {
3350
+ if (!(err instanceof Error)) return false;
3351
+ return err.name === "AbortError" || err.name === "TimeoutError";
3352
+ }
3353
+ function uniqueInstances(instances) {
3354
+ const seen = /* @__PURE__ */ new Set();
3355
+ const result = [];
3356
+ for (const instance of instances) {
3357
+ if (seen.has(instance.id)) continue;
3358
+ seen.add(instance.id);
3359
+ result.push(instance);
3360
+ }
3361
+ return result;
3362
+ }
3363
+ function publicInstance(instance) {
3364
+ return {
3365
+ id: instance.id,
3366
+ label: instance.label
3367
+ };
3368
+ }
3369
+ /**
3370
+ * Build the FleetClient tunnel-auth options for a resolved instance.
3371
+ * Resolution order: a `tunnelId` enables auto-mint + auto-refresh (and the
3372
+ * evict-on-failure hook); else a static `tunnelToken` is sent directly (no
3373
+ * retry, since it cannot be re-minted); else no tunnel auth.
3374
+ */
3375
+ function tunnelClientOptions(instance, provider) {
3376
+ if (instance.tunnelId) {
3377
+ const cfg = { tunnelId: instance.tunnelId };
3378
+ return {
3379
+ getTunnelToken: () => provider.getToken(cfg),
3380
+ onTunnelAuthInvalidate: () => provider.invalidate(cfg)
3381
+ };
3382
+ }
3383
+ if (instance.tunnelToken) {
3384
+ const token = instance.tunnelToken;
3385
+ return { getTunnelToken: async () => token };
3386
+ }
3387
+ return {};
3388
+ }
3389
+ function ok$1(value) {
3390
+ return jsonResult$1(value, false);
3391
+ }
3392
+ function jsonResult$1(value, isError) {
3393
+ return {
3394
+ content: [{
3395
+ type: "text",
3396
+ text: JSON.stringify(value)
3397
+ }],
3398
+ ...isError ? { isError: true } : {}
3399
+ };
3400
+ }
3401
+ function errorResult$1(err) {
3402
+ return jsonResult$1({ error: {
3403
+ code: errorCode$1(err),
3404
+ message: err instanceof Error ? err.message : String(err)
3405
+ } }, true);
3406
+ }
3407
+ function errorCode$1(err) {
3408
+ if (typeof err === "object" && err !== null && "code" in err) {
3409
+ const code = err.code;
3410
+ if (typeof code === "string") return code;
3411
+ }
3412
+ return "FLEET_ERROR";
3413
+ }
3414
+ function definedObject(input) {
3415
+ const result = {};
3416
+ for (const [key, value] of Object.entries(input)) if (value !== void 0) result[key] = value;
3417
+ return result;
3418
+ }
3419
+ function requiredString$1(args, key) {
3420
+ const value = args[key];
3421
+ if (typeof value !== "string" || value.trim() === "") throw new FleetToolInputError("INVALID_ARGUMENT", `arguments.${key} is required and must be a non-empty string`);
3422
+ return value;
3423
+ }
3424
+ function optionalString$1(args, key) {
3425
+ const value = args[key];
3426
+ if (value === void 0) return void 0;
3427
+ if (typeof value !== "string") throw new FleetToolInputError("INVALID_ARGUMENT", `arguments.${key} must be a string`);
3428
+ return value.trim() === "" ? void 0 : value;
3429
+ }
3430
+ function optionalNumber$1(args, key) {
3431
+ const value = args[key];
3432
+ if (value === void 0) return void 0;
3433
+ if (typeof value !== "number" || !Number.isFinite(value)) throw new FleetToolInputError("INVALID_ARGUMENT", `arguments.${key} must be a finite number`);
3434
+ return value;
3435
+ }
3436
+ function optionalBoolean(args, key) {
3437
+ const value = args[key];
3438
+ if (value === void 0) return void 0;
3439
+ if (typeof value !== "boolean") throw new FleetToolInputError("INVALID_ARGUMENT", `arguments.${key} must be a boolean`);
3440
+ return value;
3441
+ }
3442
+ function optionalStringArray(args, key) {
3443
+ const value = args[key];
3444
+ if (value === void 0) return void 0;
3445
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.trim() === "")) throw new FleetToolInputError("INVALID_ARGUMENT", `arguments.${key} must be an array of non-empty strings`);
3446
+ return value;
3447
+ }
3448
+ function objectSchema$1(properties, required) {
3449
+ return {
3450
+ type: "object",
3451
+ required,
3452
+ additionalProperties: false,
3453
+ properties
3454
+ };
3455
+ }
3456
+ function stringProp$1(description) {
3457
+ return {
3458
+ type: "string",
3459
+ description
3460
+ };
3461
+ }
3462
+ function numberProp$1(description) {
3463
+ return {
3464
+ type: "number",
3465
+ description
3466
+ };
3467
+ }
3468
+ function booleanProp(description) {
3469
+ return {
3470
+ type: "boolean",
3471
+ description
3472
+ };
3473
+ }
3474
+ function arrayProp(description) {
3475
+ return {
3476
+ type: "array",
3477
+ items: { type: "string" },
3478
+ description
3479
+ };
3480
+ }
3481
+
3482
+ //#endregion
3483
+ //#region src/lib/agent/types.ts
3484
+ var AgentError = class extends Error {
3485
+ code;
3486
+ cause;
3487
+ constructor(code, message, options) {
3488
+ super(message);
3489
+ this.name = "AgentError";
3490
+ this.code = code;
3491
+ this.cause = options?.cause;
3492
+ }
3493
+ };
3494
+
3495
+ //#endregion
3496
+ //#region src/lib/agent/rest.ts
3497
+ function restHeaders(apiVersion, extra) {
3498
+ const headers = githubAgentHeaders(state);
3499
+ if (apiVersion) headers["x-github-api-version"] = apiVersion;
3500
+ if (extra) Object.assign(headers, extra);
3501
+ return headers;
3502
+ }
3503
+ async function ghRestRaw(method, path$1, opts = {}) {
3504
+ const url = `${GITHUB_API_BASE_URL}${path$1}`;
3505
+ const requestInit = {
3506
+ method,
3507
+ headers: restHeaders(opts.apiVersion, opts.headers),
3508
+ signal: opts.signal
3509
+ };
3510
+ if (opts.body !== void 0) requestInit.body = JSON.stringify(opts.body);
3511
+ try {
3512
+ if (opts.retry === false) return await fetch(url, requestInit);
3513
+ return await fetchWithTransientRetry(() => fetch(url, requestInit), {
3514
+ label: `github-rest ${method} ${path$1}`,
3515
+ signal: opts.signal
3516
+ });
3517
+ } catch (err) {
3518
+ consola.warn(`GitHub REST ${method} ${path$1} failed before response`, err);
3519
+ throw new AgentError("UPSTREAM", `GitHub REST ${method} ${path$1} failed`, { cause: err });
3520
+ }
3521
+ }
3522
+ function rateLimitMessage(response) {
3523
+ const reset = response.headers.get("x-ratelimit-reset");
3524
+ const retryAfter = response.headers.get("retry-after");
3525
+ return `GitHub API rate limit exceeded${reset ? `; reset at ${reset}` : retryAfter ? `; retry after ${retryAfter}s` : ""}`;
3526
+ }
3527
+ function agentErrorFromResponse(response, message = `GitHub API request failed with HTTP ${response.status}`) {
3528
+ let code;
3529
+ let errorMessage = message;
3530
+ if (response.status === 401) {
3531
+ code = "AUTH_REVOKED";
3532
+ errorMessage = "GitHub agent token was revoked or is invalid";
3533
+ } else if (response.status === 403 && response.headers.get("x-ratelimit-remaining") === "0") {
3534
+ code = "RATE_LIMITED";
3535
+ errorMessage = rateLimitMessage(response);
3536
+ } else if (response.status === 403) {
3537
+ code = "NO_WRITE_ACCESS";
3538
+ errorMessage = "GitHub agent token does not have write access";
3539
+ } else if (response.status === 404) {
3540
+ code = "NOT_FOUND";
3541
+ errorMessage = "GitHub resource was not found";
3542
+ } else if (response.status === 429) {
3543
+ code = "RATE_LIMITED";
3544
+ errorMessage = rateLimitMessage(response);
3545
+ } else code = "UPSTREAM";
3546
+ return new AgentError(code, errorMessage, { cause: new HTTPError(errorMessage, response) });
3547
+ }
3548
+ async function parseJsonOrEmpty(response) {
3549
+ if (response.status === 204) return {};
3550
+ const text = await response.text();
3551
+ if (!text.trim()) return {};
3552
+ try {
3553
+ return JSON.parse(text);
3554
+ } catch (err) {
3555
+ throw new AgentError("UPSTREAM", "GitHub API returned invalid JSON", { cause: err });
3556
+ }
3557
+ }
3558
+ async function ghRest(method, path$1, opts = {}) {
3559
+ const response = await ghRestRaw(method, path$1, opts);
3560
+ if (!response.ok) throw agentErrorFromResponse(response);
3561
+ return parseJsonOrEmpty(response);
3562
+ }
3563
+
3564
+ //#endregion
3565
+ //#region src/lib/agent/graphql.ts
3566
+ function graphQLErrorMessage(error) {
3567
+ return String(error.message ?? error.type ?? error.extensions?.code ?? "GraphQL error");
3568
+ }
3569
+ function isFeatureError(error) {
3570
+ const message = String(error.message ?? "").toLowerCase();
3571
+ const type = String(error.type ?? "").toLowerCase();
3572
+ const code = String(error.extensions?.code ?? "").toLowerCase();
3573
+ const haystack = `${message} ${type} ${code}`;
3574
+ if (haystack.includes("unknown feature") || haystack.includes("disabled feature") || haystack.includes("feature flag") || haystack.includes("graphql-features") || haystack.includes("issues_copilot_assignment_api_support") || haystack.includes("does not exist on type") || haystack.includes("cannot query field") || haystack.includes("undefined field")) return true;
3575
+ return (type.includes("forbidden") || code.includes("forbidden")) && (message.includes("feature") || message.includes("preview") || message.includes("copilot assignment"));
3576
+ }
3577
+ async function parseGraphQLJson(response) {
3578
+ const text = await response.text();
3579
+ if (!text.trim()) return {};
3580
+ try {
3581
+ return JSON.parse(text);
3582
+ } catch (err) {
3583
+ throw new AgentError("UPSTREAM", "GitHub GraphQL returned invalid JSON", { cause: err });
3584
+ }
3585
+ }
3586
+ async function ghGraphQL(query, variables, opts = {}) {
3587
+ let response;
3588
+ try {
3589
+ response = await fetchWithTransientRetry(() => fetch(GITHUB_GRAPHQL_URL, {
3590
+ method: "POST",
3591
+ headers: githubAgentGraphQLHeaders(state, opts.features),
3592
+ body: JSON.stringify({
3593
+ query,
3594
+ variables
3595
+ }),
3596
+ signal: opts.signal
3597
+ }), {
3598
+ label: "github-graphql",
3599
+ signal: opts.signal
3600
+ });
3601
+ } catch (err) {
3602
+ consola.warn("GitHub GraphQL failed before response", err);
3603
+ throw new AgentError("UPSTREAM", "GitHub GraphQL request failed", { cause: err });
3604
+ }
3605
+ if (!response.ok) throw agentErrorFromResponse(response, "GitHub GraphQL request failed");
3606
+ const payload = await parseGraphQLJson(response);
3607
+ const errors = payload.errors?.filter(Boolean) ?? [];
3608
+ if (errors.length > 0) {
3609
+ const message = errors.map(graphQLErrorMessage).join("; ");
3610
+ if (errors.some(isFeatureError)) throw new AgentError("GRAPHQL_FEATURE", message);
3611
+ throw new AgentError("UPSTREAM", message);
3612
+ }
3613
+ if (payload.data === void 0) throw new AgentError("UPSTREAM", "GitHub GraphQL response did not include data");
3614
+ return payload.data;
3615
+ }
3616
+
3617
+ //#endregion
3618
+ //#region src/lib/agent/service.ts
3619
+ const CACHE_TTL_MS = 300 * 1e3;
3620
+ const CHECK_SUMMARY_LIMIT = 20;
3621
+ const FAILING_CHECK_LIMIT = 5;
3622
+ const AGENT_LOGIN_MATCHERS = {
3623
+ copilot: /^copilot(-swe-agent)?$/i,
3624
+ anthropic: /^anthropic-code-agent$/i,
3625
+ openai: /^openai-code-agent$/i
3626
+ };
3627
+ const rosterCache = /* @__PURE__ */ new Map();
3628
+ function repoCacheKey(repo) {
3629
+ return `${repo.owner.toLowerCase()}/${repo.repo.toLowerCase()}`;
3630
+ }
3631
+ function cached$1(entry) {
3632
+ if (!entry) return void 0;
3633
+ if (Date.now() - entry.timestamp > CACHE_TTL_MS) return void 0;
3634
+ return entry.value;
3635
+ }
3636
+ function segment$1(value) {
3637
+ return encodeURIComponent(String(value));
3638
+ }
3639
+ function repoPath(repo) {
3640
+ return `/repos/${segment$1(repo.owner)}/${segment$1(repo.repo)}`;
3641
+ }
3642
+ function botAssigneeLogin(login) {
3643
+ return login.toLowerCase().endsWith("[bot]") ? login : `${login}[bot]`;
3644
+ }
3645
+ /** Classify a GitHub login to an agent key via the roster matchers, or null. */
3646
+ function agentKeyForLogin(login) {
3647
+ const normalized = login.replace(/\[bot\]$/i, "");
3648
+ for (const [key, matcher] of Object.entries(AGENT_LOGIN_MATCHERS)) if (matcher.test(normalized)) return key;
3649
+ return null;
3650
+ }
3651
+ function authorMatchesBot(authorLogin, botLogin) {
3652
+ if (!authorLogin) return false;
3653
+ const author = authorLogin.toLowerCase();
3654
+ if (author === botLogin.toLowerCase() || author === botAssigneeLogin(botLogin).toLowerCase()) return true;
3655
+ const authorKey = agentKeyForLogin(authorLogin);
3656
+ return authorKey !== null && authorKey === agentKeyForLogin(botLogin);
3657
+ }
3658
+ function asRecord$7(value) {
3659
+ return typeof value === "object" && value !== null ? value : void 0;
3660
+ }
3661
+ function stringValue$2(value) {
3662
+ return typeof value === "string" ? value : void 0;
3663
+ }
3664
+ async function readJsonObject(response) {
3665
+ const text = await response.text();
3666
+ if (!text.trim()) return {};
3667
+ try {
3668
+ return asRecord$7(JSON.parse(text)) ?? {};
3669
+ } catch (err) {
3670
+ throw new AgentError("UPSTREAM", "GitHub API returned invalid JSON", { cause: err });
3671
+ }
3672
+ }
3673
+ async function resolveAgentRoster(repo) {
3674
+ const key = repoCacheKey(repo);
3675
+ const cacheEntry = rosterCache.get(key);
3676
+ const roster = cached$1(cacheEntry ? {
3677
+ timestamp: cacheEntry.timestamp,
3678
+ value: cacheEntry.roster
3679
+ } : void 0);
3680
+ if (roster) return roster;
3681
+ const data = await ghGraphQL(`query FirstMateSuggestedActors($owner: String!, $name: String!) {
3682
+ repository(owner: $owner, name: $name) {
3683
+ suggestedActors(capabilities: [CAN_BE_ASSIGNED], first: 100) {
3684
+ nodes {
3685
+ login
3686
+ __typename
3687
+ ... on Bot {
3688
+ id
3689
+ }
3690
+ }
3691
+ }
3692
+ }
3693
+ }`, {
3694
+ owner: repo.owner,
3695
+ name: repo.repo
3696
+ });
3697
+ const nextRoster = /* @__PURE__ */ new Map();
3698
+ for (const node of data.repository?.suggestedActors?.nodes ?? []) {
3699
+ if (!node.login || !node.id || node.__typename !== "Bot") continue;
3700
+ for (const [agentKey, matcher] of Object.entries(AGENT_LOGIN_MATCHERS)) {
3701
+ if (!matcher.test(node.login)) continue;
3702
+ if (!nextRoster.has(agentKey)) nextRoster.set(agentKey, {
3703
+ login: node.login,
3704
+ botId: node.id
3705
+ });
3706
+ }
3707
+ }
3708
+ rosterCache.set(key, {
3709
+ timestamp: Date.now(),
3710
+ roster: nextRoster
3711
+ });
3712
+ return nextRoster;
3713
+ }
3714
+ async function resolveAgentActor(repo, key) {
3715
+ const roster = await resolveAgentRoster(repo);
3716
+ const actor = roster.get(key);
3717
+ if (actor) return actor;
3718
+ const available = [...roster.keys()].join(", ") || "none";
3719
+ throw new AgentError("AGENT_NOT_AVAILABLE", `Agent ${key} is not available for ${repo.owner}/${repo.repo}; available: ${available}`);
3720
+ }
3721
+ async function createIssue(repo, input) {
3722
+ const issue = await ghRest("POST", `${repoPath(repo)}/issues`, { body: {
3723
+ title: input.title,
3724
+ body: input.body
3725
+ } });
3726
+ return {
3727
+ number: issue.number ?? 0,
3728
+ nodeId: issue.node_id ?? "",
3729
+ url: issue.html_url ?? issue.url ?? ""
3730
+ };
3731
+ }
3732
+ async function assignAgent(repo, input) {
3733
+ try {
3734
+ await ghGraphQL(`mutation FirstMateAssignAgent($issueNodeId: ID!, $botId: ID!) {
3735
+ replaceActorsForAssignable(input: {
3736
+ assignableId: $issueNodeId,
3737
+ actorIds: [$botId]
3738
+ }) {
3739
+ assignable {
3740
+ ... on Issue {
3741
+ id
3742
+ }
3743
+ }
3744
+ }
3745
+ }`, {
3746
+ issueNodeId: input.issueNodeId,
3747
+ botId: input.botId
3748
+ }, { features: "issues_copilot_assignment_api_support" });
3749
+ return {
3750
+ assigned: true,
3751
+ via: "graphql"
3752
+ };
3753
+ } catch (graphqlErr) {
3754
+ const reason = graphqlErr instanceof AgentError ? graphqlErr.code : "unknown";
3755
+ consola.debug(`GraphQL assignment failed (${reason}); trying REST fallback`);
3756
+ try {
3757
+ await ghRest("POST", `${repoPath(repo)}/issues/${segment$1(input.issueNumber)}/assignees`, { body: { assignees: [botAssigneeLogin(input.botLogin)] } });
3758
+ return {
3759
+ assigned: true,
3760
+ via: "rest"
3761
+ };
3762
+ } catch (restErr) {
3763
+ throw new AgentError("ASSIGN_FAILED", "Failed to assign agent to issue", { cause: {
3764
+ graphqlErr,
3765
+ restErr
3766
+ } });
3767
+ }
3768
+ }
3769
+ }
3770
+ async function findAgentPRs(repo, input) {
3771
+ input.issueNumber;
3772
+ return (await ghRest("GET", `${repoPath(repo)}/pulls?state=all&per_page=100`)).filter((pull) => {
3773
+ if (input.branch !== void 0 && input.branch.length > 0) return pull.head?.ref === input.branch;
3774
+ return authorMatchesBot(pull.user?.login ?? void 0, input.botLogin);
3775
+ }).map((pull) => ({
3776
+ number: pull.number ?? 0,
3777
+ headSha: pull.head?.sha ?? "",
3778
+ headRef: pull.head?.ref ?? "",
3779
+ isDraft: pull.draft ?? false
3780
+ }));
3781
+ }
3782
+ const COPILOT_REVIEWER_LOGIN = "copilot-pull-request-reviewer[bot]";
3783
+ /**
3784
+ * Request a code review from `reviewerLogin` on a PR. Best-effort: a 422
3785
+ * (already requested / not a collaborator) or any other error is swallowed and
3786
+ * reported as `requested:false` — a failed review request must not abort the
3787
+ * controller sweep.
3788
+ */
3789
+ async function requestReview(repo, pr, reviewerLogin) {
3790
+ try {
3791
+ await ghRest("POST", `${repoPath(repo)}/pulls/${segment$1(pr)}/requested_reviewers`, { body: { reviewers: [reviewerLogin] } });
3792
+ return { requested: true };
3793
+ } catch (err) {
3794
+ consola.debug(`first-mate: requestReview(${reviewerLogin}) on PR #${pr} skipped:`, err);
3795
+ return { requested: false };
3796
+ }
3797
+ }
3798
+ const REVIEW_BODY_LIMIT = 4e3;
3799
+ /** Compact review summaries for a PR (author + state + hard-truncated body). */
3800
+ async function getPullRequestReviews(repo, pr) {
3801
+ return (await ghRest("GET", `${repoPath(repo)}/pulls/${segment$1(pr)}/reviews?per_page=50`) ?? []).map((review) => ({
3802
+ author: review.user?.login ?? "",
3803
+ state: review.state ?? "",
3804
+ bodyExcerpt: (review.body ?? "").slice(0, REVIEW_BODY_LIMIT),
3805
+ ...review.submitted_at ? { submittedAt: review.submitted_at } : {},
3806
+ ...review.commit_id ? { commitId: review.commit_id } : {}
3807
+ }));
3808
+ }
3809
+ async function getPullRequestState(repo, pr) {
3810
+ const pullRequest = (await ghGraphQL(`query FirstMatePullRequestState($owner: String!, $name: String!, $number: Int!) {
3811
+ repository(owner: $owner, name: $name) {
3812
+ pullRequest(number: $number) {
3813
+ id
3814
+ number
3815
+ title
3816
+ isDraft
3817
+ state
3818
+ mergeable
3819
+ reviewDecision
3820
+ headRefOid
3821
+ baseRefName
3822
+ baseRefOid
3823
+ author {
3824
+ login
3825
+ }
3826
+ }
3827
+ }
3828
+ }`, {
3829
+ owner: repo.owner,
3830
+ name: repo.repo,
3831
+ number: pr
3832
+ })).repository?.pullRequest;
3833
+ if (!pullRequest) throw new AgentError("NOT_FOUND", `Pull request #${pr} was not found`);
3834
+ let baseSha = pullRequest.baseRefOid ?? void 0;
3835
+ let baseRef = pullRequest.baseRefName ?? "";
3836
+ if (!baseSha || !baseRef) {
3837
+ const restPull = await ghRest("GET", `${repoPath(repo)}/pulls/${segment$1(pr)}`);
3838
+ baseSha = baseSha ?? restPull.base?.sha ?? void 0;
3839
+ baseRef = baseRef || restPull.base?.ref || "";
3840
+ }
3841
+ return {
3842
+ number: pullRequest.number ?? pr,
3843
+ title: pullRequest.title ?? "",
3844
+ isDraft: pullRequest.isDraft ?? false,
3845
+ state: pullRequest.state ?? "UNKNOWN",
3846
+ mergeable: pullRequest.mergeable,
3847
+ reviewDecision: pullRequest.reviewDecision,
3848
+ headSha: pullRequest.headRefOid ?? "",
3849
+ baseRef,
3850
+ baseSha,
3851
+ authorLogin: pullRequest.author?.login ?? void 0,
3852
+ nodeId: pullRequest.id ?? void 0
3853
+ };
3854
+ }
3855
+ function isFailingConclusion(conclusion) {
3856
+ return [
3857
+ "action_required",
3858
+ "cancelled",
3859
+ "failure",
3860
+ "startup_failure",
3861
+ "timed_out"
3862
+ ].includes(conclusion ?? "");
3863
+ }
3864
+ async function getRequiredChecksForSha(repo, sha) {
3865
+ const checkRuns = ((await ghRest("GET", `${repoPath(repo)}/commits/${segment$1(sha)}/check-runs`)).check_runs ?? []).filter((check) => !/pull-request-reviewer/i.test(check.name ?? ""));
3866
+ const runningCount = checkRuns.filter((check) => check.status !== "completed" || !check.conclusion).length;
3867
+ const failingRuns = checkRuns.filter((check) => isFailingConclusion(check.conclusion));
3868
+ let rollup = "none";
3869
+ if (checkRuns.length > 0) if (failingRuns.length > 0) rollup = "failing";
3870
+ else if (runningCount > 0) rollup = "pending";
3871
+ else rollup = "passing";
3872
+ const checks = checkRuns.slice(0, CHECK_SUMMARY_LIMIT).map((check) => ({
3873
+ id: check.id ?? 0,
3874
+ name: check.name ?? "",
3875
+ conclusion: check.conclusion
3876
+ }));
3877
+ const failing = failingRuns.slice(0, FAILING_CHECK_LIMIT).map((check) => ({
3878
+ name: check.name ?? "",
3879
+ url: check.html_url ?? check.details_url ?? void 0
3880
+ }));
3881
+ return {
3882
+ rollup,
3883
+ checks,
3884
+ failing,
3885
+ runningCount
3886
+ };
3887
+ }
3888
+ const workflowCache = /* @__PURE__ */ new Map();
3889
+ /**
3890
+ * Whether the repo has any GitHub Actions workflow on `ref`. Lets the
3891
+ * controller distinguish "genuinely no CI" (route to cross-lab verify) from
3892
+ * "CI configured but checks not registered yet" (keep waiting) when a commit's
3893
+ * check-run rollup is "none". Cached per repo+ref; a 404 (no dir) means no CI.
3894
+ */
3895
+ async function repoHasWorkflows(repo, ref) {
3896
+ const key = `${repoPath(repo)}@${ref}`;
3897
+ const hit = cached$1(workflowCache.get(key));
3898
+ if (hit !== void 0) return hit;
3899
+ let value = false;
3900
+ try {
3901
+ const entries = await ghRest("GET", `${repoPath(repo)}/contents/.github/workflows?ref=${segment$1(ref)}`);
3902
+ value = Array.isArray(entries) && entries.some((entry) => entry.type === "file" && /\.ya?ml$/i.test(entry.name ?? ""));
3903
+ } catch (err) {
3904
+ if (!(err instanceof AgentError && err.code === "NOT_FOUND")) consola.debug("first-mate: workflow probe failed, assuming no CI:", err);
3905
+ value = false;
3906
+ }
3907
+ workflowCache.set(key, {
3908
+ timestamp: Date.now(),
3909
+ value
3910
+ });
3911
+ return value;
3912
+ }
3913
+ async function postComment(repo, number, body) {
3914
+ const comment = await ghRest("POST", `${repoPath(repo)}/issues/${segment$1(number)}/comments`, { body: { body } });
3915
+ return { url: comment.html_url ?? comment.url ?? "" };
3916
+ }
3917
+ async function submitReview(repo, pr, event, body) {
3918
+ const requestBody = { event };
3919
+ if (body !== void 0) requestBody.body = body;
3920
+ const review = await ghRest("POST", `${repoPath(repo)}/pulls/${segment$1(pr)}/reviews`, { body: requestBody });
3921
+ return {
3922
+ reviewId: review.id ?? 0,
3923
+ state: review.state ?? ""
3924
+ };
3925
+ }
3926
+ async function rerunChecks(repo, input) {
3927
+ const suffix = input.failedOnly ? "rerun-failed-jobs" : "rerun";
3928
+ const response = await ghRestRaw("POST", `${repoPath(repo)}/actions/runs/${segment$1(input.runId)}/${suffix}`);
3929
+ if (!response.ok) throw agentErrorFromResponse(response, "Rerun checks failed");
3930
+ return { rerun: true };
3931
+ }
3932
+ async function mergePullRequest(repo, input) {
3933
+ const response = await ghRestRaw("PUT", `${repoPath(repo)}/pulls/${segment$1(input.pr)}/merge`, { body: {
3934
+ merge_method: input.method ?? "squash",
3935
+ sha: input.expectedHeadSha
3936
+ } });
3937
+ if (response.status === 405 || response.status === 409) throw new AgentError("HEAD_MOVED", "Pull request head moved or is not mergeable");
3938
+ if (!response.ok) throw agentErrorFromResponse(response, "Pull request merge failed");
3939
+ const result = await readJsonObject(response);
3940
+ if (result.merged === false) throw new AgentError("UPSTREAM", stringValue$2(result.message) ?? "GitHub did not merge the pull request");
3941
+ return {
3942
+ merged: true,
3943
+ sha: stringValue$2(result.sha) ?? ""
3944
+ };
3945
+ }
3946
+ async function markReadyForReview(prNodeId) {
3947
+ await ghGraphQL(`mutation FirstMateReadyForReview($pullRequestId: ID!) {
3948
+ markPullRequestReadyForReview(input: { pullRequestId: $pullRequestId }) {
3949
+ pullRequest {
3950
+ id
3951
+ }
3952
+ }
3953
+ }`, { pullRequestId: prNodeId });
3954
+ return { ready: true };
3955
+ }
3956
+
3957
+ //#endregion
3958
+ //#region src/lib/agent/capi.ts
3959
+ const CAPI_INTEGRATION_ID = "copilot-4-cli";
3960
+ const CAPI_API_VERSION = "2026-01-09";
3961
+ const HOST_CACHE_TTL_MS = 600 * 1e3;
3962
+ const LOG_EXCERPT_LIMIT$1 = 4e3;
3963
+ const TRUNCATED_MARKER$1 = "…[truncated]…";
3964
+ const MAX_LOG_BYTES = 4 * 1024 * 1024;
3965
+ const MAX_FIELD_CHARS = 256 * 1024;
3966
+ let hostCache = null;
3967
+ async function discoverCapiHost(signal) {
3968
+ const now = Date.now();
3969
+ if (hostCache && now - hostCache.at < HOST_CACHE_TTL_MS) return hostCache.host;
3970
+ try {
3971
+ const host = (await ghGraphQL("query CopilotEndpoints { viewer { copilotEndpoints { api } } }", {}, { signal })).viewer?.copilotEndpoints?.api;
3972
+ if (typeof host === "string" && host.length > 0) {
3973
+ const normalized = host.replace(/\/+$/, "");
3974
+ if (!isAllowedCopilotHost(normalized)) {
3975
+ consola.debug(`first-mate capi: discovered host not allowlisted: ${normalized}`);
3976
+ return null;
3977
+ }
3978
+ hostCache = {
3979
+ host: normalized,
3980
+ at: now
3981
+ };
3982
+ return hostCache.host;
3983
+ }
3984
+ consola.debug("first-mate capi: discovery returned no host");
3985
+ } catch (err) {
3986
+ consola.debug("first-mate capi: host discovery failed:", err);
3987
+ }
3988
+ return null;
3989
+ }
3990
+ function capiHeaders() {
3991
+ return {
3992
+ authorization: `Bearer ${state.githubAgentToken ?? ""}`,
3993
+ "copilot-integration-id": CAPI_INTEGRATION_ID,
3994
+ "x-github-api-version": CAPI_API_VERSION,
3995
+ "user-agent": "github-router-first-mate"
3996
+ };
3997
+ }
3998
+ function asRecord$6(value) {
3999
+ return typeof value === "object" && value !== null ? value : void 0;
4000
+ }
4001
+ function truncateHead(text) {
4002
+ if (text.length <= LOG_EXCERPT_LIMIT$1) return text;
4003
+ return `${text.slice(0, LOG_EXCERPT_LIMIT$1 - 13)}${TRUNCATED_MARKER$1}`;
4004
+ }
4005
+ const PLAN_LIMIT = 2800;
4006
+ const PROGRESS_LIMIT = 900;
4007
+ function headTruncate(text, limit) {
4008
+ if (text.length <= limit) return text;
4009
+ return `${text.slice(0, limit - 13)}${TRUNCATED_MARKER$1}`;
4010
+ }
4011
+ function tailTruncate(text, limit) {
4012
+ if (text.length <= limit) return text;
4013
+ return `${TRUNCATED_MARKER$1}${text.slice(-(limit - 13))}`;
4014
+ }
4015
+ /** Extract a `<plan>…</plan>` block (the plan-mode agent wraps its plan in it). */
4016
+ function extractPlanBlock(text) {
4017
+ const inner = /<plan>([\s\S]*?)<\/plan>/i.exec(text)?.[1]?.trim();
4018
+ return inner && inner.length > 0 ? inner : null;
4019
+ }
4020
+ /** Extract the branch the agent checked out ("checked out branch X"). */
4021
+ function extractBranch(text) {
4022
+ const branch = /checked out branch\s+([a-z0-9][a-z0-9._/-]*)/.exec(text)?.[1]?.replace(/[.\-_/]+$/, "");
4023
+ return branch && branch.length > 0 ? branch : void 0;
4024
+ }
4025
+ /**
4026
+ * Drop the agent's MCP-server registration preamble. Every cloud-agent session
4027
+ * emits a large, signal-free tool-registration dump ("MCP server started
4028
+ * successfully with N tools", "- github-mcp-server/actions_get", …) at the
4029
+ * START, which would otherwise dominate a head-truncated excerpt and bury the
4030
+ * actual plan/progress that lands at the tail.
4031
+ */
4032
+ function stripMcpBoilerplate(text) {
4033
+ return text.split("\n").filter((line) => {
4034
+ const l = line.trim();
4035
+ if (/^MCP server started successfully/i.test(l)) return false;
4036
+ if (/^-\s+(runtime-tools|github-mcp-server)\//i.test(l)) return false;
4037
+ return true;
4038
+ }).join("\n");
4039
+ }
4040
+ /** Append with a per-field cap; once at the cap, further growth is dropped. */
4041
+ function capped(current, addition) {
4042
+ if (current.length >= MAX_FIELD_CHARS) return current;
4043
+ const room = MAX_FIELD_CHARS - current.length;
4044
+ return current + (addition.length > room ? addition.slice(0, room) : addition);
4045
+ }
4046
+ /**
4047
+ * Read a response body as text, capped at `MAX_LOG_BYTES`. Stops pulling once
4048
+ * the cap is reached (a runaway/untrusted stream must not exhaust memory).
4049
+ */
4050
+ async function readCappedText(response) {
4051
+ const body = response.body;
4052
+ if (!body) return response.text();
4053
+ const reader = body.getReader();
4054
+ const decoder = new TextDecoder();
4055
+ let text = "";
4056
+ try {
4057
+ for (;;) {
4058
+ const { done, value } = await reader.read();
4059
+ if (done) break;
4060
+ text += decoder.decode(value, { stream: true });
4061
+ if (text.length >= MAX_LOG_BYTES) {
4062
+ reader.cancel().catch(() => {});
4063
+ break;
4064
+ }
4065
+ }
4066
+ text += decoder.decode();
4067
+ } catch (err) {
4068
+ consola.debug("first-mate capi: capped read interrupted:", err);
4069
+ }
4070
+ return text.length > MAX_LOG_BYTES ? text.slice(0, MAX_LOG_BYTES) : text;
4071
+ }
4072
+ /**
4073
+ * Parse the SSE body of a session-log stream into a compact excerpt. Tolerant
4074
+ * of partial/incomplete streams (an in-progress agent) and SSE-compliant for
4075
+ * multi-line `data:` events (a single event's `data:` lines are joined with
4076
+ * `\n` before parsing). `finished` reflects whether a terminal chunk was seen.
4077
+ * All log content is UNTRUSTED agent text.
4078
+ */
4079
+ function parseSessionLog(body) {
4080
+ let content = "";
4081
+ let reasoning = "";
4082
+ let finished = false;
4083
+ const toolNames = [];
4084
+ const toolCalls = /* @__PURE__ */ new Map();
4085
+ const events$1 = body.replace(/\r\n/g, "\n").split(/\n\n+/);
4086
+ for (const event of events$1) {
4087
+ const dataLines = [];
4088
+ for (const rawLine of event.split("\n")) {
4089
+ const line = rawLine.trimEnd();
4090
+ if (line.startsWith("data:")) dataLines.push(line.slice(5).replace(/^ /, ""));
4091
+ }
4092
+ if (dataLines.length === 0) continue;
4093
+ const payload = dataLines.join("\n").trim();
4094
+ if (!payload || payload === "[DONE]") continue;
4095
+ let chunk;
4096
+ try {
4097
+ chunk = asRecord$6(JSON.parse(payload));
4098
+ } catch {
4099
+ continue;
4100
+ }
4101
+ if (!chunk || chunk.object !== "chat.completion.chunk") continue;
4102
+ const choices = Array.isArray(chunk.choices) ? chunk.choices : [];
4103
+ for (const choiceValue of choices) {
4104
+ const choice = asRecord$6(choiceValue);
4105
+ if (!choice) continue;
4106
+ const delta = asRecord$6(choice.delta) ?? {};
4107
+ if (typeof delta.content === "string") content = capped(content, delta.content);
4108
+ if (typeof delta.reasoning_text === "string") reasoning = capped(reasoning, delta.reasoning_text);
4109
+ if (choice.finish_reason === "stop") finished = true;
4110
+ const toolCallDeltas = Array.isArray(delta.tool_calls) ? delta.tool_calls : [];
4111
+ for (const tcValue of toolCallDeltas) {
4112
+ const call = asRecord$6(tcValue);
4113
+ if (!call) continue;
4114
+ const index = typeof call.index === "number" ? call.index : toolCalls.size;
4115
+ const fn = asRecord$6(call.function) ?? {};
4116
+ const existing = toolCalls.get(index) ?? { args: "" };
4117
+ if (typeof fn.name === "string" && fn.name.length > 0) {
4118
+ existing.name = fn.name;
4119
+ toolNames.push(fn.name);
4120
+ }
4121
+ if (typeof fn.arguments === "string") existing.args = capped(existing.args, fn.arguments);
4122
+ toolCalls.set(index, existing);
4123
+ }
4124
+ }
4125
+ }
4126
+ let planDescription = "";
4127
+ for (const call of toolCalls.values()) {
4128
+ if (call.name !== "report_progress") continue;
4129
+ try {
4130
+ const parsed = asRecord$6(JSON.parse(call.args));
4131
+ const desc = parsed?.prDescription ?? parsed?.pr_description;
4132
+ if (typeof desc === "string" && desc.length > planDescription.length) planDescription = desc;
4133
+ } catch {}
4134
+ }
4135
+ const uniqueTools = [...new Set(toolNames)];
4136
+ const planBlock = extractPlanBlock(content);
4137
+ const plan = planDescription.trim() || planBlock || "";
4138
+ const progress = stripMcpBoilerplate(planBlock ? content.replace(/<plan>[\s\S]*?<\/plan>/i, "") : content).trim();
4139
+ const parts = [];
4140
+ if (plan) parts.push(`Plan:\n${headTruncate(plan, PLAN_LIMIT)}`);
4141
+ if (reasoning.trim()) parts.push(`Reasoning:\n${tailTruncate(reasoning.trim(), PROGRESS_LIMIT)}`);
4142
+ if (progress) parts.push(`Progress:\n${tailTruncate(progress, PROGRESS_LIMIT)}`);
4143
+ if (uniqueTools.length > 0) parts.push(`Tools: ${uniqueTools.join(", ")}`);
4144
+ const branch = extractBranch(content);
4145
+ return {
4146
+ excerpt: truncateHead(parts.join("\n\n")),
4147
+ finished,
4148
+ tools: uniqueTools,
4149
+ ...branch ? { branch } : {}
4150
+ };
4151
+ }
4152
+ /**
4153
+ * Fetch and distil a cloud-agent session log. Best-effort: returns `null`
4154
+ * (never throws) when the agent token is absent, the host can't be discovered
4155
+ * or validated, or the request fails — callers fall back to the
4156
+ * `api.github.com` task text.
4157
+ */
4158
+ async function getSessionLog(sessionId, signal) {
4159
+ if (!state.githubAgentToken || !sessionId) return null;
4160
+ const host = await discoverCapiHost(signal);
4161
+ if (!host) return null;
4162
+ const url = `${host}/agents/sessions/${encodeURIComponent(sessionId)}/logs`;
4163
+ let response;
4164
+ try {
4165
+ response = await fetchWithTransientRetry(() => fetch(url, {
4166
+ headers: capiHeaders(),
4167
+ signal,
4168
+ redirect: "error"
4169
+ }), {
4170
+ label: `capi session-log ${sessionId}`,
4171
+ signal
4172
+ });
4173
+ } catch (err) {
4174
+ consola.debug("first-mate capi: session-log fetch failed:", err);
4175
+ return null;
4176
+ }
4177
+ if (!response.ok) {
4178
+ consola.debug(`first-mate capi: session-log ${sessionId} → HTTP ${response.status}`);
4179
+ return null;
4180
+ }
4181
+ try {
4182
+ return parseSessionLog(await readCappedText(response));
4183
+ } catch (err) {
4184
+ consola.debug("first-mate capi: session-log parse failed:", err);
4185
+ return null;
4186
+ }
4187
+ }
4188
+
4189
+ //#endregion
4190
+ //#region src/lib/agent/tasks.ts
4191
+ const AGENT_TASKS_API_VERSION = "2026-03-10";
4192
+ const LOG_EXCERPT_LIMIT = 4e3;
4193
+ const TRUNCATED_MARKER = "…[truncated]…";
4194
+ const FOLLOW_UP_TASK_PATH_SUFFIX = "";
4195
+ const CANCEL_TASK_PATH_SUFFIX = "/cancel";
4196
+ function segment(value) {
4197
+ return encodeURIComponent(String(value));
4198
+ }
4199
+ function repoTasksPath(repo) {
4200
+ return `/agents/repos/${segment(repo.owner)}/${segment(repo.repo)}/tasks`;
4201
+ }
4202
+ function taskPath(repo, taskId) {
4203
+ return `${repoTasksPath(repo)}/${segment(taskId)}`;
4204
+ }
4205
+ function asRecord$5(value) {
4206
+ return typeof value === "object" && value !== null ? value : void 0;
4207
+ }
4208
+ function stringField(record, keys) {
4209
+ for (const key of keys) {
4210
+ const value = record?.[key];
4211
+ if (typeof value === "string" && value.length > 0) return value;
4212
+ }
4213
+ }
4214
+ function numberField(record, keys) {
4215
+ for (const key of keys) {
4216
+ const value = record?.[key];
4217
+ if (typeof value === "number" && Number.isFinite(value)) return value;
4218
+ }
4219
+ }
4220
+ function collectText(record) {
4221
+ if (!record) return [];
4222
+ const nested = [
4223
+ asRecord$5(record.task),
4224
+ asRecord$5(record.session),
4225
+ asRecord$5(record.progress),
4226
+ asRecord$5(record.result)
4227
+ ];
4228
+ const textKeys = [
4229
+ "session_log",
4230
+ "sessionLog",
4231
+ "log",
4232
+ "logs",
4233
+ "progress",
4234
+ "plan",
4235
+ "summary",
4236
+ "status_text",
4237
+ "statusText",
4238
+ "message"
4239
+ ];
4240
+ const chunks = [];
4241
+ for (const candidate of [record, ...nested]) for (const key of textKeys) {
4242
+ const value = candidate?.[key];
4243
+ if (typeof value === "string" && value.trim().length > 0) chunks.push(value);
4244
+ }
4245
+ return chunks;
4246
+ }
4247
+ function compactKnownKeysSummary(record) {
4248
+ if (!record) return "preview task response was not an object";
4249
+ const keys = Object.keys(record).slice(0, 30);
4250
+ return `preview task response keys: ${JSON.stringify(keys)}`;
4251
+ }
4252
+ function tailExcerpt(text) {
4253
+ if (text.length <= LOG_EXCERPT_LIMIT) return text;
4254
+ return `${TRUNCATED_MARKER}${text.slice(-(LOG_EXCERPT_LIMIT - 13))}`;
4255
+ }
4256
+ function taskPrUrl(record) {
4257
+ const direct = stringField(record, [
4258
+ "pr_url",
4259
+ "prUrl",
4260
+ "pull_request_url",
4261
+ "pullRequestUrl"
4262
+ ]);
4263
+ if (direct) return direct;
4264
+ return stringField(asRecord$5(record?.pull_request) ?? asRecord$5(record?.pullRequest), ["html_url", "url"]);
4265
+ }
4266
+ function taskPrNumber$1(record) {
4267
+ const direct = numberField(record, [
4268
+ "pr",
4269
+ "pr_number",
4270
+ "prNumber",
4271
+ "pull_request_number"
4272
+ ]);
4273
+ if (direct !== void 0) return direct;
4274
+ return numberField(asRecord$5(record?.pull_request) ?? asRecord$5(record?.pullRequest), ["number"]) ?? void 0;
4275
+ }
4276
+ function latestSessionId(record) {
4277
+ const sessions$1 = record?.sessions;
4278
+ if (!Array.isArray(sessions$1) || sessions$1.length === 0) return void 0;
4279
+ for (let i = sessions$1.length - 1; i >= 0; i -= 1) {
4280
+ const id = stringField(asRecord$5(sessions$1[i]), [
4281
+ "id",
4282
+ "session_id",
4283
+ "sessionId"
4284
+ ]);
4285
+ if (id) return id;
4286
+ }
4287
+ }
4288
+ async function startTask(repo, input) {
4289
+ const body = { prompt: input.prompt };
4290
+ if (input.baseRef !== void 0) body.base_ref = input.baseRef;
4291
+ if (input.model !== void 0) body.model = input.model;
4292
+ if (input.createPullRequest !== void 0) body.create_pull_request = input.createPullRequest;
4293
+ const response = await ghRest("POST", repoTasksPath(repo), {
4294
+ apiVersion: AGENT_TASKS_API_VERSION,
4295
+ body,
4296
+ retry: false,
4297
+ ...input.idempotencyKey ? { headers: { "Idempotency-Key": input.idempotencyKey } } : {}
4298
+ });
4299
+ return {
4300
+ taskId: stringField(response, [
4301
+ "task_id",
4302
+ "taskId",
4303
+ "id"
4304
+ ]) ?? "",
4305
+ state: stringField(response, ["state", "status"]) ?? "unknown"
4306
+ };
4307
+ }
4308
+ async function getTask(repo, taskId) {
4309
+ const record = asRecord$5(await ghRest("GET", taskPath(repo, taskId), { apiVersion: AGENT_TASKS_API_VERSION }));
4310
+ const sessionId = latestSessionId(record);
4311
+ const sessionLog = sessionId ? await getSessionLog(sessionId) : null;
4312
+ const fallbackText = collectText(record).join("\n\n");
4313
+ const logExcerpt = sessionLog?.excerpt && sessionLog.excerpt.length > 0 ? sessionLog.excerpt : tailExcerpt(fallbackText.length > 0 ? fallbackText : compactKnownKeysSummary(record));
4314
+ return {
4315
+ taskId: stringField(record, [
4316
+ "task_id",
4317
+ "taskId",
4318
+ "id"
4319
+ ]) ?? taskId,
4320
+ state: stringField(record, ["state", "status"]) ?? "unknown",
4321
+ prUrl: taskPrUrl(record),
4322
+ pr: taskPrNumber$1(record),
4323
+ logExcerpt,
4324
+ ...sessionId ? { sessionId } : {},
4325
+ ...sessionLog?.branch ? { branch: sessionLog.branch } : {}
4326
+ };
4327
+ }
4328
+ async function followUpTask(repo, taskId, prompt) {
4329
+ await ghRest("POST", `${taskPath(repo, taskId)}${FOLLOW_UP_TASK_PATH_SUFFIX}`, {
4330
+ apiVersion: AGENT_TASKS_API_VERSION,
4331
+ body: { prompt }
4332
+ });
4333
+ return { ok: true };
4334
+ }
4335
+ async function cancelTask(repo, taskId) {
4336
+ await ghRest("POST", `${taskPath(repo, taskId)}${CANCEL_TASK_PATH_SUFFIX}`, { apiVersion: AGENT_TASKS_API_VERSION });
4337
+ return { cancelled: true };
4338
+ }
4339
+
4340
+ //#endregion
4341
+ //#region src/lib/first-mate/decisions.ts
4342
+ const DECISIONS_VERSION = 1;
4343
+ const DECISION_STATUSES = new Set([
4344
+ "pending",
4345
+ "answered",
4346
+ "queued_away",
4347
+ "queued_away_irreversible"
4348
+ ]);
4349
+ function decisionsPath() {
4350
+ return nodePath.join(PATHS.FIRST_MATE_DIR, "decisions.json");
4351
+ }
4352
+ function asRecord$4(value) {
4353
+ return typeof value === "object" && value !== null ? value : void 0;
4354
+ }
4355
+ function isOneOf$1(value, allowed) {
4356
+ return typeof value === "string" && allowed.has(value);
4357
+ }
4358
+ function isFiniteNumber$2(value) {
4359
+ return typeof value === "number" && Number.isFinite(value);
4360
+ }
4361
+ function isPositiveInteger(value) {
4362
+ return typeof value === "number" && Number.isInteger(value) && value > 0;
4363
+ }
4364
+ function isOptionalString$2(value) {
4365
+ return value === void 0 || typeof value === "string";
4366
+ }
4367
+ function isOptionalStringOrNull$1(value) {
4368
+ return value === void 0 || value === null || typeof value === "string";
4369
+ }
4370
+ function isOptionalFiniteNumber$2(value) {
4371
+ return value === void 0 || isFiniteNumber$2(value);
4372
+ }
4373
+ function isOptionalStringArray(value) {
4374
+ return value === void 0 || Array.isArray(value) && value.every((entry) => typeof entry === "string");
4375
+ }
4376
+ function isRepoRef$2(value) {
4377
+ const repo = asRecord$4(value);
4378
+ return repo !== void 0 && typeof repo.owner === "string" && repo.owner.length > 0 && typeof repo.name === "string" && repo.name.length > 0;
4379
+ }
4380
+ function isOptionRefs(value) {
4381
+ return value === void 0 || Array.isArray(value) && value.every((option) => {
4382
+ const row = asRecord$4(option);
4383
+ return row !== void 0 && typeof row.id === "string";
4384
+ });
4385
+ }
4386
+ function isApprovalRecord(value) {
4387
+ const approval = asRecord$4(value);
4388
+ return approval !== void 0 && typeof approval.decisionId === "string" && approval.decisionId.length > 0 && isRepoRef$2(approval.repo) && isPositiveInteger(approval.pr) && typeof approval.headSha === "string" && approval.headSha.length > 0 && isOptionalString$2(approval.baseSha) && isOptionalString$2(approval.diffDigest) && isOptionalStringArray(approval.requiredCheckIds) && isOptionalString$2(approval.floorRunId) && approval.status === "approved" && typeof approval.consumed === "boolean" && isFiniteNumber$2(approval.createdMs) && isOptionalFiniteNumber$2(approval.consumedMs);
4389
+ }
4390
+ function isOptionalApprovalRecord(value) {
4391
+ return value === void 0 || isApprovalRecord(value);
4392
+ }
4393
+ function isDecisionRecord(value) {
4394
+ const record = asRecord$4(value);
4395
+ return record !== void 0 && typeof record.decisionId === "string" && record.decisionId.length > 0 && typeof record.decisionKey === "string" && record.decisionKey.length > 0 && typeof record.type === "string" && record.type.length > 0 && isOneOf$1(record.status, DECISION_STATUSES) && isOptionalString$2(record.packetId) && typeof record.inputFingerprint === "string" && record.inputFingerprint.length > 0 && isOptionRefs(record.options) && isOptionalStringOrNull$1(record.chosenOptionId) && isOptionalStringOrNull$1(record.resolvedBy) && isFiniteNumber$2(record.createdMs) && isOptionalFiniteNumber$2(record.resolvedMs) && isOptionalApprovalRecord(record.approval);
4396
+ }
4397
+ async function readDecisionsFile() {
4398
+ let raw;
4399
+ try {
4400
+ raw = await fs.readFile(decisionsPath(), "utf8");
4401
+ } catch (err) {
4402
+ if (err.code !== "ENOENT") consola.debug("first-mate decisions read skipped:", err);
4403
+ return {
4404
+ version: DECISIONS_VERSION,
4405
+ decisions: []
4406
+ };
4407
+ }
4408
+ try {
4409
+ const parsed = asRecord$4(JSON.parse(raw));
4410
+ if (!parsed || parsed.version !== DECISIONS_VERSION || !Array.isArray(parsed.decisions)) return {
4411
+ version: DECISIONS_VERSION,
4412
+ decisions: []
4413
+ };
4414
+ const cleaned = parsed.decisions.filter(isDecisionRecord);
4415
+ if (cleaned.length !== parsed.decisions.length) consola.debug(`first-mate decisions dropped ${parsed.decisions.length - cleaned.length} corrupt decision(s)`);
4416
+ return {
4417
+ version: DECISIONS_VERSION,
4418
+ decisions: cleaned
4419
+ };
4420
+ } catch (err) {
4421
+ consola.debug("first-mate decisions corrupt, starting empty:", err);
4422
+ return {
4423
+ version: DECISIONS_VERSION,
4424
+ decisions: []
4425
+ };
4426
+ }
4427
+ }
4428
+ async function writeDecisionsFile(value) {
4429
+ await fs.mkdir(PATHS.FIRST_MATE_DIR, { recursive: true });
4430
+ const target = decisionsPath();
4431
+ const tmp = `${target}.tmp.${process.pid}.${randomBytes(4).toString("hex")}`;
4432
+ try {
4433
+ await fs.writeFile(tmp, `${JSON.stringify(value, null, 2)}\n`, { mode: 384 });
4434
+ await fs.chmod(tmp, 384).catch(() => {});
4435
+ await fs.rename(tmp, target);
4436
+ await fs.chmod(target, 384).catch(() => {});
4437
+ } catch (err) {
4438
+ await fs.unlink(tmp).catch(() => {});
4439
+ throw err;
4440
+ }
4441
+ }
4442
+ let _decisionsChain = Promise.resolve();
4443
+ function withDecisionsMutation(work) {
4444
+ const next = _decisionsChain.then(async () => {
4445
+ const file = await readDecisionsFile();
4446
+ const result = await work(file.decisions);
4447
+ await writeDecisionsFile(file);
4448
+ return result;
4449
+ });
4450
+ _decisionsChain = next.then(() => void 0, () => void 0);
4451
+ return next;
4452
+ }
4453
+ async function readDecisions() {
4454
+ return (await readDecisionsFile()).decisions;
4455
+ }
4456
+ async function upsertDecision(rec) {
4457
+ await withDecisionsMutation((decisions) => {
4458
+ const kept = decisions.filter((entry) => entry.decisionId !== rec.decisionId && entry.decisionKey !== rec.decisionKey);
4459
+ decisions.splice(0, decisions.length, ...kept, rec);
4460
+ });
4461
+ }
4462
+ async function findByKey(decisionKey) {
4463
+ return (await readDecisions()).find((record) => record.decisionKey === decisionKey);
4464
+ }
4465
+ async function markAnswered(decisionId, chosenOptionId, resolvedBy) {
4466
+ await withDecisionsMutation((decisions) => {
4467
+ const record = decisions.find((entry) => entry.decisionId === decisionId);
4468
+ if (!record) return;
4469
+ record.status = "answered";
4470
+ record.chosenOptionId = chosenOptionId;
4471
+ record.resolvedBy = resolvedBy;
4472
+ record.resolvedMs = Date.now();
4473
+ });
4474
+ }
4475
+
4476
+ //#endregion
4477
+ //#region src/lib/first-mate/approval.ts
4478
+ function sameRepo(a, b) {
4479
+ return a.owner.toLowerCase() === b.owner.toLowerCase() && a.name.toLowerCase() === b.name.toLowerCase();
4480
+ }
4481
+ function approvedRecord(a) {
4482
+ const approval = {
4483
+ decisionId: a.decisionId,
4484
+ repo: a.repo,
4485
+ pr: a.pr,
4486
+ headSha: a.headSha,
4487
+ status: "approved",
4488
+ consumed: false,
4489
+ createdMs: Date.now()
4490
+ };
4491
+ if (a.baseSha !== void 0) approval.baseSha = a.baseSha;
4492
+ if (a.diffDigest !== void 0) approval.diffDigest = a.diffDigest;
4493
+ if (a.requiredCheckIds !== void 0) approval.requiredCheckIds = [...a.requiredCheckIds];
4494
+ if (a.floorRunId !== void 0) approval.floorRunId = a.floorRunId;
4495
+ return approval;
4496
+ }
4497
+ /**
4498
+ * Record a human merge/irreversible approval in the durable decisions ledger.
4499
+ *
4500
+ * This function belongs only on the human-approval path: the model is never
4501
+ * given a tool that can call it, so an approval is a durable controller fact
4502
+ * rather than agent-authored text the model could forge.
4503
+ */
4504
+ async function recordApproval(a) {
4505
+ await withDecisionsMutation((decisions) => {
4506
+ const decision = decisions.find((entry) => entry.decisionId === a.decisionId);
4507
+ if (!decision) throw new Error(`Cannot record approval for unknown decision ${a.decisionId}`);
4508
+ decision.approval = approvedRecord(a);
4509
+ });
4510
+ }
4511
+ async function verifyAndConsumeApproval(args) {
4512
+ return withDecisionsMutation((decisions) => {
4513
+ const approvals = decisions.map((decision) => decision.approval).filter((entry) => entry !== void 0 && sameRepo(entry.repo, args.repo) && entry.pr === args.pr);
4514
+ if (approvals.length === 0) return {
4515
+ ok: false,
4516
+ reason: "no_approval"
4517
+ };
4518
+ const approval = approvals.filter((entry) => !entry.consumed).sort((a, b) => b.createdMs - a.createdMs)[0];
4519
+ if (approval === void 0) return {
4520
+ ok: false,
4521
+ reason: "replayed"
4522
+ };
4523
+ if (approval.headSha !== args.liveHeadSha) return {
4524
+ ok: false,
4525
+ reason: "head_moved"
4526
+ };
4527
+ if (approval.baseSha !== void 0 && approval.baseSha !== args.liveBaseSha) return {
4528
+ ok: false,
4529
+ reason: "base_moved"
4530
+ };
4531
+ approval.consumed = true;
4532
+ approval.consumedMs = Date.now();
4533
+ return { ok: true };
4534
+ });
4535
+ }
4536
+
4537
+ //#endregion
4538
+ //#region src/lib/first-mate/model-tiers.ts
4539
+ const T0_MODEL_CHAIN = [
4540
+ "gemini-3.5-flash",
4541
+ "gemini-3-flash-preview",
4542
+ "gpt-5.4-mini",
4543
+ "gpt-5-mini",
4544
+ "claude-haiku-4.5",
4545
+ "gpt-4o-mini"
4546
+ ];
4547
+ const T1_MODEL_CHAIN = [
4548
+ "gpt-5.4-mini",
4549
+ "gpt-5-mini",
4550
+ "gpt-5.5",
4551
+ "gemini-3.1-pro-preview"
4552
+ ];
4553
+ const T2_MODEL_CHAIN = [
4554
+ "gpt-5.5",
4555
+ "claude-opus-4.8",
4556
+ "gemini-3.1-pro-preview"
4557
+ ];
4558
+ const TIER_CHAINS = {
4559
+ T0: T0_MODEL_CHAIN,
4560
+ T1: T1_MODEL_CHAIN,
4561
+ T2: T2_MODEL_CHAIN
4562
+ };
4563
+ const T0_FALLBACK_RE = /mini|flash|nano|haiku|small/i;
4564
+ const MEMO_TTL_MS = 3e4;
4565
+ const memo = {};
4566
+ function catalogIds(data) {
4567
+ if (!data) return [];
4568
+ return data.map((model) => model.id).filter((id) => typeof id === "string" && id.length > 0);
4569
+ }
4570
+ function catalogKey(ids) {
4571
+ return ids.join("\0");
4572
+ }
4573
+ function resolveFromIds(tier$1, ids) {
4574
+ if (ids.length === 0) return void 0;
4575
+ const available = new Set(ids);
4576
+ for (const model of TIER_CHAINS[tier$1]) if (available.has(model)) return model;
4577
+ if (tier$1 === "T0") return ids.find((id) => T0_FALLBACK_RE.test(id));
4578
+ return ids[0];
4579
+ }
4580
+ function resolveTierModel(tier$1) {
4581
+ const data = state.models?.data;
4582
+ const length = data?.length ?? 0;
4583
+ const ids = catalogIds(data);
4584
+ const key = catalogKey(ids);
4585
+ const now = Date.now();
4586
+ const cached$2 = memo[tier$1];
4587
+ if (cached$2 && cached$2.expiresAt > now && cached$2.data === data && cached$2.length === length && cached$2.catalogKey === key) return cached$2.value;
4588
+ const value = resolveFromIds(tier$1, ids);
4589
+ memo[tier$1] = {
4590
+ data,
4591
+ length,
4592
+ catalogKey: key,
4593
+ expiresAt: now + MEMO_TTL_MS,
4594
+ value
4595
+ };
4596
+ return value;
4597
+ }
4598
+
4599
+ //#endregion
4600
+ //#region src/lib/first-mate/classifier.ts
4601
+ function isRecord$1(value) {
4602
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4603
+ }
4604
+ function firstMessageContent(value) {
4605
+ if (!isRecord$1(value) || !Array.isArray(value.choices)) return null;
4606
+ const first = value.choices[0];
4607
+ if (!isRecord$1(first) || !isRecord$1(first.message)) return null;
4608
+ return typeof first.message.content === "string" ? first.message.content : null;
4609
+ }
4610
+ function parseJsonObject(value) {
4611
+ try {
4612
+ const parsed = JSON.parse(value);
4613
+ return isRecord$1(parsed) ? parsed : null;
4614
+ } catch {
4615
+ return null;
4616
+ }
4617
+ }
4618
+ function confidenceOf(value) {
4619
+ const confidence = value.confidence;
4620
+ if (typeof confidence !== "number") return null;
4621
+ if (!Number.isFinite(confidence) || confidence < 0 || confidence > 1) return null;
4622
+ return confidence;
4623
+ }
4624
+ function classifierSystemPrompt(system, schemaHint) {
4625
+ return `${system}\nReply ONLY with a JSON object matching: ${schemaHint}. Include a numeric confidence field from 0 to 1. No markdown, prose, or extra text.`;
4626
+ }
4627
+ async function microClassify(opts) {
4628
+ const model = resolveTierModel("T0");
4629
+ if (!model) return null;
4630
+ let response;
4631
+ try {
4632
+ response = await fetch(`${copilotBaseUrl(state)}/chat/completions`, {
4633
+ method: "POST",
4634
+ headers: copilotHeaders(state),
4635
+ body: JSON.stringify({
4636
+ model,
4637
+ messages: [{
4638
+ role: "system",
4639
+ content: classifierSystemPrompt(opts.system, opts.schemaHint)
4640
+ }, {
4641
+ role: "user",
4642
+ content: opts.user
4643
+ }],
4644
+ temperature: 0,
4645
+ max_tokens: opts.maxTokens ?? 400,
4646
+ response_format: { type: "json_object" }
4647
+ })
4648
+ });
4649
+ } catch (err) {
4650
+ consola.debug("first-mate micro-classifier fetch failed:", err);
4651
+ return null;
4652
+ }
4653
+ try {
4654
+ const content = firstMessageContent(await response.json());
4655
+ if (!content) return null;
4656
+ const parsed = parseJsonObject(content);
4657
+ if (!parsed) return null;
4658
+ const confidence = confidenceOf(parsed);
4659
+ if (confidence === null || confidence < .6) return null;
4660
+ const value = opts.validate(parsed);
4661
+ if (value === null) return null;
4662
+ return {
4663
+ value,
4664
+ confidence
4665
+ };
4666
+ } catch {
4667
+ return null;
4668
+ }
4669
+ }
4670
+ function stringValue$1(value) {
4671
+ return typeof value === "string" ? value : null;
4672
+ }
4673
+ function booleanValue$1(value) {
4674
+ return typeof value === "boolean" ? value : null;
4675
+ }
4676
+ async function classifyPlanReady(logExcerpt) {
4677
+ return (await microClassify({
4678
+ system: "Decide whether a cloud-agent session log shows a completed implementation PLAN, not code or execution.",
4679
+ user: `Log excerpt:\n${logExcerpt}`,
4680
+ schemaHint: "{\"planReady\":boolean,\"planExcerpt\":\"<=1200 chars from the completed plan, or empty\",\"confidence\":number}",
4681
+ maxTokens: 500,
4682
+ validate(value) {
4683
+ if (!isRecord$1(value)) return null;
4684
+ const planReady = booleanValue$1(value.planReady);
4685
+ const planExcerpt = stringValue$1(value.planExcerpt);
4686
+ if (planReady === null || planExcerpt === null) return null;
4687
+ return {
4688
+ planReady,
4689
+ planExcerpt: planExcerpt.slice(0, 1200)
4690
+ };
4691
+ }
4692
+ }))?.value ?? null;
4693
+ }
4694
+ async function classifyQuestionAnswerable(question, acceptanceCriteria) {
4695
+ return (await microClassify({
4696
+ system: "Decide if the agent's question is answerable purely from the acceptance criteria. If yes, answer it tersely.",
4697
+ user: `Acceptance criteria:\n${acceptanceCriteria}\n\nAgent question:\n${question}`,
4698
+ schemaHint: "{\"answerable\":boolean,\"answer\":\"present only when answerable\",\"confidence\":number}",
4699
+ validate(value) {
4700
+ if (!isRecord$1(value)) return null;
4701
+ const answerable = booleanValue$1(value.answerable);
4702
+ if (answerable === null) return null;
4703
+ const answer = stringValue$1(value.answer);
4704
+ if (!answerable) return { answerable };
4705
+ return answer === null ? null : {
4706
+ answerable,
4707
+ answer
4708
+ };
4709
+ }
4710
+ }))?.value ?? null;
4711
+ }
4712
+ async function classifyFixAddressed(failureSummary, latestLogExcerpt) {
4713
+ return (await microClassify({
4714
+ system: "Decide whether the latest cloud-agent log indicates the summarized failure was addressed.",
4715
+ user: `Failure summary:\n${failureSummary}\n\nLatest log excerpt:\n${latestLogExcerpt}`,
4716
+ schemaHint: "{\"addressed\":boolean,\"confidence\":number}",
4717
+ validate(value) {
4718
+ if (!isRecord$1(value)) return null;
4719
+ const addressed = booleanValue$1(value.addressed);
4720
+ return addressed === null ? null : { addressed };
4721
+ }
4722
+ }))?.value ?? null;
4723
+ }
4724
+ async function classifyStuck(logExcerpt) {
4725
+ return (await microClassify({
4726
+ system: "Decide whether the cloud-agent log shows the agent is stuck, looping, blocked, or unable to proceed.",
4727
+ user: `Log excerpt:\n${logExcerpt}`,
4728
+ schemaHint: "{\"stuck\":boolean,\"confidence\":number}",
4729
+ validate(value) {
4730
+ if (!isRecord$1(value)) return null;
4731
+ const stuck = booleanValue$1(value.stuck);
4732
+ return stuck === null ? null : { stuck };
4733
+ }
4734
+ }))?.value ?? null;
4735
+ }
4736
+
4737
+ //#endregion
4738
+ //#region src/lib/first-mate/decision-packet.ts
4739
+ const HTML_ESCAPES = {
4740
+ "&": "&amp;",
4741
+ "<": "&lt;",
4742
+ ">": "&gt;",
4743
+ "\"": "&quot;",
4744
+ "'": "&#39;"
4745
+ };
4746
+ function esc(value) {
4747
+ return value.replace(/[&<>"']/g, (char) => HTML_ESCAPES[char] ?? char);
4748
+ }
4749
+ function safeHref(value) {
4750
+ try {
4751
+ const url = new URL(value);
4752
+ if (url.protocol !== "http:" && url.protocol !== "https:") return null;
4753
+ return esc(url.href);
4754
+ } catch {
4755
+ return null;
4756
+ }
4757
+ }
4758
+ function evidenceBody(input) {
4759
+ const evidence = input.evidence;
4760
+ const rows = [];
4761
+ if (evidence?.prSummary !== void 0) rows.push(`<dt>PR summary</dt><dd>${esc(evidence.prSummary)}</dd>`);
4762
+ if (evidence?.ciExcerpt !== void 0) rows.push(`<dt>CI excerpt</dt><dd><pre>${esc(evidence.ciExcerpt)}</pre></dd>`);
4763
+ if (evidence?.floorVerdict !== void 0) rows.push(`<dt>Floor verdict</dt><dd>${esc(evidence.floorVerdict)}</dd>`);
4764
+ const links = evidence?.links ?? [];
4765
+ if (links.length > 0) {
4766
+ const items = links.map((link$1) => {
4767
+ const label = esc(link$1.label);
4768
+ const href = safeHref(link$1.url);
4769
+ if (href === null) return `<li><span>${label}</span></li>`;
4770
+ return `<li><a href="${href}" rel="noreferrer noopener" target="_blank">${label}</a></li>`;
4771
+ }).join("");
4772
+ rows.push(`<dt>Links</dt><dd><ul>${items}</ul></dd>`);
4773
+ }
4774
+ if (rows.length === 0) return `<p class="muted">No evidence attached.</p>`;
4775
+ return `<dl>${rows.join("")}</dl>`;
4776
+ }
4777
+ function provenance(input, packetId, decisionId) {
4778
+ const parts = [
4779
+ `<span><strong>packetId</strong> ${esc(packetId)}</span>`,
4780
+ `<span><strong>decisionId</strong> ${esc(decisionId)}</span>`,
4781
+ `<span><strong>type</strong> ${esc(input.type)}</span>`,
4782
+ `<span><strong>timestamp</strong> timestamp-placeholder</span>`
4783
+ ];
4784
+ if (input.missionId !== void 0) parts.push(`<span><strong>mission</strong> ${esc(input.missionId)}</span>`);
4785
+ if (input.repo !== void 0) parts.push(`<span><strong>repo</strong> ${esc(input.repo.owner)}/${esc(input.repo.name)}</span>`);
4786
+ if (input.unit !== void 0) {
4787
+ const refs = [];
4788
+ if (input.unit.issue !== void 0 && input.unit.issue !== null) refs.push(`issue #${input.unit.issue}`);
4789
+ if (input.unit.pr !== void 0 && input.unit.pr !== null) refs.push(`PR #${input.unit.pr}`);
4790
+ if (refs.length > 0) parts.push(`<span><strong>unit</strong> ${esc(refs.join(", "))}</span>`);
4791
+ }
4792
+ return parts.join("\n ");
4793
+ }
4794
+ function buildDecisionPacket(input) {
4795
+ const packetId = randomUUID();
4796
+ const decisionId = randomUUID();
4797
+ const recommendedIndex = input.options.findIndex((option) => option.recommended === true);
4798
+ const optionCards = input.options.map((option, index) => {
4799
+ const badge = index === recommendedIndex ? ` <span class="badge" aria-label="Recommended option">Recommended</span>` : "";
4800
+ return `<section class="option" data-option="${esc(option.id)}">
4801
+ <h2>${esc(option.label)}${badge}</h2>
4802
+ <p class="consequence">${esc(option.consequence)}</p>
4803
+ </section>`;
4804
+ }).join("\n");
4805
+ return {
4806
+ html: `<!doctype html>
4807
+ <html lang="en">
4808
+ <head>
4809
+ <meta charset="utf-8">
4810
+ <meta name="viewport" content="width=device-width, initial-scale=1">
4811
+ <title>${esc(input.tldr)}</title>
4812
+ <style>
4813
+ :root { color-scheme: light dark; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
4814
+ body { margin: 0; padding: 32px; background: #0f172a; color: #e2e8f0; }
4815
+ main { max-width: 860px; margin: 0 auto; }
4816
+ .banner { padding: 24px; border-radius: 18px; background: linear-gradient(135deg, #2563eb, #7c3aed); box-shadow: 0 18px 45px rgba(15, 23, 42, 0.35); }
4817
+ .eyebrow { margin: 0 0 8px; font-size: 12px; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; opacity: 0.78; }
4818
+ h1 { margin: 0; font-size: clamp(28px, 5vw, 44px); line-height: 1.05; }
4819
+ .question { margin: 24px 0; padding: 18px 20px; border: 1px solid rgba(148, 163, 184, 0.35); border-radius: 14px; background: rgba(15, 23, 42, 0.72); font-size: 18px; }
4820
+ .options { display: grid; gap: 16px; }
4821
+ .option { padding: 18px 20px; border: 1px solid rgba(148, 163, 184, 0.32); border-radius: 16px; background: rgba(30, 41, 59, 0.86); }
4822
+ .option h2 { margin: 0 0 10px; display: flex; gap: 10px; align-items: center; font-size: 20px; }
4823
+ .badge { display: inline-block; padding: 4px 8px; border-radius: 999px; background: #22c55e; color: #052e16; font-size: 12px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.06em; }
4824
+ .consequence { margin: 0; color: #cbd5e1; }
4825
+ details { margin-top: 22px; padding: 16px 18px; border: 1px solid rgba(148, 163, 184, 0.28); border-radius: 14px; background: rgba(15, 23, 42, 0.58); }
4826
+ summary { cursor: pointer; font-weight: 700; }
4827
+ dl { display: grid; grid-template-columns: minmax(120px, 0.28fr) 1fr; gap: 10px 16px; margin: 16px 0 0; }
4828
+ dt { color: #93c5fd; font-weight: 700; }
4829
+ dd { margin: 0; }
4830
+ pre { white-space: pre-wrap; overflow-wrap: anywhere; margin: 0; font: inherit; }
4831
+ ul { margin: 0; padding-left: 18px; }
4832
+ a { color: #93c5fd; }
4833
+ .muted { margin: 14px 0 0; color: #94a3b8; }
4834
+ footer { margin-top: 22px; display: flex; flex-wrap: wrap; gap: 10px 14px; color: #94a3b8; font-size: 12px; }
4835
+ </style>
4836
+ </head>
4837
+ <body>
4838
+ <main>
4839
+ <header class="banner">
4840
+ <p class="eyebrow">Decision packet</p>
4841
+ <h1>${esc(input.tldr)}</h1>
4842
+ </header>
4843
+ <p class="question">${esc(input.question)}</p>
4844
+ <div class="options">
4845
+ ${optionCards}
4846
+ </div>
4847
+ <details>
4848
+ <summary>Evidence</summary>
4849
+ ${evidenceBody(input)}
4850
+ </details>
4851
+ <footer>
4852
+ ${provenance(input, packetId, decisionId)}
4853
+ </footer>
4854
+ </main>
4855
+ </body>
4856
+ </html>`,
4857
+ packetId,
4858
+ decisionId
4859
+ };
4860
+ }
4861
+
4862
+ //#endregion
4863
+ //#region src/lib/first-mate/ledger.ts
4864
+ const LEDGER_VERSION = 1;
4865
+ const DEFAULT_TERMINAL_MAX_AGE_MS = 10080 * 60 * 1e3;
4866
+ const TERMINAL_MAX_ENTRIES = 200;
4867
+ const AGENTS = new Set([
4868
+ "copilot",
4869
+ "anthropic",
4870
+ "openai"
4871
+ ]);
4872
+ function membersOf(record) {
4873
+ return new Set(Object.keys(record));
4874
+ }
4875
+ const DISPATCH_MODES = membersOf({
4876
+ plan: true,
4877
+ build: true
4878
+ });
4879
+ const PROVIDER_STATES$2 = membersOf({
4880
+ none: true,
4881
+ queued: true,
4882
+ in_progress: true,
4883
+ waiting_for_user: true,
4884
+ completed: true,
4885
+ failed: true,
4886
+ timed_out: true,
4887
+ cancelled: true
4888
+ });
4889
+ const PHASES = membersOf({
4890
+ plan: true,
4891
+ build: true,
4892
+ fix: true,
4893
+ review: true,
4894
+ merge: true,
4895
+ done: true
4896
+ });
4897
+ const ARTIFACTS = membersOf({
4898
+ no_pr: true,
4899
+ pr_open: true,
4900
+ pr_closed: true,
4901
+ pr_merged: true,
4902
+ multiple_prs: true
4903
+ });
4904
+ const VALIDATIONS = membersOf({
4905
+ unknown: true,
4906
+ ci_running: true,
4907
+ ci_passed: true,
4908
+ ci_failed: true,
4909
+ no_ci: true,
4910
+ review_pending: true,
4911
+ changes_requested: true,
4912
+ floor_pending: true,
4913
+ floor_passed: true,
4914
+ floor_failed: true
4915
+ });
4916
+ function sanitizeSegment$1(value) {
4917
+ const cleaned = value.replace(/[^A-Za-z0-9._-]/g, "_").replace(/^\.+$/, "_");
4918
+ return cleaned.length > 0 ? cleaned : "_";
4919
+ }
4920
+ function repoLedgerPath(repo) {
4921
+ return nodePath.join(PATHS.FIRST_MATE_DIR, `${sanitizeSegment$1(repo.owner)}__${sanitizeSegment$1(repo.name)}.json`);
4922
+ }
4923
+ function asRecord$3(value) {
4924
+ return typeof value === "object" && value !== null ? value : void 0;
4925
+ }
4926
+ function isOneOf(value, allowed) {
4927
+ return typeof value === "string" && allowed.has(value);
4928
+ }
4929
+ function isFiniteNumber$1(value) {
4930
+ return typeof value === "number" && Number.isFinite(value);
4931
+ }
4932
+ function isNonNegativeInteger(value) {
4933
+ return typeof value === "number" && Number.isInteger(value) && value >= 0;
4934
+ }
4935
+ function isIssueNumberOrNull(value) {
4936
+ return value === null || isNonNegativeInteger(value);
4937
+ }
4938
+ function isStringOrNull(value) {
4939
+ return value === null || typeof value === "string";
4940
+ }
4941
+ function isOptionalString$1(value) {
4942
+ return value === void 0 || typeof value === "string";
4943
+ }
4944
+ function isOptionalStringOrNull(value) {
4945
+ return value === void 0 || value === null || typeof value === "string";
4946
+ }
4947
+ function isOptionalBoolean(value) {
4948
+ return value === void 0 || typeof value === "boolean";
4949
+ }
4950
+ function isOptionalFiniteNumber$1(value) {
4951
+ return value === void 0 || isFiniteNumber$1(value);
4952
+ }
4953
+ function isRepoRef$1(value) {
4954
+ const repo = asRecord$3(value);
4955
+ return repo !== void 0 && typeof repo.owner === "string" && repo.owner.length > 0 && typeof repo.name === "string" && repo.name.length > 0;
4956
+ }
4957
+ function isStringArray(value) {
4958
+ return Array.isArray(value) && value.every((v) => typeof v === "string");
4959
+ }
4960
+ function isLastSteer(value) {
4961
+ if (value === void 0) return true;
4962
+ const steer = asRecord$3(value);
4963
+ return steer !== void 0 && isOptionalString$1(steer.cursor) && isOptionalString$1(steer.sha) && isFiniteNumber$1(steer.atMs);
4964
+ }
4965
+ function isUnitRow(value) {
4966
+ const row = asRecord$3(value);
4967
+ if (!row) return false;
4968
+ return typeof row.missionId === "string" && row.missionId.length > 0 && isRepoRef$1(row.repo) && isIssueNumberOrNull(row.issue) && isIssueNumberOrNull(row.pr) && isStringOrNull(row.taskId) && isOneOf(row.agent, AGENTS) && typeof row.botLogin === "string" && isOneOf(row.dispatchMode, DISPATCH_MODES) && isOneOf(row.provider, PROVIDER_STATES$2) && isOneOf(row.phase, PHASES) && isOneOf(row.artifact, ARTIFACTS) && isOneOf(row.validation, VALIDATIONS) && isNonNegativeInteger(row.retries) && isStringArray(row.dependsOn) && typeof row.title === "string" && isLastSteer(row.lastSteer) && (row.cancelledBy === void 0 || row.cancelledBy === "controller" || row.cancelledBy === "external") && isOptionalStringOrNull(row.bakeoffGroupId) && isOptionalStringOrNull(row.blockingDecisionId) && isOptionalBoolean(row.verifierAssigned) && (row.implementerLab === void 0 || isOneOf(row.implementerLab, AGENTS)) && isOptionalStringOrNull(row.branch) && isOptionalStringOrNull(row.headSha) && isOptionalStringOrNull(row.baseSha) && isOptionalFiniteNumber$1(row.lastCheckedMs) && isOptionalBoolean(row.terminal);
4969
+ }
4970
+ async function writeJsonSecure(target, value) {
4971
+ await fs.mkdir(nodePath.dirname(target), { recursive: true });
4972
+ const tmp = `${target}.tmp.${process.pid}.${randomBytes(4).toString("hex")}`;
4973
+ try {
4974
+ await fs.writeFile(tmp, `${JSON.stringify(value, null, 2)}\n`, { mode: 384 });
4975
+ await fs.chmod(tmp, 384).catch(() => {});
4976
+ await fs.rename(tmp, target);
4977
+ await fs.chmod(target, 384).catch(() => {});
4978
+ } catch (err) {
4979
+ await fs.unlink(tmp).catch(() => {});
4980
+ throw err;
4981
+ }
4982
+ }
4983
+ async function writeRepoLedger(repo, units) {
4984
+ await writeJsonSecure(repoLedgerPath(repo), {
4985
+ version: LEDGER_VERSION,
4986
+ units
4987
+ });
4988
+ }
4989
+ let _ledgerChain = Promise.resolve();
4990
+ function serializeLedgerWrite(work) {
4991
+ const next = _ledgerChain.then(work);
4992
+ _ledgerChain = next.catch(() => void 0);
4993
+ return next;
4994
+ }
4995
+ function sameUnitHandle(a, b) {
4996
+ return b.id != null && a.id === b.id || b.issue !== null && a.issue === b.issue || b.taskId !== null && a.taskId === b.taskId;
4997
+ }
4998
+ function terminalTimestamp(row) {
4999
+ return row.lastCheckedMs ?? row.lastSteer?.atMs ?? 0;
5000
+ }
5001
+ async function readRepoLedger(repo) {
5002
+ let raw;
5003
+ try {
5004
+ raw = await fs.readFile(repoLedgerPath(repo), "utf8");
5005
+ } catch (err) {
5006
+ if (err.code !== "ENOENT") consola.debug("first-mate ledger read skipped:", err);
5007
+ return [];
5008
+ }
5009
+ try {
5010
+ const parsed = asRecord$3(JSON.parse(raw));
5011
+ if (!parsed || parsed.version !== LEDGER_VERSION || !Array.isArray(parsed.units)) return [];
5012
+ const cleaned = parsed.units.filter(isUnitRow);
5013
+ if (cleaned.length !== parsed.units.length) consola.debug(`first-mate ledger dropped ${parsed.units.length - cleaned.length} corrupt unit(s)`);
5014
+ return cleaned;
5015
+ } catch (err) {
5016
+ consola.debug("first-mate ledger corrupt, starting empty:", err);
5017
+ return [];
5018
+ }
5019
+ }
5020
+ async function upsertUnit(repo, unit) {
5021
+ await serializeLedgerWrite(async () => {
5022
+ const next = (await readRepoLedger(repo)).filter((row) => !sameUnitHandle(row, unit));
5023
+ next.push(unit);
5024
+ await writeRepoLedger(repo, next);
5025
+ });
5026
+ }
5027
+ async function pruneTerminal(repo, maxAgeMs = DEFAULT_TERMINAL_MAX_AGE_MS) {
5028
+ await serializeLedgerWrite(async () => {
5029
+ const current = await readRepoLedger(repo);
5030
+ const now = Date.now();
5031
+ const keptTerminals = new Set(current.filter((row) => row.terminal === true).filter((row) => now - terminalTimestamp(row) < maxAgeMs).sort((a, b) => terminalTimestamp(a) - terminalTimestamp(b)).slice(-TERMINAL_MAX_ENTRIES));
5032
+ await writeRepoLedger(repo, current.filter((row) => row.terminal !== true || keptTerminals.has(row)));
5033
+ });
5034
+ }
5035
+
5036
+ //#endregion
5037
+ //#region src/lib/first-mate/registry.ts
5038
+ const REGISTRY_VERSION = 1;
5039
+ function registryPath() {
5040
+ return nodePath.join(PATHS.FIRST_MATE_DIR, "missions.json");
5041
+ }
5042
+ function asRecord$2(value) {
5043
+ return typeof value === "object" && value !== null ? value : void 0;
5044
+ }
5045
+ function isFiniteNumber(value) {
5046
+ return typeof value === "number" && Number.isFinite(value);
5047
+ }
5048
+ function isOptionalString(value) {
5049
+ return value === void 0 || typeof value === "string";
5050
+ }
5051
+ function isOptionalFiniteNumber(value) {
5052
+ return value === void 0 || isFiniteNumber(value);
5053
+ }
5054
+ function isRepoRef(value) {
5055
+ const repo = asRecord$2(value);
5056
+ return repo !== void 0 && typeof repo.owner === "string" && repo.owner.length > 0 && typeof repo.name === "string" && repo.name.length > 0;
5057
+ }
5058
+ function isMission(value) {
5059
+ const mission = asRecord$2(value);
5060
+ return mission !== void 0 && typeof mission.id === "string" && mission.id.length > 0 && typeof mission.goal === "string" && typeof mission.acceptanceCriteria === "string" && isOptionalString(mission.houseRules) && isOptionalFiniteNumber(mission.priority) && Array.isArray(mission.repos) && mission.repos.every(isRepoRef) && (mission.status === "active" || mission.status === "done" || mission.status === "abandoned") && isFiniteNumber(mission.createdMs) && isFiniteNumber(mission.updatedMs);
5061
+ }
5062
+ async function writeRegistry(value) {
5063
+ await fs.mkdir(PATHS.FIRST_MATE_DIR, { recursive: true });
5064
+ const target = registryPath();
5065
+ const tmp = `${target}.tmp.${process.pid}.${randomBytes(4).toString("hex")}`;
5066
+ try {
5067
+ await fs.writeFile(tmp, `${JSON.stringify(value, null, 2)}\n`, { mode: 384 });
5068
+ await fs.chmod(tmp, 384).catch(() => {});
5069
+ await fs.rename(tmp, target);
5070
+ await fs.chmod(target, 384).catch(() => {});
5071
+ } catch (err) {
5072
+ await fs.unlink(tmp).catch(() => {});
5073
+ throw err;
5074
+ }
5075
+ }
5076
+ let _registryChain = Promise.resolve();
5077
+ function serializeRegistryWrite(work) {
5078
+ const next = _registryChain.then(work);
5079
+ _registryChain = next.catch(() => void 0);
5080
+ return next;
5081
+ }
5082
+ function repoKey(repo) {
5083
+ return `${repo.owner.toLowerCase()}\0${repo.name.toLowerCase()}`;
5084
+ }
5085
+ async function readMissions() {
5086
+ let raw;
5087
+ try {
5088
+ raw = await fs.readFile(registryPath(), "utf8");
5089
+ } catch (err) {
5090
+ if (err.code !== "ENOENT") consola.debug("first-mate registry read skipped:", err);
5091
+ return [];
5092
+ }
5093
+ try {
5094
+ const parsed = asRecord$2(JSON.parse(raw));
5095
+ if (!parsed || parsed.version !== REGISTRY_VERSION || !Array.isArray(parsed.missions)) return [];
5096
+ const cleaned = parsed.missions.filter(isMission);
5097
+ if (cleaned.length !== parsed.missions.length) consola.debug(`first-mate registry dropped ${parsed.missions.length - cleaned.length} corrupt mission(s)`);
5098
+ return cleaned;
5099
+ } catch (err) {
5100
+ consola.debug("first-mate registry corrupt, starting empty:", err);
5101
+ return [];
5102
+ }
5103
+ }
5104
+ async function upsertMission(mission) {
5105
+ await serializeRegistryWrite(async () => {
5106
+ const missions = (await readMissions()).filter((entry) => entry.id !== mission.id);
5107
+ missions.push(mission);
5108
+ await writeRegistry({
5109
+ version: REGISTRY_VERSION,
5110
+ missions
5111
+ });
5112
+ });
5113
+ }
5114
+ async function loadAllUnits() {
5115
+ const missions = await readMissions();
5116
+ const repos = /* @__PURE__ */ new Map();
5117
+ for (const mission of missions) for (const repo of mission.repos) repos.set(repoKey(repo), repo);
5118
+ const units = [];
5119
+ for (const repo of repos.values()) units.push(...await readRepoLedger(repo));
5120
+ return units;
5121
+ }
5122
+
5123
+ //#endregion
5124
+ //#region src/lib/first-mate/observe.ts
5125
+ const PROVIDER_STATES$1 = new Set([
5126
+ "none",
5127
+ "queued",
5128
+ "in_progress",
5129
+ "waiting_for_user",
5130
+ "completed",
5131
+ "failed",
5132
+ "timed_out",
5133
+ "cancelled"
5134
+ ]);
5135
+ function agentRepo$1(unit) {
5136
+ return {
5137
+ owner: unit.repo.owner,
5138
+ repo: unit.repo.name
5139
+ };
5140
+ }
5141
+ function providerState$1(value, fallback) {
5142
+ if (value && PROVIDER_STATES$1.has(value)) return value;
5143
+ return fallback;
5144
+ }
5145
+ function normalizePrState(value) {
5146
+ if (!value) return "OPEN";
5147
+ const upper = value.toUpperCase();
5148
+ if (upper === "OPEN" || upper === "CLOSED" || upper === "MERGED") return upper;
5149
+ return value;
5150
+ }
5151
+ function parsePrNumberFromUrl(value) {
5152
+ if (!value) return null;
5153
+ const match = /\/pull\/(\d+)(?:[/?#]|$)/.exec(value);
5154
+ if (!match) return null;
5155
+ const parsed = Number.parseInt(match[1], 10);
5156
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
5157
+ }
5158
+ function taskPrNumber(task) {
5159
+ if (!task) return null;
5160
+ if (typeof task.pr === "number" && Number.isInteger(task.pr) && task.pr > 0) return task.pr;
5161
+ return parsePrNumberFromUrl(task.prUrl);
5162
+ }
5163
+ function firstSummaryNumber(prs) {
5164
+ return prs.find((pr) => Number.isInteger(pr.number) && pr.number > 0)?.number ?? null;
5165
+ }
5166
+ function branchMatchNumber(prs, branch) {
5167
+ if (!branch) return null;
5168
+ return prs.find((pr) => pr.headRef === branch && Number.isInteger(pr.number) && pr.number > 0)?.number ?? null;
5169
+ }
5170
+ function primaryPrNumber(unit, task, prs) {
5171
+ if (unit.pr !== null && unit.pr > 0) return unit.pr;
5172
+ const branch = unit.branch ?? task?.branch ?? void 0;
5173
+ const byBranch = branchMatchNumber(prs, branch);
5174
+ if (byBranch !== null) return byBranch;
5175
+ const byTask = taskPrNumber(task);
5176
+ if (byTask !== null) return byTask;
5177
+ if (branch !== void 0) return null;
5178
+ return firstSummaryNumber(prs);
5179
+ }
5180
+ async function getTaskSafe(repo, taskId) {
5181
+ if (!taskId) return null;
5182
+ try {
5183
+ return await getTask(repo, taskId);
5184
+ } catch (err) {
5185
+ consola.debug("first-mate observe: task read skipped:", err);
5186
+ return null;
5187
+ }
5188
+ }
5189
+ async function findPrsSafe(repo, unit) {
5190
+ try {
5191
+ return await findAgentPRs(repo, {
5192
+ issueNumber: unit.issue ?? 0,
5193
+ botLogin: unit.botLogin,
5194
+ ...unit.branch ? { branch: unit.branch } : {}
5195
+ });
5196
+ } catch (err) {
5197
+ consola.debug("first-mate observe: PR discovery skipped:", err);
5198
+ return [];
5199
+ }
5200
+ }
5201
+ async function getPullRequestStateSafe(repo, pr) {
5202
+ if (pr === null) return null;
5203
+ try {
5204
+ return await getPullRequestState(repo, pr);
5205
+ } catch (err) {
5206
+ consola.debug("first-mate observe: PR state read skipped:", err);
5207
+ return null;
5208
+ }
5209
+ }
5210
+ async function getCiSafe(repo, headSha, baseRef) {
5211
+ if (headSha.length === 0) return void 0;
5212
+ try {
5213
+ const checks = await getRequiredChecksForSha(repo, headSha);
5214
+ if (checks.rollup !== "none") return { rollup: checks.rollup };
5215
+ return {
5216
+ rollup: "none",
5217
+ noCi: !(baseRef ? await repoHasWorkflows(repo, baseRef) : false)
5218
+ };
5219
+ } catch (err) {
5220
+ consola.debug("first-mate observe: required checks read skipped:", err);
5221
+ return;
5222
+ }
5223
+ }
5224
+ async function verifierReviewSafe(repo, unit, pr, headSha) {
5225
+ if (!unit.verifierAssigned || pr === null) return { reviewed: false };
5226
+ try {
5227
+ const copilotReviews = (await getPullRequestReviews(repo, pr)).filter((r) => r.author === COPILOT_REVIEWER_LOGIN && (headSha === void 0 || r.commitId === void 0 || r.commitId === headSha));
5228
+ const latest = copilotReviews[copilotReviews.length - 1];
5229
+ if (latest === void 0) return { reviewed: false };
5230
+ return {
5231
+ reviewed: true,
5232
+ findings: latest.bodyExcerpt
5233
+ };
5234
+ } catch (err) {
5235
+ consola.debug("first-mate observe: review read skipped:", err);
5236
+ return { reviewed: false };
5237
+ }
5238
+ }
5239
+ function observedPrs(summaries, primaryState) {
5240
+ const prs = /* @__PURE__ */ new Map();
5241
+ for (const summary of summaries) {
5242
+ if (!Number.isInteger(summary.number) || summary.number <= 0) continue;
5243
+ prs.set(summary.number, {
5244
+ number: summary.number,
5245
+ headSha: summary.headSha,
5246
+ isDraft: summary.isDraft,
5247
+ state: "OPEN"
5248
+ });
5249
+ }
5250
+ if (primaryState) {
5251
+ const summary = prs.get(primaryState.number);
5252
+ const state$1 = normalizePrState(primaryState.state);
5253
+ prs.set(primaryState.number, {
5254
+ number: primaryState.number,
5255
+ headSha: primaryState.headSha || summary?.headSha || "",
5256
+ isDraft: primaryState.isDraft,
5257
+ state: state$1,
5258
+ merged: state$1 === "MERGED"
5259
+ });
5260
+ }
5261
+ return [...prs.values()];
5262
+ }
5263
+ function externalMutation(unit, primaryState) {
5264
+ if (!primaryState || unit.terminal || unit.cancelledBy === "controller") return;
5265
+ const state$1 = normalizePrState(primaryState.state);
5266
+ if (state$1 === "MERGED") return "merged";
5267
+ if (state$1 === "CLOSED") return "closed";
5268
+ }
5269
+ async function observeUnit(unit) {
5270
+ const repo = agentRepo$1(unit);
5271
+ const task = await getTaskSafe(repo, unit.taskId);
5272
+ if (task?.branch && task.branch.length > 0) unit.branch = task.branch;
5273
+ const provider = providerState$1(task?.state, unit.provider);
5274
+ const prSummaries = await findPrsSafe(repo, unit);
5275
+ const primaryNumber = primaryPrNumber(unit, task, prSummaries);
5276
+ const primaryState = await getPullRequestStateSafe(repo, primaryNumber);
5277
+ const ci = primaryState ? await getCiSafe(repo, primaryState.headSha, primaryState.baseRef) : void 0;
5278
+ const reviewDecision = primaryState ? primaryState.reviewDecision ?? null : void 0;
5279
+ const mutation = externalMutation(unit, primaryState);
5280
+ const logExcerpt = task?.logExcerpt && task.logExcerpt.length > 0 ? task.logExcerpt : void 0;
5281
+ const question = provider === "waiting_for_user" ? logExcerpt : void 0;
5282
+ const review = await verifierReviewSafe(repo, unit, primaryNumber, primaryState?.headSha);
5283
+ return {
5284
+ provider,
5285
+ prs: observedPrs(prSummaries, primaryState),
5286
+ ...ci ? { ci } : {},
5287
+ ...reviewDecision !== void 0 ? { reviewDecision } : {},
5288
+ ...mutation ? { externalMutation: mutation } : {},
5289
+ ...logExcerpt ? { logExcerpt } : {},
5290
+ ...question ? { question } : {},
5291
+ ...review.reviewed ? { verifierReviewed: true } : {},
5292
+ ...review.findings ? { reviewExcerpt: review.findings } : {},
5293
+ ...primaryState?.nodeId ? { prNodeId: primaryState.nodeId } : {}
5294
+ };
5295
+ }
5296
+
5297
+ //#endregion
5298
+ //#region src/lib/first-mate/state-machine.ts
5299
+ function classify(observed, row) {
5300
+ const events$1 = [];
5301
+ const provider = observed.provider;
5302
+ const artifact = classifyArtifact(observed, events$1);
5303
+ const validation = classifyValidation(observed, artifact, row);
5304
+ const phase = classifyPhase(observed, row, artifact, validation);
5305
+ if (observed.externalMutation) events$1.push(`external:${observed.externalMutation}`);
5306
+ if (observed.steerAcknowledged === false) events$1.push("steer:no_progress");
5307
+ return {
5308
+ provider,
5309
+ phase,
5310
+ artifact,
5311
+ validation,
5312
+ events: events$1
5313
+ };
5314
+ }
5315
+ function classifyArtifact(observed, events$1) {
5316
+ if (observed.externalMutation === "merged") return "pr_merged";
5317
+ const prs = observed.prs;
5318
+ if (prs.length === 0) return observed.externalMutation === "closed" ? "pr_closed" : "no_pr";
5319
+ if (prs.length > 1) {
5320
+ events$1.push("multiple_prs");
5321
+ return "multiple_prs";
5322
+ }
5323
+ const pr = prs[0];
5324
+ if (pr.merged || pr.state === "MERGED") return "pr_merged";
5325
+ if (pr.state === "CLOSED") return "pr_closed";
5326
+ return "pr_open";
5327
+ }
5328
+ function classifyValidation(observed, artifact, row) {
5329
+ if (artifact !== "pr_open") return "unknown";
5330
+ const head = observed.prs[0]?.headSha;
5331
+ if ((row.validation === "floor_passed" || row.validation === "floor_failed") && row.floorSha != null && row.floorSha.length > 0 && head !== void 0 && head === row.floorSha) return row.validation;
5332
+ if (observed.floor === "failed") return "floor_failed";
5333
+ if (observed.floor === "passed") return "floor_passed";
5334
+ if (observed.floor === "pending") return "floor_pending";
5335
+ if (row.verifierAssigned === true && observed.verifierReviewed === true) return "floor_pending";
5336
+ if (observed.reviewDecision === "CHANGES_REQUESTED") return "changes_requested";
5337
+ const rollup = observed.ci?.rollup;
5338
+ if (rollup === "failing") return "ci_failed";
5339
+ if (rollup === "pending") return "ci_running";
5340
+ if (rollup === "passing") {
5341
+ if (observed.reviewDecision === "REVIEW_REQUIRED") return "review_pending";
5342
+ return "ci_passed";
5343
+ }
5344
+ if (rollup === "none") return observed.ci?.noCi === true ? "no_ci" : "ci_running";
5345
+ return "unknown";
5346
+ }
5347
+ function classifyPhase(observed, row, artifact, validation) {
5348
+ if (row.terminal || artifact === "pr_merged") return "done";
5349
+ if (artifact === "no_pr") {
5350
+ if (observed.planReady) return "plan";
5351
+ return row.dispatchMode === "plan" ? "plan" : "build";
5352
+ }
5353
+ if (validation === "floor_passed") return "merge";
5354
+ if (validation === "ci_failed" || validation === "changes_requested") return "fix";
5355
+ if (validation === "ci_passed" || validation === "review_pending" || validation === "no_ci") return "review";
5356
+ return "build";
5357
+ }
5358
+ function nextAction(state$1, row, policy) {
5359
+ if (state$1.phase === "done" || row.terminal) return { kind: "mark_done" };
5360
+ if (state$1.artifact === "pr_merged") return { kind: "mark_done" };
5361
+ if (state$1.artifact === "pr_closed") {
5362
+ if (row.cancelledBy === "controller") return { kind: "mark_done" };
5363
+ return {
5364
+ kind: "escalate_human",
5365
+ reason: "pull request was closed outside the first mate"
5366
+ };
5367
+ }
5368
+ if (state$1.artifact === "multiple_prs") return {
5369
+ kind: "escalate_human",
5370
+ reason: "the agent opened multiple pull requests for one unit"
5371
+ };
5372
+ if (state$1.provider === "failed" || state$1.provider === "timed_out") return {
5373
+ kind: "escalate_human",
5374
+ reason: `cloud agent task ${state$1.provider}`
5375
+ };
5376
+ if (state$1.provider === "waiting_for_user") {
5377
+ if (row.blockingDecisionId) return { kind: "noop" };
5378
+ return {
5379
+ kind: "ask_model",
5380
+ request: "answer_agent_question"
5381
+ };
5382
+ }
5383
+ if (state$1.phase === "plan" && state$1.provider === "completed" && state$1.artifact === "no_pr") {
5384
+ if (row.blockingDecisionId) return { kind: "noop" };
5385
+ return {
5386
+ kind: "ask_model",
5387
+ request: "review_plan"
5388
+ };
5389
+ }
5390
+ switch (state$1.validation) {
5391
+ case "ci_failed":
5392
+ case "changes_requested":
5393
+ if (row.retries < policy.maxRetries) return {
5394
+ kind: "ask_model",
5395
+ request: "author_fix"
5396
+ };
5397
+ return {
5398
+ kind: "escalate_human",
5399
+ reason: state$1.validation === "ci_failed" ? "CI still red after the self-heal retry cap" : "changes still requested after the self-heal retry cap"
5400
+ };
5401
+ case "ci_passed":
5402
+ case "no_ci":
5403
+ if (!row.verifierAssigned || row.verifierSha != null && row.headSha != null && row.verifierSha !== row.headSha) return { kind: "assign_verifier" };
5404
+ return { kind: "noop" };
5405
+ case "review_pending":
5406
+ if (!row.verifierAssigned) return { kind: "assign_verifier" };
5407
+ return { kind: "noop" };
5408
+ case "floor_failed":
5409
+ if (row.retries < policy.maxRetries) return {
5410
+ kind: "ask_model",
5411
+ request: "author_fix"
5412
+ };
5413
+ return {
5414
+ kind: "escalate_human",
5415
+ reason: "floor-keeper verdict is no-go after the retry cap"
5416
+ };
5417
+ case "floor_passed": return {
5418
+ kind: "escalate_human",
5419
+ reason: "ready to merge — approval required"
5420
+ };
5421
+ case "ci_running": return { kind: "noop" };
5422
+ case "floor_pending": return {
5423
+ kind: "ask_model",
5424
+ request: "judge_review"
5425
+ };
5426
+ default: break;
5427
+ }
5428
+ return { kind: "noop" };
5429
+ }
5430
+
5431
+ //#endregion
5432
+ //#region src/lib/first-mate/types.ts
5433
+ const DEFAULT_POLICY = { maxRetries: 3 };
5434
+
5435
+ //#endregion
5436
+ //#region src/lib/first-mate/controller.ts
5437
+ const MODEL_KINDS = [
5438
+ "review_plan",
5439
+ "answer_agent_question",
5440
+ "author_fix",
5441
+ "judge_review"
5442
+ ];
5443
+ const PROVIDER_STATES = new Set([
5444
+ "none",
5445
+ "queued",
5446
+ "in_progress",
5447
+ "waiting_for_user",
5448
+ "completed",
5449
+ "failed",
5450
+ "timed_out",
5451
+ "cancelled"
5452
+ ]);
5453
+ const DEFAULT_MAX_IN_FLIGHT_PER_PROVIDER = 6;
5454
+ const DEFAULT_TOP_K = 6;
5455
+ const defaultDeps = {
5456
+ loadAllUnits,
5457
+ readMissions,
5458
+ upsertUnit,
5459
+ pruneTerminal,
5460
+ observeUnit,
5461
+ classifyPlanReady,
5462
+ classifyQuestionAnswerable,
5463
+ classifyFixAddressed,
5464
+ classifyStuck,
5465
+ verifyAndConsumeApproval,
5466
+ recordApproval,
5467
+ upsertDecision,
5468
+ findByKey,
5469
+ markAnswered,
5470
+ startTask,
5471
+ followUpTask,
5472
+ cancelTask,
5473
+ createIssue,
5474
+ resolveAgentActor,
5475
+ resolveAgentRoster,
5476
+ assignAgent,
5477
+ findAgentPRs,
5478
+ getPullRequestState,
5479
+ postComment,
5480
+ submitReview,
5481
+ requestReview,
5482
+ rerunChecks,
5483
+ mergePullRequest,
5484
+ markReadyForReview,
5485
+ buildDecisionPacket,
5486
+ writeDecisionPacketHtml
5487
+ };
5488
+ function sanitizeSegment(value) {
5489
+ const cleaned = value.replace(/[^A-Za-z0-9._-]/g, "_").replace(/^\.+$/, "_");
5490
+ return cleaned.length > 0 ? cleaned : "_";
5491
+ }
5492
+ async function writeDecisionPacketHtml(packetId, html) {
5493
+ const dir = nodePath.join(PATHS.FIRST_MATE_DIR, "packets");
5494
+ await fs.mkdir(dir, { recursive: true });
5495
+ const target = nodePath.join(dir, `${sanitizeSegment(packetId)}.html`);
5496
+ await fs.writeFile(target, html, { mode: 384 });
5497
+ return target;
5498
+ }
5499
+ function agentRepo(repo) {
5500
+ return {
5501
+ owner: repo.owner,
5502
+ repo: repo.name
5503
+ };
5504
+ }
5505
+ function unitHandle(unit) {
5506
+ return String(unit.issue ?? unit.taskId);
5507
+ }
5508
+ function requestIdFor(unit, kind) {
5509
+ return `${unit.missionId}:${unitHandle(unit)}:${kind}`;
5510
+ }
5511
+ function humanRequestBase(unit, type) {
5512
+ return `${unit.missionId}:${unitHandle(unit)}:${type}`;
5513
+ }
5514
+ function asRecord$1(value) {
5515
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
5516
+ }
5517
+ function stringValue(value) {
5518
+ return typeof value === "string" && value.length > 0 ? value : void 0;
5519
+ }
5520
+ function booleanValue(value) {
5521
+ return typeof value === "boolean" ? value : void 0;
5522
+ }
5523
+ function numberValue(value) {
5524
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
5525
+ }
5526
+ /** Compact single-line error text for the `applied` audit trail. */
5527
+ function errText(err) {
5528
+ return (err instanceof Error ? err.message : String(err)).replace(/\s+/g, " ").slice(0, 200);
5529
+ }
5530
+ function compact(value, max = 1200) {
5531
+ if (value === void 0) return void 0;
5532
+ const trimmed = value.trim();
5533
+ if (trimmed.length <= max) return trimmed;
5534
+ return `${trimmed.slice(0, max - 16)}…[truncated]…`;
5535
+ }
5536
+ function providerState(value, fallback) {
5537
+ return PROVIDER_STATES.has(value) ? value : fallback;
5538
+ }
5539
+ function missionMap(missions) {
5540
+ return new Map(missions.map((mission) => [mission.id, mission]));
5541
+ }
5542
+ function repoLabel$1(repo) {
5543
+ return `${repo.owner}/${repo.name}`;
5544
+ }
5545
+ function sortKey(unit) {
5546
+ return unit.lastCheckedMs ?? unit.lastSteer?.atMs ?? 0;
5547
+ }
5548
+ function findModelTarget(units, requestId) {
5549
+ for (const unit of units) for (const kind of MODEL_KINDS) if (requestIdFor(unit, kind) === requestId) return {
5550
+ unit,
5551
+ kind
5552
+ };
5553
+ }
5554
+ function mergePolicy(input) {
5555
+ return {
5556
+ ...DEFAULT_POLICY,
5557
+ ...input ?? {}
5558
+ };
5559
+ }
5560
+ function positiveInteger(value, fallback) {
5561
+ if (value === void 0) return fallback;
5562
+ return Number.isInteger(value) && value > 0 ? value : fallback;
5563
+ }
5564
+ function observedRecord(observed) {
5565
+ return observed;
5566
+ }
5567
+ function initialEvidence(observed) {
5568
+ const record = observedRecord(observed);
5569
+ return {
5570
+ planExcerpt: stringValue(record.planExcerpt),
5571
+ logExcerpt: stringValue(record.logExcerpt),
5572
+ question: stringValue(record.question) ?? stringValue(record.agentQuestion) ?? stringValue(record.prompt),
5573
+ suggestedAnswer: stringValue(record.suggestedAnswer),
5574
+ failureSummary: stringValue(record.failureSummary) ?? stringValue(record.ciExcerpt) ?? stringValue(record.reviewExcerpt),
5575
+ latestLogExcerpt: stringValue(record.latestLogExcerpt),
5576
+ runId: numberValue(record.runId),
5577
+ prNodeId: stringValue(record.prNodeId)
5578
+ };
5579
+ }
5580
+ async function fillFuzzyFields(unit, mission, observed, deps) {
5581
+ const evidence = initialEvidence(observed);
5582
+ if (observed.provider === "completed" && observed.prs.length === 0 && observed.planReady === void 0 && evidence.logExcerpt !== void 0) {
5583
+ const result = await deps.classifyPlanReady(evidence.logExcerpt);
5584
+ if (result !== null) {
5585
+ observed.planReady = result.planReady;
5586
+ if (result.planExcerpt.length > 0) evidence.planExcerpt = result.planExcerpt;
5587
+ }
5588
+ unit.planExcerpt = evidence.planExcerpt ?? evidence.logExcerpt;
5589
+ }
5590
+ if (observed.provider === "waiting_for_user" && observed.agentQuestionAnswerableFromAC === void 0 && evidence.question !== void 0) {
5591
+ const result = await deps.classifyQuestionAnswerable(evidence.question, mission.acceptanceCriteria);
5592
+ if (result !== null) {
5593
+ observed.agentQuestionAnswerableFromAC = result.answerable;
5594
+ evidence.suggestedAnswer = result.answer;
5595
+ }
5596
+ }
5597
+ if (unit.lastSteer !== void 0 && observed.steerAcknowledged === void 0 && evidence.failureSummary !== void 0 && evidence.latestLogExcerpt !== void 0) {
5598
+ const result = await deps.classifyFixAddressed(evidence.failureSummary, evidence.latestLogExcerpt);
5599
+ if (result !== null) observed.steerAcknowledged = result.addressed;
5600
+ }
5601
+ if (observed.provider === "in_progress" && observed.steerAcknowledged === void 0 && evidence.logExcerpt !== void 0) {
5602
+ const result = await deps.classifyStuck(evidence.logExcerpt);
5603
+ if (result !== null && result.stuck) observed.steerAcknowledged = false;
5604
+ }
5605
+ return evidence;
5606
+ }
5607
+ function updateUnitFromObservedPrs(unit, observed) {
5608
+ if (observed.prs.length !== 1) return;
5609
+ const pr = observed.prs[0];
5610
+ unit.pr = pr.number;
5611
+ unit.headSha = pr.headSha || unit.headSha;
5612
+ }
5613
+ function modelPayload(kind, unit, mission, observed, evidence) {
5614
+ const common = {
5615
+ goal: compact(mission.goal, 1e3),
5616
+ acceptance_criteria: compact(mission.acceptanceCriteria, 1600),
5617
+ house_rules: compact(mission.houseRules, 1e3),
5618
+ unit_title: compact(unit.title, 500),
5619
+ repo: repoLabel$1(unit.repo),
5620
+ issue: unit.issue,
5621
+ pr: unit.pr,
5622
+ phase: unit.phase,
5623
+ validation: unit.validation,
5624
+ head_sha: unit.headSha,
5625
+ base_sha: unit.baseSha
5626
+ };
5627
+ if (kind === "review_plan") return {
5628
+ ...common,
5629
+ plan_excerpt: compact(evidence.planExcerpt || evidence.logExcerpt, 1200)
5630
+ };
5631
+ if (kind === "answer_agent_question") return {
5632
+ ...common,
5633
+ question: compact(evidence.question, 1e3),
5634
+ suggested_answer_from_ac: compact(evidence.suggestedAnswer, 1e3),
5635
+ answerable_from_acceptance_criteria: observed.agentQuestionAnswerableFromAC ?? null
5636
+ };
5637
+ if (kind === "author_fix") return {
5638
+ ...common,
5639
+ failure_summary: compact(evidence.failureSummary ?? `${unit.validation} on PR #${unit.pr ?? "unknown"}`, 1400),
5640
+ ci_rollup: observed.ci?.rollup,
5641
+ review_decision: observed.reviewDecision,
5642
+ floor_verdict: observed.floor
5643
+ };
5644
+ return {
5645
+ ...common,
5646
+ review_summary: compact(evidence.failureSummary, 1400),
5647
+ plan_excerpt: compact(unit.planExcerpt, 1e3),
5648
+ ci_rollup: observed.ci?.rollup,
5649
+ floor_verdict: observed.floor
5650
+ };
5651
+ }
5652
+ function buildModelRequest(unit, mission, kind, observed, evidence) {
5653
+ return {
5654
+ requestId: requestIdFor(unit, kind),
5655
+ kind,
5656
+ missionId: unit.missionId,
5657
+ repo: unit.repo,
5658
+ issue: unit.issue,
5659
+ pr: unit.pr,
5660
+ payload: modelPayload(kind, unit, mission, observed, evidence)
5661
+ };
5662
+ }
5663
+ function isMergeEscalation(unit, reason) {
5664
+ return unit.validation === "floor_passed" || reason.toLowerCase().includes("merge");
5665
+ }
5666
+ function decisionType(unit, reason) {
5667
+ return isMergeEscalation(unit, reason) ? "merge_approval" : "human_decision";
5668
+ }
5669
+ function inputFingerprint(unit, observed, reason) {
5670
+ const observedHead = observed.prs.length === 1 ? observed.prs[0]?.headSha : void 0;
5671
+ return [
5672
+ `pr=${unit.pr ?? "none"}`,
5673
+ `head=${unit.headSha ?? observedHead ?? "none"}`,
5674
+ `base=${unit.baseSha ?? "none"}`,
5675
+ `validation=${unit.validation}`,
5676
+ `artifact=${unit.artifact}`,
5677
+ `reason=${reason}`
5678
+ ].join("|");
5679
+ }
5680
+ function decisionKeyFor(unit, observed, reason) {
5681
+ const type = decisionType(unit, reason);
5682
+ const fingerprint = inputFingerprint(unit, observed, reason);
5683
+ return {
5684
+ type,
5685
+ fingerprint,
5686
+ decisionKey: `${humanRequestBase(unit, type)}:${fingerprint}`
5687
+ };
5688
+ }
5689
+ function decisionOptions(type) {
5690
+ if (type === "merge_approval") return [
5691
+ {
5692
+ id: "approve_merge",
5693
+ label: "Approve merge",
5694
+ consequence: "If a matching durable approval is recorded, the next wake may merge the live PR head.",
5695
+ recommended: true
5696
+ },
5697
+ {
5698
+ id: "hold",
5699
+ label: "Hold",
5700
+ consequence: "The controller will leave the PR open and ask again later."
5701
+ },
5702
+ {
5703
+ id: "abandon",
5704
+ label: "Abandon",
5705
+ consequence: "The unit will be marked terminal without merging."
5706
+ }
5707
+ ];
5708
+ return [{
5709
+ id: "continue",
5710
+ label: "Continue manually",
5711
+ consequence: "A human should decide the next implementation step.",
5712
+ recommended: true
5713
+ }, {
5714
+ id: "abandon",
5715
+ label: "Abandon",
5716
+ consequence: "The unit will be marked terminal without merging."
5717
+ }];
5718
+ }
5719
+ function packetInput(unit, mission, observed, reason, type) {
5720
+ const pr = unit.pr ?? (observed.prs.length === 1 ? observed.prs[0]?.number ?? null : null);
5721
+ return {
5722
+ type,
5723
+ tldr: type === "merge_approval" ? `Merge approval needed for ${unit.title}` : `${mission.goal}: ${reason}`,
5724
+ question: type === "merge_approval" ? `Approve merging ${repoLabel$1(unit.repo)} PR #${pr ?? "unknown"}?` : `How should first mate proceed? ${reason}`,
5725
+ options: decisionOptions(type),
5726
+ evidence: {
5727
+ prSummary: pr === null ? void 0 : `${repoLabel$1(unit.repo)} PR #${pr}`,
5728
+ ciExcerpt: observed.ci?.rollup,
5729
+ floorVerdict: observed.floor ?? unit.validation,
5730
+ links: pr === null ? void 0 : [{
5731
+ label: `PR #${pr}`,
5732
+ url: `https://github.com/${unit.repo.owner}/${unit.repo.name}/pull/${pr}`
5733
+ }]
5734
+ },
5735
+ missionId: unit.missionId,
5736
+ repo: unit.repo,
5737
+ unit: {
5738
+ issue: unit.issue,
5739
+ pr
5740
+ }
5741
+ };
5742
+ }
5743
+ function isAbandonChoice(choice) {
5744
+ const normalized = choice.toLowerCase();
5745
+ return normalized.includes("abandon") || normalized.includes("cancel");
5746
+ }
5747
+ function isApproveMergeChoice(choice) {
5748
+ const normalized = choice.toLowerCase();
5749
+ return normalized.includes("approve") || normalized === "merge";
5750
+ }
5751
+ async function applyModelAnswer(answer, units, missions, deps, applied) {
5752
+ const target = findModelTarget(units, answer.requestId);
5753
+ if (target === void 0) {
5754
+ consola.debug(`first-mate controller ignored unknown model answer ${answer.requestId}`);
5755
+ return;
5756
+ }
5757
+ const { unit, kind } = target;
5758
+ const verdict = asRecord$1(answer.verdict) ?? {};
5759
+ const repo = agentRepo(unit.repo);
5760
+ if (kind === "review_plan") {
5761
+ const decision = stringValue(verdict.decision);
5762
+ const mission = missions.find((entry) => entry.id === unit.missionId);
5763
+ if (decision === "approve") {
5764
+ if (mission !== void 0) {
5765
+ const task = await dispatchWithOutbox(unit, deps, ({ idempotencyKey, promptTag }) => deps.startTask(repo, {
5766
+ prompt: buildPrompt(unit, mission) + promptTag,
5767
+ createPullRequest: true,
5768
+ idempotencyKey
5769
+ }));
5770
+ if (task) {
5771
+ unit.taskId = task.taskId;
5772
+ unit.provider = providerState(task.state, "queued");
5773
+ unit.phase = "build";
5774
+ unit.dispatchMode = "build";
5775
+ unit.implementerLab = unit.agent;
5776
+ unit.lastSteer = { atMs: Date.now() };
5777
+ applied.push(`approved plan → dispatched build for ${unit.missionId}:${unitHandle(unit)}`);
5778
+ }
5779
+ }
5780
+ } else if (decision === "refine") {
5781
+ const instruction = stringValue(verdict.instruction) ?? "Refine the plan with more concrete implementation steps.";
5782
+ if (mission !== void 0) {
5783
+ const prompt = `${planPrompt(unit, mission)}\n\nRefine your previous plan per this feedback:\n${instruction}`;
5784
+ const task = await dispatchWithOutbox(unit, deps, ({ idempotencyKey, promptTag }) => deps.startTask(repo, {
5785
+ prompt: prompt + promptTag,
5786
+ createPullRequest: false,
5787
+ idempotencyKey
5788
+ }));
5789
+ if (task) {
5790
+ unit.taskId = task.taskId;
5791
+ unit.provider = providerState(task.state, "queued");
5792
+ unit.phase = "plan";
5793
+ unit.dispatchMode = "plan";
5794
+ unit.planExcerpt = void 0;
5795
+ unit.lastSteer = { atMs: Date.now() };
5796
+ applied.push(`requested plan refinement for ${unit.missionId}:${unitHandle(unit)}`);
5797
+ }
5798
+ }
5799
+ }
5800
+ } else if (kind === "author_fix") {
5801
+ const instruction = stringValue(verdict.instruction) ?? "Fix the reported validation failure and update the PR.";
5802
+ if (unit.pr !== null) await deps.submitReview(repo, unit.pr, "REQUEST_CHANGES", instruction);
5803
+ unit.retries += 1;
5804
+ unit.phase = "fix";
5805
+ unit.lastSteer = {
5806
+ sha: unit.headSha ?? void 0,
5807
+ atMs: Date.now()
5808
+ };
5809
+ applied.push(`sent fix instruction for ${unit.missionId}:${unitHandle(unit)}`);
5810
+ } else if (kind === "answer_agent_question") {
5811
+ const answerText = stringValue(verdict.answer);
5812
+ if (answerText !== void 0 && unit.pr !== null) {
5813
+ await deps.postComment(repo, unit.pr, answerText);
5814
+ unit.lastSteer = {
5815
+ sha: unit.headSha ?? void 0,
5816
+ atMs: Date.now()
5817
+ };
5818
+ applied.push(`answered agent question for ${unit.missionId}:${unitHandle(unit)}`);
5819
+ }
5820
+ } else if (kind === "judge_review") {
5821
+ if (!(unit.verifierAssigned === true && (unit.validation === "review_pending" || unit.validation === "ci_passed" || unit.validation === "no_ci" || unit.validation === "floor_pending"))) {
5822
+ consola.debug(`first-mate: ignoring judge_review for ${unit.missionId}:${unitHandle(unit)} — unit is not in a verification state`);
5823
+ return;
5824
+ }
5825
+ const passed = booleanValue(verdict.pass) === true;
5826
+ unit.validation = passed ? "floor_passed" : "floor_failed";
5827
+ unit.floorSha = unit.headSha ?? null;
5828
+ if (unit.pr !== null) {
5829
+ const reason = stringValue(verdict.reason) ?? (passed ? "Verified: meets acceptance criteria." : "Changes requested by cross-lab verification.");
5830
+ try {
5831
+ await deps.submitReview(repo, unit.pr, passed ? "APPROVE" : "REQUEST_CHANGES", reason);
5832
+ } catch (err) {
5833
+ consola.debug(`first-mate: posting judge verdict review failed for ${unit.missionId}:${unitHandle(unit)}:`, err);
5834
+ }
5835
+ }
5836
+ applied.push(`recorded verifier judgment (${passed ? "pass" : "fail"}) for ${unit.missionId}:${unitHandle(unit)}`);
5837
+ }
5838
+ await deps.upsertUnit(unit.repo, unit);
5839
+ }
5840
+ async function applyHumanDecision(decision, units, deps, applied) {
5841
+ const decisionId = (await deps.findByKey(decision.requestId))?.decisionId ?? units.find((unit) => unit.blockingDecisionId === decision.requestId)?.blockingDecisionId;
5842
+ if (decisionId === void 0 || decisionId === null) {
5843
+ consola.debug(`first-mate controller ignored unknown human decision ${decision.requestId}`);
5844
+ return;
5845
+ }
5846
+ await deps.markAnswered(decisionId, decision.choice, "human");
5847
+ for (const unit of units.filter((row) => row.blockingDecisionId === decisionId)) {
5848
+ unit.blockingDecisionId = null;
5849
+ if (isAbandonChoice(decision.choice)) {
5850
+ unit.terminal = true;
5851
+ unit.phase = "done";
5852
+ unit.cancelledBy = "external";
5853
+ } else if (isApproveMergeChoice(decision.choice) && unit.pr !== null) {
5854
+ if (unit.validation !== "floor_passed") {
5855
+ consola.debug(`first-mate: ignoring merge approval for ${unitHandle(unit)} — unit is not floor_passed`);
5856
+ await deps.upsertUnit(unit.repo, unit);
5857
+ continue;
5858
+ }
5859
+ try {
5860
+ const live = await deps.getPullRequestState(agentRepo(unit.repo), unit.pr);
5861
+ if (unit.floorSha != null && unit.floorSha.length > 0 && live.headSha !== unit.floorSha) consola.warn(`first-mate: refusing merge approval for ${repoLabel$1(unit.repo)}#${live.number} — head moved since the floor verdict; re-verification required`);
5862
+ else if (live.headSha.length > 0) {
5863
+ await deps.recordApproval({
5864
+ decisionId,
5865
+ repo: unit.repo,
5866
+ pr: live.number,
5867
+ headSha: live.headSha,
5868
+ baseSha: live.baseSha
5869
+ });
5870
+ applied.push(`recorded merge approval for ${repoLabel$1(unit.repo)}#${live.number}`);
5871
+ }
5872
+ } catch (err) {
5873
+ consola.debug("first-mate: could not record merge approval", err);
5874
+ }
5875
+ }
5876
+ await deps.upsertUnit(unit.repo, unit);
5877
+ }
5878
+ applied.push(`recorded human decision ${decision.choice}`);
5879
+ }
5880
+ async function applySubmittedAnswers(input, deps, applied) {
5881
+ const units = await deps.loadAllUnits();
5882
+ const missions = await deps.readMissions();
5883
+ for (const answer of input.modelAnswers ?? []) try {
5884
+ if (answer.requestId.startsWith("decompose:")) await applyDecomposeAnswer(answer, missions, deps, applied);
5885
+ else await applyModelAnswer(answer, units, missions, deps, applied);
5886
+ } catch (err) {
5887
+ consola.warn(`first-mate: model answer ${answer.requestId} failed to apply:`, err);
5888
+ applied.push(`error applying answer ${answer.requestId}: ${errText(err)}`);
2960
5889
  }
2961
- let instances;
2962
- if (instanceArgs !== void 0 && instanceArgs.length > 0) {
2963
- instances = uniqueInstances(await Promise.all(instanceArgs.map((arg) => registry.resolveInstance(arg))));
2964
- const ids = new Set(instances.map((instance) => instance.id));
2965
- for (const instanceId of localSessionIdsByInstance.keys()) if (!ids.has(instanceId)) throw new FleetToolInputError("INSTANCE_MISMATCH", `sessionIds include instance ${JSON.stringify(instanceId)} which is not in arguments.instances`);
2966
- } else if (localSessionIdsByInstance.size > 0) instances = uniqueInstances(await Promise.all([...localSessionIdsByInstance.keys()].map((instanceId) => registry.resolveInstance(instanceId))));
2967
- else {
2968
- const infos = await registry.listInstances();
2969
- if (infos.length === 0) throw new FleetRegistryError("INSTANCE_REQUIRED", "await_turn requires at least one registered fleet instance");
2970
- instances = uniqueInstances(await Promise.all(infos.map((info) => registry.resolveInstance(info.id))));
5890
+ for (const decision of input.humanDecisions ?? []) try {
5891
+ await applyHumanDecision(decision, units, deps, applied);
5892
+ } catch (err) {
5893
+ consola.warn(`first-mate: human decision ${decision.requestId} failed to apply:`, err);
5894
+ applied.push(`error applying decision ${decision.requestId}: ${errText(err)}`);
2971
5895
  }
2972
- return {
2973
- instances,
2974
- localSessionIdsByInstance
2975
- };
2976
5896
  }
2977
- function globalizeSession(instanceId, session) {
5897
+ /** Parse an "owner/name" repo string into a RepoRef. */
5898
+ function parseRepoRef$1(value) {
5899
+ if (value === void 0) return void 0;
5900
+ const parts = value.split("/");
5901
+ if (parts.length !== 2 || parts[0].length === 0 || parts[1].length === 0) return;
2978
5902
  return {
2979
- ...session,
2980
- sessionId: encodeSessionId(instanceId, session.sessionId)
2981
- };
5903
+ owner: parts[0],
5904
+ name: parts[1]
5905
+ };
5906
+ }
5907
+ function asAgentKey(value) {
5908
+ return value === "copilot" || value === "anthropic" || value === "openai" ? value : void 0;
5909
+ }
5910
+ /**
5911
+ * Turn a model `decompose` answer into queued units. This is the mission→units
5912
+ * step: `start_mission` only registers the mission; `advance` emits one
5913
+ * `decompose` request per unit-less active mission, and the model answers with
5914
+ * `{ units: [{ title, repo?, agent?, dependsOn? }] }`. Each unit gets a stable
5915
+ * `id` so it survives the queued→dispatched transition without duplicating.
5916
+ */
5917
+ async function applyDecomposeAnswer(answer, missions, deps, applied) {
5918
+ const missionId = answer.requestId.slice(10);
5919
+ const mission = missions.find((m) => m.id === missionId);
5920
+ if (mission === void 0) return;
5921
+ const verdict = asRecord$1(answer.verdict) ?? {};
5922
+ const rawUnits = Array.isArray(verdict.units) ? verdict.units : [];
5923
+ const specs = [];
5924
+ for (const raw of rawUnits) {
5925
+ const spec = asRecord$1(raw) ?? {};
5926
+ const title = stringValue(spec.title);
5927
+ if (title === void 0 || title.length === 0) continue;
5928
+ const repo = parseRepoRef$1(stringValue(spec.repo)) ?? mission.repos[0];
5929
+ if (repo === void 0) continue;
5930
+ specs.push({
5931
+ spec,
5932
+ title,
5933
+ repo
5934
+ });
5935
+ }
5936
+ const ids = specs.map(() => randomUUID());
5937
+ let created = 0;
5938
+ for (let i = 0; i < specs.length; i += 1) {
5939
+ const { spec, title, repo } = specs[i];
5940
+ const dependsOn = (Array.isArray(spec.dependsOn) ? spec.dependsOn : []).filter((idx) => typeof idx === "number" && Number.isInteger(idx) && idx >= 0 && idx < ids.length && idx !== i).map((idx) => ids[idx]);
5941
+ const unit = {
5942
+ id: ids[i],
5943
+ missionId,
5944
+ repo,
5945
+ issue: null,
5946
+ pr: null,
5947
+ taskId: null,
5948
+ agent: asAgentKey(stringValue(spec.agent)) ?? "copilot",
5949
+ botLogin: "",
5950
+ dispatchMode: "plan",
5951
+ provider: "none",
5952
+ phase: "plan",
5953
+ artifact: "no_pr",
5954
+ validation: "unknown",
5955
+ retries: 0,
5956
+ dependsOn,
5957
+ title
5958
+ };
5959
+ await deps.upsertUnit(repo, unit);
5960
+ created += 1;
5961
+ }
5962
+ if (created > 0) applied.push(`decomposed ${missionId} into ${created} unit(s)`);
5963
+ }
5964
+ async function maybeMergeWithApproval(unit, observed, evidence, deps, applied) {
5965
+ if (unit.validation !== "floor_passed" && observed.floor !== "passed") return false;
5966
+ const pr = unit.pr ?? (observed.prs.length === 1 ? observed.prs[0]?.number ?? null : null);
5967
+ if (pr === null) return false;
5968
+ const live = await deps.getPullRequestState(agentRepo(unit.repo), pr);
5969
+ unit.pr = live.number;
5970
+ unit.headSha = live.headSha || unit.headSha;
5971
+ unit.baseSha = live.baseSha ?? unit.baseSha;
5972
+ unit.branch = live.baseRef || unit.branch;
5973
+ if (unit.floorSha != null && unit.floorSha.length > 0 && live.headSha.length > 0 && live.headSha !== unit.floorSha) return false;
5974
+ const head = live.headSha.length > 0 ? live.headSha : unit.headSha ?? void 0;
5975
+ if (head === void 0 || head.length === 0) return false;
5976
+ if (!(await deps.verifyAndConsumeApproval({
5977
+ repo: unit.repo,
5978
+ pr: live.number,
5979
+ liveHeadSha: head,
5980
+ liveBaseSha: live.baseSha
5981
+ })).ok) return false;
5982
+ if (live.isDraft && evidence.prNodeId !== void 0) await deps.markReadyForReview(evidence.prNodeId);
5983
+ await deps.mergePullRequest(agentRepo(unit.repo), {
5984
+ pr: live.number,
5985
+ expectedHeadSha: head
5986
+ });
5987
+ unit.terminal = true;
5988
+ unit.phase = "done";
5989
+ unit.artifact = "pr_merged";
5990
+ unit.validation = "floor_passed";
5991
+ applied.push(`merged ${repoLabel$1(unit.repo)}#${live.number}`);
5992
+ await deps.upsertUnit(unit.repo, unit);
5993
+ return true;
2982
5994
  }
2983
- function stampEvent(instance, event) {
5995
+ async function createHumanRequest(unit, mission, observed, reason, deps) {
5996
+ const { decisionKey, fingerprint, type } = decisionKeyFor(unit, observed, reason);
5997
+ const existing = await deps.findByKey(decisionKey);
5998
+ let record = existing?.status === "pending" ? existing : void 0;
5999
+ let packetHtmlPath;
6000
+ if (record === void 0) {
6001
+ const packet = deps.buildDecisionPacket(packetInput(unit, mission, observed, reason, type));
6002
+ packetHtmlPath = await deps.writeDecisionPacketHtml(packet.packetId, packet.html);
6003
+ record = {
6004
+ decisionId: packet.decisionId,
6005
+ decisionKey,
6006
+ type,
6007
+ status: "pending",
6008
+ packetId: packet.packetId,
6009
+ inputFingerprint: fingerprint,
6010
+ options: decisionOptions(type).map((option) => ({ id: option.id })),
6011
+ createdMs: Date.now()
6012
+ };
6013
+ await deps.upsertDecision(record);
6014
+ }
6015
+ unit.blockingDecisionId = record.decisionId;
2984
6016
  return {
2985
- ...event,
2986
- instance: publicInstance(instance),
2987
- ...typeof event.sessionId === "string" ? { sessionId: encodeSessionId(instance.id, event.sessionId) } : {}
6017
+ requestId: decisionKey,
6018
+ decisionId: record.decisionId,
6019
+ missionId: unit.missionId,
6020
+ repo: unit.repo,
6021
+ issue: unit.issue,
6022
+ pr: unit.pr,
6023
+ reason,
6024
+ ...packetHtmlPath !== void 0 ? { packetHtmlPath } : {}
2988
6025
  };
2989
6026
  }
2990
- function eventAtMs(value) {
2991
- if (typeof value === "number" && Number.isFinite(value)) return value;
2992
- if (typeof value === "string") {
2993
- const parsed = Date.parse(value);
2994
- if (!Number.isNaN(parsed)) return parsed;
6027
+ async function assignVerifier(unit, deps, applied) {
6028
+ if (unit.pr === null) return false;
6029
+ await deps.requestReview(agentRepo(unit.repo), unit.pr, COPILOT_REVIEWER_LOGIN);
6030
+ unit.verifierAssigned = true;
6031
+ unit.verifierSha = unit.headSha ?? void 0;
6032
+ unit.validation = "floor_pending";
6033
+ unit.lastSteer = { atMs: Date.now() };
6034
+ applied.push(`requested Copilot code review for ${unit.missionId}:${unitHandle(unit)} PR #${unit.pr}`);
6035
+ return true;
6036
+ }
6037
+ async function executeAction(action, unit, mission, observed, evidence, policy, deps, needsModel, needsHuman, applied, order) {
6038
+ switch (action.kind) {
6039
+ case "dispatch": return;
6040
+ case "steer":
6041
+ consola.debug("first-mate controller received direct steer action; v1 skips it");
6042
+ return;
6043
+ case "assign_verifier":
6044
+ if (await assignVerifier(unit, deps, applied)) return;
6045
+ needsHuman.push({
6046
+ request: await createHumanRequest(unit, mission, observed, "no different-lab verifier is available", deps),
6047
+ sortKey: sortKey(unit),
6048
+ order
6049
+ });
6050
+ return;
6051
+ case "rerun_ci":
6052
+ if (evidence.runId !== void 0) {
6053
+ await deps.rerunChecks(agentRepo(unit.repo), {
6054
+ runId: evidence.runId,
6055
+ failedOnly: true
6056
+ });
6057
+ applied.push(`reran checks for ${unit.missionId}:${unitHandle(unit)}`);
6058
+ }
6059
+ return;
6060
+ case "cancel":
6061
+ if (unit.taskId !== null) await deps.cancelTask(agentRepo(unit.repo), unit.taskId);
6062
+ unit.terminal = true;
6063
+ unit.phase = "done";
6064
+ unit.cancelledBy = "controller";
6065
+ applied.push(`cancelled ${unit.missionId}:${unitHandle(unit)}`);
6066
+ return;
6067
+ case "mark_done":
6068
+ unit.terminal = true;
6069
+ unit.phase = "done";
6070
+ applied.push(`marked done ${unit.missionId}:${unitHandle(unit)}`);
6071
+ return;
6072
+ case "ask_model":
6073
+ needsModel.push({
6074
+ request: buildModelRequest(unit, mission, action.request, observed, evidence),
6075
+ sortKey: sortKey(unit),
6076
+ order
6077
+ });
6078
+ return;
6079
+ case "escalate_human":
6080
+ needsHuman.push({
6081
+ request: await createHumanRequest(unit, mission, observed, action.reason, deps),
6082
+ sortKey: sortKey(unit),
6083
+ order
6084
+ });
6085
+ return;
6086
+ case "merge":
6087
+ case "mark_rebase":
6088
+ case "noop": return;
2995
6089
  }
2996
- return 0;
2997
6090
  }
2998
- function compareStampedEvents(a, b) {
2999
- const atA = eventAtMs(a.at);
3000
- const atB = eventAtMs(b.at);
3001
- if (atA !== atB) return atA - atB;
3002
- return (typeof a.seq === "number" ? a.seq : 0) - (typeof b.seq === "number" ? b.seq : 0);
6091
+ function isUndispatched(unit) {
6092
+ return unit.provider === "none" && unit.taskId === null && unit.dispatch === void 0;
3003
6093
  }
3004
- const MAX_WATCHER_ID_LEN = 200;
3005
- const MAX_AWAIT_TURN_CURSOR_KEYS = 1024;
3006
- function awaitTurnCursorKey(watcherId) {
3007
- const id = watcherId ?? "default";
3008
- return id.length > MAX_WATCHER_ID_LEN ? id.slice(0, MAX_WATCHER_ID_LEN) : id;
6094
+ /** A dispatch that was interrupted mid-flight (intent persisted, no taskId yet). */
6095
+ function isDispatchInterrupted(unit) {
6096
+ return unit.dispatch !== void 0 && unit.taskId === null;
3009
6097
  }
3010
- function takeAwaitTurnCursorMap(clientKey) {
3011
- const existing = awaitTurnCursors.get(clientKey);
3012
- if (existing) {
3013
- awaitTurnCursors.delete(clientKey);
3014
- awaitTurnCursors.set(clientKey, existing);
3015
- return existing;
6098
+ function isActiveMissionUnit(unit, missions) {
6099
+ return missions.get(unit.missionId)?.status === "active";
6100
+ }
6101
+ function isActiveUnit(unit, missions) {
6102
+ return isActiveMissionUnit(unit, missions) && unit.terminal !== true;
6103
+ }
6104
+ function isInFlight(unit) {
6105
+ return unit.terminal !== true && unit.taskId !== null && (unit.provider === "queued" || unit.provider === "in_progress" || unit.provider === "waiting_for_user");
6106
+ }
6107
+ function depsSatisfied(unit, units) {
6108
+ return unit.dependsOn.every((depId) => units.some((candidate) => candidate.id === depId && candidate.terminal === true && candidate.artifact === "pr_merged"));
6109
+ }
6110
+ function activeCountsByAgent(units) {
6111
+ const counts = /* @__PURE__ */ new Map();
6112
+ for (const unit of units) {
6113
+ if (!isInFlight(unit)) continue;
6114
+ counts.set(unit.agent, (counts.get(unit.agent) ?? 0) + 1);
3016
6115
  }
3017
- const created = /* @__PURE__ */ new Map();
3018
- awaitTurnCursors.set(clientKey, created);
3019
- while (awaitTurnCursors.size > MAX_AWAIT_TURN_CURSOR_KEYS) {
3020
- const oldest = awaitTurnCursors.keys().next().value;
3021
- if (oldest === void 0) break;
3022
- awaitTurnCursors.delete(oldest);
6116
+ return counts;
6117
+ }
6118
+ function planPrompt(unit, mission) {
6119
+ const parts = [
6120
+ `Mission goal:\n${mission.goal}`,
6121
+ `Acceptance criteria:\n${mission.acceptanceCriteria}`,
6122
+ `Work unit:\n${unit.title}`,
6123
+ "Analyze the repository and produce a concrete, step-by-step implementation plan for this work unit: the files you will change, the approach, key risks, and how each acceptance criterion will be verified. Do NOT edit code or open a pull request yet — output the plan and stop. It will be reviewed before implementation."
6124
+ ];
6125
+ if (mission.houseRules !== void 0) parts.splice(2, 0, `House rules:\n${mission.houseRules}`);
6126
+ return parts.join("\n\n");
6127
+ }
6128
+ function buildPrompt(unit, mission) {
6129
+ const parts = [
6130
+ `Mission goal:\n${mission.goal}`,
6131
+ `Acceptance criteria:\n${mission.acceptanceCriteria}`,
6132
+ `Work unit:\n${unit.title}`
6133
+ ];
6134
+ if (mission.houseRules !== void 0) parts.push(`House rules:\n${mission.houseRules}`);
6135
+ if (unit.planExcerpt !== void 0 && unit.planExcerpt.trim().length > 0) parts.push(`Approved plan (implement this):\n${unit.planExcerpt.trim()}`);
6136
+ parts.push("Implement this work unit end-to-end on a new branch and open a pull request for review. Follow the approved plan above. Keep the change focused on this unit and do not modify unrelated files. If anything about the acceptance criteria is ambiguous, make a reasonable choice and note it in the PR description.");
6137
+ return parts.join("\n\n");
6138
+ }
6139
+ /**
6140
+ * Durable dispatch outbox. Persists a dispatch-intent (with a correlation id)
6141
+ * BEFORE the irreversible startTask, so a crash in the startTask→persist window
6142
+ * leaves the unit marked (not `isUndispatched`) and is never blind-re-dispatched.
6143
+ * `start` receives the correlation id to embed in the prompt and send as the
6144
+ * Idempotency-Key. On success the intent is cleared (in memory; the caller's
6145
+ * upsertUnit persists it alongside the taskId). On throw the intent stays
6146
+ * pending on disk (unknown outcome — recovery, not re-dispatch, resolves it).
6147
+ * Returns null when the API returned no taskId — treated as ambiguous, so the
6148
+ * intent is LEFT pending (recovery escalates; never auto-re-dispatch).
6149
+ */
6150
+ async function dispatchWithOutbox(unit, deps, start) {
6151
+ const id = randomUUID();
6152
+ unit.dispatch = {
6153
+ id,
6154
+ requestedMs: Date.now(),
6155
+ attempts: (unit.dispatch?.attempts ?? 0) + 1
6156
+ };
6157
+ await deps.upsertUnit(unit.repo, unit);
6158
+ const task = await start({
6159
+ idempotencyKey: id,
6160
+ promptTag: `\n\n<!-- fm-dispatch:${id} -->`
6161
+ });
6162
+ if (task.taskId.length === 0) return null;
6163
+ unit.dispatch = void 0;
6164
+ return task;
6165
+ }
6166
+ async function dispatchUnit(unit, mission, deps) {
6167
+ const repo = agentRepo(unit.repo);
6168
+ const actor = await deps.resolveAgentActor(repo, unit.agent);
6169
+ const task = await dispatchWithOutbox(unit, deps, ({ idempotencyKey, promptTag }) => deps.startTask(repo, {
6170
+ prompt: planPrompt(unit, mission) + promptTag,
6171
+ createPullRequest: false,
6172
+ idempotencyKey
6173
+ }));
6174
+ if (task === null) return;
6175
+ unit.taskId = task.taskId;
6176
+ unit.provider = providerState(task.state, "queued");
6177
+ unit.botLogin = actor.login;
6178
+ unit.dispatchMode = "plan";
6179
+ unit.phase = "plan";
6180
+ unit.implementerLab = unit.agent;
6181
+ unit.lastSteer = { atMs: Date.now() };
6182
+ }
6183
+ async function dispatchWave(units, missions, maxInFlightPerProvider, deps, applied) {
6184
+ const counts = activeCountsByAgent(units);
6185
+ const candidates = units.filter((unit) => isActiveUnit(unit, missions)).filter((unit) => !unit.blockingDecisionId).filter(isUndispatched).filter((unit) => depsSatisfied(unit, units)).map((unit, index) => ({
6186
+ unit,
6187
+ index
6188
+ })).sort((a, b) => sortKey(a.unit) - sortKey(b.unit) || a.index - b.index);
6189
+ for (const { unit } of candidates) {
6190
+ const current = counts.get(unit.agent) ?? 0;
6191
+ if (current >= maxInFlightPerProvider) continue;
6192
+ const mission = missions.get(unit.missionId);
6193
+ if (mission === void 0) continue;
6194
+ try {
6195
+ await dispatchUnit(unit, mission, deps);
6196
+ counts.set(unit.agent, current + 1);
6197
+ await deps.upsertUnit(unit.repo, unit);
6198
+ applied.push(`dispatched ${unit.missionId}:${unitHandle(unit)} to ${unit.agent}`);
6199
+ } catch (err) {
6200
+ consola.warn(`first-mate: dispatch of ${unit.missionId}:${unitHandle(unit)} failed:`, err);
6201
+ applied.push(`error dispatching ${unit.missionId}:${unitHandle(unit)}: ${errText(err)}`);
6202
+ }
6203
+ }
6204
+ }
6205
+ function buildBoard$1(units, missions) {
6206
+ const rows = [];
6207
+ for (const mission of missions.filter((entry) => entry.status === "active")) {
6208
+ const missionUnits = units.filter((unit) => unit.missionId === mission.id);
6209
+ const counts = {};
6210
+ for (const unit of missionUnits) counts[unit.phase] = (counts[unit.phase] ?? 0) + 1;
6211
+ rows.push({
6212
+ missionId: mission.id,
6213
+ title: mission.goal,
6214
+ repos: mission.repos.map(repoLabel$1),
6215
+ counts,
6216
+ blocked: missionUnits.filter((unit) => unit.blockingDecisionId).length
6217
+ });
3023
6218
  }
3024
- return created;
6219
+ return rows;
3025
6220
  }
3026
- function isAwaitTurnSuccess(result) {
3027
- return result.ok;
6221
+ function compareQueued(a, b) {
6222
+ return a.sortKey - b.sortKey || a.order - b.order;
3028
6223
  }
3029
- function isAwaitTurnFailure(result) {
3030
- return !result.ok;
6224
+ function capQueued(entries, topK) {
6225
+ return entries.sort(compareQueued).slice(0, topK).map((entry) => entry.request);
3031
6226
  }
3032
- function failedProbeResult(info, code) {
3033
- const hint = fleetProbeHint(code);
6227
+ function nextWakeAt(units, missions) {
6228
+ const active = units.filter((unit) => isActiveUnit(unit, missions));
6229
+ if (active.length === 0) return null;
6230
+ const now = Date.now();
6231
+ if (active.some((unit) => unit.validation === "ci_running" || unit.provider === "in_progress")) return now + 9e4;
6232
+ if (active.every((unit) => Boolean(unit.blockingDecisionId) || unit.provider === "none" || unit.provider === "queued")) return now + 9e5;
6233
+ return now + 3e5;
6234
+ }
6235
+ const MIN_WAKE_SECONDS = 60;
6236
+ const MAX_WAKE_SECONDS = 3600;
6237
+ function wakeSeconds(wakeAt) {
6238
+ if (wakeAt === null) return null;
6239
+ const seconds = Math.round((wakeAt - Date.now()) / 1e3);
6240
+ return Math.min(MAX_WAKE_SECONDS, Math.max(MIN_WAKE_SECONDS, seconds));
6241
+ }
6242
+ async function pruneTerminalRepos(units, deps) {
6243
+ const repos = /* @__PURE__ */ new Map();
6244
+ for (const unit of units) {
6245
+ if (unit.terminal !== true) continue;
6246
+ repos.set(repoLabel$1(unit.repo), unit.repo);
6247
+ }
6248
+ for (const repo of repos.values()) await deps.pruneTerminal(repo);
6249
+ }
6250
+ /**
6251
+ * Single-pass deterministic controller wake. Real deployments should wrap this
6252
+ * in a per-repo lock before durable ledger writes; the engine itself is kept
6253
+ * dependency-injected so tests can run without network or filesystem effects.
6254
+ */
6255
+ async function advance(input = {}, deps = defaultDeps) {
6256
+ const applied = [];
6257
+ const needsModel = [];
6258
+ const needsHuman = [];
6259
+ const policy = mergePolicy(input.policy);
6260
+ const maxInFlightPerProvider = positiveInteger(input.maxInFlightPerProvider, DEFAULT_MAX_IN_FLIGHT_PER_PROVIDER);
6261
+ const topK = positiveInteger(input.topK, DEFAULT_TOP_K);
6262
+ await applySubmittedAnswers(input, deps, applied);
6263
+ const units = await deps.loadAllUnits();
6264
+ const missions = await deps.readMissions();
6265
+ const missionsById = missionMap(missions);
6266
+ let order = 0;
6267
+ for (const unit of units.filter((row) => isActiveUnit(row, missionsById))) {
6268
+ const requestOrder = order;
6269
+ order += 1;
6270
+ if (unit.blockingDecisionId) continue;
6271
+ if (isUndispatched(unit)) continue;
6272
+ const mission = missionsById.get(unit.missionId);
6273
+ if (mission === void 0) continue;
6274
+ try {
6275
+ if (isDispatchInterrupted(unit)) {
6276
+ needsHuman.push({
6277
+ request: await createHumanRequest(unit, mission, {
6278
+ provider: unit.provider,
6279
+ prs: []
6280
+ }, `dispatch interrupted before the task id was recorded (correlation ${unit.dispatch?.id ?? "?"}) — verify no orphan task on ${repoLabel$1(unit.repo)} before re-dispatch`, deps),
6281
+ sortKey: sortKey(unit),
6282
+ order: requestOrder
6283
+ });
6284
+ await deps.upsertUnit(unit.repo, unit);
6285
+ continue;
6286
+ }
6287
+ const observed = await deps.observeUnit(unit);
6288
+ const evidence = await fillFuzzyFields(unit, mission, observed, deps);
6289
+ updateUnitFromObservedPrs(unit, observed);
6290
+ unit.lastCheckedMs = Date.now();
6291
+ if (await maybeMergeWithApproval(unit, observed, evidence, deps, applied)) continue;
6292
+ const classified = classify(observed, unit);
6293
+ unit.provider = classified.provider;
6294
+ unit.phase = classified.phase;
6295
+ unit.artifact = classified.artifact;
6296
+ unit.validation = classified.validation;
6297
+ await executeAction(nextAction(classified, unit, policy), unit, mission, observed, evidence, policy, deps, needsModel, needsHuman, applied, requestOrder);
6298
+ await deps.upsertUnit(unit.repo, unit);
6299
+ } catch (err) {
6300
+ consola.warn(`first-mate: unit ${unit.missionId}:${unitHandle(unit)} step failed:`, err);
6301
+ applied.push(`error advancing ${unit.missionId}:${unitHandle(unit)}: ${errText(err)}`);
6302
+ }
6303
+ }
6304
+ for (const mission of missions) {
6305
+ if (mission.status !== "active") continue;
6306
+ if (units.some((unit) => unit.missionId === mission.id)) continue;
6307
+ const repo = mission.repos[0];
6308
+ if (repo === void 0) continue;
6309
+ needsModel.push({
6310
+ request: {
6311
+ requestId: `decompose:${mission.id}`,
6312
+ kind: "decompose",
6313
+ missionId: mission.id,
6314
+ repo,
6315
+ issue: null,
6316
+ pr: null,
6317
+ payload: {
6318
+ goal: mission.goal,
6319
+ acceptance_criteria: mission.acceptanceCriteria,
6320
+ repos: mission.repos.map((entry) => `${entry.owner}/${entry.name}`),
6321
+ house_rules: mission.houseRules ?? null
6322
+ }
6323
+ },
6324
+ sortKey: 0,
6325
+ order: order++
6326
+ });
6327
+ }
6328
+ await dispatchWave(units, missionsById, maxInFlightPerProvider, deps, applied);
6329
+ const board = buildBoard$1(units, missions);
6330
+ const wakeAt = nextWakeAt(units, missionsById);
6331
+ await pruneTerminalRepos(units, deps);
3034
6332
  return {
3035
- id: info.id,
3036
- label: info.label,
3037
- reachable: false,
3038
- error: code,
3039
- ...hint ? { hint } : {}
6333
+ board,
6334
+ needsModel: capQueued(needsModel, topK),
6335
+ needsHuman: capQueued(needsHuman, topK),
6336
+ applied,
6337
+ nextWakeAt: wakeAt,
6338
+ nextWakeSeconds: wakeSeconds(wakeAt)
3040
6339
  };
3041
6340
  }
3042
- async function mapWithConcurrency(items, limit, fn) {
3043
- const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 1;
3044
- const concurrency = Math.max(1, Math.min(items.length || 1, safeLimit));
3045
- const results = new Array(items.length);
3046
- let nextIndex = 0;
3047
- async function worker() {
3048
- while (nextIndex < items.length) {
3049
- const index = nextIndex++;
3050
- results[index] = await fn(items[index], index);
3051
- }
6341
+
6342
+ //#endregion
6343
+ //#region src/lib/first-mate/tools.ts
6344
+ const FIRST_MATE_GROUP = "first-mate";
6345
+ var FirstMateToolInputError = class extends Error {
6346
+ code;
6347
+ constructor(code, message) {
6348
+ super(message);
6349
+ this.name = "FirstMateToolInputError";
6350
+ this.code = code;
3052
6351
  }
3053
- await Promise.all(Array.from({ length: concurrency }, () => worker()));
3054
- return results;
3055
- }
3056
- function fleetFanoutConcurrency(defaultLimit) {
3057
- const raw = process.env[FLEET_FANOUT_CONCURRENCY_ENV];
3058
- const parsed = raw === void 0 ? NaN : Number.parseInt(raw, 10);
3059
- if (Number.isFinite(parsed) && parsed > 0) return Math.floor(parsed);
3060
- return defaultLimit;
3061
- }
3062
- function probeRateLimitBackoffMs(attempt) {
3063
- return Math.min(INSTANCE_PROBE_RATE_LIMIT_BACKOFF_BASE_MS * 2 ** attempt, INSTANCE_PROBE_RATE_LIMIT_BACKOFF_MAX_MS);
6352
+ };
6353
+ function createFirstMateTools() {
6354
+ function tool$1(toolNameHttp, description, inputSchema, handler) {
6355
+ return {
6356
+ toolNameHttp,
6357
+ group: FIRST_MATE_GROUP,
6358
+ description,
6359
+ inputSchema,
6360
+ capability: "agents",
6361
+ async handler(args, signal) {
6362
+ if (!hasAgentToken()) return errorResult(new FirstMateToolInputError("AGENT_TOKEN_REQUIRED", "first-mate tools require --agents or GH_ROUTER_ENABLE_AGENTS=1 with a GitHub agent token"));
6363
+ try {
6364
+ return await handler(args, signal);
6365
+ } catch (err) {
6366
+ return errorResult(err);
6367
+ }
6368
+ }
6369
+ };
6370
+ }
6371
+ return Object.freeze([
6372
+ tool$1("start_mission", "Register a first-mate mission for one or more GitHub repositories. Unit decomposition is handled by later controller/model wakes.", objectSchema({
6373
+ goal: stringProp("Mission goal."),
6374
+ repos: stringArrayProp("Repositories as owner/name strings."),
6375
+ acceptance_criteria: stringProp("User-blessed acceptance criteria for the mission."),
6376
+ priority: numberProp("Optional numeric priority; higher values are handled by controller policy."),
6377
+ house_rules: stringProp("Optional repository or operator constraints.")
6378
+ }, [
6379
+ "goal",
6380
+ "repos",
6381
+ "acceptance_criteria"
6382
+ ]), async (args) => {
6383
+ const repos = requiredStringArray(args, "repos").map(parseRepoRef);
6384
+ const now = Date.now();
6385
+ const missionId = randomUUID();
6386
+ await upsertMission({
6387
+ id: missionId,
6388
+ goal: requiredString(args, "goal"),
6389
+ acceptanceCriteria: requiredString(args, "acceptance_criteria"),
6390
+ houseRules: optionalString(args, "house_rules"),
6391
+ priority: optionalNumber(args, "priority"),
6392
+ repos,
6393
+ status: "active",
6394
+ createdMs: now,
6395
+ updatedMs: now
6396
+ });
6397
+ return ok({
6398
+ missionId,
6399
+ repos
6400
+ });
6401
+ }),
6402
+ tool$1("advance", "Wake the first-mate controller once, applying model answers or human decisions, then return the compact board and pending requests.", objectSchema({
6403
+ model_answers: arrayOfObjectsProp("Optional model judgments to apply before the wake.", {
6404
+ requestId: stringProp("Request id from a previous needsModel entry."),
6405
+ verdict: anyProp("Structured verdict for the request kind.")
6406
+ }, ["requestId", "verdict"]),
6407
+ human_decisions: arrayOfObjectsProp("Optional human choices to apply before the wake.", {
6408
+ requestId: stringProp("Request id from a previous needsHuman entry."),
6409
+ choice: stringProp("Chosen option id or short decision text.")
6410
+ }, ["requestId", "choice"]),
6411
+ top_k: numberProp("Maximum model and human requests to return."),
6412
+ max_in_flight_per_provider: numberProp("Maximum active units per cloud-agent provider.")
6413
+ }, []), async (args) => {
6414
+ const result = await advance({
6415
+ modelAnswers: optionalModelAnswers(args),
6416
+ humanDecisions: optionalHumanDecisions(args),
6417
+ topK: optionalNumber(args, "top_k"),
6418
+ maxInFlightPerProvider: optionalNumber(args, "max_in_flight_per_provider")
6419
+ });
6420
+ return ok({
6421
+ board: result.board,
6422
+ needsModel: result.needsModel,
6423
+ needsHuman: result.needsHuman,
6424
+ applied_count: result.applied.length,
6425
+ nextWakeAt: result.nextWakeAt,
6426
+ nextWakeSeconds: result.nextWakeSeconds
6427
+ });
6428
+ }),
6429
+ tool$1("board", "Read the first-mate board without waking the controller.", objectSchema({}, []), async () => {
6430
+ const [missions, units] = await Promise.all([readMissions(), loadAllUnits()]);
6431
+ return ok({ board: buildBoard(missions, units) });
6432
+ }),
6433
+ tool$1("mission_status", "Read compact status for all first-mate missions, or for one mission id.", objectSchema({ mission_id: stringProp("Optional mission id to filter to.") }, []), async (args) => {
6434
+ const [missions, units] = await Promise.all([readMissions(), loadAllUnits()]);
6435
+ return ok({ missions: buildMissionStatus(missions, units, optionalString(args, "mission_id")) });
6436
+ })
6437
+ ]);
3064
6438
  }
3065
- function nonNegativeNumberOrDefault(value, fallback) {
3066
- return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : fallback;
6439
+ const FIRST_MATE_TOOLS = createFirstMateTools();
6440
+ function hasAgentToken() {
6441
+ return typeof state.githubAgentToken === "string" && state.githubAgentToken.length > 0;
3067
6442
  }
3068
- async function delay(ms) {
3069
- if (ms <= 0) return;
3070
- await new Promise((resolve) => setTimeout(resolve, ms));
6443
+ function buildBoard(missions, units) {
6444
+ const unitsByMission = groupUnitsByMission(units);
6445
+ return missions.filter((mission) => mission.status === "active").map((mission) => {
6446
+ const missionUnits = unitsByMission.get(mission.id) ?? [];
6447
+ return {
6448
+ missionId: mission.id,
6449
+ title: mission.goal,
6450
+ repos: mission.repos.map(repoLabel),
6451
+ counts: countsByPhase(missionUnits),
6452
+ blocked: blockedCount(missionUnits)
6453
+ };
6454
+ });
3071
6455
  }
3072
- function isAbortLike(err) {
3073
- if (!(err instanceof Error)) return false;
3074
- return err.name === "AbortError" || err.name === "TimeoutError";
6456
+ function buildMissionStatus(missions, units, missionId) {
6457
+ const unitsByMission = groupUnitsByMission(units);
6458
+ return missions.filter((mission) => missionId === void 0 || mission.id === missionId).map((mission) => {
6459
+ const missionUnits = unitsByMission.get(mission.id) ?? [];
6460
+ return {
6461
+ missionId: mission.id,
6462
+ title: mission.goal,
6463
+ status: mission.status,
6464
+ counts: countsByPhase(missionUnits),
6465
+ blocked: blockedCount(missionUnits)
6466
+ };
6467
+ });
3075
6468
  }
3076
- function uniqueInstances(instances) {
3077
- const seen = /* @__PURE__ */ new Set();
3078
- const result = [];
3079
- for (const instance of instances) {
3080
- if (seen.has(instance.id)) continue;
3081
- seen.add(instance.id);
3082
- result.push(instance);
6469
+ function groupUnitsByMission(units) {
6470
+ const result = /* @__PURE__ */ new Map();
6471
+ for (const unit of units) {
6472
+ const missionUnits = result.get(unit.missionId) ?? [];
6473
+ missionUnits.push(unit);
6474
+ result.set(unit.missionId, missionUnits);
3083
6475
  }
3084
6476
  return result;
3085
6477
  }
3086
- function publicInstance(instance) {
6478
+ function countsByPhase(units) {
6479
+ const counts = {};
6480
+ for (const unit of units) counts[unit.phase] = (counts[unit.phase] ?? 0) + 1;
6481
+ return counts;
6482
+ }
6483
+ function blockedCount(units) {
6484
+ return units.filter((unit) => Boolean(unit.blockingDecisionId)).length;
6485
+ }
6486
+ function repoLabel(repo) {
6487
+ return `${repo.owner}/${repo.name}`;
6488
+ }
6489
+ function parseRepoRef(value) {
6490
+ const parts = value.trim().split("/");
6491
+ if (parts.length !== 2 || parts[0] === void 0 || parts[1] === void 0 || parts[0].trim() === "" || parts[1].trim() === "") throw new FirstMateToolInputError("INVALID_ARGUMENT", `arguments.repos entries must be owner/name strings; got ${JSON.stringify(value)}`);
3087
6492
  return {
3088
- id: instance.id,
3089
- label: instance.label
6493
+ owner: parts[0].trim(),
6494
+ name: parts[1].trim()
3090
6495
  };
3091
6496
  }
3092
- /**
3093
- * Build the FleetClient tunnel-auth options for a resolved instance.
3094
- * Resolution order: a `tunnelId` enables auto-mint + auto-refresh (and the
3095
- * evict-on-failure hook); else a static `tunnelToken` is sent directly (no
3096
- * retry, since it cannot be re-minted); else no tunnel auth.
3097
- */
3098
- function tunnelClientOptions(instance, provider) {
3099
- if (instance.tunnelId) {
3100
- const cfg = { tunnelId: instance.tunnelId };
6497
+ function optionalModelAnswers(args) {
6498
+ const entries = optionalRecordArray(args, "model_answers");
6499
+ if (entries === void 0) return void 0;
6500
+ return entries.map((entry) => {
6501
+ if (!Object.prototype.hasOwnProperty.call(entry, "verdict")) throw new FirstMateToolInputError("INVALID_ARGUMENT", "arguments.model_answers entries must include verdict");
3101
6502
  return {
3102
- getTunnelToken: () => provider.getToken(cfg),
3103
- onTunnelAuthInvalidate: () => provider.invalidate(cfg)
6503
+ requestId: requiredString(entry, "requestId"),
6504
+ verdict: entry.verdict
3104
6505
  };
3105
- }
3106
- if (instance.tunnelToken) {
3107
- const token = instance.tunnelToken;
3108
- return { getTunnelToken: async () => token };
3109
- }
3110
- return {};
6506
+ });
6507
+ }
6508
+ function optionalHumanDecisions(args) {
6509
+ const entries = optionalRecordArray(args, "human_decisions");
6510
+ if (entries === void 0) return void 0;
6511
+ return entries.map((entry) => ({
6512
+ requestId: requiredString(entry, "requestId"),
6513
+ choice: requiredString(entry, "choice")
6514
+ }));
3111
6515
  }
3112
6516
  function ok(value) {
3113
6517
  return jsonResult(value, false);
@@ -3132,42 +6536,39 @@ function errorCode(err) {
3132
6536
  const code = err.code;
3133
6537
  if (typeof code === "string") return code;
3134
6538
  }
3135
- return "FLEET_ERROR";
3136
- }
3137
- function definedObject(input) {
3138
- const result = {};
3139
- for (const [key, value] of Object.entries(input)) if (value !== void 0) result[key] = value;
3140
- return result;
6539
+ return "FIRST_MATE_ERROR";
3141
6540
  }
3142
6541
  function requiredString(args, key) {
3143
6542
  const value = args[key];
3144
- if (typeof value !== "string" || value.trim() === "") throw new FleetToolInputError("INVALID_ARGUMENT", `arguments.${key} is required and must be a non-empty string`);
6543
+ if (typeof value !== "string" || value.trim() === "") throw new FirstMateToolInputError("INVALID_ARGUMENT", `arguments.${key} is required and must be a non-empty string`);
3145
6544
  return value;
3146
6545
  }
3147
6546
  function optionalString(args, key) {
3148
6547
  const value = args[key];
3149
6548
  if (value === void 0) return void 0;
3150
- if (typeof value !== "string") throw new FleetToolInputError("INVALID_ARGUMENT", `arguments.${key} must be a string`);
6549
+ if (typeof value !== "string") throw new FirstMateToolInputError("INVALID_ARGUMENT", `arguments.${key} must be a string`);
3151
6550
  return value.trim() === "" ? void 0 : value;
3152
6551
  }
3153
6552
  function optionalNumber(args, key) {
3154
6553
  const value = args[key];
3155
6554
  if (value === void 0) return void 0;
3156
- if (typeof value !== "number" || !Number.isFinite(value)) throw new FleetToolInputError("INVALID_ARGUMENT", `arguments.${key} must be a finite number`);
6555
+ if (typeof value !== "number" || !Number.isFinite(value)) throw new FirstMateToolInputError("INVALID_ARGUMENT", `arguments.${key} must be a finite number`);
3157
6556
  return value;
3158
6557
  }
3159
- function optionalBoolean(args, key) {
6558
+ function requiredStringArray(args, key) {
3160
6559
  const value = args[key];
3161
- if (value === void 0) return void 0;
3162
- if (typeof value !== "boolean") throw new FleetToolInputError("INVALID_ARGUMENT", `arguments.${key} must be a boolean`);
6560
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.trim() === "")) throw new FirstMateToolInputError("INVALID_ARGUMENT", `arguments.${key} must be an array of non-empty strings`);
3163
6561
  return value;
3164
6562
  }
3165
- function optionalStringArray(args, key) {
6563
+ function optionalRecordArray(args, key) {
3166
6564
  const value = args[key];
3167
6565
  if (value === void 0) return void 0;
3168
- if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.trim() === "")) throw new FleetToolInputError("INVALID_ARGUMENT", `arguments.${key} must be an array of non-empty strings`);
6566
+ if (!Array.isArray(value) || value.some((item) => asRecord(item) === void 0)) throw new FirstMateToolInputError("INVALID_ARGUMENT", `arguments.${key} must be an array of objects`);
3169
6567
  return value;
3170
6568
  }
6569
+ function asRecord(value) {
6570
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
6571
+ }
3171
6572
  function objectSchema(properties, required) {
3172
6573
  return {
3173
6574
  type: "object",
@@ -3188,19 +6589,23 @@ function numberProp(description) {
3188
6589
  description
3189
6590
  };
3190
6591
  }
3191
- function booleanProp(description) {
6592
+ function stringArrayProp(description) {
3192
6593
  return {
3193
- type: "boolean",
6594
+ type: "array",
6595
+ items: { type: "string" },
3194
6596
  description
3195
6597
  };
3196
6598
  }
3197
- function arrayProp(description) {
6599
+ function arrayOfObjectsProp(description, properties, required) {
3198
6600
  return {
3199
6601
  type: "array",
3200
- items: { type: "string" },
3201
- description
6602
+ description,
6603
+ items: objectSchema(properties, required)
3202
6604
  };
3203
6605
  }
6606
+ function anyProp(description) {
6607
+ return { description };
6608
+ }
3204
6609
 
3205
6610
  //#endregion
3206
6611
  //#region src/lib/tree-sitter-grammars.ts
@@ -5024,8 +8429,8 @@ function runStructuralPassInProcess(opts) {
5024
8429
  consola.debug(`[code_search] structural skip ${relFile} (${size} bytes > cap)`);
5025
8430
  continue;
5026
8431
  }
5027
- let cached$1 = cacheGet(absPath, mtimeMs);
5028
- if (!cached$1) {
8432
+ let cached$2 = cacheGet(absPath, mtimeMs);
8433
+ if (!cached$2) {
5029
8434
  let source;
5030
8435
  try {
5031
8436
  source = readFileSync(absPath, "utf8");
@@ -5052,17 +8457,17 @@ function runStructuralPassInProcess(opts) {
5052
8457
  } catch (err) {
5053
8458
  consola.debug(`[code_search] tree-sitter parse failed for ${relFile}: ${err.message}`);
5054
8459
  }
5055
- cached$1 = {
8460
+ cached$2 = {
5056
8461
  mtimeMs,
5057
8462
  tree,
5058
8463
  source: tree ? source : null
5059
8464
  };
5060
- cachePut(absPath, cached$1);
8465
+ cachePut(absPath, cached$2);
5061
8466
  filesParsed += 1;
5062
8467
  if (benchOn) _benchStructural.filesParsed += 1;
5063
8468
  }
5064
- if (!cached$1.tree || !cached$1.source) continue;
5065
- const confirmedPositions = confirmDefinitionSites(cached$1.tree, cached$1.source, langKey, entries.map((e) => ({
8469
+ if (!cached$2.tree || !cached$2.source) continue;
8470
+ const confirmedPositions = confirmDefinitionSites(cached$2.tree, cached$2.source, langKey, entries.map((e) => ({
5066
8471
  line: e.hit.line,
5067
8472
  matchStart: e.hit.match_start,
5068
8473
  matchEnd: e.hit.match_end
@@ -5473,11 +8878,11 @@ function relativizeToWorkspace(file, workspaceCanonical) {
5473
8878
  async function enumerateWorkspaceFiles(opts) {
5474
8879
  const files = [];
5475
8880
  let total = 0;
5476
- let capped = false;
8881
+ let capped$1 = false;
5477
8882
  if (opts.signal.aborted) return {
5478
8883
  files,
5479
8884
  total,
5480
- capped
8885
+ capped: capped$1
5481
8886
  };
5482
8887
  let child;
5483
8888
  try {
@@ -5494,7 +8899,7 @@ async function enumerateWorkspaceFiles(opts) {
5494
8899
  return {
5495
8900
  files,
5496
8901
  total,
5497
- capped
8902
+ capped: capped$1
5498
8903
  };
5499
8904
  }
5500
8905
  child.on("error", () => {});
@@ -5506,7 +8911,7 @@ async function enumerateWorkspaceFiles(opts) {
5506
8911
  if (!child.stdout) return {
5507
8912
  files,
5508
8913
  total,
5509
- capped
8914
+ capped: capped$1
5510
8915
  };
5511
8916
  child.stdout.setEncoding("utf8");
5512
8917
  const rl = createInterface({
@@ -5531,7 +8936,7 @@ async function enumerateWorkspaceFiles(opts) {
5531
8936
  if (isSensitivePath(path.join(opts.workspaceCanonical, rel), opts.workspaceCanonical)) continue;
5532
8937
  total += 1;
5533
8938
  if (files.length < SCAN_MAX_FILES) files.push(rel);
5534
- else capped = true;
8939
+ else capped$1 = true;
5535
8940
  }
5536
8941
  } catch {} finally {
5537
8942
  clearTimeout(deadlineTimer);
@@ -5541,7 +8946,7 @@ async function enumerateWorkspaceFiles(opts) {
5541
8946
  return {
5542
8947
  files,
5543
8948
  total,
5544
- capped
8949
+ capped: capped$1
5545
8950
  };
5546
8951
  }
5547
8952
  async function searchCode(rawInput, externalSignal) {
@@ -5783,10 +9188,10 @@ async function searchCode(rawInput, externalSignal) {
5783
9188
  language: getLanguageKeyForPath(abs)
5784
9189
  };
5785
9190
  if (!result) try {
5786
- const cached$1 = cacheGet(abs, statSync(abs).mtimeMs);
5787
- if (cached$1?.tree) {
9191
+ const cached$2 = cacheGet(abs, statSync(abs).mtimeMs);
9192
+ if (cached$2?.tree) {
5788
9193
  const lang = getLanguageKeyForPath(abs);
5789
- if (lang) result = outlineFromTree(cached$1.tree, lang, ac.signal);
9194
+ if (lang) result = outlineFromTree(cached$2.tree, lang, ac.signal);
5790
9195
  }
5791
9196
  } catch {}
5792
9197
  const o = result ?? await outlineFile(abs, ac.signal);
@@ -7203,7 +10608,7 @@ async function runInit(workspace) {
7203
10608
  ];
7204
10609
  const onInactivityCheck = makeIndexProgressProbe(workspace);
7205
10610
  const startMs = Date.now();
7206
- let ok$2 = false;
10611
+ let ok$3 = false;
7207
10612
  let failureClass;
7208
10613
  try {
7209
10614
  const res = await runManagedExeCapture(binary, args, {
@@ -7220,10 +10625,10 @@ async function runInit(workspace) {
7220
10625
  }).catch(() => {});
7221
10626
  }
7222
10627
  });
7223
- ok$2 = !res.stalled && !res.timedOut && res.code === 0;
7224
- if (!ok$2) failureClass = res.stalled || res.timedOut ? "stuck" : "error";
10628
+ ok$3 = !res.stalled && !res.timedOut && res.code === 0;
10629
+ if (!ok$3) failureClass = res.stalled || res.timedOut ? "stuck" : "error";
7225
10630
  } catch {
7226
- ok$2 = false;
10631
+ ok$3 = false;
7227
10632
  failureClass = "launch";
7228
10633
  } finally {
7229
10634
  releaseInit(workspace);
@@ -7240,9 +10645,9 @@ async function runInit(workspace) {
7240
10645
  finalMeta.lastIndexedDirty = g.dirty;
7241
10646
  }
7242
10647
  } catch {}
7243
- finalMeta.status = ok$2 ? "ready" : "failed";
10648
+ finalMeta.status = ok$3 ? "ready" : "failed";
7244
10649
  finalMeta.lastIndexedAt = (/* @__PURE__ */ new Date()).toISOString();
7245
- if (ok$2) {
10650
+ if (ok$3) {
7246
10651
  finalMeta.failedAttempts = 0;
7247
10652
  finalMeta.failureClass = void 0;
7248
10653
  } else {
@@ -8620,7 +12025,7 @@ function logAudit$1(record) {
8620
12025
  try {
8621
12026
  const fs$2 = await import("node:fs/promises");
8622
12027
  const path$1 = await import("node:path");
8623
- const { PATHS: PATHS$1 } = await import("./paths-Bljq3UJC.js");
12028
+ const { PATHS: PATHS$1 } = await import("./paths-DhLJ9bLG.js");
8624
12029
  const dir = path$1.join(PATHS$1.APP_DIR, "browser-mcp");
8625
12030
  await fs$2.mkdir(dir, { recursive: true });
8626
12031
  const line = JSON.stringify({
@@ -9264,7 +12669,7 @@ async function readResponseBodyCapped(response, routePath, capBytes = MAX_RESPON
9264
12669
  };
9265
12670
  const chunks = [];
9266
12671
  let totalBytes = 0;
9267
- let capped = false;
12672
+ let capped$1 = false;
9268
12673
  try {
9269
12674
  while (true) {
9270
12675
  const { done, value } = await reader.read();
@@ -9272,7 +12677,7 @@ async function readResponseBodyCapped(response, routePath, capBytes = MAX_RESPON
9272
12677
  if (!value) continue;
9273
12678
  totalBytes += value.byteLength;
9274
12679
  if (totalBytes > capBytes) {
9275
- capped = true;
12680
+ capped$1 = true;
9276
12681
  try {
9277
12682
  await reader.cancel("size_cap");
9278
12683
  } catch {}
@@ -9281,9 +12686,9 @@ async function readResponseBodyCapped(response, routePath, capBytes = MAX_RESPON
9281
12686
  chunks.push(value);
9282
12687
  }
9283
12688
  } catch (err) {
9284
- if (!capped) consola.warn(`readResponseBodyCapped: read error at ${routePath}:`, err);
12689
+ if (!capped$1) consola.warn(`readResponseBodyCapped: read error at ${routePath}:`, err);
9285
12690
  }
9286
- if (capped) {
12691
+ if (capped$1) {
9287
12692
  consola.warn(`Non-streaming upstream response at ${routePath} exceeded ${capBytes} bytes (10 MiB cap); dropping body to prevent OOM. Check upstream health.`);
9288
12693
  return {
9289
12694
  ok: false,
@@ -11670,8 +15075,8 @@ function coerceWithJsonSchema(value, schema) {
11670
15075
  }
11671
15076
  function getValidator(schema) {
11672
15077
  const key = schema;
11673
- const cached$1 = validatorCache.get(key);
11674
- if (cached$1) return cached$1;
15078
+ const cached$2 = validatorCache.get(key);
15079
+ if (cached$2) return cached$2;
11675
15080
  const validator = Compile(schema);
11676
15081
  validatorCache.set(key, validator);
11677
15082
  return validator;
@@ -13846,12 +17251,12 @@ function joinTextChunks(accum, idx) {
13846
17251
  */
13847
17252
  function makeLazyTextPart(chunks) {
13848
17253
  const upTo = chunks.length;
13849
- let cached$1;
17254
+ let cached$2;
13850
17255
  return {
13851
17256
  type: "text",
13852
17257
  get text() {
13853
- if (cached$1 === void 0) cached$1 = upTo === chunks.length ? chunks.join("") : chunks.slice(0, upTo).join("");
13854
- return cached$1;
17258
+ if (cached$2 === void 0) cached$2 = upTo === chunks.length ? chunks.join("") : chunks.slice(0, upTo).join("");
17259
+ return cached$2;
13855
17260
  }
13856
17261
  };
13857
17262
  }
@@ -14672,10 +18077,10 @@ function capToolResultText(content, capBytes) {
14672
18077
  } else images.push(block);
14673
18078
  }
14674
18079
  if (textBytes <= capBytes) return void 0;
14675
- const capped = truncateModelText(texts.join("\n"), capBytes);
18080
+ const capped$1 = truncateModelText(texts.join("\n"), capBytes);
14676
18081
  return [...images, {
14677
18082
  type: "text",
14678
- text: capped
18083
+ text: capped$1
14679
18084
  }];
14680
18085
  }
14681
18086
 
@@ -14740,8 +18145,8 @@ const calculateTokens = (messages, encoder, constants) => {
14740
18145
  */
14741
18146
  const getEncodeChatFunction = async (encoding) => {
14742
18147
  if (encodingCache.has(encoding)) {
14743
- const cached$1 = encodingCache.get(encoding);
14744
- if (cached$1) return cached$1;
18148
+ const cached$2 = encodingCache.get(encoding);
18149
+ if (cached$2) return cached$2;
14745
18150
  }
14746
18151
  const supportedEncoding = encoding;
14747
18152
  if (!(supportedEncoding in ENCODING_MAP)) {
@@ -15169,6 +18574,19 @@ function fleetToolsEnabled() {
15169
18574
  return state.fleetEnabled || process.env.GH_ROUTER_ENABLE_FLEET === "1";
15170
18575
  }
15171
18576
  /**
18577
+ * Gate for the first-mate cloud-agent MCP tools (`mcp__first-mate__*`).
18578
+ *
18579
+ * Returns true iff the operator opted in (`state.agentsEnabled`, set by
18580
+ * `--agents`, OR `GH_ROUTER_ENABLE_AGENTS=1` read directly so non-
18581
+ * `setupAndServe` startup paths — tests, embedded use — can still flip
18582
+ * the gate) AND the write-capable GitHub agent token is present. First-mate
18583
+ * drives GitHub cloud agents, so exposing the surface without that token would
18584
+ * only produce unactionable auth failures.
18585
+ */
18586
+ function agentToolsEnabled() {
18587
+ return (state.agentsEnabled || process.env.GH_ROUTER_ENABLE_AGENTS === "1") && typeof state.githubAgentToken === "string" && state.githubAgentToken.length > 0;
18588
+ }
18589
+ /**
15172
18590
  * Gate for ai-or-die Artifact review tools.
15173
18591
  *
15174
18592
  * Returns true iff this github-router process was launched inside an
@@ -15376,6 +18794,7 @@ function toolEntries(scope) {
15376
18794
  if (t.capability === "stand_in") return standInToolEnabled();
15377
18795
  if (t.capability === "browser") return browserToolsEnabled();
15378
18796
  if (t.capability === "fleet") return fleetToolsEnabled();
18797
+ if (t.capability === "agents") return agentToolsEnabled();
15379
18798
  if (t.capability === "artifact") return artifactToolsEnabled();
15380
18799
  if (t.capability === "browser_compound") return browserToolsEnabled() && browserCompoundToolsEnabled();
15381
18800
  if (t.capability === "browser_power") return browserToolsEnabled() && browserPowerToolsEnabled();
@@ -15704,6 +19123,7 @@ async function handleToolsCall(body, scope) {
15704
19123
  if (nonPersonaTool && nonPersonaTool.capability === "stand_in" && !standInToolEnabled()) return rpcError(body.id, RPC_METHOD_NOT_FOUND, `tools/call: unknown tool "${name}"`);
15705
19124
  if (nonPersonaTool && nonPersonaTool.capability === "browser" && !browserToolsEnabled()) return rpcError(body.id, RPC_METHOD_NOT_FOUND, `tools/call: unknown tool "${name}"`);
15706
19125
  if (nonPersonaTool && nonPersonaTool.capability === "fleet" && !fleetToolsEnabled()) return rpcError(body.id, RPC_METHOD_NOT_FOUND, `tools/call: unknown tool "${name}"`);
19126
+ if (nonPersonaTool && nonPersonaTool.capability === "agents" && !agentToolsEnabled()) return rpcError(body.id, RPC_METHOD_NOT_FOUND, `tools/call: unknown tool "${name}"`);
15707
19127
  if (nonPersonaTool && nonPersonaTool.capability === "artifact" && !artifactToolsEnabled()) return rpcError(body.id, RPC_METHOD_NOT_FOUND, `tools/call: unknown tool "${name}"`);
15708
19128
  if (nonPersonaTool && nonPersonaTool.capability === "browser_compound" && !(browserToolsEnabled() && browserCompoundToolsEnabled())) return rpcError(body.id, RPC_METHOD_NOT_FOUND, `tools/call: unknown tool "${name}"`);
15709
19129
  if (nonPersonaTool && nonPersonaTool.capability === "browser_power" && !(browserToolsEnabled() && browserPowerToolsEnabled())) return rpcError(body.id, RPC_METHOD_NOT_FOUND, `tools/call: unknown tool "${name}"`);
@@ -19269,8 +22689,8 @@ async function runWorkerAgentOnce(opts) {
19269
22689
  afterToolCall: async (ctx) => {
19270
22690
  budget.recordToolBytes(ctx.result);
19271
22691
  if (ctxBudget) {
19272
- const capped = capToolResultText(ctx.result.content, ctxBudget.perResultCapBytes);
19273
- if (capped) return { content: capped };
22692
+ const capped$1 = capToolResultText(ctx.result.content, ctxBudget.perResultCapBytes);
22693
+ if (capped$1) return { content: capped$1 };
19274
22694
  }
19275
22695
  },
19276
22696
  prepareNextTurn: async () => {
@@ -21436,7 +24856,8 @@ const MCP_GROUPS = Object.freeze([
21436
24856
  "orchestrate",
21437
24857
  "browser",
21438
24858
  "decide",
21439
- "fleet"
24859
+ "fleet",
24860
+ "first-mate"
21440
24861
  ]);
21441
24862
  const GROUP_META = Object.freeze({
21442
24863
  peers: {
@@ -21473,6 +24894,11 @@ const GROUP_META = Object.freeze({
21473
24894
  preferredKey: "fleet",
21474
24895
  urlSuffix: "fleet",
21475
24896
  serverInfoName: "github-router-fleet"
24897
+ },
24898
+ "first-mate": {
24899
+ preferredKey: "first-mate",
24900
+ urlSuffix: "first-mate",
24901
+ serverInfoName: "github-router-first-mate"
21476
24902
  }
21477
24903
  });
21478
24904
  /** True iff `s` is a registered group name (route `:group` param validation). */
@@ -21826,6 +25252,8 @@ function buildAgentPrompt(persona, opts) {
21826
25252
  * out of the live catalog).
21827
25253
  * - Conditionally lists stand_in only when `standInAvailable`
21828
25254
  * (mirrors `standInToolEnabled()`).
25255
+ * - Conditionally lists gh-first-mate only when `agentToolsAvailable`
25256
+ * (mirrors `agentToolsEnabled()`).
21829
25257
  * - Mentions `codex-cli` stdio bridge only when `codexCli`.
21830
25258
  * - Does NOT re-document Claude Code's built-in delegation semantics
21831
25259
  * (Agent-tool recursion, agent-teams coordination) — Claude
@@ -21852,7 +25280,10 @@ function buildPeerAwarenessSnippet(opts) {
21852
25280
  if (opts.workerToolsAvailable) para2Parts.push(`\`mcp__${workersKey}__explore\` runs a Gemini-backed read-only worker that returns a summary, using its own context rather than yours; concurrent launches share the \`MAX_INFLIGHT_TOOLS_CALL\` cap (default 128) with operator traffic.`, `\`mcp__${workersKey}__review\` is the same worker framed as a code reviewer that reads the code itself to verify a change or claim, reporting findings with severity, so it checks context the \`peers\` critics (stateless calls on the pasted artifact) cannot.`, `\`mcp__${workersKey}__plan\` is the same read-only worker framed as a planner: from a task + acceptance criteria it returns an ordered implementation plan.`, `\`mcp__${workersKey}__implement\` is the same worker with edit/write/bash; \`worktree: true\` runs it in an isolated git worktree and returns the diff.`, `\`mcp__${workersKey}__test\` is a write-capable worker framed as an independent test author: it authors tests that try to break the implementation and reports pass/fail, never editing the implementation to make them pass.`, "Workers themselves have `code_search` in their toolset.");
21853
25281
  if (opts.workerToolsAvailable) para2Parts.push(`\`mcp__${orchestrateKey}__decompose\` composes an open-ended ask into a typed, VERIFIED workflow IR (a strong driver decorrelated by a cross-lab critic, so the decompose step isn't a single point of failure), and \`mcp__${orchestrateKey}__run_workflow\` executes that IR through a frozen kernel delivering max(orchestrated, baseline) over a sealed executable gate, so it never ships worse than a plain single-model run. \`mcp__${orchestrateKey}__verify_workflow\` checks an IR's floor invariants before you run it, and \`mcp__${orchestrateKey}__attest_step\` audits that a finished run's producers were each checked by a different lab. They suit non-trivial, role-separated asks; a trivial ask does not need them.`);
21854
25282
  else para2Parts.push(`\`mcp__${orchestrateKey}__verify_workflow\` statically checks a workflow IR's floor invariants and \`mcp__${orchestrateKey}__attest_step\` audits a run's cross-lab lineage (the \`decompose\`/\`run_workflow\` composer + kernel need the worker backend, unavailable here).`);
21855
- if (opts.workerToolsAvailable) para2Parts.push("Three injected skills (invoke by name): `/gh-research` saturates an ask's unknowns into a confidence-tagged, root-cause brief that grounds planning; `/gh-orchestrate` right-sizes a blind-spot-elimination pipeline whose nodes delegate to these tools; `/gh-floor-keeper` is the done-checkpoint cross-lab verification, where different-lab reviewers propose and the executable gate decides. They suit non-trivial, role-separable work. Only executable checks are deterministic; they do not catch a wrong spec, so user-blessed acceptance criteria plus the checkpoint are the defense.");
25283
+ if (opts.workerToolsAvailable) {
25284
+ const skillSentence = opts.agentToolsAvailable === true ? "Four injected skills (invoke by name): `/gh-research` saturates an ask's unknowns into a confidence-tagged, root-cause brief that grounds planning; `/gh-orchestrate` right-sizes a blind-spot-elimination pipeline whose nodes delegate to these tools; `/gh-floor-keeper` is the done-checkpoint cross-lab verification, where different-lab reviewers propose and the executable gate decides; `/gh-first-mate` drives the durable GitHub cloud-agent loop. They suit non-trivial, role-separable work. Only executable checks are deterministic; they do not catch a wrong spec, so user-blessed acceptance criteria plus the checkpoint are the defense." : "Three injected skills (invoke by name): `/gh-research` saturates an ask's unknowns into a confidence-tagged, root-cause brief that grounds planning; `/gh-orchestrate` right-sizes a blind-spot-elimination pipeline whose nodes delegate to these tools; `/gh-floor-keeper` is the done-checkpoint cross-lab verification, where different-lab reviewers propose and the executable gate decides. They suit non-trivial, role-separable work. Only executable checks are deterministic; they do not catch a wrong spec, so user-blessed acceptance criteria plus the checkpoint are the defense.";
25285
+ para2Parts.push(skillSentence);
25286
+ }
21856
25287
  para2Parts.push(`\`mcp__${searchKey}__web\` surfaces citable sources for docs, errors, and upstream issues.`);
21857
25288
  if (opts.standInAvailable) para2Parts.push(`\`mcp__${decideKey}__stand_in\` provides three-lab consensus for decision tiebreak when the user is unavailable.`);
21858
25289
  if (opts.browseAvailable) {
@@ -22586,6 +26017,7 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
22586
26017
  },
22587
26018
  ...ARTIFACT_TOOLS,
22588
26019
  ...FLEET_TOOLS,
26020
+ ...FIRST_MATE_TOOLS,
22589
26021
  ...BROWSER_TOOLS.map((t) => ({
22590
26022
  ...t,
22591
26023
  group: "browser",
@@ -22914,5 +26346,5 @@ async function runStandInToolCall(args, signal) {
22914
26346
  }
22915
26347
 
22916
26348
  //#endregion
22917
- export { readIteratorWithTimeout as $, state as $t, DEFAULT_MODEL as A, generateRandomPort as At, toolbeltSkipSet as B, filterBetaHeader as Bt, repoRoot as C, toolbeltPathOverride as Ct, resolveSealedGate as D, DEFAULT_PORT as Dt, trustRepo as E, DEFAULT_CODEX_MODEL_FALLBACKS as Et, runWorkerAgent as F, setupGitHubToken as Ft, ADVISOR_INTERNAL_TOOL_NAME as G, getModels as Gt, TOOLBELT_TOOLS$1 as H, resolveCodexModel as Ht, withNoOutputRetry as I, tryRefreshAndRetry as It, injectAdvisorTool as J, forwardError as Jt, ADVISOR_TOOL_INSTRUCTIONS as K, fetchWithTransientRetry as Kt, availableToolCommands as L, cacheCopilotVersion as Lt, PLAN_DEFAULT_MODEL as M, getPackageVersion as Mt, REVIEW_DEFAULT_MODEL as N, withInstallLock as Nt, liveExec as O, UPSTREAM_FETCH_TIMEOUT_MS as Ot, appendPlanReminder as P, setupCopilotToken as Pt, logStreamError as Q, githubHeaders as Qt, buildToolbeltAwareness as R, cacheModels as Rt, repoFingerprint as S, collapsePathKeys as St, stopReviewStateDir as T, DEFAULT_CODEX_MODEL as Tt, assetFor as U, resolveModel as Ut, vscodeRipgrepPath as V, isNullish as Vt, searchWeb as W, sleep as Wt, buildOpenAIErrorEvent as X, copilotBaseUrl as Xt, isAdvisorRequested as Y, GITHUB_API_BASE_URL as Yt, isControllerClosedError as Z, copilotHeaders as Zt, fileBaselineStore as _, provisionAndIndexColbert as _t, buildPeerAwarenessSnippet as a, standInToolEnabled as at, fileReviewDebounce as b, shouldUseInsecureTls as bt, buildSessionBindHookCommand as c, createMessages as ct, decideStopHook as d, createChatCompletions as dt, relayAnthropicStream as et, fileBlockBudget as f, MAX_RESPONSE_BODY_BYTES as ft, stopReviewEnabled as g, hasSupportedBrowserInstalled as gt, stopGateId as h, provisionBrowserAssets as ht, buildAgentPrompt as i, fleetToolsEnabled as it, IMPLEMENT_DEFAULT_MODEL as j, pickClaudeDefault as jt, BROWSE_DEFAULT_MODEL as k, UPSTREAM_INACTIVITY_TIMEOUT_MS as kt, buildStopHookCommand as l, getTokenCount as lt, launchBaselineKey as m, parseJsonOrDiagnose as mt, MCP_GROUPS as n, handleMcpPost as nt, personasFor as o, workerToolsEnabled as ot, injectStopHookIntoSettingsFile as p, readResponseBodyCapped as pt, buildAdvisorStream as q, HTTPError as qt, assertMcpToolSurfaceConsistent as r, browserToolsEnabled as rt, buildArtifactOpenHookCommand as s, countTokens as st, GROUP_META as t, handleMcpDelete as tt, captureLaunchBaseline as u, createResponses as ut, fileFindingsStore as v, extractTarGzMember as vt, stopGateEnabledForRepo as w, DEFAULT_CLAUDE_MODEL_FALLBACKS as wt, isSubagentContext as x, ArtifactClient as xt, fileLastPromptStore as y, extractZipMember as yt, toolbeltEnabled as z, cacheVSCodeVersion as zt };
22918
- //# sourceMappingURL=peer-mcp-personas-KHFhxjFn.js.map
26349
+ export { readIteratorWithTimeout as $, copilotHeaders as $t, DEFAULT_MODEL as A, UPSTREAM_INACTIVITY_TIMEOUT_MS as At, toolbeltSkipSet as B, cacheModels as Bt, repoRoot as C, collapsePathKeys as Ct, resolveSealedGate as D, DEFAULT_CODEX_MODEL_FALLBACKS as Dt, trustRepo as E, DEFAULT_CODEX_MODEL as Et, runWorkerAgent as F, setupCopilotToken as Ft, ADVISOR_INTERNAL_TOOL_NAME as G, resolveModel as Gt, TOOLBELT_TOOLS$1 as H, filterBetaHeader as Ht, withNoOutputRetry as I, setupGitHubAgentToken as It, injectAdvisorTool as J, fetchWithTransientRetry as Jt, ADVISOR_TOOL_INSTRUCTIONS as K, sleep as Kt, availableToolCommands as L, setupGitHubToken as Lt, PLAN_DEFAULT_MODEL as M, pickClaudeDefault as Mt, REVIEW_DEFAULT_MODEL as N, getPackageVersion as Nt, liveExec as O, DEFAULT_PORT as Ot, appendPlanReminder as P, withInstallLock as Pt, logStreamError as Q, copilotBaseUrl as Qt, buildToolbeltAwareness as R, tryRefreshAndRetry as Rt, repoFingerprint as S, ArtifactClient as St, stopReviewStateDir as T, DEFAULT_CLAUDE_MODEL_FALLBACKS as Tt, assetFor as U, isNullish as Ut, vscodeRipgrepPath as V, cacheVSCodeVersion as Vt, searchWeb as W, resolveCodexModel as Wt, buildOpenAIErrorEvent as X, forwardError as Xt, isAdvisorRequested as Y, HTTPError as Yt, isControllerClosedError as Z, GITHUB_API_BASE_URL as Zt, fileBaselineStore as _, hasSupportedBrowserInstalled as _t, buildPeerAwarenessSnippet as a, fleetToolsEnabled as at, fileReviewDebounce as b, extractZipMember as bt, buildSessionBindHookCommand as c, countTokens as ct, decideStopHook as d, createResponses as dt, githubHeaders as en, relayAnthropicStream as et, fileBlockBudget as f, createChatCompletions as ft, stopReviewEnabled as g, provisionBrowserAssets as gt, stopGateId as h, parseJsonOrDiagnose as ht, buildAgentPrompt as i, browserToolsEnabled as it, IMPLEMENT_DEFAULT_MODEL as j, generateRandomPort as jt, BROWSE_DEFAULT_MODEL as k, UPSTREAM_FETCH_TIMEOUT_MS as kt, buildStopHookCommand as l, createMessages as lt, launchBaselineKey as m, readResponseBodyCapped as mt, MCP_GROUPS as n, handleMcpPost as nt, personasFor as o, standInToolEnabled as ot, injectStopHookIntoSettingsFile as p, MAX_RESPONSE_BODY_BYTES as pt, buildAdvisorStream as q, getModels as qt, assertMcpToolSurfaceConsistent as r, agentToolsEnabled as rt, buildArtifactOpenHookCommand as s, workerToolsEnabled as st, GROUP_META as t, state as tn, handleMcpDelete as tt, captureLaunchBaseline as u, getTokenCount as ut, fileFindingsStore as v, provisionAndIndexColbert as vt, stopGateEnabledForRepo as w, toolbeltPathOverride as wt, isSubagentContext as x, shouldUseInsecureTls as xt, fileLastPromptStore as y, extractTarGzMember as yt, toolbeltEnabled as z, cacheCopilotVersion as zt };
26350
+ //# sourceMappingURL=peer-mcp-personas-D_HyWhUb.js.map