github-router 0.3.138 → 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";
@@ -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
@@ -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,7 +1559,7 @@ 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
@@ -1690,17 +1785,17 @@ function createTunnelTokenProvider(runner = realDevtunnelRunner()) {
1690
1785
  async getToken(cfg) {
1691
1786
  const key = cfg.tunnelId;
1692
1787
  const now = Date.now();
1693
- const cached$1 = cache.get(key);
1694
- if (cached$1 && cached$1.expMs > now) {
1695
- const comfortablyFresh = cached$1.expMs - now > REFRESH_MARGIN_MS;
1696
- const recentlyMinted = now - cached$1.mintedAt < MIN_REMINT_INTERVAL_MS;
1697
- 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;
1698
1793
  }
1699
1794
  const inf = inflight.get(key);
1700
1795
  if (inf) return inf;
1701
1796
  const bo = backoff.get(key);
1702
1797
  if (bo && now < bo.until) {
1703
- if (cached$1 && cached$1.expMs > now) return cached$1.token;
1798
+ if (cached$2 && cached$2.expMs > now) return cached$2.token;
1704
1799
  throw bo.err;
1705
1800
  }
1706
1801
  const p = mintOnce(cfg);
@@ -2638,8 +2733,8 @@ function createFleetTools(options = {}) {
2638
2733
  }
2639
2734
  async function getInstanceCapabilities(instance, signal) {
2640
2735
  const now = Date.now();
2641
- const cached$1 = capabilitiesCache.get(instance.id);
2642
- 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;
2643
2738
  try {
2644
2739
  const response = await clientFor(instance).capabilities(signal);
2645
2740
  const caps = new Set(response.capabilities);
@@ -2664,10 +2759,10 @@ function createFleetTools(options = {}) {
2664
2759
  return getRegistry().resolveInstance(arg);
2665
2760
  }
2666
2761
  async function resolveSession(args) {
2667
- const globalId = requiredString(args, "sessionId");
2762
+ const globalId = requiredString$1(args, "sessionId");
2668
2763
  const decoded = decodeSessionId(globalId);
2669
2764
  const instance = await resolve(decoded.instanceId);
2670
- const explicitInstance = optionalString(args, "instance");
2765
+ const explicitInstance = optionalString$1(args, "instance");
2671
2766
  if (explicitInstance !== void 0) {
2672
2767
  const explicit = await resolve(explicitInstance);
2673
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)}`);
@@ -2681,8 +2776,8 @@ function createFleetTools(options = {}) {
2681
2776
  async function probeInstance(info) {
2682
2777
  const cacheKey = `${info.id}\0${info.url}`;
2683
2778
  const now = Date.now();
2684
- const cached$1 = instanceProbeCache.get(cacheKey);
2685
- 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;
2686
2781
  for (let attempt = 0; attempt <= INSTANCE_PROBE_RATE_LIMIT_MAX_RETRIES; attempt++) {
2687
2782
  const timeout = createProbeTimeout();
2688
2783
  try {
@@ -2735,69 +2830,69 @@ function createFleetTools(options = {}) {
2735
2830
  try {
2736
2831
  return await handler(args, signal);
2737
2832
  } catch (err) {
2738
- return errorResult(err);
2833
+ return errorResult$1(err);
2739
2834
  }
2740
2835
  }
2741
2836
  };
2742
2837
  }
2743
2838
  return Object.freeze([
2744
- tool$1("list_instances", "List registered remote ai-or-die instances in the fleet registry. Tokens are never returned.", objectSchema({}, []), async () => {
2745
- 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)) });
2746
2841
  }),
2747
- 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) => {
2748
- 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"));
2749
2844
  const response = await clientFor(instance).listSessions(signal);
2750
- return ok({
2845
+ return ok$1({
2751
2846
  resolvedInstance: publicInstance(instance),
2752
2847
  sessions: response.sessions.map((session) => globalizeSession(instance.id, session))
2753
2848
  });
2754
2849
  }),
2755
- tool$1("read_session", "Read recent text output from an addressed fleet session.", objectSchema({
2756
- sessionId: stringProp("Global session id in the form instanceId:localSessionId."),
2757
- instance: stringProp("Optional instance id/label; when supplied it must agree with sessionId."),
2758
- lines: numberProp("Number of recent lines to read."),
2759
- 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.")
2760
2855
  }, ["sessionId"]), async (args, signal) => {
2761
2856
  const { instance, localId, globalId } = await resolveSession(args);
2762
- const lines = optionalNumber(args, "lines");
2857
+ const lines = optionalNumber$1(args, "lines");
2763
2858
  const response = await clientFor(instance).readSession(localId, lines, signal);
2764
- return ok({
2859
+ return ok$1({
2765
2860
  resolvedInstance: publicInstance(instance),
2766
2861
  ...response,
2767
2862
  sessionId: globalId
2768
2863
  });
2769
2864
  }),
2770
- tool$1("session_status", "Fetch lifecycle and interaction status for an addressed fleet session.", objectSchema({
2771
- sessionId: stringProp("Global session id in the form instanceId:localSessionId."),
2772
- 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.")
2773
2868
  }, ["sessionId"]), async (args, signal) => {
2774
2869
  const { instance, localId, globalId } = await resolveSession(args);
2775
2870
  const response = await clientFor(instance).status(localId, signal);
2776
- return ok({
2871
+ return ok$1({
2777
2872
  resolvedInstance: publicInstance(instance),
2778
2873
  ...response,
2779
2874
  sessionId: globalId
2780
2875
  });
2781
2876
  }),
2782
- 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({
2783
- sessionId: stringProp("Global session id in the form instanceId:localSessionId."),
2784
- instance: stringProp("Optional instance id/label; when supplied it must agree with sessionId."),
2785
- message: stringProp("Message text to deliver to the session."),
2786
- 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."),
2787
- 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.")
2788
2883
  }, ["sessionId", "message"]), async (args, signal) => {
2789
2884
  const { instance, localId, globalId } = await resolveSession(args);
2790
- const awaitMs = optionalNumber(args, "awaitMs");
2885
+ const awaitMs = optionalNumber$1(args, "awaitMs");
2791
2886
  const response = await clientFor(instance).sendMessage(localId, {
2792
- message: requiredString(args, "message"),
2793
- idempotencyKey: optionalString(args, "idempotencyKey") ?? randomUUID(),
2887
+ message: requiredString$1(args, "message"),
2888
+ idempotencyKey: optionalString$1(args, "idempotencyKey") ?? randomUUID(),
2794
2889
  ...awaitMs === void 0 ? {} : { awaitMs }
2795
2890
  }, signal);
2796
2891
  const delivered = !(response.delivered === false || response.delivery?.status === "failed" || response.delivery?.status === "error");
2797
2892
  const confirmed = delivered && response.confirmed === true;
2798
2893
  const confirmationTimedOut = delivered && !confirmed && (awaitMs !== void 0 && awaitMs > 0 || response.confirmationTimedOut === true);
2799
2894
  const isError = !delivered;
2800
- return jsonResult({
2895
+ return jsonResult$1({
2801
2896
  resolvedInstance: publicInstance(instance),
2802
2897
  sessionId: globalId,
2803
2898
  ...response,
@@ -2810,111 +2905,111 @@ function createFleetTools(options = {}) {
2810
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)." } : {}
2811
2906
  }, isError);
2812
2907
  }),
2813
- tool$1("send_keys", "Send key input to a fleet session.", objectSchema({
2814
- sessionId: stringProp("Global session id in the form instanceId:localSessionId."),
2815
- instance: stringProp("Optional instance id/label; when supplied it must agree with sessionId."),
2816
- keys: stringProp("Key sequence to send."),
2817
- 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."),
2818
2913
  raw: booleanProp("Pass keys through as raw input when the instance supports it.")
2819
2914
  }, ["sessionId", "keys"]), async (args, signal) => {
2820
2915
  const { instance, localId, globalId } = await resolveSession(args);
2821
2916
  const raw = optionalBoolean(args, "raw");
2822
2917
  const response = await clientFor(instance).sendKeys(localId, {
2823
- keys: requiredString(args, "keys"),
2824
- idempotencyKey: optionalString(args, "idempotencyKey") ?? randomUUID(),
2918
+ keys: requiredString$1(args, "keys"),
2919
+ idempotencyKey: optionalString$1(args, "idempotencyKey") ?? randomUUID(),
2825
2920
  ...raw === void 0 ? {} : { raw }
2826
2921
  }, signal);
2827
- return ok({
2922
+ return ok$1({
2828
2923
  resolvedInstance: publicInstance(instance),
2829
2924
  sessionId: globalId,
2830
2925
  ...response
2831
2926
  });
2832
2927
  }),
2833
- tool$1("respond", "Answer an awaited prompt in a fleet session by choice, option value, or explicit key override.", objectSchema({
2834
- sessionId: stringProp("Global session id in the form instanceId:localSessionId."),
2835
- instance: stringProp("Optional instance id/label; when supplied it must agree with sessionId."),
2836
- choice: stringProp("Named or numbered choice to select."),
2837
- optionValue: stringProp("Exact option value to select."),
2838
- keys: stringProp("Explicit key override to send instead of a mapped choice."),
2839
- 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.")
2840
2935
  }, ["sessionId"]), async (args, signal) => {
2841
2936
  const { instance, localId, globalId } = await resolveSession(args);
2842
2937
  const input = definedObject({
2843
- choice: optionalString(args, "choice"),
2844
- optionValue: optionalString(args, "optionValue"),
2845
- keys: optionalString(args, "keys"),
2846
- 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()
2847
2942
  });
2848
2943
  const response = await clientFor(instance).respond(localId, input, signal);
2849
- return ok({
2944
+ return ok$1({
2850
2945
  resolvedInstance: publicInstance(instance),
2851
2946
  sessionId: globalId,
2852
2947
  ...response
2853
2948
  });
2854
2949
  }),
2855
- tool$1("create_session", "Create a new session on a specific fleet instance. The instance argument is required; no default is used.", objectSchema({
2856
- instance: stringProp("Required instance id or label. Create never uses the registry default."),
2857
- agent: stringProp("Agent/runtime to create on the instance."),
2858
- name: stringProp("Optional display name for the session."),
2859
- workingDir: stringProp("Optional working directory on the remote instance."),
2860
- 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."),
2861
2956
  start: booleanProp("Whether the remote instance should start the session immediately."),
2862
- readyTimeoutMs: numberProp("F17: bounded ms to wait for the agent to become driveable before returning. The response carries ready/bound/blocker."),
2863
- 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."),
2864
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.")
2865
2960
  }, ["instance", "agent"]), async (args, signal) => {
2866
- const instance = await resolve(requiredString(args, "instance"));
2867
- const agent = requiredString(args, "agent");
2868
- const idempotencyKey = optionalString(args, "idempotencyKey") ?? randomUUID();
2869
- 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");
2870
2965
  const agentArgs = optionalStringArray(args, "agentArgs");
2871
2966
  if (permissionMode !== void 0) await assertCapability(instance, "permission_mode", "permissionMode", signal);
2872
2967
  if (agentArgs !== void 0) await assertCapability(instance, "agent_args", "agentArgs", signal);
2873
2968
  const response = await clientFor(instance).createSession(definedObject({
2874
2969
  agent,
2875
- name: optionalString(args, "name"),
2876
- workingDir: optionalString(args, "workingDir"),
2970
+ name: optionalString$1(args, "name"),
2971
+ workingDir: optionalString$1(args, "workingDir"),
2877
2972
  start: optionalBoolean(args, "start"),
2878
- readyTimeoutMs: optionalNumber(args, "readyTimeoutMs"),
2973
+ readyTimeoutMs: optionalNumber$1(args, "readyTimeoutMs"),
2879
2974
  permissionMode,
2880
2975
  agentArgs,
2881
2976
  idempotencyKey
2882
2977
  }), signal);
2883
2978
  const localSessionId = typeof response.sessionId === "string" ? response.sessionId : "";
2884
- return ok({
2979
+ return ok$1({
2885
2980
  resolvedInstance: publicInstance(instance),
2886
2981
  ...response,
2887
2982
  sessionId: localSessionId ? encodeSessionId(instance.id, localSessionId) : response.sessionId
2888
2983
  });
2889
2984
  }),
2890
- tool$1("stop_session", "Stop a fleet session.", objectSchema({
2891
- sessionId: stringProp("Global session id in the form instanceId:localSessionId."),
2892
- instance: stringProp("Optional instance id/label; when supplied it must agree with sessionId."),
2893
- idempotencyKey: stringProp("Optional caller idempotency key; auto-generated when omitted."),
2894
- 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.")
2895
2990
  }, ["sessionId"]), async (args, signal) => {
2896
2991
  const { instance, localId, globalId } = await resolveSession(args);
2897
- const idempotencyKey = optionalString(args, "idempotencyKey") ?? randomUUID();
2992
+ const idempotencyKey = optionalString$1(args, "idempotencyKey") ?? randomUUID();
2898
2993
  const response = await clientFor(instance).stopSession(localId, definedObject({
2899
- mode: optionalString(args, "mode"),
2994
+ mode: optionalString$1(args, "mode"),
2900
2995
  idempotencyKey
2901
2996
  }), signal);
2902
- return ok({
2997
+ return ok$1({
2903
2998
  resolvedInstance: publicInstance(instance),
2904
2999
  sessionId: globalId,
2905
3000
  ...response
2906
3001
  });
2907
3002
  }),
2908
- 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({
2909
3004
  instances: arrayProp("Instance ids or labels to poll. Omit with sessionIds to target those session instances; omit both to poll every registered instance."),
2910
3005
  sessionIds: arrayProp("Global session ids to filter to."),
2911
- timeoutMs: numberProp("Long-poll timeout per instance in milliseconds."),
3006
+ timeoutMs: numberProp$1("Long-poll timeout per instance in milliseconds."),
2912
3007
  kinds: arrayProp("Optional event kinds to filter to."),
2913
- 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.")
2914
3009
  }, []), async (args, signal) => {
2915
3010
  const target = await resolveAwaitTarget(args, getRegistry());
2916
- const cursorByInstance = takeAwaitTurnCursorMap(awaitTurnCursorKey(optionalString(args, "watcherId")));
2917
- const timeoutMs = optionalNumber(args, "timeoutMs");
3011
+ const cursorByInstance = takeAwaitTurnCursorMap(awaitTurnCursorKey(optionalString$1(args, "watcherId")));
3012
+ const timeoutMs = optionalNumber$1(args, "timeoutMs");
2918
3013
  const kinds = optionalStringArray(args, "kinds");
2919
3014
  const results = await mapWithConcurrency(target.instances, fleetFanoutConcurrency(AWAIT_TURN_FANOUT_CONCURRENCY), async (instance) => {
2920
3015
  const deadline = createAwaitTurnDeadline(timeoutMs, awaitTurnDeadlineSlackMs);
@@ -2957,7 +3052,7 @@ function createFleetTools(options = {}) {
2957
3052
  instance: publicInstance(instance),
2958
3053
  ...gap
2959
3054
  })));
2960
- return ok({
3055
+ return ok$1({
2961
3056
  resolvedInstances: target.instances.map(publicInstance),
2962
3057
  events: events$1,
2963
3058
  gaps,
@@ -2969,53 +3064,53 @@ function createFleetTools(options = {}) {
2969
3064
  ...errors.length > 0 ? { errors } : {}
2970
3065
  });
2971
3066
  }),
2972
- tool$1("read_file", "Read a file from one fleet instance via its existing /api/files/content endpoint.", objectSchema({
2973
- instance: stringProp("Instance id or label. Defaults to the registry default, or the sole instance."),
2974
- 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.")
2975
3070
  }, ["path"]), async (args, signal) => {
2976
- const instance = await resolve(optionalString(args, "instance"));
2977
- const response = await clientFor(instance).readFile(requiredString(args, "path"), signal);
2978
- 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({
2979
3074
  resolvedInstance: publicInstance(instance),
2980
3075
  ...response
2981
3076
  });
2982
3077
  }),
2983
- tool$1("list_dir", "List a directory on one fleet instance via its existing /api/files endpoint.", objectSchema({
2984
- instance: stringProp("Instance id or label. Defaults to the registry default, or the sole instance."),
2985
- 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.")
2986
3081
  }, ["path"]), async (args, signal) => {
2987
- const instance = await resolve(optionalString(args, "instance"));
2988
- const response = await clientFor(instance).listDir(requiredString(args, "path"), signal);
2989
- 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({
2990
3085
  resolvedInstance: publicInstance(instance),
2991
3086
  ...response
2992
3087
  });
2993
3088
  }),
2994
- tool$1("search", "Search files on one fleet instance via its existing /api/search endpoint.", objectSchema({
2995
- instance: stringProp("Instance id or label. Defaults to the registry default, or the sole instance."),
2996
- query: stringProp("Search query."),
2997
- 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.")
2998
3093
  }, ["query"]), async (args, signal) => {
2999
- const instance = await resolve(optionalString(args, "instance"));
3000
- const response = await clientFor(instance).search(requiredString(args, "query"), optionalString(args, "path"), signal);
3001
- 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({
3002
3097
  resolvedInstance: publicInstance(instance),
3003
3098
  ...response
3004
3099
  });
3005
3100
  }),
3006
- tool$1("git_show", "Read a file/revision through one fleet instance's existing /api/files/git-show endpoint.", objectSchema({
3007
- instance: stringProp("Instance id or label. Defaults to the registry default, or the sole instance."),
3008
- path: stringProp("Remote repository path or file path for git-show."),
3009
- ref: stringProp("Optional git ref/revision."),
3010
- rev: stringProp("Optional git revision alias."),
3011
- 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.")
3012
3107
  }, ["path"]), async (args, signal) => {
3013
- const instance = await resolve(optionalString(args, "instance"));
3108
+ const instance = await resolve(optionalString$1(args, "instance"));
3014
3109
  const response = await clientFor(instance).gitShow({
3015
3110
  ...args,
3016
3111
  instance: void 0
3017
3112
  }, signal);
3018
- return ok({
3113
+ return ok$1({
3019
3114
  resolvedInstance: publicInstance(instance),
3020
3115
  ...response
3021
3116
  });
@@ -3130,166 +3225,3293 @@ function isFleetErrorCode(code) {
3130
3225
  default: return false;
3131
3226
  }
3132
3227
  }
3133
- async function resolveAwaitTarget(args, registry) {
3134
- const instanceArgs = optionalStringArray(args, "instances");
3135
- const sessionIdArgs = optionalStringArray(args, "sessionIds");
3136
- const localSessionIdsByInstance = /* @__PURE__ */ new Map();
3137
- for (const sessionId of sessionIdArgs ?? []) {
3138
- const decoded = decodeSessionId(sessionId);
3139
- const existing = localSessionIdsByInstance.get(decoded.instanceId) ?? [];
3140
- existing.push(decoded.localId);
3141
- 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)}`);
3142
5889
  }
3143
- let instances;
3144
- if (instanceArgs !== void 0 && instanceArgs.length > 0) {
3145
- instances = uniqueInstances(await Promise.all(instanceArgs.map((arg) => registry.resolveInstance(arg))));
3146
- const ids = new Set(instances.map((instance) => instance.id));
3147
- 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`);
3148
- } else if (localSessionIdsByInstance.size > 0) instances = uniqueInstances(await Promise.all([...localSessionIdsByInstance.keys()].map((instanceId) => registry.resolveInstance(instanceId))));
3149
- else {
3150
- const infos = await registry.listInstances();
3151
- if (infos.length === 0) throw new FleetRegistryError("INSTANCE_REQUIRED", "await_turn requires at least one registered fleet instance");
3152
- 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)}`);
3153
5895
  }
3154
- return {
3155
- instances,
3156
- localSessionIdsByInstance
3157
- };
3158
5896
  }
