@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/CHANGELOG.md +57 -0
- package/capabilities/public-surface.json +86 -84
- package/dist/cli.js +140 -43
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +32 -4
- package/dist/index.js +147 -60
- package/dist/index.js.map +1 -1
- package/docs/MODULE-BOUNDARIES.md +4 -3
- package/package.json +5 -1
- package/release/manifest.json +3 -3
- package/release/notes.md +25 -0
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)
|
|
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
|
-
|
|
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
|
-
|
|
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" &&
|
|
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
|
-
|
|
1542
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 ${
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
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
|
-
|
|
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)
|
|
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: `${
|
|
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)
|
|
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))
|
|
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.
|
|
7570
|
+
process.exit(130);
|
|
7474
7571
|
});
|
|
7475
7572
|
try {
|
|
7476
7573
|
await program.parseAsync(process.argv);
|