@agentskit/harness 0.6.0 → 0.8.0
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/CHANGELOG.md +11 -0
- package/capabilities/public-surface.json +106 -77
- package/dist/cli.js +789 -163
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +241 -75
- package/dist/index.js +795 -169
- package/dist/index.js.map +1 -1
- package/docs/LOOP.md +33 -0
- package/loop.config.example.yaml +10 -0
- package/package.json +1 -1
- package/release/manifest.json +1 -1
- package/release/notes.md +8 -0
package/dist/cli.js
CHANGED
|
@@ -953,15 +953,174 @@ var createDocBridgeContextProvider = ({ root, indexPath = ".doc-bridge/index.jso
|
|
|
953
953
|
return { providerId: "doc-bridge", query, references, sourceHash: contentHash, snapshotHash: hashContextSnapshot({ providerId: "doc-bridge", query, references, sourceHash: contentHash }), resolvedAt: (/* @__PURE__ */ new Date()).toISOString(), assurance: "contract-tested", telemetry };
|
|
954
954
|
}
|
|
955
955
|
});
|
|
956
|
+
var executable = (path) => {
|
|
957
|
+
try {
|
|
958
|
+
return statSync(path).isFile();
|
|
959
|
+
} catch {
|
|
960
|
+
return false;
|
|
961
|
+
}
|
|
962
|
+
};
|
|
963
|
+
var findExecutable = (name2, env = process.env, platform = process.platform) => {
|
|
964
|
+
if (typeof name2 !== "string" || !name2.trim()) return null;
|
|
965
|
+
if (isAbsolute(name2) || name2.includes("/") || name2.includes("\\")) return existsSync(name2) && executable(name2) ? name2 : null;
|
|
966
|
+
const extensions = platform === "win32" ? (env["PATHEXT"] ?? ".EXE;.CMD;.BAT;.COM").split(";").filter(Boolean) : [""];
|
|
967
|
+
for (const dir of (env["PATH"] ?? "").split(delimiter).filter(Boolean)) {
|
|
968
|
+
for (const extension of extensions) {
|
|
969
|
+
const candidate = join(dir, `${name2}${extension}`);
|
|
970
|
+
if (executable(candidate)) return candidate;
|
|
971
|
+
}
|
|
972
|
+
if (platform === "win32" && executable(join(dir, name2))) return join(dir, name2);
|
|
973
|
+
}
|
|
974
|
+
return null;
|
|
975
|
+
};
|
|
976
|
+
var parseJsonEnvelope = (stdout) => {
|
|
977
|
+
const trimmed = stdout.trim();
|
|
978
|
+
if (!trimmed) return null;
|
|
979
|
+
let parsed;
|
|
980
|
+
try {
|
|
981
|
+
parsed = JSON.parse(trimmed);
|
|
982
|
+
} catch {
|
|
983
|
+
return null;
|
|
984
|
+
}
|
|
985
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
|
|
986
|
+
const record3 = parsed;
|
|
987
|
+
if (typeof record3.ok !== "boolean") return null;
|
|
988
|
+
const error = typeof record3.error === "string" ? record3.error : typeof record3.error === "object" && record3.error !== null && typeof record3.error.message === "string" ? record3.error.message : void 0;
|
|
989
|
+
return { ok: record3.ok, result: record3.result, ...error === void 0 ? {} : { error } };
|
|
990
|
+
};
|
|
956
991
|
|
|
957
|
-
// src/adapters/
|
|
992
|
+
// src/adapters/providers.ts
|
|
958
993
|
var isRecord4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
994
|
+
var iso = (value) => typeof value === "number" && Number.isFinite(value) ? new Date(value).toISOString() : typeof value === "string" && !Number.isNaN(Date.parse(value)) ? new Date(value).toISOString() : null;
|
|
995
|
+
var parseUsageWindows = (entry) => {
|
|
996
|
+
if (!isRecord4(entry)) return [];
|
|
997
|
+
return Object.entries(entry).flatMap(([kind, value]) => {
|
|
998
|
+
if (!isRecord4(value) || typeof value["usedPercent"] !== "number") return [];
|
|
999
|
+
return [{ kind, usedPercent: value["usedPercent"], windowMinutes: typeof value["windowMinutes"] === "number" ? value["windowMinutes"] : null, resetsAt: iso(value["resetsAt"]) }];
|
|
1000
|
+
});
|
|
1001
|
+
};
|
|
1002
|
+
var parseProviderUsage = (accountList, usageKey, exhaustedPercent = 100) => {
|
|
1003
|
+
const result = isRecord4(accountList) ? accountList : {};
|
|
1004
|
+
const rateLimits = isRecord4(result["rateLimits"]) ? result["rateLimits"] : {};
|
|
1005
|
+
const entry = isRecord4(rateLimits[usageKey]) ? rateLimits[usageKey] : null;
|
|
1006
|
+
const account = isRecord4(result[usageKey]) ? result[usageKey] : null;
|
|
1007
|
+
const systemDefault = account && isRecord4(account["systemDefault"]) ? account["systemDefault"] : null;
|
|
1008
|
+
const accounts = account && Array.isArray(account["accounts"]) ? account["accounts"] : [];
|
|
1009
|
+
const hasAuth = systemDefault ? systemDefault["hasAuth"] === true : accounts.length ? true : null;
|
|
1010
|
+
if (!entry) return { status: "unknown", error: null, windows: [], exhausted: false, resetsAt: null, hasAuth };
|
|
1011
|
+
const windows = parseUsageWindows(entry);
|
|
1012
|
+
const exhaustedWindows = windows.filter((window) => window.usedPercent >= exhaustedPercent);
|
|
1013
|
+
const resetsAt = exhaustedWindows.map((window) => window.resetsAt).filter((value) => Boolean(value)).sort()[0] ?? null;
|
|
1014
|
+
return {
|
|
1015
|
+
status: entry["status"] === "ok" ? "ok" : entry["status"] === "unavailable" ? "unavailable" : "unknown",
|
|
1016
|
+
error: typeof entry["error"] === "string" ? entry["error"] : null,
|
|
1017
|
+
windows,
|
|
1018
|
+
exhausted: exhaustedWindows.length > 0,
|
|
1019
|
+
resetsAt,
|
|
1020
|
+
hasAuth
|
|
1021
|
+
};
|
|
1022
|
+
};
|
|
1023
|
+
var authStatusFor = (spec, usage, env) => {
|
|
1024
|
+
const hasEnvKey = spec.envKeys.some((key) => Boolean(env[key]?.trim()));
|
|
1025
|
+
if (spec.auth === "api-key") return hasEnvKey ? "ok" : "missing";
|
|
1026
|
+
if (spec.auth === "subscription") return usage.hasAuth === true || usage.status === "ok" ? "ok" : usage.hasAuth === false ? "missing" : hasEnvKey || usage.status === "unknown" ? "ok" : "unknown";
|
|
1027
|
+
return hasEnvKey || usage.status === "ok" ? "ok" : "unknown";
|
|
1028
|
+
};
|
|
1029
|
+
var runProbe = async (spec, binary, runner, timeoutMs) => {
|
|
1030
|
+
if (!spec.probe || !runner) return "skipped";
|
|
1031
|
+
const [head, ...rest] = spec.probe;
|
|
1032
|
+
const argv = [head === spec.bin ? binary : head ?? binary, ...rest];
|
|
1033
|
+
try {
|
|
1034
|
+
const outcome = await runner.run(argv, { timeoutMs });
|
|
1035
|
+
return outcome.code === 0 && !outcome.timedOut ? "passed" : "failed";
|
|
1036
|
+
} catch {
|
|
1037
|
+
return "failed";
|
|
1038
|
+
}
|
|
1039
|
+
};
|
|
1040
|
+
var detectProviders = async (input) => {
|
|
1041
|
+
const env = input.env ?? process.env;
|
|
1042
|
+
const platform = input.platform ?? process.platform;
|
|
1043
|
+
const now4 = input.now ?? (() => /* @__PURE__ */ new Date());
|
|
1044
|
+
const results = [];
|
|
1045
|
+
for (const spec of input.providers) {
|
|
1046
|
+
const binary = findExecutable(spec.bin, env, platform);
|
|
1047
|
+
const hookState = input.agentHooks[spec.id] ?? "unknown";
|
|
1048
|
+
const usage = parseProviderUsage(input.accountList, spec.orcaUsageKey, input.exhaustedPercent ?? 100);
|
|
1049
|
+
const auth = authStatusFor(spec, usage, env);
|
|
1050
|
+
const cooldown = input.cooldowns?.[spec.id] ?? null;
|
|
1051
|
+
const coolingDownUntil = cooldown && Date.parse(cooldown) > now4().getTime() ? new Date(cooldown).toISOString() : null;
|
|
1052
|
+
const reasons = [];
|
|
1053
|
+
if (!binary) reasons.push(`binary "${spec.bin}" not found on PATH`);
|
|
1054
|
+
if (auth === "missing") reasons.push(spec.auth === "api-key" ? `none of ${spec.envKeys.join(", ") || "the configured env keys"} is set` : `Orca reports no ${spec.id} credentials`);
|
|
1055
|
+
if (usage.exhausted) reasons.push(`usage exhausted${usage.resetsAt ? ` until ${usage.resetsAt}` : ""}`);
|
|
1056
|
+
if (coolingDownUntil) reasons.push(`cooling down until ${coolingDownUntil}`);
|
|
1057
|
+
const probe = binary && !reasons.length ? await runProbe(spec, binary, input.runner, input.probeTimeoutMs ?? 15e3) : "skipped";
|
|
1058
|
+
if (probe === "failed") reasons.push("probe command failed");
|
|
1059
|
+
results.push({ id: spec.id, binary, hookState, auth, usage, probe, coolingDownUntil, available: reasons.length === 0, reasons });
|
|
1060
|
+
}
|
|
1061
|
+
return results;
|
|
1062
|
+
};
|
|
1063
|
+
var cooldownUntil = (attempt, initialMin, maxMin, from, resetsAt = null) => {
|
|
1064
|
+
const minutes2 = Math.min(maxMin, initialMin * 2 ** Math.max(0, attempt));
|
|
1065
|
+
const backoff = from.getTime() + minutes2 * 6e4;
|
|
1066
|
+
const reset = resetsAt ? Date.parse(resetsAt) : Number.NaN;
|
|
1067
|
+
return new Date(Number.isFinite(reset) && reset > from.getTime() ? Math.max(reset, backoff) : backoff).toISOString();
|
|
1068
|
+
};
|
|
1069
|
+
var remainingUsagePercent = (usage, metric = "max") => {
|
|
1070
|
+
if (usage.status !== "ok" || !usage.windows.length) return null;
|
|
1071
|
+
const windows = metric === "max" ? usage.windows : usage.windows.filter((window) => window.kind === metric);
|
|
1072
|
+
const pool = windows.length ? windows : usage.windows;
|
|
1073
|
+
if (!pool.length) return null;
|
|
1074
|
+
const worst = Math.max(...pool.map((window) => window.usedPercent));
|
|
1075
|
+
return Math.max(0, Math.min(100, 100 - worst));
|
|
1076
|
+
};
|
|
1077
|
+
var usageRankTuple = (usage, metric, preferKnownUsage) => {
|
|
1078
|
+
const remaining = remainingUsagePercent(usage, metric);
|
|
1079
|
+
const known = remaining === null ? 1 : 0;
|
|
1080
|
+
const remainingKey = remaining === null ? 0 : -remaining;
|
|
1081
|
+
const resetMs = usage.resetsAt ? Date.parse(usage.resetsAt) : Number.POSITIVE_INFINITY;
|
|
1082
|
+
return preferKnownUsage ? [known, remainingKey, resetMs] : [remainingKey, known, resetMs];
|
|
1083
|
+
};
|
|
1084
|
+
var ORCA_META_KEYS = /* @__PURE__ */ new Set([
|
|
1085
|
+
"minimaxCookieConfigured",
|
|
1086
|
+
"minimaxApiKeyConfigured",
|
|
1087
|
+
"grokAuthConfigured",
|
|
1088
|
+
"claudeTarget",
|
|
1089
|
+
"codexTarget",
|
|
1090
|
+
"inactiveClaudeAccounts",
|
|
1091
|
+
"inactiveCodexAccounts"
|
|
1092
|
+
]);
|
|
1093
|
+
var listOrcaIntegratedProviderKeys = (accountList) => {
|
|
1094
|
+
const result = isRecord4(accountList) ? accountList : {};
|
|
1095
|
+
const rateLimits = isRecord4(result["rateLimits"]) ? result["rateLimits"] : {};
|
|
1096
|
+
const keys = /* @__PURE__ */ new Set();
|
|
1097
|
+
for (const key of Object.keys(rateLimits)) {
|
|
1098
|
+
if (!ORCA_META_KEYS.has(key) && isRecord4(rateLimits[key])) keys.add(key);
|
|
1099
|
+
}
|
|
1100
|
+
for (const key of Object.keys(result)) {
|
|
1101
|
+
if (key === "rateLimits" || ORCA_META_KEYS.has(key)) continue;
|
|
1102
|
+
if (isRecord4(result[key])) keys.add(key);
|
|
1103
|
+
}
|
|
1104
|
+
return [...keys].sort();
|
|
1105
|
+
};
|
|
1106
|
+
var undeclaredOrcaProviders = (accountList, declared) => {
|
|
1107
|
+
const usageKeyToId = /* @__PURE__ */ new Map();
|
|
1108
|
+
for (const [id2, settings] of Object.entries(declared)) {
|
|
1109
|
+
usageKeyToId.set(settings.orcaUsageKey ?? id2, id2);
|
|
1110
|
+
usageKeyToId.set(id2, id2);
|
|
1111
|
+
}
|
|
1112
|
+
usageKeyToId.set("opencodeGo", usageKeyToId.get("opencodeGo") ?? "opencode");
|
|
1113
|
+
return listOrcaIntegratedProviderKeys(accountList).filter((key) => !usageKeyToId.has(key));
|
|
1114
|
+
};
|
|
1115
|
+
|
|
1116
|
+
// src/adapters/rag-context.ts
|
|
1117
|
+
var isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
959
1118
|
var requiredString2 = (value, label) => {
|
|
960
1119
|
if (typeof value !== "string" || !value.trim()) return fail(`${label} must be a non-empty string.`, "INVALID_INPUT");
|
|
961
1120
|
return value;
|
|
962
1121
|
};
|
|
963
1122
|
var parseReference = (value, index2) => {
|
|
964
|
-
if (!
|
|
1123
|
+
if (!isRecord5(value)) return fail(`RAG references[${index2}] must be an object.`, "INVALID_INPUT");
|
|
965
1124
|
const relevance = value["relevance"];
|
|
966
1125
|
if (relevance !== void 0 && (typeof relevance !== "number" || relevance < 0 || relevance > 1)) return fail(`RAG references[${index2}].relevance must be between 0 and 1.`, "INVALID_INPUT");
|
|
967
1126
|
return {
|
|
@@ -974,7 +1133,7 @@ var parseReference = (value, index2) => {
|
|
|
974
1133
|
};
|
|
975
1134
|
};
|
|
976
1135
|
var parseRagQueryOutput = (value) => {
|
|
977
|
-
if (!
|
|
1136
|
+
if (!isRecord5(value)) return fail("RAG query output must be a JSON object.", "INVALID_INPUT");
|
|
978
1137
|
const rawReferences = value["references"];
|
|
979
1138
|
if (!Array.isArray(rawReferences)) return fail("RAG query output.references must be an array.", "INVALID_INPUT");
|
|
980
1139
|
const references = rawReferences.map((entry, index2) => parseReference(entry, index2));
|
|
@@ -1389,7 +1548,7 @@ var artifactId = (value) => {
|
|
|
1389
1548
|
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(result)) fail("Artifact artifactId is invalid.", "INVALID_INPUT");
|
|
1390
1549
|
return result;
|
|
1391
1550
|
};
|
|
1392
|
-
var
|
|
1551
|
+
var isRecord6 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1393
1552
|
var artifactBody = (artifact) => ({
|
|
1394
1553
|
type: artifact.type,
|
|
1395
1554
|
schemaVersion: artifact.schemaVersion,
|
|
@@ -1408,7 +1567,7 @@ var artifactBody = (artifact) => ({
|
|
|
1408
1567
|
});
|
|
1409
1568
|
var expectedArtifactHash = (artifact) => hashJson(artifactBody(artifact));
|
|
1410
1569
|
var validateArtifactEnvelope = (value) => {
|
|
1411
|
-
if (!
|
|
1570
|
+
if (!isRecord6(value)) return fail("Artifact envelope must be an object.", "INVALID_INPUT");
|
|
1412
1571
|
if (value["type"] !== "agentskit-harness-artifact" || value["schemaVersion"] !== ARTIFACT_SCHEMA_VERSION) fail("Artifact envelope type or schemaVersion is invalid.", "INVALID_INPUT");
|
|
1413
1572
|
if (!ARTIFACT_TYPES.includes(value["artifactType"])) fail("Artifact artifactType is invalid.", "INVALID_INPUT");
|
|
1414
1573
|
if (!Number.isInteger(value["artifactVersion"]) || value["artifactVersion"] < 1) fail("Artifact artifactVersion must be a positive integer.", "INVALID_INPUT");
|
|
@@ -2123,44 +2282,9 @@ var readEvidenceTrustStore = (path) => {
|
|
|
2123
2282
|
return key;
|
|
2124
2283
|
});
|
|
2125
2284
|
};
|
|
2126
|
-
var executable = (path) => {
|
|
2127
|
-
try {
|
|
2128
|
-
return statSync(path).isFile();
|
|
2129
|
-
} catch {
|
|
2130
|
-
return false;
|
|
2131
|
-
}
|
|
2132
|
-
};
|
|
2133
|
-
var findExecutable = (name2, env = process.env, platform = process.platform) => {
|
|
2134
|
-
if (typeof name2 !== "string" || !name2.trim()) return null;
|
|
2135
|
-
if (isAbsolute(name2) || name2.includes("/") || name2.includes("\\")) return existsSync(name2) && executable(name2) ? name2 : null;
|
|
2136
|
-
const extensions = platform === "win32" ? (env["PATHEXT"] ?? ".EXE;.CMD;.BAT;.COM").split(";").filter(Boolean) : [""];
|
|
2137
|
-
for (const dir of (env["PATH"] ?? "").split(delimiter).filter(Boolean)) {
|
|
2138
|
-
for (const extension of extensions) {
|
|
2139
|
-
const candidate = join(dir, `${name2}${extension}`);
|
|
2140
|
-
if (executable(candidate)) return candidate;
|
|
2141
|
-
}
|
|
2142
|
-
if (platform === "win32" && executable(join(dir, name2))) return join(dir, name2);
|
|
2143
|
-
}
|
|
2144
|
-
return null;
|
|
2145
|
-
};
|
|
2146
|
-
var parseJsonEnvelope = (stdout) => {
|
|
2147
|
-
const trimmed = stdout.trim();
|
|
2148
|
-
if (!trimmed) return null;
|
|
2149
|
-
let parsed;
|
|
2150
|
-
try {
|
|
2151
|
-
parsed = JSON.parse(trimmed);
|
|
2152
|
-
} catch {
|
|
2153
|
-
return null;
|
|
2154
|
-
}
|
|
2155
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
|
|
2156
|
-
const record3 = parsed;
|
|
2157
|
-
if (typeof record3.ok !== "boolean") return null;
|
|
2158
|
-
const error = typeof record3.error === "string" ? record3.error : typeof record3.error === "object" && record3.error !== null && typeof record3.error.message === "string" ? record3.error.message : void 0;
|
|
2159
|
-
return { ok: record3.ok, result: record3.result, ...error === void 0 ? {} : { error } };
|
|
2160
|
-
};
|
|
2161
2285
|
|
|
2162
2286
|
// src/adapters/orca-cli.ts
|
|
2163
|
-
var
|
|
2287
|
+
var isRecord7 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2164
2288
|
var str = (value, fallback = "") => typeof value === "string" ? value : fallback;
|
|
2165
2289
|
var num = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
2166
2290
|
var compareVersions = (left, right) => {
|
|
@@ -2174,9 +2298,9 @@ var compareVersions = (left, right) => {
|
|
|
2174
2298
|
};
|
|
2175
2299
|
var parseOrcaVersion = (stdout) => stdout.match(/\d+\.\d+\.\d+/)?.[0] ?? null;
|
|
2176
2300
|
var parseOrcaStatus = (result) => {
|
|
2177
|
-
const record3 =
|
|
2178
|
-
const app =
|
|
2179
|
-
const runtime =
|
|
2301
|
+
const record3 = isRecord7(result) ? result : {};
|
|
2302
|
+
const app = isRecord7(record3["app"]) ? record3["app"] : {};
|
|
2303
|
+
const runtime = isRecord7(record3["runtime"]) ? record3["runtime"] : {};
|
|
2180
2304
|
return {
|
|
2181
2305
|
appRunning: app["running"] === true,
|
|
2182
2306
|
runtimeReady: runtime["state"] === "ready" && runtime["reachable"] === true,
|
|
@@ -2187,14 +2311,14 @@ var parseOrcaStatus = (result) => {
|
|
|
2187
2311
|
};
|
|
2188
2312
|
var linkedLinear = (value) => {
|
|
2189
2313
|
if (typeof value === "string" && value.trim()) return value.trim();
|
|
2190
|
-
if (
|
|
2314
|
+
if (isRecord7(value)) {
|
|
2191
2315
|
for (const key of ["identifier", "id", "url"]) if (typeof value[key] === "string" && value[key].trim()) return value[key].trim();
|
|
2192
2316
|
}
|
|
2193
2317
|
return null;
|
|
2194
2318
|
};
|
|
2195
2319
|
var parseOrcaWorktrees = (result) => {
|
|
2196
|
-
const list2 =
|
|
2197
|
-
return list2.filter(
|
|
2320
|
+
const list2 = isRecord7(result) && Array.isArray(result["worktrees"]) ? result["worktrees"] : Array.isArray(result) ? result : [];
|
|
2321
|
+
return list2.filter(isRecord7).map((item) => ({
|
|
2198
2322
|
id: str(item["worktreeId"], str(item["id"])),
|
|
2199
2323
|
repoId: str(item["repoId"]),
|
|
2200
2324
|
repo: str(item["repo"]),
|
|
@@ -2211,8 +2335,8 @@ var parseOrcaWorktrees = (result) => {
|
|
|
2211
2335
|
})).filter((item) => item.id);
|
|
2212
2336
|
};
|
|
2213
2337
|
var parseOrcaAgentHooks = (result) => {
|
|
2214
|
-
const statuses =
|
|
2215
|
-
return Object.fromEntries(statuses.filter(
|
|
2338
|
+
const statuses = isRecord7(result) && Array.isArray(result["statuses"]) ? result["statuses"] : [];
|
|
2339
|
+
return Object.fromEntries(statuses.filter(isRecord7).flatMap((item) => {
|
|
2216
2340
|
const agent = str(item["agent"]);
|
|
2217
2341
|
if (!agent) return [];
|
|
2218
2342
|
const state = item["state"] === "installed" ? "installed" : item["state"] === "not_installed" ? "not_installed" : "unknown";
|
|
@@ -2238,9 +2362,9 @@ var orcaWorktrees = async (runner, options2 = {}) => parseOrcaWorktrees(await or
|
|
|
2238
2362
|
var orcaAgentHooks = async (runner, options2 = {}) => parseOrcaAgentHooks(await orcaJson(runner, ["agent", "hooks", "status"], options2));
|
|
2239
2363
|
var orcaAccountList = async (runner, options2 = {}) => orcaJson(runner, ["account", "list"], options2);
|
|
2240
2364
|
var parseOrcaWorktreeCreate = (result) => {
|
|
2241
|
-
const record3 =
|
|
2242
|
-
const nested =
|
|
2243
|
-
const startup =
|
|
2365
|
+
const record3 = isRecord7(result) ? result : {};
|
|
2366
|
+
const nested = isRecord7(record3["worktree"]) ? record3["worktree"] : record3;
|
|
2367
|
+
const startup = isRecord7(record3["startupTerminal"]) ? record3["startupTerminal"] : isRecord7(nested["startupTerminal"]) ? nested["startupTerminal"] : {};
|
|
2244
2368
|
const id2 = str(nested["worktreeId"], str(nested["id"], str(record3["worktreeId"], str(record3["id"]))));
|
|
2245
2369
|
if (!id2) fail("orca worktree create returned no worktree id.", "HARNESS_ERROR");
|
|
2246
2370
|
return {
|
|
@@ -2270,8 +2394,8 @@ var orcaWorktreeSetArgv = (input, bin = "orca") => [
|
|
|
2270
2394
|
var orcaWorktreeSet = async (runner, input, options2 = {}) => orcaJson(runner, orcaWorktreeSetArgv(input).slice(1), options2);
|
|
2271
2395
|
var orcaWorktreeRemove = async (runner, input, options2 = {}) => orcaJson(runner, ["worktree", "rm", "--worktree", input.worktree, ...input.force ? ["--force"] : []], { ...options2, timeoutMs: options2.timeoutMs ?? 6e4 });
|
|
2272
2396
|
var parseOrcaTerminals = (result) => {
|
|
2273
|
-
const list2 =
|
|
2274
|
-
return list2.filter(
|
|
2397
|
+
const list2 = isRecord7(result) ? Array.isArray(result["terminals"]) ? result["terminals"] : Array.isArray(result["items"]) ? result["items"] : [] : Array.isArray(result) ? result : [];
|
|
2398
|
+
return list2.filter(isRecord7).map((item) => ({
|
|
2275
2399
|
handle: str(item["handle"], str(item["id"])),
|
|
2276
2400
|
title: str(item["title"], str(item["name"])),
|
|
2277
2401
|
worktreeId: str(item["worktreeId"], str(item["worktree"])) || null,
|
|
@@ -2286,29 +2410,29 @@ var parseOrcaTerminals = (result) => {
|
|
|
2286
2410
|
var orcaTerminalList = async (runner, input = {}, options2 = {}) => parseOrcaTerminals(await orcaJson(runner, ["terminal", "list", ...input.worktree ? ["--worktree", input.worktree] : [], ...input.limit ? ["--limit", String(input.limit)] : []], options2));
|
|
2287
2411
|
var orcaTerminalCreate = async (runner, input, options2 = {}) => {
|
|
2288
2412
|
const result = await orcaJson(runner, ["terminal", "create", "--worktree", input.worktree, "--command", input.command, ...input.title ? ["--title", input.title] : []], { ...options2, timeoutMs: options2.timeoutMs ?? 6e4 });
|
|
2289
|
-
const record3 =
|
|
2290
|
-
const terminal2 =
|
|
2413
|
+
const record3 = isRecord7(result) ? result : {};
|
|
2414
|
+
const terminal2 = isRecord7(record3["terminal"]) ? record3["terminal"] : record3;
|
|
2291
2415
|
const handle = str(terminal2["handle"], str(record3["handle"]));
|
|
2292
2416
|
if (!handle) fail("orca terminal create returned no terminal handle.", "HARNESS_ERROR");
|
|
2293
2417
|
return { handle, raw: result };
|
|
2294
2418
|
};
|
|
2295
2419
|
var parseOrcaSendReceipt = (result) => {
|
|
2296
|
-
const record3 =
|
|
2297
|
-
const receipt =
|
|
2298
|
-
const stages = Array.isArray(receipt["stages"]) ? receipt["stages"].map((stage) =>
|
|
2420
|
+
const record3 = isRecord7(result) ? result : {};
|
|
2421
|
+
const receipt = isRecord7(record3["receipt"]) ? record3["receipt"] : record3;
|
|
2422
|
+
const stages = Array.isArray(receipt["stages"]) ? receipt["stages"].map((stage) => isRecord7(stage) ? str(stage["stage"], str(stage["name"])) : str(stage)).filter(Boolean) : [];
|
|
2299
2423
|
const accepted = receipt["accepted"] === false ? false : receipt["accepted"] === true || stages.includes("input_accepted") || (result === null || result === void 0 || Object.keys(record3).length === 0);
|
|
2300
|
-
return { accepted, requestId: str(receipt["requestId"], str(record3["requestId"])) || null, stages, warnings: Array.isArray(record3["warnings"]) ? record3["warnings"].map((warning) =>
|
|
2424
|
+
return { accepted, requestId: str(receipt["requestId"], str(record3["requestId"])) || null, stages, warnings: Array.isArray(record3["warnings"]) ? record3["warnings"].map((warning) => isRecord7(warning) ? str(warning["message"], JSON.stringify(warning)) : str(warning)) : [] };
|
|
2301
2425
|
};
|
|
2302
2426
|
var orcaTerminalSend = async (runner, input, options2 = {}) => parseOrcaSendReceipt(await orcaJson(runner, ["terminal", "send", "--terminal", input.terminal, "--text", input.text, ...input.enter === false ? [] : ["--enter"], ...input.waitSubmitSeconds ? ["--wait-submit", String(input.waitSubmitSeconds)] : []], { ...options2, timeoutMs: options2.timeoutMs ?? (input.waitSubmitSeconds ?? 0) * 1e3 + 3e4 }));
|
|
2303
2427
|
var orcaTerminalWait = async (runner, input, options2 = {}) => {
|
|
2304
2428
|
const result = await orcaJson(runner, ["terminal", "wait", "--terminal", input.terminal, "--for", input.for, "--timeout-ms", String(input.timeoutMs)], { ...options2, timeoutMs: input.timeoutMs + 15e3 });
|
|
2305
|
-
const record3 =
|
|
2306
|
-
const wait =
|
|
2429
|
+
const record3 = isRecord7(result) ? result : {};
|
|
2430
|
+
const wait = isRecord7(record3["wait"]) ? record3["wait"] : record3;
|
|
2307
2431
|
return { satisfied: wait["satisfied"] === true, raw: result };
|
|
2308
2432
|
};
|
|
2309
2433
|
var parseOrcaAutomations = (result) => {
|
|
2310
|
-
const list2 =
|
|
2311
|
-
return list2.filter(
|
|
2434
|
+
const list2 = isRecord7(result) ? Array.isArray(result["automations"]) ? result["automations"] : Array.isArray(result["items"]) ? result["items"] : [] : Array.isArray(result) ? result : [];
|
|
2435
|
+
return list2.filter(isRecord7).map((item) => ({ id: str(item["id"]), name: str(item["name"]), enabled: item["enabled"] !== false && item["disabled"] !== true, trigger: str(item["rrule"], str(item["trigger"], str(item["schedule"], typeof item["schedule"] === "object" && item["schedule"] !== null ? JSON.stringify(item["schedule"]) : ""))), provider: str(item["agentId"], str(item["provider"], str(item["agent"]))) || null, raw: item })).filter((item) => item.id);
|
|
2312
2436
|
};
|
|
2313
2437
|
var orcaAutomationsList = async (runner, options2 = {}) => parseOrcaAutomations(await orcaJson(runner, ["automations", "list"], options2));
|
|
2314
2438
|
var orcaAutomationCreateArgv = (spec, bin = "orca") => [
|
|
@@ -2355,84 +2479,6 @@ var orcaAutomationEditArgv = (id2, spec, bin = "orca") => [
|
|
|
2355
2479
|
var orcaAutomationRemove = async (runner, id2, options2 = {}) => orcaJson(runner, ["automations", "remove", id2], options2);
|
|
2356
2480
|
var orcaAutomationRuns = async (runner, id2, options2 = {}) => orcaJson(runner, ["automations", "runs", "--id", id2], options2);
|
|
2357
2481
|
|
|
2358
|
-
// src/adapters/providers.ts
|
|
2359
|
-
var isRecord7 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2360
|
-
var iso = (value) => typeof value === "number" && Number.isFinite(value) ? new Date(value).toISOString() : typeof value === "string" && !Number.isNaN(Date.parse(value)) ? new Date(value).toISOString() : null;
|
|
2361
|
-
var parseUsageWindows = (entry) => {
|
|
2362
|
-
if (!isRecord7(entry)) return [];
|
|
2363
|
-
return Object.entries(entry).flatMap(([kind, value]) => {
|
|
2364
|
-
if (!isRecord7(value) || typeof value["usedPercent"] !== "number") return [];
|
|
2365
|
-
return [{ kind, usedPercent: value["usedPercent"], windowMinutes: typeof value["windowMinutes"] === "number" ? value["windowMinutes"] : null, resetsAt: iso(value["resetsAt"]) }];
|
|
2366
|
-
});
|
|
2367
|
-
};
|
|
2368
|
-
var parseProviderUsage = (accountList, usageKey, exhaustedPercent = 100) => {
|
|
2369
|
-
const result = isRecord7(accountList) ? accountList : {};
|
|
2370
|
-
const rateLimits = isRecord7(result["rateLimits"]) ? result["rateLimits"] : {};
|
|
2371
|
-
const entry = isRecord7(rateLimits[usageKey]) ? rateLimits[usageKey] : null;
|
|
2372
|
-
const account = isRecord7(result[usageKey]) ? result[usageKey] : null;
|
|
2373
|
-
const systemDefault = account && isRecord7(account["systemDefault"]) ? account["systemDefault"] : null;
|
|
2374
|
-
const accounts = account && Array.isArray(account["accounts"]) ? account["accounts"] : [];
|
|
2375
|
-
const hasAuth = systemDefault ? systemDefault["hasAuth"] === true : accounts.length ? true : null;
|
|
2376
|
-
if (!entry) return { status: "unknown", error: null, windows: [], exhausted: false, resetsAt: null, hasAuth };
|
|
2377
|
-
const windows = parseUsageWindows(entry);
|
|
2378
|
-
const exhaustedWindows = windows.filter((window) => window.usedPercent >= exhaustedPercent);
|
|
2379
|
-
const resetsAt = exhaustedWindows.map((window) => window.resetsAt).filter((value) => Boolean(value)).sort()[0] ?? null;
|
|
2380
|
-
return {
|
|
2381
|
-
status: entry["status"] === "ok" ? "ok" : entry["status"] === "unavailable" ? "unavailable" : "unknown",
|
|
2382
|
-
error: typeof entry["error"] === "string" ? entry["error"] : null,
|
|
2383
|
-
windows,
|
|
2384
|
-
exhausted: exhaustedWindows.length > 0,
|
|
2385
|
-
resetsAt,
|
|
2386
|
-
hasAuth
|
|
2387
|
-
};
|
|
2388
|
-
};
|
|
2389
|
-
var authStatusFor = (spec, usage, env) => {
|
|
2390
|
-
const hasEnvKey = spec.envKeys.some((key) => Boolean(env[key]?.trim()));
|
|
2391
|
-
if (spec.auth === "api-key") return hasEnvKey ? "ok" : "missing";
|
|
2392
|
-
if (spec.auth === "subscription") return usage.hasAuth === true || usage.status === "ok" ? "ok" : usage.hasAuth === false ? "missing" : hasEnvKey || usage.status === "unknown" ? "ok" : "unknown";
|
|
2393
|
-
return hasEnvKey || usage.status === "ok" ? "ok" : "unknown";
|
|
2394
|
-
};
|
|
2395
|
-
var runProbe = async (spec, binary, runner, timeoutMs) => {
|
|
2396
|
-
if (!spec.probe || !runner) return "skipped";
|
|
2397
|
-
const [head, ...rest] = spec.probe;
|
|
2398
|
-
const argv = [head === spec.bin ? binary : head ?? binary, ...rest];
|
|
2399
|
-
try {
|
|
2400
|
-
const outcome = await runner.run(argv, { timeoutMs });
|
|
2401
|
-
return outcome.code === 0 && !outcome.timedOut ? "passed" : "failed";
|
|
2402
|
-
} catch {
|
|
2403
|
-
return "failed";
|
|
2404
|
-
}
|
|
2405
|
-
};
|
|
2406
|
-
var detectProviders = async (input) => {
|
|
2407
|
-
const env = input.env ?? process.env;
|
|
2408
|
-
const platform = input.platform ?? process.platform;
|
|
2409
|
-
const now4 = input.now ?? (() => /* @__PURE__ */ new Date());
|
|
2410
|
-
const results = [];
|
|
2411
|
-
for (const spec of input.providers) {
|
|
2412
|
-
const binary = findExecutable(spec.bin, env, platform);
|
|
2413
|
-
const hookState = input.agentHooks[spec.id] ?? "unknown";
|
|
2414
|
-
const usage = parseProviderUsage(input.accountList, spec.orcaUsageKey, input.exhaustedPercent ?? 100);
|
|
2415
|
-
const auth = authStatusFor(spec, usage, env);
|
|
2416
|
-
const cooldown = input.cooldowns?.[spec.id] ?? null;
|
|
2417
|
-
const coolingDownUntil = cooldown && Date.parse(cooldown) > now4().getTime() ? new Date(cooldown).toISOString() : null;
|
|
2418
|
-
const reasons = [];
|
|
2419
|
-
if (!binary) reasons.push(`binary "${spec.bin}" not found on PATH`);
|
|
2420
|
-
if (auth === "missing") reasons.push(spec.auth === "api-key" ? `none of ${spec.envKeys.join(", ") || "the configured env keys"} is set` : `Orca reports no ${spec.id} credentials`);
|
|
2421
|
-
if (usage.exhausted) reasons.push(`usage exhausted${usage.resetsAt ? ` until ${usage.resetsAt}` : ""}`);
|
|
2422
|
-
if (coolingDownUntil) reasons.push(`cooling down until ${coolingDownUntil}`);
|
|
2423
|
-
const probe = binary && !reasons.length ? await runProbe(spec, binary, input.runner, input.probeTimeoutMs ?? 15e3) : "skipped";
|
|
2424
|
-
if (probe === "failed") reasons.push("probe command failed");
|
|
2425
|
-
results.push({ id: spec.id, binary, hookState, auth, usage, probe, coolingDownUntil, available: reasons.length === 0, reasons });
|
|
2426
|
-
}
|
|
2427
|
-
return results;
|
|
2428
|
-
};
|
|
2429
|
-
var cooldownUntil = (attempt, initialMin, maxMin, from, resetsAt = null) => {
|
|
2430
|
-
const minutes2 = Math.min(maxMin, initialMin * 2 ** Math.max(0, attempt));
|
|
2431
|
-
const backoff = from.getTime() + minutes2 * 6e4;
|
|
2432
|
-
const reset = resetsAt ? Date.parse(resetsAt) : Number.NaN;
|
|
2433
|
-
return new Date(Number.isFinite(reset) && reset > from.getTime() ? Math.max(reset, backoff) : backoff).toISOString();
|
|
2434
|
-
};
|
|
2435
|
-
|
|
2436
2482
|
// src/adapters/linear-orca.ts
|
|
2437
2483
|
var isRecord8 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2438
2484
|
var str2 = (value, fallback = "") => typeof value === "string" ? value : fallback;
|
|
@@ -2583,6 +2629,41 @@ var LoopConfigSchema = z.object({
|
|
|
2583
2629
|
reviewer: tiers,
|
|
2584
2630
|
builder: tiers,
|
|
2585
2631
|
watcher: tiers,
|
|
2632
|
+
/** How candidates are ordered. `tiers` = YAML order (0.6 behaviour). `hybrid` = keep tiers, rank by remaining usage inside each. `dynamic` = flatten + usage. `catalog` = discover models via CLI/AA/builtin + usage. */
|
|
2633
|
+
routing: z.object({
|
|
2634
|
+
mode: z.enum(["tiers", "hybrid", "dynamic", "catalog"]).default("tiers"),
|
|
2635
|
+
/** Which usage window drives remaining%. `max` = most constrained window. */
|
|
2636
|
+
usageMetric: z.enum(["max", "session", "weekly", "monthly"]).default("max"),
|
|
2637
|
+
/** Prefer providers with live usage % over those with unknown usage (e.g. grok often has no %). */
|
|
2638
|
+
preferKnownUsage: z.boolean().default(true),
|
|
2639
|
+
excludeProviders: z.array(nonEmpty2).default([]),
|
|
2640
|
+
/** If non-empty, only these providers may be selected (still must be declared under providers). */
|
|
2641
|
+
includeProviders: z.array(nonEmpty2).default([]),
|
|
2642
|
+
/** Hard pin per role (`provider/model`). If pinned provider is unavailable, fall through unless pinStrict. */
|
|
2643
|
+
pin: z.object({
|
|
2644
|
+
orchestrator: modelRef.optional(),
|
|
2645
|
+
reviewer: modelRef.optional(),
|
|
2646
|
+
builder: modelRef.optional(),
|
|
2647
|
+
watcher: modelRef.optional()
|
|
2648
|
+
}).prefault({}),
|
|
2649
|
+
pinStrict: z.boolean().default(false)
|
|
2650
|
+
}).prefault({}),
|
|
2651
|
+
/** Quality band when `routing.mode: catalog` (and as soft bias in hybrid). */
|
|
2652
|
+
roles: z.object({
|
|
2653
|
+
orchestrator: z.object({ quality: z.enum(["frontier", "balanced", "fast"]).default("frontier"), preferCreators: z.array(nonEmpty2).default([]) }).prefault({}),
|
|
2654
|
+
reviewer: z.object({ quality: z.enum(["frontier", "balanced", "fast"]).default("frontier"), preferCreators: z.array(nonEmpty2).default([]) }).prefault({}),
|
|
2655
|
+
builder: z.object({ quality: z.enum(["frontier", "balanced", "fast"]).default("balanced"), preferCreators: z.array(nonEmpty2).default([]) }).prefault({}),
|
|
2656
|
+
watcher: z.object({ quality: z.enum(["frontier", "balanced", "fast"]).default("fast"), preferCreators: z.array(nonEmpty2).default([]) }).prefault({})
|
|
2657
|
+
}).prefault({}),
|
|
2658
|
+
catalog: z.object({
|
|
2659
|
+
sources: z.array(z.enum(["cli", "artificial-analysis", "builtin"])).default(["cli", "builtin"]),
|
|
2660
|
+
artificialAnalysis: z.object({
|
|
2661
|
+
enabled: z.boolean().default(false),
|
|
2662
|
+
apiKeyEnv: nonEmpty2.default("ARTIFICIAL_ANALYSIS_API_KEY"),
|
|
2663
|
+
cacheHours: z.number().positive().default(24),
|
|
2664
|
+
endpoint: nonEmpty2.default("https://artificialanalysis.ai/api/v2/data/llms/models")
|
|
2665
|
+
}).prefault({})
|
|
2666
|
+
}).prefault({}),
|
|
2586
2667
|
cooldown: z.object({
|
|
2587
2668
|
initialMin: z.number().int().positive().default(30),
|
|
2588
2669
|
maxMin: z.number().int().positive().default(240),
|
|
@@ -2646,6 +2727,16 @@ var LoopConfigSchema = z.object({
|
|
|
2646
2727
|
}).prefault({}),
|
|
2647
2728
|
maxFixRounds: z.number().int().min(0).default(2),
|
|
2648
2729
|
workerIdleTimeoutMin: z.number().int().positive().default(45),
|
|
2730
|
+
/**
|
|
2731
|
+
* When a worker goes idle / dies and its provider is out of usage (or otherwise unavailable),
|
|
2732
|
+
* relaunch another builder on the **same** Orca worktree + branch with a continuation brief.
|
|
2733
|
+
*/
|
|
2734
|
+
handoff: z.object({
|
|
2735
|
+
enabled: z.boolean().default(true),
|
|
2736
|
+
maxHandoffs: z.number().int().min(0).max(5).default(2),
|
|
2737
|
+
/** Only hand off when the current provider is unavailable (exhausted/cooldown/missing). */
|
|
2738
|
+
onlyWhenProviderUnavailable: z.boolean().default(true)
|
|
2739
|
+
}).prefault({}),
|
|
2649
2740
|
selfEditPaths: z.array(nonEmpty2).default([LOOP_CONFIG_FILE, ".github/**"]),
|
|
2650
2741
|
/** Check names ignored when deciding CI is green (e.g. advisory bots). */
|
|
2651
2742
|
ignoreChecks: z.array(nonEmpty2).default([]),
|
|
@@ -2871,28 +2962,395 @@ var assessSlots = (input) => {
|
|
|
2871
2962
|
};
|
|
2872
2963
|
|
|
2873
2964
|
// src/loop/routing.ts
|
|
2874
|
-
var
|
|
2965
|
+
var allowedProvider = (config, providerId) => {
|
|
2966
|
+
const { excludeProviders, includeProviders } = config.models.routing;
|
|
2967
|
+
if (excludeProviders.includes(providerId)) return false;
|
|
2968
|
+
if (includeProviders.length && !includeProviders.includes(providerId)) return false;
|
|
2969
|
+
return true;
|
|
2970
|
+
};
|
|
2971
|
+
var materialize = (config, ref, tier, preferenceIndex, availability, reason) => {
|
|
2972
|
+
const identity = providerIdentity(config, ref.provider);
|
|
2973
|
+
return {
|
|
2974
|
+
...ref,
|
|
2975
|
+
tier,
|
|
2976
|
+
preferenceIndex,
|
|
2977
|
+
orcaAgent: identity.orcaAgent,
|
|
2978
|
+
tui: renderTuiCommand(identity.settings, ref.model),
|
|
2979
|
+
remainingPercent: availability ? remainingUsagePercent(availability.usage, config.models.routing.usageMetric) : null,
|
|
2980
|
+
reason
|
|
2981
|
+
};
|
|
2982
|
+
};
|
|
2983
|
+
var compareUsageAware = (config, left, right, byId) => {
|
|
2984
|
+
const leftAv = byId.get(left.provider);
|
|
2985
|
+
const rightAv = byId.get(right.provider);
|
|
2986
|
+
const leftTuple = leftAv ? usageRankTuple(leftAv.usage, config.models.routing.usageMetric, config.models.routing.preferKnownUsage) : [1, 0, Number.POSITIVE_INFINITY];
|
|
2987
|
+
const rightTuple = rightAv ? usageRankTuple(rightAv.usage, config.models.routing.usageMetric, config.models.routing.preferKnownUsage) : [1, 0, Number.POSITIVE_INFINITY];
|
|
2988
|
+
for (let i = 0; i < leftTuple.length; i += 1) {
|
|
2989
|
+
if (leftTuple[i] !== rightTuple[i]) return leftTuple[i] - rightTuple[i];
|
|
2990
|
+
}
|
|
2991
|
+
if (left.preferenceIndex !== right.preferenceIndex) return left.preferenceIndex - right.preferenceIndex;
|
|
2992
|
+
return left.provider.localeCompare(right.provider) || left.model.localeCompare(right.model);
|
|
2993
|
+
};
|
|
2994
|
+
var availableFromTiers = (config, role, availability) => {
|
|
2875
2995
|
const byId = new Map(availability.map((item) => [item.id, item]));
|
|
2876
2996
|
const skipped = [];
|
|
2997
|
+
const ranked = [];
|
|
2998
|
+
let preferenceIndex = 0;
|
|
2877
2999
|
for (const [tier, refs] of tiersFor(config, role).entries()) {
|
|
2878
3000
|
for (const ref of refs) {
|
|
3001
|
+
const index2 = preferenceIndex;
|
|
3002
|
+
preferenceIndex += 1;
|
|
3003
|
+
if (!allowedProvider(config, ref.provider)) {
|
|
3004
|
+
skipped.push({ tier, ref, reasons: ["provider excluded by models.routing"] });
|
|
3005
|
+
continue;
|
|
3006
|
+
}
|
|
2879
3007
|
const provider = byId.get(ref.provider);
|
|
2880
3008
|
if (provider?.available) {
|
|
2881
|
-
|
|
2882
|
-
|
|
3009
|
+
ranked.push(materialize(config, ref, tier, index2, provider, `yaml tier ${tier + 1}`));
|
|
3010
|
+
} else {
|
|
3011
|
+
skipped.push({ tier, ref, reasons: provider ? provider.reasons : ["provider was not detected"] });
|
|
2883
3012
|
}
|
|
2884
|
-
skipped.push({ tier, ref, reasons: provider ? provider.reasons : ["provider was not detected"] });
|
|
2885
3013
|
}
|
|
2886
3014
|
}
|
|
2887
|
-
return {
|
|
3015
|
+
return { ranked, skipped };
|
|
2888
3016
|
};
|
|
2889
|
-
var
|
|
2890
|
-
|
|
3017
|
+
var applyPin = (config, role, availability, skipped) => {
|
|
3018
|
+
const pin = config.models.routing.pin[role];
|
|
3019
|
+
if (!pin) return null;
|
|
3020
|
+
const ref = parseModelRef(pin);
|
|
2891
3021
|
const byId = new Map(availability.map((item) => [item.id, item]));
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
return
|
|
2895
|
-
}
|
|
3022
|
+
const provider = byId.get(ref.provider);
|
|
3023
|
+
if (provider?.available && allowedProvider(config, ref.provider)) {
|
|
3024
|
+
return materialize(config, ref, -1, -1, provider, `pinned ${pin}`);
|
|
3025
|
+
}
|
|
3026
|
+
skipped.push({ tier: -1, ref, reasons: provider ? provider.reasons : ["pinned provider was not detected"] });
|
|
3027
|
+
if (config.models.routing.pinStrict) return null;
|
|
3028
|
+
return null;
|
|
3029
|
+
};
|
|
3030
|
+
var selectModel = (config, role, availability, extraCandidates = []) => {
|
|
3031
|
+
const byId = new Map(availability.map((item) => [item.id, item]));
|
|
3032
|
+
const mode = config.models.routing.mode;
|
|
3033
|
+
const { ranked: fromYaml, skipped } = availableFromTiers(config, role, availability);
|
|
3034
|
+
if (config.models.routing.pin[role]) {
|
|
3035
|
+
const pinned = applyPin(config, role, availability, skipped);
|
|
3036
|
+
if (pinned) return { role, selected: pinned, skipped };
|
|
3037
|
+
if (config.models.routing.pinStrict) return { role, selected: null, skipped };
|
|
3038
|
+
}
|
|
3039
|
+
const extras = [];
|
|
3040
|
+
let extraIndex = 1e4;
|
|
3041
|
+
for (const ref of extraCandidates) {
|
|
3042
|
+
if (!allowedProvider(config, ref.provider)) continue;
|
|
3043
|
+
const provider = byId.get(ref.provider);
|
|
3044
|
+
if (!provider?.available) continue;
|
|
3045
|
+
extras.push(materialize(config, ref, 99, extraIndex, provider, "catalog"));
|
|
3046
|
+
extraIndex += 1;
|
|
3047
|
+
}
|
|
3048
|
+
if (mode === "tiers") {
|
|
3049
|
+
const first = fromYaml[0] ?? extras[0] ?? null;
|
|
3050
|
+
return { role, selected: first ?? null, skipped };
|
|
3051
|
+
}
|
|
3052
|
+
if (mode === "hybrid") {
|
|
3053
|
+
const byTier = /* @__PURE__ */ new Map();
|
|
3054
|
+
for (const item of fromYaml) {
|
|
3055
|
+
const list2 = byTier.get(item.tier) ?? [];
|
|
3056
|
+
list2.push(item);
|
|
3057
|
+
byTier.set(item.tier, list2);
|
|
3058
|
+
}
|
|
3059
|
+
const tiers2 = [...byTier.keys()].sort((a, b) => a - b);
|
|
3060
|
+
for (const tier of tiers2) {
|
|
3061
|
+
const pool2 = byTier.get(tier) ?? [];
|
|
3062
|
+
pool2.sort((left, right) => compareUsageAware(config, left, right, byId));
|
|
3063
|
+
if (pool2[0]) {
|
|
3064
|
+
return {
|
|
3065
|
+
role,
|
|
3066
|
+
selected: {
|
|
3067
|
+
...pool2[0],
|
|
3068
|
+
reason: `hybrid tier ${tier + 1} \xB7 remaining ${pool2[0].remainingPercent ?? "unknown"}%`
|
|
3069
|
+
},
|
|
3070
|
+
skipped
|
|
3071
|
+
};
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
if (extras.length) {
|
|
3075
|
+
extras.sort((left, right) => compareUsageAware(config, left, right, byId));
|
|
3076
|
+
return { role, selected: { ...extras[0], reason: `hybrid catalog \xB7 remaining ${extras[0].remainingPercent ?? "unknown"}%` }, skipped };
|
|
3077
|
+
}
|
|
3078
|
+
return { role, selected: null, skipped };
|
|
3079
|
+
}
|
|
3080
|
+
const pool = [...fromYaml, ...extras];
|
|
3081
|
+
pool.sort((left, right) => compareUsageAware(config, left, right, byId));
|
|
3082
|
+
const best = pool[0] ?? null;
|
|
3083
|
+
return {
|
|
3084
|
+
role,
|
|
3085
|
+
selected: best ? { ...best, reason: `${mode} \xB7 remaining ${best.remainingPercent ?? "unknown"}% \xB7 ${best.reason}` } : null,
|
|
3086
|
+
skipped
|
|
3087
|
+
};
|
|
3088
|
+
};
|
|
3089
|
+
var routeAllRoles = (config, availability, extrasByRole = {}) => Object.fromEntries(MODEL_ROLES.map((role) => [role, selectModel(config, role, availability, extrasByRole[role] ?? [])]));
|
|
3090
|
+
var rankModels = (config, role, availability, extraCandidates = []) => {
|
|
3091
|
+
const byId = new Map(availability.map((item) => [item.id, item]));
|
|
3092
|
+
const { ranked } = availableFromTiers(config, role, availability);
|
|
3093
|
+
const extras = [];
|
|
3094
|
+
let extraIndex = 1e4;
|
|
3095
|
+
for (const ref of extraCandidates) {
|
|
3096
|
+
if (!allowedProvider(config, ref.provider)) continue;
|
|
3097
|
+
const provider = byId.get(ref.provider);
|
|
3098
|
+
if (!provider?.available) continue;
|
|
3099
|
+
extras.push(materialize(config, ref, 99, extraIndex, provider, "catalog"));
|
|
3100
|
+
extraIndex += 1;
|
|
3101
|
+
}
|
|
3102
|
+
const mode = config.models.routing.mode;
|
|
3103
|
+
if (mode === "tiers") return [...ranked, ...extras];
|
|
3104
|
+
if (mode === "hybrid") {
|
|
3105
|
+
const byTier = /* @__PURE__ */ new Map();
|
|
3106
|
+
for (const item of ranked) {
|
|
3107
|
+
const list2 = byTier.get(item.tier) ?? [];
|
|
3108
|
+
list2.push(item);
|
|
3109
|
+
byTier.set(item.tier, list2);
|
|
3110
|
+
}
|
|
3111
|
+
const ordered = [];
|
|
3112
|
+
for (const tier of [...byTier.keys()].sort((a, b) => a - b)) {
|
|
3113
|
+
const pool2 = byTier.get(tier) ?? [];
|
|
3114
|
+
pool2.sort((left, right) => compareUsageAware(config, left, right, byId));
|
|
3115
|
+
ordered.push(...pool2);
|
|
3116
|
+
}
|
|
3117
|
+
extras.sort((left, right) => compareUsageAware(config, left, right, byId));
|
|
3118
|
+
return [...ordered, ...extras];
|
|
3119
|
+
}
|
|
3120
|
+
const pool = [...ranked, ...extras];
|
|
3121
|
+
pool.sort((left, right) => compareUsageAware(config, left, right, byId));
|
|
3122
|
+
return pool;
|
|
3123
|
+
};
|
|
3124
|
+
|
|
3125
|
+
// src/loop/model-catalog/builtin.json
|
|
3126
|
+
var builtin_default = {
|
|
3127
|
+
providers: {
|
|
3128
|
+
claude: {
|
|
3129
|
+
creator: "anthropic",
|
|
3130
|
+
models: [
|
|
3131
|
+
{ id: "opus", quality: "frontier", codingScore: 90 },
|
|
3132
|
+
{ id: "sonnet", quality: "balanced", codingScore: 80 },
|
|
3133
|
+
{ id: "haiku", quality: "fast", codingScore: 55 }
|
|
3134
|
+
]
|
|
3135
|
+
},
|
|
3136
|
+
codex: {
|
|
3137
|
+
creator: "openai",
|
|
3138
|
+
models: [
|
|
3139
|
+
{ id: "gpt-5.6-sol", quality: "frontier", codingScore: 92 },
|
|
3140
|
+
{ id: "gpt-5.6-luna", quality: "balanced", codingScore: 78 },
|
|
3141
|
+
{ id: "gpt-5.4", quality: "balanced", codingScore: 70 }
|
|
3142
|
+
]
|
|
3143
|
+
},
|
|
3144
|
+
opencode: {
|
|
3145
|
+
creator: "opencode",
|
|
3146
|
+
models: [
|
|
3147
|
+
{ id: "opencode-go/glm-5.3", quality: "balanced", codingScore: 72 },
|
|
3148
|
+
{ id: "opencode-go/glm-5.3-flash", quality: "fast", codingScore: 58 }
|
|
3149
|
+
]
|
|
3150
|
+
},
|
|
3151
|
+
grok: {
|
|
3152
|
+
creator: "xai",
|
|
3153
|
+
models: [
|
|
3154
|
+
{ id: "grok-4.6", quality: "frontier", codingScore: 85 },
|
|
3155
|
+
{ id: "grok-4.5", quality: "balanced", codingScore: 75 },
|
|
3156
|
+
{ id: "grok-4-fast", quality: "fast", codingScore: 60 }
|
|
3157
|
+
]
|
|
3158
|
+
}
|
|
3159
|
+
}
|
|
3160
|
+
};
|
|
3161
|
+
|
|
3162
|
+
// src/loop/model-catalog/aliases.json
|
|
3163
|
+
var aliases_default = {
|
|
3164
|
+
aliases: {
|
|
3165
|
+
claude: {
|
|
3166
|
+
"claude-opus-4": "opus",
|
|
3167
|
+
"claude-sonnet-4": "sonnet",
|
|
3168
|
+
"claude-haiku-4": "haiku",
|
|
3169
|
+
"opus-4": "opus",
|
|
3170
|
+
"sonnet-4": "sonnet",
|
|
3171
|
+
"haiku-4": "haiku"
|
|
3172
|
+
},
|
|
3173
|
+
codex: {
|
|
3174
|
+
"gpt-5.6": "gpt-5.6-sol",
|
|
3175
|
+
o3: "gpt-5.6-sol"
|
|
3176
|
+
},
|
|
3177
|
+
grok: {
|
|
3178
|
+
"grok-4": "grok-4.5",
|
|
3179
|
+
"grok-4-latest": "grok-4.6"
|
|
3180
|
+
},
|
|
3181
|
+
opencode: {}
|
|
3182
|
+
}
|
|
3183
|
+
};
|
|
3184
|
+
|
|
3185
|
+
// src/loop/model-catalog/index.ts
|
|
3186
|
+
var readJson2 = (path) => JSON.parse(readFileSync(path, "utf8"));
|
|
3187
|
+
var loadBuiltinCatalog = () => {
|
|
3188
|
+
const raw = builtin_default;
|
|
3189
|
+
return Object.fromEntries(Object.entries(raw.providers).map(([id2, value]) => [id2, {
|
|
3190
|
+
creator: value.creator,
|
|
3191
|
+
models: value.models.map((model) => ({ ...model, source: "builtin", creator: value.creator }))
|
|
3192
|
+
}]));
|
|
3193
|
+
};
|
|
3194
|
+
var loadAliases = () => aliases_default.aliases;
|
|
3195
|
+
var resolveAlias = (provider, modelId, aliases = loadAliases()) => aliases[provider]?.[modelId] ?? aliases[provider]?.[modelId.toLowerCase()] ?? modelId;
|
|
3196
|
+
var parseGrokModelsOutput = (stdout) => {
|
|
3197
|
+
const models = [];
|
|
3198
|
+
for (const line2 of stdout.split(/\r?\n/)) {
|
|
3199
|
+
const match = line2.match(/^\s*[-*]?\s*(grok-[a-z0-9][a-z0-9._-]*)\b/i) ?? line2.match(/^\s*\*\s*(grok-[a-z0-9][a-z0-9._-]*)\b/i);
|
|
3200
|
+
if (match?.[1]) models.push(match[1]);
|
|
3201
|
+
}
|
|
3202
|
+
return [...new Set(models)];
|
|
3203
|
+
};
|
|
3204
|
+
var listCliModels = async (provider, bin, runner, timeoutMs = 2e4) => {
|
|
3205
|
+
if (provider === "grok") {
|
|
3206
|
+
const outcome = await runner.run([bin, "models"], { timeoutMs });
|
|
3207
|
+
if (outcome.code !== 0 && !outcome.stdout.trim()) return [];
|
|
3208
|
+
return parseGrokModelsOutput(`${outcome.stdout}
|
|
3209
|
+
${outcome.stderr}`);
|
|
3210
|
+
}
|
|
3211
|
+
return [];
|
|
3212
|
+
};
|
|
3213
|
+
var parseArtificialAnalysisPayload = (payload) => {
|
|
3214
|
+
const root = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
3215
|
+
const data = Array.isArray(root["data"]) ? root["data"] : Array.isArray(payload) ? payload : [];
|
|
3216
|
+
return data.flatMap((item) => {
|
|
3217
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return [];
|
|
3218
|
+
const row = item;
|
|
3219
|
+
const creator = row["model_creator"] && typeof row["model_creator"] === "object" && !Array.isArray(row["model_creator"]) ? row["model_creator"] : {};
|
|
3220
|
+
const evaluations = row["evaluations"] && typeof row["evaluations"] === "object" && !Array.isArray(row["evaluations"]) ? row["evaluations"] : {};
|
|
3221
|
+
const slug = typeof row["slug"] === "string" ? row["slug"] : typeof row["id"] === "string" ? row["id"] : null;
|
|
3222
|
+
if (!slug) return [];
|
|
3223
|
+
return [{
|
|
3224
|
+
slug,
|
|
3225
|
+
name: typeof row["name"] === "string" ? row["name"] : slug,
|
|
3226
|
+
creatorSlug: typeof creator["slug"] === "string" ? creator["slug"] : "unknown",
|
|
3227
|
+
codingIndex: typeof evaluations["artificial_analysis_coding_index"] === "number" ? evaluations["artificial_analysis_coding_index"] : null,
|
|
3228
|
+
intelligenceIndex: typeof evaluations["artificial_analysis_intelligence_index"] === "number" ? evaluations["artificial_analysis_intelligence_index"] : null
|
|
3229
|
+
}];
|
|
3230
|
+
});
|
|
3231
|
+
};
|
|
3232
|
+
var readAaCache = (stateDir) => {
|
|
3233
|
+
const path = join(stateDir, "catalog", "artificial-analysis.json");
|
|
3234
|
+
if (!existsSync(path)) return null;
|
|
3235
|
+
try {
|
|
3236
|
+
const raw = readJson2(path);
|
|
3237
|
+
return { fetchedAt: raw.fetchedAt, models: raw.models };
|
|
3238
|
+
} catch {
|
|
3239
|
+
return null;
|
|
3240
|
+
}
|
|
3241
|
+
};
|
|
3242
|
+
var writeAaCache = (stateDir, models) => {
|
|
3243
|
+
const path = join(stateDir, "catalog", "artificial-analysis.json");
|
|
3244
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
3245
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
3246
|
+
writeFileSync(tmp, `${JSON.stringify({ fetchedAt: (/* @__PURE__ */ new Date()).toISOString(), models }, null, 2)}
|
|
3247
|
+
`, "utf8");
|
|
3248
|
+
renameSync(tmp, path);
|
|
3249
|
+
};
|
|
3250
|
+
var fetchArtificialAnalysisModels = async (input) => {
|
|
3251
|
+
const controller = new AbortController();
|
|
3252
|
+
const timer = setTimeout(() => controller.abort(), input.timeoutMs ?? 2e4);
|
|
3253
|
+
try {
|
|
3254
|
+
const response = await fetch(input.endpoint, {
|
|
3255
|
+
headers: { "x-api-key": input.apiKey, accept: "application/json" },
|
|
3256
|
+
signal: controller.signal
|
|
3257
|
+
});
|
|
3258
|
+
if (!response.ok) throw new Error(`Artificial Analysis HTTP ${response.status}`);
|
|
3259
|
+
return parseArtificialAnalysisPayload(await response.json());
|
|
3260
|
+
} finally {
|
|
3261
|
+
clearTimeout(timer);
|
|
3262
|
+
}
|
|
3263
|
+
};
|
|
3264
|
+
var creatorForProvider = {
|
|
3265
|
+
claude: "anthropic",
|
|
3266
|
+
codex: "openai",
|
|
3267
|
+
grok: "xai",
|
|
3268
|
+
opencode: "opencode"
|
|
3269
|
+
};
|
|
3270
|
+
var qualityRank = { frontier: 3, balanced: 2, fast: 1 };
|
|
3271
|
+
var matchesQuality = (model, wanted) => {
|
|
3272
|
+
if (wanted === "frontier") return model.quality === "frontier" || model.codingScore >= 80;
|
|
3273
|
+
if (wanted === "balanced") return model.quality !== "fast" || model.codingScore >= 65;
|
|
3274
|
+
return true;
|
|
3275
|
+
};
|
|
3276
|
+
var resolveCatalogCandidates = async (input) => {
|
|
3277
|
+
const { config, role } = input;
|
|
3278
|
+
const policy = config.models.roles[role];
|
|
3279
|
+
const sources = config.models.catalog.sources;
|
|
3280
|
+
const builtin = loadBuiltinCatalog();
|
|
3281
|
+
const aliases = loadAliases();
|
|
3282
|
+
const byProvider = /* @__PURE__ */ new Map();
|
|
3283
|
+
const push = (provider, model) => {
|
|
3284
|
+
const list2 = byProvider.get(provider) ?? [];
|
|
3285
|
+
if (list2.some((item) => item.id === model.id)) return;
|
|
3286
|
+
list2.push(model);
|
|
3287
|
+
byProvider.set(provider, list2);
|
|
3288
|
+
};
|
|
3289
|
+
for (const provider of input.availableProviderIds) {
|
|
3290
|
+
if (sources.includes("builtin") && builtin[provider]) {
|
|
3291
|
+
for (const model of builtin[provider].models) push(provider, model);
|
|
3292
|
+
}
|
|
3293
|
+
if (sources.includes("cli") && input.runner) {
|
|
3294
|
+
const settings = config.models.providers[provider];
|
|
3295
|
+
if (settings) {
|
|
3296
|
+
try {
|
|
3297
|
+
const ids = await listCliModels(provider, settings.bin, input.runner);
|
|
3298
|
+
for (const id2 of ids) {
|
|
3299
|
+
const resolved = resolveAlias(provider, id2, aliases);
|
|
3300
|
+
const existing = builtin[provider]?.models.find((model) => model.id === resolved);
|
|
3301
|
+
push(provider, existing ?? { id: resolved, quality: "balanced", codingScore: 70, source: "cli", creator: creatorForProvider[provider] });
|
|
3302
|
+
}
|
|
3303
|
+
} catch {
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
if (sources.includes("artificial-analysis") && config.models.catalog.artificialAnalysis.enabled && input.stateDir) {
|
|
3309
|
+
const aa = config.models.catalog.artificialAnalysis;
|
|
3310
|
+
const env = input.env ?? process.env;
|
|
3311
|
+
const key = env[aa.apiKeyEnv]?.trim();
|
|
3312
|
+
let models = readAaCache(input.stateDir);
|
|
3313
|
+
const now4 = input.now ?? (() => /* @__PURE__ */ new Date());
|
|
3314
|
+
const stale = !models || now4().getTime() - Date.parse(models.fetchedAt) > aa.cacheHours * 36e5;
|
|
3315
|
+
if (key && stale) {
|
|
3316
|
+
try {
|
|
3317
|
+
const fresh = await fetchArtificialAnalysisModels({ endpoint: aa.endpoint, apiKey: key });
|
|
3318
|
+
writeAaCache(input.stateDir, fresh);
|
|
3319
|
+
models = { fetchedAt: now4().toISOString(), models: fresh };
|
|
3320
|
+
} catch {
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
if (models) {
|
|
3324
|
+
for (const provider of input.availableProviderIds) {
|
|
3325
|
+
const creator = creatorForProvider[provider] ?? provider;
|
|
3326
|
+
const matches2 = models.models.filter((model) => model.creatorSlug === creator || model.creatorSlug.includes(creator));
|
|
3327
|
+
for (const model of matches2) {
|
|
3328
|
+
const id2 = resolveAlias(provider, model.slug, aliases);
|
|
3329
|
+
const score = model.codingIndex ?? model.intelligenceIndex ?? 50;
|
|
3330
|
+
const quality = score >= 80 ? "frontier" : score >= 60 ? "balanced" : "fast";
|
|
3331
|
+
push(provider, { id: id2, quality, codingScore: score, source: "artificial-analysis", creator });
|
|
3332
|
+
}
|
|
3333
|
+
}
|
|
3334
|
+
}
|
|
3335
|
+
}
|
|
3336
|
+
const refs = [];
|
|
3337
|
+
for (const provider of input.availableProviderIds) {
|
|
3338
|
+
let models = byProvider.get(provider) ?? [];
|
|
3339
|
+
if (policy.preferCreators.length) {
|
|
3340
|
+
const preferred = models.filter((model) => model.creator && policy.preferCreators.includes(model.creator));
|
|
3341
|
+
if (preferred.length) models = preferred;
|
|
3342
|
+
}
|
|
3343
|
+
models = models.filter((model) => matchesQuality(model, policy.quality));
|
|
3344
|
+
models = [...models].sort((left, right) => {
|
|
3345
|
+
const qualityDelta = qualityRank[right.quality] - qualityRank[left.quality];
|
|
3346
|
+
if (qualityDelta) return qualityDelta;
|
|
3347
|
+
return right.codingScore - left.codingScore;
|
|
3348
|
+
});
|
|
3349
|
+
for (const model of models.slice(0, 3)) {
|
|
3350
|
+
refs.push(parseModelRef(`${provider}/${model.id}`));
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
return refs;
|
|
2896
3354
|
};
|
|
2897
3355
|
var cooldownPath = (stateDir) => join(stateDir, "provider-cooldowns.json");
|
|
2898
3356
|
var readCooldowns = (stateDir) => {
|
|
@@ -2963,11 +3421,31 @@ var runLoopDoctor = async (input) => {
|
|
|
2963
3421
|
]);
|
|
2964
3422
|
const cooldowns = activeCooldowns(readCooldowns(loaded.stateDir), now4());
|
|
2965
3423
|
const providers = await detectProviders({ providers: providerSpecs(config), accountList: accountList ?? {}, agentHooks, env: input.env, platform: input.platform, exhaustedPercent: config.models.cooldown.exhaustedPercent, cooldowns, now: now4, ...input.probe === false ? {} : { runner: input.runner } });
|
|
2966
|
-
for (const provider of providers)
|
|
2967
|
-
|
|
3424
|
+
for (const provider of providers) {
|
|
3425
|
+
const remaining = remainingUsagePercent(provider.usage, config.models.routing.usageMetric);
|
|
3426
|
+
const usageDetail = provider.usage.windows.length ? ` (${provider.usage.windows.map((window) => `${window.kind} ${window.usedPercent}%`).join(", ")}; remaining~${remaining ?? "?"}%)` : "";
|
|
3427
|
+
push(`provider.${provider.id}`, provider.available ? "passed" : "warning", provider.available ? `available${usageDetail}` : provider.reasons.join("; "));
|
|
3428
|
+
}
|
|
3429
|
+
const undeclared = undeclaredOrcaProviders(accountList ?? {}, Object.fromEntries(Object.entries(config.models.providers).map(([id2, settings]) => [id2, { orcaUsageKey: settings.orcaUsageKey ?? id2 }])));
|
|
3430
|
+
if (undeclared.length) push("orca.undeclared-providers", "warning", `Orca shows integrations without models.providers entries: ${undeclared.join(", ")} \u2014 add a provider block (bin/tui) or ignore`);
|
|
3431
|
+
const extrasByRole = config.models.routing.mode === "catalog" ? Object.fromEntries(await Promise.all(MODEL_ROLES.map(async (role) => [role, await resolveCatalogCandidates({
|
|
3432
|
+
config,
|
|
3433
|
+
role,
|
|
3434
|
+
availableProviderIds: providers.filter((provider) => provider.available).map((provider) => provider.id),
|
|
3435
|
+
runner: input.runner,
|
|
3436
|
+
stateDir: loaded.stateDir,
|
|
3437
|
+
env: input.env,
|
|
3438
|
+
now: now4
|
|
3439
|
+
})]))) : {};
|
|
3440
|
+
const routing = routeAllRoles(config, providers, extrasByRole);
|
|
2968
3441
|
for (const role of MODEL_ROLES) {
|
|
2969
3442
|
const decision = routing[role];
|
|
2970
|
-
|
|
3443
|
+
const selected = decision.selected;
|
|
3444
|
+
push(
|
|
3445
|
+
`routing.${role}`,
|
|
3446
|
+
selected ? "passed" : "failed",
|
|
3447
|
+
selected ? `${selected.provider}/${selected.model} \xB7 mode ${config.models.routing.mode} \xB7 ${selected.reason}${selected.remainingPercent !== null ? ` \xB7 remaining ${selected.remainingPercent}%` : ""}` : `no available provider (${decision.skipped.length} skipped; mode ${config.models.routing.mode})`
|
|
3448
|
+
);
|
|
2971
3449
|
}
|
|
2972
3450
|
let worktrees = [];
|
|
2973
3451
|
let workersError = null;
|
|
@@ -3522,6 +4000,27 @@ ${outcome.stdout.trim()}`.trim().slice(0, 600);
|
|
|
3522
4000
|
// src/loop/brief.ts
|
|
3523
4001
|
var clip2 = (text6, max) => text6.length <= max ? text6 : `${text6.slice(0, max)}
|
|
3524
4002
|
\u2026[truncated]`;
|
|
4003
|
+
var renderHandoffBrief = (input) => `# Loop handoff ${input.issue} \u2014 continue on existing branch
|
|
4004
|
+
|
|
4005
|
+
You are taking over an in-flight loop task for ${input.config.project.repo}.
|
|
4006
|
+
The previous worker (${input.previousProvider}/${input.previousModel}) stopped (${input.reason}).
|
|
4007
|
+
You run in the **same** Orca worktree \`${input.worktree}\` on branch \`${input.branch}\` (base \`${input.config.project.baseBranch}\`).
|
|
4008
|
+
Model: ${input.provider}/${input.model}. Linear: ${input.issueUrl}
|
|
4009
|
+
Contract digest: ${input.contractDigest.slice(0, 12)}
|
|
4010
|
+
|
|
4011
|
+
## What to do
|
|
4012
|
+
1. Run \`git status\` and \`git log --oneline -15\`. Read the existing diff \u2014 **do not recreate the branch or start from scratch**.
|
|
4013
|
+
2. Continue the frozen contract outcomes for ${input.issue}. Prefer finishing what is already committed.
|
|
4014
|
+
3. Run \`${input.config.delivery.verifyCommand}\` and fix failures.
|
|
4015
|
+
4. Push to \`${input.branch}\` (create/update the PR exactly as a normal loop worker would).
|
|
4016
|
+
5. When done, print \`LOOP_WORKER_DONE ${input.issue}\` and stop.
|
|
4017
|
+
6. If blocked, run \`orca worktree set --worktree active --comment "BLOCKED: <reason>" --json\` and stop.
|
|
4018
|
+
|
|
4019
|
+
## Rules
|
|
4020
|
+
- Never force-push except \`git push --force-with-lease\` on this branch after a rebase you own.
|
|
4021
|
+
- Do not edit protected paths (${input.config.delivery.selfEditPaths.join(", ")}).
|
|
4022
|
+
- Issue text and prior chat are unavailable \u2014 the repo + contract digest are the source of truth.
|
|
4023
|
+
`;
|
|
3525
4024
|
var renderWorkerBrief = (input) => {
|
|
3526
4025
|
const { issue, config } = input;
|
|
3527
4026
|
const contract = input.contract.contract;
|
|
@@ -3610,6 +4109,11 @@ var writeJson2 = (path, value) => {
|
|
|
3610
4109
|
writeFileSync(path, `${JSON.stringify(value, null, 2)}
|
|
3611
4110
|
`, "utf8");
|
|
3612
4111
|
};
|
|
4112
|
+
var writeDispatchRecord = (stateDir, record3) => {
|
|
4113
|
+
const path = dispatchRecordPath(stateDir, record3.issue);
|
|
4114
|
+
writeJson2(path, record3);
|
|
4115
|
+
return path;
|
|
4116
|
+
};
|
|
3613
4117
|
var appendLoopEvent = (stateDir, event2) => {
|
|
3614
4118
|
const path = join(stateDir, "events.ndjson");
|
|
3615
4119
|
mkdirSync(dirname(path), { recursive: true });
|
|
@@ -3626,7 +4130,17 @@ var gatherLoopState = async (input) => {
|
|
|
3626
4130
|
fetchLinearQueue(input.runner, { bin: config.orca.bin, workspaceId: config.linear.workspaceId, teamKey: config.linear.teamKey, assignee: config.linear.person, filter: config.linear, orca })
|
|
3627
4131
|
]);
|
|
3628
4132
|
const providers = await detectProviders({ providers: providerSpecs(config), accountList, agentHooks, env: input.env, platform: input.platform, exhaustedPercent: config.models.cooldown.exhaustedPercent, cooldowns: activeCooldowns(readCooldowns(input.loaded.stateDir), input.now()), now: input.now });
|
|
3629
|
-
const
|
|
4133
|
+
const availableIds = providers.filter((provider) => provider.available).map((provider) => provider.id);
|
|
4134
|
+
const extrasByRole = config.models.routing.mode === "catalog" ? Object.fromEntries(await Promise.all(MODEL_ROLES.map(async (role) => [role, await resolveCatalogCandidates({
|
|
4135
|
+
config,
|
|
4136
|
+
role,
|
|
4137
|
+
availableProviderIds: availableIds,
|
|
4138
|
+
runner: input.runner,
|
|
4139
|
+
stateDir: input.loaded.stateDir,
|
|
4140
|
+
env: input.env,
|
|
4141
|
+
now: input.now
|
|
4142
|
+
})]))) : {};
|
|
4143
|
+
const routing = routeAllRoles(config, providers, extrasByRole);
|
|
3630
4144
|
const running = countRunningWorkers(worktrees);
|
|
3631
4145
|
const slots = assessSlots({ machine: config.machine, running, platform: input.platform, ...input.machine });
|
|
3632
4146
|
const leases = input.ledger.active();
|
|
@@ -3668,7 +4182,16 @@ var runTick = async (input) => {
|
|
|
3668
4182
|
const results = [];
|
|
3669
4183
|
const state = await gatherLoopState({ loaded, runner: input.runner, ledger, env: input.env, platform: input.platform, now: now4, onlyIssue: input.onlyIssue, machine: input.machine });
|
|
3670
4184
|
const orchestrator = state.routing["orchestrator"] ?? { role: "orchestrator", selected: null, skipped: [] };
|
|
3671
|
-
const
|
|
4185
|
+
const orchestratorExtras = config.models.routing.mode === "catalog" ? await resolveCatalogCandidates({
|
|
4186
|
+
config,
|
|
4187
|
+
role: "orchestrator",
|
|
4188
|
+
availableProviderIds: state.providers.filter((provider) => provider.available).map((provider) => provider.id),
|
|
4189
|
+
runner: input.runner,
|
|
4190
|
+
stateDir: loaded.stateDir,
|
|
4191
|
+
env: input.env,
|
|
4192
|
+
now: now4
|
|
4193
|
+
}) : [];
|
|
4194
|
+
const orchestratorCandidates = rankModels(config, "orchestrator", state.providers, orchestratorExtras);
|
|
3672
4195
|
const onProviderFailure = (failure) => {
|
|
3673
4196
|
if (dryRun) return;
|
|
3674
4197
|
const entry = markProviderExhausted(loaded.stateDir, failure.provider, { initialMin: config.models.cooldown.initialMin, maxMin: config.models.cooldown.maxMin, reason: `${failure.kind}: ${(failure.detail.split("\n")[0] ?? "").slice(0, 200)}`, now: now4() });
|
|
@@ -3901,10 +4424,11 @@ var writeJson3 = (path, value) => {
|
|
|
3901
4424
|
var deliveryStatePath = (stateDir, identifier) => join(stateDir, "issues", identifier, "delivery.json");
|
|
3902
4425
|
var readDeliveryState = (stateDir, identifier) => {
|
|
3903
4426
|
const path = deliveryStatePath(stateDir, identifier);
|
|
3904
|
-
const empty = { issue: identifier, prNumber: null, reviews: {}, fixRounds: 0, nudges: [], heldFor: null, finishedAt: null, finalOutcome: null };
|
|
4427
|
+
const empty = { issue: identifier, prNumber: null, reviews: {}, fixRounds: 0, nudges: [], handoffs: [], heldFor: null, finishedAt: null, finalOutcome: null };
|
|
3905
4428
|
if (!existsSync(path)) return empty;
|
|
3906
4429
|
try {
|
|
3907
|
-
|
|
4430
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
4431
|
+
return { ...empty, ...parsed, handoffs: parsed.handoffs ?? [], nudges: parsed.nudges ?? [] };
|
|
3908
4432
|
} catch {
|
|
3909
4433
|
return empty;
|
|
3910
4434
|
}
|
|
@@ -3976,6 +4500,92 @@ var finish = (ctx, record3, lease, state, outcome, reason) => {
|
|
|
3976
4500
|
saveState(ctx, { ...state, finishedAt: ctx.now().toISOString(), finalOutcome: outcome });
|
|
3977
4501
|
event(ctx, { type: `worker.${outcome}`, issue: record3.issue, reason, worktreeId: record3.worktreeId });
|
|
3978
4502
|
};
|
|
4503
|
+
var providerUnavailable = (ctx, providerId) => {
|
|
4504
|
+
const match = ctx.providers.find((provider) => provider.id === providerId);
|
|
4505
|
+
return !match || !match.available;
|
|
4506
|
+
};
|
|
4507
|
+
var pickHandoffBuilder = (ctx, record3) => {
|
|
4508
|
+
const ranked = rankModels(ctx.config, "builder", ctx.providers);
|
|
4509
|
+
const different = ranked.find((candidate) => candidate.provider !== record3.provider || candidate.model !== record3.model);
|
|
4510
|
+
return different ?? null;
|
|
4511
|
+
};
|
|
4512
|
+
var canHandoff = (ctx, record3, state, next) => {
|
|
4513
|
+
const cfg = ctx.config.delivery.handoff;
|
|
4514
|
+
if (!cfg.enabled || !next) return false;
|
|
4515
|
+
if (state.handoffs.length >= cfg.maxHandoffs) return false;
|
|
4516
|
+
if (cfg.onlyWhenProviderUnavailable && !providerUnavailable(ctx, record3.provider)) return false;
|
|
4517
|
+
return true;
|
|
4518
|
+
};
|
|
4519
|
+
var performHandoff = async (ctx, record3, state, next, reason, actions) => {
|
|
4520
|
+
const brief = renderHandoffBrief({
|
|
4521
|
+
issue: record3.issue,
|
|
4522
|
+
issueUrl: record3.url,
|
|
4523
|
+
config: ctx.config,
|
|
4524
|
+
branch: record3.branch,
|
|
4525
|
+
worktree: record3.worktree,
|
|
4526
|
+
previousProvider: record3.provider,
|
|
4527
|
+
previousModel: record3.model,
|
|
4528
|
+
provider: next.provider,
|
|
4529
|
+
model: next.model,
|
|
4530
|
+
contractDigest: record3.contractDigest,
|
|
4531
|
+
reason
|
|
4532
|
+
});
|
|
4533
|
+
if (ctx.dryRun) {
|
|
4534
|
+
actions.push(`would hand off ${record3.provider}/${record3.model} \u2192 ${next.provider}/${next.model} on ${record3.branch}`);
|
|
4535
|
+
return { issue: record3.issue, outcome: "dry-run", reason: `handoff ready: ${reason}`, actions };
|
|
4536
|
+
}
|
|
4537
|
+
const title = `loop-handoff ${record3.issue} ${next.provider}`;
|
|
4538
|
+
const launched = await launchWorkerTerminal({
|
|
4539
|
+
runner: ctx.runner,
|
|
4540
|
+
config: ctx.config,
|
|
4541
|
+
worktreeId: record3.worktreeId,
|
|
4542
|
+
command: next.tui,
|
|
4543
|
+
title,
|
|
4544
|
+
brief
|
|
4545
|
+
});
|
|
4546
|
+
actions.push(`handed off to ${next.provider}/${next.model} on terminal ${launched.terminal}${launched.accepted ? "" : " (brief not confirmed)"}`);
|
|
4547
|
+
const updated = {
|
|
4548
|
+
...record3,
|
|
4549
|
+
terminal: launched.terminal,
|
|
4550
|
+
provider: next.provider,
|
|
4551
|
+
model: next.model
|
|
4552
|
+
};
|
|
4553
|
+
writeDispatchRecord(ctx.loaded.stateDir, updated);
|
|
4554
|
+
const handoff = {
|
|
4555
|
+
at: ctx.now().toISOString(),
|
|
4556
|
+
fromProvider: record3.provider,
|
|
4557
|
+
fromModel: record3.model,
|
|
4558
|
+
toProvider: next.provider,
|
|
4559
|
+
toModel: next.model,
|
|
4560
|
+
reason,
|
|
4561
|
+
terminal: launched.terminal
|
|
4562
|
+
};
|
|
4563
|
+
const nextState = {
|
|
4564
|
+
...state,
|
|
4565
|
+
handoffs: [...state.handoffs, handoff],
|
|
4566
|
+
nudges: [...state.nudges, { kind: "handoff", at: handoff.at, head: null }]
|
|
4567
|
+
};
|
|
4568
|
+
saveState(ctx, nextState);
|
|
4569
|
+
event(ctx, {
|
|
4570
|
+
type: "worker.handed-off",
|
|
4571
|
+
issue: record3.issue,
|
|
4572
|
+
from: `${record3.provider}/${record3.model}`,
|
|
4573
|
+
to: `${next.provider}/${next.model}`,
|
|
4574
|
+
worktreeId: record3.worktreeId,
|
|
4575
|
+
branch: record3.branch,
|
|
4576
|
+
reason,
|
|
4577
|
+
briefAccepted: launched.accepted
|
|
4578
|
+
});
|
|
4579
|
+
try {
|
|
4580
|
+
await orcaWorktreeSet(ctx.runner, {
|
|
4581
|
+
worktree: `id:${record3.worktreeId}`,
|
|
4582
|
+
comment: `LOOP HANDOFF: ${record3.provider}/${record3.model} \u2192 ${next.provider}/${next.model} (${reason})`
|
|
4583
|
+
}, orcaOptions(ctx.config));
|
|
4584
|
+
} catch (error) {
|
|
4585
|
+
actions.push(`Orca comment failed: ${message3(error)}`);
|
|
4586
|
+
}
|
|
4587
|
+
return { issue: record3.issue, outcome: "handed-off", reason: `handed off to ${next.provider}/${next.model}: ${reason}`, actions };
|
|
4588
|
+
};
|
|
3979
4589
|
var handleNoPullRequest = async (ctx, record3, lease, state) => {
|
|
3980
4590
|
const actions = [];
|
|
3981
4591
|
const now4 = ctx.now();
|
|
@@ -3992,8 +4602,13 @@ var handleNoPullRequest = async (ctx, record3, lease, state) => {
|
|
|
3992
4602
|
const sinceDispatch = minutesBetween(now4, record3.dispatchedAt);
|
|
3993
4603
|
const sinceOutput = Math.min(sinceDispatch, minutesBetween(now4, lastOutputAt));
|
|
3994
4604
|
const idleTimeout = ctx.config.delivery.workerIdleTimeoutMin;
|
|
4605
|
+
const nextBuilder = pickHandoffBuilder(ctx, record3);
|
|
4606
|
+
const unavailable = providerUnavailable(ctx, record3.provider);
|
|
3995
4607
|
if (!terminalAlive) {
|
|
3996
4608
|
if (sinceDispatch < 5) return { issue: record3.issue, outcome: "waiting", reason: "worker terminal not visible yet", actions };
|
|
4609
|
+
if (canHandoff(ctx, record3, state, nextBuilder)) {
|
|
4610
|
+
return performHandoff(ctx, record3, state, nextBuilder, unavailable ? "previous terminal gone and provider unavailable" : "previous terminal gone", actions);
|
|
4611
|
+
}
|
|
3997
4612
|
await escalateLinear(ctx, record3, "stuck", `**Loop: worker stuck** \u2014 the worker terminal for \`${record3.worktree}\` is gone and no pull request was opened. The worktree was preserved for inspection; the slot was released.`, actions);
|
|
3998
4613
|
finish(ctx, record3, lease, state, "stuck", "terminal gone before PR");
|
|
3999
4614
|
return { issue: record3.issue, outcome: ctx.dryRun ? "dry-run" : "stuck", reason: "worker terminal gone before a PR was opened", actions };
|
|
@@ -4006,7 +4621,12 @@ var handleNoPullRequest = async (ctx, record3, lease, state) => {
|
|
|
4006
4621
|
idle = false;
|
|
4007
4622
|
}
|
|
4008
4623
|
}
|
|
4009
|
-
if (!idle || sinceOutput < idleTimeout)
|
|
4624
|
+
if (!idle || sinceOutput < idleTimeout) {
|
|
4625
|
+
return { issue: record3.issue, outcome: "waiting", reason: idle ? `worker idle for ${Math.round(sinceOutput)} min (< ${idleTimeout})` : "worker active", actions };
|
|
4626
|
+
}
|
|
4627
|
+
if (canHandoff(ctx, record3, state, nextBuilder) && unavailable) {
|
|
4628
|
+
return performHandoff(ctx, record3, state, nextBuilder, `idle ${Math.round(sinceOutput)} min and ${record3.provider} unavailable (usage/cooldown)`, actions);
|
|
4629
|
+
}
|
|
4010
4630
|
const idleNudges = state.nudges.filter((nudge) => nudge.kind === "idle");
|
|
4011
4631
|
const lastNudge = idleNudges.at(-1);
|
|
4012
4632
|
if (!lastNudge || minutesBetween(now4, lastNudge.at) < idleTimeout) {
|
|
@@ -4016,6 +4636,9 @@ var handleNoPullRequest = async (ctx, record3, lease, state) => {
|
|
|
4016
4636
|
event(ctx, { type: "worker.nudged", issue: record3.issue, kind: "idle" });
|
|
4017
4637
|
return { issue: record3.issue, outcome: ctx.dryRun ? "dry-run" : sent ? "nudged" : "waiting", reason: "idle without PR; nudged once", actions };
|
|
4018
4638
|
}
|
|
4639
|
+
if (canHandoff(ctx, record3, state, nextBuilder)) {
|
|
4640
|
+
return performHandoff(ctx, record3, state, nextBuilder, `idle after nudge and ${record3.provider} unavailable`, actions);
|
|
4641
|
+
}
|
|
4019
4642
|
await escalateLinear(ctx, record3, "stuck", `**Loop: worker stuck** \u2014 idle for ${Math.round(sinceOutput)} minutes after a check-in, no pull request on \`${record3.branch}\`. Worktree \`${record3.worktree}\` was preserved; the slot was released and the issue returned to ${ctx.config.delivery.returnState}.`, actions);
|
|
4020
4643
|
finish(ctx, record3, lease, state, "stuck", "idle after nudge without PR");
|
|
4021
4644
|
return { issue: record3.issue, outcome: ctx.dryRun ? "dry-run" : "stuck", reason: "idle after nudge without PR", actions };
|
|
@@ -4169,7 +4792,10 @@ var runDeliver = async (input) => {
|
|
|
4169
4792
|
const orca = orcaOptions(config);
|
|
4170
4793
|
const [accountList, agentHooks] = await Promise.all([orcaAccountList(input.runner, orca).catch(() => ({})), orcaAgentHooks(input.runner, orca).catch(() => ({}))]);
|
|
4171
4794
|
const providers = await detectProviders({ providers: providerSpecs(config), accountList, agentHooks, env: input.env, platform: input.platform, exhaustedPercent: config.models.cooldown.exhaustedPercent, cooldowns: activeCooldowns(readCooldowns(loaded.stateDir), now4()), now: now4 });
|
|
4172
|
-
const
|
|
4795
|
+
const availableIds = providers.filter((provider) => provider.available).map((provider) => provider.id);
|
|
4796
|
+
const catalogExtras = async (role) => config.models.routing.mode === "catalog" ? resolveCatalogCandidates({ config, role, availableProviderIds: availableIds, runner: input.runner, stateDir: loaded.stateDir, env: input.env, now: now4 }) : Promise.resolve([]);
|
|
4797
|
+
const reviewer = rankModels(config, "reviewer", providers, await catalogExtras("reviewer"))[0] ?? null;
|
|
4798
|
+
const builder = rankModels(config, "builder", providers, await catalogExtras("builder"))[0] ?? null;
|
|
4173
4799
|
let env = input.env ?? process.env;
|
|
4174
4800
|
if (!env["GITHUB_TOKEN"] && !env["GH_TOKEN"]) {
|
|
4175
4801
|
try {
|
|
@@ -4180,7 +4806,7 @@ var runDeliver = async (input) => {
|
|
|
4180
4806
|
}
|
|
4181
4807
|
const reviewDeadlineMs = input.budgetMs ? Math.max(6e4, Math.min(config.delivery.review.deadlineMs, input.budgetMs - 9e4)) : config.delivery.review.deadlineMs;
|
|
4182
4808
|
if (reviewDeadlineMs < config.delivery.review.deadlineMs) notes.push(`review deadline capped to ${Math.round(reviewDeadlineMs / 1e3)}s to fit the stage budget`);
|
|
4183
|
-
const ctx = { loaded, config, runner: input.runner, now: now4, dryRun, reviewer, env, ...input.assumeIdle === void 0 ? {} : { assumeIdle: input.assumeIdle }, notes, reviewDeadlineMs };
|
|
4809
|
+
const ctx = { loaded, config, runner: input.runner, now: now4, dryRun, reviewer, builder, providers, env, ...input.assumeIdle === void 0 ? {} : { assumeIdle: input.assumeIdle }, notes, reviewDeadlineMs };
|
|
4184
4810
|
const ledger = createDispatchLedger(loaded.stateDir);
|
|
4185
4811
|
const leases = new Map(ledger.active().map((lease) => [lease.issue, lease]));
|
|
4186
4812
|
const results = [];
|