3159
- 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;
3160
5902
  return {
3161
- ...session,
3162
- sessionId: encodeSessionId(instanceId, session.sessionId)
3163
- };
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;
3164
5994
  }
3165
- 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;
3166
6016
  return {
3167
- ...event,
3168
- instance: publicInstance(instance),
3169
- ...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 } : {}
3170
6025
  };
3171
6026
  }
3172
- function eventAtMs(value) {
3173
- if (typeof value === "number" && Number.isFinite(value)) return value;
3174
- if (typeof value === "string") {
3175
- const parsed = Date.parse(value);
3176
- 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;
3177
6089
  }
3178
- return 0;
3179
6090
  }
3180
- function compareStampedEvents(a, b) {
3181
- const atA = eventAtMs(a.at);
3182
- const atB = eventAtMs(b.at);
3183
- if (atA !== atB) return atA - atB;
3184
- 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;
3185
6093
  }
3186
- const MAX_WATCHER_ID_LEN = 200;
3187
- const MAX_AWAIT_TURN_CURSOR_KEYS = 1024;
3188
- function awaitTurnCursorKey(watcherId) {
3189
- const id = watcherId ?? "default";
3190
- 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;
3191
6097
  }
3192
- function takeAwaitTurnCursorMap(clientKey) {
3193
- const existing = awaitTurnCursors.get(clientKey);
3194
- if (existing) {
3195
- awaitTurnCursors.delete(clientKey);
3196
- awaitTurnCursors.set(clientKey, existing);
3197
- 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);
3198
6115
  }
