@agentskit/harness 0.11.0 → 0.13.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,6 +1,6 @@
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, statSync, writeSync, appendFileSync, mkdtempSync, renameSync, rmSync } from 'fs';
3
+ import { readFileSync, mkdirSync, existsSync, unlinkSync, openSync, writeFileSync, closeSync, readdirSync, statSync, writeSync, appendFileSync, mkdtempSync, renameSync, rmSync, accessSync, constants } from 'fs';
4
4
  import { resolve, dirname, relative, basename, join, extname, isAbsolute, delimiter, sep } from 'path';
5
5
  import { Command } from 'commander';
6
6
  import { execFile, spawn, execFileSync } from 'child_process';
@@ -970,9 +970,9 @@ var createDocBridgeContextProvider = ({ root, indexPath = ".doc-bridge/index.jso
970
970
  const started = Date.now();
971
971
  const ageBudget = maxAgeHours ?? 0;
972
972
  const inspection = ageBudget > 0 ? inspectDocBridgeIndex(root, indexPath, now4()) : null;
973
- if (inspection?.error) throw new Error(`Doc Bridge index is unreadable: ${inspection.error}`);
973
+ if (inspection?.error) fail(`Doc Bridge index is unreadable: ${inspection.error}`, "INVALID_STATE");
974
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.`);
975
+ fail(`Doc Bridge index is ${inspection.ageHours.toFixed(1)}h old; refresh it before resolving context.`, "STALE");
976
976
  }
977
977
  const document = index(root, indexPath);
978
978
  const contentHash = sourceHash(document);
@@ -993,7 +993,9 @@ var createDocBridgeContextProvider = ({ root, indexPath = ".doc-bridge/index.jso
993
993
  });
994
994
  var executable = (path) => {
995
995
  try {
996
- return statSync(path).isFile();
996
+ if (!statSync(path).isFile()) return false;
997
+ accessSync(path, constants.X_OK);
998
+ return true;
997
999
  } catch {
998
1000
  return false;
999
1001
  }
@@ -1524,7 +1526,7 @@ var validateIteration = (iteration, index2) => {
1524
1526
  if (typeof result !== "object" || result === null || Array.isArray(result)) return fail(`iterations[${index2}].steps[${stepIndex}] must be an object.`, "INVALID_INPUT");
1525
1527
  if (result.step !== IMPROVEMENT_CYCLE_STEPS[stepIndex]) return fail(`iterations[${index2}].steps[${stepIndex}] must be ${IMPROVEMENT_CYCLE_STEPS[stepIndex]}.`, "INVALID_INPUT");
1526
1528
  if (!["passed", "failed", "blocked", "pending"].includes(result.status)) return fail(`iterations[${index2}].steps[${stepIndex}].status is invalid.`, "INVALID_INPUT");
1527
- if (result.status !== "passed" && !nonEmpty(result.reason, `iterations[${index2}].steps[${stepIndex}].reason`)) return fail(`iterations[${index2}].steps[${stepIndex}].reason is required when the step does not pass.`, "INVALID_INPUT");
1529
+ if (result.status !== "passed" && (typeof result.reason !== "string" || !result.reason.trim())) return fail(`iterations[${index2}].steps[${stepIndex}].reason is required when the step does not pass.`, "INVALID_INPUT");
1528
1530
  });
1529
1531
  if (iteration.adjustment !== void 0) nonEmpty(iteration.adjustment, `iterations[${index2}].adjustment`);
1530
1532
  return { ...iteration, metrics: validateMetrics(iteration.metrics, index2) };
@@ -1538,8 +1540,10 @@ var assessImprovementCycle = (input) => {
1538
1540
  const iterations = input.iterations.map(validateIteration);
1539
1541
  iterations.forEach((iteration, index2) => {
1540
1542
  if (iteration.iteration !== index2 + 1) return fail("iterations must be sequential and start at 1.", "INVALID_INPUT");
1541
- if (index2 > 0 && iterations[index2 - 1]?.steps.every((step) => step.status === "passed")) return fail("a completed cycle cannot have later iterations.", "INVALID_INPUT");
1542
- if (index2 < iterations.length - 1 && !iteration.adjustment) return fail(`iterations[${index2}].adjustment is required before repeating.`, "INVALID_INPUT");
1543
+ const isLast = index2 === iterations.length - 1;
1544
+ const iterationComplete = iteration.steps.every((step) => step.status === "passed");
1545
+ if (iterationComplete && !isLast) return fail("a completed cycle cannot have later iterations.", "INVALID_INPUT");
1546
+ if (!isLast && !iteration.adjustment) return fail(`iterations[${index2}].adjustment is required before repeating.`, "INVALID_INPUT");
1543
1547
  });
1544
1548
  const matrix = iterations.map((iteration) => {
1545
1549
  const statuses = Object.fromEntries(iteration.steps.map((step) => [step.step, step.status]));
@@ -2241,7 +2245,17 @@ var MODEL_ROLES = ["orchestrator", "reviewer", "builder", "watcher"];
2241
2245
 
2242
2246
  // src/kernel/pii.ts
2243
2247
  var PATTERNS = [
2244
- { kind: "api-key", regex: /\b(?:sk-[A-Za-z0-9]{16,}|gh[opsu]_[A-Za-z0-9]{20,}|AKIA[0-9A-Z]{16}|xox[baprs]-[A-Za-z0-9-]{10,})\b/g },
2248
+ // PEM key blocks first: large, unambiguous, and must claim their content before any narrower pattern below
2249
+ // could otherwise match a substring inside the base64 body (unlikely, but claimed-range order matters).
2250
+ { kind: "private-key", regex: /-----BEGIN (?:RSA |EC |DSA |OPENSSH |ENCRYPTED |)PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |DSA |OPENSSH |ENCRYPTED |)PRIVATE KEY-----/g },
2251
+ // `sk-` body allows `-`/`_` (not just alnum) so a project/scoped key like `sk-proj-...`/`sk-live-...` matches
2252
+ // as one token instead of the hyphen splitting it into a too-short fragment. `github_pat_` (fine-grained PAT)
2253
+ // and `AIza…` (Google API key) are current real-world formats missing from the original list entirely.
2254
+ { kind: "api-key", regex: /\b(?:sk-[A-Za-z0-9_-]{16,}|sk_(?:live|test)_[A-Za-z0-9]{16,}|pk_(?:live|test)_[A-Za-z0-9]{16,}|gh[opsu]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}|AKIA[0-9A-Z]{16}|AIza[A-Za-z0-9_-]{30,}|xox[baprs]-[A-Za-z0-9-]{10,})\b/g },
2255
+ // The AWS *secret* half (as opposed to the `AKIA…` access-key id above) has no recognizable prefix — a bare
2256
+ // 40-char base64-shaped run is too generic to scan for on its own (matches hashes, tokens, arbitrary base64).
2257
+ // Anchoring on the conventional key name it's almost always assigned to/from keeps this pattern high-signal.
2258
+ { kind: "api-key", regex: /\b(?:aws_secret_access_key|AWS_SECRET_ACCESS_KEY|SecretAccessKey)\s*[:=]\s*["']?([A-Za-z0-9/+=]{40})["']?/g },
2245
2259
  { kind: "email", regex: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g },
2246
2260
  { kind: "credit-card", regex: /\b(?:\d[ -]?){13,16}\b/g },
2247
2261
  { kind: "phone", regex: /\b\+?\d{1,3}?[\s().-]?\(?\d{2,4}\)?[\s.-]?\d{3,4}[\s.-]?\d{4}\b/g }
@@ -2389,7 +2403,9 @@ var exportEvidenceBundle = async ({ configPath, runId, outputPath, privateKeyPat
2389
2403
  `, "utf8");
2390
2404
  return bundle;
2391
2405
  };
