@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/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, statSync } from 'fs';
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().toLowerCase();
926
- const scopes = query.scope?.map((scope) => scope.toLowerCase()) ?? [];
933
+ const needle = query.query.trim();
934
+ if (!needle) return 0;
927
935
  const value = text(entry);
928
- return Boolean(needle && value.includes(needle) && (scopes.length === 0 || scopes.some((scope) => value.includes(scope))));
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.0.0",
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 entries = Array.isArray(document.knowledge) ? document.knowledge.filter((value) => typeof value === "object" && value !== null && !Array.isArray(value)).filter((entry) => matches(entry, query)).sort((left, right) => String(left.id ?? "").localeCompare(String(right.id ?? ""))).slice(0, 8) : [];
951
- const references = entries.flatMap((entry) => 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: 1 }] : []);
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 ids = await store.get(indexKey);
1562
- const records = Array.isArray(ids) ? await Promise.all(ids.filter((id2) => typeof id2 === "string").map((id2) => store.get(`agentskit-harness:memory:${id2}`))) : [];
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 receipt = isRecord7(record3["receipt"]) ? record3["receipt"] : record3;
2518
- const stages = Array.isArray(receipt["stages"]) ? receipt["stages"].map((stage) => isRecord7(stage) ? str(stage["stage"], str(stage["name"])) : str(stage)).filter(Boolean) : [];
2519
- const accepted = receipt["accepted"] === false ? false : receipt["accepted"] === true || stages.includes("input_accepted") || (result === null || result === void 0 || Object.keys(record3).length === 0);
2520
- 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)) : [] };
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 perAgentBytes = input.machine.agentRssMb * 1024 ** 2;
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 ${input.machine.agentRssMb} MB each`);
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 score = model.codingIndex ?? model.intelligenceIndex ?? 50;
3533
- const quality = score >= 80 ? "frontier" : score >= 60 ? "balanced" : "fast";
3534
- push(provider, { id: id2, quality, codingScore: score, source: "artificial-analysis", creator });
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 machine = assessSlots({ machine: config.machine, running, platform: input.platform });
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((sum, ref) => sum + JSON.stringify(ref).length, 0) + issueBudgetDefault;
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((sum, ref) => sum + JSON.stringify(ref).length, 0) + issueCharBudget + selected.approxChars;
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 appendLoopEvent = (stateDir, event2, bus) => {
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 orchestratorExtras = config.models.routing.mode === "catalog" ? await resolveCatalogCandidates({
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.length, now: now4() });
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
- if (remainingMs() < config.contract.timeoutMs + setupBudgetMs + 12e4 && !readStoredContract(loaded.stateDir, candidate.identifier)) {
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
- if (isIssuePaused(loaded.stateDir, candidate.identifier)) {
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 ${readIssueFailures(loaded.stateDir, candidate.identifier).consecutive} consecutive failures; remove the "${config.resilience.pausedLabel}" label or run "ak-harness loop resume ${candidate.identifier}" to retry` });
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 = readStoredContract(loaded.stateDir, detail.identifier);
4801
- const memoryProbe = memory ? await planMemoryContext({
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(), memoryProbe?.memoryDigest)) stored = null;
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 = memory ? await planMemoryContext({
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 = loadPinnedSkills(loaded.root, config.brief.skills, config.brief.maxSkillChars);
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: `${body2}
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 builder = rankModels(config, "builder", providers, await catalogExtras("builder"))[0] ?? null;
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 readLoopEvents = (stateDir) => {
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((sum, value) => sum + value, 0) / durations.length) : null, maxDurationSec: durations.length ? Math.round(Math.max(...durations)) : null };
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: config.linear.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, count2]) => ({ reason: reason2, count: count2 })).sort((left, right) => right.count - left.count) },
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, count2]) => `- ${key}: ${count2} dispatch(es)`), ...report.providers.cooldowns.map((row) => `- cooldown ${row.provider} until ${row.until.slice(0, 16)}Z \u2014 ${row.reason}`), "");
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),