3199
- const created = /* @__PURE__ */ new Map();
3200
- awaitTurnCursors.set(clientKey, created);
3201
- while (awaitTurnCursors.size > MAX_AWAIT_TURN_CURSOR_KEYS) {
3202
- const oldest = awaitTurnCursors.keys().next().value;
3203
- if (oldest === void 0) break;
3204
- 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
+ });
3205
6218
  }
3206
- return created;
6219
+ return rows;
3207
6220
  }
3208
- function isAwaitTurnSuccess(result) {
3209
- return result.ok;
6221
+ function compareQueued(a, b) {
6222
+ return a.sortKey - b.sortKey || a.order - b.order;
3210
6223
  }
3211
- function isAwaitTurnFailure(result) {
3212
- return !result.ok;
6224
+ function capQueued(entries, topK) {
6225
+ return entries.sort(compareQueued).slice(0, topK).map((entry) => entry.request);
3213
6226
  }
3214
- function failedProbeResult(info, code) {
3215
- 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);
3216
6332
  return {
3217
- id: info.id,
3218
- label: info.label,
3219
- reachable: false,
3220
- error: code,
3221
- ...hint ? { hint } : {}
6333
+ board,
6334
+ needsModel: capQueued(needsModel, topK),
6335
+ needsHuman: capQueued(needsHuman, topK),
6336
+ applied,
6337
+ nextWakeAt: wakeAt,
6338
+ nextWakeSeconds: wakeSeconds(wakeAt)
3222
6339
  };
3223
6340
  }
3224
- async function mapWithConcurrency(items, limit, fn) {
3225
- const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 1;
3226
- const concurrency = Math.max(1, Math.min(items.length || 1, safeLimit));
3227
- const results = new Array(items.length);
3228
- let nextIndex = 0;
3229
- async function worker() {
3230
- while (nextIndex < items.length) {
3231
- const index = nextIndex++;
3232
- results[index] = await fn(items[index], index);
3233
- }
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;
3234
6351
  }
3235
- await Promise.all(Array.from({ length: concurrency }, () => worker()));
3236
- return results;
3237
- }
3238
- function fleetFanoutConcurrency(defaultLimit) {
3239
- const raw = process.env[FLEET_FANOUT_CONCURRENCY_ENV];
3240
- const parsed = raw === void 0 ? NaN : Number.parseInt(raw, 10);
3241
- if (Number.isFinite(parsed) && parsed > 0) return Math.floor(parsed);
3242
- return defaultLimit;
3243
- }
3244
- function probeRateLimitBackoffMs(attempt) {
3245
- 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
+ ]);
3246
6438
  }
