@agentskit/harness 0.10.0 → 0.12.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 +32 -0
- package/README.md +1 -1
- package/capabilities/public-surface.json +107 -84
- package/dist/cli.js +407 -73
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +211 -56
- package/dist/index.js +373 -85
- package/dist/index.js.map +1 -1
- package/docs/ADR-0003-doc-bridge-context-binding.md +10 -3
- package/docs/ADR-0031-loop-observability.md +36 -0
- package/docs/LOOP.md +7 -0
- package/docs/MODULE-BOUNDARIES.md +1 -0
- package/loop.config.example.yaml +1 -1
- package/package.json +2 -2
- package/release/manifest.json +3 -3
- package/release/notes.md +19 -0
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createHash, randomUUID, createPrivateKey, createPublicKey, sign, verify } from 'crypto';
|
|
3
|
-
import { readFileSync, mkdirSync, existsSync, unlinkSync, openSync, writeFileSync, closeSync, readdirSync, writeSync, appendFileSync, mkdtempSync, renameSync, rmSync
|
|
4
|
-
import { Command } from 'commander';
|
|
3
|
+
import { readFileSync, mkdirSync, existsSync, unlinkSync, openSync, writeFileSync, closeSync, readdirSync, statSync, writeSync, appendFileSync, mkdtempSync, renameSync, rmSync } from 'fs';
|
|
5
4
|
import { resolve, dirname, relative, basename, join, extname, isAbsolute, delimiter, sep } from 'path';
|
|
5
|
+
import { Command } from 'commander';
|
|
6
6
|
import { execFile, spawn, execFileSync } from 'child_process';
|
|
7
7
|
import { promisify } from 'util';
|
|
8
8
|
import { tmpdir, totalmem, release, freemem, cpus, loadavg } from 'os';
|
|
@@ -921,11 +921,34 @@ var cleanTaskArtifacts = (configPath) => cleanConfiguredArtifacts(loadConfig(con
|
|
|
921
921
|
var index = (root, indexPath) => JSON.parse(readFileSync(resolve(root, indexPath), "utf8"));
|
|
922
922
|
var text = (entry) => [entry.id, entry.type, entry.title, entry.path, entry.description, entry.body, ...Array.isArray(entry.tags) ? entry.tags : []].filter((value) => typeof value === "string").join(" ").toLowerCase();
|
|
923
923
|
var sourceHash = (document) => typeof document.contentHash === "string" && document.contentHash.length > 0 ? document.contentHash : hashJson(document);
|
|
924
|
+
var tokenSeparator = /[^\p{L}\p{N}@/_-]+/gu;
|
|
925
|
+
var tokenize = (value) => value.toLowerCase().split(tokenSeparator).filter((token) => token.length >= 2);
|
|
926
|
+
var containsToken = (value, token) => {
|
|
927
|
+
if (/[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}]/u.test(token)) return value.includes(token);
|
|
928
|
+
const escaped = token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
929
|
+
return new RegExp(`(?:^|[^\\p{L}\\p{N}])${escaped}(?:s|es)?(?:[^\\p{L}\\p{N}]|$)`, "u").test(value);
|
|
930
|
+
};
|
|
931
|
+
var score = (value, query) => tokenize(query).reduce((total, token) => total + (containsToken(value, token) ? token.length : 0), 0);
|
|
924
932
|
var matches = (entry, query) => {
|
|
925
|
-
const needle = query.query.trim()
|
|
926
|
-
|
|
933
|
+
const needle = query.query.trim();
|
|
934
|
+
if (!needle) return 0;
|
|
927
935
|
const value = text(entry);
|
|
928
|
-
|
|
936
|
+
if (query.scope?.length && !query.scope.some((scope) => containsToken(value, scope.toLowerCase()))) return 0;
|
|
937
|
+
return score(value, needle);
|
|
938
|
+
};
|
|
939
|
+
var ownershipEntries = (document) => {
|
|
940
|
+
if (typeof document.lookup !== "object" || document.lookup === null || Array.isArray(document.lookup)) return [];
|
|
941
|
+
const ownership = document.lookup.ownership;
|
|
942
|
+
if (typeof ownership !== "object" || ownership === null || Array.isArray(ownership)) return [];
|
|
943
|
+
return Object.entries(ownership).flatMap(([id2, value]) => {
|
|
944
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return [];
|
|
945
|
+
const owner = value;
|
|
946
|
+
const path = typeof owner["agentDoc"] === "string" ? owner["agentDoc"] : owner["path"];
|
|
947
|
+
if (!path) return [];
|
|
948
|
+
const description = typeof owner["purpose"] === "string" ? owner["purpose"] : void 0;
|
|
949
|
+
const body2 = [owner["purpose"], owner["group"], owner["layer"], owner["agentDoc"], owner["humanDoc"]].filter((item) => typeof item === "string").join(" ");
|
|
950
|
+
return [{ id: typeof owner["id"] === "string" ? owner["id"] : id2, type: "ownership", path, ...description ? { description } : {}, ...body2 ? { body: body2 } : {} }];
|
|
951
|
+
});
|
|
929
952
|
};
|
|
930
953
|
var inspectDocBridgeIndex = (root, indexPath = ".doc-bridge/index.json", now4 = Date.now()) => {
|
|
931
954
|
const path = resolve(root, indexPath);
|
|
@@ -940,15 +963,30 @@ var inspectDocBridgeIndex = (root, indexPath = ".doc-bridge/index.json", now4 =
|
|
|
940
963
|
return { present: true, path, contentHash: null, mtimeMs: null, ageHours: null, error: error instanceof Error ? error.message : String(error) };
|
|
941
964
|
}
|
|
942
965
|
};
|
|
943
|
-
var createDocBridgeContextProvider = ({ root, indexPath = ".doc-bridge/index.json" }) => ({
|
|
966
|
+
var createDocBridgeContextProvider = ({ root, indexPath = ".doc-bridge/index.json", maxAgeHours, now: now4 = Date.now }) => ({
|
|
944
967
|
id: "doc-bridge",
|
|
945
|
-
version: "1.
|
|
968
|
+
version: "1.1.0",
|
|
946
969
|
resolve: async (query) => {
|
|
947
970
|
const started = Date.now();
|
|
971
|
+
const ageBudget = maxAgeHours ?? 0;
|
|
972
|
+
const inspection = ageBudget > 0 ? inspectDocBridgeIndex(root, indexPath, now4()) : null;
|
|
973
|
+
if (inspection?.error) throw new Error(`Doc Bridge index is unreadable: ${inspection.error}`);
|
|
974
|
+
if (inspection?.ageHours !== null && inspection?.ageHours !== void 0 && inspection.ageHours > ageBudget) {
|
|
975
|
+
throw new Error(`Doc Bridge index is ${inspection.ageHours.toFixed(1)}h old; refresh it before resolving context.`);
|
|
976
|
+
}
|
|
948
977
|
const document = index(root, indexPath);
|
|
949
978
|
const contentHash = sourceHash(document);
|
|
950
|
-
const
|
|
951
|
-
const
|
|
979
|
+
const knowledge = Array.isArray(document.knowledge) ? document.knowledge.filter((value) => typeof value === "object" && value !== null && !Array.isArray(value)) : [];
|
|
980
|
+
const ranked = [...knowledge, ...ownershipEntries(document)].map((entry) => ({ entry, score: matches(entry, query) })).filter(({ score: entryScore }) => entryScore > 0);
|
|
981
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
982
|
+
for (const candidate of ranked) {
|
|
983
|
+
const path = typeof candidate.entry.path === "string" ? candidate.entry.path : String(candidate.entry.id ?? "");
|
|
984
|
+
const current = byPath.get(path);
|
|
985
|
+
if (!current || candidate.score > current.score || candidate.score === current.score && candidate.entry.type === "ownership" && current.entry.type !== "ownership") byPath.set(path, candidate);
|
|
986
|
+
}
|
|
987
|
+
const entries = [...byPath.values()].sort((left, right) => right.score - left.score || String(left.entry.id ?? "").localeCompare(String(right.entry.id ?? ""))).slice(0, 8);
|
|
988
|
+
const maxScore = entries[0]?.score ?? 1;
|
|
989
|
+
const references = entries.flatMap(({ entry, score: entryScore }) => typeof entry.id === "string" && typeof entry.path === "string" ? [{ id: entry.id, uri: `doc-bridge://${entry.path}`, ...typeof entry.title === "string" ? { title: entry.title } : {}, contentHash: typeof entry.contentHash === "string" ? entry.contentHash : contentHash, relevance: entryScore / maxScore }] : []);
|
|
952
990
|
const telemetry = { status: "measured", durationMs: Date.now() - started, contextReferences: references.length, contextCostTokens: Math.max(1, Math.ceil(JSON.stringify(references).length / 4)) };
|
|
953
991
|
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
992
|
}
|
|
@@ -1543,6 +1581,14 @@ var createKvMemoryAdapter = (store, options2 = {}) => {
|
|
|
1543
1581
|
const scopeMatch = record3.scope === "global" || (record3.scope === "issue" ? Boolean(issueId && record3.source.includes(issueId)) : Boolean(project && record3.source.includes(project)));
|
|
1544
1582
|
return scopeMatch && (!query || `${record3.summary} ${record3.source}`.toLowerCase().includes(query));
|
|
1545
1583
|
};
|
|
1584
|
+
let allRecordsCache = null;
|
|
1585
|
+
const allRecords = async () => {
|
|
1586
|
+
if (allRecordsCache) return allRecordsCache;
|
|
1587
|
+
const ids = await store.get(indexKey);
|
|
1588
|
+
const records = Array.isArray(ids) ? await Promise.all(ids.filter((id2) => typeof id2 === "string").map((id2) => store.get(`agentskit-harness:memory:${id2}`))) : [];
|
|
1589
|
+
allRecordsCache = records.filter((record3) => Boolean(record3 && typeof record3 === "object" && record3.approved === true));
|
|
1590
|
+
return allRecordsCache;
|
|
1591
|
+
};
|
|
1546
1592
|
return {
|
|
1547
1593
|
id: options2.id ?? "agentskit-kv",
|
|
1548
1594
|
version: options2.version ?? "1",
|
|
@@ -1554,13 +1600,13 @@ var createKvMemoryAdapter = (store, options2 = {}) => {
|
|
|
1554
1600
|
const index2 = Array.isArray(ids) ? ids.filter((id2) => typeof id2 === "string") : [];
|
|
1555
1601
|
if (!index2.includes(valid.id)) await store.set(indexKey, [...index2, valid.id].sort());
|
|
1556
1602
|
await store.set(`agentskit-harness:memory:${valid.id}`, valid);
|
|
1603
|
+
allRecordsCache = null;
|
|
1557
1604
|
writes += 1;
|
|
1558
1605
|
},
|
|
1559
1606
|
async recall({ query, issueId, project, sourceRevision }) {
|
|
1560
1607
|
reads += 1;
|
|
1561
|
-
const
|
|
1562
|
-
const
|
|
1563
|
-
const hits = records.filter((record3) => Boolean(record3 && typeof record3 === "object" && record3.approved === true)).filter((record3) => matches2(record3, query.trim().toLowerCase(), issueId, project)).map((record3) => ({ record: record3, relevant: true, stale: sourceRevision !== void 0 && record3.sourceRevision !== sourceRevision }));
|
|
1608
|
+
const records = await allRecords();
|
|
1609
|
+
const hits = records.filter((record3) => matches2(record3, query.trim().toLowerCase(), issueId, project)).map((record3) => ({ record: record3, relevant: true, stale: sourceRevision !== void 0 && record3.sourceRevision !== sourceRevision }));
|
|
1564
1610
|
relevantHits += hits.length;
|
|
1565
1611
|
staleHits += hits.filter((hit) => hit.stale).length;
|
|
1566
1612
|
return hits;
|
|
@@ -2457,6 +2503,21 @@ var orcaStatus = async (runner, options2 = {}) => parseOrcaStatus(await orcaJson
|
|
|
2457
2503
|
var orcaWorktrees = async (runner, options2 = {}) => parseOrcaWorktrees(await orcaJson(runner, ["worktree", "ps"], options2));
|
|
2458
2504
|
var orcaAgentHooks = async (runner, options2 = {}) => parseOrcaAgentHooks(await orcaJson(runner, ["agent", "hooks", "status"], options2));
|
|
2459
2505
|
var orcaAccountList = async (runner, options2 = {}) => orcaJson(runner, ["account", "list"], options2);
|
|
2506
|
+
var orcaDiagnosticsMemory = async (runner, options2 = {}) => {
|
|
2507
|
+
try {
|
|
2508
|
+
const result = await orcaJson(runner, ["diagnostics", "memory"], options2);
|
|
2509
|
+
if (!isRecord7(result)) return null;
|
|
2510
|
+
const host = isRecord7(result["host"]) ? result["host"] : {};
|
|
2511
|
+
const availableBytes = host["availableMemory"];
|
|
2512
|
+
if (typeof availableBytes !== "number" || !Number.isFinite(availableBytes) || availableBytes <= 0) return null;
|
|
2513
|
+
const totalBytes = typeof host["totalMemory"] === "number" ? host["totalMemory"] : null;
|
|
2514
|
+
const worktrees = Array.isArray(result["worktrees"]) ? result["worktrees"] : [];
|
|
2515
|
+
const agentRssSamples = worktrees.filter(isRecord7).flatMap((worktree) => Array.isArray(worktree["sessions"]) ? worktree["sessions"] : []).filter(isRecord7).map((session) => session["memory"]).filter((value) => typeof value === "number" && Number.isFinite(value) && value > 0);
|
|
2516
|
+
return { availableBytes, totalBytes, agentRssSamples };
|
|
2517
|
+
} catch {
|
|
2518
|
+
return null;
|
|
2519
|
+
}
|
|
2520
|
+
};
|
|
2460
2521
|
var parseOrcaWorktreeCreate = (result) => {
|
|
2461
2522
|
const record3 = isRecord7(result) ? result : {};
|
|
2462
2523
|
const nested = isRecord7(record3["worktree"]) ? record3["worktree"] : record3;
|
|
@@ -2514,10 +2575,16 @@ var orcaTerminalCreate = async (runner, input, options2 = {}) => {
|
|
|
2514
2575
|
};
|
|
2515
2576
|
var parseOrcaSendReceipt = (result) => {
|
|
2516
2577
|
const record3 = isRecord7(result) ? result : {};
|
|
2517
|
-
const
|
|
2518
|
-
const
|
|
2519
|
-
const
|
|
2520
|
-
|
|
2578
|
+
const send = isRecord7(record3["send"]) ? record3["send"] : null;
|
|
2579
|
+
const prompt = send && isRecord7(send["prompt"]) ? send["prompt"] : null;
|
|
2580
|
+
const receipt = isRecord7(record3["receipt"]) ? record3["receipt"] : send ?? record3;
|
|
2581
|
+
const rawStages = Array.isArray(receipt["stages"]) ? receipt["stages"] : prompt && Array.isArray(prompt["stages"]) ? prompt["stages"] : [];
|
|
2582
|
+
const stages = rawStages.map((stage) => isRecord7(stage) ? str(stage["stage"], str(stage["name"])) : str(stage)).filter(Boolean);
|
|
2583
|
+
const inputAccepted = stages.some((stage) => ["input_accepted", "input_queued", "prompt_accepted", "queued"].includes(stage.toLowerCase()));
|
|
2584
|
+
const acceptedValue = receipt["accepted"] ?? send?.["accepted"];
|
|
2585
|
+
const accepted = inputAccepted || acceptedValue === true || acceptedValue !== false && (result === null || result === void 0 || Object.keys(record3).length === 0);
|
|
2586
|
+
const warnings = Array.isArray(record3["warnings"]) ? record3["warnings"] : send && Array.isArray(send["warnings"]) ? send["warnings"] : [];
|
|
2587
|
+
return { accepted, requestId: str(receipt["requestId"], str(prompt?.["requestId"], str(record3["requestId"]))) || null, stages, warnings: warnings.map((warning) => isRecord7(warning) ? str(warning["message"], JSON.stringify(warning)) : str(warning)) };
|
|
2521
2588
|
};
|
|
2522
2589
|
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 }));
|
|
2523
2590
|
var orcaTerminalWait = async (runner, input, options2 = {}) => {
|
|
@@ -2526,6 +2593,12 @@ var orcaTerminalWait = async (runner, input, options2 = {}) => {
|
|
|
2526
2593
|
const wait = isRecord7(record3["wait"]) ? record3["wait"] : record3;
|
|
2527
2594
|
return { satisfied: wait["satisfied"] === true, raw: result };
|
|
2528
2595
|
};
|
|
2596
|
+
var orcaTerminalScreen = async (runner, input, options2 = {}) => {
|
|
2597
|
+
const result = await orcaJson(runner, ["terminal", "read", "--terminal", input.terminal, "--screen"], options2);
|
|
2598
|
+
const record3 = isRecord7(result) ? isRecord7(result["terminal"]) ? result["terminal"] : result : {};
|
|
2599
|
+
const screen = record3["tail"] ?? record3["screen"] ?? record3["lines"] ?? record3["text"] ?? record3["output"];
|
|
2600
|
+
return Array.isArray(screen) ? screen.map((line2) => isRecord7(line2) ? str(line2["text"], str(line2["line"])) : String(line2)).join("\n") : typeof screen === "string" ? screen : "";
|
|
2601
|
+
};
|
|
2529
2602
|
var parseOrcaAutomations = (result) => {
|
|
2530
2603
|
const list2 = isRecord7(result) ? Array.isArray(result["automations"]) ? result["automations"] : Array.isArray(result["items"]) ? result["items"] : [] : Array.isArray(result) ? result : [];
|
|
2531
2604
|
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);
|
|
@@ -2770,6 +2843,8 @@ var LoopConfigSchema = z.object({
|
|
|
2770
2843
|
}).prefault({}),
|
|
2771
2844
|
catalog: z.object({
|
|
2772
2845
|
sources: z.array(z.enum(["cli", "artificial-analysis", "builtin"])).default(["cli", "builtin"]),
|
|
2846
|
+
/** How long a provider's CLI-discovered model list (e.g. `grok models`) is trusted before spawning the CLI again — it rarely changes between releases. */
|
|
2847
|
+
cliCacheHours: z.number().positive().default(6),
|
|
2773
2848
|
artificialAnalysis: z.object({
|
|
2774
2849
|
enabled: z.boolean().default(false),
|
|
2775
2850
|
apiKeyEnv: nonEmpty2.default("ARTIFICIAL_ANALYSIS_API_KEY"),
|
|
@@ -3140,8 +3215,8 @@ var isWsl = (platform = process.platform, osRelease = release(), env = process.e
|
|
|
3140
3215
|
var assessSlots = (input) => {
|
|
3141
3216
|
const platform = input.platform ?? process.platform;
|
|
3142
3217
|
const wsl = isWsl(platform, input.osRelease);
|
|
3143
|
-
const freeBytes = input.freeBytes ?? availableMemoryBytes(platform);
|
|
3144
|
-
const totalBytes = input.totalBytes ?? totalmem();
|
|
3218
|
+
const freeBytes = input.freeBytes ?? input.orcaMemory?.availableBytes ?? availableMemoryBytes(platform);
|
|
3219
|
+
const totalBytes = input.totalBytes ?? input.orcaMemory?.totalBytes ?? totalmem();
|
|
3145
3220
|
const sample = input.sample ?? { ...sampleMachine(), memoryUsedPercent: Number(Math.max(0, Math.min(100, (1 - freeBytes / Math.max(1, totalBytes)) * 100)).toFixed(2)) };
|
|
3146
3221
|
const freeRamGb = Number((freeBytes / 1024 ** 3).toFixed(2));
|
|
3147
3222
|
const reasons = [];
|
|
@@ -3149,9 +3224,11 @@ var assessSlots = (input) => {
|
|
|
3149
3224
|
const adaptive = adaptiveConcurrency(ceiling, sample, { warningPercent: input.machine.warningPercent, criticalPercent: input.machine.criticalPercent });
|
|
3150
3225
|
if (adaptive < ceiling) reasons.push(`machine pressure capped concurrency at ${adaptive} (load ${sample.load1PerCpuPercent}%, memory ${sample.memoryUsedPercent}%)`);
|
|
3151
3226
|
const reservedBytes = input.machine.minFreeRamGb * 1024 ** 3;
|
|
3152
|
-
const
|
|
3227
|
+
const measuredAgentBytes = input.orcaMemory?.agentRssSamples.length ? input.orcaMemory.agentRssSamples.reduce((total, value) => total + value, 0) / input.orcaMemory.agentRssSamples.length : null;
|
|
3228
|
+
const perAgentBytes = measuredAgentBytes ?? input.machine.agentRssMb * 1024 ** 2;
|
|
3229
|
+
const perAgentMb = Math.round(perAgentBytes / 1024 ** 2);
|
|
3153
3230
|
const ramBound = Math.max(0, Math.floor((freeBytes - reservedBytes) / perAgentBytes)) + input.running;
|
|
3154
|
-
if (ramBound < adaptive) reasons.push(`free RAM ${freeRamGb} GB minus ${input.machine.minFreeRamGb} GB reserve fits ${Math.max(0, ramBound - input.running)} more agent(s) at ${
|
|
3231
|
+
if (ramBound < adaptive) reasons.push(`free RAM ${freeRamGb} GB minus ${input.machine.minFreeRamGb} GB reserve fits ${Math.max(0, ramBound - input.running)} more agent(s) at ${perAgentMb} MB each${measuredAgentBytes ? " (measured)" : ""}`);
|
|
3155
3232
|
let maxAgents = Math.min(adaptive, ramBound);
|
|
3156
3233
|
if (wsl && maxAgents > input.machine.wslCap) {
|
|
3157
3234
|
maxAgents = input.machine.wslCap;
|
|
@@ -3292,7 +3369,12 @@ var selectModel = (config, role, availability, extraCandidates = []) => {
|
|
|
3292
3369
|
var routeAllRoles = (config, availability, extrasByRole = {}) => Object.fromEntries(MODEL_ROLES.map((role) => [role, selectModel(config, role, availability, extrasByRole[role] ?? [])]));
|
|
3293
3370
|
var rankModels = (config, role, availability, extraCandidates = []) => {
|
|
3294
3371
|
const byId = new Map(availability.map((item) => [item.id, item]));
|
|
3295
|
-
const { ranked } = availableFromTiers(config, role, availability);
|
|
3372
|
+
const { ranked, skipped } = availableFromTiers(config, role, availability);
|
|
3373
|
+
if (config.models.routing.pin[role]) {
|
|
3374
|
+
const pinned = applyPin(config, role, availability, skipped);
|
|
3375
|
+
if (pinned) return [pinned, ...ranked.filter((item) => !(item.provider === pinned.provider && item.model === pinned.model))];
|
|
3376
|
+
if (config.models.routing.pinStrict) return [];
|
|
3377
|
+
}
|
|
3296
3378
|
const extras = [];
|
|
3297
3379
|
let extraIndex = 1e4;
|
|
3298
3380
|
for (const ref of extraCandidates) {
|
|
@@ -3413,6 +3495,32 @@ ${outcome.stderr}`);
|
|
|
3413
3495
|
}
|
|
3414
3496
|
return [];
|
|
3415
3497
|
};
|
|
3498
|
+
var cliModelsCachePath = (stateDir, provider) => join(stateDir, "catalog", `cli-${provider}.json`);
|
|
3499
|
+
var readCliModelsCache = (stateDir, provider) => {
|
|
3500
|
+
const path = cliModelsCachePath(stateDir, provider);
|
|
3501
|
+
if (!existsSync(path)) return null;
|
|
3502
|
+
try {
|
|
3503
|
+
const raw = readJson2(path);
|
|
3504
|
+
return typeof raw.fetchedAt === "string" && Array.isArray(raw.ids) ? { fetchedAt: raw.fetchedAt, ids: raw.ids } : null;
|
|
3505
|
+
} catch {
|
|
3506
|
+
return null;
|
|
3507
|
+
}
|
|
3508
|
+
};
|
|
3509
|
+
var writeCliModelsCache = (stateDir, provider, ids, now4 = /* @__PURE__ */ new Date()) => {
|
|
3510
|
+
const path = cliModelsCachePath(stateDir, provider);
|
|
3511
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
3512
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
3513
|
+
writeFileSync(tmp, `${JSON.stringify({ fetchedAt: now4.toISOString(), ids }, null, 2)}
|
|
3514
|
+
`, "utf8");
|
|
3515
|
+
renameSync(tmp, path);
|
|
3516
|
+
};
|
|
3517
|
+
var listCliModelsCached = async (provider, bin, runner, stateDir, cacheHours, now4 = () => /* @__PURE__ */ new Date()) => {
|
|
3518
|
+
const cached = readCliModelsCache(stateDir, provider);
|
|
3519
|
+
if (cached && now4().getTime() - Date.parse(cached.fetchedAt) <= cacheHours * 36e5) return cached.ids;
|
|
3520
|
+
const ids = await listCliModels(provider, bin, runner);
|
|
3521
|
+
if (ids.length) writeCliModelsCache(stateDir, provider, ids, now4());
|
|
3522
|
+
return ids.length ? ids : cached?.ids ?? [];
|
|
3523
|
+
};
|
|
3416
3524
|
var parseArtificialAnalysisPayload = (payload) => {
|
|
3417
3525
|
const root = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
3418
3526
|
const data = Array.isArray(root["data"]) ? root["data"] : Array.isArray(payload) ? payload : [];
|
|
@@ -3497,7 +3605,7 @@ var resolveCatalogCandidates = async (input) => {
|
|
|
3497
3605
|
const settings = config.models.providers[provider];
|
|
3498
3606
|
if (settings) {
|
|
3499
3607
|
try {
|
|
3500
|
-
const ids = await listCliModels(provider, settings.bin, input.runner);
|
|
3608
|
+
const ids = input.stateDir ? await listCliModelsCached(provider, settings.bin, input.runner, input.stateDir, config.models.catalog.cliCacheHours, input.now) : await listCliModels(provider, settings.bin, input.runner);
|
|
3501
3609
|
for (const id2 of ids) {
|
|
3502
3610
|
const resolved = resolveAlias(provider, id2, aliases);
|
|
3503
3611
|
const existing = builtin[provider]?.models.find((model) => model.id === resolved);
|
|
@@ -3529,9 +3637,9 @@ var resolveCatalogCandidates = async (input) => {
|
|
|
3529
3637
|
const matches2 = models.models.filter((model) => model.creatorSlug === creator || model.creatorSlug.includes(creator));
|
|
3530
3638
|
for (const model of matches2) {
|
|
3531
3639
|
const id2 = resolveAlias(provider, model.slug, aliases);
|
|
3532
|
-
const
|
|
3533
|
-
const quality =
|
|
3534
|
-
push(provider, { id: id2, quality, codingScore:
|
|
3640
|
+
const score2 = model.codingIndex ?? model.intelligenceIndex ?? 50;
|
|
3641
|
+
const quality = score2 >= 80 ? "frontier" : score2 >= 60 ? "balanced" : "fast";
|
|
3642
|
+
push(provider, { id: id2, quality, codingScore: score2, source: "artificial-analysis", creator });
|
|
3535
3643
|
}
|
|
3536
3644
|
}
|
|
3537
3645
|
}
|
|
@@ -3583,6 +3691,16 @@ var markProviderExhausted = (stateDir, provider, options2) => {
|
|
|
3583
3691
|
return entry;
|
|
3584
3692
|
};
|
|
3585
3693
|
var rotationStatePath = (stateDir) => join(stateDir, "queue-owner.json");
|
|
3694
|
+
var countRotationBlockingLeases = (loaded, leases) => leases.filter((lease) => {
|
|
3695
|
+
const path = join(loaded.stateDir, "issues", lease.issue, "delivery.json");
|
|
3696
|
+
if (!existsSync(path)) return true;
|
|
3697
|
+
try {
|
|
3698
|
+
const delivery2 = JSON.parse(readFileSync(path, "utf8"));
|
|
3699
|
+
return delivery2.prNumber == null && !delivery2.heldFor && !delivery2.finalOutcome;
|
|
3700
|
+
} catch {
|
|
3701
|
+
return true;
|
|
3702
|
+
}
|
|
3703
|
+
}).length;
|
|
3586
3704
|
var queueOwner = (loaded) => {
|
|
3587
3705
|
const { rotation } = loaded.config.linear;
|
|
3588
3706
|
if (!rotation.enabled || !rotation.owners.length) return loaded.config.linear.person;
|
|
@@ -3760,7 +3878,8 @@ var runLoopDoctor = async (input) => {
|
|
|
3760
3878
|
push("orca.worktrees", "warning", `worktree ps unavailable: ${workersError}`);
|
|
3761
3879
|
}
|
|
3762
3880
|
const running = countRunningWorkers(worktrees);
|
|
3763
|
-
const
|
|
3881
|
+
const orcaMemory = await orcaDiagnosticsMemory(input.runner, orcaOptions2);
|
|
3882
|
+
const machine = assessSlots({ machine: config.machine, running, platform: input.platform, orcaMemory });
|
|
3764
3883
|
push("machine.slots", machine.free > 0 ? "passed" : "warning", `${machine.free} free of ${machine.maxAgents} (running ${running}, cpus ${machine.sample.cpus}, load ${machine.sample.load1PerCpuPercent}%, free RAM ${machine.freeRamGb} GB)${machine.reasons.length ? `; ${machine.reasons.join("; ")}` : ""}`);
|
|
3765
3884
|
let queue = [];
|
|
3766
3885
|
let queueError = null;
|
|
@@ -4077,10 +4196,10 @@ var planMemoryContext = async (input) => {
|
|
|
4077
4196
|
hits = [];
|
|
4078
4197
|
}
|
|
4079
4198
|
const selected = selectMemoryForPrompt(hits, memory);
|
|
4080
|
-
const beforeChars = input.references.reduce((
|
|
4199
|
+
const beforeChars = input.references.reduce((sum2, ref) => sum2 + JSON.stringify(ref).length, 0) + issueBudgetDefault;
|
|
4081
4200
|
const preferred = memory.preferOverDocBridge ? preferMemoryOverDocBridge(input.references, selected.hits, memory.minDocBridgeWhenMemory) : { references: input.references};
|
|
4082
4201
|
const issueCharBudget = selected.hits.length && memory.shrinkIssueCharsWhenMemory ? Math.min(issueBudgetDefault, memory.issueCharsWithMemory) : issueBudgetDefault;
|
|
4083
|
-
const afterChars = preferred.references.reduce((
|
|
4202
|
+
const afterChars = preferred.references.reduce((sum2, ref) => sum2 + JSON.stringify(ref).length, 0) + issueCharBudget + selected.approxChars;
|
|
4084
4203
|
return {
|
|
4085
4204
|
hits: selected.hits,
|
|
4086
4205
|
references: preferred.references,
|
|
@@ -4535,7 +4654,6 @@ var resumeIssue = (stateDir, issue) => {
|
|
|
4535
4654
|
writeIssueFailures(stateDir, next);
|
|
4536
4655
|
return next;
|
|
4537
4656
|
};
|
|
4538
|
-
var isIssuePaused = (stateDir, issue) => readIssueFailures(stateDir, issue).pausedAt !== null;
|
|
4539
4657
|
var listPausedIssues = (stateDir) => {
|
|
4540
4658
|
const dir = join(stateDir, "issues");
|
|
4541
4659
|
if (!existsSync(dir)) return [];
|
|
@@ -4637,9 +4755,14 @@ var writeDispatchRecord = (stateDir, record3) => {
|
|
|
4637
4755
|
writeJson2(path, record3);
|
|
4638
4756
|
return path;
|
|
4639
4757
|
};
|
|
4640
|
-
var
|
|
4758
|
+
var EVENTS_ROTATE_AT_BYTES = 10 * 1024 * 1024;
|
|
4759
|
+
var appendLoopEvent = (stateDir, event2, bus, now4 = () => /* @__PURE__ */ new Date()) => {
|
|
4641
4760
|
const path = join(stateDir, "events.ndjson");
|
|
4642
4761
|
mkdirSync(dirname(path), { recursive: true });
|
|
4762
|
+
try {
|
|
4763
|
+
if (statSync(path).size > EVENTS_ROTATE_AT_BYTES) renameSync(path, join(stateDir, `events-archive-${now4().getTime()}.ndjson`));
|
|
4764
|
+
} catch {
|
|
4765
|
+
}
|
|
4643
4766
|
appendFileSync(path, `${JSON.stringify(event2)}
|
|
4644
4767
|
`, "utf8");
|
|
4645
4768
|
if (bus && typeof event2["type"] === "string") bus.emit(event2);
|
|
@@ -4648,11 +4771,12 @@ var gatherLoopState = async (input) => {
|
|
|
4648
4771
|
const { config } = input.loaded;
|
|
4649
4772
|
const person = queueOwner(input.loaded);
|
|
4650
4773
|
const orca = { bin: config.orca.bin, timeoutMs: config.orca.timeoutMs };
|
|
4651
|
-
const [accountList, agentHooks, worktrees, queue] = await Promise.all([
|
|
4774
|
+
const [accountList, agentHooks, worktrees, queue, orcaMemory] = await Promise.all([
|
|
4652
4775
|
orcaAccountList(input.runner, orca).catch(() => ({})),
|
|
4653
4776
|
orcaAgentHooks(input.runner, orca).catch(() => ({})),
|
|
4654
4777
|
orcaWorktrees(input.runner, orca),
|
|
4655
|
-
fetchLinearQueue(input.runner, { bin: config.orca.bin, workspaceId: config.linear.workspaceId, teamKey: config.linear.teamKey, assignee: person, filter: config.linear, orca })
|
|
4778
|
+
fetchLinearQueue(input.runner, { bin: config.orca.bin, workspaceId: config.linear.workspaceId, teamKey: config.linear.teamKey, assignee: person, filter: config.linear, orca }),
|
|
4779
|
+
orcaDiagnosticsMemory(input.runner, orca)
|
|
4656
4780
|
]);
|
|
4657
4781
|
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 });
|
|
4658
4782
|
const availableIds = providers.filter((provider) => provider.available).map((provider) => provider.id);
|
|
@@ -4667,11 +4791,11 @@ var gatherLoopState = async (input) => {
|
|
|
4667
4791
|
})]))) : {};
|
|
4668
4792
|
const routing = routeAllRoles(config, providers, extrasByRole);
|
|
4669
4793
|
const running = countRunningWorkers(worktrees);
|
|
4670
|
-
const slots = assessSlots({ machine: config.machine, running, platform: input.platform, ...input.machine });
|
|
4794
|
+
const slots = assessSlots({ machine: config.machine, running, platform: input.platform, orcaMemory, ...input.machine });
|
|
4671
4795
|
const leases = input.ledger.active();
|
|
4672
4796
|
const busy = busyIssues(queue, leases, worktrees, person);
|
|
4673
4797
|
const candidates = queue.filter((issue) => !busy.has(issue.identifier) && (!input.onlyIssue || issue.identifier === input.onlyIssue));
|
|
4674
|
-
return { person, providers, routing, worktrees, slots, queue, leases, busy, candidates };
|
|
4798
|
+
return { person, providers, routing, worktrees, slots, queue, leases, busy, candidates, extrasByRole };
|
|
4675
4799
|
};
|
|
4676
4800
|
var precheckTick = async (input) => {
|
|
4677
4801
|
const loaded = input.loaded ?? loadLoopConfig(input.configPath);
|
|
@@ -4712,16 +4836,7 @@ var runTick = async (input) => {
|
|
|
4712
4836
|
}
|
|
4713
4837
|
const state = await gatherLoopState({ loaded, runner: input.runner, ledger, env: input.env, platform: input.platform, now: now4, onlyIssue: input.onlyIssue, machine: input.machine });
|
|
4714
4838
|
const orchestrator = state.routing["orchestrator"] ?? { role: "orchestrator", selected: null, skipped: [] };
|
|
4715
|
-
const
|
|
4716
|
-
config,
|
|
4717
|
-
role: "orchestrator",
|
|
4718
|
-
availableProviderIds: state.providers.filter((provider) => provider.available).map((provider) => provider.id),
|
|
4719
|
-
runner: input.runner,
|
|
4720
|
-
stateDir: loaded.stateDir,
|
|
4721
|
-
env: input.env,
|
|
4722
|
-
now: now4
|
|
4723
|
-
}) : [];
|
|
4724
|
-
const orchestratorCandidates = rankModels(config, "orchestrator", state.providers, orchestratorExtras);
|
|
4839
|
+
const orchestratorCandidates = rankModels(config, "orchestrator", state.providers, state.extrasByRole["orchestrator"] ?? []);
|
|
4725
4840
|
const onProviderFailure = (failure) => {
|
|
4726
4841
|
if (dryRun) return;
|
|
4727
4842
|
const resetsAt = extractResetsAt(failure.detail, now4());
|
|
@@ -4741,7 +4856,7 @@ var runTick = async (input) => {
|
|
|
4741
4856
|
return { ...base, status: "idle", results, notes };
|
|
4742
4857
|
}
|
|
4743
4858
|
if (!state.candidates.length) {
|
|
4744
|
-
const rotation = dryRun ? { owner: state.person, advanced: false } : advanceQueueOwner(loaded, { queueEmpty: state.queue.length === 0, activeLeases: state.leases
|
|
4859
|
+
const rotation = dryRun ? { owner: state.person, advanced: false } : advanceQueueOwner(loaded, { queueEmpty: state.queue.length === 0, activeLeases: countRotationBlockingLeases(loaded, state.leases), now: now4() });
|
|
4745
4860
|
if (rotation.advanced) notes.push(`queue drained for ${state.person}; switched to ${rotation.owner}`);
|
|
4746
4861
|
else notes.push("queue has no dispatchable candidate");
|
|
4747
4862
|
return { ...base, status: "idle", results, notes };
|
|
@@ -4774,17 +4889,32 @@ The loop will not retry this issue until you remove the \`${config.resilience.pa
|
|
|
4774
4889
|
appendLoopEvent(loaded.stateDir, { at: now4().toISOString(), type: "issue.paused", issue, kind, consecutive: failureState.consecutive, reason }, bus);
|
|
4775
4890
|
await bus.runHook("onPause", { issue, kind, consecutive: failureState.consecutive, reason });
|
|
4776
4891
|
};
|
|
4892
|
+
let pinnedSkillsOnce;
|
|
4893
|
+
const getPinnedSkills = () => {
|
|
4894
|
+
if (pinnedSkillsOnce === void 0) {
|
|
4895
|
+
try {
|
|
4896
|
+
pinnedSkillsOnce = loadPinnedSkills(loaded.root, config.brief.skills, config.brief.maxSkillChars);
|
|
4897
|
+
} catch (error) {
|
|
4898
|
+
pinnedSkillsOnce = { error };
|
|
4899
|
+
throw error;
|
|
4900
|
+
}
|
|
4901
|
+
}
|
|
4902
|
+
if ("error" in pinnedSkillsOnce) throw pinnedSkillsOnce.error;
|
|
4903
|
+
return pinnedSkillsOnce;
|
|
4904
|
+
};
|
|
4777
4905
|
let dispatched = 0;
|
|
4778
4906
|
for (const candidate of state.candidates) {
|
|
4779
4907
|
if (dispatched >= budget) break;
|
|
4780
4908
|
const setupBudgetMs = config.project.setup.command ? Number.isFinite(timeBudgetMs) ? Math.min(config.project.setup.timeoutSec * 1e3, Math.max(0, timeBudgetMs - config.contract.timeoutMs - 125e3)) : config.project.setup.timeoutSec * 1e3 : 0;
|
|
4781
|
-
|
|
4909
|
+
const cachedContract = readStoredContract(loaded.stateDir, candidate.identifier);
|
|
4910
|
+
if (remainingMs() < config.contract.timeoutMs + setupBudgetMs + 12e4 && !cachedContract) {
|
|
4782
4911
|
notes.push(`time budget: ${candidate.identifier} left for the next tick (${Math.round(remainingMs() / 1e3)}s remaining)`);
|
|
4783
4912
|
continue;
|
|
4784
4913
|
}
|
|
4785
|
-
|
|
4914
|
+
const failureState = readIssueFailures(loaded.stateDir, candidate.identifier);
|
|
4915
|
+
if (failureState.pausedAt !== null) {
|
|
4786
4916
|
if (candidate.labels.includes(config.resilience.pausedLabel)) {
|
|
4787
|
-
results.push({ issue: candidate.identifier, outcome: "skipped", reason: `paused after ${
|
|
4917
|
+
results.push({ issue: candidate.identifier, outcome: "skipped", reason: `paused after ${failureState.consecutive} consecutive failures; remove the "${config.resilience.pausedLabel}" label or run "ak-harness loop resume ${candidate.identifier}" to retry` });
|
|
4788
4918
|
continue;
|
|
4789
4919
|
}
|
|
4790
4920
|
if (!dryRun) clearIssueFailures(loaded.stateDir, candidate.identifier);
|
|
@@ -4797,8 +4927,8 @@ The loop will not retry this issue until you remove the \`${config.resilience.pa
|
|
|
4797
4927
|
results.push({ issue: candidate.identifier, outcome: "failed", reason: `issue fetch failed: ${message2(error)}` });
|
|
4798
4928
|
continue;
|
|
4799
4929
|
}
|
|
4800
|
-
let stored =
|
|
4801
|
-
const
|
|
4930
|
+
let stored = cachedContract;
|
|
4931
|
+
const memoryPlan = memory ? await planMemoryContext({
|
|
4802
4932
|
adapter: memory,
|
|
4803
4933
|
config,
|
|
4804
4934
|
issueId: detail.identifier,
|
|
@@ -4806,7 +4936,7 @@ The loop will not retry this issue until you remove the \`${config.resilience.pa
|
|
|
4806
4936
|
project: config.project.name,
|
|
4807
4937
|
references: []
|
|
4808
4938
|
}) : null;
|
|
4809
|
-
if (stored && !contractIsFresh(stored, detail, config.contract.reuseHours, now4(),
|
|
4939
|
+
if (stored && !contractIsFresh(stored, detail, config.contract.reuseHours, now4(), memoryPlan?.memoryDigest)) stored = null;
|
|
4810
4940
|
if (!stored) {
|
|
4811
4941
|
if (input.skipContractGeneration) {
|
|
4812
4942
|
results.push({ issue: detail.identifier, outcome: "skipped", reason: "no cached contract; generation skipped" });
|
|
@@ -4906,16 +5036,9 @@ The loop will not retry this issue until you remove the \`${config.resilience.pa
|
|
|
4906
5036
|
}
|
|
4907
5037
|
if (setupFailed) notes.push(`${detail.identifier}: setup command failed but project.setup.required is false \u2014 continuing`);
|
|
4908
5038
|
}
|
|
4909
|
-
const briefMemory =
|
|
4910
|
-
adapter: memory,
|
|
4911
|
-
config,
|
|
4912
|
-
issueId: detail.identifier,
|
|
4913
|
-
issueTitle: detail.title,
|
|
4914
|
-
project: config.project.name,
|
|
4915
|
-
references: []
|
|
4916
|
-
}) : { memoryBlock: "", issueCharBudget: config.contract.maxIssueChars, hits: [] };
|
|
5039
|
+
const briefMemory = memoryPlan ?? { memoryBlock: "", issueCharBudget: config.contract.maxIssueChars, hits: [] };
|
|
4917
5040
|
const guidanceRefs = config.contract.maxBriefReferences > 0 && config.contract.briefScopes.length ? await resolveDocContext(loaded.root, `${detail.identifier} ${detail.title}`, config.contract.maxBriefReferences, config.contract.briefScopes) : [];
|
|
4918
|
-
const pinnedSkills =
|
|
5041
|
+
const pinnedSkills = getPinnedSkills();
|
|
4919
5042
|
const brief = renderWorkerBrief({
|
|
4920
5043
|
issue: detail,
|
|
4921
5044
|
contract: stored,
|
|
@@ -5180,14 +5303,34 @@ ${JSON.stringify(stored.contract, null, 2)}
|
|
|
5180
5303
|
return false;
|
|
5181
5304
|
}
|
|
5182
5305
|
};
|
|
5306
|
+
var captureWorkerOutput = async (ctx, terminal2) => {
|
|
5307
|
+
if (!terminal2) return null;
|
|
5308
|
+
try {
|
|
5309
|
+
const screen = (await orcaTerminalScreen(ctx.runner, { terminal: terminal2 }, orcaOptions(ctx.config))).trim();
|
|
5310
|
+
return screen ? screen.slice(-2e3) : null;
|
|
5311
|
+
} catch {
|
|
5312
|
+
return null;
|
|
5313
|
+
}
|
|
5314
|
+
};
|
|
5183
5315
|
var escalateLinear = async (ctx, record3, kind, body2, actions) => {
|
|
5184
5316
|
if (ctx.dryRun) {
|
|
5185
5317
|
actions.push(`would mark ${kind} in Linear and Orca`);
|
|
5186
5318
|
return;
|
|
5187
5319
|
}
|
|
5320
|
+
const workerOutput = await captureWorkerOutput(ctx, record3.terminal);
|
|
5321
|
+
const fullBody = workerOutput ? `${body2}
|
|
5322
|
+
|
|
5323
|
+
<details><summary>Worker's last terminal output</summary>
|
|
5324
|
+
|
|
5325
|
+
\`\`\`
|
|
5326
|
+
${workerOutput}
|
|
5327
|
+
\`\`\`
|
|
5328
|
+
|
|
5329
|
+
</details>` : body2;
|
|
5330
|
+
if (workerOutput) actions.push("captured worker terminal output for the escalation");
|
|
5188
5331
|
const linear = linearOptions(ctx.config);
|
|
5189
5332
|
try {
|
|
5190
|
-
await linearCommentAdd(ctx.runner, { issue: record3.issue, body: `${
|
|
5333
|
+
await linearCommentAdd(ctx.runner, { issue: record3.issue, body: `${fullBody}
|
|
5191
5334
|
|
|
5192
5335
|
<!-- loop:${kind}:${record3.leaseId} -->`, dedupeKey: `${kind}:${record3.issue}:${record3.leaseId}` }, linear);
|
|
5193
5336
|
await linearLabelAdd(ctx.runner, { issue: record3.issue, labels: [ctx.config.linear.blockedLabel] }, linear);
|
|
@@ -5245,7 +5388,7 @@ var providerUnavailable = (ctx, providerId) => {
|
|
|
5245
5388
|
return !match || !match.available;
|
|
5246
5389
|
};
|
|
5247
5390
|
var pickHandoffBuilder = (ctx, record3) => {
|
|
5248
|
-
const ranked = rankModels(ctx.config, "builder", ctx.providers);
|
|
5391
|
+
const ranked = rankModels(ctx.config, "builder", ctx.providers, ctx.builderExtras);
|
|
5249
5392
|
const different = ranked.find((candidate) => candidate.provider !== record3.provider || candidate.model !== record3.model);
|
|
5250
5393
|
return different ?? null;
|
|
5251
5394
|
};
|
|
@@ -5686,7 +5829,8 @@ var runDeliver = async (input) => {
|
|
|
5686
5829
|
const availableIds = providers.filter((provider) => provider.available).map((provider) => provider.id);
|
|
5687
5830
|
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([]);
|
|
5688
5831
|
const reviewer = rankModels(config, "reviewer", providers, await catalogExtras("reviewer"))[0] ?? null;
|
|
5689
|
-
const
|
|
5832
|
+
const builderExtras = await catalogExtras("builder");
|
|
5833
|
+
const builder = rankModels(config, "builder", providers, builderExtras)[0] ?? null;
|
|
5690
5834
|
let env = input.env ?? process.env;
|
|
5691
5835
|
if (!env["GITHUB_TOKEN"] && !env["GH_TOKEN"]) {
|
|
5692
5836
|
try {
|
|
@@ -5702,13 +5846,14 @@ var runDeliver = async (input) => {
|
|
|
5702
5846
|
const { errors } = await loadLoopPlugins(loaded.root, config.plugins.modules, bus);
|
|
5703
5847
|
for (const failure of errors) notes.push(`plugin ${failure.path} failed to load: ${failure.error}`);
|
|
5704
5848
|
}
|
|
5705
|
-
const ctx = { loaded, config, runner: input.runner, now: now4, dryRun, reviewer, builder, providers, env, ...input.assumeIdle === void 0 ? {} : { assumeIdle: input.assumeIdle }, notes, reviewDeadlineMs, bus };
|
|
5849
|
+
const ctx = { loaded, config, runner: input.runner, now: now4, dryRun, reviewer, builder, providers, env, ...input.assumeIdle === void 0 ? {} : { assumeIdle: input.assumeIdle }, notes, reviewDeadlineMs, bus, builderExtras };
|
|
5706
5850
|
const ledger = createDispatchLedger(loaded.stateDir);
|
|
5707
5851
|
const leases = new Map(ledger.active().map((lease) => [lease.issue, lease]));
|
|
5708
5852
|
const results = [];
|
|
5709
5853
|
for (const record3 of listDispatched(loaded.stateDir)) {
|
|
5710
5854
|
if (input.onlyIssue && record3.issue !== input.onlyIssue) continue;
|
|
5711
5855
|
let state = readDeliveryState(loaded.stateDir, record3.issue);
|
|
5856
|
+
if (state.finishedAt && state.finalOutcome === "merged") continue;
|
|
5712
5857
|
const lease = leases.get(record3.issue);
|
|
5713
5858
|
if (!state.finishedAt) {
|
|
5714
5859
|
const ageMinutes = minutesBetween(now4(), record3.dispatchedAt);
|
|
@@ -5747,7 +5892,6 @@ var runDeliver = async (input) => {
|
|
|
5747
5892
|
results.push(await handlePullRequest(ctx, record3, lease, state, pr));
|
|
5748
5893
|
continue;
|
|
5749
5894
|
}
|
|
5750
|
-
if (state.finishedAt && state.finalOutcome === "merged") continue;
|
|
5751
5895
|
const recordedMerge = readMergedEvent(loaded.stateDir, record3.issue);
|
|
5752
5896
|
if (recordedMerge) {
|
|
5753
5897
|
try {
|
|
@@ -6308,10 +6452,19 @@ ${step && total ? `${step}/${total} ` : ""}${title}`),
|
|
|
6308
6452
|
] }))
|
|
6309
6453
|
};
|
|
6310
6454
|
};
|
|
6455
|
+
var newestIssueMtimeMs = (stateDir, issue) => {
|
|
6456
|
+
const mtimes = [dispatchRecordPath(stateDir, issue), deliveryStatePath(stateDir, issue), contractPath(stateDir, issue)].map((path) => {
|
|
6457
|
+
try {
|
|
6458
|
+
return statSync(path).mtimeMs;
|
|
6459
|
+
} catch {
|
|
6460
|
+
return null;
|
|
6461
|
+
}
|
|
6462
|
+
}).filter((value) => value !== null);
|
|
6463
|
+
return mtimes.length ? Math.max(...mtimes) : null;
|
|
6464
|
+
};
|
|
6311
6465
|
var HARNESS_REPO_URL = "https://github.com/AgentsKit-io/harness";
|
|
6312
6466
|
var isRecord13 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6313
|
-
var
|
|
6314
|
-
const path = join(stateDir, "events.ndjson");
|
|
6467
|
+
var parseEventsFile = (path) => {
|
|
6315
6468
|
if (!existsSync(path)) return [];
|
|
6316
6469
|
return readFileSync(path, "utf8").split(/\r?\n/).filter(Boolean).flatMap((line2) => {
|
|
6317
6470
|
try {
|
|
@@ -6322,6 +6475,11 @@ var readLoopEvents = (stateDir) => {
|
|
|
6322
6475
|
}
|
|
6323
6476
|
});
|
|
6324
6477
|
};
|
|
6478
|
+
var eventsArchivePattern = /^events-archive-(\d+)\.ndjson$/;
|
|
6479
|
+
var readLoopEvents = (stateDir, sinceMs) => {
|
|
6480
|
+
const archives = existsSync(stateDir) ? readdirSync(stateDir).map((name2) => name2.match(eventsArchivePattern)).filter((match) => match !== null).map((match) => ({ path: join(stateDir, match[0]), rotatedAtMs: Number(match[1]) })).filter((archive) => sinceMs === void 0 || archive.rotatedAtMs >= sinceMs).sort((a, b) => a.rotatedAtMs - b.rotatedAtMs) : [];
|
|
6481
|
+
return [...archives.flatMap((archive) => parseEventsFile(archive.path)), ...parseEventsFile(join(stateDir, "events.ndjson"))];
|
|
6482
|
+
};
|
|
6325
6483
|
var parseSince = (value, now4) => {
|
|
6326
6484
|
if (!value) return new Date(now4.getTime() - 7 * 864e5);
|
|
6327
6485
|
const match = value.match(/^(\d+)([dhm])$/);
|
|
@@ -6368,10 +6526,11 @@ var buildSuggestions = (input) => {
|
|
|
6368
6526
|
var buildRetroReport = async (input) => {
|
|
6369
6527
|
const loaded = input.loaded ?? loadLoopConfig(input.configPath);
|
|
6370
6528
|
const { config } = loaded;
|
|
6529
|
+
const person = queueOwner(loaded);
|
|
6371
6530
|
const now4 = (input.now ?? (() => /* @__PURE__ */ new Date()))();
|
|
6372
6531
|
const since = parseSince(input.since, now4);
|
|
6373
6532
|
const inWindow = (at) => typeof at === "string" && Date.parse(at) >= since.getTime() && Date.parse(at) <= now4.getTime();
|
|
6374
|
-
const events2 = readLoopEvents(loaded.stateDir).filter((event2) => inWindow(event2.at));
|
|
6533
|
+
const events2 = readLoopEvents(loaded.stateDir, since.getTime()).filter((event2) => inWindow(event2.at));
|
|
6375
6534
|
const counts = {};
|
|
6376
6535
|
for (const event2 of events2) counts[event2.type] = (counts[event2.type] ?? 0) + 1;
|
|
6377
6536
|
const escalations = events2.filter((event2) => event2.type === "contract.escalated");
|
|
@@ -6393,6 +6552,8 @@ var buildRetroReport = async (input) => {
|
|
|
6393
6552
|
if (existsSync(issuesDir)) for (const entry of readdirSync(issuesDir, { withFileTypes: true })) {
|
|
6394
6553
|
if (!entry.isDirectory()) continue;
|
|
6395
6554
|
const issue = entry.name;
|
|
6555
|
+
const newestMtime = newestIssueMtimeMs(loaded.stateDir, issue);
|
|
6556
|
+
if (newestMtime !== null && newestMtime < since.getTime()) continue;
|
|
6396
6557
|
const dispatch = readDispatchRecord(loaded.stateDir, issue);
|
|
6397
6558
|
const delivery2 = readDeliveryState(loaded.stateDir, issue);
|
|
6398
6559
|
const contract = readStoredContract(loaded.stateDir, issue);
|
|
@@ -6450,7 +6611,7 @@ var buildRetroReport = async (input) => {
|
|
|
6450
6611
|
else if (status2 === "ok") work += 1;
|
|
6451
6612
|
}
|
|
6452
6613
|
}
|
|
6453
|
-
orca = { runs, idle, work, timedOut, avgDurationSec: durations.length ? Math.round(durations.reduce((
|
|
6614
|
+
orca = { runs, idle, work, timedOut, avgDurationSec: durations.length ? Math.round(durations.reduce((sum2, value) => sum2 + value, 0) / durations.length) : null, maxDurationSec: durations.length ? Math.round(Math.max(...durations)) : null };
|
|
6454
6615
|
} catch {
|
|
6455
6616
|
orca = null;
|
|
6456
6617
|
}
|
|
@@ -6459,9 +6620,9 @@ var buildRetroReport = async (input) => {
|
|
|
6459
6620
|
generatedAt: now4.toISOString(),
|
|
6460
6621
|
window: { since: since.toISOString(), until: now4.toISOString(), days: Number(((now4.getTime() - since.getTime()) / 864e5).toFixed(2)) },
|
|
6461
6622
|
project: config.project.repo,
|
|
6462
|
-
person
|
|
6623
|
+
person,
|
|
6463
6624
|
counts,
|
|
6464
|
-
escalations: { total: escalations.length, issues: [...new Set(escalations.map((event2) => String(event2.issue ?? "?")))], reasons: [...reasonCounts.entries()].map(([reason2,
|
|
6625
|
+
escalations: { total: escalations.length, issues: [...new Set(escalations.map((event2) => String(event2.issue ?? "?")))], reasons: [...reasonCounts.entries()].map(([reason2, count3]) => ({ reason: reason2, count: count3 })).sort((left, right) => right.count - left.count) },
|
|
6465
6626
|
dispatches: { total: dispatchEvents.length, failed: counts["worker.dispatch-failed"] ?? 0, byProvider },
|
|
6466
6627
|
delivery: { merged: tally("merged"), blocked: tally("blocked"), stuck: tally("stuck"), abandoned: tally("abandoned"), inFlight: tally("in-flight"), fixRounds, reviewsClean, reviewsFindings, reviewsIncomplete, medianLeadTimeMin: median2(rows.map((row) => row.leadTimeMin).filter((value) => value !== null)) },
|
|
6467
6628
|
providers: { cooldowns, cooldownEvents: counts["provider.cooldown"] ?? 0 },
|
|
@@ -6488,7 +6649,7 @@ var renderRetroMarkdown = (report) => {
|
|
|
6488
6649
|
if (report.orca) lines.push(`| Orca runs (idle / work / timed out) | ${report.orca.runs} (${report.orca.idle} / ${report.orca.work} / ${report.orca.timedOut}) \xB7 avg ${report.orca.avgDurationSec ?? "\u2014"} s \xB7 max ${report.orca.maxDurationSec ?? "\u2014"} s |`);
|
|
6489
6650
|
lines.push("");
|
|
6490
6651
|
if (Object.keys(report.dispatches.byProvider).length) {
|
|
6491
|
-
lines.push("## Providers", "", ...Object.entries(report.dispatches.byProvider).map(([key,
|
|
6652
|
+
lines.push("## Providers", "", ...Object.entries(report.dispatches.byProvider).map(([key, count3]) => `- ${key}: ${count3} dispatch(es)`), ...report.providers.cooldowns.map((row) => `- cooldown ${row.provider} until ${row.until.slice(0, 16)}Z \u2014 ${row.reason}`), "");
|
|
6492
6653
|
}
|
|
6493
6654
|
if (report.escalations.reasons.length) {
|
|
6494
6655
|
lines.push("## Problems", "", ...report.escalations.reasons.map((row) => `- ${row.count}\xD7 ${row.reason}`), ...report.issues.filter((row) => ["blocked", "stuck", "abandoned"].includes(row.outcome)).map((row) => `- ${row.issue} ${row.outcome}${row.pr ? ` (PR #${row.pr})` : ""} after ${row.fixRounds} fix round(s), ${row.nudges} nudge(s)`), "");
|
|
@@ -6669,7 +6830,7 @@ var buildDebriefReport = (input) => {
|
|
|
6669
6830
|
}
|
|
6670
6831
|
const inFlight = rows.filter((row) => !row.finalOutcome && row.phase !== "escalated");
|
|
6671
6832
|
const held = rows.filter((row) => row.phase === "held" || row.phase === "held-incomplete-review" || row.heldFor);
|
|
6672
|
-
const events2 = readLoopEvents(stateDir).filter((event2) => Date.parse(event2.at) >= since.getTime());
|
|
6833
|
+
const events2 = readLoopEvents(stateDir, since.getTime()).filter((event2) => Date.parse(event2.at) >= since.getTime());
|
|
6673
6834
|
const recentEscalations = events2.filter((event2) => event2.type === "contract.escalated").slice(-10).map((event2) => ({
|
|
6674
6835
|
issue: typeof event2.issue === "string" ? event2.issue : "?",
|
|
6675
6836
|
at: event2.at,
|
|
@@ -6751,6 +6912,138 @@ var renderDebriefMarkdown = (report) => {
|
|
|
6751
6912
|
lines.push("_Read-only. Run `ak-harness loop deliver` / `tick` to act; `loop retro` for the weekly digest._");
|
|
6752
6913
|
return lines.join("\n");
|
|
6753
6914
|
};
|
|
6915
|
+
var connectedStatuses = /* @__PURE__ */ new Set(["connected", "running", "active", "idle"]);
|
|
6916
|
+
var stalledPhases = /* @__PURE__ */ new Set(["waiting-for-pr", "awaiting-review", "review-incomplete", "fix-round"]);
|
|
6917
|
+
var heldRow = (row) => Boolean(row.heldFor) || row.phase === "held" || row.phase === "held-incomplete-review";
|
|
6918
|
+
var count2 = (events2, type) => events2.filter((event2) => event2.type === type).length;
|
|
6919
|
+
var sum = (events2, key) => events2.reduce((total, event2) => total + (typeof event2[key] === "number" && Number.isFinite(event2[key]) ? Number(event2[key]) : 0), 0);
|
|
6920
|
+
var uniqueIssues = (events2, types) => new Set(events2.filter((event2) => types.includes(event2.type) && typeof event2.issue === "string").map((event2) => event2.issue)).size;
|
|
6921
|
+
var assessObservability = (input) => {
|
|
6922
|
+
const anomalies = [];
|
|
6923
|
+
for (const issue of input.missingDeliveryIssues) anomalies.push({ id: "claim-without-delivery", severity: "action_required", issue, message: `${issue} has an active claim but no delivery.json`, evidence: { issue } });
|
|
6924
|
+
for (const terminal2 of input.terminals) {
|
|
6925
|
+
if (terminal2.worktreeId && connectedStatuses.has(terminal2.status.toLowerCase()) && terminal2.lastOutputAt === null && !terminal2.preview.trim()) anomalies.push({ id: "connected-without-output", severity: "warning", issue: null, message: `terminal ${terminal2.handle} is connected but has not emitted output`, evidence: { handle: terminal2.handle, worktreeId: terminal2.worktreeId, status: terminal2.status } });
|
|
6926
|
+
}
|
|
6927
|
+
for (const worktree of input.finalizedDirtyWorktrees) anomalies.push({ id: "finalized-dirty-worktree", severity: "action_required", issue: worktree.issue, message: `finalized worktree ${worktree.worktreeId} still has ${worktree.files} uncommitted file(s)`, evidence: { ...worktree } });
|
|
6928
|
+
const latestDispatch = input.events.filter((event2) => event2.type === "worker.dispatched").map((event2) => Date.parse(event2.at)).filter(Number.isFinite).sort((a, b) => b - a)[0];
|
|
6929
|
+
const quietForMin = latestDispatch === void 0 ? Number.POSITIVE_INFINITY : Math.max(0, Math.round((Date.parse(input.generatedAt) - latestDispatch) / 6e4));
|
|
6930
|
+
if (input.queueReady > 0 && input.freeSlots > 0 && quietForMin >= 15) anomalies.push({ id: "queue-ready-no-dispatch", severity: "action_required", issue: null, message: `${input.queueReady} ready issue(s) and ${input.freeSlots} free slot(s), but no dispatch in ${Number.isFinite(quietForMin) ? `${quietForMin} min` : "the observation window"}`, evidence: { queueReady: input.queueReady, freeSlots: input.freeSlots, quietForMin } });
|
|
6931
|
+
for (const row of input.issues) {
|
|
6932
|
+
if (row.heldFor || !stalledPhases.has(row.phase) || row.ageMin === null || row.ageMin < input.workerIdleTimeoutMin) continue;
|
|
6933
|
+
anomalies.push({ id: "stalled-delivery", severity: "action_required", issue: row.issue, message: `${row.issue} is in ${row.phase} for ${row.ageMin} min (threshold ${input.workerIdleTimeoutMin} min)`, evidence: { issue: row.issue, phase: row.phase, ageMin: row.ageMin, thresholdMin: input.workerIdleTimeoutMin } });
|
|
6934
|
+
}
|
|
6935
|
+
const events2 = {};
|
|
6936
|
+
for (const event2 of input.events) events2[event2.type] = (events2[event2.type] ?? 0) + 1;
|
|
6937
|
+
const report = {
|
|
6938
|
+
status: anomalies.some((item) => item.severity === "action_required") ? "action_required" : "healthy",
|
|
6939
|
+
generatedAt: input.generatedAt,
|
|
6940
|
+
project: input.project,
|
|
6941
|
+
person: input.person,
|
|
6942
|
+
windowHours: input.windowHours,
|
|
6943
|
+
anomalies,
|
|
6944
|
+
metrics: {
|
|
6945
|
+
queueReady: input.queueReady,
|
|
6946
|
+
freeSlots: input.freeSlots,
|
|
6947
|
+
runningWorkers: input.runningWorkers,
|
|
6948
|
+
maxAgents: input.maxAgents,
|
|
6949
|
+
activeClaims: input.activeClaims,
|
|
6950
|
+
inFlight: input.issues.filter((row) => !heldRow(row)).length,
|
|
6951
|
+
held: input.issues.filter(heldRow).length,
|
|
6952
|
+
merged: input.merged,
|
|
6953
|
+
blocked: input.blocked,
|
|
6954
|
+
fixRounds: input.fixRounds,
|
|
6955
|
+
reviewFindings: input.reviewFindings,
|
|
6956
|
+
reviewIncomplete: input.reviewIncomplete,
|
|
6957
|
+
medianLeadTimeMin: input.medianLeadTimeMin,
|
|
6958
|
+
providerRemainingPercent: input.providerRemainingPercent,
|
|
6959
|
+
machine: input.machine,
|
|
6960
|
+
memory: input.memory,
|
|
6961
|
+
cache: input.cache,
|
|
6962
|
+
tokens: input.tokens,
|
|
6963
|
+
events: events2
|
|
6964
|
+
}
|
|
6965
|
+
};
|
|
6966
|
+
return report;
|
|
6967
|
+
};
|
|
6968
|
+
var compactTerminal = (terminal2) => ({ handle: terminal2.handle, status: terminal2.status, worktreeId: terminal2.worktreeId, lastOutputAt: terminal2.lastOutputAt, preview: terminal2.preview });
|
|
6969
|
+
var dirtyFinalizedWorktrees = async (runner, worktrees) => {
|
|
6970
|
+
const out = [];
|
|
6971
|
+
for (const worktree of worktrees) {
|
|
6972
|
+
if (worktree.workspaceStatus.trim().toLowerCase() !== "completed" || !worktree.path) continue;
|
|
6973
|
+
try {
|
|
6974
|
+
const result = await runner.run(["git", "-C", worktree.path, "status", "--porcelain"], { timeoutMs: 1e4 });
|
|
6975
|
+
if (result.code === 0 && result.stdout.trim()) out.push({ worktreeId: worktree.id, issue: worktree.linkedLinearIssue, files: result.stdout.trim().split(/\r?\n/).length });
|
|
6976
|
+
} catch {
|
|
6977
|
+
}
|
|
6978
|
+
}
|
|
6979
|
+
return out;
|
|
6980
|
+
};
|
|
6981
|
+
var runObservability = async (input) => {
|
|
6982
|
+
const loaded = input.loaded ?? loadLoopConfig(input.configPath ?? "loop.config.yaml");
|
|
6983
|
+
const now4 = input.now ?? (() => /* @__PURE__ */ new Date());
|
|
6984
|
+
const at = now4();
|
|
6985
|
+
const since = parseSince(input.since ?? "24h", at);
|
|
6986
|
+
const [doctor, debrief, worktrees, terminals] = await Promise.all([
|
|
6987
|
+
runLoopDoctor({ loaded, runner: input.runner, env: input.env, platform: input.platform, now: () => at, probe: false }),
|
|
6988
|
+
Promise.resolve(buildDebriefReport({ loaded, since: input.since ?? "24h", now: () => at })),
|
|
6989
|
+
orcaWorktrees(input.runner, { bin: loaded.config.orca.bin, timeoutMs: loaded.config.orca.timeoutMs }).catch(() => []),
|
|
6990
|
+
orcaTerminalList(input.runner, {}, { bin: loaded.config.orca.bin, timeoutMs: loaded.config.orca.timeoutMs }).catch(() => [])
|
|
6991
|
+
]);
|
|
6992
|
+
const events2 = readLoopEvents(loaded.stateDir).filter((event2) => Date.parse(event2.at) >= since.getTime() && Date.parse(event2.at) <= at.getTime());
|
|
6993
|
+
const ledger = createDispatchLedger(loaded.stateDir);
|
|
6994
|
+
const active = ledger.active();
|
|
6995
|
+
const missingDeliveryIssues = active.filter((lease) => !existsSync(deliveryStatePath(loaded.stateDir, lease.issue))).map((lease) => lease.issue);
|
|
6996
|
+
const records = listDispatched(loaded.stateDir);
|
|
6997
|
+
const completed = records.map((record3) => ({ record: record3, state: readDeliveryState(loaded.stateDir, record3.issue) })).filter(({ state }) => state.finishedAt && Date.parse(state.finishedAt) >= since.getTime());
|
|
6998
|
+
const leadTimes = completed.map(({ record: record3, state }) => state.finishedAt ? (Date.parse(state.finishedAt) - Date.parse(record3.dispatchedAt)) / 6e4 : null).filter((value) => value !== null && Number.isFinite(value)).sort((a, b) => a - b);
|
|
6999
|
+
const medianLeadTimeMin = leadTimes.length ? leadTimes.length % 2 ? leadTimes[Math.floor(leadTimes.length / 2)] : (leadTimes[leadTimes.length / 2 - 1] + leadTimes[leadTimes.length / 2]) / 2 : null;
|
|
7000
|
+
const providerRemainingPercent = Object.fromEntries(doctor.providers.map((provider) => [provider.id, remainingUsagePercent(provider.usage, loaded.config.models.routing.usageMetric)]));
|
|
7001
|
+
const cachedContracts = records.filter((record3) => existsSync(contractPath(loaded.stateDir, record3.issue))).length;
|
|
7002
|
+
const memoryEvents = events2.filter((event2) => event2.type === "memory.recalled");
|
|
7003
|
+
const tokens = { input: sum(events2, "inputTokens"), output: sum(events2, "outputTokens"), total: sum(events2, "totalTokens"), cacheRead: sum(events2, "cacheReadTokens"), cacheWrite: sum(events2, "cacheWriteTokens") };
|
|
7004
|
+
const machine = { cpuCount: doctor.machine.sample.cpus, load1PerCpuPercent: doctor.machine.sample.load1PerCpuPercent, memoryUsedPercent: doctor.machine.sample.memoryUsedPercent, freeRamGb: doctor.machine.freeRamGb };
|
|
7005
|
+
const snapshot = {
|
|
7006
|
+
generatedAt: at.toISOString(),
|
|
7007
|
+
project: doctor.config.project,
|
|
7008
|
+
person: doctor.config.person,
|
|
7009
|
+
windowHours: Math.max(1, Math.round((at.getTime() - since.getTime()) / 36e5)),
|
|
7010
|
+
workerIdleTimeoutMin: loaded.config.delivery.workerIdleTimeoutMin,
|
|
7011
|
+
queueReady: doctor.queue.count,
|
|
7012
|
+
freeSlots: doctor.machine.free,
|
|
7013
|
+
runningWorkers: doctor.workers.running,
|
|
7014
|
+
maxAgents: doctor.machine.maxAgents,
|
|
7015
|
+
activeClaims: active.length,
|
|
7016
|
+
missingDeliveryIssues,
|
|
7017
|
+
terminals: terminals.map(compactTerminal),
|
|
7018
|
+
finalizedDirtyWorktrees: await dirtyFinalizedWorktrees(input.runner, worktrees),
|
|
7019
|
+
issues: debrief.inFlight.map(({ issue, phase, ageMin, heldFor }) => ({ issue, phase, ageMin, heldFor })),
|
|
7020
|
+
events: events2,
|
|
7021
|
+
merged: uniqueIssues(events2, ["pr.merged", "worker.merged"]),
|
|
7022
|
+
blocked: Math.max(records.filter((record3) => readDeliveryState(loaded.stateDir, record3.issue).finalOutcome === "blocked").length, uniqueIssues(events2, ["worker.blocked"])),
|
|
7023
|
+
fixRounds: records.reduce((total, record3) => total + readDeliveryState(loaded.stateDir, record3.issue).fixRounds, 0),
|
|
7024
|
+
reviewFindings: count2(events2, "pr.reviewed") - count2(events2.filter((event2) => event2["status"] !== "findings"), "pr.reviewed"),
|
|
7025
|
+
reviewIncomplete: events2.filter((event2) => event2.type === "pr.reviewed" && event2["status"] === "incomplete").length,
|
|
7026
|
+
medianLeadTimeMin,
|
|
7027
|
+
providerRemainingPercent,
|
|
7028
|
+
machine,
|
|
7029
|
+
memory: { recalls: memoryEvents.length, hits: sum(memoryEvents, "hits"), approxCharsSaved: sum(memoryEvents, "approxCharsSaved") },
|
|
7030
|
+
cache: { cachedContracts },
|
|
7031
|
+
tokens
|
|
7032
|
+
};
|
|
7033
|
+
return assessObservability(snapshot);
|
|
7034
|
+
};
|
|
7035
|
+
var renderObservabilityMarkdown = (report) => {
|
|
7036
|
+
const m = report.metrics;
|
|
7037
|
+
const headroom = Object.entries(m.providerRemainingPercent).map(([provider, remaining]) => `${provider} ${remaining === null ? "?" : `${remaining}%`}`).join(", ");
|
|
7038
|
+
const lines = [`# Loop observability \u2014 ${report.project} \xB7 ${report.person}`, "", `_${report.status}_ \xB7 generated ${report.generatedAt.slice(0, 19)}Z \xB7 last ${report.windowHours}h`, "", "## Metrics", "", `- Queue: ${m.queueReady} ready \xB7 ${m.freeSlots} free slot(s) \xB7 ${m.runningWorkers}/${m.maxAgents} workers`, `- Delivery: ${m.inFlight} in flight \xB7 ${m.held} held \xB7 ${m.merged} merged \xB7 ${m.blocked} blocked \xB7 ${m.fixRounds} fix round(s)`, `- Reviews: ${m.reviewFindings} findings \xB7 ${m.reviewIncomplete} incomplete`, `- Machine: ${m.machine.cpuCount} CPU \xB7 ${m.machine.load1PerCpuPercent}% load \xB7 ${m.machine.memoryUsedPercent}% memory \xB7 ${m.machine.freeRamGb} GB free`, `- Providers: ${headroom || "n/a"}`, `- Memory/cache: ${m.memory.recalls} recall(s), ${m.memory.hits} hit(s), ${m.memory.approxCharsSaved} chars saved \xB7 ${m.cache.cachedContracts} cached contract(s)`, `- Tokens observed: ${m.tokens.total || m.tokens.input + m.tokens.output || "n/a"}`, ""];
|
|
7039
|
+
if (report.anomalies.length) {
|
|
7040
|
+
lines.push("## Anomalies", "");
|
|
7041
|
+
for (const anomaly of report.anomalies) lines.push(`- **${anomaly.severity}**${anomaly.issue ? ` \xB7 ${anomaly.issue}` : ""}: ${anomaly.message}`);
|
|
7042
|
+
lines.push("");
|
|
7043
|
+
} else lines.push("## Anomalies", "", "_None detected._", "");
|
|
7044
|
+
lines.push("_Read-only. Run `ak-harness loop tick` or `deliver` to act on the queue._");
|
|
7045
|
+
return lines.join("\n");
|
|
7046
|
+
};
|
|
6754
7047
|
|
|
6755
7048
|
// src/loop/watch.ts
|
|
6756
7049
|
var defaultSleep = (ms) => new Promise((resolve9) => setTimeout(resolve9, ms));
|
|
@@ -6887,6 +7180,32 @@ var print = (value) => {
|
|
|
6887
7180
|
if (options().json) console.log(JSON.stringify(value));
|
|
6888
7181
|
else console.log(typeof value === "string" ? value : JSON.stringify(value, null, 2));
|
|
6889
7182
|
};
|
|
7183
|
+
var acquireStageLock = (stateDir, stage) => {
|
|
7184
|
+
const path = join(stateDir, `.stage-${stage}.lock`);
|
|
7185
|
+
mkdirSync(stateDir, { recursive: true });
|
|
7186
|
+
try {
|
|
7187
|
+
const fd = openSync(path, "wx");
|
|
7188
|
+
writeFileSync(fd, `${JSON.stringify({ pid: process.pid, stage, at: (/* @__PURE__ */ new Date()).toISOString() })}
|
|
7189
|
+
`, "utf8");
|
|
7190
|
+
closeSync(fd);
|
|
7191
|
+
return () => {
|
|
7192
|
+
try {
|
|
7193
|
+
unlinkSync(path);
|
|
7194
|
+
} catch {
|
|
7195
|
+
}
|
|
7196
|
+
};
|
|
7197
|
+
} catch (error) {
|
|
7198
|
+
if (error.code !== "EEXIST") throw error;
|
|
7199
|
+
try {
|
|
7200
|
+
if (Date.now() - statSync(path).mtimeMs > 30 * 6e4) {
|
|
7201
|
+
unlinkSync(path);
|
|
7202
|
+
return acquireStageLock(stateDir, stage);
|
|
7203
|
+
}
|
|
7204
|
+
} catch {
|
|
7205
|
+
}
|
|
7206
|
+
return null;
|
|
7207
|
+
}
|
|
7208
|
+
};
|
|
6890
7209
|
var readBenchmarkEvidence = (path) => {
|
|
6891
7210
|
try {
|
|
6892
7211
|
const content = readFileSync(path, "utf8");
|
|
@@ -6991,6 +7310,12 @@ loop.command("stage <stage>").description("Run one stage (tick | deliver | retro
|
|
|
6991
7310
|
process.exitCode = 1;
|
|
6992
7311
|
return;
|
|
6993
7312
|
}
|
|
7313
|
+
const stageLock = acquireStageLock(loaded.stateDir, stage);
|
|
7314
|
+
if (!stageLock) {
|
|
7315
|
+
console.log(JSON.stringify({ status: "locked", stage, reason: "another stage run is still active" }, null, 2));
|
|
7316
|
+
process.exitCode = 1;
|
|
7317
|
+
return;
|
|
7318
|
+
}
|
|
6994
7319
|
const budgetMs = Math.max(6e4, loaded.config.schedule.stageTimeoutSec * 1e3 - 6e4);
|
|
6995
7320
|
const threshold = loaded.config.resilience.stagePauseAfterRuns;
|
|
6996
7321
|
try {
|
|
@@ -7001,6 +7326,8 @@ loop.command("stage <stage>").description("Run one stage (tick | deliver | retro
|
|
|
7001
7326
|
const reason = error instanceof Error ? error.message : String(error);
|
|
7002
7327
|
const entry = stage !== "retro" ? recordStageRunResult(loaded.stateDir, trackedStage, { succeeded: false, reason }, threshold) : null;
|
|
7003
7328
|
console.log(JSON.stringify({ status: "error", stage, error: reason, ...entry ? { consecutiveFailures: entry.consecutiveFailures, paused: entry.pausedAt !== null } : {} }, null, 2));
|
|
7329
|
+
} finally {
|
|
7330
|
+
stageLock();
|
|
7004
7331
|
}
|
|
7005
7332
|
process.exitCode = 1;
|
|
7006
7333
|
});
|
|
@@ -7080,6 +7407,13 @@ loop.command("debrief").description("Human-facing explanation of what the loop i
|
|
|
7080
7407
|
if (options().json) return print(report);
|
|
7081
7408
|
console.log(renderDebriefMarkdown(report));
|
|
7082
7409
|
});
|
|
7410
|
+
loop.command("observe").description("Read-only anomaly scan and operating metrics for the loop (queue, workers, delivery, machine, memory, cache, tokens).").option("--since <window>", "window such as 24h, 7d or an ISO date", "24h").option("--precheck", "exit 0 when an action is required, 1 when healthy (for schedulers)").action(async function(command) {
|
|
7411
|
+
const report = await runObservability({ configPath: loopFile(this), runner: createProcessRunner(), since: command.since });
|
|
7412
|
+
if (options().json) print(report);
|
|
7413
|
+
else console.log(renderObservabilityMarkdown(report));
|
|
7414
|
+
if (command.precheck) process.exitCode = report.status === "action_required" ? 0 : 1;
|
|
7415
|
+
else if (report.status === "action_required") process.exitCode = 2;
|
|
7416
|
+
});
|
|
7083
7417
|
loop.command("watch").description("Watch delivery.json (+ optional live PR) for in-flight issues; prints DONE / FAILED / ACTION_REQUIRED / PROGRESS. Read-only.").option("--issue <identifier>", "restrict to one issue").option("--interval <seconds>", "poll interval", (value) => Number(value), 30).option("--once", "single snapshot then exit").option("--timeout <seconds>", "stop after N seconds (0 = until terminal)", (value) => Number(value), 0).option("--no-live-pr", "do not call gh; filesystem state only").action(async function(command) {
|
|
7084
7418
|
const report = await watchDeliveries({
|
|
7085
7419
|
configPath: loopFile(this),
|