2392
- var verifyEvidenceBundle = (path, { trustedKeys = [] } = {}) => {
2406
+ var EVIDENCE_MAX_FILE_BYTES = 25 * 1048576;
2407
+ var EVIDENCE_MAX_TOTAL_BYTES = 200 * 1048576;
2408
+ var verifyEvidenceBundle = (path, { trustedKeys = [], maxFileBytes = EVIDENCE_MAX_FILE_BYTES, maxTotalBytes = EVIDENCE_MAX_TOTAL_BYTES } = {}) => {
2393
2409
  const bundle = parseBundle(path);
2394
2410
  if (bundle.type !== "agentskit-harness-evidence-bundle" || bundle.schemaVersion !== EVIDENCE_BUNDLE_SCHEMA_VERSION || !bundle.runId || !validKeyId(bundle.signerKeyId) || !validDigest(bundle.payloadHash) || bundle.signature?.algorithm !== "ed25519" || bundle.signature.keyId !== bundle.signerKeyId || typeof bundle.signature.publicKeyPem !== "string" || typeof bundle.signature.signatureBase64 !== "string" || !Array.isArray(bundle.files)) fail("Evidence bundle metadata is invalid.", "HARNESS_ERROR");
2395
2411
  if (trustedKeys.length) {
@@ -2399,10 +2415,14 @@ var verifyEvidenceBundle = (path, { trustedKeys = [] } = {}) => {
2399
2415
  if (trusted.publicKeyPem !== bundle.signature.publicKeyPem) fail(`Evidence bundle key does not match trust store: ${bundle.signerKeyId}`, "HARNESS_ERROR");
2400
2416
  }
2401
2417
  const paths = /* @__PURE__ */ new Set();
2418
+ let totalBytes = 0;
2402
2419
  for (const file of bundle.files) {
2403
2420
  if (!file || typeof file.path !== "string" || paths.has(file.path) || !validDigest(file.sha256) || typeof file.contentBase64 !== "string") fail("Evidence bundle file metadata is invalid.", "HARNESS_ERROR");
2404
2421
  paths.add(file.path);
2422
+ if (file.contentBase64.length > Math.ceil(maxFileBytes / 3) * 4) fail(`Evidence bundle file exceeds the maximum allowed size: ${file.path}`, "HARNESS_ERROR");
2405
2423
  const content = Buffer.from(file.contentBase64, "base64");
2424
+ totalBytes += content.length;
2425
+ if (totalBytes > maxTotalBytes) fail("Evidence bundle exceeds the maximum total allowed size.", "HARNESS_ERROR");
2406
2426
  if (sha256(content) !== file.sha256) fail(`Evidence bundle file hash mismatch: ${file.path}`, "HARNESS_ERROR");
2407
2427
  }
2408
2428
  if (!paths.has(`runs/${bundle.runId}/run.json`) || !paths.has(`runs/${bundle.runId}/events.ndjson`)) fail("Evidence bundle is missing the run projection or event log.", "HARNESS_ERROR");
@@ -2503,6 +2523,21 @@ var orcaStatus = async (runner, options2 = {}) => parseOrcaStatus(await orcaJson
2503
2523
  var orcaWorktrees = async (runner, options2 = {}) => parseOrcaWorktrees(await orcaJson(runner, ["worktree", "ps"], options2));
2504
2524
  var orcaAgentHooks = async (runner, options2 = {}) => parseOrcaAgentHooks(await orcaJson(runner, ["agent", "hooks", "status"], options2));
2505
2525
  var orcaAccountList = async (runner, options2 = {}) => orcaJson(runner, ["account", "list"], options2);
2526
+ var orcaDiagnosticsMemory = async (runner, options2 = {}) => {
2527
+ try {
2528
+ const result = await orcaJson(runner, ["diagnostics", "memory"], options2);
2529
+ if (!isRecord7(result)) return null;
2530
+ const host = isRecord7(result["host"]) ? result["host"] : {};
2531
+ const availableBytes = host["availableMemory"];
2532
+ if (typeof availableBytes !== "number" || !Number.isFinite(availableBytes) || availableBytes <= 0) return null;
2533
+ const totalBytes = typeof host["totalMemory"] === "number" ? host["totalMemory"] : null;
2534
+ const worktrees = Array.isArray(result["worktrees"]) ? result["worktrees"] : [];
2535
+ 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);
2536
+ return { availableBytes, totalBytes, agentRssSamples };
2537
+ } catch {
2538
+ return null;
2539
+ }
2540
+ };
2506
2541
  var parseOrcaWorktreeCreate = (result) => {
2507
2542
  const record3 = isRecord7(result) ? result : {};
2508
2543
  const nested = isRecord7(record3["worktree"]) ? record3["worktree"] : record3;
@@ -2578,6 +2613,12 @@ var orcaTerminalWait = async (runner, input, options2 = {}) => {
2578
2613
  const wait = isRecord7(record3["wait"]) ? record3["wait"] : record3;
2579
2614
  return { satisfied: wait["satisfied"] === true, raw: result };
2580
2615
  };
2616
+ var orcaTerminalScreen = async (runner, input, options2 = {}) => {
2617
+ const result = await orcaJson(runner, ["terminal", "read", "--terminal", input.terminal, "--screen"], options2);
2618
+ const record3 = isRecord7(result) ? isRecord7(result["terminal"]) ? result["terminal"] : result : {};
2619
+ const screen = record3["tail"] ?? record3["screen"] ?? record3["lines"] ?? record3["text"] ?? record3["output"];
2620
+ return Array.isArray(screen) ? screen.map((line2) => isRecord7(line2) ? str(line2["text"], str(line2["line"])) : String(line2)).join("\n") : typeof screen === "string" ? screen : "";
2621
+ };
2581
2622
  var parseOrcaAutomations = (result) => {
2582
2623
  const list2 = isRecord7(result) ? Array.isArray(result["automations"]) ? result["automations"] : Array.isArray(result["items"]) ? result["items"] : [] : Array.isArray(result) ? result : [];
2583
2624
  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);
@@ -3194,8 +3235,8 @@ var isWsl = (platform = process.platform, osRelease = release(), env = process.e
3194
3235
  var assessSlots = (input) => {
3195
3236
  const platform = input.platform ?? process.platform;
3196
3237
  const wsl = isWsl(platform, input.osRelease);
3197
- const freeBytes = input.freeBytes ?? availableMemoryBytes(platform);
3198
- const totalBytes = input.totalBytes ?? totalmem();
3238
+ const freeBytes = input.freeBytes ?? input.orcaMemory?.availableBytes ?? availableMemoryBytes(platform);
3239
+ const totalBytes = input.totalBytes ?? input.orcaMemory?.totalBytes ?? totalmem();
3199
3240
  const sample = input.sample ?? { ...sampleMachine(), memoryUsedPercent: Number(Math.max(0, Math.min(100, (1 - freeBytes / Math.max(1, totalBytes)) * 100)).toFixed(2)) };
3200
3241
  const freeRamGb = Number((freeBytes / 1024 ** 3).toFixed(2));
3201
3242
  const reasons = [];
@@ -3203,9 +3244,11 @@ var assessSlots = (input) => {
3203
3244
  const adaptive = adaptiveConcurrency(ceiling, sample, { warningPercent: input.machine.warningPercent, criticalPercent: input.machine.criticalPercent });
3204
3245
  if (adaptive < ceiling) reasons.push(`machine pressure capped concurrency at ${adaptive} (load ${sample.load1PerCpuPercent}%, memory ${sample.memoryUsedPercent}%)`);
3205
3246
  const reservedBytes = input.machine.minFreeRamGb * 1024 ** 3;
3206
- const perAgentBytes = input.machine.agentRssMb * 1024 ** 2;
3247
+ const measuredAgentBytes = input.orcaMemory?.agentRssSamples.length ? input.orcaMemory.agentRssSamples.reduce((total, value) => total + value, 0) / input.orcaMemory.agentRssSamples.length : null;
3248
+ const perAgentBytes = measuredAgentBytes ?? input.machine.agentRssMb * 1024 ** 2;
3249
+ const perAgentMb = Math.round(perAgentBytes / 1024 ** 2);
3207
3250
  const ramBound = Math.max(0, Math.floor((freeBytes - reservedBytes) / perAgentBytes)) + input.running;
3208
- 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`);
3251
+ 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)" : ""}`);
3209
3252
  let maxAgents = Math.min(adaptive, ramBound);
3210
3253
  if (wsl && maxAgents > input.machine.wslCap) {
3211
3254
  maxAgents = input.machine.wslCap;
@@ -3855,7 +3898,8 @@ var runLoopDoctor = async (input) => {
3855
3898
  push("orca.worktrees", "warning", `worktree ps unavailable: ${workersError}`);
3856
3899
  }
3857
3900
  const running = countRunningWorkers(worktrees);
3858
- const machine = assessSlots({ machine: config.machine, running, platform: input.platform });
3901
+ const orcaMemory = await orcaDiagnosticsMemory(input.runner, orcaOptions2);
3902
+ const machine = assessSlots({ machine: config.machine, running, platform: input.platform, orcaMemory });
3859
3903
  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("; ")}` : ""}`);
3860
3904
  let queue = [];
3861
3905
  let queueError = null;
@@ -4076,6 +4120,14 @@ var githubCommentExists = async (runner, input, options2 = {}) => {
4076
4120
  const list2 = await ghJson(runner, ["api", "--paginate", `repos/${input.repo}/issues/${input.number}/comments`, "--jq", "[.[].body]"], options2);
4077
4121
  return Array.isArray(list2) && list2.some((body2) => typeof body2 === "string" && body2.includes(input.marker));
4078
4122
  };
4123
+ var writeJsonAtomic = (path, value) => {
4124
+ const dir = dirname(path);
4125
+ mkdirSync(dir, { recursive: true });
4126
+ const tmp = join(dir, `.${basename(path)}.${process.pid}.${Date.now()}.tmp`);
4127
+ writeFileSync(tmp, `${JSON.stringify(value, null, 2)}
4128
+ `, "utf8");
4129
+ renameSync(tmp, path);
4130
+ };
4079
4131
  var clip = (text6, max) => text6.length <= max ? text6 : `${text6.slice(0, Math.max(0, max - 1))}\u2026`;
4080
4132
  var createFileMemoryKvStore = (dir) => {
4081
4133
  mkdirSync(dir, { recursive: true });
@@ -4285,9 +4337,7 @@ var readStoredContract = (stateDir, identifier) => {
4285
4337
  };
4286
4338
  var writeStoredContract = (stateDir, stored) => {
4287
4339
  const path = contractPath(stateDir, stored.issue);
4288
- mkdirSync(dirname(path), { recursive: true });
4289
- writeFileSync(path, `${JSON.stringify(stored, null, 2)}
4290
- `, "utf8");
4340
+ writeJsonAtomic(path, stored);
4291
4341
  return path;
4292
4342
  };
4293
4343
  var contractIsFresh = (stored, issue, reuseHours, now4, memoryDigest) => stored.issueUpdatedAt === issue.updatedAt && (reuseHours === 0 || now4.getTime() - Date.parse(stored.generatedAt) <= reuseHours * 36e5) && (memoryDigest === void 0 || (stored.memoryDigest ?? hashJson([])) === memoryDigest);
@@ -4721,37 +4771,68 @@ var readDispatchRecord = (stateDir, identifier) => {
4721
4771
  return null;
4722
4772
  }
4723
4773
  };
4724
- var writeJson2 = (path, value) => {
4725
- mkdirSync(dirname(path), { recursive: true });
4726
- writeFileSync(path, `${JSON.stringify(value, null, 2)}
4727
- `, "utf8");
4728
- };
4729
4774
  var writeDispatchRecord = (stateDir, record3) => {
4730
4775
  const path = dispatchRecordPath(stateDir, record3.issue);
4731
- writeJson2(path, record3);
4776
+ writeJsonAtomic(path, record3);
4732
4777
  return path;
4733
4778
  };
4734
4779
  var EVENTS_ROTATE_AT_BYTES = 10 * 1024 * 1024;
4780
+ var EVENTS_LOCK_STALE_MS = 5e3;
4781
+ var EVENTS_LOCK_MAX_ATTEMPTS = 100;
4782
+ var EVENTS_LOCK_RETRY_MS = 10;
4783
+ var acquireEventsLock = (lockFilePath) => {
4784
+ for (let attempt = 0; attempt < EVENTS_LOCK_MAX_ATTEMPTS; attempt += 1) {
4785
+ try {
4786
+ return openSync(lockFilePath, "wx");
4787
+ } catch (error) {
4788
+ if (error.code !== "EEXIST") throw error;
4789
+ try {
4790
+ if (Date.now() - statSync(lockFilePath).mtimeMs > EVENTS_LOCK_STALE_MS) unlinkSync(lockFilePath);
4791
+ } catch {
4792
+ }
4793
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, EVENTS_LOCK_RETRY_MS);
4794
+ }
4795
+ }
4796
+ return null;
4797
+ };
4735
4798
  var appendLoopEvent = (stateDir, event2, bus, now4 = () => /* @__PURE__ */ new Date()) => {
4736
4799
  const path = join(stateDir, "events.ndjson");
4737
4800
  mkdirSync(dirname(path), { recursive: true });
4801
+ const lockFilePath = `${path}.lock`;
4802
+ const lockFd = acquireEventsLock(lockFilePath);
4738
4803
  try {
4739
- if (statSync(path).size > EVENTS_ROTATE_AT_BYTES) renameSync(path, join(stateDir, `events-archive-${now4().getTime()}.ndjson`));
4740
- } catch {
4741
- }
4742
- appendFileSync(path, `${JSON.stringify(event2)}
4804
+ if (lockFd !== null) {
4805
+ try {
4806
+ if (statSync(path).size > EVENTS_ROTATE_AT_BYTES) renameSync(path, join(stateDir, `events-archive-${now4().getTime()}.ndjson`));
4807
+ } catch {
4808
+ }
4809
+ }
4810
+ appendFileSync(path, `${JSON.stringify(event2)}
4743
4811
  `, "utf8");
4812
+ } finally {
4813
+ if (lockFd !== null) {
4814
+ try {
4815
+ closeSync(lockFd);
4816
+ } catch {
4817
+ }
4818
+ try {
4819
+ unlinkSync(lockFilePath);
4820
+ } catch {
4821
+ }
4822
+ }
4823
+ }
4744
4824
  if (bus && typeof event2["type"] === "string") bus.emit(event2);
4745
4825
  };
4746
4826
  var gatherLoopState = async (input) => {
4747
4827
  const { config } = input.loaded;
4748
4828
  const person = queueOwner(input.loaded);
4749
4829
  const orca = { bin: config.orca.bin, timeoutMs: config.orca.timeoutMs };
4750
- const [accountList, agentHooks, worktrees, queue] = await Promise.all([
4830
+ const [accountList, agentHooks, worktrees, queue, orcaMemory] = await Promise.all([
4751
4831
  orcaAccountList(input.runner, orca).catch(() => ({})),
4752
4832
  orcaAgentHooks(input.runner, orca).catch(() => ({})),
4753
4833
  orcaWorktrees(input.runner, orca),
4754
- fetchLinearQueue(input.runner, { bin: config.orca.bin, workspaceId: config.linear.workspaceId, teamKey: config.linear.teamKey, assignee: person, filter: config.linear, orca })
4834
+ fetchLinearQueue(input.runner, { bin: config.orca.bin, workspaceId: config.linear.workspaceId, teamKey: config.linear.teamKey, assignee: person, filter: config.linear, orca }),
4835
+ orcaDiagnosticsMemory(input.runner, orca)
4755
4836
  ]);
4756
4837
  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 });
4757
4838
  const availableIds = providers.filter((provider) => provider.available).map((provider) => provider.id);
@@ -4766,7 +4847,7 @@ var gatherLoopState = async (input) => {
4766
4847
  })]))) : {};
4767
4848
  const routing = routeAllRoles(config, providers, extrasByRole);
4768
4849
  const running = countRunningWorkers(worktrees);
4769
- const slots = assessSlots({ machine: config.machine, running, platform: input.platform, ...input.machine });
4850
+ const slots = assessSlots({ machine: config.machine, running, platform: input.platform, orcaMemory, ...input.machine });
4770
4851
  const leases = input.ledger.active();
4771
4852
  const busy = busyIssues(queue, leases, worktrees, person);
4772
4853
  const candidates = queue.filter((issue) => !busy.has(issue.identifier) && (!input.onlyIssue || issue.identifier === input.onlyIssue));
@@ -5035,7 +5116,7 @@ The loop will not retry this issue until you remove the \`${config.resilience.pa
5035
5116
  if (!launched.accepted) notes.push(`${detail.identifier}: terminal ${launched.terminal} did not confirm the brief; deliver will nudge it if it stays idle`);
5036
5117
  ledger.recordDispatch({ lease: claim.lease, idempotencyKey: plan.idempotencyKey, commandDigest: plan.commandDigest });
5037
5118
  const record3 = { issue: detail.identifier, worktreeId: created.id, worktree, branch: actualBranch, terminal: launched.terminal, provider: builder.provider, model: builder.model, contractDigest: stored.digest, leaseKey: claim.lease.key, leaseId: claim.lease.leaseId, dispatchedAt: now4().toISOString(), url: detail.url, briefDigest, skills: skillRefs(pinnedSkills), setup: setupResult, effort: builder.effort, initialRemainingPercent: builder.remainingPercent, worktreePath: created.path };
5038
- writeJson2(dispatchRecordPath(loaded.stateDir, detail.identifier), record3);
5119
+ writeJsonAtomic(dispatchRecordPath(loaded.stateDir, detail.identifier), record3);
5039
5120
  appendLoopEvent(loaded.stateDir, { at: record3.dispatchedAt, type: "worker.dispatched", ...record3, command: builder.tui, briefAccepted: launched.accepted, tuiIdle: launched.idle }, bus);
5040
5121
  await bus.runHook("afterDispatch", { issue: detail.identifier, provider: record3.provider, model: record3.model, branch: record3.branch, worktreeId: record3.worktreeId });
5041
5122
  clearIssueFailures(loaded.stateDir, detail.identifier);
@@ -5152,11 +5233,6 @@ var discoverIntake = async (runner, input, options2 = {}) => {
5152
5233
  // src/loop/deliver.ts
5153
5234
  var message3 = (error) => error instanceof HarnessError ? `${error.code}: ${error.message}` : error instanceof Error ? error.message : String(error);
5154
5235
  var isMissingOrcaWorktree = (error) => message3(error).includes("selector_not_found");
5155
- var writeJson3 = (path, value) => {
5156
- mkdirSync(dirname(path), { recursive: true });
5157
- writeFileSync(path, `${JSON.stringify(value, null, 2)}
5158
- `, "utf8");
5159
- };
5160
5236
  var deliveryStatePath = (stateDir, identifier) => join(stateDir, "issues", identifier, "delivery.json");
5161
5237
  var readDeliveryState = (stateDir, identifier) => {
5162
5238
  const path = deliveryStatePath(stateDir, identifier);
@@ -5183,7 +5259,7 @@ var minutesBetween = (later, earlier) => earlier === null ? Number.POSITIVE_INFI
5183
5259
  var orcaOptions = (config) => ({ bin: config.orca.bin, timeoutMs: config.orca.timeoutMs });
5184
5260
  var linearOptions = (config) => ({ bin: config.orca.bin, workspaceId: config.linear.workspaceId, orca: { timeoutMs: config.orca.timeoutMs } });
5185
5261
  var saveState = (ctx, state) => {
5186
- if (!ctx.dryRun) writeJson3(deliveryStatePath(ctx.loaded.stateDir, state.issue), state);
5262
+ if (!ctx.dryRun) writeJsonAtomic(deliveryStatePath(ctx.loaded.stateDir, state.issue), state);
5187
5263
  };
5188
5264
  var event = (ctx, payload) => {
5189
5265
  if (!ctx.dryRun) appendLoopEvent(ctx.loaded.stateDir, { at: ctx.now().toISOString(), ...payload }, ctx.bus);
@@ -5278,14 +5354,34 @@ ${JSON.stringify(stored.contract, null, 2)}
5278
5354
  return false;
5279
5355
  }
5280
5356
  };
5357
+ var captureWorkerOutput = async (ctx, terminal2) => {
5358
+ if (!terminal2) return null;
5359
+ try {
5360
+ const screen = (await orcaTerminalScreen(ctx.runner, { terminal: terminal2 }, orcaOptions(ctx.config))).trim();
5361
+ return screen ? screen.slice(-2e3) : null;
5362
+ } catch {
5363
+ return null;
5364
+ }
5365
+ };
5281
5366
  var escalateLinear = async (ctx, record3, kind, body2, actions) => {
5282
5367
  if (ctx.dryRun) {
5283
5368
  actions.push(`would mark ${kind} in Linear and Orca`);
5284
5369
  return;
5285
5370
  }
5371
+ const workerOutput = await captureWorkerOutput(ctx, record3.terminal);
5372
+ const fullBody = workerOutput ? `${body2}
5373
+
5374
+ <details><summary>Worker's last terminal output</summary>
5375
+
5376
+ \`\`\`
5377
+ ${workerOutput}
5378
+ \`\`\`
5379
+
5380
+ </details>` : body2;
5381
+ if (workerOutput) actions.push("captured worker terminal output for the escalation");
5286
5382
  const linear = linearOptions(ctx.config);
5287
5383
  try {
5288
- await linearCommentAdd(ctx.runner, { issue: record3.issue, body: `${body2}
5384
+ await linearCommentAdd(ctx.runner, { issue: record3.issue, body: `${fullBody}
5289
5385
 
5290
5386
  <!-- loop:${kind}:${record3.leaseId} -->`, dedupeKey: `${kind}:${record3.issue}:${record3.leaseId}` }, linear);
5291
5387
  await linearLabelAdd(ctx.runner, { issue: record3.issue, labels: [ctx.config.linear.blockedLabel] }, linear);
@@ -5835,7 +5931,7 @@ var runDeliver = async (input) => {
5835
5931
  const candidates = (await githubOpenPullRequests(input.runner, { repo: config.project.repo, limit: 100 })).filter((item) => item.headRef === record3.branch || item.headRef.endsWith(`/${record3.worktree}`) || item.headRef === record3.worktree);
5836
5932
  if (candidates.length) {
5837
5933
  open = candidates;
5838
- if (!dryRun) writeJson3(dispatchRecordPath(loaded.stateDir, record3.issue), { ...record3, branch: candidates[0].headRef });
5934
+ if (!dryRun) writeJsonAtomic(dispatchRecordPath(loaded.stateDir, record3.issue), { ...record3, branch: candidates[0].headRef });
5839
5935
  notes.push(`${record3.issue}: PR found on branch ${candidates[0].headRef}; dispatch record updated`);
5840
5936
  }
5841
5937
  }
@@ -6444,7 +6540,7 @@ var parseSince = (value, now4) => {
6444
6540
  return new Date(now4.getTime() - amount * unit);
6445
6541
  }
6446
6542
  const parsed = Date.parse(value);
6447
- if (Number.isNaN(parsed)) throw new Error(`Unrecognised --since value: ${value} (use 7d, 12h, 30m or an ISO date)`);
6543
+ if (Number.isNaN(parsed)) fail(`Unrecognised --since value: ${value} (use 7d, 12h, 30m or an ISO date)`, "INVALID_INPUT");
6448
6544
  return new Date(parsed);
6449
6545
  };
6450
6546
  var median2 = (values) => {
@@ -6693,6 +6789,7 @@ var phaseOf = (dispatch, delivery2) => {
6693
6789
  return "in-flight";
6694
6790
  };
6695
6791
  var summarize2 = (phase, delivery2, dispatch) => {
6792
+ if (phase === "idle") return "Not yet dispatched";
6696
6793
  if (phase === "merged") return `Merged PR #${delivery2.prNumber ?? "?"}`;
6697
6794
  if (phase === "held" || phase === "held-incomplete-review") {
6698
6795
  if (delivery2.heldFor) return `Held for a human (self-edit or protected path at ${delivery2.heldFor.slice(0, 7)})`;
@@ -6779,7 +6876,7 @@ var buildDebriefReport = (input) => {
6779
6876
  });
6780
6877
  continue;
6781
6878
  }
6782
- continue;
6879
+ if (!input.issue) continue;
6783
6880
  }
6784
6881
  rows.push(rowFor({ issue, dispatch, delivery: delivery2, intent, repo: config.project.repo, now: now4 }));
6785
6882
  }
@@ -6947,7 +7044,7 @@ var runObservability = async (input) => {
6947
7044
  const events2 = readLoopEvents(loaded.stateDir).filter((event2) => Date.parse(event2.at) >= since.getTime() && Date.parse(event2.at) <= at.getTime());
6948
7045
  const ledger = createDispatchLedger(loaded.stateDir);
6949
7046
  const active = ledger.active();
6950
- const missingDeliveryIssues = active.filter((lease) => !existsSync(deliveryStatePath(loaded.stateDir, lease.issue))).map((lease) => lease.issue);
7047
+ const missingDeliveryIssues = active.filter((lease) => !existsSync(deliveryStatePath(loaded.stateDir, lease.issue)) && !existsSync(dispatchRecordPath(loaded.stateDir, lease.issue))).map((lease) => lease.issue);
6951
7048
  const records = listDispatched(loaded.stateDir);
6952
7049
  const completed = records.map((record3) => ({ record: record3, state: readDeliveryState(loaded.stateDir, record3.issue) })).filter(({ state }) => state.finishedAt && Date.parse(state.finishedAt) >= since.getTime());
6953
7050
  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);
@@ -7470,7 +7567,7 @@ benchmark.command("baseline <taskId>").description("Record one controlled baseli
7470
7567
  program.command("clean").description("Remove only configured task-owned temporary artifacts.").action(() => print(cleanTaskArtifacts(options().config)));
7471
7568
  process.on("SIGINT", () => {
7472
7569
  process.stderr.write("Cancelled.\n");
7473
- process.exitCode = 130;
7570
+ process.exit(130);
7474
7571
  });
7475
7572
  try {
7476
7573
  await program.parseAsync(process.argv);