3247
- function nonNegativeNumberOrDefault(value, fallback) {
3248
- 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;
3249
6442
  }
3250
- async function delay(ms) {
3251
- if (ms <= 0) return;
3252
- 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
+ });
3253
6455
  }
3254
- function isAbortLike(err) {
3255
- if (!(err instanceof Error)) return false;
3256
- 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
+ });
3257
6468
  }
3258
- function uniqueInstances(instances) {
3259
- const seen = /* @__PURE__ */ new Set();
3260
- const result = [];
3261
- for (const instance of instances) {
3262
- if (seen.has(instance.id)) continue;
3263
- seen.add(instance.id);
3264
- 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);
3265
6475
  }
3266
6476
  return result;
3267
6477
  }
3268
- 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)}`);
3269
6492
  return {
3270
- id: instance.id,
3271
- label: instance.label
6493
+ owner: parts[0].trim(),
6494
+ name: parts[1].trim()
3272
6495
  };
3273
6496
  }
3274
- /**
3275
- * Build the FleetClient tunnel-auth options for a resolved instance.
3276
- * Resolution order: a `tunnelId` enables auto-mint + auto-refresh (and the
3277
- * evict-on-failure hook); else a static `tunnelToken` is sent directly (no
3278
- * retry, since it cannot be re-minted); else no tunnel auth.
3279
- */
3280
- function tunnelClientOptions(instance, provider) {
3281
- if (instance.tunnelId) {
3282
- 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");
3283
6502
  return {
3284
- getTunnelToken: () => provider.getToken(cfg),
3285
- onTunnelAuthInvalidate: () => provider.invalidate(cfg)
6503
+ requestId: requiredString(entry, "requestId"),
6504
+ verdict: entry.verdict
3286
6505
  };
3287
- }
3288
- if (instance.tunnelToken) {
3289
- const token = instance.tunnelToken;
3290
- return { getTunnelToken: async () => token };
3291
- }
3292
- 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
+ }));
3293
6515
  }
3294
6516
  function ok(value) {
3295
6517
  return jsonResult(value, false);
@@ -3314,42 +6536,39 @@ function errorCode(err) {
3314
6536
  const code = err.code;
3315
6537
  if (typeof code === "string") return code;
3316
6538
  }
3317
- return "FLEET_ERROR";
3318
- }
3319
- function definedObject(input) {
3320
- const result = {};
3321
- for (const [key, value] of Object.entries(input)) if (value !== void 0) result[key] = value;
3322
- return result;
6539
+ return "FIRST_MATE_ERROR";
3323
6540
  }
3324
6541
  function requiredString(args, key) {
3325
6542
  const value = args[key];
3326
- 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`);
3327
6544
  return value;
