github-router 0.3.138 → 0.3.150
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser-ext/manifest.json +1 -1
- package/dist/{engine-kSGZBPRK.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-CThUmeHE.js → peer-mcp-personas-D_HyWhUb.js} +3584 -334
- 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-CThUmeHE.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";
|
|
@@ -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
|
|
@@ -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,7 +1559,7 @@ 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
|
|
@@ -1690,17 +1785,17 @@ function createTunnelTokenProvider(runner = realDevtunnelRunner()) {
|
|
|
1690
1785
|
async getToken(cfg) {
|
|
1691
1786
|
const key = cfg.tunnelId;
|
|
1692
1787
|
const now = Date.now();
|
|
1693
|
-
const cached$
|
|
1694
|
-
if (cached$
|
|
1695
|
-
const comfortablyFresh = cached$
|
|
1696
|
-
const recentlyMinted = now - cached$
|
|
1697
|
-
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;
|
|
1698
1793
|
}
|
|
1699
1794
|
const inf = inflight.get(key);
|
|
1700
1795
|
if (inf) return inf;
|
|
1701
1796
|
const bo = backoff.get(key);
|
|
1702
1797
|
if (bo && now < bo.until) {
|
|
1703
|
-
if (cached$
|
|
1798
|
+
if (cached$2 && cached$2.expMs > now) return cached$2.token;
|
|
1704
1799
|
throw bo.err;
|
|
1705
1800
|
}
|
|
1706
1801
|
const p = mintOnce(cfg);
|
|
@@ -2638,8 +2733,8 @@ function createFleetTools(options = {}) {
|
|
|
2638
2733
|
}
|
|
2639
2734
|
async function getInstanceCapabilities(instance, signal) {
|
|
2640
2735
|
const now = Date.now();
|
|
2641
|
-
const cached$
|
|
2642
|
-
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;
|
|
2643
2738
|
try {
|
|
2644
2739
|
const response = await clientFor(instance).capabilities(signal);
|
|
2645
2740
|
const caps = new Set(response.capabilities);
|
|
@@ -2664,10 +2759,10 @@ function createFleetTools(options = {}) {
|
|
|
2664
2759
|
return getRegistry().resolveInstance(arg);
|
|
2665
2760
|
}
|
|
2666
2761
|
async function resolveSession(args) {
|
|
2667
|
-
const globalId = requiredString(args, "sessionId");
|
|
2762
|
+
const globalId = requiredString$1(args, "sessionId");
|
|
2668
2763
|
const decoded = decodeSessionId(globalId);
|
|
2669
2764
|
const instance = await resolve(decoded.instanceId);
|
|
2670
|
-
const explicitInstance = optionalString(args, "instance");
|
|
2765
|
+
const explicitInstance = optionalString$1(args, "instance");
|
|
2671
2766
|
if (explicitInstance !== void 0) {
|
|
2672
2767
|
const explicit = await resolve(explicitInstance);
|
|
2673
2768
|
if (explicit.id !== decoded.instanceId) throw new FleetToolInputError("INSTANCE_MISMATCH", `sessionId is for instance ${JSON.stringify(decoded.instanceId)} but arguments.instance resolved to ${JSON.stringify(explicit.id)}`);
|
|
@@ -2681,8 +2776,8 @@ function createFleetTools(options = {}) {
|
|
|
2681
2776
|
async function probeInstance(info) {
|
|
2682
2777
|
const cacheKey = `${info.id}\0${info.url}`;
|
|
2683
2778
|
const now = Date.now();
|
|
2684
|
-
const cached$
|
|
2685
|
-
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;
|
|
2686
2781
|
for (let attempt = 0; attempt <= INSTANCE_PROBE_RATE_LIMIT_MAX_RETRIES; attempt++) {
|
|
2687
2782
|
const timeout = createProbeTimeout();
|
|
2688
2783
|
try {
|
|
@@ -2735,69 +2830,69 @@ function createFleetTools(options = {}) {
|
|
|
2735
2830
|
try {
|
|
2736
2831
|
return await handler(args, signal);
|
|
2737
2832
|
} catch (err) {
|
|
2738
|
-
return errorResult(err);
|
|
2833
|
+
return errorResult$1(err);
|
|
2739
2834
|
}
|
|
2740
2835
|
}
|
|
2741
2836
|
};
|
|
2742
2837
|
}
|
|
2743
2838
|
return Object.freeze([
|
|
2744
|
-
tool$1("list_instances", "List registered remote ai-or-die instances in the fleet registry. Tokens are never returned.", objectSchema({}, []), async () => {
|
|
2745
|
-
return ok({ instances: await mapWithConcurrency(await getRegistry().listInstances(), fleetFanoutConcurrency(LIST_INSTANCES_FANOUT_CONCURRENCY), (instance) => probeInstance(instance)) });
|
|
2839
|
+
tool$1("list_instances", "List registered remote ai-or-die instances in the fleet registry. Tokens are never returned.", objectSchema$1({}, []), async () => {
|
|
2840
|
+
return ok$1({ instances: await mapWithConcurrency(await getRegistry().listInstances(), fleetFanoutConcurrency(LIST_INSTANCES_FANOUT_CONCURRENCY), (instance) => probeInstance(instance)) });
|
|
2746
2841
|
}),
|
|
2747
|
-
tool$1("list_sessions", "List sessions on one fleet instance, returning globally-addressable session ids.", objectSchema({ instance: stringProp("Instance id or label. Defaults to the registry default, or the sole instance.") }, []), async (args, signal) => {
|
|
2748
|
-
const instance = await resolve(optionalString(args, "instance"));
|
|
2842
|
+
tool$1("list_sessions", "List sessions on one fleet instance, returning globally-addressable session ids.", objectSchema$1({ instance: stringProp$1("Instance id or label. Defaults to the registry default, or the sole instance.") }, []), async (args, signal) => {
|
|
2843
|
+
const instance = await resolve(optionalString$1(args, "instance"));
|
|
2749
2844
|
const response = await clientFor(instance).listSessions(signal);
|
|
2750
|
-
return ok({
|
|
2845
|
+
return ok$1({
|
|
2751
2846
|
resolvedInstance: publicInstance(instance),
|
|
2752
2847
|
sessions: response.sessions.map((session) => globalizeSession(instance.id, session))
|
|
2753
2848
|
});
|
|
2754
2849
|
}),
|
|
2755
|
-
tool$1("read_session", "Read recent text output from an addressed fleet session.", objectSchema({
|
|
2756
|
-
sessionId: stringProp("Global session id in the form instanceId:localSessionId."),
|
|
2757
|
-
instance: stringProp("Optional instance id/label; when supplied it must agree with sessionId."),
|
|
2758
|
-
lines: numberProp("Number of recent lines to read."),
|
|
2759
|
-
format: stringProp("Reserved for future formatting; results are JSON text today.")
|
|
2850
|
+
tool$1("read_session", "Read recent text output from an addressed fleet session.", objectSchema$1({
|
|
2851
|
+
sessionId: stringProp$1("Global session id in the form instanceId:localSessionId."),
|
|
2852
|
+
instance: stringProp$1("Optional instance id/label; when supplied it must agree with sessionId."),
|
|
2853
|
+
lines: numberProp$1("Number of recent lines to read."),
|
|
2854
|
+
format: stringProp$1("Reserved for future formatting; results are JSON text today.")
|
|
2760
2855
|
}, ["sessionId"]), async (args, signal) => {
|
|
2761
2856
|
const { instance, localId, globalId } = await resolveSession(args);
|
|
2762
|
-
const lines = optionalNumber(args, "lines");
|
|
2857
|
+
const lines = optionalNumber$1(args, "lines");
|
|
2763
2858
|
const response = await clientFor(instance).readSession(localId, lines, signal);
|
|
2764
|
-
return ok({
|
|
2859
|
+
return ok$1({
|
|
2765
2860
|
resolvedInstance: publicInstance(instance),
|
|
2766
2861
|
...response,
|
|
2767
2862
|
sessionId: globalId
|
|
2768
2863
|
});
|
|
2769
2864
|
}),
|
|
2770
|
-
tool$1("session_status", "Fetch lifecycle and interaction status for an addressed fleet session.", objectSchema({
|
|
2771
|
-
sessionId: stringProp("Global session id in the form instanceId:localSessionId."),
|
|
2772
|
-
instance: stringProp("Optional instance id/label; when supplied it must agree with sessionId.")
|
|
2865
|
+
tool$1("session_status", "Fetch lifecycle and interaction status for an addressed fleet session.", objectSchema$1({
|
|
2866
|
+
sessionId: stringProp$1("Global session id in the form instanceId:localSessionId."),
|
|
2867
|
+
instance: stringProp$1("Optional instance id/label; when supplied it must agree with sessionId.")
|
|
2773
2868
|
}, ["sessionId"]), async (args, signal) => {
|
|
2774
2869
|
const { instance, localId, globalId } = await resolveSession(args);
|
|
2775
2870
|
const response = await clientFor(instance).status(localId, signal);
|
|
2776
|
-
return ok({
|
|
2871
|
+
return ok$1({
|
|
2777
2872
|
resolvedInstance: publicInstance(instance),
|
|
2778
2873
|
...response,
|
|
2779
2874
|
sessionId: globalId
|
|
2780
2875
|
});
|
|
2781
2876
|
}),
|
|
2782
|
-
tool$1("send_message", "Send a message to a fleet session. isError reflects DELIVERY ONLY: it is true only when the message could not be delivered to the session (transport/precondition failure). A delivered message whose confirmation did not arrive within awaitMs is NOT an error — it returns delivered:true with confirmationPending/confirmationTimedOut, because a long turn legitimately outruns awaitMs. Recommended pattern: send with awaitMs:0 for a fast delivery ack that never blocks on confirmation, then call await_turn (filtered to this sessionId) to observe the session's actual turn completion. The idempotencyKey makes a retried send safe (a retry never re-types the message).", objectSchema({
|
|
2783
|
-
sessionId: stringProp("Global session id in the form instanceId:localSessionId."),
|
|
2784
|
-
instance: stringProp("Optional instance id/label; when supplied it must agree with sessionId."),
|
|
2785
|
-
message: stringProp("Message text to deliver to the session."),
|
|
2786
|
-
idempotencyKey: stringProp("Optional caller idempotency key; AUTO-GENERATED when omitted, so you normally never pass it. Supply your OWN stable key only when you will retry the SAME send and need the upstream to dedupe it."),
|
|
2787
|
-
awaitMs: numberProp("Optional best-effort confirmation wait (ms) — NOT a deadline. Prefer awaitMs:0 plus await_turn; a turn that outruns awaitMs returns confirmationPending, not an error.")
|
|
2877
|
+
tool$1("send_message", "Send a message to a fleet session. isError reflects DELIVERY ONLY: it is true only when the message could not be delivered to the session (transport/precondition failure). A delivered message whose confirmation did not arrive within awaitMs is NOT an error — it returns delivered:true with confirmationPending/confirmationTimedOut, because a long turn legitimately outruns awaitMs. Recommended pattern: send with awaitMs:0 for a fast delivery ack that never blocks on confirmation, then call await_turn (filtered to this sessionId) to observe the session's actual turn completion. The idempotencyKey makes a retried send safe (a retry never re-types the message).", objectSchema$1({
|
|
2878
|
+
sessionId: stringProp$1("Global session id in the form instanceId:localSessionId."),
|
|
2879
|
+
instance: stringProp$1("Optional instance id/label; when supplied it must agree with sessionId."),
|
|
2880
|
+
message: stringProp$1("Message text to deliver to the session."),
|
|
2881
|
+
idempotencyKey: stringProp$1("Optional caller idempotency key; AUTO-GENERATED when omitted, so you normally never pass it. Supply your OWN stable key only when you will retry the SAME send and need the upstream to dedupe it."),
|
|
2882
|
+
awaitMs: numberProp$1("Optional best-effort confirmation wait (ms) — NOT a deadline. Prefer awaitMs:0 plus await_turn; a turn that outruns awaitMs returns confirmationPending, not an error.")
|
|
2788
2883
|
}, ["sessionId", "message"]), async (args, signal) => {
|
|
2789
2884
|
const { instance, localId, globalId } = await resolveSession(args);
|
|
2790
|
-
const awaitMs = optionalNumber(args, "awaitMs");
|
|
2885
|
+
const awaitMs = optionalNumber$1(args, "awaitMs");
|
|
2791
2886
|
const response = await clientFor(instance).sendMessage(localId, {
|
|
2792
|
-
message: requiredString(args, "message"),
|
|
2793
|
-
idempotencyKey: optionalString(args, "idempotencyKey") ?? randomUUID(),
|
|
2887
|
+
message: requiredString$1(args, "message"),
|
|
2888
|
+
idempotencyKey: optionalString$1(args, "idempotencyKey") ?? randomUUID(),
|
|
2794
2889
|
...awaitMs === void 0 ? {} : { awaitMs }
|
|
2795
2890
|
}, signal);
|
|
2796
2891
|
const delivered = !(response.delivered === false || response.delivery?.status === "failed" || response.delivery?.status === "error");
|
|
2797
2892
|
const confirmed = delivered && response.confirmed === true;
|
|
2798
2893
|
const confirmationTimedOut = delivered && !confirmed && (awaitMs !== void 0 && awaitMs > 0 || response.confirmationTimedOut === true);
|
|
2799
2894
|
const isError = !delivered;
|
|
2800
|
-
return jsonResult({
|
|
2895
|
+
return jsonResult$1({
|
|
2801
2896
|
resolvedInstance: publicInstance(instance),
|
|
2802
2897
|
sessionId: globalId,
|
|
2803
2898
|
...response,
|
|
@@ -2810,111 +2905,111 @@ function createFleetTools(options = {}) {
|
|
|
2810
2905
|
...isError ? { message: "message was not delivered to the session by the upstream instance" } : confirmationTimedOut ? { message: "delivered; turn completion not confirmed in the await window. Use await_turn filtered to this sessionId to observe completion (the idempotencyKey makes a retried send safe)." } : {}
|
|
2811
2906
|
}, isError);
|
|
2812
2907
|
}),
|
|
2813
|
-
tool$1("send_keys", "Send key input to a fleet session.", objectSchema({
|
|
2814
|
-
sessionId: stringProp("Global session id in the form instanceId:localSessionId."),
|
|
2815
|
-
instance: stringProp("Optional instance id/label; when supplied it must agree with sessionId."),
|
|
2816
|
-
keys: stringProp("Key sequence to send."),
|
|
2817
|
-
idempotencyKey: stringProp("Optional caller idempotency key; auto-generated when omitted."),
|
|
2908
|
+
tool$1("send_keys", "Send key input to a fleet session.", objectSchema$1({
|
|
2909
|
+
sessionId: stringProp$1("Global session id in the form instanceId:localSessionId."),
|
|
2910
|
+
instance: stringProp$1("Optional instance id/label; when supplied it must agree with sessionId."),
|
|
2911
|
+
keys: stringProp$1("Key sequence to send."),
|
|
2912
|
+
idempotencyKey: stringProp$1("Optional caller idempotency key; auto-generated when omitted."),
|
|
2818
2913
|
raw: booleanProp("Pass keys through as raw input when the instance supports it.")
|
|
2819
2914
|
}, ["sessionId", "keys"]), async (args, signal) => {
|
|
2820
2915
|
const { instance, localId, globalId } = await resolveSession(args);
|
|
2821
2916
|
const raw = optionalBoolean(args, "raw");
|
|
2822
2917
|
const response = await clientFor(instance).sendKeys(localId, {
|
|
2823
|
-
keys: requiredString(args, "keys"),
|
|
2824
|
-
idempotencyKey: optionalString(args, "idempotencyKey") ?? randomUUID(),
|
|
2918
|
+
keys: requiredString$1(args, "keys"),
|
|
2919
|
+
idempotencyKey: optionalString$1(args, "idempotencyKey") ?? randomUUID(),
|
|
2825
2920
|
...raw === void 0 ? {} : { raw }
|
|
2826
2921
|
}, signal);
|
|
2827
|
-
return ok({
|
|
2922
|
+
return ok$1({
|
|
2828
2923
|
resolvedInstance: publicInstance(instance),
|
|
2829
2924
|
sessionId: globalId,
|
|
2830
2925
|
...response
|
|
2831
2926
|
});
|
|
2832
2927
|
}),
|
|
2833
|
-
tool$1("respond", "Answer an awaited prompt in a fleet session by choice, option value, or explicit key override.", objectSchema({
|
|
2834
|
-
sessionId: stringProp("Global session id in the form instanceId:localSessionId."),
|
|
2835
|
-
instance: stringProp("Optional instance id/label; when supplied it must agree with sessionId."),
|
|
2836
|
-
choice: stringProp("Named or numbered choice to select."),
|
|
2837
|
-
optionValue: stringProp("Exact option value to select."),
|
|
2838
|
-
keys: stringProp("Explicit key override to send instead of a mapped choice."),
|
|
2839
|
-
idempotencyKey: stringProp("Optional caller idempotency key; auto-generated when omitted.")
|
|
2928
|
+
tool$1("respond", "Answer an awaited prompt in a fleet session by choice, option value, or explicit key override.", objectSchema$1({
|
|
2929
|
+
sessionId: stringProp$1("Global session id in the form instanceId:localSessionId."),
|
|
2930
|
+
instance: stringProp$1("Optional instance id/label; when supplied it must agree with sessionId."),
|
|
2931
|
+
choice: stringProp$1("Named or numbered choice to select."),
|
|
2932
|
+
optionValue: stringProp$1("Exact option value to select."),
|
|
2933
|
+
keys: stringProp$1("Explicit key override to send instead of a mapped choice."),
|
|
2934
|
+
idempotencyKey: stringProp$1("Optional caller idempotency key; auto-generated when omitted.")
|
|
2840
2935
|
}, ["sessionId"]), async (args, signal) => {
|
|
2841
2936
|
const { instance, localId, globalId } = await resolveSession(args);
|
|
2842
2937
|
const input = definedObject({
|
|
2843
|
-
choice: optionalString(args, "choice"),
|
|
2844
|
-
optionValue: optionalString(args, "optionValue"),
|
|
2845
|
-
keys: optionalString(args, "keys"),
|
|
2846
|
-
idempotencyKey: optionalString(args, "idempotencyKey") ?? randomUUID()
|
|
2938
|
+
choice: optionalString$1(args, "choice"),
|
|
2939
|
+
optionValue: optionalString$1(args, "optionValue"),
|
|
2940
|
+
keys: optionalString$1(args, "keys"),
|
|
2941
|
+
idempotencyKey: optionalString$1(args, "idempotencyKey") ?? randomUUID()
|
|
2847
2942
|
});
|
|
2848
2943
|
const response = await clientFor(instance).respond(localId, input, signal);
|
|
2849
|
-
return ok({
|
|
2944
|
+
return ok$1({
|
|
2850
2945
|
resolvedInstance: publicInstance(instance),
|
|
2851
2946
|
sessionId: globalId,
|
|
2852
2947
|
...response
|
|
2853
2948
|
});
|
|
2854
2949
|
}),
|
|
2855
|
-
tool$1("create_session", "Create a new session on a specific fleet instance. The instance argument is required; no default is used.", objectSchema({
|
|
2856
|
-
instance: stringProp("Required instance id or label. Create never uses the registry default."),
|
|
2857
|
-
agent: stringProp("Agent/runtime to create on the instance."),
|
|
2858
|
-
name: stringProp("Optional display name for the session."),
|
|
2859
|
-
workingDir: stringProp("Optional working directory on the remote instance."),
|
|
2860
|
-
idempotencyKey: stringProp("Optional caller idempotency key; auto-generated when omitted."),
|
|
2950
|
+
tool$1("create_session", "Create a new session on a specific fleet instance. The instance argument is required; no default is used.", objectSchema$1({
|
|
2951
|
+
instance: stringProp$1("Required instance id or label. Create never uses the registry default."),
|
|
2952
|
+
agent: stringProp$1("Agent/runtime to create on the instance."),
|
|
2953
|
+
name: stringProp$1("Optional display name for the session."),
|
|
2954
|
+
workingDir: stringProp$1("Optional working directory on the remote instance."),
|
|
2955
|
+
idempotencyKey: stringProp$1("Optional caller idempotency key; auto-generated when omitted."),
|
|
2861
2956
|
start: booleanProp("Whether the remote instance should start the session immediately."),
|
|
2862
|
-
readyTimeoutMs: numberProp("F17: bounded ms to wait for the agent to become driveable before returning. The response carries ready/bound/blocker."),
|
|
2863
|
-
permissionMode: stringProp("F10 (claude only): permission mode the launched agent starts in — one of plan | acceptEdits | default | bypassPermissions. Rejected with BAD_REQUEST if unknown or if agentArgs also sets it."),
|
|
2957
|
+
readyTimeoutMs: numberProp$1("F17: bounded ms to wait for the agent to become driveable before returning. The response carries ready/bound/blocker."),
|
|
2958
|
+
permissionMode: stringProp$1("F10 (claude only): permission mode the launched agent starts in — one of plan | acceptEdits | default | bypassPermissions. Rejected with BAD_REQUEST if unknown or if agentArgs also sets it."),
|
|
2864
2959
|
agentArgs: arrayProp("F10 (claude only): extra launcher args appended after the github-router prefix. Must NOT include --permission-mode or --dangerously-skip-permissions (use permissionMode) — rejected with BAD_REQUEST.")
|
|
2865
2960
|
}, ["instance", "agent"]), async (args, signal) => {
|
|
2866
|
-
const instance = await resolve(requiredString(args, "instance"));
|
|
2867
|
-
const agent = requiredString(args, "agent");
|
|
2868
|
-
const idempotencyKey = optionalString(args, "idempotencyKey") ?? randomUUID();
|
|
2869
|
-
const permissionMode = optionalString(args, "permissionMode");
|
|
2961
|
+
const instance = await resolve(requiredString$1(args, "instance"));
|
|
2962
|
+
const agent = requiredString$1(args, "agent");
|
|
2963
|
+
const idempotencyKey = optionalString$1(args, "idempotencyKey") ?? randomUUID();
|
|
2964
|
+
const permissionMode = optionalString$1(args, "permissionMode");
|
|
2870
2965
|
const agentArgs = optionalStringArray(args, "agentArgs");
|
|
2871
2966
|
if (permissionMode !== void 0) await assertCapability(instance, "permission_mode", "permissionMode", signal);
|
|
2872
2967
|
if (agentArgs !== void 0) await assertCapability(instance, "agent_args", "agentArgs", signal);
|
|
2873
2968
|
const response = await clientFor(instance).createSession(definedObject({
|
|
2874
2969
|
agent,
|
|
2875
|
-
name: optionalString(args, "name"),
|
|
2876
|
-
workingDir: optionalString(args, "workingDir"),
|
|
2970
|
+
name: optionalString$1(args, "name"),
|
|
2971
|
+
workingDir: optionalString$1(args, "workingDir"),
|
|
2877
2972
|
start: optionalBoolean(args, "start"),
|
|
2878
|
-
readyTimeoutMs: optionalNumber(args, "readyTimeoutMs"),
|
|
2973
|
+
readyTimeoutMs: optionalNumber$1(args, "readyTimeoutMs"),
|
|
2879
2974
|
permissionMode,
|
|
2880
2975
|
agentArgs,
|
|
2881
2976
|
idempotencyKey
|
|
2882
2977
|
}), signal);
|
|
2883
2978
|
const localSessionId = typeof response.sessionId === "string" ? response.sessionId : "";
|
|
2884
|
-
return ok({
|
|
2979
|
+
return ok$1({
|
|
2885
2980
|
resolvedInstance: publicInstance(instance),
|
|
2886
2981
|
...response,
|
|
2887
2982
|
sessionId: localSessionId ? encodeSessionId(instance.id, localSessionId) : response.sessionId
|
|
2888
2983
|
});
|
|
2889
2984
|
}),
|
|
2890
|
-
tool$1("stop_session", "Stop a fleet session.", objectSchema({
|
|
2891
|
-
sessionId: stringProp("Global session id in the form instanceId:localSessionId."),
|
|
2892
|
-
instance: stringProp("Optional instance id/label; when supplied it must agree with sessionId."),
|
|
2893
|
-
idempotencyKey: stringProp("Optional caller idempotency key; auto-generated when omitted."),
|
|
2894
|
-
mode: stringProp("Optional stop mode understood by the remote instance.")
|
|
2985
|
+
tool$1("stop_session", "Stop a fleet session.", objectSchema$1({
|
|
2986
|
+
sessionId: stringProp$1("Global session id in the form instanceId:localSessionId."),
|
|
2987
|
+
instance: stringProp$1("Optional instance id/label; when supplied it must agree with sessionId."),
|
|
2988
|
+
idempotencyKey: stringProp$1("Optional caller idempotency key; auto-generated when omitted."),
|
|
2989
|
+
mode: stringProp$1("Optional stop mode understood by the remote instance.")
|
|
2895
2990
|
}, ["sessionId"]), async (args, signal) => {
|
|
2896
2991
|
const { instance, localId, globalId } = await resolveSession(args);
|
|
2897
|
-
const idempotencyKey = optionalString(args, "idempotencyKey") ?? randomUUID();
|
|
2992
|
+
const idempotencyKey = optionalString$1(args, "idempotencyKey") ?? randomUUID();
|
|
2898
2993
|
const response = await clientFor(instance).stopSession(localId, definedObject({
|
|
2899
|
-
mode: optionalString(args, "mode"),
|
|
2994
|
+
mode: optionalString$1(args, "mode"),
|
|
2900
2995
|
idempotencyKey
|
|
2901
2996
|
}), signal);
|
|
2902
|
-
return ok({
|
|
2997
|
+
return ok$1({
|
|
2903
2998
|
resolvedInstance: publicInstance(instance),
|
|
2904
2999
|
sessionId: globalId,
|
|
2905
3000
|
...response
|
|
2906
3001
|
});
|
|
2907
3002
|
}),
|
|
2908
|
-
tool$1("await_turn", "Long-poll session events across fleet instances. The server owns per-target opaque cursors, so callers do not pass cursor tokens. Distinct concurrent watchers over the same instance set should pass a distinct watcherId so they do not share a cursor.", objectSchema({
|
|
3003
|
+
tool$1("await_turn", "Long-poll session events across fleet instances. The server owns per-target opaque cursors, so callers do not pass cursor tokens. Distinct concurrent watchers over the same instance set should pass a distinct watcherId so they do not share a cursor.", objectSchema$1({
|
|
2909
3004
|
instances: arrayProp("Instance ids or labels to poll. Omit with sessionIds to target those session instances; omit both to poll every registered instance."),
|
|
2910
3005
|
sessionIds: arrayProp("Global session ids to filter to."),
|
|
2911
|
-
timeoutMs: numberProp("Long-poll timeout per instance in milliseconds."),
|
|
3006
|
+
timeoutMs: numberProp$1("Long-poll timeout per instance in milliseconds."),
|
|
2912
3007
|
kinds: arrayProp("Optional event kinds to filter to."),
|
|
2913
|
-
watcherId: stringProp("Optional stable id for this watcher. Use a distinct value for concurrent watchers over the same target set to keep cursors isolated.")
|
|
3008
|
+
watcherId: stringProp$1("Optional stable id for this watcher. Use a distinct value for concurrent watchers over the same target set to keep cursors isolated.")
|
|
2914
3009
|
}, []), async (args, signal) => {
|
|
2915
3010
|
const target = await resolveAwaitTarget(args, getRegistry());
|
|
2916
|
-
const cursorByInstance = takeAwaitTurnCursorMap(awaitTurnCursorKey(optionalString(args, "watcherId")));
|
|
2917
|
-
const timeoutMs = optionalNumber(args, "timeoutMs");
|
|
3011
|
+
const cursorByInstance = takeAwaitTurnCursorMap(awaitTurnCursorKey(optionalString$1(args, "watcherId")));
|
|
3012
|
+
const timeoutMs = optionalNumber$1(args, "timeoutMs");
|
|
2918
3013
|
const kinds = optionalStringArray(args, "kinds");
|
|
2919
3014
|
const results = await mapWithConcurrency(target.instances, fleetFanoutConcurrency(AWAIT_TURN_FANOUT_CONCURRENCY), async (instance) => {
|
|
2920
3015
|
const deadline = createAwaitTurnDeadline(timeoutMs, awaitTurnDeadlineSlackMs);
|
|
@@ -2957,7 +3052,7 @@ function createFleetTools(options = {}) {
|
|
|
2957
3052
|
instance: publicInstance(instance),
|
|
2958
3053
|
...gap
|
|
2959
3054
|
})));
|
|
2960
|
-
return ok({
|
|
3055
|
+
return ok$1({
|
|
2961
3056
|
resolvedInstances: target.instances.map(publicInstance),
|
|
2962
3057
|
events: events$1,
|
|
2963
3058
|
gaps,
|
|
@@ -2969,53 +3064,53 @@ function createFleetTools(options = {}) {
|
|
|
2969
3064
|
...errors.length > 0 ? { errors } : {}
|
|
2970
3065
|
});
|
|
2971
3066
|
}),
|
|
2972
|
-
tool$1("read_file", "Read a file from one fleet instance via its existing /api/files/content endpoint.", objectSchema({
|
|
2973
|
-
instance: stringProp("Instance id or label. Defaults to the registry default, or the sole instance."),
|
|
2974
|
-
path: stringProp("Remote file path to read.")
|
|
3067
|
+
tool$1("read_file", "Read a file from one fleet instance via its existing /api/files/content endpoint.", objectSchema$1({
|
|
3068
|
+
instance: stringProp$1("Instance id or label. Defaults to the registry default, or the sole instance."),
|
|
3069
|
+
path: stringProp$1("Remote file path to read.")
|
|
2975
3070
|
}, ["path"]), async (args, signal) => {
|
|
2976
|
-
const instance = await resolve(optionalString(args, "instance"));
|
|
2977
|
-
const response = await clientFor(instance).readFile(requiredString(args, "path"), signal);
|
|
2978
|
-
return ok({
|
|
3071
|
+
const instance = await resolve(optionalString$1(args, "instance"));
|
|
3072
|
+
const response = await clientFor(instance).readFile(requiredString$1(args, "path"), signal);
|
|
3073
|
+
return ok$1({
|
|
2979
3074
|
resolvedInstance: publicInstance(instance),
|
|
2980
3075
|
...response
|
|
2981
3076
|
});
|
|
2982
3077
|
}),
|
|
2983
|
-
tool$1("list_dir", "List a directory on one fleet instance via its existing /api/files endpoint.", objectSchema({
|
|
2984
|
-
instance: stringProp("Instance id or label. Defaults to the registry default, or the sole instance."),
|
|
2985
|
-
path: stringProp("Remote directory path to list.")
|
|
3078
|
+
tool$1("list_dir", "List a directory on one fleet instance via its existing /api/files endpoint.", objectSchema$1({
|
|
3079
|
+
instance: stringProp$1("Instance id or label. Defaults to the registry default, or the sole instance."),
|
|
3080
|
+
path: stringProp$1("Remote directory path to list.")
|
|
2986
3081
|
}, ["path"]), async (args, signal) => {
|
|
2987
|
-
const instance = await resolve(optionalString(args, "instance"));
|
|
2988
|
-
const response = await clientFor(instance).listDir(requiredString(args, "path"), signal);
|
|
2989
|
-
return ok({
|
|
3082
|
+
const instance = await resolve(optionalString$1(args, "instance"));
|
|
3083
|
+
const response = await clientFor(instance).listDir(requiredString$1(args, "path"), signal);
|
|
3084
|
+
return ok$1({
|
|
2990
3085
|
resolvedInstance: publicInstance(instance),
|
|
2991
3086
|
...response
|
|
2992
3087
|
});
|
|
2993
3088
|
}),
|
|
2994
|
-
tool$1("search", "Search files on one fleet instance via its existing /api/search endpoint.", objectSchema({
|
|
2995
|
-
instance: stringProp("Instance id or label. Defaults to the registry default, or the sole instance."),
|
|
2996
|
-
query: stringProp("Search query."),
|
|
2997
|
-
path: stringProp("Optional path scope.")
|
|
3089
|
+
tool$1("search", "Search files on one fleet instance via its existing /api/search endpoint.", objectSchema$1({
|
|
3090
|
+
instance: stringProp$1("Instance id or label. Defaults to the registry default, or the sole instance."),
|
|
3091
|
+
query: stringProp$1("Search query."),
|
|
3092
|
+
path: stringProp$1("Optional path scope.")
|
|
2998
3093
|
}, ["query"]), async (args, signal) => {
|
|
2999
|
-
const instance = await resolve(optionalString(args, "instance"));
|
|
3000
|
-
const response = await clientFor(instance).search(requiredString(args, "query"), optionalString(args, "path"), signal);
|
|
3001
|
-
return ok({
|
|
3094
|
+
const instance = await resolve(optionalString$1(args, "instance"));
|
|
3095
|
+
const response = await clientFor(instance).search(requiredString$1(args, "query"), optionalString$1(args, "path"), signal);
|
|
3096
|
+
return ok$1({
|
|
3002
3097
|
resolvedInstance: publicInstance(instance),
|
|
3003
3098
|
...response
|
|
3004
3099
|
});
|
|
3005
3100
|
}),
|
|
3006
|
-
tool$1("git_show", "Read a file/revision through one fleet instance's existing /api/files/git-show endpoint.", objectSchema({
|
|
3007
|
-
instance: stringProp("Instance id or label. Defaults to the registry default, or the sole instance."),
|
|
3008
|
-
path: stringProp("Remote repository path or file path for git-show."),
|
|
3009
|
-
ref: stringProp("Optional git ref/revision."),
|
|
3010
|
-
rev: stringProp("Optional git revision alias."),
|
|
3011
|
-
commit: stringProp("Optional commit id.")
|
|
3101
|
+
tool$1("git_show", "Read a file/revision through one fleet instance's existing /api/files/git-show endpoint.", objectSchema$1({
|
|
3102
|
+
instance: stringProp$1("Instance id or label. Defaults to the registry default, or the sole instance."),
|
|
3103
|
+
path: stringProp$1("Remote repository path or file path for git-show."),
|
|
3104
|
+
ref: stringProp$1("Optional git ref/revision."),
|
|
3105
|
+
rev: stringProp$1("Optional git revision alias."),
|
|
3106
|
+
commit: stringProp$1("Optional commit id.")
|
|
3012
3107
|
}, ["path"]), async (args, signal) => {
|
|
3013
|
-
const instance = await resolve(optionalString(args, "instance"));
|
|
3108
|
+
const instance = await resolve(optionalString$1(args, "instance"));
|
|
3014
3109
|
const response = await clientFor(instance).gitShow({
|
|
3015
3110
|
...args,
|
|
3016
3111
|
instance: void 0
|
|
3017
3112
|
}, signal);
|
|
3018
|
-
return ok({
|
|
3113
|
+
return ok$1({
|
|
3019
3114
|
resolvedInstance: publicInstance(instance),
|
|
3020
3115
|
...response
|
|
3021
3116
|
});
|
|
@@ -3130,166 +3225,3293 @@ function isFleetErrorCode(code) {
|
|
|
3130
3225
|
default: return false;
|
|
3131
3226
|
}
|
|
3132
3227
|
}
|
|
3133
|
-
async function resolveAwaitTarget(args, registry) {
|
|
3134
|
-
const instanceArgs = optionalStringArray(args, "instances");
|
|
3135
|
-
const sessionIdArgs = optionalStringArray(args, "sessionIds");
|
|
3136
|
-
const localSessionIdsByInstance = /* @__PURE__ */ new Map();
|
|
3137
|
-
for (const sessionId of sessionIdArgs ?? []) {
|
|
3138
|
-
const decoded = decodeSessionId(sessionId);
|
|
3139
|
-
const existing = localSessionIdsByInstance.get(decoded.instanceId) ?? [];
|
|
3140
|
-
existing.push(decoded.localId);
|
|
3141
|
-
localSessionIdsByInstance.set(decoded.instanceId, existing);
|
|
3228
|
+
async function resolveAwaitTarget(args, registry) {
|
|
3229
|
+
const instanceArgs = optionalStringArray(args, "instances");
|
|
3230
|
+
const sessionIdArgs = optionalStringArray(args, "sessionIds");
|
|
3231
|
+
const localSessionIdsByInstance = /* @__PURE__ */ new Map();
|
|
3232
|
+
for (const sessionId of sessionIdArgs ?? []) {
|
|
3233
|
+
const decoded = decodeSessionId(sessionId);
|
|
3234
|
+
const existing = localSessionIdsByInstance.get(decoded.instanceId) ?? [];
|
|
3235
|
+
existing.push(decoded.localId);
|
|
3236
|
+
localSessionIdsByInstance.set(decoded.instanceId, existing);
|
|
3237
|
+
}
|
|
3238
|
+
let instances;
|
|
3239
|
+
if (instanceArgs !== void 0 && instanceArgs.length > 0) {
|
|
3240
|
+
instances = uniqueInstances(await Promise.all(instanceArgs.map((arg) => registry.resolveInstance(arg))));
|
|
3241
|
+
const ids = new Set(instances.map((instance) => instance.id));
|
|
3242
|
+
for (const instanceId of localSessionIdsByInstance.keys()) if (!ids.has(instanceId)) throw new FleetToolInputError("INSTANCE_MISMATCH", `sessionIds include instance ${JSON.stringify(instanceId)} which is not in arguments.instances`);
|
|
3243
|
+
} else if (localSessionIdsByInstance.size > 0) instances = uniqueInstances(await Promise.all([...localSessionIdsByInstance.keys()].map((instanceId) => registry.resolveInstance(instanceId))));
|
|
3244
|
+
else {
|
|
3245
|
+
const infos = await registry.listInstances();
|
|
3246
|
+
if (infos.length === 0) throw new FleetRegistryError("INSTANCE_REQUIRED", "await_turn requires at least one registered fleet instance");
|
|
3247
|
+
instances = uniqueInstances(await Promise.all(infos.map((info) => registry.resolveInstance(info.id))));
|
|
3248
|
+
}
|
|
3249
|
+
return {
|
|
3250
|
+
instances,
|
|
3251
|
+
localSessionIdsByInstance
|
|
3252
|
+
};
|
|
3253
|
+
}
|
|
3254
|
+
function globalizeSession(instanceId, session) {
|
|
3255
|
+
return {
|
|
3256
|
+
...session,
|
|
3257
|
+
sessionId: encodeSessionId(instanceId, session.sessionId)
|
|
3258
|
+
};
|
|
3259
|
+
}
|
|
3260
|
+
function stampEvent(instance, event) {
|
|
3261
|
+
return {
|
|
3262
|
+
...event,
|
|
3263
|
+
instance: publicInstance(instance),
|
|
3264
|
+
...typeof event.sessionId === "string" ? { sessionId: encodeSessionId(instance.id, event.sessionId) } : {}
|
|
3265
|
+
};
|
|
3266
|
+
}
|
|
3267
|
+
function eventAtMs(value) {
|
|
3268
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
3269
|
+
if (typeof value === "string") {
|
|
3270
|
+
const parsed = Date.parse(value);
|
|
3271
|
+
if (!Number.isNaN(parsed)) return parsed;
|
|
3272
|
+
}
|
|
3273
|
+
return 0;
|
|
3274
|
+
}
|
|
3275
|
+
function compareStampedEvents(a, b) {
|
|
3276
|
+
const atA = eventAtMs(a.at);
|
|
3277
|
+
const atB = eventAtMs(b.at);
|
|
3278
|
+
if (atA !== atB) return atA - atB;
|
|
3279
|
+
return (typeof a.seq === "number" ? a.seq : 0) - (typeof b.seq === "number" ? b.seq : 0);
|
|
3280
|
+
}
|
|
3281
|
+
const MAX_WATCHER_ID_LEN = 200;
|
|
3282
|
+
const MAX_AWAIT_TURN_CURSOR_KEYS = 1024;
|
|
3283
|
+
function awaitTurnCursorKey(watcherId) {
|
|
3284
|
+
const id = watcherId ?? "default";
|
|
3285
|
+
return id.length > MAX_WATCHER_ID_LEN ? id.slice(0, MAX_WATCHER_ID_LEN) : id;
|
|
3286
|
+
}
|
|
3287
|
+
function takeAwaitTurnCursorMap(clientKey) {
|
|
3288
|
+
const existing = awaitTurnCursors.get(clientKey);
|
|
3289
|
+
if (existing) {
|
|
3290
|
+
awaitTurnCursors.delete(clientKey);
|
|
3291
|
+
awaitTurnCursors.set(clientKey, existing);
|
|
3292
|
+
return existing;
|
|
3293
|
+
}
|
|
3294
|
+
const created = /* @__PURE__ */ new Map();
|
|
3295
|
+
awaitTurnCursors.set(clientKey, created);
|
|
3296
|
+
while (awaitTurnCursors.size > MAX_AWAIT_TURN_CURSOR_KEYS) {
|
|
3297
|
+
const oldest = awaitTurnCursors.keys().next().value;
|
|
3298
|
+
if (oldest === void 0) break;
|
|
3299
|
+
awaitTurnCursors.delete(oldest);
|
|
3300
|
+
}
|
|
3301
|
+
return created;
|
|
3302
|
+
}
|
|
3303
|
+
function isAwaitTurnSuccess(result) {
|
|
3304
|
+
return result.ok;
|
|
3305
|
+
}
|
|
3306
|
+
function isAwaitTurnFailure(result) {
|
|
3307
|
+
return !result.ok;
|
|
3308
|
+
}
|
|
3309
|
+
function failedProbeResult(info, code) {
|
|
3310
|
+
const hint = fleetProbeHint(code);
|
|
3311
|
+
return {
|
|
3312
|
+
id: info.id,
|
|
3313
|
+
label: info.label,
|
|
3314
|
+
reachable: false,
|
|
3315
|
+
error: code,
|
|
3316
|
+
...hint ? { hint } : {}
|
|
3317
|
+
};
|
|
3318
|
+
}
|
|
3319
|
+
async function mapWithConcurrency(items, limit, fn) {
|
|
3320
|
+
const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 1;
|
|
3321
|
+
const concurrency = Math.max(1, Math.min(items.length || 1, safeLimit));
|
|
3322
|
+
const results = new Array(items.length);
|
|
3323
|
+
let nextIndex = 0;
|
|
3324
|
+
async function worker() {
|
|
3325
|
+
while (nextIndex < items.length) {
|
|
3326
|
+
const index = nextIndex++;
|
|
3327
|
+
results[index] = await fn(items[index], index);
|
|
3328
|
+
}
|
|
3329
|
+
}
|
|
3330
|
+
await Promise.all(Array.from({ length: concurrency }, () => worker()));
|
|
3331
|
+
return results;
|
|
3332
|
+
}
|
|
3333
|
+
function fleetFanoutConcurrency(defaultLimit) {
|
|
3334
|
+
const raw = process.env[FLEET_FANOUT_CONCURRENCY_ENV];
|
|
3335
|
+
const parsed = raw === void 0 ? NaN : Number.parseInt(raw, 10);
|
|
3336
|
+
if (Number.isFinite(parsed) && parsed > 0) return Math.floor(parsed);
|
|
3337
|
+
return defaultLimit;
|
|
3338
|
+
}
|
|
3339
|
+
function probeRateLimitBackoffMs(attempt) {
|
|
3340
|
+
return Math.min(INSTANCE_PROBE_RATE_LIMIT_BACKOFF_BASE_MS * 2 ** attempt, INSTANCE_PROBE_RATE_LIMIT_BACKOFF_MAX_MS);
|
|
3341
|
+
}
|
|
3342
|
+
function nonNegativeNumberOrDefault(value, fallback) {
|
|
3343
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : fallback;
|
|
3344
|
+
}
|
|
3345
|
+
async function delay(ms) {
|
|
3346
|
+
if (ms <= 0) return;
|
|
3347
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
3348
|
+
}
|
|
3349
|
+
function isAbortLike(err) {
|
|
3350
|
+
if (!(err instanceof Error)) return false;
|
|
3351
|
+
return err.name === "AbortError" || err.name === "TimeoutError";
|
|
3352
|
+
}
|
|
3353
|
+
function uniqueInstances(instances) {
|
|
3354
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3355
|
+
const result = [];
|
|
3356
|
+
for (const instance of instances) {
|
|
3357
|
+
if (seen.has(instance.id)) continue;
|
|
3358
|
+
seen.add(instance.id);
|
|
3359
|
+
result.push(instance);
|
|
3360
|
+
}
|
|
3361
|
+
return result;
|
|
3362
|
+
}
|
|
3363
|
+
function publicInstance(instance) {
|
|
3364
|
+
return {
|
|
3365
|
+
id: instance.id,
|
|
3366
|
+
label: instance.label
|
|
3367
|
+
};
|
|
3368
|
+
}
|
|
3369
|
+
/**
|
|
3370
|
+
* Build the FleetClient tunnel-auth options for a resolved instance.
|
|
3371
|
+
* Resolution order: a `tunnelId` enables auto-mint + auto-refresh (and the
|
|
3372
|
+
* evict-on-failure hook); else a static `tunnelToken` is sent directly (no
|
|
3373
|
+
* retry, since it cannot be re-minted); else no tunnel auth.
|
|
3374
|
+
*/
|
|
3375
|
+
function tunnelClientOptions(instance, provider) {
|
|
3376
|
+
if (instance.tunnelId) {
|
|
3377
|
+
const cfg = { tunnelId: instance.tunnelId };
|
|
3378
|
+
return {
|
|
3379
|
+
getTunnelToken: () => provider.getToken(cfg),
|
|
3380
|
+
onTunnelAuthInvalidate: () => provider.invalidate(cfg)
|
|
3381
|
+
};
|
|
3382
|
+
}
|
|
3383
|
+
if (instance.tunnelToken) {
|
|
3384
|
+
const token = instance.tunnelToken;
|
|
3385
|
+
return { getTunnelToken: async () => token };
|
|
3386
|
+
}
|
|
3387
|
+
return {};
|
|
3388
|
+
}
|
|
3389
|
+
function ok$1(value) {
|
|
3390
|
+
return jsonResult$1(value, false);
|
|
3391
|
+
}
|
|
3392
|
+
function jsonResult$1(value, isError) {
|
|
3393
|
+
return {
|
|
3394
|
+
content: [{
|
|
3395
|
+
type: "text",
|
|
3396
|
+
text: JSON.stringify(value)
|
|
3397
|
+
}],
|
|
3398
|
+
...isError ? { isError: true } : {}
|
|
3399
|
+
};
|
|
3400
|
+
}
|
|
3401
|
+
function errorResult$1(err) {
|
|
3402
|
+
return jsonResult$1({ error: {
|
|
3403
|
+
code: errorCode$1(err),
|
|
3404
|
+
message: err instanceof Error ? err.message : String(err)
|
|
3405
|
+
} }, true);
|
|
3406
|
+
}
|
|
3407
|
+
function errorCode$1(err) {
|
|
3408
|
+
if (typeof err === "object" && err !== null && "code" in err) {
|
|
3409
|
+
const code = err.code;
|
|
3410
|
+
if (typeof code === "string") return code;
|
|
3411
|
+
}
|
|
3412
|
+
return "FLEET_ERROR";
|
|
3413
|
+
}
|
|
3414
|
+
function definedObject(input) {
|
|
3415
|
+
const result = {};
|
|
3416
|
+
for (const [key, value] of Object.entries(input)) if (value !== void 0) result[key] = value;
|
|
3417
|
+
return result;
|
|
3418
|
+
}
|
|
3419
|
+
function requiredString$1(args, key) {
|
|
3420
|
+
const value = args[key];
|
|
3421
|
+
if (typeof value !== "string" || value.trim() === "") throw new FleetToolInputError("INVALID_ARGUMENT", `arguments.${key} is required and must be a non-empty string`);
|
|
3422
|
+
return value;
|
|
3423
|
+
}
|
|
3424
|
+
function optionalString$1(args, key) {
|
|
3425
|
+
const value = args[key];
|
|
3426
|
+
if (value === void 0) return void 0;
|
|
3427
|
+
if (typeof value !== "string") throw new FleetToolInputError("INVALID_ARGUMENT", `arguments.${key} must be a string`);
|
|
3428
|
+
return value.trim() === "" ? void 0 : value;
|
|
3429
|
+
}
|
|
3430
|
+
function optionalNumber$1(args, key) {
|
|
3431
|
+
const value = args[key];
|
|
3432
|
+
if (value === void 0) return void 0;
|
|
3433
|
+
if (typeof value !== "number" || !Number.isFinite(value)) throw new FleetToolInputError("INVALID_ARGUMENT", `arguments.${key} must be a finite number`);
|
|
3434
|
+
return value;
|
|
3435
|
+
}
|
|
3436
|
+
function optionalBoolean(args, key) {
|
|
3437
|
+
const value = args[key];
|
|
3438
|
+
if (value === void 0) return void 0;
|
|
3439
|
+
if (typeof value !== "boolean") throw new FleetToolInputError("INVALID_ARGUMENT", `arguments.${key} must be a boolean`);
|
|
3440
|
+
return value;
|
|
3441
|
+
}
|
|
3442
|
+
function optionalStringArray(args, key) {
|
|
3443
|
+
const value = args[key];
|
|
3444
|
+
if (value === void 0) return void 0;
|
|
3445
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.trim() === "")) throw new FleetToolInputError("INVALID_ARGUMENT", `arguments.${key} must be an array of non-empty strings`);
|
|
3446
|
+
return value;
|
|
3447
|
+
}
|
|
3448
|
+
function objectSchema$1(properties, required) {
|
|
3449
|
+
return {
|
|
3450
|
+
type: "object",
|
|
3451
|
+
required,
|
|
3452
|
+
additionalProperties: false,
|
|
3453
|
+
properties
|
|
3454
|
+
};
|
|
3455
|
+
}
|
|
3456
|
+
function stringProp$1(description) {
|
|
3457
|
+
return {
|
|
3458
|
+
type: "string",
|
|
3459
|
+
description
|
|
3460
|
+
};
|
|
3461
|
+
}
|
|
3462
|
+
function numberProp$1(description) {
|
|
3463
|
+
return {
|
|
3464
|
+
type: "number",
|
|
3465
|
+
description
|
|
3466
|
+
};
|
|
3467
|
+
}
|
|
3468
|
+
function booleanProp(description) {
|
|
3469
|
+
return {
|
|
3470
|
+
type: "boolean",
|
|
3471
|
+
description
|
|
3472
|
+
};
|
|
3473
|
+
}
|
|
3474
|
+
function arrayProp(description) {
|
|
3475
|
+
return {
|
|
3476
|
+
type: "array",
|
|
3477
|
+
items: { type: "string" },
|
|
3478
|
+
description
|
|
3479
|
+
};
|
|
3480
|
+
}
|
|
3481
|
+
|
|
3482
|
+
//#endregion
|
|
3483
|
+
//#region src/lib/agent/types.ts
|
|
3484
|
+
var AgentError = class extends Error {
|
|
3485
|
+
code;
|
|
3486
|
+
cause;
|
|
3487
|
+
constructor(code, message, options) {
|
|
3488
|
+
super(message);
|
|
3489
|
+
this.name = "AgentError";
|
|
3490
|
+
this.code = code;
|
|
3491
|
+
this.cause = options?.cause;
|
|
3492
|
+
}
|
|
3493
|
+
};
|
|
3494
|
+
|
|
3495
|
+
//#endregion
|
|
3496
|
+
//#region src/lib/agent/rest.ts
|
|
3497
|
+
function restHeaders(apiVersion, extra) {
|
|
3498
|
+
const headers = githubAgentHeaders(state);
|
|
3499
|
+
if (apiVersion) headers["x-github-api-version"] = apiVersion;
|
|
3500
|
+
if (extra) Object.assign(headers, extra);
|
|
3501
|
+
return headers;
|
|
3502
|
+
}
|
|
3503
|
+
async function ghRestRaw(method, path$1, opts = {}) {
|
|
3504
|
+
const url = `${GITHUB_API_BASE_URL}${path$1}`;
|
|
3505
|
+
const requestInit = {
|
|
3506
|
+
method,
|
|
3507
|
+
headers: restHeaders(opts.apiVersion, opts.headers),
|
|
3508
|
+
signal: opts.signal
|
|
3509
|
+
};
|
|
3510
|
+
if (opts.body !== void 0) requestInit.body = JSON.stringify(opts.body);
|
|
3511
|
+
try {
|
|
3512
|
+
if (opts.retry === false) return await fetch(url, requestInit);
|
|
3513
|
+
return await fetchWithTransientRetry(() => fetch(url, requestInit), {
|
|
3514
|
+
label: `github-rest ${method} ${path$1}`,
|
|
3515
|
+
signal: opts.signal
|
|
3516
|
+
});
|
|
3517
|
+
} catch (err) {
|
|
3518
|
+
consola.warn(`GitHub REST ${method} ${path$1} failed before response`, err);
|
|
3519
|
+
throw new AgentError("UPSTREAM", `GitHub REST ${method} ${path$1} failed`, { cause: err });
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
3522
|
+
function rateLimitMessage(response) {
|
|
3523
|
+
const reset = response.headers.get("x-ratelimit-reset");
|
|
3524
|
+
const retryAfter = response.headers.get("retry-after");
|
|
3525
|
+
return `GitHub API rate limit exceeded${reset ? `; reset at ${reset}` : retryAfter ? `; retry after ${retryAfter}s` : ""}`;
|
|
3526
|
+
}
|
|
3527
|
+
function agentErrorFromResponse(response, message = `GitHub API request failed with HTTP ${response.status}`) {
|
|
3528
|
+
let code;
|
|
3529
|
+
let errorMessage = message;
|
|
3530
|
+
if (response.status === 401) {
|
|
3531
|
+
code = "AUTH_REVOKED";
|
|
3532
|
+
errorMessage = "GitHub agent token was revoked or is invalid";
|
|
3533
|
+
} else if (response.status === 403 && response.headers.get("x-ratelimit-remaining") === "0") {
|
|
3534
|
+
code = "RATE_LIMITED";
|
|
3535
|
+
errorMessage = rateLimitMessage(response);
|
|
3536
|
+
} else if (response.status === 403) {
|
|
3537
|
+
code = "NO_WRITE_ACCESS";
|
|
3538
|
+
errorMessage = "GitHub agent token does not have write access";
|
|
3539
|
+
} else if (response.status === 404) {
|
|
3540
|
+
code = "NOT_FOUND";
|
|
3541
|
+
errorMessage = "GitHub resource was not found";
|
|
3542
|
+
} else if (response.status === 429) {
|
|
3543
|
+
code = "RATE_LIMITED";
|
|
3544
|
+
errorMessage = rateLimitMessage(response);
|
|
3545
|
+
} else code = "UPSTREAM";
|
|
3546
|
+
return new AgentError(code, errorMessage, { cause: new HTTPError(errorMessage, response) });
|
|
3547
|
+
}
|
|
3548
|
+
async function parseJsonOrEmpty(response) {
|
|
3549
|
+
if (response.status === 204) return {};
|
|
3550
|
+
const text = await response.text();
|
|
3551
|
+
if (!text.trim()) return {};
|
|
3552
|
+
try {
|
|
3553
|
+
return JSON.parse(text);
|
|
3554
|
+
} catch (err) {
|
|
3555
|
+
throw new AgentError("UPSTREAM", "GitHub API returned invalid JSON", { cause: err });
|
|
3556
|
+
}
|
|
3557
|
+
}
|
|
3558
|
+
async function ghRest(method, path$1, opts = {}) {
|
|
3559
|
+
const response = await ghRestRaw(method, path$1, opts);
|
|
3560
|
+
if (!response.ok) throw agentErrorFromResponse(response);
|
|
3561
|
+
return parseJsonOrEmpty(response);
|
|
3562
|
+
}
|
|
3563
|
+
|
|
3564
|
+
//#endregion
|
|
3565
|
+
//#region src/lib/agent/graphql.ts
|
|
3566
|
+
function graphQLErrorMessage(error) {
|
|
3567
|
+
return String(error.message ?? error.type ?? error.extensions?.code ?? "GraphQL error");
|
|
3568
|
+
}
|
|
3569
|
+
function isFeatureError(error) {
|
|
3570
|
+
const message = String(error.message ?? "").toLowerCase();
|
|
3571
|
+
const type = String(error.type ?? "").toLowerCase();
|
|
3572
|
+
const code = String(error.extensions?.code ?? "").toLowerCase();
|
|
3573
|
+
const haystack = `${message} ${type} ${code}`;
|
|
3574
|
+
if (haystack.includes("unknown feature") || haystack.includes("disabled feature") || haystack.includes("feature flag") || haystack.includes("graphql-features") || haystack.includes("issues_copilot_assignment_api_support") || haystack.includes("does not exist on type") || haystack.includes("cannot query field") || haystack.includes("undefined field")) return true;
|
|
3575
|
+
return (type.includes("forbidden") || code.includes("forbidden")) && (message.includes("feature") || message.includes("preview") || message.includes("copilot assignment"));
|
|
3576
|
+
}
|
|
3577
|
+
async function parseGraphQLJson(response) {
|
|
3578
|
+
const text = await response.text();
|
|
3579
|
+
if (!text.trim()) return {};
|
|
3580
|
+
try {
|
|
3581
|
+
return JSON.parse(text);
|
|
3582
|
+
} catch (err) {
|
|
3583
|
+
throw new AgentError("UPSTREAM", "GitHub GraphQL returned invalid JSON", { cause: err });
|
|
3584
|
+
}
|
|
3585
|
+
}
|
|
3586
|
+
async function ghGraphQL(query, variables, opts = {}) {
|
|
3587
|
+
let response;
|
|
3588
|
+
try {
|
|
3589
|
+
response = await fetchWithTransientRetry(() => fetch(GITHUB_GRAPHQL_URL, {
|
|
3590
|
+
method: "POST",
|
|
3591
|
+
headers: githubAgentGraphQLHeaders(state, opts.features),
|
|
3592
|
+
body: JSON.stringify({
|
|
3593
|
+
query,
|
|
3594
|
+
variables
|
|
3595
|
+
}),
|
|
3596
|
+
signal: opts.signal
|
|
3597
|
+
}), {
|
|
3598
|
+
label: "github-graphql",
|
|
3599
|
+
signal: opts.signal
|
|
3600
|
+
});
|
|
3601
|
+
} catch (err) {
|
|
3602
|
+
consola.warn("GitHub GraphQL failed before response", err);
|
|
3603
|
+
throw new AgentError("UPSTREAM", "GitHub GraphQL request failed", { cause: err });
|
|
3604
|
+
}
|
|
3605
|
+
if (!response.ok) throw agentErrorFromResponse(response, "GitHub GraphQL request failed");
|
|
3606
|
+
const payload = await parseGraphQLJson(response);
|
|
3607
|
+
const errors = payload.errors?.filter(Boolean) ?? [];
|
|
3608
|
+
if (errors.length > 0) {
|
|
3609
|
+
const message = errors.map(graphQLErrorMessage).join("; ");
|
|
3610
|
+
if (errors.some(isFeatureError)) throw new AgentError("GRAPHQL_FEATURE", message);
|
|
3611
|
+
throw new AgentError("UPSTREAM", message);
|
|
3612
|
+
}
|
|
3613
|
+
if (payload.data === void 0) throw new AgentError("UPSTREAM", "GitHub GraphQL response did not include data");
|
|
3614
|
+
return payload.data;
|
|
3615
|
+
}
|
|
3616
|
+
|
|
3617
|
+
//#endregion
|
|
3618
|
+
//#region src/lib/agent/service.ts
|
|
3619
|
+
const CACHE_TTL_MS = 300 * 1e3;
|
|
3620
|
+
const CHECK_SUMMARY_LIMIT = 20;
|
|
3621
|
+
const FAILING_CHECK_LIMIT = 5;
|
|
3622
|
+
const AGENT_LOGIN_MATCHERS = {
|
|
3623
|
+
copilot: /^copilot(-swe-agent)?$/i,
|
|
3624
|
+
anthropic: /^anthropic-code-agent$/i,
|
|
3625
|
+
openai: /^openai-code-agent$/i
|
|
3626
|
+
};
|
|
3627
|
+
const rosterCache = /* @__PURE__ */ new Map();
|
|
3628
|
+
function repoCacheKey(repo) {
|
|
3629
|
+
return `${repo.owner.toLowerCase()}/${repo.repo.toLowerCase()}`;
|
|
3630
|
+
}
|
|
3631
|
+
function cached$1(entry) {
|
|
3632
|
+
if (!entry) return void 0;
|
|
3633
|
+
if (Date.now() - entry.timestamp > CACHE_TTL_MS) return void 0;
|
|
3634
|
+
return entry.value;
|
|
3635
|
+
}
|
|
3636
|
+
function segment$1(value) {
|
|
3637
|
+
return encodeURIComponent(String(value));
|
|
3638
|
+
}
|
|
3639
|
+
function repoPath(repo) {
|
|
3640
|
+
return `/repos/${segment$1(repo.owner)}/${segment$1(repo.repo)}`;
|
|
3641
|
+
}
|
|
3642
|
+
function botAssigneeLogin(login) {
|
|
3643
|
+
return login.toLowerCase().endsWith("[bot]") ? login : `${login}[bot]`;
|
|
3644
|
+
}
|
|
3645
|
+
/** Classify a GitHub login to an agent key via the roster matchers, or null. */
|
|
3646
|
+
function agentKeyForLogin(login) {
|
|
3647
|
+
const normalized = login.replace(/\[bot\]$/i, "");
|
|
3648
|
+
for (const [key, matcher] of Object.entries(AGENT_LOGIN_MATCHERS)) if (matcher.test(normalized)) return key;
|
|
3649
|
+
return null;
|
|
3650
|
+
}
|
|
3651
|
+
function authorMatchesBot(authorLogin, botLogin) {
|
|
3652
|
+
if (!authorLogin) return false;
|
|
3653
|
+
const author = authorLogin.toLowerCase();
|
|
3654
|
+
if (author === botLogin.toLowerCase() || author === botAssigneeLogin(botLogin).toLowerCase()) return true;
|
|
3655
|
+
const authorKey = agentKeyForLogin(authorLogin);
|
|
3656
|
+
return authorKey !== null && authorKey === agentKeyForLogin(botLogin);
|
|
3657
|
+
}
|
|
3658
|
+
function asRecord$7(value) {
|
|
3659
|
+
return typeof value === "object" && value !== null ? value : void 0;
|
|
3660
|
+
}
|
|
3661
|
+
function stringValue$2(value) {
|
|
3662
|
+
return typeof value === "string" ? value : void 0;
|
|
3663
|
+
}
|
|
3664
|
+
async function readJsonObject(response) {
|
|
3665
|
+
const text = await response.text();
|
|
3666
|
+
if (!text.trim()) return {};
|
|
3667
|
+
try {
|
|
3668
|
+
return asRecord$7(JSON.parse(text)) ?? {};
|
|
3669
|
+
} catch (err) {
|
|
3670
|
+
throw new AgentError("UPSTREAM", "GitHub API returned invalid JSON", { cause: err });
|
|
3671
|
+
}
|
|
3672
|
+
}
|
|
3673
|
+
async function resolveAgentRoster(repo) {
|
|
3674
|
+
const key = repoCacheKey(repo);
|
|
3675
|
+
const cacheEntry = rosterCache.get(key);
|
|
3676
|
+
const roster = cached$1(cacheEntry ? {
|
|
3677
|
+
timestamp: cacheEntry.timestamp,
|
|
3678
|
+
value: cacheEntry.roster
|
|
3679
|
+
} : void 0);
|
|
3680
|
+
if (roster) return roster;
|
|
3681
|
+
const data = await ghGraphQL(`query FirstMateSuggestedActors($owner: String!, $name: String!) {
|
|
3682
|
+
repository(owner: $owner, name: $name) {
|
|
3683
|
+
suggestedActors(capabilities: [CAN_BE_ASSIGNED], first: 100) {
|
|
3684
|
+
nodes {
|
|
3685
|
+
login
|
|
3686
|
+
__typename
|
|
3687
|
+
... on Bot {
|
|
3688
|
+
id
|
|
3689
|
+
}
|
|
3690
|
+
}
|
|
3691
|
+
}
|
|
3692
|
+
}
|
|
3693
|
+
}`, {
|
|
3694
|
+
owner: repo.owner,
|
|
3695
|
+
name: repo.repo
|
|
3696
|
+
});
|
|
3697
|
+
const nextRoster = /* @__PURE__ */ new Map();
|
|
3698
|
+
for (const node of data.repository?.suggestedActors?.nodes ?? []) {
|
|
3699
|
+
if (!node.login || !node.id || node.__typename !== "Bot") continue;
|
|
3700
|
+
for (const [agentKey, matcher] of Object.entries(AGENT_LOGIN_MATCHERS)) {
|
|
3701
|
+
if (!matcher.test(node.login)) continue;
|
|
3702
|
+
if (!nextRoster.has(agentKey)) nextRoster.set(agentKey, {
|
|
3703
|
+
login: node.login,
|
|
3704
|
+
botId: node.id
|
|
3705
|
+
});
|
|
3706
|
+
}
|
|
3707
|
+
}
|
|
3708
|
+
rosterCache.set(key, {
|
|
3709
|
+
timestamp: Date.now(),
|
|
3710
|
+
roster: nextRoster
|
|
3711
|
+
});
|
|
3712
|
+
return nextRoster;
|
|
3713
|
+
}
|
|
3714
|
+
async function resolveAgentActor(repo, key) {
|
|
3715
|
+
const roster = await resolveAgentRoster(repo);
|
|
3716
|
+
const actor = roster.get(key);
|
|
3717
|
+
if (actor) return actor;
|
|
3718
|
+
const available = [...roster.keys()].join(", ") || "none";
|
|
3719
|
+
throw new AgentError("AGENT_NOT_AVAILABLE", `Agent ${key} is not available for ${repo.owner}/${repo.repo}; available: ${available}`);
|
|
3720
|
+
}
|
|
3721
|
+
async function createIssue(repo, input) {
|
|
3722
|
+
const issue = await ghRest("POST", `${repoPath(repo)}/issues`, { body: {
|
|
3723
|
+
title: input.title,
|
|
3724
|
+
body: input.body
|
|
3725
|
+
} });
|
|
3726
|
+
return {
|
|
3727
|
+
number: issue.number ?? 0,
|
|
3728
|
+
nodeId: issue.node_id ?? "",
|
|
3729
|
+
url: issue.html_url ?? issue.url ?? ""
|
|
3730
|
+
};
|
|
3731
|
+
}
|
|
3732
|
+
async function assignAgent(repo, input) {
|
|
3733
|
+
try {
|
|
3734
|
+
await ghGraphQL(`mutation FirstMateAssignAgent($issueNodeId: ID!, $botId: ID!) {
|
|
3735
|
+
replaceActorsForAssignable(input: {
|
|
3736
|
+
assignableId: $issueNodeId,
|
|
3737
|
+
actorIds: [$botId]
|
|
3738
|
+
}) {
|
|
3739
|
+
assignable {
|
|
3740
|
+
... on Issue {
|
|
3741
|
+
id
|
|
3742
|
+
}
|
|
3743
|
+
}
|
|
3744
|
+
}
|
|
3745
|
+
}`, {
|
|
3746
|
+
issueNodeId: input.issueNodeId,
|
|
3747
|
+
botId: input.botId
|
|
3748
|
+
}, { features: "issues_copilot_assignment_api_support" });
|
|
3749
|
+
return {
|
|
3750
|
+
assigned: true,
|
|
3751
|
+
via: "graphql"
|
|
3752
|
+
};
|
|
3753
|
+
} catch (graphqlErr) {
|
|
3754
|
+
const reason = graphqlErr instanceof AgentError ? graphqlErr.code : "unknown";
|
|
3755
|
+
consola.debug(`GraphQL assignment failed (${reason}); trying REST fallback`);
|
|
3756
|
+
try {
|
|
3757
|
+
await ghRest("POST", `${repoPath(repo)}/issues/${segment$1(input.issueNumber)}/assignees`, { body: { assignees: [botAssigneeLogin(input.botLogin)] } });
|
|
3758
|
+
return {
|
|
3759
|
+
assigned: true,
|
|
3760
|
+
via: "rest"
|
|
3761
|
+
};
|
|
3762
|
+
} catch (restErr) {
|
|
3763
|
+
throw new AgentError("ASSIGN_FAILED", "Failed to assign agent to issue", { cause: {
|
|
3764
|
+
graphqlErr,
|
|
3765
|
+
restErr
|
|
3766
|
+
} });
|
|
3767
|
+
}
|
|
3768
|
+
}
|
|
3769
|
+
}
|
|
3770
|
+
async function findAgentPRs(repo, input) {
|
|
3771
|
+
input.issueNumber;
|
|
3772
|
+
return (await ghRest("GET", `${repoPath(repo)}/pulls?state=all&per_page=100`)).filter((pull) => {
|
|
3773
|
+
if (input.branch !== void 0 && input.branch.length > 0) return pull.head?.ref === input.branch;
|
|
3774
|
+
return authorMatchesBot(pull.user?.login ?? void 0, input.botLogin);
|
|
3775
|
+
}).map((pull) => ({
|
|
3776
|
+
number: pull.number ?? 0,
|
|
3777
|
+
headSha: pull.head?.sha ?? "",
|
|
3778
|
+
headRef: pull.head?.ref ?? "",
|
|
3779
|
+
isDraft: pull.draft ?? false
|
|
3780
|
+
}));
|
|
3781
|
+
}
|
|
3782
|
+
const COPILOT_REVIEWER_LOGIN = "copilot-pull-request-reviewer[bot]";
|
|
3783
|
+
/**
|
|
3784
|
+
* Request a code review from `reviewerLogin` on a PR. Best-effort: a 422
|
|
3785
|
+
* (already requested / not a collaborator) or any other error is swallowed and
|
|
3786
|
+
* reported as `requested:false` — a failed review request must not abort the
|
|
3787
|
+
* controller sweep.
|
|
3788
|
+
*/
|
|
3789
|
+
async function requestReview(repo, pr, reviewerLogin) {
|
|
3790
|
+
try {
|
|
3791
|
+
await ghRest("POST", `${repoPath(repo)}/pulls/${segment$1(pr)}/requested_reviewers`, { body: { reviewers: [reviewerLogin] } });
|
|
3792
|
+
return { requested: true };
|
|
3793
|
+
} catch (err) {
|
|
3794
|
+
consola.debug(`first-mate: requestReview(${reviewerLogin}) on PR #${pr} skipped:`, err);
|
|
3795
|
+
return { requested: false };
|
|
3796
|
+
}
|
|
3797
|
+
}
|
|
3798
|
+
const REVIEW_BODY_LIMIT = 4e3;
|
|
3799
|
+
/** Compact review summaries for a PR (author + state + hard-truncated body). */
|
|
3800
|
+
async function getPullRequestReviews(repo, pr) {
|
|
3801
|
+
return (await ghRest("GET", `${repoPath(repo)}/pulls/${segment$1(pr)}/reviews?per_page=50`) ?? []).map((review) => ({
|
|
3802
|
+
author: review.user?.login ?? "",
|
|
3803
|
+
state: review.state ?? "",
|
|
3804
|
+
bodyExcerpt: (review.body ?? "").slice(0, REVIEW_BODY_LIMIT),
|
|
3805
|
+
...review.submitted_at ? { submittedAt: review.submitted_at } : {},
|
|
3806
|
+
...review.commit_id ? { commitId: review.commit_id } : {}
|
|
3807
|
+
}));
|
|
3808
|
+
}
|
|
3809
|
+
async function getPullRequestState(repo, pr) {
|
|
3810
|
+
const pullRequest = (await ghGraphQL(`query FirstMatePullRequestState($owner: String!, $name: String!, $number: Int!) {
|
|
3811
|
+
repository(owner: $owner, name: $name) {
|
|
3812
|
+
pullRequest(number: $number) {
|
|
3813
|
+
id
|
|
3814
|
+
number
|
|
3815
|
+
title
|
|
3816
|
+
isDraft
|
|
3817
|
+
state
|
|
3818
|
+
mergeable
|
|
3819
|
+
reviewDecision
|
|
3820
|
+
headRefOid
|
|
3821
|
+
baseRefName
|
|
3822
|
+
baseRefOid
|
|
3823
|
+
author {
|
|
3824
|
+
login
|
|
3825
|
+
}
|
|
3826
|
+
}
|
|
3827
|
+
}
|
|
3828
|
+
}`, {
|
|
3829
|
+
owner: repo.owner,
|
|
3830
|
+
name: repo.repo,
|
|
3831
|
+
number: pr
|
|
3832
|
+
})).repository?.pullRequest;
|
|
3833
|
+
if (!pullRequest) throw new AgentError("NOT_FOUND", `Pull request #${pr} was not found`);
|
|
3834
|
+
let baseSha = pullRequest.baseRefOid ?? void 0;
|
|
3835
|
+
let baseRef = pullRequest.baseRefName ?? "";
|
|
3836
|
+
if (!baseSha || !baseRef) {
|
|
3837
|
+
const restPull = await ghRest("GET", `${repoPath(repo)}/pulls/${segment$1(pr)}`);
|
|
3838
|
+
baseSha = baseSha ?? restPull.base?.sha ?? void 0;
|
|
3839
|
+
baseRef = baseRef || restPull.base?.ref || "";
|
|
3840
|
+
}
|
|
3841
|
+
return {
|
|
3842
|
+
number: pullRequest.number ?? pr,
|
|
3843
|
+
title: pullRequest.title ?? "",
|
|
3844
|
+
isDraft: pullRequest.isDraft ?? false,
|
|
3845
|
+
state: pullRequest.state ?? "UNKNOWN",
|
|
3846
|
+
mergeable: pullRequest.mergeable,
|
|
3847
|
+
reviewDecision: pullRequest.reviewDecision,
|
|
3848
|
+
headSha: pullRequest.headRefOid ?? "",
|
|
3849
|
+
baseRef,
|
|
3850
|
+
baseSha,
|
|
3851
|
+
authorLogin: pullRequest.author?.login ?? void 0,
|
|
3852
|
+
nodeId: pullRequest.id ?? void 0
|
|
3853
|
+
};
|
|
3854
|
+
}
|
|
3855
|
+
function isFailingConclusion(conclusion) {
|
|
3856
|
+
return [
|
|
3857
|
+
"action_required",
|
|
3858
|
+
"cancelled",
|
|
3859
|
+
"failure",
|
|
3860
|
+
"startup_failure",
|
|
3861
|
+
"timed_out"
|
|
3862
|
+
].includes(conclusion ?? "");
|
|
3863
|
+
}
|
|
3864
|
+
async function getRequiredChecksForSha(repo, sha) {
|
|
3865
|
+
const checkRuns = ((await ghRest("GET", `${repoPath(repo)}/commits/${segment$1(sha)}/check-runs`)).check_runs ?? []).filter((check) => !/pull-request-reviewer/i.test(check.name ?? ""));
|
|
3866
|
+
const runningCount = checkRuns.filter((check) => check.status !== "completed" || !check.conclusion).length;
|
|
3867
|
+
const failingRuns = checkRuns.filter((check) => isFailingConclusion(check.conclusion));
|
|
3868
|
+
let rollup = "none";
|
|
3869
|
+
if (checkRuns.length > 0) if (failingRuns.length > 0) rollup = "failing";
|
|
3870
|
+
else if (runningCount > 0) rollup = "pending";
|
|
3871
|
+
else rollup = "passing";
|
|
3872
|
+
const checks = checkRuns.slice(0, CHECK_SUMMARY_LIMIT).map((check) => ({
|
|
3873
|
+
id: check.id ?? 0,
|
|
3874
|
+
name: check.name ?? "",
|
|
3875
|
+
conclusion: check.conclusion
|
|
3876
|
+
}));
|
|
3877
|
+
const failing = failingRuns.slice(0, FAILING_CHECK_LIMIT).map((check) => ({
|
|
3878
|
+
name: check.name ?? "",
|
|
3879
|
+
url: check.html_url ?? check.details_url ?? void 0
|
|
3880
|
+
}));
|
|
3881
|
+
return {
|
|
3882
|
+
rollup,
|
|
3883
|
+
checks,
|
|
3884
|
+
failing,
|
|
3885
|
+
runningCount
|
|
3886
|
+
};
|
|
3887
|
+
}
|
|
3888
|
+
const workflowCache = /* @__PURE__ */ new Map();
|
|
3889
|
+
/**
|
|
3890
|
+
* Whether the repo has any GitHub Actions workflow on `ref`. Lets the
|
|
3891
|
+
* controller distinguish "genuinely no CI" (route to cross-lab verify) from
|
|
3892
|
+
* "CI configured but checks not registered yet" (keep waiting) when a commit's
|
|
3893
|
+
* check-run rollup is "none". Cached per repo+ref; a 404 (no dir) means no CI.
|
|
3894
|
+
*/
|
|
3895
|
+
async function repoHasWorkflows(repo, ref) {
|
|
3896
|
+
const key = `${repoPath(repo)}@${ref}`;
|
|
3897
|
+
const hit = cached$1(workflowCache.get(key));
|
|
3898
|
+
if (hit !== void 0) return hit;
|
|
3899
|
+
let value = false;
|
|
3900
|
+
try {
|
|
3901
|
+
const entries = await ghRest("GET", `${repoPath(repo)}/contents/.github/workflows?ref=${segment$1(ref)}`);
|
|
3902
|
+
value = Array.isArray(entries) && entries.some((entry) => entry.type === "file" && /\.ya?ml$/i.test(entry.name ?? ""));
|
|
3903
|
+
} catch (err) {
|
|
3904
|
+
if (!(err instanceof AgentError && err.code === "NOT_FOUND")) consola.debug("first-mate: workflow probe failed, assuming no CI:", err);
|
|
3905
|
+
value = false;
|
|
3906
|
+
}
|
|
3907
|
+
workflowCache.set(key, {
|
|
3908
|
+
timestamp: Date.now(),
|
|
3909
|
+
value
|
|
3910
|
+
});
|
|
3911
|
+
return value;
|
|
3912
|
+
}
|
|
3913
|
+
async function postComment(repo, number, body) {
|
|
3914
|
+
const comment = await ghRest("POST", `${repoPath(repo)}/issues/${segment$1(number)}/comments`, { body: { body } });
|
|
3915
|
+
return { url: comment.html_url ?? comment.url ?? "" };
|
|
3916
|
+
}
|
|
3917
|
+
async function submitReview(repo, pr, event, body) {
|
|
3918
|
+
const requestBody = { event };
|
|
3919
|
+
if (body !== void 0) requestBody.body = body;
|
|
3920
|
+
const review = await ghRest("POST", `${repoPath(repo)}/pulls/${segment$1(pr)}/reviews`, { body: requestBody });
|
|
3921
|
+
return {
|
|
3922
|
+
reviewId: review.id ?? 0,
|
|
3923
|
+
state: review.state ?? ""
|
|
3924
|
+
};
|
|
3925
|
+
}
|
|
3926
|
+
async function rerunChecks(repo, input) {
|
|
3927
|
+
const suffix = input.failedOnly ? "rerun-failed-jobs" : "rerun";
|
|
3928
|
+
const response = await ghRestRaw("POST", `${repoPath(repo)}/actions/runs/${segment$1(input.runId)}/${suffix}`);
|
|
3929
|
+
if (!response.ok) throw agentErrorFromResponse(response, "Rerun checks failed");
|
|
3930
|
+
return { rerun: true };
|
|
3931
|
+
}
|
|
3932
|
+
async function mergePullRequest(repo, input) {
|
|
3933
|
+
const response = await ghRestRaw("PUT", `${repoPath(repo)}/pulls/${segment$1(input.pr)}/merge`, { body: {
|
|
3934
|
+
merge_method: input.method ?? "squash",
|
|
3935
|
+
sha: input.expectedHeadSha
|
|
3936
|
+
} });
|
|
3937
|
+
if (response.status === 405 || response.status === 409) throw new AgentError("HEAD_MOVED", "Pull request head moved or is not mergeable");
|
|
3938
|
+
if (!response.ok) throw agentErrorFromResponse(response, "Pull request merge failed");
|
|
3939
|
+
const result = await readJsonObject(response);
|
|
3940
|
+
if (result.merged === false) throw new AgentError("UPSTREAM", stringValue$2(result.message) ?? "GitHub did not merge the pull request");
|
|
3941
|
+
return {
|
|
3942
|
+
merged: true,
|
|
3943
|
+
sha: stringValue$2(result.sha) ?? ""
|
|
3944
|
+
};
|
|
3945
|
+
}
|
|
3946
|
+
async function markReadyForReview(prNodeId) {
|
|
3947
|
+
await ghGraphQL(`mutation FirstMateReadyForReview($pullRequestId: ID!) {
|
|
3948
|
+
markPullRequestReadyForReview(input: { pullRequestId: $pullRequestId }) {
|
|
3949
|
+
pullRequest {
|
|
3950
|
+
id
|
|
3951
|
+
}
|
|
3952
|
+
}
|
|
3953
|
+
}`, { pullRequestId: prNodeId });
|
|
3954
|
+
return { ready: true };
|
|
3955
|
+
}
|
|
3956
|
+
|
|
3957
|
+
//#endregion
|
|
3958
|
+
//#region src/lib/agent/capi.ts
|
|
3959
|
+
const CAPI_INTEGRATION_ID = "copilot-4-cli";
|
|
3960
|
+
const CAPI_API_VERSION = "2026-01-09";
|
|
3961
|
+
const HOST_CACHE_TTL_MS = 600 * 1e3;
|
|
3962
|
+
const LOG_EXCERPT_LIMIT$1 = 4e3;
|
|
3963
|
+
const TRUNCATED_MARKER$1 = "…[truncated]…";
|
|
3964
|
+
const MAX_LOG_BYTES = 4 * 1024 * 1024;
|
|
3965
|
+
const MAX_FIELD_CHARS = 256 * 1024;
|
|
3966
|
+
let hostCache = null;
|
|
3967
|
+
async function discoverCapiHost(signal) {
|
|
3968
|
+
const now = Date.now();
|
|
3969
|
+
if (hostCache && now - hostCache.at < HOST_CACHE_TTL_MS) return hostCache.host;
|
|
3970
|
+
try {
|
|
3971
|
+
const host = (await ghGraphQL("query CopilotEndpoints { viewer { copilotEndpoints { api } } }", {}, { signal })).viewer?.copilotEndpoints?.api;
|
|
3972
|
+
if (typeof host === "string" && host.length > 0) {
|
|
3973
|
+
const normalized = host.replace(/\/+$/, "");
|
|
3974
|
+
if (!isAllowedCopilotHost(normalized)) {
|
|
3975
|
+
consola.debug(`first-mate capi: discovered host not allowlisted: ${normalized}`);
|
|
3976
|
+
return null;
|
|
3977
|
+
}
|
|
3978
|
+
hostCache = {
|
|
3979
|
+
host: normalized,
|
|
3980
|
+
at: now
|
|
3981
|
+
};
|
|
3982
|
+
return hostCache.host;
|
|
3983
|
+
}
|
|
3984
|
+
consola.debug("first-mate capi: discovery returned no host");
|
|
3985
|
+
} catch (err) {
|
|
3986
|
+
consola.debug("first-mate capi: host discovery failed:", err);
|
|
3987
|
+
}
|
|
3988
|
+
return null;
|
|
3989
|
+
}
|
|
3990
|
+
function capiHeaders() {
|
|
3991
|
+
return {
|
|
3992
|
+
authorization: `Bearer ${state.githubAgentToken ?? ""}`,
|
|
3993
|
+
"copilot-integration-id": CAPI_INTEGRATION_ID,
|
|
3994
|
+
"x-github-api-version": CAPI_API_VERSION,
|
|
3995
|
+
"user-agent": "github-router-first-mate"
|
|
3996
|
+
};
|
|
3997
|
+
}
|
|
3998
|
+
function asRecord$6(value) {
|
|
3999
|
+
return typeof value === "object" && value !== null ? value : void 0;
|
|
4000
|
+
}
|
|
4001
|
+
function truncateHead(text) {
|
|
4002
|
+
if (text.length <= LOG_EXCERPT_LIMIT$1) return text;
|
|
4003
|
+
return `${text.slice(0, LOG_EXCERPT_LIMIT$1 - 13)}${TRUNCATED_MARKER$1}`;
|
|
4004
|
+
}
|
|
4005
|
+
const PLAN_LIMIT = 2800;
|
|
4006
|
+
const PROGRESS_LIMIT = 900;
|
|
4007
|
+
function headTruncate(text, limit) {
|
|
4008
|
+
if (text.length <= limit) return text;
|
|
4009
|
+
return `${text.slice(0, limit - 13)}${TRUNCATED_MARKER$1}`;
|
|
4010
|
+
}
|
|
4011
|
+
function tailTruncate(text, limit) {
|
|
4012
|
+
if (text.length <= limit) return text;
|
|
4013
|
+
return `${TRUNCATED_MARKER$1}${text.slice(-(limit - 13))}`;
|
|
4014
|
+
}
|
|
4015
|
+
/** Extract a `<plan>…</plan>` block (the plan-mode agent wraps its plan in it). */
|
|
4016
|
+
function extractPlanBlock(text) {
|
|
4017
|
+
const inner = /<plan>([\s\S]*?)<\/plan>/i.exec(text)?.[1]?.trim();
|
|
4018
|
+
return inner && inner.length > 0 ? inner : null;
|
|
4019
|
+
}
|
|
4020
|
+
/** Extract the branch the agent checked out ("checked out branch X"). */
|
|
4021
|
+
function extractBranch(text) {
|
|
4022
|
+
const branch = /checked out branch\s+([a-z0-9][a-z0-9._/-]*)/.exec(text)?.[1]?.replace(/[.\-_/]+$/, "");
|
|
4023
|
+
return branch && branch.length > 0 ? branch : void 0;
|
|
4024
|
+
}
|
|
4025
|
+
/**
|
|
4026
|
+
* Drop the agent's MCP-server registration preamble. Every cloud-agent session
|
|
4027
|
+
* emits a large, signal-free tool-registration dump ("MCP server started
|
|
4028
|
+
* successfully with N tools", "- github-mcp-server/actions_get", …) at the
|
|
4029
|
+
* START, which would otherwise dominate a head-truncated excerpt and bury the
|
|
4030
|
+
* actual plan/progress that lands at the tail.
|
|
4031
|
+
*/
|
|
4032
|
+
function stripMcpBoilerplate(text) {
|
|
4033
|
+
return text.split("\n").filter((line) => {
|
|
4034
|
+
const l = line.trim();
|
|
4035
|
+
if (/^MCP server started successfully/i.test(l)) return false;
|
|
4036
|
+
if (/^-\s+(runtime-tools|github-mcp-server)\//i.test(l)) return false;
|
|
4037
|
+
return true;
|
|
4038
|
+
}).join("\n");
|
|
4039
|
+
}
|
|
4040
|
+
/** Append with a per-field cap; once at the cap, further growth is dropped. */
|
|
4041
|
+
function capped(current, addition) {
|
|
4042
|
+
if (current.length >= MAX_FIELD_CHARS) return current;
|
|
4043
|
+
const room = MAX_FIELD_CHARS - current.length;
|
|
4044
|
+
return current + (addition.length > room ? addition.slice(0, room) : addition);
|
|
4045
|
+
}
|
|
4046
|
+
/**
|
|
4047
|
+
* Read a response body as text, capped at `MAX_LOG_BYTES`. Stops pulling once
|
|
4048
|
+
* the cap is reached (a runaway/untrusted stream must not exhaust memory).
|
|
4049
|
+
*/
|
|
4050
|
+
async function readCappedText(response) {
|
|
4051
|
+
const body = response.body;
|
|
4052
|
+
if (!body) return response.text();
|
|
4053
|
+
const reader = body.getReader();
|
|
4054
|
+
const decoder = new TextDecoder();
|
|
4055
|
+
let text = "";
|
|
4056
|
+
try {
|
|
4057
|
+
for (;;) {
|
|
4058
|
+
const { done, value } = await reader.read();
|
|
4059
|
+
if (done) break;
|
|
4060
|
+
text += decoder.decode(value, { stream: true });
|
|
4061
|
+
if (text.length >= MAX_LOG_BYTES) {
|
|
4062
|
+
reader.cancel().catch(() => {});
|
|
4063
|
+
break;
|
|
4064
|
+
}
|
|
4065
|
+
}
|
|
4066
|
+
text += decoder.decode();
|
|
4067
|
+
} catch (err) {
|
|
4068
|
+
consola.debug("first-mate capi: capped read interrupted:", err);
|
|
4069
|
+
}
|
|
4070
|
+
return text.length > MAX_LOG_BYTES ? text.slice(0, MAX_LOG_BYTES) : text;
|
|
4071
|
+
}
|
|
4072
|
+
/**
|
|
4073
|
+
* Parse the SSE body of a session-log stream into a compact excerpt. Tolerant
|
|
4074
|
+
* of partial/incomplete streams (an in-progress agent) and SSE-compliant for
|
|
4075
|
+
* multi-line `data:` events (a single event's `data:` lines are joined with
|
|
4076
|
+
* `\n` before parsing). `finished` reflects whether a terminal chunk was seen.
|
|
4077
|
+
* All log content is UNTRUSTED agent text.
|
|
4078
|
+
*/
|
|
4079
|
+
function parseSessionLog(body) {
|
|
4080
|
+
let content = "";
|
|
4081
|
+
let reasoning = "";
|
|
4082
|
+
let finished = false;
|
|
4083
|
+
const toolNames = [];
|
|
4084
|
+
const toolCalls = /* @__PURE__ */ new Map();
|
|
4085
|
+
const events$1 = body.replace(/\r\n/g, "\n").split(/\n\n+/);
|
|
4086
|
+
for (const event of events$1) {
|
|
4087
|
+
const dataLines = [];
|
|
4088
|
+
for (const rawLine of event.split("\n")) {
|
|
4089
|
+
const line = rawLine.trimEnd();
|
|
4090
|
+
if (line.startsWith("data:")) dataLines.push(line.slice(5).replace(/^ /, ""));
|
|
4091
|
+
}
|
|
4092
|
+
if (dataLines.length === 0) continue;
|
|
4093
|
+
const payload = dataLines.join("\n").trim();
|
|
4094
|
+
if (!payload || payload === "[DONE]") continue;
|
|
4095
|
+
let chunk;
|
|
4096
|
+
try {
|
|
4097
|
+
chunk = asRecord$6(JSON.parse(payload));
|
|
4098
|
+
} catch {
|
|
4099
|
+
continue;
|
|
4100
|
+
}
|
|
4101
|
+
if (!chunk || chunk.object !== "chat.completion.chunk") continue;
|
|
4102
|
+
const choices = Array.isArray(chunk.choices) ? chunk.choices : [];
|
|
4103
|
+
for (const choiceValue of choices) {
|
|
4104
|
+
const choice = asRecord$6(choiceValue);
|
|
4105
|
+
if (!choice) continue;
|
|
4106
|
+
const delta = asRecord$6(choice.delta) ?? {};
|
|
4107
|
+
if (typeof delta.content === "string") content = capped(content, delta.content);
|
|
4108
|
+
if (typeof delta.reasoning_text === "string") reasoning = capped(reasoning, delta.reasoning_text);
|
|
4109
|
+
if (choice.finish_reason === "stop") finished = true;
|
|
4110
|
+
const toolCallDeltas = Array.isArray(delta.tool_calls) ? delta.tool_calls : [];
|
|
4111
|
+
for (const tcValue of toolCallDeltas) {
|
|
4112
|
+
const call = asRecord$6(tcValue);
|
|
4113
|
+
if (!call) continue;
|
|
4114
|
+
const index = typeof call.index === "number" ? call.index : toolCalls.size;
|
|
4115
|
+
const fn = asRecord$6(call.function) ?? {};
|
|
4116
|
+
const existing = toolCalls.get(index) ?? { args: "" };
|
|
4117
|
+
if (typeof fn.name === "string" && fn.name.length > 0) {
|
|
4118
|
+
existing.name = fn.name;
|
|
4119
|
+
toolNames.push(fn.name);
|
|
4120
|
+
}
|
|
4121
|
+
if (typeof fn.arguments === "string") existing.args = capped(existing.args, fn.arguments);
|
|
4122
|
+
toolCalls.set(index, existing);
|
|
4123
|
+
}
|
|
4124
|
+
}
|
|
4125
|
+
}
|
|
4126
|
+
let planDescription = "";
|
|
4127
|
+
for (const call of toolCalls.values()) {
|
|
4128
|
+
if (call.name !== "report_progress") continue;
|
|
4129
|
+
try {
|
|
4130
|
+
const parsed = asRecord$6(JSON.parse(call.args));
|
|
4131
|
+
const desc = parsed?.prDescription ?? parsed?.pr_description;
|
|
4132
|
+
if (typeof desc === "string" && desc.length > planDescription.length) planDescription = desc;
|
|
4133
|
+
} catch {}
|
|
4134
|
+
}
|
|
4135
|
+
const uniqueTools = [...new Set(toolNames)];
|
|
4136
|
+
const planBlock = extractPlanBlock(content);
|
|
4137
|
+
const plan = planDescription.trim() || planBlock || "";
|
|
4138
|
+
const progress = stripMcpBoilerplate(planBlock ? content.replace(/<plan>[\s\S]*?<\/plan>/i, "") : content).trim();
|
|
4139
|
+
const parts = [];
|
|
4140
|
+
if (plan) parts.push(`Plan:\n${headTruncate(plan, PLAN_LIMIT)}`);
|
|
4141
|
+
if (reasoning.trim()) parts.push(`Reasoning:\n${tailTruncate(reasoning.trim(), PROGRESS_LIMIT)}`);
|
|
4142
|
+
if (progress) parts.push(`Progress:\n${tailTruncate(progress, PROGRESS_LIMIT)}`);
|
|
4143
|
+
if (uniqueTools.length > 0) parts.push(`Tools: ${uniqueTools.join(", ")}`);
|
|
4144
|
+
const branch = extractBranch(content);
|
|
4145
|
+
return {
|
|
4146
|
+
excerpt: truncateHead(parts.join("\n\n")),
|
|
4147
|
+
finished,
|
|
4148
|
+
tools: uniqueTools,
|
|
4149
|
+
...branch ? { branch } : {}
|
|
4150
|
+
};
|
|
4151
|
+
}
|
|
4152
|
+
/**
|
|
4153
|
+
* Fetch and distil a cloud-agent session log. Best-effort: returns `null`
|
|
4154
|
+
* (never throws) when the agent token is absent, the host can't be discovered
|
|
4155
|
+
* or validated, or the request fails — callers fall back to the
|
|
4156
|
+
* `api.github.com` task text.
|
|
4157
|
+
*/
|
|
4158
|
+
async function getSessionLog(sessionId, signal) {
|
|
4159
|
+
if (!state.githubAgentToken || !sessionId) return null;
|
|
4160
|
+
const host = await discoverCapiHost(signal);
|
|
4161
|
+
if (!host) return null;
|
|
4162
|
+
const url = `${host}/agents/sessions/${encodeURIComponent(sessionId)}/logs`;
|
|
4163
|
+
let response;
|
|
4164
|
+
try {
|
|
4165
|
+
response = await fetchWithTransientRetry(() => fetch(url, {
|
|
4166
|
+
headers: capiHeaders(),
|
|
4167
|
+
signal,
|
|
4168
|
+
redirect: "error"
|
|
4169
|
+
}), {
|
|
4170
|
+
label: `capi session-log ${sessionId}`,
|
|
4171
|
+
signal
|
|
4172
|
+
});
|
|
4173
|
+
} catch (err) {
|
|
4174
|
+
consola.debug("first-mate capi: session-log fetch failed:", err);
|
|
4175
|
+
return null;
|
|
4176
|
+
}
|
|
4177
|
+
if (!response.ok) {
|
|
4178
|
+
consola.debug(`first-mate capi: session-log ${sessionId} → HTTP ${response.status}`);
|
|
4179
|
+
return null;
|
|
4180
|
+
}
|
|
4181
|
+
try {
|
|
4182
|
+
return parseSessionLog(await readCappedText(response));
|
|
4183
|
+
} catch (err) {
|
|
4184
|
+
consola.debug("first-mate capi: session-log parse failed:", err);
|
|
4185
|
+
return null;
|
|
4186
|
+
}
|
|
4187
|
+
}
|
|
4188
|
+
|
|
4189
|
+
//#endregion
|
|
4190
|
+
//#region src/lib/agent/tasks.ts
|
|
4191
|
+
const AGENT_TASKS_API_VERSION = "2026-03-10";
|
|
4192
|
+
const LOG_EXCERPT_LIMIT = 4e3;
|
|
4193
|
+
const TRUNCATED_MARKER = "…[truncated]…";
|
|
4194
|
+
const FOLLOW_UP_TASK_PATH_SUFFIX = "";
|
|
4195
|
+
const CANCEL_TASK_PATH_SUFFIX = "/cancel";
|
|
4196
|
+
function segment(value) {
|
|
4197
|
+
return encodeURIComponent(String(value));
|
|
4198
|
+
}
|
|
4199
|
+
function repoTasksPath(repo) {
|
|
4200
|
+
return `/agents/repos/${segment(repo.owner)}/${segment(repo.repo)}/tasks`;
|
|
4201
|
+
}
|
|
4202
|
+
function taskPath(repo, taskId) {
|
|
4203
|
+
return `${repoTasksPath(repo)}/${segment(taskId)}`;
|
|
4204
|
+
}
|
|
4205
|
+
function asRecord$5(value) {
|
|
4206
|
+
return typeof value === "object" && value !== null ? value : void 0;
|
|
4207
|
+
}
|
|
4208
|
+
function stringField(record, keys) {
|
|
4209
|
+
for (const key of keys) {
|
|
4210
|
+
const value = record?.[key];
|
|
4211
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
4212
|
+
}
|
|
4213
|
+
}
|
|
4214
|
+
function numberField(record, keys) {
|
|
4215
|
+
for (const key of keys) {
|
|
4216
|
+
const value = record?.[key];
|
|
4217
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
4218
|
+
}
|
|
4219
|
+
}
|
|
4220
|
+
function collectText(record) {
|
|
4221
|
+
if (!record) return [];
|
|
4222
|
+
const nested = [
|
|
4223
|
+
asRecord$5(record.task),
|
|
4224
|
+
asRecord$5(record.session),
|
|
4225
|
+
asRecord$5(record.progress),
|
|
4226
|
+
asRecord$5(record.result)
|
|
4227
|
+
];
|
|
4228
|
+
const textKeys = [
|
|
4229
|
+
"session_log",
|
|
4230
|
+
"sessionLog",
|
|
4231
|
+
"log",
|
|
4232
|
+
"logs",
|
|
4233
|
+
"progress",
|
|
4234
|
+
"plan",
|
|
4235
|
+
"summary",
|
|
4236
|
+
"status_text",
|
|
4237
|
+
"statusText",
|
|
4238
|
+
"message"
|
|
4239
|
+
];
|
|
4240
|
+
const chunks = [];
|
|
4241
|
+
for (const candidate of [record, ...nested]) for (const key of textKeys) {
|
|
4242
|
+
const value = candidate?.[key];
|
|
4243
|
+
if (typeof value === "string" && value.trim().length > 0) chunks.push(value);
|
|
4244
|
+
}
|
|
4245
|
+
return chunks;
|
|
4246
|
+
}
|
|
4247
|
+
function compactKnownKeysSummary(record) {
|
|
4248
|
+
if (!record) return "preview task response was not an object";
|
|
4249
|
+
const keys = Object.keys(record).slice(0, 30);
|
|
4250
|
+
return `preview task response keys: ${JSON.stringify(keys)}`;
|
|
4251
|
+
}
|
|
4252
|
+
function tailExcerpt(text) {
|
|
4253
|
+
if (text.length <= LOG_EXCERPT_LIMIT) return text;
|
|
4254
|
+
return `${TRUNCATED_MARKER}${text.slice(-(LOG_EXCERPT_LIMIT - 13))}`;
|
|
4255
|
+
}
|
|
4256
|
+
function taskPrUrl(record) {
|
|
4257
|
+
const direct = stringField(record, [
|
|
4258
|
+
"pr_url",
|
|
4259
|
+
"prUrl",
|
|
4260
|
+
"pull_request_url",
|
|
4261
|
+
"pullRequestUrl"
|
|
4262
|
+
]);
|
|
4263
|
+
if (direct) return direct;
|
|
4264
|
+
return stringField(asRecord$5(record?.pull_request) ?? asRecord$5(record?.pullRequest), ["html_url", "url"]);
|
|
4265
|
+
}
|
|
4266
|
+
function taskPrNumber$1(record) {
|
|
4267
|
+
const direct = numberField(record, [
|
|
4268
|
+
"pr",
|
|
4269
|
+
"pr_number",
|
|
4270
|
+
"prNumber",
|
|
4271
|
+
"pull_request_number"
|
|
4272
|
+
]);
|
|
4273
|
+
if (direct !== void 0) return direct;
|
|
4274
|
+
return numberField(asRecord$5(record?.pull_request) ?? asRecord$5(record?.pullRequest), ["number"]) ?? void 0;
|
|
4275
|
+
}
|
|
4276
|
+
function latestSessionId(record) {
|
|
4277
|
+
const sessions$1 = record?.sessions;
|
|
4278
|
+
if (!Array.isArray(sessions$1) || sessions$1.length === 0) return void 0;
|
|
4279
|
+
for (let i = sessions$1.length - 1; i >= 0; i -= 1) {
|
|
4280
|
+
const id = stringField(asRecord$5(sessions$1[i]), [
|
|
4281
|
+
"id",
|
|
4282
|
+
"session_id",
|
|
4283
|
+
"sessionId"
|
|
4284
|
+
]);
|
|
4285
|
+
if (id) return id;
|
|
4286
|
+
}
|
|
4287
|
+
}
|
|
4288
|
+
async function startTask(repo, input) {
|
|
4289
|
+
const body = { prompt: input.prompt };
|
|
4290
|
+
if (input.baseRef !== void 0) body.base_ref = input.baseRef;
|
|
4291
|
+
if (input.model !== void 0) body.model = input.model;
|
|
4292
|
+
if (input.createPullRequest !== void 0) body.create_pull_request = input.createPullRequest;
|
|
4293
|
+
const response = await ghRest("POST", repoTasksPath(repo), {
|
|
4294
|
+
apiVersion: AGENT_TASKS_API_VERSION,
|
|
4295
|
+
body,
|
|
4296
|
+
retry: false,
|
|
4297
|
+
...input.idempotencyKey ? { headers: { "Idempotency-Key": input.idempotencyKey } } : {}
|
|
4298
|
+
});
|
|
4299
|
+
return {
|
|
4300
|
+
taskId: stringField(response, [
|
|
4301
|
+
"task_id",
|
|
4302
|
+
"taskId",
|
|
4303
|
+
"id"
|
|
4304
|
+
]) ?? "",
|
|
4305
|
+
state: stringField(response, ["state", "status"]) ?? "unknown"
|
|
4306
|
+
};
|
|
4307
|
+
}
|
|
4308
|
+
async function getTask(repo, taskId) {
|
|
4309
|
+
const record = asRecord$5(await ghRest("GET", taskPath(repo, taskId), { apiVersion: AGENT_TASKS_API_VERSION }));
|
|
4310
|
+
const sessionId = latestSessionId(record);
|
|
4311
|
+
const sessionLog = sessionId ? await getSessionLog(sessionId) : null;
|
|
4312
|
+
const fallbackText = collectText(record).join("\n\n");
|
|
4313
|
+
const logExcerpt = sessionLog?.excerpt && sessionLog.excerpt.length > 0 ? sessionLog.excerpt : tailExcerpt(fallbackText.length > 0 ? fallbackText : compactKnownKeysSummary(record));
|
|
4314
|
+
return {
|
|
4315
|
+
taskId: stringField(record, [
|
|
4316
|
+
"task_id",
|
|
4317
|
+
"taskId",
|
|
4318
|
+
"id"
|
|
4319
|
+
]) ?? taskId,
|
|
4320
|
+
state: stringField(record, ["state", "status"]) ?? "unknown",
|
|
4321
|
+
prUrl: taskPrUrl(record),
|
|
4322
|
+
pr: taskPrNumber$1(record),
|
|
4323
|
+
logExcerpt,
|
|
4324
|
+
...sessionId ? { sessionId } : {},
|
|
4325
|
+
...sessionLog?.branch ? { branch: sessionLog.branch } : {}
|
|
4326
|
+
};
|
|
4327
|
+
}
|
|
4328
|
+
async function followUpTask(repo, taskId, prompt) {
|
|
4329
|
+
await ghRest("POST", `${taskPath(repo, taskId)}${FOLLOW_UP_TASK_PATH_SUFFIX}`, {
|
|
4330
|
+
apiVersion: AGENT_TASKS_API_VERSION,
|
|
4331
|
+
body: { prompt }
|
|
4332
|
+
});
|
|
4333
|
+
return { ok: true };
|
|
4334
|
+
}
|
|
4335
|
+
async function cancelTask(repo, taskId) {
|
|
4336
|
+
await ghRest("POST", `${taskPath(repo, taskId)}${CANCEL_TASK_PATH_SUFFIX}`, { apiVersion: AGENT_TASKS_API_VERSION });
|
|
4337
|
+
return { cancelled: true };
|
|
4338
|
+
}
|
|
4339
|
+
|
|
4340
|
+
//#endregion
|
|
4341
|
+
//#region src/lib/first-mate/decisions.ts
|
|
4342
|
+
const DECISIONS_VERSION = 1;
|
|
4343
|
+
const DECISION_STATUSES = new Set([
|
|
4344
|
+
"pending",
|
|
4345
|
+
"answered",
|
|
4346
|
+
"queued_away",
|
|
4347
|
+
"queued_away_irreversible"
|
|
4348
|
+
]);
|
|
4349
|
+
function decisionsPath() {
|
|
4350
|
+
return nodePath.join(PATHS.FIRST_MATE_DIR, "decisions.json");
|
|
4351
|
+
}
|
|
4352
|
+
function asRecord$4(value) {
|
|
4353
|
+
return typeof value === "object" && value !== null ? value : void 0;
|
|
4354
|
+
}
|
|
4355
|
+
function isOneOf$1(value, allowed) {
|
|
4356
|
+
return typeof value === "string" && allowed.has(value);
|
|
4357
|
+
}
|
|
4358
|
+
function isFiniteNumber$2(value) {
|
|
4359
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
4360
|
+
}
|
|
4361
|
+
function isPositiveInteger(value) {
|
|
4362
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
4363
|
+
}
|
|
4364
|
+
function isOptionalString$2(value) {
|
|
4365
|
+
return value === void 0 || typeof value === "string";
|
|
4366
|
+
}
|
|
4367
|
+
function isOptionalStringOrNull$1(value) {
|
|
4368
|
+
return value === void 0 || value === null || typeof value === "string";
|
|
4369
|
+
}
|
|
4370
|
+
function isOptionalFiniteNumber$2(value) {
|
|
4371
|
+
return value === void 0 || isFiniteNumber$2(value);
|
|
4372
|
+
}
|
|
4373
|
+
function isOptionalStringArray(value) {
|
|
4374
|
+
return value === void 0 || Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
4375
|
+
}
|
|
4376
|
+
function isRepoRef$2(value) {
|
|
4377
|
+
const repo = asRecord$4(value);
|
|
4378
|
+
return repo !== void 0 && typeof repo.owner === "string" && repo.owner.length > 0 && typeof repo.name === "string" && repo.name.length > 0;
|
|
4379
|
+
}
|
|
4380
|
+
function isOptionRefs(value) {
|
|
4381
|
+
return value === void 0 || Array.isArray(value) && value.every((option) => {
|
|
4382
|
+
const row = asRecord$4(option);
|
|
4383
|
+
return row !== void 0 && typeof row.id === "string";
|
|
4384
|
+
});
|
|
4385
|
+
}
|
|
4386
|
+
function isApprovalRecord(value) {
|
|
4387
|
+
const approval = asRecord$4(value);
|
|
4388
|
+
return approval !== void 0 && typeof approval.decisionId === "string" && approval.decisionId.length > 0 && isRepoRef$2(approval.repo) && isPositiveInteger(approval.pr) && typeof approval.headSha === "string" && approval.headSha.length > 0 && isOptionalString$2(approval.baseSha) && isOptionalString$2(approval.diffDigest) && isOptionalStringArray(approval.requiredCheckIds) && isOptionalString$2(approval.floorRunId) && approval.status === "approved" && typeof approval.consumed === "boolean" && isFiniteNumber$2(approval.createdMs) && isOptionalFiniteNumber$2(approval.consumedMs);
|
|
4389
|
+
}
|
|
4390
|
+
function isOptionalApprovalRecord(value) {
|
|
4391
|
+
return value === void 0 || isApprovalRecord(value);
|
|
4392
|
+
}
|
|
4393
|
+
function isDecisionRecord(value) {
|
|
4394
|
+
const record = asRecord$4(value);
|
|
4395
|
+
return record !== void 0 && typeof record.decisionId === "string" && record.decisionId.length > 0 && typeof record.decisionKey === "string" && record.decisionKey.length > 0 && typeof record.type === "string" && record.type.length > 0 && isOneOf$1(record.status, DECISION_STATUSES) && isOptionalString$2(record.packetId) && typeof record.inputFingerprint === "string" && record.inputFingerprint.length > 0 && isOptionRefs(record.options) && isOptionalStringOrNull$1(record.chosenOptionId) && isOptionalStringOrNull$1(record.resolvedBy) && isFiniteNumber$2(record.createdMs) && isOptionalFiniteNumber$2(record.resolvedMs) && isOptionalApprovalRecord(record.approval);
|
|
4396
|
+
}
|
|
4397
|
+
async function readDecisionsFile() {
|
|
4398
|
+
let raw;
|
|
4399
|
+
try {
|
|
4400
|
+
raw = await fs.readFile(decisionsPath(), "utf8");
|
|
4401
|
+
} catch (err) {
|
|
4402
|
+
if (err.code !== "ENOENT") consola.debug("first-mate decisions read skipped:", err);
|
|
4403
|
+
return {
|
|
4404
|
+
version: DECISIONS_VERSION,
|
|
4405
|
+
decisions: []
|
|
4406
|
+
};
|
|
4407
|
+
}
|
|
4408
|
+
try {
|
|
4409
|
+
const parsed = asRecord$4(JSON.parse(raw));
|
|
4410
|
+
if (!parsed || parsed.version !== DECISIONS_VERSION || !Array.isArray(parsed.decisions)) return {
|
|
4411
|
+
version: DECISIONS_VERSION,
|
|
4412
|
+
decisions: []
|
|
4413
|
+
};
|
|
4414
|
+
const cleaned = parsed.decisions.filter(isDecisionRecord);
|
|
4415
|
+
if (cleaned.length !== parsed.decisions.length) consola.debug(`first-mate decisions dropped ${parsed.decisions.length - cleaned.length} corrupt decision(s)`);
|
|
4416
|
+
return {
|
|
4417
|
+
version: DECISIONS_VERSION,
|
|
4418
|
+
decisions: cleaned
|
|
4419
|
+
};
|
|
4420
|
+
} catch (err) {
|
|
4421
|
+
consola.debug("first-mate decisions corrupt, starting empty:", err);
|
|
4422
|
+
return {
|
|
4423
|
+
version: DECISIONS_VERSION,
|
|
4424
|
+
decisions: []
|
|
4425
|
+
};
|
|
4426
|
+
}
|
|
4427
|
+
}
|
|
4428
|
+
async function writeDecisionsFile(value) {
|
|
4429
|
+
await fs.mkdir(PATHS.FIRST_MATE_DIR, { recursive: true });
|
|
4430
|
+
const target = decisionsPath();
|
|
4431
|
+
const tmp = `${target}.tmp.${process.pid}.${randomBytes(4).toString("hex")}`;
|
|
4432
|
+
try {
|
|
4433
|
+
await fs.writeFile(tmp, `${JSON.stringify(value, null, 2)}\n`, { mode: 384 });
|
|
4434
|
+
await fs.chmod(tmp, 384).catch(() => {});
|
|
4435
|
+
await fs.rename(tmp, target);
|
|
4436
|
+
await fs.chmod(target, 384).catch(() => {});
|
|
4437
|
+
} catch (err) {
|
|
4438
|
+
await fs.unlink(tmp).catch(() => {});
|
|
4439
|
+
throw err;
|
|
4440
|
+
}
|
|
4441
|
+
}
|
|
4442
|
+
let _decisionsChain = Promise.resolve();
|
|
4443
|
+
function withDecisionsMutation(work) {
|
|
4444
|
+
const next = _decisionsChain.then(async () => {
|
|
4445
|
+
const file = await readDecisionsFile();
|
|
4446
|
+
const result = await work(file.decisions);
|
|
4447
|
+
await writeDecisionsFile(file);
|
|
4448
|
+
return result;
|
|
4449
|
+
});
|
|
4450
|
+
_decisionsChain = next.then(() => void 0, () => void 0);
|
|
4451
|
+
return next;
|
|
4452
|
+
}
|
|
4453
|
+
async function readDecisions() {
|
|
4454
|
+
return (await readDecisionsFile()).decisions;
|
|
4455
|
+
}
|
|
4456
|
+
async function upsertDecision(rec) {
|
|
4457
|
+
await withDecisionsMutation((decisions) => {
|
|
4458
|
+
const kept = decisions.filter((entry) => entry.decisionId !== rec.decisionId && entry.decisionKey !== rec.decisionKey);
|
|
4459
|
+
decisions.splice(0, decisions.length, ...kept, rec);
|
|
4460
|
+
});
|
|
4461
|
+
}
|
|
4462
|
+
async function findByKey(decisionKey) {
|
|
4463
|
+
return (await readDecisions()).find((record) => record.decisionKey === decisionKey);
|
|
4464
|
+
}
|
|
4465
|
+
async function markAnswered(decisionId, chosenOptionId, resolvedBy) {
|
|
4466
|
+
await withDecisionsMutation((decisions) => {
|
|
4467
|
+
const record = decisions.find((entry) => entry.decisionId === decisionId);
|
|
4468
|
+
if (!record) return;
|
|
4469
|
+
record.status = "answered";
|
|
4470
|
+
record.chosenOptionId = chosenOptionId;
|
|
4471
|
+
record.resolvedBy = resolvedBy;
|
|
4472
|
+
record.resolvedMs = Date.now();
|
|
4473
|
+
});
|
|
4474
|
+
}
|
|
4475
|
+
|
|
4476
|
+
//#endregion
|
|
4477
|
+
//#region src/lib/first-mate/approval.ts
|
|
4478
|
+
function sameRepo(a, b) {
|
|
4479
|
+
return a.owner.toLowerCase() === b.owner.toLowerCase() && a.name.toLowerCase() === b.name.toLowerCase();
|
|
4480
|
+
}
|
|
4481
|
+
function approvedRecord(a) {
|
|
4482
|
+
const approval = {
|
|
4483
|
+
decisionId: a.decisionId,
|
|
4484
|
+
repo: a.repo,
|
|
4485
|
+
pr: a.pr,
|
|
4486
|
+
headSha: a.headSha,
|
|
4487
|
+
status: "approved",
|
|
4488
|
+
consumed: false,
|
|
4489
|
+
createdMs: Date.now()
|
|
4490
|
+
};
|
|
4491
|
+
if (a.baseSha !== void 0) approval.baseSha = a.baseSha;
|
|
4492
|
+
if (a.diffDigest !== void 0) approval.diffDigest = a.diffDigest;
|
|
4493
|
+
if (a.requiredCheckIds !== void 0) approval.requiredCheckIds = [...a.requiredCheckIds];
|
|
4494
|
+
if (a.floorRunId !== void 0) approval.floorRunId = a.floorRunId;
|
|
4495
|
+
return approval;
|
|
4496
|
+
}
|
|
4497
|
+
/**
|
|
4498
|
+
* Record a human merge/irreversible approval in the durable decisions ledger.
|
|
4499
|
+
*
|
|
4500
|
+
* This function belongs only on the human-approval path: the model is never
|
|
4501
|
+
* given a tool that can call it, so an approval is a durable controller fact
|
|
4502
|
+
* rather than agent-authored text the model could forge.
|
|
4503
|
+
*/
|
|
4504
|
+
async function recordApproval(a) {
|
|
4505
|
+
await withDecisionsMutation((decisions) => {
|
|
4506
|
+
const decision = decisions.find((entry) => entry.decisionId === a.decisionId);
|
|
4507
|
+
if (!decision) throw new Error(`Cannot record approval for unknown decision ${a.decisionId}`);
|
|
4508
|
+
decision.approval = approvedRecord(a);
|
|
4509
|
+
});
|
|
4510
|
+
}
|
|
4511
|
+
async function verifyAndConsumeApproval(args) {
|
|
4512
|
+
return withDecisionsMutation((decisions) => {
|
|
4513
|
+
const approvals = decisions.map((decision) => decision.approval).filter((entry) => entry !== void 0 && sameRepo(entry.repo, args.repo) && entry.pr === args.pr);
|
|
4514
|
+
if (approvals.length === 0) return {
|
|
4515
|
+
ok: false,
|
|
4516
|
+
reason: "no_approval"
|
|
4517
|
+
};
|
|
4518
|
+
const approval = approvals.filter((entry) => !entry.consumed).sort((a, b) => b.createdMs - a.createdMs)[0];
|
|
4519
|
+
if (approval === void 0) return {
|
|
4520
|
+
ok: false,
|
|
4521
|
+
reason: "replayed"
|
|
4522
|
+
};
|
|
4523
|
+
if (approval.headSha !== args.liveHeadSha) return {
|
|
4524
|
+
ok: false,
|
|
4525
|
+
reason: "head_moved"
|
|
4526
|
+
};
|
|
4527
|
+
if (approval.baseSha !== void 0 && approval.baseSha !== args.liveBaseSha) return {
|
|
4528
|
+
ok: false,
|
|
4529
|
+
reason: "base_moved"
|
|
4530
|
+
};
|
|
4531
|
+
approval.consumed = true;
|
|
4532
|
+
approval.consumedMs = Date.now();
|
|
4533
|
+
return { ok: true };
|
|
4534
|
+
});
|
|
4535
|
+
}
|
|
4536
|
+
|
|
4537
|
+
//#endregion
|
|
4538
|
+
//#region src/lib/first-mate/model-tiers.ts
|
|
4539
|
+
const T0_MODEL_CHAIN = [
|
|
4540
|
+
"gemini-3.5-flash",
|
|
4541
|
+
"gemini-3-flash-preview",
|
|
4542
|
+
"gpt-5.4-mini",
|
|
4543
|
+
"gpt-5-mini",
|
|
4544
|
+
"claude-haiku-4.5",
|
|
4545
|
+
"gpt-4o-mini"
|
|
4546
|
+
];
|
|
4547
|
+
const T1_MODEL_CHAIN = [
|
|
4548
|
+
"gpt-5.4-mini",
|
|
4549
|
+
"gpt-5-mini",
|
|
4550
|
+
"gpt-5.5",
|
|
4551
|
+
"gemini-3.1-pro-preview"
|
|
4552
|
+
];
|
|
4553
|
+
const T2_MODEL_CHAIN = [
|
|
4554
|
+
"gpt-5.5",
|
|
4555
|
+
"claude-opus-4.8",
|
|
4556
|
+
"gemini-3.1-pro-preview"
|
|
4557
|
+
];
|
|
4558
|
+
const TIER_CHAINS = {
|
|
4559
|
+
T0: T0_MODEL_CHAIN,
|
|
4560
|
+
T1: T1_MODEL_CHAIN,
|
|
4561
|
+
T2: T2_MODEL_CHAIN
|
|
4562
|
+
};
|
|
4563
|
+
const T0_FALLBACK_RE = /mini|flash|nano|haiku|small/i;
|
|
4564
|
+
const MEMO_TTL_MS = 3e4;
|
|
4565
|
+
const memo = {};
|
|
4566
|
+
function catalogIds(data) {
|
|
4567
|
+
if (!data) return [];
|
|
4568
|
+
return data.map((model) => model.id).filter((id) => typeof id === "string" && id.length > 0);
|
|
4569
|
+
}
|
|
4570
|
+
function catalogKey(ids) {
|
|
4571
|
+
return ids.join("\0");
|
|
4572
|
+
}
|
|
4573
|
+
function resolveFromIds(tier$1, ids) {
|
|
4574
|
+
if (ids.length === 0) return void 0;
|
|
4575
|
+
const available = new Set(ids);
|
|
4576
|
+
for (const model of TIER_CHAINS[tier$1]) if (available.has(model)) return model;
|
|
4577
|
+
if (tier$1 === "T0") return ids.find((id) => T0_FALLBACK_RE.test(id));
|
|
4578
|
+
return ids[0];
|
|
4579
|
+
}
|
|
4580
|
+
function resolveTierModel(tier$1) {
|
|
4581
|
+
const data = state.models?.data;
|
|
4582
|
+
const length = data?.length ?? 0;
|
|
4583
|
+
const ids = catalogIds(data);
|
|
4584
|
+
const key = catalogKey(ids);
|
|
4585
|
+
const now = Date.now();
|
|
4586
|
+
const cached$2 = memo[tier$1];
|
|
4587
|
+
if (cached$2 && cached$2.expiresAt > now && cached$2.data === data && cached$2.length === length && cached$2.catalogKey === key) return cached$2.value;
|
|
4588
|
+
const value = resolveFromIds(tier$1, ids);
|
|
4589
|
+
memo[tier$1] = {
|
|
4590
|
+
data,
|
|
4591
|
+
length,
|
|
4592
|
+
catalogKey: key,
|
|
4593
|
+
expiresAt: now + MEMO_TTL_MS,
|
|
4594
|
+
value
|
|
4595
|
+
};
|
|
4596
|
+
return value;
|
|
4597
|
+
}
|
|
4598
|
+
|
|
4599
|
+
//#endregion
|
|
4600
|
+
//#region src/lib/first-mate/classifier.ts
|
|
4601
|
+
function isRecord$1(value) {
|
|
4602
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4603
|
+
}
|
|
4604
|
+
function firstMessageContent(value) {
|
|
4605
|
+
if (!isRecord$1(value) || !Array.isArray(value.choices)) return null;
|
|
4606
|
+
const first = value.choices[0];
|
|
4607
|
+
if (!isRecord$1(first) || !isRecord$1(first.message)) return null;
|
|
4608
|
+
return typeof first.message.content === "string" ? first.message.content : null;
|
|
4609
|
+
}
|
|
4610
|
+
function parseJsonObject(value) {
|
|
4611
|
+
try {
|
|
4612
|
+
const parsed = JSON.parse(value);
|
|
4613
|
+
return isRecord$1(parsed) ? parsed : null;
|
|
4614
|
+
} catch {
|
|
4615
|
+
return null;
|
|
4616
|
+
}
|
|
4617
|
+
}
|
|
4618
|
+
function confidenceOf(value) {
|
|
4619
|
+
const confidence = value.confidence;
|
|
4620
|
+
if (typeof confidence !== "number") return null;
|
|
4621
|
+
if (!Number.isFinite(confidence) || confidence < 0 || confidence > 1) return null;
|
|
4622
|
+
return confidence;
|
|
4623
|
+
}
|
|
4624
|
+
function classifierSystemPrompt(system, schemaHint) {
|
|
4625
|
+
return `${system}\nReply ONLY with a JSON object matching: ${schemaHint}. Include a numeric confidence field from 0 to 1. No markdown, prose, or extra text.`;
|
|
4626
|
+
}
|
|
4627
|
+
async function microClassify(opts) {
|
|
4628
|
+
const model = resolveTierModel("T0");
|
|
4629
|
+
if (!model) return null;
|
|
4630
|
+
let response;
|
|
4631
|
+
try {
|
|
4632
|
+
response = await fetch(`${copilotBaseUrl(state)}/chat/completions`, {
|
|
4633
|
+
method: "POST",
|
|
4634
|
+
headers: copilotHeaders(state),
|
|
4635
|
+
body: JSON.stringify({
|
|
4636
|
+
model,
|
|
4637
|
+
messages: [{
|
|
4638
|
+
role: "system",
|
|
4639
|
+
content: classifierSystemPrompt(opts.system, opts.schemaHint)
|
|
4640
|
+
}, {
|
|
4641
|
+
role: "user",
|
|
4642
|
+
content: opts.user
|
|
4643
|
+
}],
|
|
4644
|
+
temperature: 0,
|
|
4645
|
+
max_tokens: opts.maxTokens ?? 400,
|
|
4646
|
+
response_format: { type: "json_object" }
|
|
4647
|
+
})
|
|
4648
|
+
});
|
|
4649
|
+
} catch (err) {
|
|
4650
|
+
consola.debug("first-mate micro-classifier fetch failed:", err);
|
|
4651
|
+
return null;
|
|
4652
|
+
}
|
|
4653
|
+
try {
|
|
4654
|
+
const content = firstMessageContent(await response.json());
|
|
4655
|
+
if (!content) return null;
|
|
4656
|
+
const parsed = parseJsonObject(content);
|
|
4657
|
+
if (!parsed) return null;
|
|
4658
|
+
const confidence = confidenceOf(parsed);
|
|
4659
|
+
if (confidence === null || confidence < .6) return null;
|
|
4660
|
+
const value = opts.validate(parsed);
|
|
4661
|
+
if (value === null) return null;
|
|
4662
|
+
return {
|
|
4663
|
+
value,
|
|
4664
|
+
confidence
|
|
4665
|
+
};
|
|
4666
|
+
} catch {
|
|
4667
|
+
return null;
|
|
4668
|
+
}
|
|
4669
|
+
}
|
|
4670
|
+
function stringValue$1(value) {
|
|
4671
|
+
return typeof value === "string" ? value : null;
|
|
4672
|
+
}
|
|
4673
|
+
function booleanValue$1(value) {
|
|
4674
|
+
return typeof value === "boolean" ? value : null;
|
|
4675
|
+
}
|
|
4676
|
+
async function classifyPlanReady(logExcerpt) {
|
|
4677
|
+
return (await microClassify({
|
|
4678
|
+
system: "Decide whether a cloud-agent session log shows a completed implementation PLAN, not code or execution.",
|
|
4679
|
+
user: `Log excerpt:\n${logExcerpt}`,
|
|
4680
|
+
schemaHint: "{\"planReady\":boolean,\"planExcerpt\":\"<=1200 chars from the completed plan, or empty\",\"confidence\":number}",
|
|
4681
|
+
maxTokens: 500,
|
|
4682
|
+
validate(value) {
|
|
4683
|
+
if (!isRecord$1(value)) return null;
|
|
4684
|
+
const planReady = booleanValue$1(value.planReady);
|
|
4685
|
+
const planExcerpt = stringValue$1(value.planExcerpt);
|
|
4686
|
+
if (planReady === null || planExcerpt === null) return null;
|
|
4687
|
+
return {
|
|
4688
|
+
planReady,
|
|
4689
|
+
planExcerpt: planExcerpt.slice(0, 1200)
|
|
4690
|
+
};
|
|
4691
|
+
}
|
|
4692
|
+
}))?.value ?? null;
|
|
4693
|
+
}
|
|
4694
|
+
async function classifyQuestionAnswerable(question, acceptanceCriteria) {
|
|
4695
|
+
return (await microClassify({
|
|
4696
|
+
system: "Decide if the agent's question is answerable purely from the acceptance criteria. If yes, answer it tersely.",
|
|
4697
|
+
user: `Acceptance criteria:\n${acceptanceCriteria}\n\nAgent question:\n${question}`,
|
|
4698
|
+
schemaHint: "{\"answerable\":boolean,\"answer\":\"present only when answerable\",\"confidence\":number}",
|
|
4699
|
+
validate(value) {
|
|
4700
|
+
if (!isRecord$1(value)) return null;
|
|
4701
|
+
const answerable = booleanValue$1(value.answerable);
|
|
4702
|
+
if (answerable === null) return null;
|
|
4703
|
+
const answer = stringValue$1(value.answer);
|
|
4704
|
+
if (!answerable) return { answerable };
|
|
4705
|
+
return answer === null ? null : {
|
|
4706
|
+
answerable,
|
|
4707
|
+
answer
|
|
4708
|
+
};
|
|
4709
|
+
}
|
|
4710
|
+
}))?.value ?? null;
|
|
4711
|
+
}
|
|
4712
|
+
async function classifyFixAddressed(failureSummary, latestLogExcerpt) {
|
|
4713
|
+
return (await microClassify({
|
|
4714
|
+
system: "Decide whether the latest cloud-agent log indicates the summarized failure was addressed.",
|
|
4715
|
+
user: `Failure summary:\n${failureSummary}\n\nLatest log excerpt:\n${latestLogExcerpt}`,
|
|
4716
|
+
schemaHint: "{\"addressed\":boolean,\"confidence\":number}",
|
|
4717
|
+
validate(value) {
|
|
4718
|
+
if (!isRecord$1(value)) return null;
|
|
4719
|
+
const addressed = booleanValue$1(value.addressed);
|
|
4720
|
+
return addressed === null ? null : { addressed };
|
|
4721
|
+
}
|
|
4722
|
+
}))?.value ?? null;
|
|
4723
|
+
}
|
|
4724
|
+
async function classifyStuck(logExcerpt) {
|
|
4725
|
+
return (await microClassify({
|
|
4726
|
+
system: "Decide whether the cloud-agent log shows the agent is stuck, looping, blocked, or unable to proceed.",
|
|
4727
|
+
user: `Log excerpt:\n${logExcerpt}`,
|
|
4728
|
+
schemaHint: "{\"stuck\":boolean,\"confidence\":number}",
|
|
4729
|
+
validate(value) {
|
|
4730
|
+
if (!isRecord$1(value)) return null;
|
|
4731
|
+
const stuck = booleanValue$1(value.stuck);
|
|
4732
|
+
return stuck === null ? null : { stuck };
|
|
4733
|
+
}
|
|
4734
|
+
}))?.value ?? null;
|
|
4735
|
+
}
|
|
4736
|
+
|
|
4737
|
+
//#endregion
|
|
4738
|
+
//#region src/lib/first-mate/decision-packet.ts
|
|
4739
|
+
const HTML_ESCAPES = {
|
|
4740
|
+
"&": "&",
|
|
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)}`);
|
|
3142
5889
|
}
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
} else if (localSessionIdsByInstance.size > 0) instances = uniqueInstances(await Promise.all([...localSessionIdsByInstance.keys()].map((instanceId) => registry.resolveInstance(instanceId))));
|
|
3149
|
-
else {
|
|
3150
|
-
const infos = await registry.listInstances();
|
|
3151
|
-
if (infos.length === 0) throw new FleetRegistryError("INSTANCE_REQUIRED", "await_turn requires at least one registered fleet instance");
|
|
3152
|
-
instances = uniqueInstances(await Promise.all(infos.map((info) => registry.resolveInstance(info.id))));
|
|
5890
|
+
for (const decision of input.humanDecisions ?? []) try {
|
|
5891
|
+
await applyHumanDecision(decision, units, deps, applied);
|
|
5892
|
+
} catch (err) {
|
|
5893
|
+
consola.warn(`first-mate: human decision ${decision.requestId} failed to apply:`, err);
|
|
5894
|
+
applied.push(`error applying decision ${decision.requestId}: ${errText(err)}`);
|
|
3153
5895
|
}
|
|
3154
|
-
return {
|
|
3155
|
-
instances,
|
|
3156
|
-
localSessionIdsByInstance
|
|
3157
|
-
};
|
|
3158
5896
|
}
|
|
3159
|
-
|
|
5897
|
+
/** Parse an "owner/name" repo string into a RepoRef. */
|
|
5898
|
+
function parseRepoRef$1(value) {
|
|
5899
|
+
if (value === void 0) return void 0;
|
|
5900
|
+
const parts = value.split("/");
|
|
5901
|
+
if (parts.length !== 2 || parts[0].length === 0 || parts[1].length === 0) return;
|
|
3160
5902
|
return {
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
};
|
|
5903
|
+
owner: parts[0],
|
|
5904
|
+
name: parts[1]
|
|
5905
|
+
};
|
|
5906
|
+
}
|
|
5907
|
+
function asAgentKey(value) {
|
|
5908
|
+
return value === "copilot" || value === "anthropic" || value === "openai" ? value : void 0;
|
|
5909
|
+
}
|
|
5910
|
+
/**
|
|
5911
|
+
* Turn a model `decompose` answer into queued units. This is the mission→units
|
|
5912
|
+
* step: `start_mission` only registers the mission; `advance` emits one
|
|
5913
|
+
* `decompose` request per unit-less active mission, and the model answers with
|
|
5914
|
+
* `{ units: [{ title, repo?, agent?, dependsOn? }] }`. Each unit gets a stable
|
|
5915
|
+
* `id` so it survives the queued→dispatched transition without duplicating.
|
|
5916
|
+
*/
|
|
5917
|
+
async function applyDecomposeAnswer(answer, missions, deps, applied) {
|
|
5918
|
+
const missionId = answer.requestId.slice(10);
|
|
5919
|
+
const mission = missions.find((m) => m.id === missionId);
|
|
5920
|
+
if (mission === void 0) return;
|
|
5921
|
+
const verdict = asRecord$1(answer.verdict) ?? {};
|
|
5922
|
+
const rawUnits = Array.isArray(verdict.units) ? verdict.units : [];
|
|
5923
|
+
const specs = [];
|
|
5924
|
+
for (const raw of rawUnits) {
|
|
5925
|
+
const spec = asRecord$1(raw) ?? {};
|
|
5926
|
+
const title = stringValue(spec.title);
|
|
5927
|
+
if (title === void 0 || title.length === 0) continue;
|
|
5928
|
+
const repo = parseRepoRef$1(stringValue(spec.repo)) ?? mission.repos[0];
|
|
5929
|
+
if (repo === void 0) continue;
|
|
5930
|
+
specs.push({
|
|
5931
|
+
spec,
|
|
5932
|
+
title,
|
|
5933
|
+
repo
|
|
5934
|
+
});
|
|
5935
|
+
}
|
|
5936
|
+
const ids = specs.map(() => randomUUID());
|
|
5937
|
+
let created = 0;
|
|
5938
|
+
for (let i = 0; i < specs.length; i += 1) {
|
|
5939
|
+
const { spec, title, repo } = specs[i];
|
|
5940
|
+
const dependsOn = (Array.isArray(spec.dependsOn) ? spec.dependsOn : []).filter((idx) => typeof idx === "number" && Number.isInteger(idx) && idx >= 0 && idx < ids.length && idx !== i).map((idx) => ids[idx]);
|
|
5941
|
+
const unit = {
|
|
5942
|
+
id: ids[i],
|
|
5943
|
+
missionId,
|
|
5944
|
+
repo,
|
|
5945
|
+
issue: null,
|
|
5946
|
+
pr: null,
|
|
5947
|
+
taskId: null,
|
|
5948
|
+
agent: asAgentKey(stringValue(spec.agent)) ?? "copilot",
|
|
5949
|
+
botLogin: "",
|
|
5950
|
+
dispatchMode: "plan",
|
|
5951
|
+
provider: "none",
|
|
5952
|
+
phase: "plan",
|
|
5953
|
+
artifact: "no_pr",
|
|
5954
|
+
validation: "unknown",
|
|
5955
|
+
retries: 0,
|
|
5956
|
+
dependsOn,
|
|
5957
|
+
title
|
|
5958
|
+
};
|
|
5959
|
+
await deps.upsertUnit(repo, unit);
|
|
5960
|
+
created += 1;
|
|
5961
|
+
}
|
|
5962
|
+
if (created > 0) applied.push(`decomposed ${missionId} into ${created} unit(s)`);
|
|
5963
|
+
}
|
|
5964
|
+
async function maybeMergeWithApproval(unit, observed, evidence, deps, applied) {
|
|
5965
|
+
if (unit.validation !== "floor_passed" && observed.floor !== "passed") return false;
|
|
5966
|
+
const pr = unit.pr ?? (observed.prs.length === 1 ? observed.prs[0]?.number ?? null : null);
|
|
5967
|
+
if (pr === null) return false;
|
|
5968
|
+
const live = await deps.getPullRequestState(agentRepo(unit.repo), pr);
|
|
5969
|
+
unit.pr = live.number;
|
|
5970
|
+
unit.headSha = live.headSha || unit.headSha;
|
|
5971
|
+
unit.baseSha = live.baseSha ?? unit.baseSha;
|
|
5972
|
+
unit.branch = live.baseRef || unit.branch;
|
|
5973
|
+
if (unit.floorSha != null && unit.floorSha.length > 0 && live.headSha.length > 0 && live.headSha !== unit.floorSha) return false;
|
|
5974
|
+
const head = live.headSha.length > 0 ? live.headSha : unit.headSha ?? void 0;
|
|
5975
|
+
if (head === void 0 || head.length === 0) return false;
|
|
5976
|
+
if (!(await deps.verifyAndConsumeApproval({
|
|
5977
|
+
repo: unit.repo,
|
|
5978
|
+
pr: live.number,
|
|
5979
|
+
liveHeadSha: head,
|
|
5980
|
+
liveBaseSha: live.baseSha
|
|
5981
|
+
})).ok) return false;
|
|
5982
|
+
if (live.isDraft && evidence.prNodeId !== void 0) await deps.markReadyForReview(evidence.prNodeId);
|
|
5983
|
+
await deps.mergePullRequest(agentRepo(unit.repo), {
|
|
5984
|
+
pr: live.number,
|
|
5985
|
+
expectedHeadSha: head
|
|
5986
|
+
});
|
|
5987
|
+
unit.terminal = true;
|
|
5988
|
+
unit.phase = "done";
|
|
5989
|
+
unit.artifact = "pr_merged";
|
|
5990
|
+
unit.validation = "floor_passed";
|
|
5991
|
+
applied.push(`merged ${repoLabel$1(unit.repo)}#${live.number}`);
|
|
5992
|
+
await deps.upsertUnit(unit.repo, unit);
|
|
5993
|
+
return true;
|
|
3164
5994
|
}
|
|
3165
|
-
function
|
|
5995
|
+
async function createHumanRequest(unit, mission, observed, reason, deps) {
|
|
5996
|
+
const { decisionKey, fingerprint, type } = decisionKeyFor(unit, observed, reason);
|
|
5997
|
+
const existing = await deps.findByKey(decisionKey);
|
|
5998
|
+
let record = existing?.status === "pending" ? existing : void 0;
|
|
5999
|
+
let packetHtmlPath;
|
|
6000
|
+
if (record === void 0) {
|
|
6001
|
+
const packet = deps.buildDecisionPacket(packetInput(unit, mission, observed, reason, type));
|
|
6002
|
+
packetHtmlPath = await deps.writeDecisionPacketHtml(packet.packetId, packet.html);
|
|
6003
|
+
record = {
|
|
6004
|
+
decisionId: packet.decisionId,
|
|
6005
|
+
decisionKey,
|
|
6006
|
+
type,
|
|
6007
|
+
status: "pending",
|
|
6008
|
+
packetId: packet.packetId,
|
|
6009
|
+
inputFingerprint: fingerprint,
|
|
6010
|
+
options: decisionOptions(type).map((option) => ({ id: option.id })),
|
|
6011
|
+
createdMs: Date.now()
|
|
6012
|
+
};
|
|
6013
|
+
await deps.upsertDecision(record);
|
|
6014
|
+
}
|
|
6015
|
+
unit.blockingDecisionId = record.decisionId;
|
|
3166
6016
|
return {
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
6017
|
+
requestId: decisionKey,
|
|
6018
|
+
decisionId: record.decisionId,
|
|
6019
|
+
missionId: unit.missionId,
|
|
6020
|
+
repo: unit.repo,
|
|
6021
|
+
issue: unit.issue,
|
|
6022
|
+
pr: unit.pr,
|
|
6023
|
+
reason,
|
|
6024
|
+
...packetHtmlPath !== void 0 ? { packetHtmlPath } : {}
|
|
3170
6025
|
};
|
|
3171
6026
|
}
|
|
3172
|
-
function
|
|
3173
|
-
if (
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
6027
|
+
async function assignVerifier(unit, deps, applied) {
|
|
6028
|
+
if (unit.pr === null) return false;
|
|
6029
|
+
await deps.requestReview(agentRepo(unit.repo), unit.pr, COPILOT_REVIEWER_LOGIN);
|
|
6030
|
+
unit.verifierAssigned = true;
|
|
6031
|
+
unit.verifierSha = unit.headSha ?? void 0;
|
|
6032
|
+
unit.validation = "floor_pending";
|
|
6033
|
+
unit.lastSteer = { atMs: Date.now() };
|
|
6034
|
+
applied.push(`requested Copilot code review for ${unit.missionId}:${unitHandle(unit)} PR #${unit.pr}`);
|
|
6035
|
+
return true;
|
|
6036
|
+
}
|
|
6037
|
+
async function executeAction(action, unit, mission, observed, evidence, policy, deps, needsModel, needsHuman, applied, order) {
|
|
6038
|
+
switch (action.kind) {
|
|
6039
|
+
case "dispatch": return;
|
|
6040
|
+
case "steer":
|
|
6041
|
+
consola.debug("first-mate controller received direct steer action; v1 skips it");
|
|
6042
|
+
return;
|
|
6043
|
+
case "assign_verifier":
|
|
6044
|
+
if (await assignVerifier(unit, deps, applied)) return;
|
|
6045
|
+
needsHuman.push({
|
|
6046
|
+
request: await createHumanRequest(unit, mission, observed, "no different-lab verifier is available", deps),
|
|
6047
|
+
sortKey: sortKey(unit),
|
|
6048
|
+
order
|
|
6049
|
+
});
|
|
6050
|
+
return;
|
|
6051
|
+
case "rerun_ci":
|
|
6052
|
+
if (evidence.runId !== void 0) {
|
|
6053
|
+
await deps.rerunChecks(agentRepo(unit.repo), {
|
|
6054
|
+
runId: evidence.runId,
|
|
6055
|
+
failedOnly: true
|
|
6056
|
+
});
|
|
6057
|
+
applied.push(`reran checks for ${unit.missionId}:${unitHandle(unit)}`);
|
|
6058
|
+
}
|
|
6059
|
+
return;
|
|
6060
|
+
case "cancel":
|
|
6061
|
+
if (unit.taskId !== null) await deps.cancelTask(agentRepo(unit.repo), unit.taskId);
|
|
6062
|
+
unit.terminal = true;
|
|
6063
|
+
unit.phase = "done";
|
|
6064
|
+
unit.cancelledBy = "controller";
|
|
6065
|
+
applied.push(`cancelled ${unit.missionId}:${unitHandle(unit)}`);
|
|
6066
|
+
return;
|
|
6067
|
+
case "mark_done":
|
|
6068
|
+
unit.terminal = true;
|
|
6069
|
+
unit.phase = "done";
|
|
6070
|
+
applied.push(`marked done ${unit.missionId}:${unitHandle(unit)}`);
|
|
6071
|
+
return;
|
|
6072
|
+
case "ask_model":
|
|
6073
|
+
needsModel.push({
|
|
6074
|
+
request: buildModelRequest(unit, mission, action.request, observed, evidence),
|
|
6075
|
+
sortKey: sortKey(unit),
|
|
6076
|
+
order
|
|
6077
|
+
});
|
|
6078
|
+
return;
|
|
6079
|
+
case "escalate_human":
|
|
6080
|
+
needsHuman.push({
|
|
6081
|
+
request: await createHumanRequest(unit, mission, observed, action.reason, deps),
|
|
6082
|
+
sortKey: sortKey(unit),
|
|
6083
|
+
order
|
|
6084
|
+
});
|
|
6085
|
+
return;
|
|
6086
|
+
case "merge":
|
|
6087
|
+
case "mark_rebase":
|
|
6088
|
+
case "noop": return;
|
|
3177
6089
|
}
|
|
3178
|
-
return 0;
|
|
3179
6090
|
}
|
|
3180
|
-
function
|
|
3181
|
-
|
|
3182
|
-
const atB = eventAtMs(b.at);
|
|
3183
|
-
if (atA !== atB) return atA - atB;
|
|
3184
|
-
return (typeof a.seq === "number" ? a.seq : 0) - (typeof b.seq === "number" ? b.seq : 0);
|
|
6091
|
+
function isUndispatched(unit) {
|
|
6092
|
+
return unit.provider === "none" && unit.taskId === null && unit.dispatch === void 0;
|
|
3185
6093
|
}
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
const id = watcherId ?? "default";
|
|
3190
|
-
return id.length > MAX_WATCHER_ID_LEN ? id.slice(0, MAX_WATCHER_ID_LEN) : id;
|
|
6094
|
+
/** A dispatch that was interrupted mid-flight (intent persisted, no taskId yet). */
|
|
6095
|
+
function isDispatchInterrupted(unit) {
|
|
6096
|
+
return unit.dispatch !== void 0 && unit.taskId === null;
|
|
3191
6097
|
}
|
|
3192
|
-
function
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
6098
|
+
function isActiveMissionUnit(unit, missions) {
|
|
6099
|
+
return missions.get(unit.missionId)?.status === "active";
|
|
6100
|
+
}
|
|
6101
|
+
function isActiveUnit(unit, missions) {
|
|
6102
|
+
return isActiveMissionUnit(unit, missions) && unit.terminal !== true;
|
|
6103
|
+
}
|
|
6104
|
+
function isInFlight(unit) {
|
|
6105
|
+
return unit.terminal !== true && unit.taskId !== null && (unit.provider === "queued" || unit.provider === "in_progress" || unit.provider === "waiting_for_user");
|
|
6106
|
+
}
|
|
6107
|
+
function depsSatisfied(unit, units) {
|
|
6108
|
+
return unit.dependsOn.every((depId) => units.some((candidate) => candidate.id === depId && candidate.terminal === true && candidate.artifact === "pr_merged"));
|
|
6109
|
+
}
|
|
6110
|
+
function activeCountsByAgent(units) {
|
|
6111
|
+
const counts = /* @__PURE__ */ new Map();
|
|
6112
|
+
for (const unit of units) {
|
|
6113
|
+
if (!isInFlight(unit)) continue;
|
|
6114
|
+
counts.set(unit.agent, (counts.get(unit.agent) ?? 0) + 1);
|
|
3198
6115
|
}
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
6116
|
+
return counts;
|
|
6117
|
+
}
|
|
6118
|
+
function planPrompt(unit, mission) {
|
|
6119
|
+
const parts = [
|
|
6120
|
+
`Mission goal:\n${mission.goal}`,
|
|
6121
|
+
`Acceptance criteria:\n${mission.acceptanceCriteria}`,
|
|
6122
|
+
`Work unit:\n${unit.title}`,
|
|
6123
|
+
"Analyze the repository and produce a concrete, step-by-step implementation plan for this work unit: the files you will change, the approach, key risks, and how each acceptance criterion will be verified. Do NOT edit code or open a pull request yet — output the plan and stop. It will be reviewed before implementation."
|
|
6124
|
+
];
|
|
6125
|
+
if (mission.houseRules !== void 0) parts.splice(2, 0, `House rules:\n${mission.houseRules}`);
|
|
6126
|
+
return parts.join("\n\n");
|
|
6127
|
+
}
|
|
6128
|
+
function buildPrompt(unit, mission) {
|
|
6129
|
+
const parts = [
|
|
6130
|
+
`Mission goal:\n${mission.goal}`,
|
|
6131
|
+
`Acceptance criteria:\n${mission.acceptanceCriteria}`,
|
|
6132
|
+
`Work unit:\n${unit.title}`
|
|
6133
|
+
];
|
|
6134
|
+
if (mission.houseRules !== void 0) parts.push(`House rules:\n${mission.houseRules}`);
|
|
6135
|
+
if (unit.planExcerpt !== void 0 && unit.planExcerpt.trim().length > 0) parts.push(`Approved plan (implement this):\n${unit.planExcerpt.trim()}`);
|
|
6136
|
+
parts.push("Implement this work unit end-to-end on a new branch and open a pull request for review. Follow the approved plan above. Keep the change focused on this unit and do not modify unrelated files. If anything about the acceptance criteria is ambiguous, make a reasonable choice and note it in the PR description.");
|
|
6137
|
+
return parts.join("\n\n");
|
|
6138
|
+
}
|
|
6139
|
+
/**
|
|
6140
|
+
* Durable dispatch outbox. Persists a dispatch-intent (with a correlation id)
|
|
6141
|
+
* BEFORE the irreversible startTask, so a crash in the startTask→persist window
|
|
6142
|
+
* leaves the unit marked (not `isUndispatched`) and is never blind-re-dispatched.
|
|
6143
|
+
* `start` receives the correlation id to embed in the prompt and send as the
|
|
6144
|
+
* Idempotency-Key. On success the intent is cleared (in memory; the caller's
|
|
6145
|
+
* upsertUnit persists it alongside the taskId). On throw the intent stays
|
|
6146
|
+
* pending on disk (unknown outcome — recovery, not re-dispatch, resolves it).
|
|
6147
|
+
* Returns null when the API returned no taskId — treated as ambiguous, so the
|
|
6148
|
+
* intent is LEFT pending (recovery escalates; never auto-re-dispatch).
|
|
6149
|
+
*/
|
|
6150
|
+
async function dispatchWithOutbox(unit, deps, start) {
|
|
6151
|
+
const id = randomUUID();
|
|
6152
|
+
unit.dispatch = {
|
|
6153
|
+
id,
|
|
6154
|
+
requestedMs: Date.now(),
|
|
6155
|
+
attempts: (unit.dispatch?.attempts ?? 0) + 1
|
|
6156
|
+
};
|
|
6157
|
+
await deps.upsertUnit(unit.repo, unit);
|
|
6158
|
+
const task = await start({
|
|
6159
|
+
idempotencyKey: id,
|
|
6160
|
+
promptTag: `\n\n<!-- fm-dispatch:${id} -->`
|
|
6161
|
+
});
|
|
6162
|
+
if (task.taskId.length === 0) return null;
|
|
6163
|
+
unit.dispatch = void 0;
|
|
6164
|
+
return task;
|
|
6165
|
+
}
|
|
6166
|
+
async function dispatchUnit(unit, mission, deps) {
|
|
6167
|
+
const repo = agentRepo(unit.repo);
|
|
6168
|
+
const actor = await deps.resolveAgentActor(repo, unit.agent);
|
|
6169
|
+
const task = await dispatchWithOutbox(unit, deps, ({ idempotencyKey, promptTag }) => deps.startTask(repo, {
|
|
6170
|
+
prompt: planPrompt(unit, mission) + promptTag,
|
|
6171
|
+
createPullRequest: false,
|
|
6172
|
+
idempotencyKey
|
|
6173
|
+
}));
|
|
6174
|
+
if (task === null) return;
|
|
6175
|
+
unit.taskId = task.taskId;
|
|
6176
|
+
unit.provider = providerState(task.state, "queued");
|
|
6177
|
+
unit.botLogin = actor.login;
|
|
6178
|
+
unit.dispatchMode = "plan";
|
|
6179
|
+
unit.phase = "plan";
|
|
6180
|
+
unit.implementerLab = unit.agent;
|
|
6181
|
+
unit.lastSteer = { atMs: Date.now() };
|
|
6182
|
+
}
|
|
6183
|
+
async function dispatchWave(units, missions, maxInFlightPerProvider, deps, applied) {
|
|
6184
|
+
const counts = activeCountsByAgent(units);
|
|
6185
|
+
const candidates = units.filter((unit) => isActiveUnit(unit, missions)).filter((unit) => !unit.blockingDecisionId).filter(isUndispatched).filter((unit) => depsSatisfied(unit, units)).map((unit, index) => ({
|
|
6186
|
+
unit,
|
|
6187
|
+
index
|
|
6188
|
+
})).sort((a, b) => sortKey(a.unit) - sortKey(b.unit) || a.index - b.index);
|
|
6189
|
+
for (const { unit } of candidates) {
|
|
6190
|
+
const current = counts.get(unit.agent) ?? 0;
|
|
6191
|
+
if (current >= maxInFlightPerProvider) continue;
|
|
6192
|
+
const mission = missions.get(unit.missionId);
|
|
6193
|
+
if (mission === void 0) continue;
|
|
6194
|
+
try {
|
|
6195
|
+
await dispatchUnit(unit, mission, deps);
|
|
6196
|
+
counts.set(unit.agent, current + 1);
|
|
6197
|
+
await deps.upsertUnit(unit.repo, unit);
|
|
6198
|
+
applied.push(`dispatched ${unit.missionId}:${unitHandle(unit)} to ${unit.agent}`);
|
|
6199
|
+
} catch (err) {
|
|
6200
|
+
consola.warn(`first-mate: dispatch of ${unit.missionId}:${unitHandle(unit)} failed:`, err);
|
|
6201
|
+
applied.push(`error dispatching ${unit.missionId}:${unitHandle(unit)}: ${errText(err)}`);
|
|
6202
|
+
}
|
|
6203
|
+
}
|
|
6204
|
+
}
|
|
6205
|
+
function buildBoard$1(units, missions) {
|
|
6206
|
+
const rows = [];
|
|
6207
|
+
for (const mission of missions.filter((entry) => entry.status === "active")) {
|
|
6208
|
+
const missionUnits = units.filter((unit) => unit.missionId === mission.id);
|
|
6209
|
+
const counts = {};
|
|
6210
|
+
for (const unit of missionUnits) counts[unit.phase] = (counts[unit.phase] ?? 0) + 1;
|
|
6211
|
+
rows.push({
|
|
6212
|
+
missionId: mission.id,
|
|
6213
|
+
title: mission.goal,
|
|
6214
|
+
repos: mission.repos.map(repoLabel$1),
|
|
6215
|
+
counts,
|
|
6216
|
+
blocked: missionUnits.filter((unit) => unit.blockingDecisionId).length
|
|
6217
|
+
});
|
|
3205
6218
|
}
|
|
3206
|
-
return
|
|
6219
|
+
return rows;
|
|
3207
6220
|
}
|
|
3208
|
-
function
|
|
3209
|
-
return
|
|
6221
|
+
function compareQueued(a, b) {
|
|
6222
|
+
return a.sortKey - b.sortKey || a.order - b.order;
|
|
3210
6223
|
}
|
|
3211
|
-
function
|
|
3212
|
-
return
|
|
6224
|
+
function capQueued(entries, topK) {
|
|
6225
|
+
return entries.sort(compareQueued).slice(0, topK).map((entry) => entry.request);
|
|
3213
6226
|
}
|
|
3214
|
-
function
|
|
3215
|
-
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);
|
|
3216
6332
|
return {
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
6333
|
+
board,
|
|
6334
|
+
needsModel: capQueued(needsModel, topK),
|
|
6335
|
+
needsHuman: capQueued(needsHuman, topK),
|
|
6336
|
+
applied,
|
|
6337
|
+
nextWakeAt: wakeAt,
|
|
6338
|
+
nextWakeSeconds: wakeSeconds(wakeAt)
|
|
3222
6339
|
};
|
|
3223
6340
|
}
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
6341
|
+
|
|
6342
|
+
//#endregion
|
|
6343
|
+
//#region src/lib/first-mate/tools.ts
|
|
6344
|
+
const FIRST_MATE_GROUP = "first-mate";
|
|
6345
|
+
var FirstMateToolInputError = class extends Error {
|
|
6346
|
+
code;
|
|
6347
|
+
constructor(code, message) {
|
|
6348
|
+
super(message);
|
|
6349
|
+
this.name = "FirstMateToolInputError";
|
|
6350
|
+
this.code = code;
|
|
3234
6351
|
}
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
6352
|
+
};
|
|
6353
|
+
function createFirstMateTools() {
|
|
6354
|
+
function tool$1(toolNameHttp, description, inputSchema, handler) {
|
|
6355
|
+
return {
|
|
6356
|
+
toolNameHttp,
|
|
6357
|
+
group: FIRST_MATE_GROUP,
|
|
6358
|
+
description,
|
|
6359
|
+
inputSchema,
|
|
6360
|
+
capability: "agents",
|
|
6361
|
+
async handler(args, signal) {
|
|
6362
|
+
if (!hasAgentToken()) return errorResult(new FirstMateToolInputError("AGENT_TOKEN_REQUIRED", "first-mate tools require --agents or GH_ROUTER_ENABLE_AGENTS=1 with a GitHub agent token"));
|
|
6363
|
+
try {
|
|
6364
|
+
return await handler(args, signal);
|
|
6365
|
+
} catch (err) {
|
|
6366
|
+
return errorResult(err);
|
|
6367
|
+
}
|
|
6368
|
+
}
|
|
6369
|
+
};
|
|
6370
|
+
}
|
|
6371
|
+
return Object.freeze([
|
|
6372
|
+
tool$1("start_mission", "Register a first-mate mission for one or more GitHub repositories. Unit decomposition is handled by later controller/model wakes.", objectSchema({
|
|
6373
|
+
goal: stringProp("Mission goal."),
|
|
6374
|
+
repos: stringArrayProp("Repositories as owner/name strings."),
|
|
6375
|
+
acceptance_criteria: stringProp("User-blessed acceptance criteria for the mission."),
|
|
6376
|
+
priority: numberProp("Optional numeric priority; higher values are handled by controller policy."),
|
|
6377
|
+
house_rules: stringProp("Optional repository or operator constraints.")
|
|
6378
|
+
}, [
|
|
6379
|
+
"goal",
|
|
6380
|
+
"repos",
|
|
6381
|
+
"acceptance_criteria"
|
|
6382
|
+
]), async (args) => {
|
|
6383
|
+
const repos = requiredStringArray(args, "repos").map(parseRepoRef);
|
|
6384
|
+
const now = Date.now();
|
|
6385
|
+
const missionId = randomUUID();
|
|
6386
|
+
await upsertMission({
|
|
6387
|
+
id: missionId,
|
|
6388
|
+
goal: requiredString(args, "goal"),
|
|
6389
|
+
acceptanceCriteria: requiredString(args, "acceptance_criteria"),
|
|
6390
|
+
houseRules: optionalString(args, "house_rules"),
|
|
6391
|
+
priority: optionalNumber(args, "priority"),
|
|
6392
|
+
repos,
|
|
6393
|
+
status: "active",
|
|
6394
|
+
createdMs: now,
|
|
6395
|
+
updatedMs: now
|
|
6396
|
+
});
|
|
6397
|
+
return ok({
|
|
6398
|
+
missionId,
|
|
6399
|
+
repos
|
|
6400
|
+
});
|
|
6401
|
+
}),
|
|
6402
|
+
tool$1("advance", "Wake the first-mate controller once, applying model answers or human decisions, then return the compact board and pending requests.", objectSchema({
|
|
6403
|
+
model_answers: arrayOfObjectsProp("Optional model judgments to apply before the wake.", {
|
|
6404
|
+
requestId: stringProp("Request id from a previous needsModel entry."),
|
|
6405
|
+
verdict: anyProp("Structured verdict for the request kind.")
|
|
6406
|
+
}, ["requestId", "verdict"]),
|
|
6407
|
+
human_decisions: arrayOfObjectsProp("Optional human choices to apply before the wake.", {
|
|
6408
|
+
requestId: stringProp("Request id from a previous needsHuman entry."),
|
|
6409
|
+
choice: stringProp("Chosen option id or short decision text.")
|
|
6410
|
+
}, ["requestId", "choice"]),
|
|
6411
|
+
top_k: numberProp("Maximum model and human requests to return."),
|
|
6412
|
+
max_in_flight_per_provider: numberProp("Maximum active units per cloud-agent provider.")
|
|
6413
|
+
}, []), async (args) => {
|
|
6414
|
+
const result = await advance({
|
|
6415
|
+
modelAnswers: optionalModelAnswers(args),
|
|
6416
|
+
humanDecisions: optionalHumanDecisions(args),
|
|
6417
|
+
topK: optionalNumber(args, "top_k"),
|
|
6418
|
+
maxInFlightPerProvider: optionalNumber(args, "max_in_flight_per_provider")
|
|
6419
|
+
});
|
|
6420
|
+
return ok({
|
|
6421
|
+
board: result.board,
|
|
6422
|
+
needsModel: result.needsModel,
|
|
6423
|
+
needsHuman: result.needsHuman,
|
|
6424
|
+
applied_count: result.applied.length,
|
|
6425
|
+
nextWakeAt: result.nextWakeAt,
|
|
6426
|
+
nextWakeSeconds: result.nextWakeSeconds
|
|
6427
|
+
});
|
|
6428
|
+
}),
|
|
6429
|
+
tool$1("board", "Read the first-mate board without waking the controller.", objectSchema({}, []), async () => {
|
|
6430
|
+
const [missions, units] = await Promise.all([readMissions(), loadAllUnits()]);
|
|
6431
|
+
return ok({ board: buildBoard(missions, units) });
|
|
6432
|
+
}),
|
|
6433
|
+
tool$1("mission_status", "Read compact status for all first-mate missions, or for one mission id.", objectSchema({ mission_id: stringProp("Optional mission id to filter to.") }, []), async (args) => {
|
|
6434
|
+
const [missions, units] = await Promise.all([readMissions(), loadAllUnits()]);
|
|
6435
|
+
return ok({ missions: buildMissionStatus(missions, units, optionalString(args, "mission_id")) });
|
|
6436
|
+
})
|
|
6437
|
+
]);
|
|
3246
6438
|
}
|
|
3247
|
-
|
|
3248
|
-
|
|
6439
|
+
const FIRST_MATE_TOOLS = createFirstMateTools();
|
|
6440
|
+
function hasAgentToken() {
|
|
6441
|
+
return typeof state.githubAgentToken === "string" && state.githubAgentToken.length > 0;
|
|
3249
6442
|
}
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
6443
|
+
function buildBoard(missions, units) {
|
|
6444
|
+
const unitsByMission = groupUnitsByMission(units);
|
|
6445
|
+
return missions.filter((mission) => mission.status === "active").map((mission) => {
|
|
6446
|
+
const missionUnits = unitsByMission.get(mission.id) ?? [];
|
|
6447
|
+
return {
|
|
6448
|
+
missionId: mission.id,
|
|
6449
|
+
title: mission.goal,
|
|
6450
|
+
repos: mission.repos.map(repoLabel),
|
|
6451
|
+
counts: countsByPhase(missionUnits),
|
|
6452
|
+
blocked: blockedCount(missionUnits)
|
|
6453
|
+
};
|
|
6454
|
+
});
|
|
3253
6455
|
}
|
|
3254
|
-
function
|
|
3255
|
-
|
|
3256
|
-
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
|
+
});
|
|
3257
6468
|
}
|
|
3258
|
-
function
|
|
3259
|
-
const
|
|
3260
|
-
const
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
result.push(instance);
|
|
6469
|
+
function groupUnitsByMission(units) {
|
|
6470
|
+
const result = /* @__PURE__ */ new Map();
|
|
6471
|
+
for (const unit of units) {
|
|
6472
|
+
const missionUnits = result.get(unit.missionId) ?? [];
|
|
6473
|
+
missionUnits.push(unit);
|
|
6474
|
+
result.set(unit.missionId, missionUnits);
|
|
3265
6475
|
}
|
|
3266
6476
|
return result;
|
|
3267
6477
|
}
|
|
3268
|
-
function
|
|
6478
|
+
function countsByPhase(units) {
|
|
6479
|
+
const counts = {};
|
|
6480
|
+
for (const unit of units) counts[unit.phase] = (counts[unit.phase] ?? 0) + 1;
|
|
6481
|
+
return counts;
|
|
6482
|
+
}
|
|
6483
|
+
function blockedCount(units) {
|
|
6484
|
+
return units.filter((unit) => Boolean(unit.blockingDecisionId)).length;
|
|
6485
|
+
}
|
|
6486
|
+
function repoLabel(repo) {
|
|
6487
|
+
return `${repo.owner}/${repo.name}`;
|
|
6488
|
+
}
|
|
6489
|
+
function parseRepoRef(value) {
|
|
6490
|
+
const parts = value.trim().split("/");
|
|
6491
|
+
if (parts.length !== 2 || parts[0] === void 0 || parts[1] === void 0 || parts[0].trim() === "" || parts[1].trim() === "") throw new FirstMateToolInputError("INVALID_ARGUMENT", `arguments.repos entries must be owner/name strings; got ${JSON.stringify(value)}`);
|
|
3269
6492
|
return {
|
|
3270
|
-
|
|
3271
|
-
|
|
6493
|
+
owner: parts[0].trim(),
|
|
6494
|
+
name: parts[1].trim()
|
|
3272
6495
|
};
|
|
3273
6496
|
}
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
*/
|
|
3280
|
-
function tunnelClientOptions(instance, provider) {
|
|
3281
|
-
if (instance.tunnelId) {
|
|
3282
|
-
const cfg = { tunnelId: instance.tunnelId };
|
|
6497
|
+
function optionalModelAnswers(args) {
|
|
6498
|
+
const entries = optionalRecordArray(args, "model_answers");
|
|
6499
|
+
if (entries === void 0) return void 0;
|
|
6500
|
+
return entries.map((entry) => {
|
|
6501
|
+
if (!Object.prototype.hasOwnProperty.call(entry, "verdict")) throw new FirstMateToolInputError("INVALID_ARGUMENT", "arguments.model_answers entries must include verdict");
|
|
3283
6502
|
return {
|
|
3284
|
-
|
|
3285
|
-
|
|
6503
|
+
requestId: requiredString(entry, "requestId"),
|
|
6504
|
+
verdict: entry.verdict
|
|
3286
6505
|
};
|
|
3287
|
-
}
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
return {
|
|
6506
|
+
});
|
|
6507
|
+
}
|
|
6508
|
+
function optionalHumanDecisions(args) {
|
|
6509
|
+
const entries = optionalRecordArray(args, "human_decisions");
|
|
6510
|
+
if (entries === void 0) return void 0;
|
|
6511
|
+
return entries.map((entry) => ({
|
|
6512
|
+
requestId: requiredString(entry, "requestId"),
|
|
6513
|
+
choice: requiredString(entry, "choice")
|
|
6514
|
+
}));
|
|
3293
6515
|
}
|
|
3294
6516
|
function ok(value) {
|
|
3295
6517
|
return jsonResult(value, false);
|
|
@@ -3314,42 +6536,39 @@ function errorCode(err) {
|
|
|
3314
6536
|
const code = err.code;
|
|
3315
6537
|
if (typeof code === "string") return code;
|
|
3316
6538
|
}
|
|
3317
|
-
return "
|
|
3318
|
-
}
|
|
3319
|
-
function definedObject(input) {
|
|
3320
|
-
const result = {};
|
|
3321
|
-
for (const [key, value] of Object.entries(input)) if (value !== void 0) result[key] = value;
|
|
3322
|
-
return result;
|
|
6539
|
+
return "FIRST_MATE_ERROR";
|
|
3323
6540
|
}
|
|
3324
6541
|
function requiredString(args, key) {
|
|
3325
6542
|
const value = args[key];
|
|
3326
|
-
if (typeof value !== "string" || value.trim() === "") throw new
|
|
6543
|
+
if (typeof value !== "string" || value.trim() === "") throw new FirstMateToolInputError("INVALID_ARGUMENT", `arguments.${key} is required and must be a non-empty string`);
|
|
3327
6544
|
return value;
|
|
3328
6545
|
}
|
|
3329
6546
|
function optionalString(args, key) {
|
|
3330
6547
|
const value = args[key];
|
|
3331
6548
|
if (value === void 0) return void 0;
|
|
3332
|
-
if (typeof value !== "string") throw new
|
|
6549
|
+
if (typeof value !== "string") throw new FirstMateToolInputError("INVALID_ARGUMENT", `arguments.${key} must be a string`);
|
|
3333
6550
|
return value.trim() === "" ? void 0 : value;
|
|
3334
6551
|
}
|
|
3335
6552
|
function optionalNumber(args, key) {
|
|
3336
6553
|
const value = args[key];
|
|
3337
6554
|
if (value === void 0) return void 0;
|
|
3338
|
-
if (typeof value !== "number" || !Number.isFinite(value)) throw new
|
|
6555
|
+
if (typeof value !== "number" || !Number.isFinite(value)) throw new FirstMateToolInputError("INVALID_ARGUMENT", `arguments.${key} must be a finite number`);
|
|
3339
6556
|
return value;
|
|
3340
6557
|
}
|
|
3341
|
-
function
|
|
6558
|
+
function requiredStringArray(args, key) {
|
|
3342
6559
|
const value = args[key];
|
|
3343
|
-
if (value
|
|
3344
|
-
if (typeof value !== "boolean") throw new FleetToolInputError("INVALID_ARGUMENT", `arguments.${key} must be a boolean`);
|
|
6560
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.trim() === "")) throw new FirstMateToolInputError("INVALID_ARGUMENT", `arguments.${key} must be an array of non-empty strings`);
|
|
3345
6561
|
return value;
|
|
3346
6562
|
}
|
|
3347
|
-
function
|
|
6563
|
+
function optionalRecordArray(args, key) {
|
|
3348
6564
|
const value = args[key];
|
|
3349
6565
|
if (value === void 0) return void 0;
|
|
3350
|
-
if (!Array.isArray(value) || value.some((item) =>
|
|
6566
|
+
if (!Array.isArray(value) || value.some((item) => asRecord(item) === void 0)) throw new FirstMateToolInputError("INVALID_ARGUMENT", `arguments.${key} must be an array of objects`);
|
|
3351
6567
|
return value;
|
|
3352
6568
|
}
|
|
6569
|
+
function asRecord(value) {
|
|
6570
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
6571
|
+
}
|
|
3353
6572
|
function objectSchema(properties, required) {
|
|
3354
6573
|
return {
|
|
3355
6574
|
type: "object",
|
|
@@ -3370,19 +6589,23 @@ function numberProp(description) {
|
|
|
3370
6589
|
description
|
|
3371
6590
|
};
|
|
3372
6591
|
}
|
|
3373
|
-
function
|
|
6592
|
+
function stringArrayProp(description) {
|
|
3374
6593
|
return {
|
|
3375
|
-
type: "
|
|
6594
|
+
type: "array",
|
|
6595
|
+
items: { type: "string" },
|
|
3376
6596
|
description
|
|
3377
6597
|
};
|
|
3378
6598
|
}
|
|
3379
|
-
function
|
|
6599
|
+
function arrayOfObjectsProp(description, properties, required) {
|
|
3380
6600
|
return {
|
|
3381
6601
|
type: "array",
|
|
3382
|
-
|
|
3383
|
-
|
|
6602
|
+
description,
|
|
6603
|
+
items: objectSchema(properties, required)
|
|
3384
6604
|
};
|
|
3385
6605
|
}
|
|
6606
|
+
function anyProp(description) {
|
|
6607
|
+
return { description };
|
|
6608
|
+
}
|
|
3386
6609
|
|
|
3387
6610
|
//#endregion
|
|
3388
6611
|
//#region src/lib/tree-sitter-grammars.ts
|
|
@@ -5206,8 +8429,8 @@ function runStructuralPassInProcess(opts) {
|
|
|
5206
8429
|
consola.debug(`[code_search] structural skip ${relFile} (${size} bytes > cap)`);
|
|
5207
8430
|
continue;
|
|
5208
8431
|
}
|
|
5209
|
-
let cached$
|
|
5210
|
-
if (!cached$
|
|
8432
|
+
let cached$2 = cacheGet(absPath, mtimeMs);
|
|
8433
|
+
if (!cached$2) {
|
|
5211
8434
|
let source;
|
|
5212
8435
|
try {
|
|
5213
8436
|
source = readFileSync(absPath, "utf8");
|
|
@@ -5234,17 +8457,17 @@ function runStructuralPassInProcess(opts) {
|
|
|
5234
8457
|
} catch (err) {
|
|
5235
8458
|
consola.debug(`[code_search] tree-sitter parse failed for ${relFile}: ${err.message}`);
|
|
5236
8459
|
}
|
|
5237
|
-
cached$
|
|
8460
|
+
cached$2 = {
|
|
5238
8461
|
mtimeMs,
|
|
5239
8462
|
tree,
|
|
5240
8463
|
source: tree ? source : null
|
|
5241
8464
|
};
|
|
5242
|
-
cachePut(absPath, cached$
|
|
8465
|
+
cachePut(absPath, cached$2);
|
|
5243
8466
|
filesParsed += 1;
|
|
5244
8467
|
if (benchOn) _benchStructural.filesParsed += 1;
|
|
5245
8468
|
}
|
|
5246
|
-
if (!cached$
|
|
5247
|
-
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) => ({
|
|
5248
8471
|
line: e.hit.line,
|
|
5249
8472
|
matchStart: e.hit.match_start,
|
|
5250
8473
|
matchEnd: e.hit.match_end
|
|
@@ -5655,11 +8878,11 @@ function relativizeToWorkspace(file, workspaceCanonical) {
|
|
|
5655
8878
|
async function enumerateWorkspaceFiles(opts) {
|
|
5656
8879
|
const files = [];
|
|
5657
8880
|
let total = 0;
|
|
5658
|
-
let capped = false;
|
|
8881
|
+
let capped$1 = false;
|
|
5659
8882
|
if (opts.signal.aborted) return {
|
|
5660
8883
|
files,
|
|
5661
8884
|
total,
|
|
5662
|
-
capped
|
|
8885
|
+
capped: capped$1
|
|
5663
8886
|
};
|
|
5664
8887
|
let child;
|
|
5665
8888
|
try {
|
|
@@ -5676,7 +8899,7 @@ async function enumerateWorkspaceFiles(opts) {
|
|
|
5676
8899
|
return {
|
|
5677
8900
|
files,
|
|
5678
8901
|
total,
|
|
5679
|
-
capped
|
|
8902
|
+
capped: capped$1
|
|
5680
8903
|
};
|
|
5681
8904
|
}
|
|
5682
8905
|
child.on("error", () => {});
|
|
@@ -5688,7 +8911,7 @@ async function enumerateWorkspaceFiles(opts) {
|
|
|
5688
8911
|
if (!child.stdout) return {
|
|
5689
8912
|
files,
|
|
5690
8913
|
total,
|
|
5691
|
-
capped
|
|
8914
|
+
capped: capped$1
|
|
5692
8915
|
};
|
|
5693
8916
|
child.stdout.setEncoding("utf8");
|
|
5694
8917
|
const rl = createInterface({
|
|
@@ -5713,7 +8936,7 @@ async function enumerateWorkspaceFiles(opts) {
|
|
|
5713
8936
|
if (isSensitivePath(path.join(opts.workspaceCanonical, rel), opts.workspaceCanonical)) continue;
|
|
5714
8937
|
total += 1;
|
|
5715
8938
|
if (files.length < SCAN_MAX_FILES) files.push(rel);
|
|
5716
|
-
else capped = true;
|
|
8939
|
+
else capped$1 = true;
|
|
5717
8940
|
}
|
|
5718
8941
|
} catch {} finally {
|
|
5719
8942
|
clearTimeout(deadlineTimer);
|
|
@@ -5723,7 +8946,7 @@ async function enumerateWorkspaceFiles(opts) {
|
|
|
5723
8946
|
return {
|
|
5724
8947
|
files,
|
|
5725
8948
|
total,
|
|
5726
|
-
capped
|
|
8949
|
+
capped: capped$1
|
|
5727
8950
|
};
|
|
5728
8951
|
}
|
|
5729
8952
|
async function searchCode(rawInput, externalSignal) {
|
|
@@ -5965,10 +9188,10 @@ async function searchCode(rawInput, externalSignal) {
|
|
|
5965
9188
|
language: getLanguageKeyForPath(abs)
|
|
5966
9189
|
};
|
|
5967
9190
|
if (!result) try {
|
|
5968
|
-
const cached$
|
|
5969
|
-
if (cached$
|
|
9191
|
+
const cached$2 = cacheGet(abs, statSync(abs).mtimeMs);
|
|
9192
|
+
if (cached$2?.tree) {
|
|
5970
9193
|
const lang = getLanguageKeyForPath(abs);
|
|
5971
|
-
if (lang) result = outlineFromTree(cached$
|
|
9194
|
+
if (lang) result = outlineFromTree(cached$2.tree, lang, ac.signal);
|
|
5972
9195
|
}
|
|
5973
9196
|
} catch {}
|
|
5974
9197
|
const o = result ?? await outlineFile(abs, ac.signal);
|
|
@@ -7385,7 +10608,7 @@ async function runInit(workspace) {
|
|
|
7385
10608
|
];
|
|
7386
10609
|
const onInactivityCheck = makeIndexProgressProbe(workspace);
|
|
7387
10610
|
const startMs = Date.now();
|
|
7388
|
-
let ok$
|
|
10611
|
+
let ok$3 = false;
|
|
7389
10612
|
let failureClass;
|
|
7390
10613
|
try {
|
|
7391
10614
|
const res = await runManagedExeCapture(binary, args, {
|
|
@@ -7402,10 +10625,10 @@ async function runInit(workspace) {
|
|
|
7402
10625
|
}).catch(() => {});
|
|
7403
10626
|
}
|
|
7404
10627
|
});
|
|
7405
|
-
ok$
|
|
7406
|
-
if (!ok$
|
|
10628
|
+
ok$3 = !res.stalled && !res.timedOut && res.code === 0;
|
|
10629
|
+
if (!ok$3) failureClass = res.stalled || res.timedOut ? "stuck" : "error";
|
|
7407
10630
|
} catch {
|
|
7408
|
-
ok$
|
|
10631
|
+
ok$3 = false;
|
|
7409
10632
|
failureClass = "launch";
|
|
7410
10633
|
} finally {
|
|
7411
10634
|
releaseInit(workspace);
|
|
@@ -7422,9 +10645,9 @@ async function runInit(workspace) {
|
|
|
7422
10645
|
finalMeta.lastIndexedDirty = g.dirty;
|
|
7423
10646
|
}
|
|
7424
10647
|
} catch {}
|
|
7425
|
-
finalMeta.status = ok$
|
|
10648
|
+
finalMeta.status = ok$3 ? "ready" : "failed";
|
|
7426
10649
|
finalMeta.lastIndexedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
7427
|
-
if (ok$
|
|
10650
|
+
if (ok$3) {
|
|
7428
10651
|
finalMeta.failedAttempts = 0;
|
|
7429
10652
|
finalMeta.failureClass = void 0;
|
|
7430
10653
|
} else {
|
|
@@ -8802,7 +12025,7 @@ function logAudit$1(record) {
|
|
|
8802
12025
|
try {
|
|
8803
12026
|
const fs$2 = await import("node:fs/promises");
|
|
8804
12027
|
const path$1 = await import("node:path");
|
|
8805
|
-
const { PATHS: PATHS$1 } = await import("./paths-
|
|
12028
|
+
const { PATHS: PATHS$1 } = await import("./paths-DhLJ9bLG.js");
|
|
8806
12029
|
const dir = path$1.join(PATHS$1.APP_DIR, "browser-mcp");
|
|
8807
12030
|
await fs$2.mkdir(dir, { recursive: true });
|
|
8808
12031
|
const line = JSON.stringify({
|
|
@@ -9446,7 +12669,7 @@ async function readResponseBodyCapped(response, routePath, capBytes = MAX_RESPON
|
|
|
9446
12669
|
};
|
|
9447
12670
|
const chunks = [];
|
|
9448
12671
|
let totalBytes = 0;
|
|
9449
|
-
let capped = false;
|
|
12672
|
+
let capped$1 = false;
|
|
9450
12673
|
try {
|
|
9451
12674
|
while (true) {
|
|
9452
12675
|
const { done, value } = await reader.read();
|
|
@@ -9454,7 +12677,7 @@ async function readResponseBodyCapped(response, routePath, capBytes = MAX_RESPON
|
|
|
9454
12677
|
if (!value) continue;
|
|
9455
12678
|
totalBytes += value.byteLength;
|
|
9456
12679
|
if (totalBytes > capBytes) {
|
|
9457
|
-
capped = true;
|
|
12680
|
+
capped$1 = true;
|
|
9458
12681
|
try {
|
|
9459
12682
|
await reader.cancel("size_cap");
|
|
9460
12683
|
} catch {}
|
|
@@ -9463,9 +12686,9 @@ async function readResponseBodyCapped(response, routePath, capBytes = MAX_RESPON
|
|
|
9463
12686
|
chunks.push(value);
|
|
9464
12687
|
}
|
|
9465
12688
|
} catch (err) {
|
|
9466
|
-
if (!capped) consola.warn(`readResponseBodyCapped: read error at ${routePath}:`, err);
|
|
12689
|
+
if (!capped$1) consola.warn(`readResponseBodyCapped: read error at ${routePath}:`, err);
|
|
9467
12690
|
}
|
|
9468
|
-
if (capped) {
|
|
12691
|
+
if (capped$1) {
|
|
9469
12692
|
consola.warn(`Non-streaming upstream response at ${routePath} exceeded ${capBytes} bytes (10 MiB cap); dropping body to prevent OOM. Check upstream health.`);
|
|
9470
12693
|
return {
|
|
9471
12694
|
ok: false,
|
|
@@ -11852,8 +15075,8 @@ function coerceWithJsonSchema(value, schema) {
|
|
|
11852
15075
|
}
|
|
11853
15076
|
function getValidator(schema) {
|
|
11854
15077
|
const key = schema;
|
|
11855
|
-
const cached$
|
|
11856
|
-
if (cached$
|
|
15078
|
+
const cached$2 = validatorCache.get(key);
|
|
15079
|
+
if (cached$2) return cached$2;
|
|
11857
15080
|
const validator = Compile(schema);
|
|
11858
15081
|
validatorCache.set(key, validator);
|
|
11859
15082
|
return validator;
|
|
@@ -14028,12 +17251,12 @@ function joinTextChunks(accum, idx) {
|
|
|
14028
17251
|
*/
|
|
14029
17252
|
function makeLazyTextPart(chunks) {
|
|
14030
17253
|
const upTo = chunks.length;
|
|
14031
|
-
let cached$
|
|
17254
|
+
let cached$2;
|
|
14032
17255
|
return {
|
|
14033
17256
|
type: "text",
|
|
14034
17257
|
get text() {
|
|
14035
|
-
if (cached$
|
|
14036
|
-
return cached$
|
|
17258
|
+
if (cached$2 === void 0) cached$2 = upTo === chunks.length ? chunks.join("") : chunks.slice(0, upTo).join("");
|
|
17259
|
+
return cached$2;
|
|
14037
17260
|
}
|
|
14038
17261
|
};
|
|
14039
17262
|
}
|
|
@@ -14854,10 +18077,10 @@ function capToolResultText(content, capBytes) {
|
|
|
14854
18077
|
} else images.push(block);
|
|
14855
18078
|
}
|
|
14856
18079
|
if (textBytes <= capBytes) return void 0;
|
|
14857
|
-
const capped = truncateModelText(texts.join("\n"), capBytes);
|
|
18080
|
+
const capped$1 = truncateModelText(texts.join("\n"), capBytes);
|
|
14858
18081
|
return [...images, {
|
|
14859
18082
|
type: "text",
|
|
14860
|
-
text: capped
|
|
18083
|
+
text: capped$1
|
|
14861
18084
|
}];
|
|
14862
18085
|
}
|
|
14863
18086
|
|
|
@@ -14922,8 +18145,8 @@ const calculateTokens = (messages, encoder, constants) => {
|
|
|
14922
18145
|
*/
|
|
14923
18146
|
const getEncodeChatFunction = async (encoding) => {
|
|
14924
18147
|
if (encodingCache.has(encoding)) {
|
|
14925
|
-
const cached$
|
|
14926
|
-
if (cached$
|
|
18148
|
+
const cached$2 = encodingCache.get(encoding);
|
|
18149
|
+
if (cached$2) return cached$2;
|
|
14927
18150
|
}
|
|
14928
18151
|
const supportedEncoding = encoding;
|
|
14929
18152
|
if (!(supportedEncoding in ENCODING_MAP)) {
|
|
@@ -15351,6 +18574,19 @@ function fleetToolsEnabled() {
|
|
|
15351
18574
|
return state.fleetEnabled || process.env.GH_ROUTER_ENABLE_FLEET === "1";
|
|
15352
18575
|
}
|
|
15353
18576
|
/**
|
|
18577
|
+
* Gate for the first-mate cloud-agent MCP tools (`mcp__first-mate__*`).
|
|
18578
|
+
*
|
|
18579
|
+
* Returns true iff the operator opted in (`state.agentsEnabled`, set by
|
|
18580
|
+
* `--agents`, OR `GH_ROUTER_ENABLE_AGENTS=1` read directly so non-
|
|
18581
|
+
* `setupAndServe` startup paths — tests, embedded use — can still flip
|
|
18582
|
+
* the gate) AND the write-capable GitHub agent token is present. First-mate
|
|
18583
|
+
* drives GitHub cloud agents, so exposing the surface without that token would
|
|
18584
|
+
* only produce unactionable auth failures.
|
|
18585
|
+
*/
|
|
18586
|
+
function agentToolsEnabled() {
|
|
18587
|
+
return (state.agentsEnabled || process.env.GH_ROUTER_ENABLE_AGENTS === "1") && typeof state.githubAgentToken === "string" && state.githubAgentToken.length > 0;
|
|
18588
|
+
}
|
|
18589
|
+
/**
|
|
15354
18590
|
* Gate for ai-or-die Artifact review tools.
|
|
15355
18591
|
*
|
|
15356
18592
|
* Returns true iff this github-router process was launched inside an
|
|
@@ -15558,6 +18794,7 @@ function toolEntries(scope) {
|
|
|
15558
18794
|
if (t.capability === "stand_in") return standInToolEnabled();
|
|
15559
18795
|
if (t.capability === "browser") return browserToolsEnabled();
|
|
15560
18796
|
if (t.capability === "fleet") return fleetToolsEnabled();
|
|
18797
|
+
if (t.capability === "agents") return agentToolsEnabled();
|
|
15561
18798
|
if (t.capability === "artifact") return artifactToolsEnabled();
|
|
15562
18799
|
if (t.capability === "browser_compound") return browserToolsEnabled() && browserCompoundToolsEnabled();
|
|
15563
18800
|
if (t.capability === "browser_power") return browserToolsEnabled() && browserPowerToolsEnabled();
|
|
@@ -15886,6 +19123,7 @@ async function handleToolsCall(body, scope) {
|
|
|
15886
19123
|
if (nonPersonaTool && nonPersonaTool.capability === "stand_in" && !standInToolEnabled()) return rpcError(body.id, RPC_METHOD_NOT_FOUND, `tools/call: unknown tool "${name}"`);
|
|
15887
19124
|
if (nonPersonaTool && nonPersonaTool.capability === "browser" && !browserToolsEnabled()) return rpcError(body.id, RPC_METHOD_NOT_FOUND, `tools/call: unknown tool "${name}"`);
|
|
15888
19125
|
if (nonPersonaTool && nonPersonaTool.capability === "fleet" && !fleetToolsEnabled()) return rpcError(body.id, RPC_METHOD_NOT_FOUND, `tools/call: unknown tool "${name}"`);
|
|
19126
|
+
if (nonPersonaTool && nonPersonaTool.capability === "agents" && !agentToolsEnabled()) return rpcError(body.id, RPC_METHOD_NOT_FOUND, `tools/call: unknown tool "${name}"`);
|
|
15889
19127
|
if (nonPersonaTool && nonPersonaTool.capability === "artifact" && !artifactToolsEnabled()) return rpcError(body.id, RPC_METHOD_NOT_FOUND, `tools/call: unknown tool "${name}"`);
|
|
15890
19128
|
if (nonPersonaTool && nonPersonaTool.capability === "browser_compound" && !(browserToolsEnabled() && browserCompoundToolsEnabled())) return rpcError(body.id, RPC_METHOD_NOT_FOUND, `tools/call: unknown tool "${name}"`);
|
|
15891
19129
|
if (nonPersonaTool && nonPersonaTool.capability === "browser_power" && !(browserToolsEnabled() && browserPowerToolsEnabled())) return rpcError(body.id, RPC_METHOD_NOT_FOUND, `tools/call: unknown tool "${name}"`);
|
|
@@ -19451,8 +22689,8 @@ async function runWorkerAgentOnce(opts) {
|
|
|
19451
22689
|
afterToolCall: async (ctx) => {
|
|
19452
22690
|
budget.recordToolBytes(ctx.result);
|
|
19453
22691
|
if (ctxBudget) {
|
|
19454
|
-
const capped = capToolResultText(ctx.result.content, ctxBudget.perResultCapBytes);
|
|
19455
|
-
if (capped) return { content: capped };
|
|
22692
|
+
const capped$1 = capToolResultText(ctx.result.content, ctxBudget.perResultCapBytes);
|
|
22693
|
+
if (capped$1) return { content: capped$1 };
|
|
19456
22694
|
}
|
|
19457
22695
|
},
|
|
19458
22696
|
prepareNextTurn: async () => {
|
|
@@ -21618,7 +24856,8 @@ const MCP_GROUPS = Object.freeze([
|
|
|
21618
24856
|
"orchestrate",
|
|
21619
24857
|
"browser",
|
|
21620
24858
|
"decide",
|
|
21621
|
-
"fleet"
|
|
24859
|
+
"fleet",
|
|
24860
|
+
"first-mate"
|
|
21622
24861
|
]);
|
|
21623
24862
|
const GROUP_META = Object.freeze({
|
|
21624
24863
|
peers: {
|
|
@@ -21655,6 +24894,11 @@ const GROUP_META = Object.freeze({
|
|
|
21655
24894
|
preferredKey: "fleet",
|
|
21656
24895
|
urlSuffix: "fleet",
|
|
21657
24896
|
serverInfoName: "github-router-fleet"
|
|
24897
|
+
},
|
|
24898
|
+
"first-mate": {
|
|
24899
|
+
preferredKey: "first-mate",
|
|
24900
|
+
urlSuffix: "first-mate",
|
|
24901
|
+
serverInfoName: "github-router-first-mate"
|
|
21658
24902
|
}
|
|
21659
24903
|
});
|
|
21660
24904
|
/** True iff `s` is a registered group name (route `:group` param validation). */
|
|
@@ -22008,6 +25252,8 @@ function buildAgentPrompt(persona, opts) {
|
|
|
22008
25252
|
* out of the live catalog).
|
|
22009
25253
|
* - Conditionally lists stand_in only when `standInAvailable`
|
|
22010
25254
|
* (mirrors `standInToolEnabled()`).
|
|
25255
|
+
* - Conditionally lists gh-first-mate only when `agentToolsAvailable`
|
|
25256
|
+
* (mirrors `agentToolsEnabled()`).
|
|
22011
25257
|
* - Mentions `codex-cli` stdio bridge only when `codexCli`.
|
|
22012
25258
|
* - Does NOT re-document Claude Code's built-in delegation semantics
|
|
22013
25259
|
* (Agent-tool recursion, agent-teams coordination) — Claude
|
|
@@ -22034,7 +25280,10 @@ function buildPeerAwarenessSnippet(opts) {
|
|
|
22034
25280
|
if (opts.workerToolsAvailable) para2Parts.push(`\`mcp__${workersKey}__explore\` runs a Gemini-backed read-only worker that returns a summary, using its own context rather than yours; concurrent launches share the \`MAX_INFLIGHT_TOOLS_CALL\` cap (default 128) with operator traffic.`, `\`mcp__${workersKey}__review\` is the same worker framed as a code reviewer that reads the code itself to verify a change or claim, reporting findings with severity, so it checks context the \`peers\` critics (stateless calls on the pasted artifact) cannot.`, `\`mcp__${workersKey}__plan\` is the same read-only worker framed as a planner: from a task + acceptance criteria it returns an ordered implementation plan.`, `\`mcp__${workersKey}__implement\` is the same worker with edit/write/bash; \`worktree: true\` runs it in an isolated git worktree and returns the diff.`, `\`mcp__${workersKey}__test\` is a write-capable worker framed as an independent test author: it authors tests that try to break the implementation and reports pass/fail, never editing the implementation to make them pass.`, "Workers themselves have `code_search` in their toolset.");
|
|
22035
25281
|
if (opts.workerToolsAvailable) para2Parts.push(`\`mcp__${orchestrateKey}__decompose\` composes an open-ended ask into a typed, VERIFIED workflow IR (a strong driver decorrelated by a cross-lab critic, so the decompose step isn't a single point of failure), and \`mcp__${orchestrateKey}__run_workflow\` executes that IR through a frozen kernel delivering max(orchestrated, baseline) over a sealed executable gate, so it never ships worse than a plain single-model run. \`mcp__${orchestrateKey}__verify_workflow\` checks an IR's floor invariants before you run it, and \`mcp__${orchestrateKey}__attest_step\` audits that a finished run's producers were each checked by a different lab. They suit non-trivial, role-separated asks; a trivial ask does not need them.`);
|
|
22036
25282
|
else para2Parts.push(`\`mcp__${orchestrateKey}__verify_workflow\` statically checks a workflow IR's floor invariants and \`mcp__${orchestrateKey}__attest_step\` audits a run's cross-lab lineage (the \`decompose\`/\`run_workflow\` composer + kernel need the worker backend, unavailable here).`);
|
|
22037
|
-
if (opts.workerToolsAvailable)
|
|
25283
|
+
if (opts.workerToolsAvailable) {
|
|
25284
|
+
const skillSentence = opts.agentToolsAvailable === true ? "Four injected skills (invoke by name): `/gh-research` saturates an ask's unknowns into a confidence-tagged, root-cause brief that grounds planning; `/gh-orchestrate` right-sizes a blind-spot-elimination pipeline whose nodes delegate to these tools; `/gh-floor-keeper` is the done-checkpoint cross-lab verification, where different-lab reviewers propose and the executable gate decides; `/gh-first-mate` drives the durable GitHub cloud-agent loop. They suit non-trivial, role-separable work. Only executable checks are deterministic; they do not catch a wrong spec, so user-blessed acceptance criteria plus the checkpoint are the defense." : "Three injected skills (invoke by name): `/gh-research` saturates an ask's unknowns into a confidence-tagged, root-cause brief that grounds planning; `/gh-orchestrate` right-sizes a blind-spot-elimination pipeline whose nodes delegate to these tools; `/gh-floor-keeper` is the done-checkpoint cross-lab verification, where different-lab reviewers propose and the executable gate decides. They suit non-trivial, role-separable work. Only executable checks are deterministic; they do not catch a wrong spec, so user-blessed acceptance criteria plus the checkpoint are the defense.";
|
|
25285
|
+
para2Parts.push(skillSentence);
|
|
25286
|
+
}
|
|
22038
25287
|
para2Parts.push(`\`mcp__${searchKey}__web\` surfaces citable sources for docs, errors, and upstream issues.`);
|
|
22039
25288
|
if (opts.standInAvailable) para2Parts.push(`\`mcp__${decideKey}__stand_in\` provides three-lab consensus for decision tiebreak when the user is unavailable.`);
|
|
22040
25289
|
if (opts.browseAvailable) {
|
|
@@ -22768,6 +26017,7 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
|
|
|
22768
26017
|
},
|
|
22769
26018
|
...ARTIFACT_TOOLS,
|
|
22770
26019
|
...FLEET_TOOLS,
|
|
26020
|
+
...FIRST_MATE_TOOLS,
|
|
22771
26021
|
...BROWSER_TOOLS.map((t) => ({
|
|
22772
26022
|
...t,
|
|
22773
26023
|
group: "browser",
|
|
@@ -23096,5 +26346,5 @@ async function runStandInToolCall(args, signal) {
|
|
|
23096
26346
|
}
|
|
23097
26347
|
|
|
23098
26348
|
//#endregion
|
|
23099
|
-
export { readIteratorWithTimeout as $,
|
|
23100
|
-
//# 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
|