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