3328
6545
  }
3329
6546
  function optionalString(args, key) {
3330
6547
  const value = args[key];
3331
6548
  if (value === void 0) return void 0;
3332
- 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`);
3333
6550
  return value.trim() === "" ? void 0 : value;
3334
6551
  }
3335
6552
  function optionalNumber(args, key) {
3336
6553
  const value = args[key];
3337
6554
  if (value === void 0) return void 0;
3338
- 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`);
3339
6556
  return value;
3340
6557
  }
3341
- function optionalBoolean(args, key) {
6558
+ function requiredStringArray(args, key) {
3342
6559
  const value = args[key];
3343
- if (value === void 0) return void 0;
3344
- 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`);
3345
6561
  return value;
3346
6562
  }
3347
- function optionalStringArray(args, key) {
6563
+ function optionalRecordArray(args, key) {
3348
6564
  const value = args[key];
3349
6565
  if (value === void 0) return void 0;
3350
- 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`);
3351
6567
  return value;
3352
6568
  }
6569
+ function asRecord(value) {
6570
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
6571
+ }
3353
6572
  function objectSchema(properties, required) {
3354
6573
  return {
3355
6574
  type: "object",
@@ -3370,19 +6589,23 @@ function numberProp(description) {
3370
6589
  description
3371
6590
  };
3372
6591
  }
3373
- function booleanProp(description) {
6592
+ function stringArrayProp(description) {
3374
6593
  return {
3375
- type: "boolean",
6594
+ type: "array",
6595
+ items: { type: "string" },
3376
6596
  description
3377
6597
  };
3378
6598
  }
3379
- function arrayProp(description) {
6599
+ function arrayOfObjectsProp(description, properties, required) {
3380
6600
  return {
3381
6601
  type: "array",
3382
- items: { type: "string" },
3383
- description
6602
+ description,
6603
+ items: objectSchema(properties, required)
3384
6604
  };
3385
6605
  }
6606
+ function anyProp(description) {
6607
+ return { description };
6608
+ }
3386
6609
 
3387
6610
  //#endregion
3388
6611
  //#region src/lib/tree-sitter-grammars.ts
@@ -5206,8 +8429,8 @@ function runStructuralPassInProcess(opts) {
5206
8429
  consola.debug(`[code_search] structural skip ${relFile} (${size} bytes > cap)`);
5207
8430
  continue;
5208
8431
  }
5209
- let cached$1 = cacheGet(absPath, mtimeMs);
5210
- if (!cached$1) {
8432
+ let cached$2 = cacheGet(absPath, mtimeMs);
8433
+ if (!cached$2) {
5211
8434
  let source;
5212
8435
  try {
5213
8436
  source = readFileSync(absPath, "utf8");
@@ -5234,17 +8457,17 @@ function runStructuralPassInProcess(opts) {
5234
8457
  } catch (err) {
5235
8458
  consola.debug(`[code_search] tree-sitter parse failed for ${relFile}: ${err.message}`);
5236
8459
  }
5237
- cached$1 = {
8460
+ cached$2 = {
5238
8461
  mtimeMs,
5239
8462
  tree,
5240
8463
  source: tree ? source : null
5241
8464
  };
5242
- cachePut(absPath, cached$1);
8465
+ cachePut(absPath, cached$2);
5243
8466
  filesParsed += 1;
5244
8467
  if (benchOn) _benchStructural.filesParsed += 1;
5245
8468
  }
5246
- if (!cached$1.tree || !cached$1.source) continue;
5247
- 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) => ({
5248
8471
  line: e.hit.line,
5249
8472
  matchStart: e.hit.match_start,
5250
8473
  matchEnd: e.hit.match_end
@@ -5655,11 +8878,11 @@ function relativizeToWorkspace(file, workspaceCanonical) {
5655
8878
  async function enumerateWorkspaceFiles(opts) {
5656
8879
  const files = [];
5657
8880
  let total = 0;
5658
- let capped = false;
8881
+ let capped$1 = false;
5659
8882
  if (opts.signal.aborted) return {
5660
8883
  files,
5661
8884
  total,
5662
- capped
8885
+ capped: capped$1
5663
8886
  };
5664
8887
  let child;
5665
8888
  try {
@@ -5676,7 +8899,7 @@ async function enumerateWorkspaceFiles(opts) {
5676
8899
  return {
5677
8900
  files,
5678
8901
  total,
5679
- capped
8902
+ capped: capped$1
5680
8903
  };
5681
8904
  }
5682
8905
  child.on("error", () => {});
@@ -5688,7 +8911,7 @@ async function enumerateWorkspaceFiles(opts) {
5688
8911
  if (!child.stdout) return {
5689
8912
  files,
5690
8913
  total,
5691
- capped
8914
+ capped: capped$1
5692
8915
  };
5693
8916
  child.stdout.setEncoding("utf8");
5694
8917
  const rl = createInterface({
@@ -5713,7 +8936,7 @@ async function enumerateWorkspaceFiles(opts) {
5713
8936
  if (isSensitivePath(path.join(opts.workspaceCanonical, rel), opts.workspaceCanonical)) continue;
5714
8937
  total += 1;
5715
8938
  if (files.length < SCAN_MAX_FILES) files.push(rel);
5716
- else capped = true;
8939
+ else capped$1 = true;
5717
8940
  }
5718
8941
  } catch {} finally {
5719
8942
  clearTimeout(deadlineTimer);
@@ -5723,7 +8946,7 @@ async function enumerateWorkspaceFiles(opts) {
5723
8946
  return {
5724
8947
  files,
5725
8948
  total,
5726
- capped
8949
+ capped: capped$1
5727
8950
  };
5728
8951
  }
5729
8952
  async function searchCode(rawInput, externalSignal) {
@@ -5965,10 +9188,10 @@ async function searchCode(rawInput, externalSignal) {
5965
9188
  language: getLanguageKeyForPath(abs)
5966
9189
  };
5967
9190
  if (!result) try {
5968
- const cached$1 = cacheGet(abs, statSync(abs).mtimeMs);
5969
- if (cached$1?.tree) {
9191
+ const cached$2 = cacheGet(abs, statSync(abs).mtimeMs);
9192
+ if (cached$2?.tree) {
5970
9193
  const lang = getLanguageKeyForPath(abs);
5971
- if (lang) result = outlineFromTree(cached$1.tree, lang, ac.signal);
9194
+ if (lang) result = outlineFromTree(cached$2.tree, lang, ac.signal);
5972
9195
  }
5973
9196
  } catch {}
5974
9197
  const o = result ?? await outlineFile(abs, ac.signal);
@@ -7385,7 +10608,7 @@ async function runInit(workspace) {
7385
10608
  ];
7386
10609
  const onInactivityCheck = makeIndexProgressProbe(workspace);
7387
10610
  const startMs = Date.now();
7388
- let ok$2 = false;
10611
+ let ok$3 = false;
7389
10612
  let failureClass;
7390
10613
  try {
7391
10614
  const res = await runManagedExeCapture(binary, args, {
@@ -7402,10 +10625,10 @@ async function runInit(workspace) {
7402
10625
  }).catch(() => {});
7403
10626
  }
7404
10627
  });
7405
- ok$2 = !res.stalled && !res.timedOut && res.code === 0;
7406
- 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";
7407
10630
  } catch {
7408
- ok$2 = false;
10631
+ ok$3 = false;
7409
10632
  failureClass = "launch";
7410
10633
  } finally {
7411
10634
  releaseInit(workspace);
@@ -7422,9 +10645,9 @@ async function runInit(workspace) {
7422
10645
  finalMeta.lastIndexedDirty = g.dirty;
7423
10646
  }
7424
10647
  } catch {}
7425
- finalMeta.status = ok$2 ? "ready" : "failed";
10648
+ finalMeta.status = ok$3 ? "ready" : "failed";
7426
10649
  finalMeta.lastIndexedAt = (/* @__PURE__ */ new Date()).toISOString();
7427
- if (ok$2) {
10650
+ if (ok$3) {
7428
10651
  finalMeta.failedAttempts = 0;
7429
10652
  finalMeta.failureClass = void 0;
7430
10653
  } else {
@@ -8802,7 +12025,7 @@ function logAudit$1(record) {
8802
12025
  try {
8803
12026
  const fs$2 = await import("node:fs/promises");
8804
12027
  const path$1 = await import("node:path");
8805
- const { PATHS: PATHS$1 } = await import("./paths-Bljq3UJC.js");
12028
+ const { PATHS: PATHS$1 } = await import("./paths-DhLJ9bLG.js");
8806
12029
  const dir = path$1.join(PATHS$1.APP_DIR, "browser-mcp");
8807
12030
  await fs$2.mkdir(dir, { recursive: true });
8808
12031
  const line = JSON.stringify({
@@ -9446,7 +12669,7 @@ async function readResponseBodyCapped(response, routePath, capBytes = MAX_RESPON
9446
12669
  };
9447
12670
  const chunks = [];
9448
12671
  let totalBytes = 0;
9449
- let capped = false;
12672
+ let capped$1 = false;
9450
12673
  try {
9451
12674
  while (true) {
9452
12675
  const { done, value } = await reader.read();
@@ -9454,7 +12677,7 @@ async function readResponseBodyCapped(response, routePath, capBytes = MAX_RESPON
9454
12677
  if (!value) continue;
9455
12678
  totalBytes += value.byteLength;
9456
12679
  if (totalBytes > capBytes) {
9457
- capped = true;
12680
+ capped$1 = true;
9458
12681
  try {
9459
12682
  await reader.cancel("size_cap");
9460
12683
  } catch {}
@@ -9463,9 +12686,9 @@ async function readResponseBodyCapped(response, routePath, capBytes = MAX_RESPON
9463
12686
  chunks.push(value);
9464
12687
  }
9465
12688
  } catch (err) {
9466
- if (!capped) consola.warn(`readResponseBodyCapped: read error at ${routePath}:`, err);
12689
+ if (!capped$1) consola.warn(`readResponseBodyCapped: read error at ${routePath}:`, err);
9467
12690
  }
9468
- if (capped) {
12691
+ if (capped$1) {
9469
12692
  consola.warn(`Non-streaming upstream response at ${routePath} exceeded ${capBytes} bytes (10 MiB cap); dropping body to prevent OOM. Check upstream health.`);
9470
12693
  return {
9471
12694
  ok: false,
@@ -11852,8 +15075,8 @@ function coerceWithJsonSchema(value, schema) {
11852
15075
  }
11853
15076
  function getValidator(schema) {
11854
15077
  const key = schema;
11855
- const cached$1 = validatorCache.get(key);
11856
- if (cached$1) return cached$1;
15078
+ const cached$2 = validatorCache.get(key);
15079
+ if (cached$2) return cached$2;
11857
15080
  const validator = Compile(schema);
11858
15081
  validatorCache.set(key, validator);
11859
15082
  return validator;
@@ -14028,12 +17251,12 @@ function joinTextChunks(accum, idx) {
14028
17251
  */
14029
17252
  function makeLazyTextPart(chunks) {
14030
17253
  const upTo = chunks.length;
14031
- let cached$1;
17254
+ let cached$2;
14032
17255
  return {
14033
17256
  type: "text",
14034
17257
  get text() {
14035
- if (cached$1 === void 0) cached$1 = upTo === chunks.length ? chunks.join("") : chunks.slice(0, upTo).join("");
14036
- 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;
14037
17260
  }
14038
17261
  };
14039
17262
  }
@@ -14854,10 +18077,10 @@ function capToolResultText(content, capBytes) {
14854
18077
  } else images.push(block);
14855
18078
  }
14856
18079
  if (textBytes <= capBytes) return void 0;
14857
- const capped = truncateModelText(texts.join("\n"), capBytes);
18080
+ const capped$1 = truncateModelText(texts.join("\n"), capBytes);
14858
18081
  return [...images, {
14859
18082
  type: "text",
14860
- text: capped
18083
+ text: capped$1
14861
18084
  }];
14862
18085
  }
14863
18086
 
@@ -14922,8 +18145,8 @@ const calculateTokens = (messages, encoder, constants) => {
14922
18145
  */
14923
18146
  const getEncodeChatFunction = async (encoding) => {
14924
18147
  if (encodingCache.has(encoding)) {
14925
- const cached$1 = encodingCache.get(encoding);
14926
- if (cached$1) return cached$1;
18148
+ const cached$2 = encodingCache.get(encoding);
18149
+ if (cached$2) return cached$2;
14927
18150
  }
14928
18151
  const supportedEncoding = encoding;
14929
18152
  if (!(supportedEncoding in ENCODING_MAP)) {
@@ -15351,6 +18574,19 @@ function fleetToolsEnabled() {
15351
18574
  return state.fleetEnabled || process.env.GH_ROUTER_ENABLE_FLEET === "1";
15352
18575
  }
15353
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
+ /**
15354
18590
  * Gate for ai-or-die Artifact review tools.
15355
18591
  *
15356
18592
  * Returns true iff this github-router process was launched inside an
@@ -15558,6 +18794,7 @@ function toolEntries(scope) {
15558
18794
  if (t.capability === "stand_in") return standInToolEnabled();
15559
18795
  if (t.capability === "browser") return browserToolsEnabled();
15560
18796
  if (t.capability === "fleet") return fleetToolsEnabled();
18797
+ if (t.capability === "agents") return agentToolsEnabled();
15561
18798
  if (t.capability === "artifact") return artifactToolsEnabled();
15562
18799
  if (t.capability === "browser_compound") return browserToolsEnabled() && browserCompoundToolsEnabled();
15563
18800
  if (t.capability === "browser_power") return browserToolsEnabled() && browserPowerToolsEnabled();
@@ -15886,6 +19123,7 @@ async function handleToolsCall(body, scope) {
15886
19123
  if (nonPersonaTool && nonPersonaTool.capability === "stand_in" && !standInToolEnabled()) return rpcError(body.id, RPC_METHOD_NOT_FOUND, `tools/call: unknown tool "${name}"`);
15887
19124
  if (nonPersonaTool && nonPersonaTool.capability === "browser" && !browserToolsEnabled()) return rpcError(body.id, RPC_METHOD_NOT_FOUND, `tools/call: unknown tool "${name}"`);
15888
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}"`);
15889
19127
  if (nonPersonaTool && nonPersonaTool.capability === "artifact" && !artifactToolsEnabled()) return rpcError(body.id, RPC_METHOD_NOT_FOUND, `tools/call: unknown tool "${name}"`);
15890
19128
  if (nonPersonaTool && nonPersonaTool.capability === "browser_compound" && !(browserToolsEnabled() && browserCompoundToolsEnabled())) return rpcError(body.id, RPC_METHOD_NOT_FOUND, `tools/call: unknown tool "${name}"`);
15891
19129
  if (nonPersonaTool && nonPersonaTool.capability === "browser_power" && !(browserToolsEnabled() && browserPowerToolsEnabled())) return rpcError(body.id, RPC_METHOD_NOT_FOUND, `tools/call: unknown tool "${name}"`);
@@ -19451,8 +22689,8 @@ async function runWorkerAgentOnce(opts) {
19451
22689
  afterToolCall: async (ctx) => {
19452
22690
  budget.recordToolBytes(ctx.result);
19453
22691
  if (ctxBudget) {
19454
- const capped = capToolResultText(ctx.result.content, ctxBudget.perResultCapBytes);
19455
- if (capped) return { content: capped };
22692
+ const capped$1 = capToolResultText(ctx.result.content, ctxBudget.perResultCapBytes);
22693
+ if (capped$1) return { content: capped$1 };
19456
22694
  }
19457
22695
  },
19458
22696
  prepareNextTurn: async () => {
@@ -21618,7 +24856,8 @@ const MCP_GROUPS = Object.freeze([
21618
24856
  "orchestrate",
21619
24857
  "browser",
21620
24858
  "decide",
21621
- "fleet"
24859
+ "fleet",
24860
+ "first-mate"
21622
24861
  ]);
21623
24862
  const GROUP_META = Object.freeze({
21624
24863
  peers: {
@@ -21655,6 +24894,11 @@ const GROUP_META = Object.freeze({
21655
24894
  preferredKey: "fleet",
21656
24895
  urlSuffix: "fleet",
21657
24896
  serverInfoName: "github-router-fleet"
24897
+ },
24898
+ "first-mate": {
24899
+ preferredKey: "first-mate",
24900
+ urlSuffix: "first-mate",
24901
+ serverInfoName: "github-router-first-mate"
21658
24902
  }
21659
24903
  });
21660
24904
  /** True iff `s` is a registered group name (route `:group` param validation). */
@@ -22008,6 +25252,8 @@ function buildAgentPrompt(persona, opts) {
22008
25252
  * out of the live catalog).
22009
25253
  * - Conditionally lists stand_in only when `standInAvailable`
22010
25254
  * (mirrors `standInToolEnabled()`).
25255
+ * - Conditionally lists gh-first-mate only when `agentToolsAvailable`
25256
+ * (mirrors `agentToolsEnabled()`).
22011
25257
  * - Mentions `codex-cli` stdio bridge only when `codexCli`.
22012
25258
  * - Does NOT re-document Claude Code's built-in delegation semantics
22013
25259
  * (Agent-tool recursion, agent-teams coordination) — Claude
@@ -22034,7 +25280,10 @@ function buildPeerAwarenessSnippet(opts) {
22034
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.");
22035
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.`);
22036
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).`);
22037
- 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
+ }
22038
25287
  para2Parts.push(`\`mcp__${searchKey}__web\` surfaces citable sources for docs, errors, and upstream issues.`);
22039
25288
  if (opts.standInAvailable) para2Parts.push(`\`mcp__${decideKey}__stand_in\` provides three-lab consensus for decision tiebreak when the user is unavailable.`);
22040
25289
  if (opts.browseAvailable) {
@@ -22768,6 +26017,7 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
22768
26017
  },
22769
26018
  ...ARTIFACT_TOOLS,
22770
26019
  ...FLEET_TOOLS,
26020
+ ...FIRST_MATE_TOOLS,
22771
26021
  ...BROWSER_TOOLS.map((t) => ({
22772
26022
  ...t,
22773
26023
  group: "browser",
@@ -23096,5 +26346,5 @@ async function runStandInToolCall(args, signal) {
23096
26346
  }
23097
26347
 
23098
26348
  //#endregion
23099
- 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 };
23100
- //# sourceMappingURL=peer-mcp-personas-CThUmeHE.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