@kynver-app/runtime 0.1.32 → 0.1.37
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 +1649 -114
- package/dist/cli.js.map +4 -4
- package/dist/index.js +1703 -153
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,13 +1,49 @@
|
|
|
1
|
+
// src/package-version.ts
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
function resolvePackageRoot(moduleUrl) {
|
|
6
|
+
let dir = dirname(fileURLToPath(moduleUrl));
|
|
7
|
+
for (let depth = 0; depth < 6; depth += 1) {
|
|
8
|
+
if (existsSync(join(dir, "package.json"))) return dir;
|
|
9
|
+
const parent = dirname(dir);
|
|
10
|
+
if (parent === dir) break;
|
|
11
|
+
dir = parent;
|
|
12
|
+
}
|
|
13
|
+
throw new Error(`package.json not found above ${dirname(fileURLToPath(moduleUrl))}`);
|
|
14
|
+
}
|
|
15
|
+
function readOwnPackageVersion(moduleUrl = import.meta.url) {
|
|
16
|
+
const pkgPath = join(resolvePackageRoot(moduleUrl), "package.json");
|
|
17
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
18
|
+
if (typeof pkg.version !== "string" || !pkg.version.trim()) {
|
|
19
|
+
throw new Error(`Missing package.json version at ${pkgPath}`);
|
|
20
|
+
}
|
|
21
|
+
return pkg.version;
|
|
22
|
+
}
|
|
23
|
+
var PACKAGE_VERSION = readOwnPackageVersion();
|
|
24
|
+
function wantsCliVersion(argv) {
|
|
25
|
+
return argv.some((arg) => arg === "--version" || arg === "-v");
|
|
26
|
+
}
|
|
27
|
+
function printCliVersionAndExit(version, binName) {
|
|
28
|
+
console.log(binName ? `${binName} ${version}` : version);
|
|
29
|
+
process.exit(0);
|
|
30
|
+
}
|
|
31
|
+
function handleCliVersionFlag(argv, moduleUrl = import.meta.url, binName) {
|
|
32
|
+
if (!wantsCliVersion(argv)) return false;
|
|
33
|
+
printCliVersionAndExit(readOwnPackageVersion(moduleUrl), binName);
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
|
|
1
37
|
// src/dispatch.ts
|
|
2
|
-
import
|
|
38
|
+
import path16 from "node:path";
|
|
3
39
|
|
|
4
40
|
// src/config.ts
|
|
5
|
-
import { existsSync as
|
|
41
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
|
|
6
42
|
import { homedir } from "node:os";
|
|
7
43
|
import path2 from "node:path";
|
|
8
44
|
|
|
9
45
|
// src/util.ts
|
|
10
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
46
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
11
47
|
import path from "node:path";
|
|
12
48
|
function fail(message) {
|
|
13
49
|
console.error(message);
|
|
@@ -30,7 +66,7 @@ function safeJson(line) {
|
|
|
30
66
|
}
|
|
31
67
|
function readJson(file, fallback) {
|
|
32
68
|
try {
|
|
33
|
-
return JSON.parse(
|
|
69
|
+
return JSON.parse(readFileSync2(file, "utf8"));
|
|
34
70
|
} catch (error) {
|
|
35
71
|
if (arguments.length > 1) return fallback;
|
|
36
72
|
fail(`failed to read ${file}: ${error.message}`);
|
|
@@ -68,15 +104,15 @@ function fileMtime(file) {
|
|
|
68
104
|
}
|
|
69
105
|
}
|
|
70
106
|
function tailFile(file, lines) {
|
|
71
|
-
if (!
|
|
72
|
-
const data =
|
|
107
|
+
if (!existsSync2(file)) return "";
|
|
108
|
+
const data = readFileSync2(file, "utf8");
|
|
73
109
|
return data.split("\n").slice(-lines).join("\n");
|
|
74
110
|
}
|
|
75
111
|
function readMaybeFile(file) {
|
|
76
|
-
return file ?
|
|
112
|
+
return file ? readFileSync2(path.resolve(file), "utf8") : "";
|
|
77
113
|
}
|
|
78
114
|
function listRunIds(runsDir) {
|
|
79
|
-
if (!
|
|
115
|
+
if (!existsSync2(runsDir)) return [];
|
|
80
116
|
return readdirSync(runsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
81
117
|
}
|
|
82
118
|
function sleepMs(ms) {
|
|
@@ -120,9 +156,9 @@ var CONFIG_DIR = path2.join(homedir(), ".kynver");
|
|
|
120
156
|
var CONFIG_FILE = path2.join(CONFIG_DIR, "config.json");
|
|
121
157
|
var CREDENTIALS_FILE = path2.join(CONFIG_DIR, "credentials");
|
|
122
158
|
function loadUserConfig() {
|
|
123
|
-
if (!
|
|
159
|
+
if (!existsSync3(CONFIG_FILE)) return {};
|
|
124
160
|
try {
|
|
125
|
-
return JSON.parse(
|
|
161
|
+
return JSON.parse(readFileSync3(CONFIG_FILE, "utf8"));
|
|
126
162
|
} catch {
|
|
127
163
|
return {};
|
|
128
164
|
}
|
|
@@ -133,9 +169,9 @@ function saveUserConfig(config) {
|
|
|
133
169
|
`, { mode: 384 });
|
|
134
170
|
}
|
|
135
171
|
function loadCredentialsFile() {
|
|
136
|
-
if (!
|
|
172
|
+
if (!existsSync3(CREDENTIALS_FILE)) return {};
|
|
137
173
|
try {
|
|
138
|
-
return JSON.parse(
|
|
174
|
+
return JSON.parse(readFileSync3(CREDENTIALS_FILE, "utf8"));
|
|
139
175
|
} catch {
|
|
140
176
|
return {};
|
|
141
177
|
}
|
|
@@ -425,12 +461,12 @@ var DEFAULT_CRITICAL_FREE_BYTES = 15 * 1024 * 1024 * 1024;
|
|
|
425
461
|
var DEFAULT_MAX_USED_PERCENT = 80;
|
|
426
462
|
var DEFAULT_HARD_MAX_USED_PERCENT = 90;
|
|
427
463
|
function observeRunnerDiskGate(input = {}) {
|
|
428
|
-
const
|
|
464
|
+
const path32 = input.diskPath?.trim() || "/";
|
|
429
465
|
const warnBelowBytes = input.diskFreeWarnBytes ?? DEFAULT_WARN_FREE_BYTES;
|
|
430
466
|
const criticalBelowBytes = input.diskFreeCriticalBytes ?? DEFAULT_CRITICAL_FREE_BYTES;
|
|
431
467
|
const maxUsedPercent = input.diskMaxUsedPercent ?? DEFAULT_MAX_USED_PERCENT;
|
|
432
468
|
const hardMaxUsedPercent = input.diskHardMaxUsedPercent ?? DEFAULT_HARD_MAX_USED_PERCENT;
|
|
433
|
-
const stats = statfsSync(
|
|
469
|
+
const stats = statfsSync(path32);
|
|
434
470
|
const freeBytes = Number(stats.bavail) * Number(stats.bsize);
|
|
435
471
|
const totalBytes = Number(stats.blocks) * Number(stats.bsize);
|
|
436
472
|
const usedPercent = totalBytes > 0 ? (totalBytes - freeBytes) / totalBytes * 100 : 100;
|
|
@@ -450,7 +486,7 @@ function observeRunnerDiskGate(input = {}) {
|
|
|
450
486
|
}
|
|
451
487
|
return {
|
|
452
488
|
ok,
|
|
453
|
-
path:
|
|
489
|
+
path: path32,
|
|
454
490
|
freeBytes,
|
|
455
491
|
totalBytes,
|
|
456
492
|
usedPercent,
|
|
@@ -463,16 +499,16 @@ function observeRunnerDiskGate(input = {}) {
|
|
|
463
499
|
}
|
|
464
500
|
|
|
465
501
|
// src/resource-gate.ts
|
|
466
|
-
import { readFileSync as
|
|
502
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
467
503
|
import os from "node:os";
|
|
468
504
|
import path5 from "node:path";
|
|
469
505
|
|
|
470
506
|
// src/run-store.ts
|
|
471
|
-
import { existsSync as
|
|
507
|
+
import { existsSync as existsSync5, readdirSync as readdirSync2 } from "node:fs";
|
|
472
508
|
import path4 from "node:path";
|
|
473
509
|
|
|
474
510
|
// src/paths.ts
|
|
475
|
-
import { existsSync as
|
|
511
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
476
512
|
import { homedir as homedir2 } from "node:os";
|
|
477
513
|
import path3 from "node:path";
|
|
478
514
|
var LEGACY_ROOT = path3.join(homedir2(), ".openclaw", "harness");
|
|
@@ -480,8 +516,8 @@ function resolveHarnessRoot() {
|
|
|
480
516
|
const env = process.env.KYNVER_HARNESS_ROOT || process.env.OPUS_HARNESS_ROOT;
|
|
481
517
|
if (env) return path3.resolve(env);
|
|
482
518
|
const kynverRoot = path3.join(homedir2(), ".kynver", "harness");
|
|
483
|
-
if (
|
|
484
|
-
if (
|
|
519
|
+
if (existsSync4(kynverRoot)) return kynverRoot;
|
|
520
|
+
if (existsSync4(LEGACY_ROOT)) return LEGACY_ROOT;
|
|
485
521
|
return kynverRoot;
|
|
486
522
|
}
|
|
487
523
|
function getHarnessPaths() {
|
|
@@ -506,7 +542,7 @@ function loadRun(id) {
|
|
|
506
542
|
}
|
|
507
543
|
function listRunRecords() {
|
|
508
544
|
const { runsDir } = getPaths();
|
|
509
|
-
if (!
|
|
545
|
+
if (!existsSync5(runsDir)) return [];
|
|
510
546
|
const runs = [];
|
|
511
547
|
for (const entry of readdirSync2(runsDir, { withFileTypes: true })) {
|
|
512
548
|
if (!entry.isDirectory()) continue;
|
|
@@ -538,7 +574,8 @@ function runDirectory(id) {
|
|
|
538
574
|
}
|
|
539
575
|
|
|
540
576
|
// src/heartbeat.ts
|
|
541
|
-
import { existsSync as
|
|
577
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4 } from "node:fs";
|
|
578
|
+
var HEARTBEAT_FUTURE_SKEW_MS = 6e4;
|
|
542
579
|
function isTerminalHeartbeatPhase(phase) {
|
|
543
580
|
return phase === "complete";
|
|
544
581
|
}
|
|
@@ -553,16 +590,31 @@ function parseHeartbeat(file) {
|
|
|
553
590
|
lastHeartbeatAt: null,
|
|
554
591
|
lastHeartbeatPhase: null,
|
|
555
592
|
lastHeartbeatSummary: null,
|
|
556
|
-
heartbeatBlocker: null
|
|
593
|
+
heartbeatBlocker: null,
|
|
594
|
+
timestampAnomalies: []
|
|
557
595
|
};
|
|
558
|
-
if (!
|
|
559
|
-
const
|
|
596
|
+
if (!existsSync6(file)) return result;
|
|
597
|
+
const maxFutureMs = Date.now() + HEARTBEAT_FUTURE_SKEW_MS;
|
|
598
|
+
const clampedTo = new Date(maxFutureMs).toISOString();
|
|
599
|
+
const lines = readFileSync4(file, "utf8").split("\n").filter(Boolean);
|
|
560
600
|
for (const line of lines) {
|
|
561
601
|
const entry = safeJson(line);
|
|
562
602
|
if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
|
|
563
603
|
const row = entry;
|
|
564
604
|
result.heartbeatCount++;
|
|
565
|
-
if (row.ts)
|
|
605
|
+
if (row.ts) {
|
|
606
|
+
const ts = String(row.ts);
|
|
607
|
+
const tsMs = Date.parse(ts);
|
|
608
|
+
if (Number.isFinite(tsMs) && tsMs > maxFutureMs) {
|
|
609
|
+
result.timestampAnomalies.push({
|
|
610
|
+
kind: "future_heartbeat_timestamp",
|
|
611
|
+
observedAt: ts,
|
|
612
|
+
clampedTo
|
|
613
|
+
});
|
|
614
|
+
} else {
|
|
615
|
+
result.lastHeartbeatAt = ts;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
566
618
|
if (row.phase !== void 0 && row.phase !== null) result.lastHeartbeatPhase = String(row.phase);
|
|
567
619
|
if (row.summary !== void 0 && row.summary !== null) result.lastHeartbeatSummary = String(row.summary);
|
|
568
620
|
result.heartbeatBlocker = row.blocker ? String(row.blocker) : null;
|
|
@@ -571,7 +623,7 @@ function parseHeartbeat(file) {
|
|
|
571
623
|
}
|
|
572
624
|
|
|
573
625
|
// src/stream.ts
|
|
574
|
-
import { existsSync as
|
|
626
|
+
import { existsSync as existsSync7, readFileSync as readFileSync5 } from "node:fs";
|
|
575
627
|
function eventTimestampIso(event) {
|
|
576
628
|
const tsMs = event.timestamp_ms;
|
|
577
629
|
return event.timestamp || event.ts || (tsMs ? new Date(tsMs).toISOString() : void 0);
|
|
@@ -600,8 +652,8 @@ function parseHarnessStream(file) {
|
|
|
600
652
|
finalResult: null,
|
|
601
653
|
error: null
|
|
602
654
|
};
|
|
603
|
-
if (!
|
|
604
|
-
const lines =
|
|
655
|
+
if (!existsSync7(file)) return result;
|
|
656
|
+
const lines = readFileSync5(file, "utf8").split("\n").filter(Boolean);
|
|
605
657
|
for (const line of lines) {
|
|
606
658
|
const event = safeJson(line);
|
|
607
659
|
if (!event) continue;
|
|
@@ -949,6 +1001,112 @@ function landingAttentionReason(verdict) {
|
|
|
949
1001
|
return verdict.detail ?? verdict.reason ?? "dirty_worktree_no_pr";
|
|
950
1002
|
}
|
|
951
1003
|
|
|
1004
|
+
// src/landing-contract-gate.ts
|
|
1005
|
+
function trimOrNull3(value) {
|
|
1006
|
+
if (typeof value !== "string") return null;
|
|
1007
|
+
const t = value.trim();
|
|
1008
|
+
return t.length ? t : null;
|
|
1009
|
+
}
|
|
1010
|
+
function hasFinalResult3(value) {
|
|
1011
|
+
if (value === void 0 || value === null) return false;
|
|
1012
|
+
if (typeof value === "string") return value.trim().length > 0;
|
|
1013
|
+
if (typeof value === "object") return Object.keys(value).length > 0;
|
|
1014
|
+
return true;
|
|
1015
|
+
}
|
|
1016
|
+
function normalizePrUrl(url) {
|
|
1017
|
+
const m = url.trim().match(/github\.com\/([^/]+\/[^/]+)\/(?:pull|pulls)\/(\d+)/i);
|
|
1018
|
+
if (!m) return trimOrNull3(url);
|
|
1019
|
+
return `https://github.com/${m[1]}/pull/${m[2]}`;
|
|
1020
|
+
}
|
|
1021
|
+
function parseReconciliation(finalResult) {
|
|
1022
|
+
if (!finalResult || typeof finalResult !== "object" || Array.isArray(finalResult)) return [];
|
|
1023
|
+
const record = finalResult;
|
|
1024
|
+
const raw = record.targetPrReconciliation ?? record.target_pr_reconciliation;
|
|
1025
|
+
if (!Array.isArray(raw)) return [];
|
|
1026
|
+
const out = [];
|
|
1027
|
+
for (const item of raw) {
|
|
1028
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
|
|
1029
|
+
const row = item;
|
|
1030
|
+
const prUrl = normalizePrUrl(String(row.prUrl ?? row.pr_url ?? ""));
|
|
1031
|
+
const outcome = trimOrNull3(row.outcome);
|
|
1032
|
+
if (!prUrl || outcome !== "merged" && outcome !== "skipped" && outcome !== "blocked") continue;
|
|
1033
|
+
out.push({
|
|
1034
|
+
prUrl,
|
|
1035
|
+
outcome,
|
|
1036
|
+
mergeCommit: trimOrNull3(row.mergeCommit ?? row.merge_commit),
|
|
1037
|
+
reason: trimOrNull3(row.reason)
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
return out;
|
|
1041
|
+
}
|
|
1042
|
+
function workerPrUrls(snapshot, finalResult) {
|
|
1043
|
+
const urls = [];
|
|
1044
|
+
const fromSnapshot = normalizePrUrl(trimOrNull3(snapshot.prUrl) ?? "");
|
|
1045
|
+
if (fromSnapshot) urls.push(fromSnapshot);
|
|
1046
|
+
if (finalResult && typeof finalResult === "object" && !Array.isArray(finalResult)) {
|
|
1047
|
+
const pr = normalizePrUrl(String(finalResult.prUrl ?? ""));
|
|
1048
|
+
if (pr) urls.push(pr);
|
|
1049
|
+
}
|
|
1050
|
+
return [...new Set(urls)];
|
|
1051
|
+
}
|
|
1052
|
+
function assessWorkerLandingContract(input) {
|
|
1053
|
+
const { contract, snapshot } = input;
|
|
1054
|
+
const finalResult = input.finalResult ?? snapshot.finalResult;
|
|
1055
|
+
if (!contract.landingOnly && contract.targetPrUrls.length === 0) {
|
|
1056
|
+
return { blocked: false };
|
|
1057
|
+
}
|
|
1058
|
+
if (!hasFinalResult3(finalResult)) return { blocked: false };
|
|
1059
|
+
const reconciliation = parseReconciliation(finalResult);
|
|
1060
|
+
const byUrl = new Map(reconciliation.map((r) => [r.prUrl, r]));
|
|
1061
|
+
const targetSet = new Set(
|
|
1062
|
+
contract.targetPrUrls.map((u) => normalizePrUrl(u) ?? u).filter(Boolean)
|
|
1063
|
+
);
|
|
1064
|
+
const workerPrs = workerPrUrls(snapshot, finalResult);
|
|
1065
|
+
if (contract.landingOnly) {
|
|
1066
|
+
for (const pr of workerPrs) {
|
|
1067
|
+
if (targetSet.size > 0 && !targetSet.has(pr)) {
|
|
1068
|
+
return {
|
|
1069
|
+
blocked: true,
|
|
1070
|
+
reason: "unrelated_implementation_pr",
|
|
1071
|
+
detail: `Landing-only worker attached unrelated PR ${pr}`
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
if (targetSet.size === 0) {
|
|
1075
|
+
return {
|
|
1076
|
+
blocked: true,
|
|
1077
|
+
reason: "unrelated_implementation_pr",
|
|
1078
|
+
detail: "Landing-only worker must not open new implementation PRs"
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
if (contract.targetPrUrls.length === 0) return { blocked: false };
|
|
1084
|
+
const missing = [];
|
|
1085
|
+
for (const target of contract.targetPrUrls) {
|
|
1086
|
+
const key = normalizePrUrl(target) ?? target;
|
|
1087
|
+
const entry = byUrl.get(key);
|
|
1088
|
+
if (!entry) {
|
|
1089
|
+
missing.push(key);
|
|
1090
|
+
continue;
|
|
1091
|
+
}
|
|
1092
|
+
if (entry.outcome !== "merged" && !entry.reason?.trim()) {
|
|
1093
|
+
missing.push(key);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
if (missing.length > 0) {
|
|
1097
|
+
return {
|
|
1098
|
+
blocked: true,
|
|
1099
|
+
reason: missing.every((u) => byUrl.has(u)) ? "incomplete_target_pr_landing" : "missing_target_pr_reconciliation",
|
|
1100
|
+
detail: `Target PR reconciliation incomplete: ${missing.join(", ")}`
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
return { blocked: false };
|
|
1104
|
+
}
|
|
1105
|
+
function landingContractAttentionReason(verdict) {
|
|
1106
|
+
if (!verdict.blocked) return void 0;
|
|
1107
|
+
return verdict.detail ?? verdict.reason;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
952
1110
|
// src/status.ts
|
|
953
1111
|
var NO_START_MS = 18e4;
|
|
954
1112
|
var STALE_MS = 6e5;
|
|
@@ -958,11 +1116,13 @@ function computeAttention(input) {
|
|
|
958
1116
|
return { state: "blocked", reason: input.completionBlocker };
|
|
959
1117
|
}
|
|
960
1118
|
if (input.finalResult) {
|
|
961
|
-
const
|
|
1119
|
+
const landingSnapshot = {
|
|
962
1120
|
finalResult: input.finalResult,
|
|
963
1121
|
changedFiles: input.changedFiles ?? [],
|
|
964
|
-
gitAncestry: input.gitAncestry ?? null
|
|
965
|
-
|
|
1122
|
+
gitAncestry: input.gitAncestry ?? null,
|
|
1123
|
+
prUrl: input.prUrl ?? null
|
|
1124
|
+
};
|
|
1125
|
+
const landing = assessWorkerLanding(landingSnapshot);
|
|
966
1126
|
if (landing.blocked) {
|
|
967
1127
|
const detail = landingAttentionReason(landing);
|
|
968
1128
|
return {
|
|
@@ -970,6 +1130,20 @@ function computeAttention(input) {
|
|
|
970
1130
|
reason: landing.reason ? `landing blocked (${landing.reason}): ${detail}` : `landing blocked: ${detail}`
|
|
971
1131
|
};
|
|
972
1132
|
}
|
|
1133
|
+
if (input.landingContract) {
|
|
1134
|
+
const contractVerdict = assessWorkerLandingContract({
|
|
1135
|
+
contract: input.landingContract,
|
|
1136
|
+
snapshot: landingSnapshot,
|
|
1137
|
+
finalResult: input.finalResult
|
|
1138
|
+
});
|
|
1139
|
+
const contractDetail = landingContractAttentionReason(contractVerdict);
|
|
1140
|
+
if (contractDetail) {
|
|
1141
|
+
return {
|
|
1142
|
+
state: "needs_attention",
|
|
1143
|
+
reason: contractVerdict.reason ? `landing contract (${contractVerdict.reason}): ${contractDetail}` : `landing contract: ${contractDetail}`
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
973
1147
|
return { state: "done", reason: "final result recorded" };
|
|
974
1148
|
}
|
|
975
1149
|
if (!input.alive) {
|
|
@@ -1007,11 +1181,18 @@ function computeAttention(input) {
|
|
|
1007
1181
|
}
|
|
1008
1182
|
return { state: "ok", reason: "recent activity" };
|
|
1009
1183
|
}
|
|
1184
|
+
function resolveFinalResult(worker, parsedFinalResult, heartbeat) {
|
|
1185
|
+
if (parsedFinalResult) return parsedFinalResult;
|
|
1186
|
+
const ackSnapshot = worker.completionSnapshot?.finalResult;
|
|
1187
|
+
if (ackSnapshot !== void 0 && ackSnapshot !== null) return ackSnapshot;
|
|
1188
|
+
return terminalFinalResultFromHeartbeat(heartbeat);
|
|
1189
|
+
}
|
|
1010
1190
|
function computeWorkerStatus(worker, options = {}) {
|
|
1011
1191
|
const parsed = parseHarnessStream(worker.stdoutPath);
|
|
1012
1192
|
const heartbeat = parseHeartbeat(worker.heartbeatPath);
|
|
1013
|
-
const
|
|
1014
|
-
const
|
|
1193
|
+
const completionAcknowledged = typeof worker.completionReportedAt === "string" && worker.completionReportedAt.trim().length > 0;
|
|
1194
|
+
const finalResult = resolveFinalResult(worker, parsed.finalResult, heartbeat);
|
|
1195
|
+
const alive = completionAcknowledged ? false : isPidAlive(worker.pid);
|
|
1015
1196
|
const stdoutBytes = fileSize(worker.stdoutPath);
|
|
1016
1197
|
const stderrBytes = fileSize(worker.stderrPath);
|
|
1017
1198
|
const heartbeatBytes = fileSize(worker.heartbeatPath);
|
|
@@ -1043,7 +1224,7 @@ function computeWorkerStatus(worker, options = {}) {
|
|
|
1043
1224
|
gitAncestry,
|
|
1044
1225
|
completionBlocker
|
|
1045
1226
|
});
|
|
1046
|
-
const workerStatusLabel = completionBlocker || attention.state === "blocked" ? "blocked" : attention.state === "done" ? "done" : finalResult ? "exited" : alive ? "running" : "exited";
|
|
1227
|
+
const workerStatusLabel = completionBlocker || attention.state === "blocked" ? "blocked" : completionAcknowledged || attention.state === "done" ? "done" : finalResult ? "exited" : alive ? "running" : "exited";
|
|
1047
1228
|
return {
|
|
1048
1229
|
runId: worker.runId,
|
|
1049
1230
|
worker: worker.name,
|
|
@@ -1060,12 +1241,13 @@ function computeWorkerStatus(worker, options = {}) {
|
|
|
1060
1241
|
firstEventAt: parsed.firstEventAt,
|
|
1061
1242
|
lastEventAt: parsed.lastEventAt,
|
|
1062
1243
|
lastActivityAt,
|
|
1063
|
-
currentTool: parsed.currentTool,
|
|
1244
|
+
currentTool: completionAcknowledged ? null : parsed.currentTool,
|
|
1064
1245
|
heartbeatCount: heartbeat.heartbeatCount,
|
|
1065
1246
|
lastHeartbeatAt: heartbeat.lastHeartbeatAt,
|
|
1066
1247
|
lastHeartbeatPhase: heartbeat.lastHeartbeatPhase,
|
|
1067
1248
|
lastHeartbeatSummary: heartbeat.lastHeartbeatSummary,
|
|
1068
1249
|
heartbeatBlocker: heartbeat.heartbeatBlocker,
|
|
1250
|
+
timestampAnomalies: heartbeat.timestampAnomalies,
|
|
1069
1251
|
finalResult,
|
|
1070
1252
|
error,
|
|
1071
1253
|
changedFiles,
|
|
@@ -1124,7 +1306,7 @@ function computeAutoMaxWorkers(totalMemBytes, opts = {}) {
|
|
|
1124
1306
|
function readAvailableMemBytes() {
|
|
1125
1307
|
if (process.platform === "linux") {
|
|
1126
1308
|
try {
|
|
1127
|
-
const meminfo =
|
|
1309
|
+
const meminfo = readFileSync6("/proc/meminfo", "utf8");
|
|
1128
1310
|
const match = meminfo.match(/^MemAvailable:\s+(\d+)\s*kB/m);
|
|
1129
1311
|
if (match) return Number(match[1]) * 1024;
|
|
1130
1312
|
} catch {
|
|
@@ -1205,6 +1387,16 @@ function normalize(value) {
|
|
|
1205
1387
|
return value.toLowerCase();
|
|
1206
1388
|
}
|
|
1207
1389
|
var PERSONA_DEFAULT_LANE = {
|
|
1390
|
+
ghost: "system",
|
|
1391
|
+
astra: "plan_author",
|
|
1392
|
+
rhea: "implementer",
|
|
1393
|
+
mnemo: "implementer",
|
|
1394
|
+
sentinel: "deep_reviewer",
|
|
1395
|
+
pixel: "implementer",
|
|
1396
|
+
schema: "implementer",
|
|
1397
|
+
atlas: "runtime_verifier",
|
|
1398
|
+
bridge: "implementer",
|
|
1399
|
+
catalyst: "implementer",
|
|
1208
1400
|
dalton: "implementer",
|
|
1209
1401
|
lorentz: "report_reviewer"
|
|
1210
1402
|
};
|
|
@@ -1583,8 +1775,8 @@ function hasLiveWorkerForTask(runId, taskId) {
|
|
|
1583
1775
|
}
|
|
1584
1776
|
|
|
1585
1777
|
// src/supervisor.ts
|
|
1586
|
-
import { existsSync as
|
|
1587
|
-
import
|
|
1778
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync3 } from "node:fs";
|
|
1779
|
+
import path12 from "node:path";
|
|
1588
1780
|
|
|
1589
1781
|
// src/prompt.ts
|
|
1590
1782
|
function buildPrompt(input) {
|
|
@@ -1606,6 +1798,7 @@ function buildPrompt(input) {
|
|
|
1606
1798
|
const planArtifactLines = compact ? [
|
|
1607
1799
|
"Plan artifacts: when authoring/revising docs/superpowers/plans/, open a GitHub PR early and iterate from that PR branch; do not leave the canonical plan only in the harness worktree."
|
|
1608
1800
|
] : [
|
|
1801
|
+
"Plan persistence: use AgentOS API/MCP first; on approval/auth/network/server/interruption failures run `kynver plan persist` (queues under ~/.kynver/state/plan-outbox) then `kynver plan outbox drain` when connectivity returns. Never treat /tmp-only files as persisted plans.",
|
|
1609
1802
|
"PR-first plan artifacts (when authoring or revising docs/superpowers/plans/):",
|
|
1610
1803
|
"- Before substantial plan drafting: create a feature branch, open a GitHub PR (draft OK), commit and push the plan file \u2014 do not leave the canonical plan only in this harness worktree.",
|
|
1611
1804
|
"- Iterate review on that PR branch; link prUrl on the AgentOS task and plan progress evidence (`--evidence pr:<url>`).",
|
|
@@ -1619,6 +1812,7 @@ function buildPrompt(input) {
|
|
|
1619
1812
|
`Progress heartbeat file: ${input.heartbeatPath}`,
|
|
1620
1813
|
"After each major step, append one JSON line to the heartbeat file with fields: ts, phase, summary, changedFiles, blocker.",
|
|
1621
1814
|
"Final response must include files changed, verification commands, and unresolved risks.",
|
|
1815
|
+
"Structured final result (recommended): record completion as JSON with summary, laneExpertise { whatChanged, why, files, prUrls, verification, risks, blockers, lessonsLearned, laneGuidance }, and targetPrReconciliation [{ prUrl, outcome: merged|skipped|blocked, mergeCommit?, reason? }] for every target PR on landing-only tasks.",
|
|
1622
1816
|
"Completion handoff (required): before you stop, ensure the harness records a final result \u2014 summarize outcome in your last message and append a heartbeat line with phase `complete`. If you leave uncommitted changes or committed work without a PR, the orchestrator blocks completion until a GitHub PR exists (or you discard/commit cleanly). Exiting with only dirty files and no PR routes to salvage review, not production review.",
|
|
1623
1817
|
"PR-ready handoff: for substantial implementation work, commit, push, and open a GitHub PR (draft OK) on your branch before finishing \u2014 or rely on the harness to run `gh pr create` at completion when `gh` is authenticated.",
|
|
1624
1818
|
"Worker resource guard: do not run full monorepo verification (`npm run typecheck`, `npm run build`, or equivalent) from this worker lane unless an operator explicitly requests it. Use targeted checks for touched paths and rely on CI/operator lanes for heavy gates.",
|
|
@@ -1636,12 +1830,12 @@ function buildPrompt(input) {
|
|
|
1636
1830
|
}
|
|
1637
1831
|
|
|
1638
1832
|
// src/providers/cursor.ts
|
|
1639
|
-
import { closeSync as closeSync2, existsSync as
|
|
1833
|
+
import { closeSync as closeSync2, existsSync as existsSync9, openSync as openSync2 } from "node:fs";
|
|
1640
1834
|
import { spawn as spawn2 } from "node:child_process";
|
|
1641
1835
|
import path8 from "node:path";
|
|
1642
1836
|
|
|
1643
1837
|
// src/providers/cursor-windows.ts
|
|
1644
|
-
import { existsSync as
|
|
1838
|
+
import { existsSync as existsSync8, readdirSync as readdirSync3 } from "node:fs";
|
|
1645
1839
|
import path7 from "node:path";
|
|
1646
1840
|
var CURSOR_VERSION_DIR = /^\d{4}\.\d{1,2}\.\d{1,2}-[a-f0-9]+$/i;
|
|
1647
1841
|
function parseCursorVersionSortKey(versionName) {
|
|
@@ -1654,7 +1848,7 @@ function parseCursorVersionSortKey(versionName) {
|
|
|
1654
1848
|
}
|
|
1655
1849
|
function pickLatestCursorVersionDir(agentRoot) {
|
|
1656
1850
|
const versionsRoot = path7.join(agentRoot, "versions");
|
|
1657
|
-
if (!
|
|
1851
|
+
if (!existsSync8(versionsRoot)) return null;
|
|
1658
1852
|
let bestDir = null;
|
|
1659
1853
|
let bestKey = -1;
|
|
1660
1854
|
for (const entry of readdirSync3(versionsRoot, { withFileTypes: true })) {
|
|
@@ -1670,14 +1864,14 @@ function resolveWindowsCursorBundled(agentRoot) {
|
|
|
1670
1864
|
const root = agentRoot?.trim() || path7.join(process.env.LOCALAPPDATA || "", "cursor-agent");
|
|
1671
1865
|
const directNode = path7.join(root, "node.exe");
|
|
1672
1866
|
const directIndex = path7.join(root, "index.js");
|
|
1673
|
-
if (
|
|
1867
|
+
if (existsSync8(directNode) && existsSync8(directIndex)) {
|
|
1674
1868
|
return { nodeExe: directNode, indexJs: directIndex, versionDir: root };
|
|
1675
1869
|
}
|
|
1676
1870
|
const versionDir = pickLatestCursorVersionDir(root);
|
|
1677
1871
|
if (!versionDir) return null;
|
|
1678
1872
|
const nodeExe = path7.join(versionDir, "node.exe");
|
|
1679
1873
|
const indexJs = path7.join(versionDir, "index.js");
|
|
1680
|
-
if (!
|
|
1874
|
+
if (!existsSync8(nodeExe) || !existsSync8(indexJs)) return null;
|
|
1681
1875
|
return { nodeExe, indexJs, versionDir };
|
|
1682
1876
|
}
|
|
1683
1877
|
|
|
@@ -1695,7 +1889,7 @@ function bundledSpawnTarget(nodeExe, indexJs, versionDir) {
|
|
|
1695
1889
|
function resolveCursorSpawn(agentBin) {
|
|
1696
1890
|
if (process.platform === "win32") {
|
|
1697
1891
|
const isCursorWrapper = /\.(cmd|bat)$/i.test(agentBin);
|
|
1698
|
-
const isBundledNode = /node\.exe$/i.test(agentBin) &&
|
|
1892
|
+
const isBundledNode = /node\.exe$/i.test(agentBin) && existsSync9(path8.join(path8.dirname(agentBin), "index.js"));
|
|
1699
1893
|
const isDefaultShim = agentBin === "agent";
|
|
1700
1894
|
if (isCursorWrapper || isBundledNode || isDefaultShim) {
|
|
1701
1895
|
const bundled = isCursorWrapper ? resolveWindowsCursorBundled(path8.dirname(agentBin)) : isBundledNode ? {
|
|
@@ -1722,7 +1916,7 @@ function resolveAgentBin() {
|
|
|
1722
1916
|
);
|
|
1723
1917
|
if (bundled) return bundled.nodeExe;
|
|
1724
1918
|
const localAgent = path8.join(process.env.LOCALAPPDATA || "", "cursor-agent", "agent.cmd");
|
|
1725
|
-
if (
|
|
1919
|
+
if (existsSync9(localAgent)) return localAgent;
|
|
1726
1920
|
}
|
|
1727
1921
|
return "agent";
|
|
1728
1922
|
}
|
|
@@ -1804,16 +1998,16 @@ function resolveWorkerProvider(name) {
|
|
|
1804
1998
|
|
|
1805
1999
|
// src/auto-complete.ts
|
|
1806
2000
|
import { spawn as spawn3 } from "node:child_process";
|
|
1807
|
-
import { existsSync as
|
|
1808
|
-
import
|
|
1809
|
-
import { fileURLToPath } from "node:url";
|
|
2001
|
+
import { existsSync as existsSync10, openSync as openSync3, closeSync as closeSync3 } from "node:fs";
|
|
2002
|
+
import path11 from "node:path";
|
|
2003
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1810
2004
|
|
|
1811
2005
|
// src/worker-ops.ts
|
|
1812
|
-
import
|
|
2006
|
+
import path10 from "node:path";
|
|
1813
2007
|
|
|
1814
2008
|
// src/pr-handoff/pr-handoff-assess.ts
|
|
1815
2009
|
var REVIEW_LANE_RULE = /^(lane:)?(review|deep_review|planning|landing)(:|$)/i;
|
|
1816
|
-
function
|
|
2010
|
+
function trimOrNull4(value) {
|
|
1817
2011
|
if (typeof value !== "string") return null;
|
|
1818
2012
|
const trimmed = value.trim();
|
|
1819
2013
|
return trimmed.length ? trimmed : null;
|
|
@@ -1821,7 +2015,7 @@ function trimOrNull3(value) {
|
|
|
1821
2015
|
function committedHead(ancestry) {
|
|
1822
2016
|
if (!ancestry?.checked) return null;
|
|
1823
2017
|
if (ancestry.headIsAncestorOfBase !== false) return null;
|
|
1824
|
-
return
|
|
2018
|
+
return trimOrNull4(ancestry.head);
|
|
1825
2019
|
}
|
|
1826
2020
|
function extractPrUrlFromText(value) {
|
|
1827
2021
|
if (value === void 0 || value === null) return null;
|
|
@@ -1829,11 +2023,11 @@ function extractPrUrlFromText(value) {
|
|
|
1829
2023
|
const m = text.match(
|
|
1830
2024
|
/https?:\/\/[^\s)>"]+\/(?:pull|pulls|merge_requests|pull-requests)\/\d+/i
|
|
1831
2025
|
);
|
|
1832
|
-
return m ?
|
|
2026
|
+
return m ? trimOrNull4(m[0]) : null;
|
|
1833
2027
|
}
|
|
1834
2028
|
function hasWorkProduct(snapshot) {
|
|
1835
2029
|
if (snapshot.changedFiles.length > 0) return true;
|
|
1836
|
-
if (
|
|
2030
|
+
if (trimOrNull4(snapshot.headCommit)) return true;
|
|
1837
2031
|
if (committedHead(snapshot.gitAncestry)) return true;
|
|
1838
2032
|
return false;
|
|
1839
2033
|
}
|
|
@@ -1841,14 +2035,14 @@ function assessPrHandoffRequirement(input) {
|
|
|
1841
2035
|
if (!input.dispatched) {
|
|
1842
2036
|
return { required: false, reason: "not_dispatched" };
|
|
1843
2037
|
}
|
|
1844
|
-
const rule =
|
|
2038
|
+
const rule = trimOrNull4(input.routingRule) ?? "";
|
|
1845
2039
|
if (rule && REVIEW_LANE_RULE.test(rule)) {
|
|
1846
2040
|
return { required: false, reason: "review_lane" };
|
|
1847
2041
|
}
|
|
1848
|
-
if (
|
|
2042
|
+
if (trimOrNull4(input.patchPath) || trimOrNull4(input.artifactBundlePath)) {
|
|
1849
2043
|
return { required: false, reason: "patch_or_bundle" };
|
|
1850
2044
|
}
|
|
1851
|
-
const prUrl =
|
|
2045
|
+
const prUrl = trimOrNull4(input.prUrl) ?? trimOrNull4(input.snapshot.prUrl);
|
|
1852
2046
|
if (prUrl) {
|
|
1853
2047
|
return { required: false, reason: "already_has_pr" };
|
|
1854
2048
|
}
|
|
@@ -1864,8 +2058,8 @@ function buildPrHandoffSnapshotFromStatus(status, extras) {
|
|
|
1864
2058
|
worktreePath: status.worktreePath,
|
|
1865
2059
|
gitAncestry: status.gitAncestry,
|
|
1866
2060
|
finalResult: status.finalResult,
|
|
1867
|
-
headCommit:
|
|
1868
|
-
prUrl:
|
|
2061
|
+
headCommit: trimOrNull4(extras?.headCommit) ?? committedHead(status.gitAncestry),
|
|
2062
|
+
prUrl: trimOrNull4(extras?.prUrl) ?? null
|
|
1869
2063
|
};
|
|
1870
2064
|
}
|
|
1871
2065
|
|
|
@@ -2158,6 +2352,80 @@ function persistCompletionAck(worker, runId, fields) {
|
|
|
2158
2352
|
saveWorker(runId, worker);
|
|
2159
2353
|
}
|
|
2160
2354
|
|
|
2355
|
+
// src/worker-lifecycle.ts
|
|
2356
|
+
import path9 from "node:path";
|
|
2357
|
+
var TASK_LEFT_RUNNING = /* @__PURE__ */ new Set([
|
|
2358
|
+
"awaiting_review",
|
|
2359
|
+
"blocked",
|
|
2360
|
+
"done",
|
|
2361
|
+
"needs_input",
|
|
2362
|
+
"waiting",
|
|
2363
|
+
"scheduled",
|
|
2364
|
+
"ready",
|
|
2365
|
+
"cancelled",
|
|
2366
|
+
"failed"
|
|
2367
|
+
]);
|
|
2368
|
+
function isCompletionAcknowledged(worker) {
|
|
2369
|
+
return Boolean(
|
|
2370
|
+
typeof worker.completionReportedAt === "string" && worker.completionReportedAt.trim()
|
|
2371
|
+
);
|
|
2372
|
+
}
|
|
2373
|
+
function completionSnapshotFromStatus(status) {
|
|
2374
|
+
const summary = typeof status.lastHeartbeatSummary === "string" ? status.lastHeartbeatSummary : null;
|
|
2375
|
+
return {
|
|
2376
|
+
finalResult: status.finalResult ?? summary ?? "completed",
|
|
2377
|
+
summary
|
|
2378
|
+
};
|
|
2379
|
+
}
|
|
2380
|
+
function persistCompletionAcknowledged(worker, status, opts) {
|
|
2381
|
+
if (isCompletionAcknowledged(worker) && worker.status === "done" && worker.completionSnapshot != null) {
|
|
2382
|
+
return;
|
|
2383
|
+
}
|
|
2384
|
+
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
2385
|
+
const snapshot = completionSnapshotFromStatus(status);
|
|
2386
|
+
const source = opts?.source?.trim();
|
|
2387
|
+
worker.completionReportedAt = at;
|
|
2388
|
+
worker.completionSnapshot = snapshot;
|
|
2389
|
+
worker.status = "done";
|
|
2390
|
+
if (source) worker.completionAckSource = source;
|
|
2391
|
+
saveWorker(worker.runId, worker);
|
|
2392
|
+
}
|
|
2393
|
+
function taskStatusByIdFromOperatorTick(operatorTick) {
|
|
2394
|
+
const map = /* @__PURE__ */ new Map();
|
|
2395
|
+
const body = operatorTick;
|
|
2396
|
+
const items = body.response?.tick?.filteredItems;
|
|
2397
|
+
if (!Array.isArray(items)) return map;
|
|
2398
|
+
for (const item of items) {
|
|
2399
|
+
if (item.kind !== "task" || typeof item.id !== "string") continue;
|
|
2400
|
+
const status = typeof item.taskStatus === "string" ? item.taskStatus.trim() : "";
|
|
2401
|
+
if (status) map.set(item.id, status);
|
|
2402
|
+
}
|
|
2403
|
+
return map;
|
|
2404
|
+
}
|
|
2405
|
+
function syncCompletionAcknowledgedFromOperatorTick(runId, operatorTick) {
|
|
2406
|
+
const taskStatusById = taskStatusByIdFromOperatorTick(operatorTick);
|
|
2407
|
+
if (taskStatusById.size === 0) return [];
|
|
2408
|
+
const run = loadRun(runId);
|
|
2409
|
+
const synced = [];
|
|
2410
|
+
for (const name of Object.keys(run.workers || {})) {
|
|
2411
|
+
const worker = readJson(
|
|
2412
|
+
path9.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
2413
|
+
void 0
|
|
2414
|
+
);
|
|
2415
|
+
if (!worker?.taskId || isCompletionAcknowledged(worker)) continue;
|
|
2416
|
+
const taskStatus = taskStatusById.get(worker.taskId);
|
|
2417
|
+
if (!taskStatus || taskStatus === "running" || !TASK_LEFT_RUNNING.has(taskStatus)) {
|
|
2418
|
+
continue;
|
|
2419
|
+
}
|
|
2420
|
+
const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
|
|
2421
|
+
persistCompletionAcknowledged(worker, status, {
|
|
2422
|
+
source: `board-task-${taskStatus}`
|
|
2423
|
+
});
|
|
2424
|
+
synced.push({ worker: name, taskId: worker.taskId, taskStatus });
|
|
2425
|
+
}
|
|
2426
|
+
return synced;
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2161
2429
|
// src/worker-ops.ts
|
|
2162
2430
|
async function postCompletion(url, secret, body) {
|
|
2163
2431
|
const res = await fetch(url, {
|
|
@@ -2289,6 +2557,7 @@ async function tryCompleteWorker(args) {
|
|
|
2289
2557
|
workerInjection: {
|
|
2290
2558
|
instructionPolicyFingerprint: worker.instructionPolicyFingerprint ?? null,
|
|
2291
2559
|
instructionPolicyEvidence: worker.instructionPolicyEvidence ?? null,
|
|
2560
|
+
policyAt: worker.instructionPolicyEvidence && typeof worker.instructionPolicyEvidence === "object" && "policyAt" in worker.instructionPolicyEvidence && typeof worker.instructionPolicyEvidence.policyAt === "string" ? worker.instructionPolicyEvidence.policyAt : null,
|
|
2292
2561
|
personaSlug: worker.personaSlug ?? null,
|
|
2293
2562
|
personaEvidence: worker.personaEvidence ?? null
|
|
2294
2563
|
}
|
|
@@ -2309,6 +2578,7 @@ async function tryCompleteWorker(args) {
|
|
|
2309
2578
|
completionResponse: result.parsed
|
|
2310
2579
|
};
|
|
2311
2580
|
persistCompletionAck(worker, worker.runId, ack);
|
|
2581
|
+
persistCompletionAcknowledged(worker, status, { source: "harness-completion" });
|
|
2312
2582
|
const prUrl = status.prUrl;
|
|
2313
2583
|
return {
|
|
2314
2584
|
ok: true,
|
|
@@ -2376,7 +2646,7 @@ function workerStatus(args) {
|
|
|
2376
2646
|
const worker = loadWorker(String(args.run), String(args.name));
|
|
2377
2647
|
const run = loadRun(worker.runId);
|
|
2378
2648
|
const status = computeWorkerStatus(worker, workerStatusOptions(run));
|
|
2379
|
-
writeJson(
|
|
2649
|
+
writeJson(path10.join(worker.workerDir, "last-status.json"), status);
|
|
2380
2650
|
console.log(JSON.stringify(status, null, 2));
|
|
2381
2651
|
}
|
|
2382
2652
|
function buildRunBoard(runId) {
|
|
@@ -2384,7 +2654,7 @@ function buildRunBoard(runId) {
|
|
|
2384
2654
|
const names = Object.keys(run.workers || {});
|
|
2385
2655
|
const workers = names.map((name) => {
|
|
2386
2656
|
const worker = readJson(
|
|
2387
|
-
|
|
2657
|
+
path10.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
2388
2658
|
void 0
|
|
2389
2659
|
);
|
|
2390
2660
|
if (!worker) {
|
|
@@ -2494,7 +2764,7 @@ function buildRunBoard(runId) {
|
|
|
2494
2764
|
needsAttention: workers.filter((w) => w.attention && w.attention !== "ok" && w.attention !== "done").map((w) => w.worker),
|
|
2495
2765
|
workers
|
|
2496
2766
|
};
|
|
2497
|
-
writeJson(
|
|
2767
|
+
writeJson(path10.join(runDirectory(run.id), "last-board.json"), board);
|
|
2498
2768
|
return board;
|
|
2499
2769
|
}
|
|
2500
2770
|
async function publishHarnessBoardSnapshot(args, source) {
|
|
@@ -2661,12 +2931,12 @@ async function autoCompleteWorkerCli(raw) {
|
|
|
2661
2931
|
}
|
|
2662
2932
|
}
|
|
2663
2933
|
function resolveDefaultCliPath() {
|
|
2664
|
-
return
|
|
2934
|
+
return path11.join(fileURLToPath2(new URL(".", import.meta.url)), "cli.js");
|
|
2665
2935
|
}
|
|
2666
2936
|
function spawnCompletionSidecar(opts) {
|
|
2667
2937
|
const cliPath = opts.cliPath ?? resolveDefaultCliPath();
|
|
2668
|
-
if (!
|
|
2669
|
-
const logPath =
|
|
2938
|
+
if (!existsSync10(cliPath)) return void 0;
|
|
2939
|
+
const logPath = path11.join(opts.workerDir, "auto-complete.log");
|
|
2670
2940
|
let logFd;
|
|
2671
2941
|
try {
|
|
2672
2942
|
logFd = openSync3(logPath, "a");
|
|
@@ -2748,16 +3018,16 @@ function spawnWorkerProcess(run, opts) {
|
|
|
2748
3018
|
launchModel = preflight.model;
|
|
2749
3019
|
}
|
|
2750
3020
|
const { worktreesDir } = getPaths();
|
|
2751
|
-
const workerDir =
|
|
3021
|
+
const workerDir = path12.join(runDirectory(run.id), "workers", name);
|
|
2752
3022
|
mkdirSync3(workerDir, { recursive: true });
|
|
2753
|
-
const worktreePath =
|
|
3023
|
+
const worktreePath = path12.join(worktreesDir, run.id, name);
|
|
2754
3024
|
const branch = opts.branch || `agent/${run.id}/${name}`;
|
|
2755
|
-
if (
|
|
3025
|
+
if (existsSync11(worktreePath)) throw new Error(`worktree path already exists: ${worktreePath}`);
|
|
2756
3026
|
git(run.repo, ["fetch", "origin", "--prune"], { allowFailure: true });
|
|
2757
3027
|
git(run.repo, ["worktree", "add", "-b", branch, worktreePath, run.baseCommit], { throwError: true });
|
|
2758
|
-
const stdoutPath =
|
|
2759
|
-
const stderrPath =
|
|
2760
|
-
const heartbeatPath =
|
|
3028
|
+
const stdoutPath = path12.join(workerDir, "stdout.jsonl");
|
|
3029
|
+
const stderrPath = path12.join(workerDir, "stderr.log");
|
|
3030
|
+
const heartbeatPath = path12.join(workerDir, "heartbeat.jsonl");
|
|
2761
3031
|
const prompt = buildPrompt({
|
|
2762
3032
|
task: opts.task,
|
|
2763
3033
|
ownedPaths: opts.ownedPaths || [],
|
|
@@ -2818,7 +3088,7 @@ function spawnWorkerProcess(run, opts) {
|
|
|
2818
3088
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2819
3089
|
};
|
|
2820
3090
|
saveWorker(run.id, worker);
|
|
2821
|
-
run.workers = { ...run.workers || {}, [name]: { workerDir, statusPath:
|
|
3091
|
+
run.workers = { ...run.workers || {}, [name]: { workerDir, statusPath: path12.join(workerDir, "worker.json") } };
|
|
2822
3092
|
run.status = "running";
|
|
2823
3093
|
saveRun(run);
|
|
2824
3094
|
if (worker.agentOsId && worker.taskId) {
|
|
@@ -2912,6 +3182,579 @@ async function startWorker(args) {
|
|
|
2912
3182
|
}
|
|
2913
3183
|
}
|
|
2914
3184
|
|
|
3185
|
+
// src/plan-persist/body-hash.ts
|
|
3186
|
+
import { createHash } from "node:crypto";
|
|
3187
|
+
function hashPlanBody(body) {
|
|
3188
|
+
const normalized = body.replace(/\r\n/g, "\n").trimEnd();
|
|
3189
|
+
return createHash("sha256").update(normalized, "utf8").digest("hex");
|
|
3190
|
+
}
|
|
3191
|
+
function hashSummary(summary) {
|
|
3192
|
+
if (summary == null) return null;
|
|
3193
|
+
const trimmed = summary.trim();
|
|
3194
|
+
if (!trimmed) return null;
|
|
3195
|
+
return createHash("sha256").update(trimmed, "utf8").digest("hex");
|
|
3196
|
+
}
|
|
3197
|
+
|
|
3198
|
+
// src/plan-persist/errors.ts
|
|
3199
|
+
var PlanPersistError = class extends Error {
|
|
3200
|
+
kind;
|
|
3201
|
+
httpStatus;
|
|
3202
|
+
constructor(kind, message, httpStatus) {
|
|
3203
|
+
super(message);
|
|
3204
|
+
this.name = "PlanPersistError";
|
|
3205
|
+
this.kind = kind;
|
|
3206
|
+
this.httpStatus = httpStatus;
|
|
3207
|
+
}
|
|
3208
|
+
};
|
|
3209
|
+
function classifyHttpFailure(status, message) {
|
|
3210
|
+
if (status === 401 || status === 403) {
|
|
3211
|
+
return new PlanPersistError("auth", message, status);
|
|
3212
|
+
}
|
|
3213
|
+
if (status >= 500) {
|
|
3214
|
+
return new PlanPersistError("server", message, status);
|
|
3215
|
+
}
|
|
3216
|
+
return new PlanPersistError("permanent", message, status);
|
|
3217
|
+
}
|
|
3218
|
+
function classifyFetchFailure(err) {
|
|
3219
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3220
|
+
if (/ECONNREFUSED|ENOTFOUND|ETIMEDOUT|fetch failed|network/i.test(message)) {
|
|
3221
|
+
return new PlanPersistError("network", message);
|
|
3222
|
+
}
|
|
3223
|
+
return new PlanPersistError("tool_interruption", message);
|
|
3224
|
+
}
|
|
3225
|
+
function isRetryableFailure(kind) {
|
|
3226
|
+
return kind !== "permanent";
|
|
3227
|
+
}
|
|
3228
|
+
|
|
3229
|
+
// src/plan-persist/agentos-api.ts
|
|
3230
|
+
function authHeaders(apiKey) {
|
|
3231
|
+
const headers = { "Content-Type": "application/json" };
|
|
3232
|
+
if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
|
|
3233
|
+
return headers;
|
|
3234
|
+
}
|
|
3235
|
+
async function parseJsonResponse(res) {
|
|
3236
|
+
const text = await res.text();
|
|
3237
|
+
try {
|
|
3238
|
+
return JSON.parse(text);
|
|
3239
|
+
} catch {
|
|
3240
|
+
return text;
|
|
3241
|
+
}
|
|
3242
|
+
}
|
|
3243
|
+
async function agentOsGetPlan(slug, planId, deps = {}) {
|
|
3244
|
+
const base = resolveBaseUrl(deps.baseUrl);
|
|
3245
|
+
const apiKey = deps.apiKey ?? loadApiKey();
|
|
3246
|
+
const fetchFn = deps.fetchFn ?? fetch;
|
|
3247
|
+
const url = `${base}/api/agent-os/${encodeURIComponent(slug)}/plans/${encodeURIComponent(planId)}`;
|
|
3248
|
+
try {
|
|
3249
|
+
const res = await fetchFn(url, { method: "GET", headers: authHeaders(apiKey) });
|
|
3250
|
+
const parsed = await parseJsonResponse(res);
|
|
3251
|
+
if (!res.ok) {
|
|
3252
|
+
const msg = typeof parsed === "object" && parsed && "error" in parsed ? String(parsed.error) : `GET plan failed (${res.status})`;
|
|
3253
|
+
throw classifyHttpFailure(res.status, msg);
|
|
3254
|
+
}
|
|
3255
|
+
return parsed;
|
|
3256
|
+
} catch (err) {
|
|
3257
|
+
if (err instanceof PlanPersistError) throw err;
|
|
3258
|
+
throw classifyFetchFailure(err);
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
3261
|
+
async function agentOsWritePlan(input, deps = {}) {
|
|
3262
|
+
const base = resolveBaseUrl(deps.baseUrl);
|
|
3263
|
+
const apiKey = deps.apiKey ?? loadApiKey();
|
|
3264
|
+
const fetchFn = deps.fetchFn ?? fetch;
|
|
3265
|
+
const slug = input.agentOsSlug;
|
|
3266
|
+
try {
|
|
3267
|
+
if (input.operation === "create") {
|
|
3268
|
+
const url2 = `${base}/api/agent-os/${encodeURIComponent(slug)}/plans`;
|
|
3269
|
+
const body2 = {
|
|
3270
|
+
title: input.title,
|
|
3271
|
+
summary: input.summary ?? null,
|
|
3272
|
+
slug: input.planSlug ?? null,
|
|
3273
|
+
sourceRefs: mergeSourceRefs(input),
|
|
3274
|
+
initialVersion: {
|
|
3275
|
+
title: input.title,
|
|
3276
|
+
body: input.body,
|
|
3277
|
+
summary: input.summary ?? null,
|
|
3278
|
+
changeSummary: input.changeSummary ?? null,
|
|
3279
|
+
author: input.author ?? null,
|
|
3280
|
+
sourceRefs: mergeSourceRefs(input)
|
|
3281
|
+
}
|
|
3282
|
+
};
|
|
3283
|
+
const res2 = await fetchFn(url2, {
|
|
3284
|
+
method: "POST",
|
|
3285
|
+
headers: authHeaders(apiKey),
|
|
3286
|
+
body: JSON.stringify(body2)
|
|
3287
|
+
});
|
|
3288
|
+
const parsed2 = await parseJsonResponse(res2);
|
|
3289
|
+
if (!res2.ok) {
|
|
3290
|
+
const msg = typeof parsed2 === "object" && parsed2 && "error" in parsed2 ? String(parsed2.error) : `create plan failed (${res2.status})`;
|
|
3291
|
+
throw classifyHttpFailure(res2.status, msg);
|
|
3292
|
+
}
|
|
3293
|
+
const row2 = parsed2;
|
|
3294
|
+
return {
|
|
3295
|
+
planId: row2.plan.id,
|
|
3296
|
+
versionId: row2.version.id,
|
|
3297
|
+
versionNumber: row2.version.versionNumber
|
|
3298
|
+
};
|
|
3299
|
+
}
|
|
3300
|
+
const planId = input.planId;
|
|
3301
|
+
if (!planId) throw new PlanPersistError("permanent", "planId is required for this operation");
|
|
3302
|
+
if (input.operation === "update_metadata") {
|
|
3303
|
+
const url2 = `${base}/api/agent-os/${encodeURIComponent(slug)}/plans/${encodeURIComponent(planId)}`;
|
|
3304
|
+
const body2 = {
|
|
3305
|
+
title: input.title,
|
|
3306
|
+
summary: input.summary ?? null,
|
|
3307
|
+
sourceRefs: mergeSourceRefs(input)
|
|
3308
|
+
};
|
|
3309
|
+
const res2 = await fetchFn(url2, {
|
|
3310
|
+
method: "PATCH",
|
|
3311
|
+
headers: authHeaders(apiKey),
|
|
3312
|
+
body: JSON.stringify(body2)
|
|
3313
|
+
});
|
|
3314
|
+
const parsed2 = await parseJsonResponse(res2);
|
|
3315
|
+
if (!res2.ok) {
|
|
3316
|
+
const msg = typeof parsed2 === "object" && parsed2 && "error" in parsed2 ? String(parsed2.error) : `update plan failed (${res2.status})`;
|
|
3317
|
+
throw classifyHttpFailure(res2.status, msg);
|
|
3318
|
+
}
|
|
3319
|
+
const row2 = parsed2;
|
|
3320
|
+
return {
|
|
3321
|
+
planId: row2.id,
|
|
3322
|
+
versionId: row2.currentVersionId,
|
|
3323
|
+
versionNumber: null
|
|
3324
|
+
};
|
|
3325
|
+
}
|
|
3326
|
+
const url = `${base}/api/agent-os/${encodeURIComponent(slug)}/plans/${encodeURIComponent(planId)}/versions`;
|
|
3327
|
+
const body = {
|
|
3328
|
+
title: input.title,
|
|
3329
|
+
body: input.body,
|
|
3330
|
+
summary: input.summary ?? null,
|
|
3331
|
+
changeSummary: input.changeSummary ?? null,
|
|
3332
|
+
author: input.author ?? null,
|
|
3333
|
+
sourceRefs: mergeSourceRefs(input),
|
|
3334
|
+
markCurrent: input.markCurrent !== false
|
|
3335
|
+
};
|
|
3336
|
+
const res = await fetchFn(url, {
|
|
3337
|
+
method: "POST",
|
|
3338
|
+
headers: authHeaders(apiKey),
|
|
3339
|
+
body: JSON.stringify(body)
|
|
3340
|
+
});
|
|
3341
|
+
const parsed = await parseJsonResponse(res);
|
|
3342
|
+
if (!res.ok) {
|
|
3343
|
+
const msg = typeof parsed === "object" && parsed && "error" in parsed ? String(parsed.error) : `add version failed (${res.status})`;
|
|
3344
|
+
throw classifyHttpFailure(res.status, msg);
|
|
3345
|
+
}
|
|
3346
|
+
const row = parsed;
|
|
3347
|
+
return {
|
|
3348
|
+
planId: row.version.planId,
|
|
3349
|
+
versionId: row.version.id,
|
|
3350
|
+
versionNumber: row.version.versionNumber
|
|
3351
|
+
};
|
|
3352
|
+
} catch (err) {
|
|
3353
|
+
if (err instanceof PlanPersistError) throw err;
|
|
3354
|
+
throw classifyFetchFailure(err);
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
function mergeSourceRefs(input) {
|
|
3358
|
+
const refs = { ...input.sourceRefs ?? {} };
|
|
3359
|
+
if (input.model) refs.model = input.model;
|
|
3360
|
+
if (!Object.keys(refs).length) return input.sourceRefs ?? null;
|
|
3361
|
+
return refs;
|
|
3362
|
+
}
|
|
3363
|
+
|
|
3364
|
+
// src/plan-persist/idempotency.ts
|
|
3365
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
3366
|
+
function buildPlanPersistIdempotencyKey(input) {
|
|
3367
|
+
const payload = {
|
|
3368
|
+
operation: input.operation,
|
|
3369
|
+
agentOsSlug: input.agentOsSlug,
|
|
3370
|
+
planId: input.planId ?? null,
|
|
3371
|
+
planSlug: input.planSlug ?? null,
|
|
3372
|
+
title: input.title.trim(),
|
|
3373
|
+
summaryHash: hashSummary(input.summary),
|
|
3374
|
+
bodyHash: hashPlanBody(input.body),
|
|
3375
|
+
changeSummary: input.changeSummary?.trim() ?? null,
|
|
3376
|
+
markCurrent: input.markCurrent ?? true
|
|
3377
|
+
};
|
|
3378
|
+
return createHash2("sha256").update(JSON.stringify(payload), "utf8").digest("hex");
|
|
3379
|
+
}
|
|
3380
|
+
|
|
3381
|
+
// src/plan-persist/paths.ts
|
|
3382
|
+
import { mkdirSync as mkdirSync4 } from "node:fs";
|
|
3383
|
+
import { homedir as homedir3 } from "node:os";
|
|
3384
|
+
import path13 from "node:path";
|
|
3385
|
+
function resolveKynverStateRoot() {
|
|
3386
|
+
const env = process.env.KYNVER_STATE_ROOT;
|
|
3387
|
+
if (env) return path13.resolve(env);
|
|
3388
|
+
return path13.join(homedir3(), ".kynver", "state");
|
|
3389
|
+
}
|
|
3390
|
+
function planOutboxDir() {
|
|
3391
|
+
return path13.join(resolveKynverStateRoot(), "plan-outbox");
|
|
3392
|
+
}
|
|
3393
|
+
function planOutboxArchiveDir() {
|
|
3394
|
+
return path13.join(resolveKynverStateRoot(), "plan-outbox-archive");
|
|
3395
|
+
}
|
|
3396
|
+
function ensurePlanOutboxDirs() {
|
|
3397
|
+
const outboxDir = planOutboxDir();
|
|
3398
|
+
const archiveDir = planOutboxArchiveDir();
|
|
3399
|
+
mkdirSync4(outboxDir, { recursive: true });
|
|
3400
|
+
mkdirSync4(archiveDir, { recursive: true });
|
|
3401
|
+
return { outboxDir, archiveDir };
|
|
3402
|
+
}
|
|
3403
|
+
function isTmpOnlyPath(filePath) {
|
|
3404
|
+
const resolved = path13.resolve(filePath);
|
|
3405
|
+
return resolved.startsWith("/tmp/") || resolved.startsWith(path13.join("/var", "folders"));
|
|
3406
|
+
}
|
|
3407
|
+
|
|
3408
|
+
// src/plan-persist/outbox-store.ts
|
|
3409
|
+
import {
|
|
3410
|
+
existsSync as existsSync13,
|
|
3411
|
+
readFileSync as readFileSync7,
|
|
3412
|
+
renameSync,
|
|
3413
|
+
readdirSync as readdirSync4,
|
|
3414
|
+
writeFileSync as writeFileSync3,
|
|
3415
|
+
unlinkSync
|
|
3416
|
+
} from "node:fs";
|
|
3417
|
+
import path14 from "node:path";
|
|
3418
|
+
import { randomUUID } from "node:crypto";
|
|
3419
|
+
var DEFAULT_MAX_RETRIES = 12;
|
|
3420
|
+
function listOutboxItems() {
|
|
3421
|
+
const { outboxDir } = ensurePlanOutboxDirs();
|
|
3422
|
+
const files = readdirSync4(outboxDir).filter((f) => f.endsWith(".json"));
|
|
3423
|
+
const items = [];
|
|
3424
|
+
for (const file of files) {
|
|
3425
|
+
const item = readOutboxItem(path14.join(outboxDir, file));
|
|
3426
|
+
if (item && item.queueStatus === "queued") items.push(item);
|
|
3427
|
+
}
|
|
3428
|
+
return items.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
3429
|
+
}
|
|
3430
|
+
function findOutboxByIdempotencyKey(key) {
|
|
3431
|
+
for (const item of listOutboxItems()) {
|
|
3432
|
+
if (item.idempotencyKey === key) return item;
|
|
3433
|
+
}
|
|
3434
|
+
return null;
|
|
3435
|
+
}
|
|
3436
|
+
function readOutboxItem(jsonPath) {
|
|
3437
|
+
if (!existsSync13(jsonPath)) return null;
|
|
3438
|
+
try {
|
|
3439
|
+
return JSON.parse(readFileSync7(jsonPath, "utf8"));
|
|
3440
|
+
} catch {
|
|
3441
|
+
return null;
|
|
3442
|
+
}
|
|
3443
|
+
}
|
|
3444
|
+
function readOutboxBody(item) {
|
|
3445
|
+
const { outboxDir } = ensurePlanOutboxDirs();
|
|
3446
|
+
const bodyFile = path14.join(outboxDir, item.bodyPath);
|
|
3447
|
+
return readFileSync7(bodyFile, "utf8");
|
|
3448
|
+
}
|
|
3449
|
+
function writeOutboxItem(input, opts) {
|
|
3450
|
+
const { outboxDir } = ensurePlanOutboxDirs();
|
|
3451
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3452
|
+
const id = opts.existing?.id ?? randomUUID();
|
|
3453
|
+
const bodyPath = opts.existing?.bodyPath ?? `${id}.body.md`;
|
|
3454
|
+
const jsonPath = path14.join(outboxDir, `${id}.json`);
|
|
3455
|
+
const bodyFile = path14.join(outboxDir, bodyPath);
|
|
3456
|
+
if (!opts.existing) {
|
|
3457
|
+
writeFileSync3(bodyFile, input.body, "utf8");
|
|
3458
|
+
}
|
|
3459
|
+
const item = {
|
|
3460
|
+
id,
|
|
3461
|
+
idempotencyKey: buildPlanPersistIdempotencyKey(input),
|
|
3462
|
+
operation: input.operation,
|
|
3463
|
+
agentOsSlug: input.agentOsSlug,
|
|
3464
|
+
planId: input.planId ?? opts.existing?.planId ?? null,
|
|
3465
|
+
planSlug: input.planSlug ?? opts.existing?.planSlug ?? null,
|
|
3466
|
+
title: input.title,
|
|
3467
|
+
summary: input.summary ?? null,
|
|
3468
|
+
bodyPath,
|
|
3469
|
+
bodyHash: hashPlanBody(input.body),
|
|
3470
|
+
author: input.author ?? null,
|
|
3471
|
+
model: input.model ?? null,
|
|
3472
|
+
sourceRefs: input.sourceRefs ?? null,
|
|
3473
|
+
changeSummary: input.changeSummary ?? null,
|
|
3474
|
+
markCurrent: input.markCurrent ?? true,
|
|
3475
|
+
createdAt: opts.existing?.createdAt ?? now,
|
|
3476
|
+
updatedAt: now,
|
|
3477
|
+
retryCount: (opts.existing?.retryCount ?? 0) + (opts.existing ? 1 : 0),
|
|
3478
|
+
maxRetries: input.maxRetries ?? opts.existing?.maxRetries ?? DEFAULT_MAX_RETRIES,
|
|
3479
|
+
lastError: opts.lastError,
|
|
3480
|
+
lastFailureKind: opts.lastFailureKind,
|
|
3481
|
+
queueStatus: "queued",
|
|
3482
|
+
userStatus: "queued for retry",
|
|
3483
|
+
readbackEvidence: null
|
|
3484
|
+
};
|
|
3485
|
+
writeFileSync3(jsonPath, `${JSON.stringify(item, null, 2)}
|
|
3486
|
+
`, { mode: 384 });
|
|
3487
|
+
return item;
|
|
3488
|
+
}
|
|
3489
|
+
function saveOutboxItem(item) {
|
|
3490
|
+
const { outboxDir } = ensurePlanOutboxDirs();
|
|
3491
|
+
const jsonPath = path14.join(outboxDir, `${item.id}.json`);
|
|
3492
|
+
writeFileSync3(jsonPath, `${JSON.stringify(item, null, 2)}
|
|
3493
|
+
`, { mode: 384 });
|
|
3494
|
+
}
|
|
3495
|
+
function archiveOutboxItem(item) {
|
|
3496
|
+
const { outboxDir, archiveDir } = ensurePlanOutboxDirs();
|
|
3497
|
+
const jsonSrc = path14.join(outboxDir, `${item.id}.json`);
|
|
3498
|
+
const bodySrc = path14.join(outboxDir, item.bodyPath);
|
|
3499
|
+
const jsonDst = path14.join(archiveDir, `${item.id}.json`);
|
|
3500
|
+
const bodyDst = path14.join(archiveDir, item.bodyPath);
|
|
3501
|
+
if (existsSync13(jsonSrc)) renameSync(jsonSrc, jsonDst);
|
|
3502
|
+
if (existsSync13(bodySrc)) renameSync(bodySrc, bodyDst);
|
|
3503
|
+
}
|
|
3504
|
+
function outboxItemPaths(item) {
|
|
3505
|
+
const { outboxDir } = ensurePlanOutboxDirs();
|
|
3506
|
+
return {
|
|
3507
|
+
jsonPath: path14.join(outboxDir, `${item.id}.json`),
|
|
3508
|
+
bodyPath: path14.join(outboxDir, item.bodyPath)
|
|
3509
|
+
};
|
|
3510
|
+
}
|
|
3511
|
+
function outboxInputFromItem(item, body) {
|
|
3512
|
+
return {
|
|
3513
|
+
operation: item.operation,
|
|
3514
|
+
agentOsSlug: item.agentOsSlug,
|
|
3515
|
+
planId: item.planId,
|
|
3516
|
+
planSlug: item.planSlug,
|
|
3517
|
+
title: item.title,
|
|
3518
|
+
summary: item.summary,
|
|
3519
|
+
body,
|
|
3520
|
+
changeSummary: item.changeSummary ?? void 0,
|
|
3521
|
+
author: item.author ?? void 0,
|
|
3522
|
+
model: item.model ?? void 0,
|
|
3523
|
+
sourceRefs: item.sourceRefs,
|
|
3524
|
+
markCurrent: item.markCurrent ?? true,
|
|
3525
|
+
maxRetries: item.maxRetries
|
|
3526
|
+
};
|
|
3527
|
+
}
|
|
3528
|
+
|
|
3529
|
+
// src/plan-persist/readback.ts
|
|
3530
|
+
async function verifyPlanReadback(slug, expectation, deps = {}) {
|
|
3531
|
+
const payload = await agentOsGetPlan(slug, expectation.planId, deps);
|
|
3532
|
+
const plan = payload.plan;
|
|
3533
|
+
const current = payload.currentVersion;
|
|
3534
|
+
if (plan.title.trim() !== expectation.title.trim()) {
|
|
3535
|
+
throw new PlanPersistError(
|
|
3536
|
+
"verification_failed",
|
|
3537
|
+
`title mismatch: expected "${expectation.title}", got "${plan.title}"`
|
|
3538
|
+
);
|
|
3539
|
+
}
|
|
3540
|
+
const expectedSummaryHash = hashSummary(expectation.summary);
|
|
3541
|
+
const actualSummaryHash = hashSummary(plan.summary);
|
|
3542
|
+
if (expectedSummaryHash !== actualSummaryHash) {
|
|
3543
|
+
throw new PlanPersistError("verification_failed", "summary mismatch on readback");
|
|
3544
|
+
}
|
|
3545
|
+
if (expectation.versionId && plan.currentVersionId !== expectation.versionId) {
|
|
3546
|
+
throw new PlanPersistError(
|
|
3547
|
+
"verification_failed",
|
|
3548
|
+
`currentVersionId mismatch: expected ${expectation.versionId}, got ${plan.currentVersionId}`
|
|
3549
|
+
);
|
|
3550
|
+
}
|
|
3551
|
+
if (expectation.versionNumber != null) {
|
|
3552
|
+
if (!current || current.versionNumber !== expectation.versionNumber) {
|
|
3553
|
+
throw new PlanPersistError(
|
|
3554
|
+
"verification_failed",
|
|
3555
|
+
`versionNumber mismatch: expected ${expectation.versionNumber}, got ${current?.versionNumber ?? "none"}`
|
|
3556
|
+
);
|
|
3557
|
+
}
|
|
3558
|
+
}
|
|
3559
|
+
const bodyForHash = current?.body ?? "";
|
|
3560
|
+
const actualBodyHash = hashPlanBody(bodyForHash);
|
|
3561
|
+
if (expectation.bodyHash && actualBodyHash !== expectation.bodyHash) {
|
|
3562
|
+
throw new PlanPersistError("verification_failed", "body hash mismatch on readback");
|
|
3563
|
+
}
|
|
3564
|
+
return {
|
|
3565
|
+
planId: plan.id,
|
|
3566
|
+
currentVersionId: plan.currentVersionId,
|
|
3567
|
+
versionNumber: current?.versionNumber ?? null,
|
|
3568
|
+
title: plan.title,
|
|
3569
|
+
summary: plan.summary,
|
|
3570
|
+
bodyHash: expectation.bodyHash || actualBodyHash,
|
|
3571
|
+
readAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3572
|
+
};
|
|
3573
|
+
}
|
|
3574
|
+
function buildReadbackExpectation(input, write) {
|
|
3575
|
+
return {
|
|
3576
|
+
planId: write.planId,
|
|
3577
|
+
title: input.title,
|
|
3578
|
+
summary: input.summary ?? null,
|
|
3579
|
+
body: input.body,
|
|
3580
|
+
bodyHash: hashPlanBody(input.body),
|
|
3581
|
+
versionId: write.versionId,
|
|
3582
|
+
versionNumber: write.versionNumber
|
|
3583
|
+
};
|
|
3584
|
+
}
|
|
3585
|
+
|
|
3586
|
+
// src/plan-persist/persist.ts
|
|
3587
|
+
var SUCCESS_STATUS = "persisted and read back";
|
|
3588
|
+
var QUEUED_STATUS = "queued for retry";
|
|
3589
|
+
var FAILED_STATUS = "failed and needs action";
|
|
3590
|
+
async function persistPlan(input, deps = {}) {
|
|
3591
|
+
if (input.bodyPathHint && isTmpOnlyPath(input.bodyPathHint)) {
|
|
3592
|
+
}
|
|
3593
|
+
const idempotencyKey = buildPlanPersistIdempotencyKey(input);
|
|
3594
|
+
const existingOutbox = findOutboxByIdempotencyKey(idempotencyKey);
|
|
3595
|
+
if (existingOutbox?.readbackEvidence) {
|
|
3596
|
+
return {
|
|
3597
|
+
userStatus: SUCCESS_STATUS,
|
|
3598
|
+
outboxId: existingOutbox.id,
|
|
3599
|
+
planId: existingOutbox.readbackEvidence.planId,
|
|
3600
|
+
readbackEvidence: existingOutbox.readbackEvidence,
|
|
3601
|
+
idempotencyKey
|
|
3602
|
+
};
|
|
3603
|
+
}
|
|
3604
|
+
if (input.immediateFailure) {
|
|
3605
|
+
return queueForRetry(input, input.immediateFailure.message, input.immediateFailure.kind, existingOutbox);
|
|
3606
|
+
}
|
|
3607
|
+
const writeFn = deps.writePlan ?? agentOsWritePlan;
|
|
3608
|
+
const verifyFn = deps.verifyReadback ?? verifyPlanReadback;
|
|
3609
|
+
try {
|
|
3610
|
+
const write = await writeFn(input, deps);
|
|
3611
|
+
const enriched = { ...input, planId: write.planId };
|
|
3612
|
+
const expectation = buildReadbackExpectation(enriched, write);
|
|
3613
|
+
const readback = await verifyFn(
|
|
3614
|
+
input.agentOsSlug,
|
|
3615
|
+
readbackExpectationForOperation(input, expectation),
|
|
3616
|
+
deps
|
|
3617
|
+
);
|
|
3618
|
+
if (existingOutbox) archiveOutboxItem(existingOutbox);
|
|
3619
|
+
return {
|
|
3620
|
+
userStatus: SUCCESS_STATUS,
|
|
3621
|
+
planId: write.planId,
|
|
3622
|
+
versionId: write.versionId ?? void 0,
|
|
3623
|
+
readbackEvidence: readback,
|
|
3624
|
+
idempotencyKey
|
|
3625
|
+
};
|
|
3626
|
+
} catch (err) {
|
|
3627
|
+
const failure = err instanceof PlanPersistError ? err : new PlanPersistError("tool_interruption", err instanceof Error ? err.message : String(err));
|
|
3628
|
+
if (!isRetryableFailure(failure.kind)) {
|
|
3629
|
+
const item = writeOutboxItem(input, {
|
|
3630
|
+
lastError: failure.message,
|
|
3631
|
+
lastFailureKind: failure.kind,
|
|
3632
|
+
existing: existingOutbox ?? void 0
|
|
3633
|
+
});
|
|
3634
|
+
const paths = outboxItemPaths(item);
|
|
3635
|
+
const failed = markOutboxFailed(item, failure.message);
|
|
3636
|
+
return {
|
|
3637
|
+
userStatus: FAILED_STATUS,
|
|
3638
|
+
outboxId: failed.id,
|
|
3639
|
+
outboxPath: paths.jsonPath,
|
|
3640
|
+
bodyPath: paths.bodyPath,
|
|
3641
|
+
lastError: failure.message,
|
|
3642
|
+
idempotencyKey
|
|
3643
|
+
};
|
|
3644
|
+
}
|
|
3645
|
+
return queueForRetry(input, failure.message, failure.kind, existingOutbox);
|
|
3646
|
+
}
|
|
3647
|
+
}
|
|
3648
|
+
function readbackExpectationForOperation(input, expectation) {
|
|
3649
|
+
if (input.operation === "update_metadata") {
|
|
3650
|
+
return { ...expectation, body: "", bodyHash: "" };
|
|
3651
|
+
}
|
|
3652
|
+
return expectation;
|
|
3653
|
+
}
|
|
3654
|
+
function queueForRetry(input, message, kind, existing) {
|
|
3655
|
+
const item = writeOutboxItem(input, {
|
|
3656
|
+
lastError: message,
|
|
3657
|
+
lastFailureKind: kind,
|
|
3658
|
+
existing: existing ?? void 0
|
|
3659
|
+
});
|
|
3660
|
+
const paths = outboxItemPaths(item);
|
|
3661
|
+
if (item.retryCount >= item.maxRetries) {
|
|
3662
|
+
const failed = markOutboxFailed(item, message);
|
|
3663
|
+
return {
|
|
3664
|
+
userStatus: FAILED_STATUS,
|
|
3665
|
+
outboxId: failed.id,
|
|
3666
|
+
outboxPath: paths.jsonPath,
|
|
3667
|
+
bodyPath: paths.bodyPath,
|
|
3668
|
+
lastError: message,
|
|
3669
|
+
idempotencyKey: item.idempotencyKey
|
|
3670
|
+
};
|
|
3671
|
+
}
|
|
3672
|
+
return {
|
|
3673
|
+
userStatus: QUEUED_STATUS,
|
|
3674
|
+
outboxId: item.id,
|
|
3675
|
+
outboxPath: paths.jsonPath,
|
|
3676
|
+
bodyPath: paths.bodyPath,
|
|
3677
|
+
lastError: message,
|
|
3678
|
+
idempotencyKey: item.idempotencyKey
|
|
3679
|
+
};
|
|
3680
|
+
}
|
|
3681
|
+
function markOutboxFailed(item, message) {
|
|
3682
|
+
const failed = {
|
|
3683
|
+
...item,
|
|
3684
|
+
queueStatus: "failed",
|
|
3685
|
+
userStatus: FAILED_STATUS,
|
|
3686
|
+
lastError: message,
|
|
3687
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3688
|
+
};
|
|
3689
|
+
saveOutboxItem(failed);
|
|
3690
|
+
return failed;
|
|
3691
|
+
}
|
|
3692
|
+
|
|
3693
|
+
// src/plan-persist/drain.ts
|
|
3694
|
+
import path15 from "node:path";
|
|
3695
|
+
async function drainPlanOutbox(opts = {}, deps = {}) {
|
|
3696
|
+
const items = listOutboxItems().filter(
|
|
3697
|
+
(item) => opts.outboxId ? item.id === opts.outboxId : true
|
|
3698
|
+
);
|
|
3699
|
+
const slice = opts.max && opts.max > 0 ? items.slice(0, opts.max) : items;
|
|
3700
|
+
const result = {
|
|
3701
|
+
processed: 0,
|
|
3702
|
+
succeeded: 0,
|
|
3703
|
+
stillQueued: 0,
|
|
3704
|
+
failed: 0,
|
|
3705
|
+
results: []
|
|
3706
|
+
};
|
|
3707
|
+
for (const item of slice) {
|
|
3708
|
+
result.processed += 1;
|
|
3709
|
+
const body = readOutboxBody(item);
|
|
3710
|
+
const input = outboxInputFromItem(item, body);
|
|
3711
|
+
const attempt = await persistPlan(input, deps);
|
|
3712
|
+
if (attempt.userStatus === "persisted and read back") {
|
|
3713
|
+
result.succeeded += 1;
|
|
3714
|
+
} else if (attempt.userStatus === "failed and needs action") {
|
|
3715
|
+
result.failed += 1;
|
|
3716
|
+
} else {
|
|
3717
|
+
result.stillQueued += 1;
|
|
3718
|
+
}
|
|
3719
|
+
result.results.push({
|
|
3720
|
+
outboxId: item.id,
|
|
3721
|
+
userStatus: attempt.userStatus,
|
|
3722
|
+
lastError: attempt.lastError
|
|
3723
|
+
});
|
|
3724
|
+
}
|
|
3725
|
+
return result;
|
|
3726
|
+
}
|
|
3727
|
+
function loadOutboxById(outboxId) {
|
|
3728
|
+
const jsonPath = path15.join(planOutboxDir(), `${outboxId}.json`);
|
|
3729
|
+
return readOutboxItem(jsonPath);
|
|
3730
|
+
}
|
|
3731
|
+
|
|
3732
|
+
// src/plan-persist/handoff.ts
|
|
3733
|
+
function formatPlanOutboxHandoffBlock(item) {
|
|
3734
|
+
const paths = outboxItemPaths(item);
|
|
3735
|
+
return [
|
|
3736
|
+
"## Plan persistence risk",
|
|
3737
|
+
"",
|
|
3738
|
+
`AgentOS plan write is **not** confirmed (${item.userStatus}).`,
|
|
3739
|
+
`- outboxId: \`${item.id}\``,
|
|
3740
|
+
item.planId ? `- planId: \`${item.planId}\`` : "- planId: (pending \u2014 create not yet applied)",
|
|
3741
|
+
`- outbox: \`${paths.jsonPath}\``,
|
|
3742
|
+
`- body: \`${paths.bodyPath}\``,
|
|
3743
|
+
"",
|
|
3744
|
+
"Drain when approval/connectivity returns: `kynver plan outbox drain`"
|
|
3745
|
+
].join("\n");
|
|
3746
|
+
}
|
|
3747
|
+
function extractPlanOutboxFromTask(task) {
|
|
3748
|
+
const meta = task.metadata && typeof task.metadata === "object" ? task.metadata : null;
|
|
3749
|
+
const outboxId = typeof task.planPersistenceOutboxId === "string" && task.planPersistenceOutboxId || (meta && typeof meta.planPersistenceOutboxId === "string" ? meta.planPersistenceOutboxId : void 0);
|
|
3750
|
+
if (!outboxId) return null;
|
|
3751
|
+
return {
|
|
3752
|
+
outboxId,
|
|
3753
|
+
jsonPath: typeof task.planPersistenceOutboxPath === "string" ? task.planPersistenceOutboxPath : meta && typeof meta.planPersistenceOutboxPath === "string" ? meta.planPersistenceOutboxPath : void 0,
|
|
3754
|
+
bodyPath: typeof task.planPersistenceBodyPath === "string" ? task.planPersistenceBodyPath : meta && typeof meta.planPersistenceBodyPath === "string" ? meta.planPersistenceBodyPath : void 0
|
|
3755
|
+
};
|
|
3756
|
+
}
|
|
3757
|
+
|
|
2915
3758
|
// src/dispatch.ts
|
|
2916
3759
|
var DEFAULT_DISPATCH_LEASE_MS = 60 * 60 * 1e3;
|
|
2917
3760
|
function readHarnessWorkerContext(decision) {
|
|
@@ -2941,14 +3784,29 @@ function normalizePersonaSlug(value) {
|
|
|
2941
3784
|
return trimmed.length ? trimmed : null;
|
|
2942
3785
|
}
|
|
2943
3786
|
function buildDispatchTaskText(task, agentOsId) {
|
|
2944
|
-
|
|
3787
|
+
const lines = [
|
|
2945
3788
|
`[AgentOS task ${task.id}] ${task.title}`,
|
|
2946
3789
|
"",
|
|
2947
3790
|
task.description ? String(task.description) : "(no description on the board task)",
|
|
2948
3791
|
"",
|
|
2949
3792
|
`Board linkage: agentOsId=${agentOsId}, taskId=${task.id}, attempt=${task.attempt}, executor=${task.executor}${task.executorRef ? `, executorRef=${task.executorRef}` : ""}.`,
|
|
2950
3793
|
"This worker was dispatched from the AgentOS board. The harness reports your completion back to the board when you finish."
|
|
2951
|
-
]
|
|
3794
|
+
];
|
|
3795
|
+
const outboxRef = extractPlanOutboxFromTask(task);
|
|
3796
|
+
if (outboxRef?.outboxId) {
|
|
3797
|
+
const item = loadOutboxById(outboxRef.outboxId);
|
|
3798
|
+
if (item) {
|
|
3799
|
+
lines.push("", formatPlanOutboxHandoffBlock(item));
|
|
3800
|
+
} else {
|
|
3801
|
+
lines.push(
|
|
3802
|
+
"",
|
|
3803
|
+
`## Plan persistence risk`,
|
|
3804
|
+
"",
|
|
3805
|
+
`Unconfirmed AgentOS plan write (outboxId=${outboxRef.outboxId}).`
|
|
3806
|
+
);
|
|
3807
|
+
}
|
|
3808
|
+
}
|
|
3809
|
+
return lines.join("\n");
|
|
2952
3810
|
}
|
|
2953
3811
|
async function dispatchRun(args) {
|
|
2954
3812
|
const pipeline = args.pipeline === true || args.pipeline === "true";
|
|
@@ -2967,7 +3825,7 @@ async function dispatchRun(args) {
|
|
|
2967
3825
|
const activeHarnessWorkers = [];
|
|
2968
3826
|
for (const name of Object.keys(run.workers || {})) {
|
|
2969
3827
|
const worker = readJson(
|
|
2970
|
-
|
|
3828
|
+
path16.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
2971
3829
|
void 0
|
|
2972
3830
|
);
|
|
2973
3831
|
if (!worker?.taskId || !isPidAlive(worker.pid)) continue;
|
|
@@ -3180,7 +4038,7 @@ function redactHarness(text, secret) {
|
|
|
3180
4038
|
}
|
|
3181
4039
|
|
|
3182
4040
|
// src/validate.ts
|
|
3183
|
-
import
|
|
4041
|
+
import path17 from "node:path";
|
|
3184
4042
|
var RUN_ID_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/i;
|
|
3185
4043
|
var WORKER_NAME_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/i;
|
|
3186
4044
|
function validateRunId(runId) {
|
|
@@ -3194,15 +4052,15 @@ function validateWorkerName(name) {
|
|
|
3194
4052
|
return trimmed;
|
|
3195
4053
|
}
|
|
3196
4054
|
function validateRepo(repo) {
|
|
3197
|
-
const resolved =
|
|
4055
|
+
const resolved = path17.resolve(repo);
|
|
3198
4056
|
if (resolved.includes("..")) throw new Error("repo path must not contain .. segments");
|
|
3199
4057
|
return resolved;
|
|
3200
4058
|
}
|
|
3201
4059
|
function validateOwnedPaths(repoRoot, ownedPaths) {
|
|
3202
4060
|
return ownedPaths.map((owned) => {
|
|
3203
|
-
const resolved =
|
|
3204
|
-
const rel =
|
|
3205
|
-
if (rel.startsWith("..") ||
|
|
4061
|
+
const resolved = path17.resolve(repoRoot, owned);
|
|
4062
|
+
const rel = path17.relative(repoRoot, resolved);
|
|
4063
|
+
if (rel.startsWith("..") || path17.isAbsolute(rel)) {
|
|
3206
4064
|
throw new Error(`owned path escapes repo: ${owned}`);
|
|
3207
4065
|
}
|
|
3208
4066
|
return resolved;
|
|
@@ -3214,15 +4072,15 @@ function validateTailLines(lines) {
|
|
|
3214
4072
|
}
|
|
3215
4073
|
|
|
3216
4074
|
// src/worktree.ts
|
|
3217
|
-
import { existsSync as
|
|
3218
|
-
import
|
|
4075
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync5 } from "node:fs";
|
|
4076
|
+
import path18 from "node:path";
|
|
3219
4077
|
function createRun(args) {
|
|
3220
4078
|
const repo = validateRepo(required(String(args.repo || ""), "--repo"));
|
|
3221
4079
|
ensureGitRepo(repo);
|
|
3222
4080
|
const id = args.id ? validateRunId(String(args.id)) : timestampSlug(String(args.name || "run"));
|
|
3223
4081
|
const dir = runDirectory(id);
|
|
3224
|
-
if (
|
|
3225
|
-
|
|
4082
|
+
if (existsSync14(dir)) failExists(`run already exists: ${id}`);
|
|
4083
|
+
mkdirSync5(dir, { recursive: true });
|
|
3226
4084
|
const base = String(args.base || "origin/main");
|
|
3227
4085
|
const baseCommit = git(repo, ["rev-parse", base]).trim();
|
|
3228
4086
|
const run = {
|
|
@@ -3235,12 +4093,12 @@ function createRun(args) {
|
|
|
3235
4093
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3236
4094
|
workers: {}
|
|
3237
4095
|
};
|
|
3238
|
-
writeJson(
|
|
4096
|
+
writeJson(path18.join(dir, "run.json"), run);
|
|
3239
4097
|
console.log(JSON.stringify({ runId: id, runDir: dir, repo, base, baseCommit }, null, 2));
|
|
3240
4098
|
}
|
|
3241
4099
|
function listRuns() {
|
|
3242
4100
|
const { runsDir } = getPaths();
|
|
3243
|
-
const rows = listRunIds(runsDir).map((id) => readJson(
|
|
4101
|
+
const rows = listRunIds(runsDir).map((id) => readJson(path18.join(runDirectory(id), "run.json"), void 0)).filter(Boolean).map((run) => ({
|
|
3244
4102
|
id: run.id,
|
|
3245
4103
|
name: run.name,
|
|
3246
4104
|
status: run.status,
|
|
@@ -3255,7 +4113,7 @@ function failExists(message) {
|
|
|
3255
4113
|
}
|
|
3256
4114
|
|
|
3257
4115
|
// src/sweep.ts
|
|
3258
|
-
import
|
|
4116
|
+
import path19 from "node:path";
|
|
3259
4117
|
async function sweepRun(args) {
|
|
3260
4118
|
const pipeline = args.pipeline === true || args.pipeline === "true";
|
|
3261
4119
|
try {
|
|
@@ -3268,7 +4126,7 @@ async function sweepRun(args) {
|
|
|
3268
4126
|
const releasedLocalOrphans = [];
|
|
3269
4127
|
for (const name of Object.keys(run.workers || {})) {
|
|
3270
4128
|
const worker = readJson(
|
|
3271
|
-
|
|
4129
|
+
path19.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
3272
4130
|
void 0
|
|
3273
4131
|
);
|
|
3274
4132
|
if (!worker || !worker.dispatched || !worker.taskId) continue;
|
|
@@ -3311,17 +4169,68 @@ async function sweepRun(args) {
|
|
|
3311
4169
|
}
|
|
3312
4170
|
|
|
3313
4171
|
// src/cli.ts
|
|
3314
|
-
import { mkdirSync as
|
|
3315
|
-
import { fileURLToPath as
|
|
4172
|
+
import { mkdirSync as mkdirSync7, realpathSync } from "node:fs";
|
|
4173
|
+
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
3316
4174
|
|
|
3317
4175
|
// src/pipeline-tick.ts
|
|
3318
|
-
import
|
|
4176
|
+
import path28 from "node:path";
|
|
4177
|
+
|
|
4178
|
+
// src/pipeline-dispatch.ts
|
|
4179
|
+
var RESERVED_REVIEW_STARTS = 1;
|
|
4180
|
+
function countDispatchStarts(result) {
|
|
4181
|
+
if (!result || typeof result !== "object") return 0;
|
|
4182
|
+
const startedCount = result.startedCount;
|
|
4183
|
+
if (typeof startedCount === "number") return startedCount;
|
|
4184
|
+
const outcomes = result.outcomes;
|
|
4185
|
+
if (!Array.isArray(outcomes)) return 0;
|
|
4186
|
+
return outcomes.filter((o) => o.started).length;
|
|
4187
|
+
}
|
|
4188
|
+
function stripCliMaxStarts(args) {
|
|
4189
|
+
const { maxStarts: _maxStarts, ...rest } = args;
|
|
4190
|
+
return rest;
|
|
4191
|
+
}
|
|
4192
|
+
async function runPipelineDispatch(args, slots) {
|
|
4193
|
+
if (slots <= 0) {
|
|
4194
|
+
return { ok: true, skipped: true, reason: "no slots", maxStarts: 0, startedCount: 0 };
|
|
4195
|
+
}
|
|
4196
|
+
const base = stripCliMaxStarts(args);
|
|
4197
|
+
const reviewBudget = Math.min(slots, RESERVED_REVIEW_STARTS);
|
|
4198
|
+
const workBudget = Math.max(0, slots - reviewBudget);
|
|
4199
|
+
const review = await dispatchRun({
|
|
4200
|
+
...base,
|
|
4201
|
+
execute: true,
|
|
4202
|
+
pipeline: true,
|
|
4203
|
+
lane: "review",
|
|
4204
|
+
maxStarts: String(reviewBudget)
|
|
4205
|
+
});
|
|
4206
|
+
const reviewStarted = countDispatchStarts(review);
|
|
4207
|
+
const workSlots = workBudget + (reviewBudget - reviewStarted);
|
|
4208
|
+
if (workSlots <= 0) {
|
|
4209
|
+
return {
|
|
4210
|
+
...typeof review === "object" && review !== null ? review : {},
|
|
4211
|
+
passes: { review },
|
|
4212
|
+
startedCount: reviewStarted
|
|
4213
|
+
};
|
|
4214
|
+
}
|
|
4215
|
+
const work = await dispatchRun({
|
|
4216
|
+
...base,
|
|
4217
|
+
execute: true,
|
|
4218
|
+
pipeline: true,
|
|
4219
|
+
maxStarts: String(workSlots)
|
|
4220
|
+
});
|
|
4221
|
+
const workStarted = countDispatchStarts(work);
|
|
4222
|
+
return {
|
|
4223
|
+
passes: { review, work },
|
|
4224
|
+
startedCount: reviewStarted + workStarted,
|
|
4225
|
+
ok: true
|
|
4226
|
+
};
|
|
4227
|
+
}
|
|
3319
4228
|
|
|
3320
4229
|
// src/stale-reconcile.ts
|
|
3321
|
-
import
|
|
4230
|
+
import path21 from "node:path";
|
|
3322
4231
|
|
|
3323
4232
|
// src/finalize.ts
|
|
3324
|
-
import
|
|
4233
|
+
import path20 from "node:path";
|
|
3325
4234
|
var ACTIVE_RUN_STATUSES = /* @__PURE__ */ new Set(["running", "dispatching", "pending", "queued"]);
|
|
3326
4235
|
function terminalStatusFor(run) {
|
|
3327
4236
|
const names = Object.keys(run.workers || {});
|
|
@@ -3332,7 +4241,7 @@ function terminalStatusFor(run) {
|
|
|
3332
4241
|
let anyLandingBlocked = false;
|
|
3333
4242
|
for (const name of names) {
|
|
3334
4243
|
const worker = readJson(
|
|
3335
|
-
|
|
4244
|
+
path20.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
3336
4245
|
void 0
|
|
3337
4246
|
);
|
|
3338
4247
|
if (!worker) continue;
|
|
@@ -3384,7 +4293,7 @@ function reconcileStaleWorkers() {
|
|
|
3384
4293
|
const now = Date.now();
|
|
3385
4294
|
for (const run of listRunRecords()) {
|
|
3386
4295
|
for (const name of Object.keys(run.workers || {})) {
|
|
3387
|
-
const workerPath =
|
|
4296
|
+
const workerPath = path21.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
|
|
3388
4297
|
const worker = readJson(workerPath, void 0);
|
|
3389
4298
|
if (!worker || worker.status !== "running") {
|
|
3390
4299
|
outcomes.push({
|
|
@@ -3460,7 +4369,7 @@ function reconcileStaleWorkers() {
|
|
|
3460
4369
|
}
|
|
3461
4370
|
|
|
3462
4371
|
// src/plan-progress-daemon-sync.ts
|
|
3463
|
-
import
|
|
4372
|
+
import path22 from "node:path";
|
|
3464
4373
|
|
|
3465
4374
|
// src/plan-progress-sync.ts
|
|
3466
4375
|
async function syncPlanProgress(args) {
|
|
@@ -3484,7 +4393,7 @@ async function syncActiveWorkerPlanProgress(runId, args) {
|
|
|
3484
4393
|
const outcomes = [];
|
|
3485
4394
|
for (const name of Object.keys(run.workers || {})) {
|
|
3486
4395
|
const worker = readJson(
|
|
3487
|
-
|
|
4396
|
+
path22.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
3488
4397
|
void 0
|
|
3489
4398
|
);
|
|
3490
4399
|
if (!worker?.dispatched || !worker.taskId) continue;
|
|
@@ -3533,7 +4442,7 @@ async function fetchWorkspaceRuntimePreferences(agentOsId, args) {
|
|
|
3533
4442
|
}
|
|
3534
4443
|
|
|
3535
4444
|
// src/cleanup.ts
|
|
3536
|
-
import
|
|
4445
|
+
import path27 from "node:path";
|
|
3537
4446
|
|
|
3538
4447
|
// src/cleanup-types.ts
|
|
3539
4448
|
var DEFAULT_NODE_MODULES_AGE_MS = 6 * 60 * 60 * 1e3;
|
|
@@ -3604,14 +4513,14 @@ function skipNodeModulesRemoval(input) {
|
|
|
3604
4513
|
}
|
|
3605
4514
|
|
|
3606
4515
|
// src/cleanup-execute.ts
|
|
3607
|
-
import { existsSync as
|
|
3608
|
-
import
|
|
4516
|
+
import { existsSync as existsSync16, rmSync } from "node:fs";
|
|
4517
|
+
import path24 from "node:path";
|
|
3609
4518
|
|
|
3610
4519
|
// src/cleanup-dir-size.ts
|
|
3611
|
-
import { existsSync as
|
|
3612
|
-
import
|
|
4520
|
+
import { existsSync as existsSync15, readdirSync as readdirSync5, statSync as statSync2 } from "node:fs";
|
|
4521
|
+
import path23 from "node:path";
|
|
3613
4522
|
function directorySizeBytes(root, maxEntries = 5e4) {
|
|
3614
|
-
if (!
|
|
4523
|
+
if (!existsSync15(root)) return 0;
|
|
3615
4524
|
let total = 0;
|
|
3616
4525
|
let seen = 0;
|
|
3617
4526
|
const stack = [root];
|
|
@@ -3619,13 +4528,13 @@ function directorySizeBytes(root, maxEntries = 5e4) {
|
|
|
3619
4528
|
const current = stack.pop();
|
|
3620
4529
|
let entries;
|
|
3621
4530
|
try {
|
|
3622
|
-
entries =
|
|
4531
|
+
entries = readdirSync5(current);
|
|
3623
4532
|
} catch {
|
|
3624
4533
|
continue;
|
|
3625
4534
|
}
|
|
3626
4535
|
for (const name of entries) {
|
|
3627
4536
|
if (seen++ > maxEntries) return null;
|
|
3628
|
-
const full =
|
|
4537
|
+
const full = path23.join(current, name);
|
|
3629
4538
|
let st;
|
|
3630
4539
|
try {
|
|
3631
4540
|
st = statSync2(full);
|
|
@@ -3641,7 +4550,7 @@ function directorySizeBytes(root, maxEntries = 5e4) {
|
|
|
3641
4550
|
|
|
3642
4551
|
// src/cleanup-execute.ts
|
|
3643
4552
|
function removeNodeModules(candidate, execute) {
|
|
3644
|
-
if (!
|
|
4553
|
+
if (!existsSync16(candidate.path)) {
|
|
3645
4554
|
return {
|
|
3646
4555
|
...candidate,
|
|
3647
4556
|
executed: false,
|
|
@@ -3672,7 +4581,7 @@ function removeNodeModules(candidate, execute) {
|
|
|
3672
4581
|
}
|
|
3673
4582
|
}
|
|
3674
4583
|
function removeWorktree(candidate, execute) {
|
|
3675
|
-
if (!
|
|
4584
|
+
if (!existsSync16(candidate.path)) {
|
|
3676
4585
|
return {
|
|
3677
4586
|
...candidate,
|
|
3678
4587
|
executed: false,
|
|
@@ -3689,7 +4598,7 @@ function removeWorktree(candidate, execute) {
|
|
|
3689
4598
|
if (repo) {
|
|
3690
4599
|
git(repo, ["worktree", "remove", "--force", candidate.path], { allowFailure: true });
|
|
3691
4600
|
}
|
|
3692
|
-
if (
|
|
4601
|
+
if (existsSync16(candidate.path)) {
|
|
3693
4602
|
rmSync(candidate.path, { recursive: true, force: true });
|
|
3694
4603
|
}
|
|
3695
4604
|
return {
|
|
@@ -3709,20 +4618,20 @@ function removeWorktree(candidate, execute) {
|
|
|
3709
4618
|
}
|
|
3710
4619
|
}
|
|
3711
4620
|
function isHarnessNodeModulesPath(targetPath, harnessRoot, worktreesDir) {
|
|
3712
|
-
const resolved =
|
|
3713
|
-
const nm = resolved.endsWith(`${
|
|
4621
|
+
const resolved = path24.resolve(targetPath);
|
|
4622
|
+
const nm = resolved.endsWith(`${path24.sep}node_modules`) ? resolved : null;
|
|
3714
4623
|
if (!nm) return "path_outside_harness";
|
|
3715
|
-
const rel =
|
|
3716
|
-
if (rel.startsWith("..") ||
|
|
3717
|
-
const parts = rel.split(
|
|
4624
|
+
const rel = path24.relative(worktreesDir, nm);
|
|
4625
|
+
if (rel.startsWith("..") || path24.isAbsolute(rel)) return "path_outside_harness";
|
|
4626
|
+
const parts = rel.split(path24.sep);
|
|
3718
4627
|
if (parts.length < 3 || parts[parts.length - 1] !== "node_modules") return "path_outside_harness";
|
|
3719
|
-
if (!resolved.startsWith(
|
|
4628
|
+
if (!resolved.startsWith(path24.resolve(harnessRoot))) return "path_outside_harness";
|
|
3720
4629
|
return null;
|
|
3721
4630
|
}
|
|
3722
4631
|
|
|
3723
4632
|
// src/cleanup-scan.ts
|
|
3724
|
-
import { existsSync as
|
|
3725
|
-
import
|
|
4633
|
+
import { existsSync as existsSync17, readdirSync as readdirSync6, statSync as statSync3 } from "node:fs";
|
|
4634
|
+
import path25 from "node:path";
|
|
3726
4635
|
function pathAgeMs(target, now) {
|
|
3727
4636
|
try {
|
|
3728
4637
|
const mtime = statSync3(target).mtimeMs;
|
|
@@ -3732,17 +4641,17 @@ function pathAgeMs(target, now) {
|
|
|
3732
4641
|
}
|
|
3733
4642
|
}
|
|
3734
4643
|
function isPathInside(child, parent) {
|
|
3735
|
-
const rel =
|
|
3736
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
4644
|
+
const rel = path25.relative(parent, child);
|
|
4645
|
+
return rel === "" || !rel.startsWith("..") && !path25.isAbsolute(rel);
|
|
3737
4646
|
}
|
|
3738
4647
|
function scanNodeModulesCandidates(opts) {
|
|
3739
4648
|
const candidates = [];
|
|
3740
4649
|
const seen = /* @__PURE__ */ new Set();
|
|
3741
4650
|
for (const entry of opts.index.values()) {
|
|
3742
4651
|
if (opts.runIdFilter && entry.runId !== opts.runIdFilter) continue;
|
|
3743
|
-
const nm =
|
|
3744
|
-
if (!
|
|
3745
|
-
const resolved =
|
|
4652
|
+
const nm = path25.join(entry.worktreePath, "node_modules");
|
|
4653
|
+
if (!existsSync17(nm)) continue;
|
|
4654
|
+
const resolved = path25.resolve(nm);
|
|
3746
4655
|
if (seen.has(resolved)) continue;
|
|
3747
4656
|
seen.add(resolved);
|
|
3748
4657
|
candidates.push({
|
|
@@ -3755,16 +4664,16 @@ function scanNodeModulesCandidates(opts) {
|
|
|
3755
4664
|
ageMs: pathAgeMs(resolved, opts.now)
|
|
3756
4665
|
});
|
|
3757
4666
|
}
|
|
3758
|
-
if (!opts.includeOrphans || !
|
|
3759
|
-
for (const runEntry of
|
|
4667
|
+
if (!opts.includeOrphans || !existsSync17(opts.worktreesDir)) return candidates;
|
|
4668
|
+
for (const runEntry of readdirSync6(opts.worktreesDir, { withFileTypes: true })) {
|
|
3760
4669
|
if (!runEntry.isDirectory()) continue;
|
|
3761
|
-
const runPath =
|
|
3762
|
-
for (const workerEntry of
|
|
4670
|
+
const runPath = path25.join(opts.worktreesDir, runEntry.name);
|
|
4671
|
+
for (const workerEntry of readdirSync6(runPath, { withFileTypes: true })) {
|
|
3763
4672
|
if (!workerEntry.isDirectory()) continue;
|
|
3764
|
-
const worktreePath =
|
|
3765
|
-
const nm =
|
|
3766
|
-
if (!
|
|
3767
|
-
const resolved =
|
|
4673
|
+
const worktreePath = path25.join(runPath, workerEntry.name);
|
|
4674
|
+
const nm = path25.join(worktreePath, "node_modules");
|
|
4675
|
+
if (!existsSync17(nm)) continue;
|
|
4676
|
+
const resolved = path25.resolve(nm);
|
|
3768
4677
|
if (seen.has(resolved)) continue;
|
|
3769
4678
|
if (!isPathInside(resolved, opts.harnessRoot)) continue;
|
|
3770
4679
|
seen.add(resolved);
|
|
@@ -3787,7 +4696,7 @@ function scanWorktreeCandidates(opts) {
|
|
|
3787
4696
|
for (const entry of opts.index.values()) {
|
|
3788
4697
|
if (opts.runIdFilter && entry.runId !== opts.runIdFilter) continue;
|
|
3789
4698
|
const resolved = entry.worktreePath;
|
|
3790
|
-
if (!
|
|
4699
|
+
if (!existsSync17(resolved)) continue;
|
|
3791
4700
|
if (seen.has(resolved)) continue;
|
|
3792
4701
|
seen.add(resolved);
|
|
3793
4702
|
candidates.push({
|
|
@@ -3804,17 +4713,17 @@ function scanWorktreeCandidates(opts) {
|
|
|
3804
4713
|
}
|
|
3805
4714
|
|
|
3806
4715
|
// src/cleanup-worktree-index.ts
|
|
3807
|
-
import
|
|
4716
|
+
import path26 from "node:path";
|
|
3808
4717
|
function buildWorktreeIndex() {
|
|
3809
4718
|
const index = /* @__PURE__ */ new Map();
|
|
3810
4719
|
for (const run of listRunRecords()) {
|
|
3811
4720
|
for (const name of Object.keys(run.workers || {})) {
|
|
3812
|
-
const workerPath =
|
|
4721
|
+
const workerPath = path26.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
|
|
3813
4722
|
const worker = readJson(workerPath, void 0);
|
|
3814
4723
|
if (!worker?.worktreePath) continue;
|
|
3815
4724
|
const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
|
|
3816
|
-
index.set(
|
|
3817
|
-
worktreePath:
|
|
4725
|
+
index.set(path26.resolve(worker.worktreePath), {
|
|
4726
|
+
worktreePath: path26.resolve(worker.worktreePath),
|
|
3818
4727
|
runId: run.id,
|
|
3819
4728
|
workerName: name,
|
|
3820
4729
|
run,
|
|
@@ -3828,8 +4737,8 @@ function buildWorktreeIndex() {
|
|
|
3828
4737
|
|
|
3829
4738
|
// src/cleanup.ts
|
|
3830
4739
|
function resolveOptions(options = {}) {
|
|
3831
|
-
const harnessRoot = options.harnessRoot ?
|
|
3832
|
-
const { worktreesDir } = options.harnessRoot ? { worktreesDir:
|
|
4740
|
+
const harnessRoot = options.harnessRoot ? path27.resolve(options.harnessRoot) : resolveHarnessRoot();
|
|
4741
|
+
const { worktreesDir } = options.harnessRoot ? { worktreesDir: path27.join(harnessRoot, "worktrees") } : getHarnessPaths();
|
|
3833
4742
|
const execute = options.execute === true;
|
|
3834
4743
|
const nodeModulesAgeMs = options.nodeModulesAgeMs ?? DEFAULT_NODE_MODULES_AGE_MS;
|
|
3835
4744
|
const worktreesAgeMs = options.worktreesAgeMs ?? 0;
|
|
@@ -3873,7 +4782,7 @@ function runHarnessCleanup(options = {}) {
|
|
|
3873
4782
|
actions.push({ ...candidate, executed: false, skipped: true, skipReason: pathSkip });
|
|
3874
4783
|
continue;
|
|
3875
4784
|
}
|
|
3876
|
-
const worktreePath =
|
|
4785
|
+
const worktreePath = path27.resolve(candidate.path, "..");
|
|
3877
4786
|
const indexed = index.get(worktreePath) ?? null;
|
|
3878
4787
|
const guardReason = skipNodeModulesRemoval({
|
|
3879
4788
|
indexed,
|
|
@@ -3889,7 +4798,7 @@ function runHarnessCleanup(options = {}) {
|
|
|
3889
4798
|
actions.push(removeNodeModules(candidate, resolved.execute));
|
|
3890
4799
|
}
|
|
3891
4800
|
for (const candidate of scanWorktreeCandidates(scanOpts)) {
|
|
3892
|
-
const indexed = index.get(
|
|
4801
|
+
const indexed = index.get(path27.resolve(candidate.path)) ?? null;
|
|
3893
4802
|
const guardReason = skipWorktreeRemoval({
|
|
3894
4803
|
indexed,
|
|
3895
4804
|
includeOrphans: resolved.includeOrphans,
|
|
@@ -3958,7 +4867,7 @@ async function completeFinishedWorkers(runId, args) {
|
|
|
3958
4867
|
const outcomes = [];
|
|
3959
4868
|
for (const name of Object.keys(run.workers || {})) {
|
|
3960
4869
|
const worker = readJson(
|
|
3961
|
-
|
|
4870
|
+
path28.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
3962
4871
|
void 0
|
|
3963
4872
|
);
|
|
3964
4873
|
if (!worker?.taskId || worker.localOnly) continue;
|
|
@@ -4009,6 +4918,7 @@ async function runPipelineTick(args) {
|
|
|
4009
4918
|
configuredMaxWorkersOverride: workspacePrefs?.maxConcurrentWorkers
|
|
4010
4919
|
});
|
|
4011
4920
|
const operatorTick = await postOperatorTick(agentOsId, runId, resourceGate, args);
|
|
4921
|
+
const completionAckSync = syncCompletionAcknowledgedFromOperatorTick(runId, operatorTick);
|
|
4012
4922
|
const leaseRenewal = await renewActiveTaskLeases(runId, args);
|
|
4013
4923
|
const completedWorkers = await completeFinishedWorkers(runId, args);
|
|
4014
4924
|
const staleReconcile = reconcileStaleWorkers();
|
|
@@ -4023,14 +4933,14 @@ async function runPipelineTick(args) {
|
|
|
4023
4933
|
const sweep = await sweepRun({ run: runId, agentOsId, pipeline: true, ...args });
|
|
4024
4934
|
let dispatch = null;
|
|
4025
4935
|
if (execute && maxStarts > 0) {
|
|
4026
|
-
dispatch = await
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4936
|
+
dispatch = await runPipelineDispatch(
|
|
4937
|
+
{
|
|
4938
|
+
...args,
|
|
4939
|
+
run: runId,
|
|
4940
|
+
agentOsId
|
|
4941
|
+
},
|
|
4942
|
+
maxStarts
|
|
4943
|
+
);
|
|
4034
4944
|
} else {
|
|
4035
4945
|
dispatch = {
|
|
4036
4946
|
ok: true,
|
|
@@ -4051,6 +4961,7 @@ async function runPipelineTick(args) {
|
|
|
4051
4961
|
staleReconcile,
|
|
4052
4962
|
harnessCleanup,
|
|
4053
4963
|
planProgressSync,
|
|
4964
|
+
completionAckSync,
|
|
4054
4965
|
operatorTick,
|
|
4055
4966
|
sweep,
|
|
4056
4967
|
dispatch,
|
|
@@ -4122,6 +5033,8 @@ async function emitPlanProgress(args) {
|
|
|
4122
5033
|
const url = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/plans/${encodeURIComponent(planId)}/progress-events`;
|
|
4123
5034
|
const cfg = loadUserConfig();
|
|
4124
5035
|
const provider = cfg.workerProvider ? `provider:${cfg.workerProvider}` : void 0;
|
|
5036
|
+
const explicitProposed = args.proposed === true || args.proposed === "true" ? true : args.proposed === false || args.proposed === "false" ? false : void 0;
|
|
5037
|
+
const proposed = explicitProposed ?? (status !== "done" && (roleLane === "implementer" || roleLane === "repair_implementer"));
|
|
4125
5038
|
const body = {
|
|
4126
5039
|
rowKey: args.row ? String(args.row) : void 0,
|
|
4127
5040
|
rowId: args.rowId ? String(args.rowId) : void 0,
|
|
@@ -4132,9 +5045,9 @@ async function emitPlanProgress(args) {
|
|
|
4132
5045
|
note: args.note ? String(args.note) : void 0,
|
|
4133
5046
|
remainingWork: args.remaining ? String(args.remaining) : void 0,
|
|
4134
5047
|
evidence: evidence.length ? evidence : void 0,
|
|
4135
|
-
proposed: args.proposed === true || args.proposed === "true",
|
|
4136
5048
|
executorRef: args.executorRef ? String(args.executorRef) : provider
|
|
4137
5049
|
};
|
|
5050
|
+
if (proposed !== void 0) body.proposed = proposed;
|
|
4138
5051
|
const res = await fetch(url, {
|
|
4139
5052
|
method: "POST",
|
|
4140
5053
|
headers: buildHarnessCallbackHeaders(secret),
|
|
@@ -4188,6 +5101,86 @@ async function verifyPlan(args) {
|
|
|
4188
5101
|
console.log(JSON.stringify(parsed, null, 2));
|
|
4189
5102
|
}
|
|
4190
5103
|
|
|
5104
|
+
// src/plan-persist-cli.ts
|
|
5105
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
5106
|
+
var OPERATIONS = ["create", "add_version", "update_metadata"];
|
|
5107
|
+
var FAILURE_KINDS = [
|
|
5108
|
+
"approval_guard",
|
|
5109
|
+
"auth",
|
|
5110
|
+
"network",
|
|
5111
|
+
"server",
|
|
5112
|
+
"tool_interruption"
|
|
5113
|
+
];
|
|
5114
|
+
function readBodyArg(args) {
|
|
5115
|
+
const bodyFile = args.bodyFile ? String(args.bodyFile) : void 0;
|
|
5116
|
+
if (bodyFile) {
|
|
5117
|
+
return { body: readFileSync8(bodyFile, "utf8"), bodyPathHint: bodyFile };
|
|
5118
|
+
}
|
|
5119
|
+
const inline = args.body ? String(args.body) : void 0;
|
|
5120
|
+
if (inline) return { body: inline };
|
|
5121
|
+
throw new Error("requires --body-file PATH or --body TEXT");
|
|
5122
|
+
}
|
|
5123
|
+
async function runPlanPersist(args) {
|
|
5124
|
+
const operationRaw = required(args.operation ? String(args.operation) : void 0, "operation");
|
|
5125
|
+
if (!OPERATIONS.includes(operationRaw)) {
|
|
5126
|
+
throw new Error(`invalid --operation ${operationRaw}`);
|
|
5127
|
+
}
|
|
5128
|
+
const operation = operationRaw;
|
|
5129
|
+
const cfg = loadUserConfig();
|
|
5130
|
+
const agentOsSlug = required(
|
|
5131
|
+
args.slug ? String(args.slug) : cfg.agentOsSlug,
|
|
5132
|
+
"slug (or agentOsSlug in ~/.kynver/config.json)"
|
|
5133
|
+
);
|
|
5134
|
+
const title = required(args.title ? String(args.title) : void 0, "title");
|
|
5135
|
+
const { body, bodyPathHint } = readBodyArg(args);
|
|
5136
|
+
if (bodyPathHint && isTmpOnlyPath(bodyPathHint)) {
|
|
5137
|
+
console.warn(
|
|
5138
|
+
JSON.stringify({
|
|
5139
|
+
warning: "/tmp-only body path is not durable; AgentOS persistence requires outbox or successful API write",
|
|
5140
|
+
bodyPathHint
|
|
5141
|
+
})
|
|
5142
|
+
);
|
|
5143
|
+
}
|
|
5144
|
+
const input = {
|
|
5145
|
+
operation,
|
|
5146
|
+
agentOsSlug,
|
|
5147
|
+
title,
|
|
5148
|
+
body,
|
|
5149
|
+
bodyPathHint,
|
|
5150
|
+
summary: args.summary ? String(args.summary) : void 0,
|
|
5151
|
+
planId: args.plan ? String(args.plan) : void 0,
|
|
5152
|
+
planSlug: args.planSlug ? String(args.planSlug) : void 0,
|
|
5153
|
+
changeSummary: args.changeSummary ? String(args.changeSummary) : void 0,
|
|
5154
|
+
author: args.author ? String(args.author) : void 0,
|
|
5155
|
+
model: args.model ? String(args.model) : void 0,
|
|
5156
|
+
maxRetries: args.maxRetries ? Number(args.maxRetries) : void 0,
|
|
5157
|
+
immediateFailure: parseImmediateFailure(args)
|
|
5158
|
+
};
|
|
5159
|
+
const result = await persistPlan(input);
|
|
5160
|
+
console.log(JSON.stringify(result, null, 2));
|
|
5161
|
+
if (result.userStatus === "failed and needs action") process.exit(1);
|
|
5162
|
+
}
|
|
5163
|
+
function parseImmediateFailure(args) {
|
|
5164
|
+
const kind = args.failureKind ? String(args.failureKind) : void 0;
|
|
5165
|
+
if (!kind) return void 0;
|
|
5166
|
+
if (!FAILURE_KINDS.includes(kind)) {
|
|
5167
|
+
throw new Error(`invalid --failure-kind ${kind}`);
|
|
5168
|
+
}
|
|
5169
|
+
const message = args.failureMessage ? String(args.failureMessage) : `immediate failure (${kind})`;
|
|
5170
|
+
return { kind, message };
|
|
5171
|
+
}
|
|
5172
|
+
async function runPlanOutboxList() {
|
|
5173
|
+
const items = listOutboxItems();
|
|
5174
|
+
console.log(JSON.stringify({ count: items.length, items }, null, 2));
|
|
5175
|
+
}
|
|
5176
|
+
async function runPlanOutboxDrain(args) {
|
|
5177
|
+
const max = args.max ? Number(args.max) : void 0;
|
|
5178
|
+
const outboxId = args.id ? String(args.id) : void 0;
|
|
5179
|
+
const result = await drainPlanOutbox({ max, outboxId });
|
|
5180
|
+
console.log(JSON.stringify(result, null, 2));
|
|
5181
|
+
if (result.failed > 0) process.exit(1);
|
|
5182
|
+
}
|
|
5183
|
+
|
|
4191
5184
|
// src/cleanup-cli.ts
|
|
4192
5185
|
function runCleanupCli(args) {
|
|
4193
5186
|
const execute = args.execute === true || args.execute === "true";
|
|
@@ -4208,6 +5201,519 @@ function runCleanupCli(args) {
|
|
|
4208
5201
|
}
|
|
4209
5202
|
}
|
|
4210
5203
|
|
|
5204
|
+
// src/monitor/monitor.service.ts
|
|
5205
|
+
import path30 from "node:path";
|
|
5206
|
+
|
|
5207
|
+
// src/monitor/monitor.classify.ts
|
|
5208
|
+
function expectedLeaseOwner(runId) {
|
|
5209
|
+
return `kynver-harness:${runId}`;
|
|
5210
|
+
}
|
|
5211
|
+
function classifyWorkerHealth(input) {
|
|
5212
|
+
const { worker, status, taskLease } = input;
|
|
5213
|
+
const leaseOwner = taskLease?.leaseOwner ?? null;
|
|
5214
|
+
const expectedOwner = expectedLeaseOwner(worker.runId);
|
|
5215
|
+
if (worker.dispatched && taskLease) {
|
|
5216
|
+
if (taskLease.status === "running" && leaseOwner && leaseOwner !== expectedOwner) {
|
|
5217
|
+
return {
|
|
5218
|
+
health: "orphaned",
|
|
5219
|
+
reason: `task lease held by ${leaseOwner}, expected ${expectedOwner}`
|
|
5220
|
+
};
|
|
5221
|
+
}
|
|
5222
|
+
if (taskLease.status === "running" && !status.alive && !status.finalResult) {
|
|
5223
|
+
return {
|
|
5224
|
+
health: "orphaned",
|
|
5225
|
+
reason: "board task running but worker process is not alive"
|
|
5226
|
+
};
|
|
5227
|
+
}
|
|
5228
|
+
}
|
|
5229
|
+
if (worker.status === "running" && !status.alive && !status.finalResult) {
|
|
5230
|
+
return {
|
|
5231
|
+
health: "orphaned",
|
|
5232
|
+
reason: "worker.json still running but process is dead"
|
|
5233
|
+
};
|
|
5234
|
+
}
|
|
5235
|
+
if (status.attention.state === "stale") {
|
|
5236
|
+
return { health: "stale", reason: status.attention.reason };
|
|
5237
|
+
}
|
|
5238
|
+
const hbMs = status.lastHeartbeatAt ? Date.parse(status.lastHeartbeatAt) : NaN;
|
|
5239
|
+
if (status.alive && Number.isFinite(hbMs) && Date.now() - hbMs > STALE_MS) {
|
|
5240
|
+
return {
|
|
5241
|
+
health: "stale",
|
|
5242
|
+
reason: `heartbeat older than ${Math.floor(STALE_MS / 1e3)}s`
|
|
5243
|
+
};
|
|
5244
|
+
}
|
|
5245
|
+
if (status.alive && worker.pid && !isPidAlive(worker.pid)) {
|
|
5246
|
+
return { health: "orphaned", reason: "pid recorded but process is not alive" };
|
|
5247
|
+
}
|
|
5248
|
+
if (taskLease?.status === "running" && !status.alive && status.finalResult) {
|
|
5249
|
+
return {
|
|
5250
|
+
health: "healthy",
|
|
5251
|
+
reason: "finished worker awaiting completion replay"
|
|
5252
|
+
};
|
|
5253
|
+
}
|
|
5254
|
+
return {
|
|
5255
|
+
health: "healthy",
|
|
5256
|
+
reason: status.attention.reason || "worker within expected lifecycle bounds"
|
|
5257
|
+
};
|
|
5258
|
+
}
|
|
5259
|
+
|
|
5260
|
+
// src/monitor/monitor.store.ts
|
|
5261
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync6, readdirSync as readdirSync7, unlinkSync as unlinkSync2 } from "node:fs";
|
|
5262
|
+
import path29 from "node:path";
|
|
5263
|
+
function monitorsDir() {
|
|
5264
|
+
const { harnessRoot } = getHarnessPaths();
|
|
5265
|
+
const dir = path29.join(harnessRoot, "monitors");
|
|
5266
|
+
mkdirSync6(dir, { recursive: true });
|
|
5267
|
+
return dir;
|
|
5268
|
+
}
|
|
5269
|
+
function monitorIdFor(runId, workerName) {
|
|
5270
|
+
return workerName ? `${safeSlug(runId)}--${safeSlug(workerName)}` : safeSlug(runId);
|
|
5271
|
+
}
|
|
5272
|
+
function monitorPath(monitorId) {
|
|
5273
|
+
return path29.join(monitorsDir(), `${monitorId}.json`);
|
|
5274
|
+
}
|
|
5275
|
+
function loadMonitorSession(monitorId) {
|
|
5276
|
+
return readJson(monitorPath(monitorId), void 0);
|
|
5277
|
+
}
|
|
5278
|
+
function saveMonitorSession(session) {
|
|
5279
|
+
writeJson(monitorPath(session.monitorId), session);
|
|
5280
|
+
}
|
|
5281
|
+
function deleteMonitorSession(monitorId) {
|
|
5282
|
+
const file = monitorPath(monitorId);
|
|
5283
|
+
if (!existsSync18(file)) return false;
|
|
5284
|
+
unlinkSync2(file);
|
|
5285
|
+
return true;
|
|
5286
|
+
}
|
|
5287
|
+
function listMonitorSessions() {
|
|
5288
|
+
const dir = monitorsDir();
|
|
5289
|
+
if (!existsSync18(dir)) return [];
|
|
5290
|
+
const entries = [];
|
|
5291
|
+
for (const name of readdirSync7(dir)) {
|
|
5292
|
+
if (!name.endsWith(".json")) continue;
|
|
5293
|
+
const session = readJson(
|
|
5294
|
+
path29.join(dir, name),
|
|
5295
|
+
void 0
|
|
5296
|
+
);
|
|
5297
|
+
if (!session?.monitorId) continue;
|
|
5298
|
+
entries.push({
|
|
5299
|
+
monitorId: session.monitorId,
|
|
5300
|
+
runId: session.runId,
|
|
5301
|
+
workerName: session.workerName,
|
|
5302
|
+
agentOsId: session.agentOsId,
|
|
5303
|
+
pid: session.pid,
|
|
5304
|
+
alive: session.pid ? isPidAlive(session.pid) : false,
|
|
5305
|
+
startedAt: session.startedAt,
|
|
5306
|
+
pollMs: session.pollMs,
|
|
5307
|
+
logPath: session.logPath
|
|
5308
|
+
});
|
|
5309
|
+
}
|
|
5310
|
+
return entries.sort((a, b) => a.startedAt.localeCompare(b.startedAt));
|
|
5311
|
+
}
|
|
5312
|
+
|
|
5313
|
+
// src/monitor/monitor.terminal.ts
|
|
5314
|
+
function assessAutoCompleteEligibility(input) {
|
|
5315
|
+
const { worker, status } = input;
|
|
5316
|
+
const blockers = [];
|
|
5317
|
+
if (worker.localOnly) {
|
|
5318
|
+
blockers.push("local-only worker (no board linkage)");
|
|
5319
|
+
}
|
|
5320
|
+
if (!worker.agentOsId || !worker.taskId) {
|
|
5321
|
+
blockers.push("missing agentOsId/taskId linkage");
|
|
5322
|
+
}
|
|
5323
|
+
if (hasCompletionAck(worker)) {
|
|
5324
|
+
blockers.push("completion already acknowledged");
|
|
5325
|
+
}
|
|
5326
|
+
if (worker.completionBlocker) {
|
|
5327
|
+
blockers.push(worker.completionBlocker);
|
|
5328
|
+
}
|
|
5329
|
+
if (status.heartbeatBlocker && status.alive) {
|
|
5330
|
+
blockers.push(`worker heartbeat blocker: ${status.heartbeatBlocker}`);
|
|
5331
|
+
}
|
|
5332
|
+
if (status.attention.state === "blocked") {
|
|
5333
|
+
blockers.push(status.attention.reason || "worker attention blocked");
|
|
5334
|
+
}
|
|
5335
|
+
if (isLandingBlockedWorkerStatus(status)) {
|
|
5336
|
+
blockers.push(status.attention.reason || "landing gate blocked");
|
|
5337
|
+
}
|
|
5338
|
+
const terminalVerified = isFinishedWorkerStatus(status);
|
|
5339
|
+
let terminalReason;
|
|
5340
|
+
if (terminalVerified) {
|
|
5341
|
+
if (status.finalResult) terminalReason = "final_result";
|
|
5342
|
+
else if (!status.alive) terminalReason = "process_exited";
|
|
5343
|
+
else terminalReason = "terminal_status";
|
|
5344
|
+
} else {
|
|
5345
|
+
blockers.push("worker has not reached a terminal condition");
|
|
5346
|
+
}
|
|
5347
|
+
const eligible = terminalVerified && blockers.length === 0;
|
|
5348
|
+
return {
|
|
5349
|
+
eligible,
|
|
5350
|
+
terminalVerified,
|
|
5351
|
+
terminalReason,
|
|
5352
|
+
blockers
|
|
5353
|
+
};
|
|
5354
|
+
}
|
|
5355
|
+
|
|
5356
|
+
// src/monitor/monitor.task-lease.ts
|
|
5357
|
+
async function fetchTaskLeasesForWorkers(input) {
|
|
5358
|
+
const out = /* @__PURE__ */ new Map();
|
|
5359
|
+
const agentOsId = input.agentOsId?.trim();
|
|
5360
|
+
if (!agentOsId || input.taskIds.length === 0) return out;
|
|
5361
|
+
const base = resolveBaseUrl(input.baseUrl);
|
|
5362
|
+
try {
|
|
5363
|
+
const secret = await resolveCallbackSecretWithMint(input.secret, agentOsId, { baseUrl: base });
|
|
5364
|
+
const url = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/harness/monitor/task-leases`;
|
|
5365
|
+
const res = await postJsonWithCredentialRefresh(
|
|
5366
|
+
url,
|
|
5367
|
+
secret,
|
|
5368
|
+
{ taskIds: [...new Set(input.taskIds)] },
|
|
5369
|
+
{ agentOsId, baseUrl: base }
|
|
5370
|
+
);
|
|
5371
|
+
if (!res.ok || !res.response || typeof res.response !== "object") return out;
|
|
5372
|
+
const rows = res.response.tasks;
|
|
5373
|
+
if (!Array.isArray(rows)) return out;
|
|
5374
|
+
for (const row of rows) {
|
|
5375
|
+
if (row?.taskId) out.set(row.taskId, row);
|
|
5376
|
+
}
|
|
5377
|
+
} catch {
|
|
5378
|
+
}
|
|
5379
|
+
return out;
|
|
5380
|
+
}
|
|
5381
|
+
|
|
5382
|
+
// src/monitor/monitor.service.ts
|
|
5383
|
+
function workerRecord2(runId, name) {
|
|
5384
|
+
return readJson(
|
|
5385
|
+
path30.join(runDirectory(runId), "workers", safeSlug(name), "worker.json"),
|
|
5386
|
+
void 0
|
|
5387
|
+
);
|
|
5388
|
+
}
|
|
5389
|
+
function workerNamesForRun(runId, scope) {
|
|
5390
|
+
const run = loadRun(runId);
|
|
5391
|
+
const names = Object.keys(run.workers || {});
|
|
5392
|
+
if (!scope) return names;
|
|
5393
|
+
const wanted = safeSlug(scope);
|
|
5394
|
+
return names.filter((n) => safeSlug(n) === wanted);
|
|
5395
|
+
}
|
|
5396
|
+
function buildWorkerView(worker, taskLeases) {
|
|
5397
|
+
const run = loadRun(worker.runId);
|
|
5398
|
+
const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
|
|
5399
|
+
const taskLease = worker.taskId ? taskLeases.get(worker.taskId) ?? null : null;
|
|
5400
|
+
const health = classifyWorkerHealth({ worker, status, taskLease });
|
|
5401
|
+
const autoComplete = assessAutoCompleteEligibility({ worker, status });
|
|
5402
|
+
return {
|
|
5403
|
+
runId: worker.runId,
|
|
5404
|
+
worker: worker.name,
|
|
5405
|
+
health: health.health,
|
|
5406
|
+
healthReason: health.reason,
|
|
5407
|
+
workerStatus: status.status,
|
|
5408
|
+
attentionState: status.attention.state,
|
|
5409
|
+
attentionReason: status.attention.reason,
|
|
5410
|
+
alive: status.alive,
|
|
5411
|
+
taskId: worker.taskId,
|
|
5412
|
+
leaseOwner: taskLease?.leaseOwner ?? void 0,
|
|
5413
|
+
taskStatus: taskLease?.status,
|
|
5414
|
+
autoComplete,
|
|
5415
|
+
status
|
|
5416
|
+
};
|
|
5417
|
+
}
|
|
5418
|
+
async function runMonitorTick(args) {
|
|
5419
|
+
const runId = String(args.run || "");
|
|
5420
|
+
required(runId, "--run");
|
|
5421
|
+
const scope = args.name ? String(args.name) : void 0;
|
|
5422
|
+
const agentOsId = args.agentOsId ? String(args.agentOsId) : void 0;
|
|
5423
|
+
const run = loadRun(runId);
|
|
5424
|
+
const names = workerNamesForRun(runId, scope);
|
|
5425
|
+
const workers = [];
|
|
5426
|
+
for (const name of names) {
|
|
5427
|
+
const worker = workerRecord2(runId, name);
|
|
5428
|
+
if (worker) workers.push(worker);
|
|
5429
|
+
}
|
|
5430
|
+
const resolvedAgentOsId = agentOsId || workers.map((w) => w.agentOsId).find((id) => typeof id === "string" && id.trim()) || void 0;
|
|
5431
|
+
const taskIds = workers.map((w) => w.taskId).filter((id) => Boolean(id));
|
|
5432
|
+
const taskLeases = await fetchTaskLeasesForWorkers({
|
|
5433
|
+
agentOsId: resolvedAgentOsId,
|
|
5434
|
+
taskIds,
|
|
5435
|
+
baseUrl: args.baseUrl ? String(args.baseUrl) : void 0,
|
|
5436
|
+
secret: args.secret ? String(args.secret) : void 0
|
|
5437
|
+
});
|
|
5438
|
+
const views = workers.map((w) => buildWorkerView(w, taskLeases));
|
|
5439
|
+
let leaseRenewal;
|
|
5440
|
+
if (resolvedAgentOsId && args.renewLeases !== false && args.renewLeases !== "false") {
|
|
5441
|
+
leaseRenewal = await renewActiveTaskLeases(runId, {
|
|
5442
|
+
...args,
|
|
5443
|
+
agentOsId: resolvedAgentOsId
|
|
5444
|
+
});
|
|
5445
|
+
}
|
|
5446
|
+
const autoCompleted = [];
|
|
5447
|
+
const shouldAutoComplete = args.autoComplete === true || args.autoComplete === "true";
|
|
5448
|
+
if (shouldAutoComplete) {
|
|
5449
|
+
for (const view of views) {
|
|
5450
|
+
if (!view.autoComplete.eligible) {
|
|
5451
|
+
autoCompleted.push({
|
|
5452
|
+
worker: view.worker,
|
|
5453
|
+
outcome: "skipped",
|
|
5454
|
+
ok: false,
|
|
5455
|
+
reason: view.autoComplete.blockers.join("; ") || "not eligible"
|
|
5456
|
+
});
|
|
5457
|
+
continue;
|
|
5458
|
+
}
|
|
5459
|
+
const outcome = await autoCompleteWorker({
|
|
5460
|
+
run: runId,
|
|
5461
|
+
name: view.worker,
|
|
5462
|
+
...resolvedAgentOsId ? { agentOsId: resolvedAgentOsId } : {},
|
|
5463
|
+
...args.baseUrl ? { baseUrl: String(args.baseUrl) } : {},
|
|
5464
|
+
...args.secret ? { secret: String(args.secret) } : {}
|
|
5465
|
+
});
|
|
5466
|
+
autoCompleted.push({
|
|
5467
|
+
worker: view.worker,
|
|
5468
|
+
outcome: outcome.outcome,
|
|
5469
|
+
ok: outcome.outcome === "completed",
|
|
5470
|
+
reason: outcome.reason
|
|
5471
|
+
});
|
|
5472
|
+
}
|
|
5473
|
+
}
|
|
5474
|
+
return {
|
|
5475
|
+
runId,
|
|
5476
|
+
agentOsId: resolvedAgentOsId,
|
|
5477
|
+
workers: views,
|
|
5478
|
+
leaseRenewal,
|
|
5479
|
+
autoCompleted
|
|
5480
|
+
};
|
|
5481
|
+
}
|
|
5482
|
+
function getMonitorStatus(args) {
|
|
5483
|
+
const runId = String(args.run || "");
|
|
5484
|
+
required(runId, "--run");
|
|
5485
|
+
const scope = args.name ? String(args.name) : void 0;
|
|
5486
|
+
const names = workerNamesForRun(runId, scope);
|
|
5487
|
+
const workers = [];
|
|
5488
|
+
for (const name of names) {
|
|
5489
|
+
const worker = workerRecord2(runId, name);
|
|
5490
|
+
if (!worker) continue;
|
|
5491
|
+
workers.push(buildWorkerView(worker, /* @__PURE__ */ new Map()));
|
|
5492
|
+
}
|
|
5493
|
+
return { runId, workers, autoCompleted: [] };
|
|
5494
|
+
}
|
|
5495
|
+
function listMonitors() {
|
|
5496
|
+
return listMonitorSessions();
|
|
5497
|
+
}
|
|
5498
|
+
function stopMonitor(args) {
|
|
5499
|
+
const runId = String(args.run || "");
|
|
5500
|
+
required(runId, "--run");
|
|
5501
|
+
const monitorId = monitorIdFor(runId, args.name ? String(args.name) : void 0);
|
|
5502
|
+
const session = loadMonitorSession(monitorId);
|
|
5503
|
+
if (!session) {
|
|
5504
|
+
return { monitorId, stopped: false };
|
|
5505
|
+
}
|
|
5506
|
+
if (session.pid && isPidAlive(session.pid)) {
|
|
5507
|
+
try {
|
|
5508
|
+
process.kill(session.pid, "SIGTERM");
|
|
5509
|
+
} catch {
|
|
5510
|
+
}
|
|
5511
|
+
}
|
|
5512
|
+
session.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5513
|
+
saveMonitorSession(session);
|
|
5514
|
+
deleteMonitorSession(monitorId);
|
|
5515
|
+
return { monitorId, stopped: true, pid: session.pid };
|
|
5516
|
+
}
|
|
5517
|
+
async function monitorAutoCompleteCli(args) {
|
|
5518
|
+
const runId = String(args.run || "");
|
|
5519
|
+
const name = String(args.name || "");
|
|
5520
|
+
required(runId, "--run");
|
|
5521
|
+
required(name, "--name");
|
|
5522
|
+
const worker = loadWorker(runId, name);
|
|
5523
|
+
const run = loadRun(runId);
|
|
5524
|
+
const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
|
|
5525
|
+
const assessment = assessAutoCompleteEligibility({ worker, status });
|
|
5526
|
+
if (!assessment.eligible) {
|
|
5527
|
+
console.log(
|
|
5528
|
+
JSON.stringify(
|
|
5529
|
+
{
|
|
5530
|
+
runId,
|
|
5531
|
+
worker: name,
|
|
5532
|
+
outcome: "blocked",
|
|
5533
|
+
blockers: assessment.blockers,
|
|
5534
|
+
terminalVerified: assessment.terminalVerified
|
|
5535
|
+
},
|
|
5536
|
+
null,
|
|
5537
|
+
2
|
|
5538
|
+
)
|
|
5539
|
+
);
|
|
5540
|
+
process.exitCode = 1;
|
|
5541
|
+
return;
|
|
5542
|
+
}
|
|
5543
|
+
const outcome = await autoCompleteWorker({
|
|
5544
|
+
...args,
|
|
5545
|
+
run: runId,
|
|
5546
|
+
name
|
|
5547
|
+
});
|
|
5548
|
+
console.log(JSON.stringify(outcome, null, 2));
|
|
5549
|
+
if (outcome.outcome !== "completed" && outcome.outcome !== "blocked") {
|
|
5550
|
+
process.exitCode = 1;
|
|
5551
|
+
}
|
|
5552
|
+
}
|
|
5553
|
+
|
|
5554
|
+
// src/monitor/monitor-loop.ts
|
|
5555
|
+
var DEFAULT_POLL_MS2 = 5e3;
|
|
5556
|
+
var DEFAULT_MAX_TOTAL_MS2 = 6 * 60 * 60 * 1e3;
|
|
5557
|
+
async function runMonitorLoop(args) {
|
|
5558
|
+
const monitorId = String(args.monitorId || "");
|
|
5559
|
+
const pollMs = Number(args.pollMs) > 0 ? Math.floor(Number(args.pollMs)) : DEFAULT_POLL_MS2;
|
|
5560
|
+
const maxTotalMs = Number(args.maxTotalMs) > 0 ? Math.floor(Number(args.maxTotalMs)) : DEFAULT_MAX_TOTAL_MS2;
|
|
5561
|
+
const startMs = Date.now();
|
|
5562
|
+
while (Date.now() - startMs <= maxTotalMs) {
|
|
5563
|
+
const session = monitorId ? loadMonitorSession(monitorId) : void 0;
|
|
5564
|
+
if (session?.stoppedAt) break;
|
|
5565
|
+
const tick = await runMonitorTick({
|
|
5566
|
+
...args,
|
|
5567
|
+
autoComplete: args.autoComplete ?? true,
|
|
5568
|
+
renewLeases: args.renewLeases ?? true
|
|
5569
|
+
});
|
|
5570
|
+
console.log(JSON.stringify({ monitorId, phase: "tick", ...tick }));
|
|
5571
|
+
const allTerminal = tick.workers.length > 0 && tick.workers.every(
|
|
5572
|
+
(w) => w.autoComplete.terminalVerified && (w.autoComplete.eligible || w.autoComplete.blockers.some((b) => b.includes("already acknowledged")))
|
|
5573
|
+
);
|
|
5574
|
+
if (allTerminal && tick.autoCompleted.every((a) => a.ok || a.outcome === "skipped")) {
|
|
5575
|
+
if (monitorId && session) {
|
|
5576
|
+
session.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5577
|
+
saveMonitorSession(session);
|
|
5578
|
+
}
|
|
5579
|
+
break;
|
|
5580
|
+
}
|
|
5581
|
+
sleepMs(pollMs);
|
|
5582
|
+
}
|
|
5583
|
+
}
|
|
5584
|
+
|
|
5585
|
+
// src/monitor/monitor-spawn.ts
|
|
5586
|
+
import { spawn as spawn4 } from "node:child_process";
|
|
5587
|
+
import { closeSync as closeSync4, existsSync as existsSync19, openSync as openSync4 } from "node:fs";
|
|
5588
|
+
import path31 from "node:path";
|
|
5589
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
5590
|
+
function resolveDefaultCliPath2() {
|
|
5591
|
+
return path31.join(fileURLToPath3(new URL(".", import.meta.url)), "..", "cli.js");
|
|
5592
|
+
}
|
|
5593
|
+
function spawnMonitorSidecar(opts) {
|
|
5594
|
+
const cliPath = opts.cliPath ?? resolveDefaultCliPath2();
|
|
5595
|
+
if (!existsSync19(cliPath)) return void 0;
|
|
5596
|
+
const monitorId = monitorIdFor(opts.runId, opts.workerName);
|
|
5597
|
+
const { harnessRoot } = getHarnessPaths();
|
|
5598
|
+
const logPath = path31.join(harnessRoot, "monitors", `${monitorId}.log`);
|
|
5599
|
+
let logFd;
|
|
5600
|
+
try {
|
|
5601
|
+
logFd = openSync4(logPath, "a");
|
|
5602
|
+
} catch {
|
|
5603
|
+
logFd = void 0;
|
|
5604
|
+
}
|
|
5605
|
+
const nodeExecutable = opts.nodeExecutable ?? process.execPath;
|
|
5606
|
+
const pollMs = opts.pollMs ?? 5e3;
|
|
5607
|
+
const args = [
|
|
5608
|
+
cliPath,
|
|
5609
|
+
"monitor",
|
|
5610
|
+
"run-loop",
|
|
5611
|
+
"--run",
|
|
5612
|
+
opts.runId,
|
|
5613
|
+
"--monitor-id",
|
|
5614
|
+
monitorId,
|
|
5615
|
+
"--poll-ms",
|
|
5616
|
+
String(pollMs),
|
|
5617
|
+
"--auto-complete",
|
|
5618
|
+
"true",
|
|
5619
|
+
"--renew-leases",
|
|
5620
|
+
"true"
|
|
5621
|
+
];
|
|
5622
|
+
if (opts.workerName) args.push("--name", opts.workerName);
|
|
5623
|
+
if (opts.agentOsId) args.push("--agent-os-id", opts.agentOsId);
|
|
5624
|
+
if (opts.baseUrl) args.push("--base-url", opts.baseUrl);
|
|
5625
|
+
if (opts.secret) args.push("--secret", opts.secret);
|
|
5626
|
+
const stdio = [
|
|
5627
|
+
"ignore",
|
|
5628
|
+
logFd ?? "ignore",
|
|
5629
|
+
logFd ?? "ignore"
|
|
5630
|
+
];
|
|
5631
|
+
try {
|
|
5632
|
+
const child = spawn4(
|
|
5633
|
+
nodeExecutable,
|
|
5634
|
+
args,
|
|
5635
|
+
hiddenSpawnOptions({
|
|
5636
|
+
detached: true,
|
|
5637
|
+
stdio,
|
|
5638
|
+
env: process.env
|
|
5639
|
+
})
|
|
5640
|
+
);
|
|
5641
|
+
if (logFd !== void 0) closeSync4(logFd);
|
|
5642
|
+
child.unref();
|
|
5643
|
+
const session = {
|
|
5644
|
+
monitorId,
|
|
5645
|
+
runId: opts.runId,
|
|
5646
|
+
workerName: opts.workerName,
|
|
5647
|
+
agentOsId: opts.agentOsId,
|
|
5648
|
+
pid: child.pid,
|
|
5649
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5650
|
+
pollMs,
|
|
5651
|
+
logPath
|
|
5652
|
+
};
|
|
5653
|
+
saveMonitorSession(session);
|
|
5654
|
+
return { monitorId, pid: child.pid, logPath, session };
|
|
5655
|
+
} catch {
|
|
5656
|
+
if (logFd !== void 0) {
|
|
5657
|
+
try {
|
|
5658
|
+
closeSync4(logFd);
|
|
5659
|
+
} catch {
|
|
5660
|
+
}
|
|
5661
|
+
}
|
|
5662
|
+
return void 0;
|
|
5663
|
+
}
|
|
5664
|
+
}
|
|
5665
|
+
|
|
5666
|
+
// src/monitor/monitor-cli.ts
|
|
5667
|
+
async function startMonitorCli(args) {
|
|
5668
|
+
const runId = String(args.run || "");
|
|
5669
|
+
required(runId, "--run");
|
|
5670
|
+
const workerName = args.name ? String(args.name) : void 0;
|
|
5671
|
+
const monitorId = monitorIdFor(runId, workerName);
|
|
5672
|
+
const existing = loadMonitorSession(monitorId);
|
|
5673
|
+
if (existing?.pid && !existing.stoppedAt) {
|
|
5674
|
+
return { monitorId, session: existing, spawned: false, pid: existing.pid };
|
|
5675
|
+
}
|
|
5676
|
+
const spawned = spawnMonitorSidecar({
|
|
5677
|
+
runId,
|
|
5678
|
+
workerName,
|
|
5679
|
+
agentOsId: args.agentOsId ? String(args.agentOsId) : void 0,
|
|
5680
|
+
pollMs: Number(args.pollMs) > 0 ? Math.floor(Number(args.pollMs)) : void 0,
|
|
5681
|
+
baseUrl: args.baseUrl ? String(args.baseUrl) : void 0,
|
|
5682
|
+
secret: args.secret ? String(args.secret) : void 0
|
|
5683
|
+
});
|
|
5684
|
+
if (!spawned) {
|
|
5685
|
+
throw new Error("failed to spawn monitor sidecar (cli.js missing or spawn error)");
|
|
5686
|
+
}
|
|
5687
|
+
return {
|
|
5688
|
+
monitorId,
|
|
5689
|
+
session: spawned.session,
|
|
5690
|
+
spawned: true,
|
|
5691
|
+
pid: spawned.pid
|
|
5692
|
+
};
|
|
5693
|
+
}
|
|
5694
|
+
async function monitorStatusCli(args) {
|
|
5695
|
+
const runId = String(args.run || "");
|
|
5696
|
+
if (runId) {
|
|
5697
|
+
const tick = args.tick === true || args.tick === "true" ? await runMonitorTick({ ...args, autoComplete: false }) : getMonitorStatus(args);
|
|
5698
|
+
console.log(JSON.stringify(tick, null, 2));
|
|
5699
|
+
return;
|
|
5700
|
+
}
|
|
5701
|
+
console.log(JSON.stringify({ monitors: listMonitors() }, null, 2));
|
|
5702
|
+
}
|
|
5703
|
+
function monitorStopCli(args) {
|
|
5704
|
+
console.log(JSON.stringify(stopMonitor(args), null, 2));
|
|
5705
|
+
}
|
|
5706
|
+
function monitorListCli() {
|
|
5707
|
+
console.log(JSON.stringify({ monitors: listMonitors() }, null, 2));
|
|
5708
|
+
}
|
|
5709
|
+
async function monitorRunLoopCli(args) {
|
|
5710
|
+
await runMonitorLoop(args);
|
|
5711
|
+
}
|
|
5712
|
+
async function monitorTickCli(args) {
|
|
5713
|
+
const tick = await runMonitorTick(args);
|
|
5714
|
+
console.log(JSON.stringify(tick, null, 2));
|
|
5715
|
+
}
|
|
5716
|
+
|
|
4211
5717
|
// src/cli.ts
|
|
4212
5718
|
function isHelpFlag(arg) {
|
|
4213
5719
|
return arg === "help" || arg === "--help" || arg === "-h";
|
|
@@ -4239,17 +5745,28 @@ function usage(code = 0) {
|
|
|
4239
5745
|
" kynver worker auto-complete --run RUN_ID --name worker [--agent-os-id AOS_ID] [--poll-ms 5000] [--max-total-ms 21600000] [--complete-attempts 3] [--complete-backoff-ms 5000] [--base-url URL] [--secret SECRET]",
|
|
4240
5746
|
" kynver plan progress --plan PLAN_ID --row ROW_KEY --role ROLE --status STATUS [--task TASK_ID] [--note NOTE] [--evidence type:value] [--agent-os-id AOS_ID]",
|
|
4241
5747
|
" kynver plan verify --plan PLAN_ID [--worktree PATH] [--task TASK_ID] [--human-override]",
|
|
4242
|
-
" kynver
|
|
5748
|
+
" kynver plan persist --operation create|add_version|update_metadata --title TITLE (--body-file PATH | --body TEXT) [--slug SLUG] [--plan PLAN_ID] [--summary TEXT] [--failure-kind approval_guard|auth|network|server|tool_interruption]",
|
|
5749
|
+
" kynver plan outbox list",
|
|
5750
|
+
" kynver plan outbox drain [--max N] [--id OUTBOX_ID]",
|
|
5751
|
+
" kynver cleanup [--execute] [--node-modules-age-ms MS] [--worktrees-age-ms MS] [--harness-root PATH] [--include-orphans]",
|
|
5752
|
+
" kynver monitor start --run RUN_ID [--name worker] [--agent-os-id AOS_ID] [--poll-ms MS]",
|
|
5753
|
+
" kynver monitor status [--run RUN_ID] [--name worker] [--tick]",
|
|
5754
|
+
" kynver monitor stop --run RUN_ID [--name worker]",
|
|
5755
|
+
" kynver monitor list",
|
|
5756
|
+
" kynver monitor tick --run RUN_ID [--name worker] [--agent-os-id AOS_ID] [--auto-complete] [--renew-leases]",
|
|
5757
|
+
" kynver monitor auto-complete --run RUN_ID --name worker [--agent-os-id AOS_ID] [--base-url URL] [--secret SECRET]",
|
|
5758
|
+
" kynver monitor run-loop --run RUN_ID --monitor-id ID [--name worker] [--agent-os-id AOS_ID] [--poll-ms MS] [--auto-complete] [--renew-leases]"
|
|
4243
5759
|
].join("\n")
|
|
4244
5760
|
);
|
|
4245
5761
|
process.exit(code);
|
|
4246
5762
|
}
|
|
4247
5763
|
async function main(argv = process.argv.slice(2)) {
|
|
5764
|
+
if (handleCliVersionFlag(argv, import.meta.url, "kynver")) return;
|
|
4248
5765
|
if (argv.length === 0 || isHelpFlag(argv[0])) return usage(0);
|
|
4249
5766
|
const scope = argv.shift();
|
|
4250
5767
|
let action;
|
|
4251
5768
|
let rest;
|
|
4252
|
-
if (scope === "run" || scope === "worker" || scope === "plan" || scope === "runner") {
|
|
5769
|
+
if (scope === "run" || scope === "worker" || scope === "plan" || scope === "runner" || scope === "monitor") {
|
|
4253
5770
|
action = argv.shift();
|
|
4254
5771
|
rest = argv;
|
|
4255
5772
|
} else {
|
|
@@ -4258,14 +5775,21 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
4258
5775
|
if (action && isHelpFlag(action) || rest.some(isHelpFlag)) return usage(0);
|
|
4259
5776
|
const args = parseArgs(rest);
|
|
4260
5777
|
const { runsDir, worktreesDir } = getPaths();
|
|
4261
|
-
|
|
4262
|
-
|
|
5778
|
+
mkdirSync7(runsDir, { recursive: true });
|
|
5779
|
+
mkdirSync7(worktreesDir, { recursive: true });
|
|
4263
5780
|
if (scope === "login") return void await runLogin(args);
|
|
4264
5781
|
if (scope === "runner" && action === "credential") return void await mintRunnerCredential(args);
|
|
4265
5782
|
if (scope === "setup") return void await runSetup(args);
|
|
4266
5783
|
if (scope === "daemon") return void await runDaemon(args);
|
|
4267
5784
|
if (scope === "plan" && action === "progress") return void await emitPlanProgress(args);
|
|
4268
5785
|
if (scope === "plan" && action === "verify") return void await verifyPlan(args);
|
|
5786
|
+
if (scope === "plan" && action === "persist") return void await runPlanPersist(args);
|
|
5787
|
+
if (scope === "plan" && action === "outbox") {
|
|
5788
|
+
const outboxAction = rest.shift();
|
|
5789
|
+
if (outboxAction === "list") return void await runPlanOutboxList();
|
|
5790
|
+
if (outboxAction === "drain") return void await runPlanOutboxDrain(parseArgs(rest));
|
|
5791
|
+
unknownCommand("plan", `outbox ${outboxAction ?? ""}`.trim());
|
|
5792
|
+
}
|
|
4269
5793
|
if (scope === "cleanup") return runCleanupCli(args);
|
|
4270
5794
|
if (scope === "run" && action === "create") return createRun(args);
|
|
4271
5795
|
if (scope === "run" && action === "list") return listRuns();
|
|
@@ -4278,9 +5802,20 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
4278
5802
|
if (scope === "worker" && action === "stop") return stopWorker(args);
|
|
4279
5803
|
if (scope === "worker" && action === "complete") return void await completeWorker(args);
|
|
4280
5804
|
if (scope === "worker" && action === "auto-complete") return void await autoCompleteWorkerCli(args);
|
|
5805
|
+
if (scope === "monitor" && action === "start") {
|
|
5806
|
+
const result = await startMonitorCli(args);
|
|
5807
|
+
console.log(JSON.stringify(result, null, 2));
|
|
5808
|
+
return;
|
|
5809
|
+
}
|
|
5810
|
+
if (scope === "monitor" && action === "status") return void await monitorStatusCli(args);
|
|
5811
|
+
if (scope === "monitor" && action === "stop") return monitorStopCli(args);
|
|
5812
|
+
if (scope === "monitor" && action === "list") return monitorListCli();
|
|
5813
|
+
if (scope === "monitor" && action === "tick") return void await monitorTickCli(args);
|
|
5814
|
+
if (scope === "monitor" && action === "auto-complete") return void await monitorAutoCompleteCli(args);
|
|
5815
|
+
if (scope === "monitor" && action === "run-loop") return void await monitorRunLoopCli(args);
|
|
4281
5816
|
unknownCommand(scope, action);
|
|
4282
5817
|
}
|
|
4283
|
-
var isCliEntry = process.argv[1] && realpathSync.native(process.argv[1]) === realpathSync.native(
|
|
5818
|
+
var isCliEntry = process.argv[1] && realpathSync.native(process.argv[1]) === realpathSync.native(fileURLToPath4(import.meta.url));
|
|
4284
5819
|
if (isCliEntry) {
|
|
4285
5820
|
void main().catch((error) => {
|
|
4286
5821
|
console.error(error);
|
|
@@ -4289,25 +5824,37 @@ if (isCliEntry) {
|
|
|
4289
5824
|
}
|
|
4290
5825
|
export {
|
|
4291
5826
|
DEFAULT_DISPATCH_LEASE_MS,
|
|
5827
|
+
PACKAGE_VERSION,
|
|
5828
|
+
assessAutoCompleteEligibility,
|
|
4292
5829
|
assessExitedWorkerSalvage,
|
|
4293
5830
|
assessPrHandoffRequirement,
|
|
4294
5831
|
assessWorkerLanding,
|
|
5832
|
+
assessWorkerLandingContract,
|
|
4295
5833
|
autoCompleteWorker,
|
|
4296
5834
|
autoCompleteWorkerCli,
|
|
4297
5835
|
buildDispatchTaskText,
|
|
4298
5836
|
buildPrompt,
|
|
5837
|
+
classifyWorkerHealth,
|
|
4299
5838
|
completeWorker,
|
|
4300
5839
|
computeAttention,
|
|
4301
5840
|
computeWorkerStatus,
|
|
4302
5841
|
createRun,
|
|
4303
5842
|
deriveRunStatus,
|
|
4304
5843
|
dispatchRun,
|
|
5844
|
+
drainPlanOutbox,
|
|
4305
5845
|
ensurePrReadyHandoff,
|
|
5846
|
+
extractPlanOutboxFromTask,
|
|
4306
5847
|
extractPrUrlFromText,
|
|
5848
|
+
formatPlanOutboxHandoffBlock,
|
|
4307
5849
|
getHarnessPaths,
|
|
5850
|
+
getMonitorStatus,
|
|
5851
|
+
hashPlanBody,
|
|
4308
5852
|
isFinishedWorkerStatus,
|
|
4309
5853
|
isLandingBlockedWorkerStatus,
|
|
4310
5854
|
isTerminalHeartbeatPhase,
|
|
5855
|
+
landingContractAttentionReason,
|
|
5856
|
+
listMonitors,
|
|
5857
|
+
listOutboxItems,
|
|
4311
5858
|
listRuns,
|
|
4312
5859
|
loadUserConfig,
|
|
4313
5860
|
main,
|
|
@@ -4317,6 +5864,7 @@ export {
|
|
|
4317
5864
|
parseClaudeStream,
|
|
4318
5865
|
parseHarnessStream,
|
|
4319
5866
|
parseHeartbeat,
|
|
5867
|
+
persistPlan,
|
|
4320
5868
|
postJson,
|
|
4321
5869
|
preflightCursorModel,
|
|
4322
5870
|
redactHarness,
|
|
@@ -4325,9 +5873,11 @@ export {
|
|
|
4325
5873
|
resolveCallbackSecretWithMint,
|
|
4326
5874
|
resolveHarnessRoot,
|
|
4327
5875
|
runDaemon,
|
|
5876
|
+
runMonitorTick,
|
|
4328
5877
|
runStatus,
|
|
4329
5878
|
saveUserConfig,
|
|
4330
5879
|
spawnCompletionSidecar,
|
|
5880
|
+
spawnMonitorSidecar,
|
|
4331
5881
|
spawnWorkerProcess,
|
|
4332
5882
|
startWorker,
|
|
4333
5883
|
stopWorker,
|