@kynver-app/runtime 0.1.34 → 0.1.38
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 +1650 -114
- package/dist/cli.js.map +4 -4
- package/dist/index.js +1704 -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,580 @@ 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
|
+
if (filePath.startsWith("/tmp/") || filePath.startsWith("/var/folders/")) return true;
|
|
3405
|
+
const resolved = path13.resolve(filePath);
|
|
3406
|
+
return resolved.startsWith("/tmp/") || resolved.startsWith(path13.join("/var", "folders"));
|
|
3407
|
+
}
|
|
3408
|
+
|
|
3409
|
+
// src/plan-persist/outbox-store.ts
|
|
3410
|
+
import {
|
|
3411
|
+
existsSync as existsSync13,
|
|
3412
|
+
readFileSync as readFileSync7,
|
|
3413
|
+
renameSync,
|
|
3414
|
+
readdirSync as readdirSync4,
|
|
3415
|
+
writeFileSync as writeFileSync3,
|
|
3416
|
+
unlinkSync
|
|
3417
|
+
} from "node:fs";
|
|
3418
|
+
import path14 from "node:path";
|
|
3419
|
+
import { randomUUID } from "node:crypto";
|
|
3420
|
+
var DEFAULT_MAX_RETRIES = 12;
|
|
3421
|
+
function listOutboxItems() {
|
|
3422
|
+
const { outboxDir } = ensurePlanOutboxDirs();
|
|
3423
|
+
const files = readdirSync4(outboxDir).filter((f) => f.endsWith(".json"));
|
|
3424
|
+
const items = [];
|
|
3425
|
+
for (const file of files) {
|
|
3426
|
+
const item = readOutboxItem(path14.join(outboxDir, file));
|
|
3427
|
+
if (item && item.queueStatus === "queued") items.push(item);
|
|
3428
|
+
}
|
|
3429
|
+
return items.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
3430
|
+
}
|
|
3431
|
+
function findOutboxByIdempotencyKey(key) {
|
|
3432
|
+
for (const item of listOutboxItems()) {
|
|
3433
|
+
if (item.idempotencyKey === key) return item;
|
|
3434
|
+
}
|
|
3435
|
+
return null;
|
|
3436
|
+
}
|
|
3437
|
+
function readOutboxItem(jsonPath) {
|
|
3438
|
+
if (!existsSync13(jsonPath)) return null;
|
|
3439
|
+
try {
|
|
3440
|
+
return JSON.parse(readFileSync7(jsonPath, "utf8"));
|
|
3441
|
+
} catch {
|
|
3442
|
+
return null;
|
|
3443
|
+
}
|
|
3444
|
+
}
|
|
3445
|
+
function readOutboxBody(item) {
|
|
3446
|
+
const { outboxDir } = ensurePlanOutboxDirs();
|
|
3447
|
+
const bodyFile = path14.join(outboxDir, item.bodyPath);
|
|
3448
|
+
return readFileSync7(bodyFile, "utf8");
|
|
3449
|
+
}
|
|
3450
|
+
function writeOutboxItem(input, opts) {
|
|
3451
|
+
const { outboxDir } = ensurePlanOutboxDirs();
|
|
3452
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3453
|
+
const id = opts.existing?.id ?? randomUUID();
|
|
3454
|
+
const bodyPath = opts.existing?.bodyPath ?? `${id}.body.md`;
|
|
3455
|
+
const jsonPath = path14.join(outboxDir, `${id}.json`);
|
|
3456
|
+
const bodyFile = path14.join(outboxDir, bodyPath);
|
|
3457
|
+
if (!opts.existing) {
|
|
3458
|
+
writeFileSync3(bodyFile, input.body, "utf8");
|
|
3459
|
+
}
|
|
3460
|
+
const item = {
|
|
3461
|
+
id,
|
|
3462
|
+
idempotencyKey: buildPlanPersistIdempotencyKey(input),
|
|
3463
|
+
operation: input.operation,
|
|
3464
|
+
agentOsSlug: input.agentOsSlug,
|
|
3465
|
+
planId: input.planId ?? opts.existing?.planId ?? null,
|
|
3466
|
+
planSlug: input.planSlug ?? opts.existing?.planSlug ?? null,
|
|
3467
|
+
title: input.title,
|
|
3468
|
+
summary: input.summary ?? null,
|
|
3469
|
+
bodyPath,
|
|
3470
|
+
bodyHash: hashPlanBody(input.body),
|
|
3471
|
+
author: input.author ?? null,
|
|
3472
|
+
model: input.model ?? null,
|
|
3473
|
+
sourceRefs: input.sourceRefs ?? null,
|
|
3474
|
+
changeSummary: input.changeSummary ?? null,
|
|
3475
|
+
markCurrent: input.markCurrent ?? true,
|
|
3476
|
+
createdAt: opts.existing?.createdAt ?? now,
|
|
3477
|
+
updatedAt: now,
|
|
3478
|
+
retryCount: (opts.existing?.retryCount ?? 0) + (opts.existing ? 1 : 0),
|
|
3479
|
+
maxRetries: input.maxRetries ?? opts.existing?.maxRetries ?? DEFAULT_MAX_RETRIES,
|
|
3480
|
+
lastError: opts.lastError,
|
|
3481
|
+
lastFailureKind: opts.lastFailureKind,
|
|
3482
|
+
queueStatus: "queued",
|
|
3483
|
+
userStatus: "queued for retry",
|
|
3484
|
+
readbackEvidence: null
|
|
3485
|
+
};
|
|
3486
|
+
writeFileSync3(jsonPath, `${JSON.stringify(item, null, 2)}
|
|
3487
|
+
`, { mode: 384 });
|
|
3488
|
+
return item;
|
|
3489
|
+
}
|
|
3490
|
+
function saveOutboxItem(item) {
|
|
3491
|
+
const { outboxDir } = ensurePlanOutboxDirs();
|
|
3492
|
+
const jsonPath = path14.join(outboxDir, `${item.id}.json`);
|
|
3493
|
+
writeFileSync3(jsonPath, `${JSON.stringify(item, null, 2)}
|
|
3494
|
+
`, { mode: 384 });
|
|
3495
|
+
}
|
|
3496
|
+
function archiveOutboxItem(item) {
|
|
3497
|
+
const { outboxDir, archiveDir } = ensurePlanOutboxDirs();
|
|
3498
|
+
const jsonSrc = path14.join(outboxDir, `${item.id}.json`);
|
|
3499
|
+
const bodySrc = path14.join(outboxDir, item.bodyPath);
|
|
3500
|
+
const jsonDst = path14.join(archiveDir, `${item.id}.json`);
|
|
3501
|
+
const bodyDst = path14.join(archiveDir, item.bodyPath);
|
|
3502
|
+
if (existsSync13(jsonSrc)) renameSync(jsonSrc, jsonDst);
|
|
3503
|
+
if (existsSync13(bodySrc)) renameSync(bodySrc, bodyDst);
|
|
3504
|
+
}
|
|
3505
|
+
function outboxItemPaths(item) {
|
|
3506
|
+
const { outboxDir } = ensurePlanOutboxDirs();
|
|
3507
|
+
return {
|
|
3508
|
+
jsonPath: path14.join(outboxDir, `${item.id}.json`),
|
|
3509
|
+
bodyPath: path14.join(outboxDir, item.bodyPath)
|
|
3510
|
+
};
|
|
3511
|
+
}
|
|
3512
|
+
function outboxInputFromItem(item, body) {
|
|
3513
|
+
return {
|
|
3514
|
+
operation: item.operation,
|
|
3515
|
+
agentOsSlug: item.agentOsSlug,
|
|
3516
|
+
planId: item.planId,
|
|
3517
|
+
planSlug: item.planSlug,
|
|
3518
|
+
title: item.title,
|
|
3519
|
+
summary: item.summary,
|
|
3520
|
+
body,
|
|
3521
|
+
changeSummary: item.changeSummary ?? void 0,
|
|
3522
|
+
author: item.author ?? void 0,
|
|
3523
|
+
model: item.model ?? void 0,
|
|
3524
|
+
sourceRefs: item.sourceRefs,
|
|
3525
|
+
markCurrent: item.markCurrent ?? true,
|
|
3526
|
+
maxRetries: item.maxRetries
|
|
3527
|
+
};
|
|
3528
|
+
}
|
|
3529
|
+
|
|
3530
|
+
// src/plan-persist/readback.ts
|
|
3531
|
+
async function verifyPlanReadback(slug, expectation, deps = {}) {
|
|
3532
|
+
const payload = await agentOsGetPlan(slug, expectation.planId, deps);
|
|
3533
|
+
const plan = payload.plan;
|
|
3534
|
+
const current = payload.currentVersion;
|
|
3535
|
+
if (plan.title.trim() !== expectation.title.trim()) {
|
|
3536
|
+
throw new PlanPersistError(
|
|
3537
|
+
"verification_failed",
|
|
3538
|
+
`title mismatch: expected "${expectation.title}", got "${plan.title}"`
|
|
3539
|
+
);
|
|
3540
|
+
}
|
|
3541
|
+
const expectedSummaryHash = hashSummary(expectation.summary);
|
|
3542
|
+
const actualSummaryHash = hashSummary(plan.summary);
|
|
3543
|
+
if (expectedSummaryHash !== actualSummaryHash) {
|
|
3544
|
+
throw new PlanPersistError("verification_failed", "summary mismatch on readback");
|
|
3545
|
+
}
|
|
3546
|
+
if (expectation.versionId && plan.currentVersionId !== expectation.versionId) {
|
|
3547
|
+
throw new PlanPersistError(
|
|
3548
|
+
"verification_failed",
|
|
3549
|
+
`currentVersionId mismatch: expected ${expectation.versionId}, got ${plan.currentVersionId}`
|
|
3550
|
+
);
|
|
3551
|
+
}
|
|
3552
|
+
if (expectation.versionNumber != null) {
|
|
3553
|
+
if (!current || current.versionNumber !== expectation.versionNumber) {
|
|
3554
|
+
throw new PlanPersistError(
|
|
3555
|
+
"verification_failed",
|
|
3556
|
+
`versionNumber mismatch: expected ${expectation.versionNumber}, got ${current?.versionNumber ?? "none"}`
|
|
3557
|
+
);
|
|
3558
|
+
}
|
|
3559
|
+
}
|
|
3560
|
+
const bodyForHash = current?.body ?? "";
|
|
3561
|
+
const actualBodyHash = hashPlanBody(bodyForHash);
|
|
3562
|
+
if (expectation.bodyHash && actualBodyHash !== expectation.bodyHash) {
|
|
3563
|
+
throw new PlanPersistError("verification_failed", "body hash mismatch on readback");
|
|
3564
|
+
}
|
|
3565
|
+
return {
|
|
3566
|
+
planId: plan.id,
|
|
3567
|
+
currentVersionId: plan.currentVersionId,
|
|
3568
|
+
versionNumber: current?.versionNumber ?? null,
|
|
3569
|
+
title: plan.title,
|
|
3570
|
+
summary: plan.summary,
|
|
3571
|
+
bodyHash: expectation.bodyHash || actualBodyHash,
|
|
3572
|
+
readAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3573
|
+
};
|
|
3574
|
+
}
|
|
3575
|
+
function buildReadbackExpectation(input, write) {
|
|
3576
|
+
return {
|
|
3577
|
+
planId: write.planId,
|
|
3578
|
+
title: input.title,
|
|
3579
|
+
summary: input.summary ?? null,
|
|
3580
|
+
body: input.body,
|
|
3581
|
+
bodyHash: hashPlanBody(input.body),
|
|
3582
|
+
versionId: write.versionId,
|
|
3583
|
+
versionNumber: write.versionNumber
|
|
3584
|
+
};
|
|
3585
|
+
}
|
|
3586
|
+
|
|
3587
|
+
// src/plan-persist/persist.ts
|
|
3588
|
+
var SUCCESS_STATUS = "persisted and read back";
|
|
3589
|
+
var QUEUED_STATUS = "queued for retry";
|
|
3590
|
+
var FAILED_STATUS = "failed and needs action";
|
|
3591
|
+
async function persistPlan(input, deps = {}) {
|
|
3592
|
+
if (input.bodyPathHint && isTmpOnlyPath(input.bodyPathHint)) {
|
|
3593
|
+
}
|
|
3594
|
+
const idempotencyKey = buildPlanPersistIdempotencyKey(input);
|
|
3595
|
+
const existingOutbox = findOutboxByIdempotencyKey(idempotencyKey);
|
|
3596
|
+
if (existingOutbox?.readbackEvidence) {
|
|
3597
|
+
return {
|
|
3598
|
+
userStatus: SUCCESS_STATUS,
|
|
3599
|
+
outboxId: existingOutbox.id,
|
|
3600
|
+
planId: existingOutbox.readbackEvidence.planId,
|
|
3601
|
+
readbackEvidence: existingOutbox.readbackEvidence,
|
|
3602
|
+
idempotencyKey
|
|
3603
|
+
};
|
|
3604
|
+
}
|
|
3605
|
+
if (input.immediateFailure) {
|
|
3606
|
+
return queueForRetry(input, input.immediateFailure.message, input.immediateFailure.kind, existingOutbox);
|
|
3607
|
+
}
|
|
3608
|
+
const writeFn = deps.writePlan ?? agentOsWritePlan;
|
|
3609
|
+
const verifyFn = deps.verifyReadback ?? verifyPlanReadback;
|
|
3610
|
+
try {
|
|
3611
|
+
const write = await writeFn(input, deps);
|
|
3612
|
+
const enriched = { ...input, planId: write.planId };
|
|
3613
|
+
const expectation = buildReadbackExpectation(enriched, write);
|
|
3614
|
+
const readback = await verifyFn(
|
|
3615
|
+
input.agentOsSlug,
|
|
3616
|
+
readbackExpectationForOperation(input, expectation),
|
|
3617
|
+
deps
|
|
3618
|
+
);
|
|
3619
|
+
if (existingOutbox) archiveOutboxItem(existingOutbox);
|
|
3620
|
+
return {
|
|
3621
|
+
userStatus: SUCCESS_STATUS,
|
|
3622
|
+
planId: write.planId,
|
|
3623
|
+
versionId: write.versionId ?? void 0,
|
|
3624
|
+
readbackEvidence: readback,
|
|
3625
|
+
idempotencyKey
|
|
3626
|
+
};
|
|
3627
|
+
} catch (err) {
|
|
3628
|
+
const failure = err instanceof PlanPersistError ? err : new PlanPersistError("tool_interruption", err instanceof Error ? err.message : String(err));
|
|
3629
|
+
if (!isRetryableFailure(failure.kind)) {
|
|
3630
|
+
const item = writeOutboxItem(input, {
|
|
3631
|
+
lastError: failure.message,
|
|
3632
|
+
lastFailureKind: failure.kind,
|
|
3633
|
+
existing: existingOutbox ?? void 0
|
|
3634
|
+
});
|
|
3635
|
+
const paths = outboxItemPaths(item);
|
|
3636
|
+
const failed = markOutboxFailed(item, failure.message);
|
|
3637
|
+
return {
|
|
3638
|
+
userStatus: FAILED_STATUS,
|
|
3639
|
+
outboxId: failed.id,
|
|
3640
|
+
outboxPath: paths.jsonPath,
|
|
3641
|
+
bodyPath: paths.bodyPath,
|
|
3642
|
+
lastError: failure.message,
|
|
3643
|
+
idempotencyKey
|
|
3644
|
+
};
|
|
3645
|
+
}
|
|
3646
|
+
return queueForRetry(input, failure.message, failure.kind, existingOutbox);
|
|
3647
|
+
}
|
|
3648
|
+
}
|
|
3649
|
+
function readbackExpectationForOperation(input, expectation) {
|
|
3650
|
+
if (input.operation === "update_metadata") {
|
|
3651
|
+
return { ...expectation, body: "", bodyHash: "" };
|
|
3652
|
+
}
|
|
3653
|
+
return expectation;
|
|
3654
|
+
}
|
|
3655
|
+
function queueForRetry(input, message, kind, existing) {
|
|
3656
|
+
const item = writeOutboxItem(input, {
|
|
3657
|
+
lastError: message,
|
|
3658
|
+
lastFailureKind: kind,
|
|
3659
|
+
existing: existing ?? void 0
|
|
3660
|
+
});
|
|
3661
|
+
const paths = outboxItemPaths(item);
|
|
3662
|
+
if (item.retryCount >= item.maxRetries) {
|
|
3663
|
+
const failed = markOutboxFailed(item, message);
|
|
3664
|
+
return {
|
|
3665
|
+
userStatus: FAILED_STATUS,
|
|
3666
|
+
outboxId: failed.id,
|
|
3667
|
+
outboxPath: paths.jsonPath,
|
|
3668
|
+
bodyPath: paths.bodyPath,
|
|
3669
|
+
lastError: message,
|
|
3670
|
+
idempotencyKey: item.idempotencyKey
|
|
3671
|
+
};
|
|
3672
|
+
}
|
|
3673
|
+
return {
|
|
3674
|
+
userStatus: QUEUED_STATUS,
|
|
3675
|
+
outboxId: item.id,
|
|
3676
|
+
outboxPath: paths.jsonPath,
|
|
3677
|
+
bodyPath: paths.bodyPath,
|
|
3678
|
+
lastError: message,
|
|
3679
|
+
idempotencyKey: item.idempotencyKey
|
|
3680
|
+
};
|
|
3681
|
+
}
|
|
3682
|
+
function markOutboxFailed(item, message) {
|
|
3683
|
+
const failed = {
|
|
3684
|
+
...item,
|
|
3685
|
+
queueStatus: "failed",
|
|
3686
|
+
userStatus: FAILED_STATUS,
|
|
3687
|
+
lastError: message,
|
|
3688
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3689
|
+
};
|
|
3690
|
+
saveOutboxItem(failed);
|
|
3691
|
+
return failed;
|
|
3692
|
+
}
|
|
3693
|
+
|
|
3694
|
+
// src/plan-persist/drain.ts
|
|
3695
|
+
import path15 from "node:path";
|
|
3696
|
+
async function drainPlanOutbox(opts = {}, deps = {}) {
|
|
3697
|
+
const items = listOutboxItems().filter(
|
|
3698
|
+
(item) => opts.outboxId ? item.id === opts.outboxId : true
|
|
3699
|
+
);
|
|
3700
|
+
const slice = opts.max && opts.max > 0 ? items.slice(0, opts.max) : items;
|
|
3701
|
+
const result = {
|
|
3702
|
+
processed: 0,
|
|
3703
|
+
succeeded: 0,
|
|
3704
|
+
stillQueued: 0,
|
|
3705
|
+
failed: 0,
|
|
3706
|
+
results: []
|
|
3707
|
+
};
|
|
3708
|
+
for (const item of slice) {
|
|
3709
|
+
result.processed += 1;
|
|
3710
|
+
const body = readOutboxBody(item);
|
|
3711
|
+
const input = outboxInputFromItem(item, body);
|
|
3712
|
+
const attempt = await persistPlan(input, deps);
|
|
3713
|
+
if (attempt.userStatus === "persisted and read back") {
|
|
3714
|
+
result.succeeded += 1;
|
|
3715
|
+
} else if (attempt.userStatus === "failed and needs action") {
|
|
3716
|
+
result.failed += 1;
|
|
3717
|
+
} else {
|
|
3718
|
+
result.stillQueued += 1;
|
|
3719
|
+
}
|
|
3720
|
+
result.results.push({
|
|
3721
|
+
outboxId: item.id,
|
|
3722
|
+
userStatus: attempt.userStatus,
|
|
3723
|
+
lastError: attempt.lastError
|
|
3724
|
+
});
|
|
3725
|
+
}
|
|
3726
|
+
return result;
|
|
3727
|
+
}
|
|
3728
|
+
function loadOutboxById(outboxId) {
|
|
3729
|
+
const jsonPath = path15.join(planOutboxDir(), `${outboxId}.json`);
|
|
3730
|
+
return readOutboxItem(jsonPath);
|
|
3731
|
+
}
|
|
3732
|
+
|
|
3733
|
+
// src/plan-persist/handoff.ts
|
|
3734
|
+
function formatPlanOutboxHandoffBlock(item) {
|
|
3735
|
+
const paths = outboxItemPaths(item);
|
|
3736
|
+
return [
|
|
3737
|
+
"## Plan persistence risk",
|
|
3738
|
+
"",
|
|
3739
|
+
`AgentOS plan write is **not** confirmed (${item.userStatus}).`,
|
|
3740
|
+
`- outboxId: \`${item.id}\``,
|
|
3741
|
+
item.planId ? `- planId: \`${item.planId}\`` : "- planId: (pending \u2014 create not yet applied)",
|
|
3742
|
+
`- outbox: \`${paths.jsonPath}\``,
|
|
3743
|
+
`- body: \`${paths.bodyPath}\``,
|
|
3744
|
+
"",
|
|
3745
|
+
"Drain when approval/connectivity returns: `kynver plan outbox drain`"
|
|
3746
|
+
].join("\n");
|
|
3747
|
+
}
|
|
3748
|
+
function extractPlanOutboxFromTask(task) {
|
|
3749
|
+
const meta = task.metadata && typeof task.metadata === "object" ? task.metadata : null;
|
|
3750
|
+
const outboxId = typeof task.planPersistenceOutboxId === "string" && task.planPersistenceOutboxId || (meta && typeof meta.planPersistenceOutboxId === "string" ? meta.planPersistenceOutboxId : void 0);
|
|
3751
|
+
if (!outboxId) return null;
|
|
3752
|
+
return {
|
|
3753
|
+
outboxId,
|
|
3754
|
+
jsonPath: typeof task.planPersistenceOutboxPath === "string" ? task.planPersistenceOutboxPath : meta && typeof meta.planPersistenceOutboxPath === "string" ? meta.planPersistenceOutboxPath : void 0,
|
|
3755
|
+
bodyPath: typeof task.planPersistenceBodyPath === "string" ? task.planPersistenceBodyPath : meta && typeof meta.planPersistenceBodyPath === "string" ? meta.planPersistenceBodyPath : void 0
|
|
3756
|
+
};
|
|
3757
|
+
}
|
|
3758
|
+
|
|
2915
3759
|
// src/dispatch.ts
|
|
2916
3760
|
var DEFAULT_DISPATCH_LEASE_MS = 60 * 60 * 1e3;
|
|
2917
3761
|
function readHarnessWorkerContext(decision) {
|
|
@@ -2941,14 +3785,29 @@ function normalizePersonaSlug(value) {
|
|
|
2941
3785
|
return trimmed.length ? trimmed : null;
|
|
2942
3786
|
}
|
|
2943
3787
|
function buildDispatchTaskText(task, agentOsId) {
|
|
2944
|
-
|
|
3788
|
+
const lines = [
|
|
2945
3789
|
`[AgentOS task ${task.id}] ${task.title}`,
|
|
2946
3790
|
"",
|
|
2947
3791
|
task.description ? String(task.description) : "(no description on the board task)",
|
|
2948
3792
|
"",
|
|
2949
3793
|
`Board linkage: agentOsId=${agentOsId}, taskId=${task.id}, attempt=${task.attempt}, executor=${task.executor}${task.executorRef ? `, executorRef=${task.executorRef}` : ""}.`,
|
|
2950
3794
|
"This worker was dispatched from the AgentOS board. The harness reports your completion back to the board when you finish."
|
|
2951
|
-
]
|
|
3795
|
+
];
|
|
3796
|
+
const outboxRef = extractPlanOutboxFromTask(task);
|
|
3797
|
+
if (outboxRef?.outboxId) {
|
|
3798
|
+
const item = loadOutboxById(outboxRef.outboxId);
|
|
3799
|
+
if (item) {
|
|
3800
|
+
lines.push("", formatPlanOutboxHandoffBlock(item));
|
|
3801
|
+
} else {
|
|
3802
|
+
lines.push(
|
|
3803
|
+
"",
|
|
3804
|
+
`## Plan persistence risk`,
|
|
3805
|
+
"",
|
|
3806
|
+
`Unconfirmed AgentOS plan write (outboxId=${outboxRef.outboxId}).`
|
|
3807
|
+
);
|
|
3808
|
+
}
|
|
3809
|
+
}
|
|
3810
|
+
return lines.join("\n");
|
|
2952
3811
|
}
|
|
2953
3812
|
async function dispatchRun(args) {
|
|
2954
3813
|
const pipeline = args.pipeline === true || args.pipeline === "true";
|
|
@@ -2967,7 +3826,7 @@ async function dispatchRun(args) {
|
|
|
2967
3826
|
const activeHarnessWorkers = [];
|
|
2968
3827
|
for (const name of Object.keys(run.workers || {})) {
|
|
2969
3828
|
const worker = readJson(
|
|
2970
|
-
|
|
3829
|
+
path16.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
2971
3830
|
void 0
|
|
2972
3831
|
);
|
|
2973
3832
|
if (!worker?.taskId || !isPidAlive(worker.pid)) continue;
|
|
@@ -3180,7 +4039,7 @@ function redactHarness(text, secret) {
|
|
|
3180
4039
|
}
|
|
3181
4040
|
|
|
3182
4041
|
// src/validate.ts
|
|
3183
|
-
import
|
|
4042
|
+
import path17 from "node:path";
|
|
3184
4043
|
var RUN_ID_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/i;
|
|
3185
4044
|
var WORKER_NAME_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/i;
|
|
3186
4045
|
function validateRunId(runId) {
|
|
@@ -3194,15 +4053,15 @@ function validateWorkerName(name) {
|
|
|
3194
4053
|
return trimmed;
|
|
3195
4054
|
}
|
|
3196
4055
|
function validateRepo(repo) {
|
|
3197
|
-
const resolved =
|
|
4056
|
+
const resolved = path17.resolve(repo);
|
|
3198
4057
|
if (resolved.includes("..")) throw new Error("repo path must not contain .. segments");
|
|
3199
4058
|
return resolved;
|
|
3200
4059
|
}
|
|
3201
4060
|
function validateOwnedPaths(repoRoot, ownedPaths) {
|
|
3202
4061
|
return ownedPaths.map((owned) => {
|
|
3203
|
-
const resolved =
|
|
3204
|
-
const rel =
|
|
3205
|
-
if (rel.startsWith("..") ||
|
|
4062
|
+
const resolved = path17.resolve(repoRoot, owned);
|
|
4063
|
+
const rel = path17.relative(repoRoot, resolved);
|
|
4064
|
+
if (rel.startsWith("..") || path17.isAbsolute(rel)) {
|
|
3206
4065
|
throw new Error(`owned path escapes repo: ${owned}`);
|
|
3207
4066
|
}
|
|
3208
4067
|
return resolved;
|
|
@@ -3214,15 +4073,15 @@ function validateTailLines(lines) {
|
|
|
3214
4073
|
}
|
|
3215
4074
|
|
|
3216
4075
|
// src/worktree.ts
|
|
3217
|
-
import { existsSync as
|
|
3218
|
-
import
|
|
4076
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync5 } from "node:fs";
|
|
4077
|
+
import path18 from "node:path";
|
|
3219
4078
|
function createRun(args) {
|
|
3220
4079
|
const repo = validateRepo(required(String(args.repo || ""), "--repo"));
|
|
3221
4080
|
ensureGitRepo(repo);
|
|
3222
4081
|
const id = args.id ? validateRunId(String(args.id)) : timestampSlug(String(args.name || "run"));
|
|
3223
4082
|
const dir = runDirectory(id);
|
|
3224
|
-
if (
|
|
3225
|
-
|
|
4083
|
+
if (existsSync14(dir)) failExists(`run already exists: ${id}`);
|
|
4084
|
+
mkdirSync5(dir, { recursive: true });
|
|
3226
4085
|
const base = String(args.base || "origin/main");
|
|
3227
4086
|
const baseCommit = git(repo, ["rev-parse", base]).trim();
|
|
3228
4087
|
const run = {
|
|
@@ -3235,12 +4094,12 @@ function createRun(args) {
|
|
|
3235
4094
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3236
4095
|
workers: {}
|
|
3237
4096
|
};
|
|
3238
|
-
writeJson(
|
|
4097
|
+
writeJson(path18.join(dir, "run.json"), run);
|
|
3239
4098
|
console.log(JSON.stringify({ runId: id, runDir: dir, repo, base, baseCommit }, null, 2));
|
|
3240
4099
|
}
|
|
3241
4100
|
function listRuns() {
|
|
3242
4101
|
const { runsDir } = getPaths();
|
|
3243
|
-
const rows = listRunIds(runsDir).map((id) => readJson(
|
|
4102
|
+
const rows = listRunIds(runsDir).map((id) => readJson(path18.join(runDirectory(id), "run.json"), void 0)).filter(Boolean).map((run) => ({
|
|
3244
4103
|
id: run.id,
|
|
3245
4104
|
name: run.name,
|
|
3246
4105
|
status: run.status,
|
|
@@ -3255,7 +4114,7 @@ function failExists(message) {
|
|
|
3255
4114
|
}
|
|
3256
4115
|
|
|
3257
4116
|
// src/sweep.ts
|
|
3258
|
-
import
|
|
4117
|
+
import path19 from "node:path";
|
|
3259
4118
|
async function sweepRun(args) {
|
|
3260
4119
|
const pipeline = args.pipeline === true || args.pipeline === "true";
|
|
3261
4120
|
try {
|
|
@@ -3268,7 +4127,7 @@ async function sweepRun(args) {
|
|
|
3268
4127
|
const releasedLocalOrphans = [];
|
|
3269
4128
|
for (const name of Object.keys(run.workers || {})) {
|
|
3270
4129
|
const worker = readJson(
|
|
3271
|
-
|
|
4130
|
+
path19.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
3272
4131
|
void 0
|
|
3273
4132
|
);
|
|
3274
4133
|
if (!worker || !worker.dispatched || !worker.taskId) continue;
|
|
@@ -3311,17 +4170,68 @@ async function sweepRun(args) {
|
|
|
3311
4170
|
}
|
|
3312
4171
|
|
|
3313
4172
|
// src/cli.ts
|
|
3314
|
-
import { mkdirSync as
|
|
3315
|
-
import { fileURLToPath as
|
|
4173
|
+
import { mkdirSync as mkdirSync7, realpathSync } from "node:fs";
|
|
4174
|
+
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
3316
4175
|
|
|
3317
4176
|
// src/pipeline-tick.ts
|
|
3318
|
-
import
|
|
4177
|
+
import path28 from "node:path";
|
|
4178
|
+
|
|
4179
|
+
// src/pipeline-dispatch.ts
|
|
4180
|
+
var RESERVED_REVIEW_STARTS = 1;
|
|
4181
|
+
function countDispatchStarts(result) {
|
|
4182
|
+
if (!result || typeof result !== "object") return 0;
|
|
4183
|
+
const startedCount = result.startedCount;
|
|
4184
|
+
if (typeof startedCount === "number") return startedCount;
|
|
4185
|
+
const outcomes = result.outcomes;
|
|
4186
|
+
if (!Array.isArray(outcomes)) return 0;
|
|
4187
|
+
return outcomes.filter((o) => o.started).length;
|
|
4188
|
+
}
|
|
4189
|
+
function stripCliMaxStarts(args) {
|
|
4190
|
+
const { maxStarts: _maxStarts, ...rest } = args;
|
|
4191
|
+
return rest;
|
|
4192
|
+
}
|
|
4193
|
+
async function runPipelineDispatch(args, slots) {
|
|
4194
|
+
if (slots <= 0) {
|
|
4195
|
+
return { ok: true, skipped: true, reason: "no slots", maxStarts: 0, startedCount: 0 };
|
|
4196
|
+
}
|
|
4197
|
+
const base = stripCliMaxStarts(args);
|
|
4198
|
+
const reviewBudget = Math.min(slots, RESERVED_REVIEW_STARTS);
|
|
4199
|
+
const workBudget = Math.max(0, slots - reviewBudget);
|
|
4200
|
+
const review = await dispatchRun({
|
|
4201
|
+
...base,
|
|
4202
|
+
execute: true,
|
|
4203
|
+
pipeline: true,
|
|
4204
|
+
lane: "review",
|
|
4205
|
+
maxStarts: String(reviewBudget)
|
|
4206
|
+
});
|
|
4207
|
+
const reviewStarted = countDispatchStarts(review);
|
|
4208
|
+
const workSlots = workBudget + (reviewBudget - reviewStarted);
|
|
4209
|
+
if (workSlots <= 0) {
|
|
4210
|
+
return {
|
|
4211
|
+
...typeof review === "object" && review !== null ? review : {},
|
|
4212
|
+
passes: { review },
|
|
4213
|
+
startedCount: reviewStarted
|
|
4214
|
+
};
|
|
4215
|
+
}
|
|
4216
|
+
const work = await dispatchRun({
|
|
4217
|
+
...base,
|
|
4218
|
+
execute: true,
|
|
4219
|
+
pipeline: true,
|
|
4220
|
+
maxStarts: String(workSlots)
|
|
4221
|
+
});
|
|
4222
|
+
const workStarted = countDispatchStarts(work);
|
|
4223
|
+
return {
|
|
4224
|
+
passes: { review, work },
|
|
4225
|
+
startedCount: reviewStarted + workStarted,
|
|
4226
|
+
ok: true
|
|
4227
|
+
};
|
|
4228
|
+
}
|
|
3319
4229
|
|
|
3320
4230
|
// src/stale-reconcile.ts
|
|
3321
|
-
import
|
|
4231
|
+
import path21 from "node:path";
|
|
3322
4232
|
|
|
3323
4233
|
// src/finalize.ts
|
|
3324
|
-
import
|
|
4234
|
+
import path20 from "node:path";
|
|
3325
4235
|
var ACTIVE_RUN_STATUSES = /* @__PURE__ */ new Set(["running", "dispatching", "pending", "queued"]);
|
|
3326
4236
|
function terminalStatusFor(run) {
|
|
3327
4237
|
const names = Object.keys(run.workers || {});
|
|
@@ -3332,7 +4242,7 @@ function terminalStatusFor(run) {
|
|
|
3332
4242
|
let anyLandingBlocked = false;
|
|
3333
4243
|
for (const name of names) {
|
|
3334
4244
|
const worker = readJson(
|
|
3335
|
-
|
|
4245
|
+
path20.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
3336
4246
|
void 0
|
|
3337
4247
|
);
|
|
3338
4248
|
if (!worker) continue;
|
|
@@ -3384,7 +4294,7 @@ function reconcileStaleWorkers() {
|
|
|
3384
4294
|
const now = Date.now();
|
|
3385
4295
|
for (const run of listRunRecords()) {
|
|
3386
4296
|
for (const name of Object.keys(run.workers || {})) {
|
|
3387
|
-
const workerPath =
|
|
4297
|
+
const workerPath = path21.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
|
|
3388
4298
|
const worker = readJson(workerPath, void 0);
|
|
3389
4299
|
if (!worker || worker.status !== "running") {
|
|
3390
4300
|
outcomes.push({
|
|
@@ -3460,7 +4370,7 @@ function reconcileStaleWorkers() {
|
|
|
3460
4370
|
}
|
|
3461
4371
|
|
|
3462
4372
|
// src/plan-progress-daemon-sync.ts
|
|
3463
|
-
import
|
|
4373
|
+
import path22 from "node:path";
|
|
3464
4374
|
|
|
3465
4375
|
// src/plan-progress-sync.ts
|
|
3466
4376
|
async function syncPlanProgress(args) {
|
|
@@ -3484,7 +4394,7 @@ async function syncActiveWorkerPlanProgress(runId, args) {
|
|
|
3484
4394
|
const outcomes = [];
|
|
3485
4395
|
for (const name of Object.keys(run.workers || {})) {
|
|
3486
4396
|
const worker = readJson(
|
|
3487
|
-
|
|
4397
|
+
path22.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
3488
4398
|
void 0
|
|
3489
4399
|
);
|
|
3490
4400
|
if (!worker?.dispatched || !worker.taskId) continue;
|
|
@@ -3533,7 +4443,7 @@ async function fetchWorkspaceRuntimePreferences(agentOsId, args) {
|
|
|
3533
4443
|
}
|
|
3534
4444
|
|
|
3535
4445
|
// src/cleanup.ts
|
|
3536
|
-
import
|
|
4446
|
+
import path27 from "node:path";
|
|
3537
4447
|
|
|
3538
4448
|
// src/cleanup-types.ts
|
|
3539
4449
|
var DEFAULT_NODE_MODULES_AGE_MS = 6 * 60 * 60 * 1e3;
|
|
@@ -3604,14 +4514,14 @@ function skipNodeModulesRemoval(input) {
|
|
|
3604
4514
|
}
|
|
3605
4515
|
|
|
3606
4516
|
// src/cleanup-execute.ts
|
|
3607
|
-
import { existsSync as
|
|
3608
|
-
import
|
|
4517
|
+
import { existsSync as existsSync16, rmSync } from "node:fs";
|
|
4518
|
+
import path24 from "node:path";
|
|
3609
4519
|
|
|
3610
4520
|
// src/cleanup-dir-size.ts
|
|
3611
|
-
import { existsSync as
|
|
3612
|
-
import
|
|
4521
|
+
import { existsSync as existsSync15, readdirSync as readdirSync5, statSync as statSync2 } from "node:fs";
|
|
4522
|
+
import path23 from "node:path";
|
|
3613
4523
|
function directorySizeBytes(root, maxEntries = 5e4) {
|
|
3614
|
-
if (!
|
|
4524
|
+
if (!existsSync15(root)) return 0;
|
|
3615
4525
|
let total = 0;
|
|
3616
4526
|
let seen = 0;
|
|
3617
4527
|
const stack = [root];
|
|
@@ -3619,13 +4529,13 @@ function directorySizeBytes(root, maxEntries = 5e4) {
|
|
|
3619
4529
|
const current = stack.pop();
|
|
3620
4530
|
let entries;
|
|
3621
4531
|
try {
|
|
3622
|
-
entries =
|
|
4532
|
+
entries = readdirSync5(current);
|
|
3623
4533
|
} catch {
|
|
3624
4534
|
continue;
|
|
3625
4535
|
}
|
|
3626
4536
|
for (const name of entries) {
|
|
3627
4537
|
if (seen++ > maxEntries) return null;
|
|
3628
|
-
const full =
|
|
4538
|
+
const full = path23.join(current, name);
|
|
3629
4539
|
let st;
|
|
3630
4540
|
try {
|
|
3631
4541
|
st = statSync2(full);
|
|
@@ -3641,7 +4551,7 @@ function directorySizeBytes(root, maxEntries = 5e4) {
|
|
|
3641
4551
|
|
|
3642
4552
|
// src/cleanup-execute.ts
|
|
3643
4553
|
function removeNodeModules(candidate, execute) {
|
|
3644
|
-
if (!
|
|
4554
|
+
if (!existsSync16(candidate.path)) {
|
|
3645
4555
|
return {
|
|
3646
4556
|
...candidate,
|
|
3647
4557
|
executed: false,
|
|
@@ -3672,7 +4582,7 @@ function removeNodeModules(candidate, execute) {
|
|
|
3672
4582
|
}
|
|
3673
4583
|
}
|
|
3674
4584
|
function removeWorktree(candidate, execute) {
|
|
3675
|
-
if (!
|
|
4585
|
+
if (!existsSync16(candidate.path)) {
|
|
3676
4586
|
return {
|
|
3677
4587
|
...candidate,
|
|
3678
4588
|
executed: false,
|
|
@@ -3689,7 +4599,7 @@ function removeWorktree(candidate, execute) {
|
|
|
3689
4599
|
if (repo) {
|
|
3690
4600
|
git(repo, ["worktree", "remove", "--force", candidate.path], { allowFailure: true });
|
|
3691
4601
|
}
|
|
3692
|
-
if (
|
|
4602
|
+
if (existsSync16(candidate.path)) {
|
|
3693
4603
|
rmSync(candidate.path, { recursive: true, force: true });
|
|
3694
4604
|
}
|
|
3695
4605
|
return {
|
|
@@ -3709,20 +4619,20 @@ function removeWorktree(candidate, execute) {
|
|
|
3709
4619
|
}
|
|
3710
4620
|
}
|
|
3711
4621
|
function isHarnessNodeModulesPath(targetPath, harnessRoot, worktreesDir) {
|
|
3712
|
-
const resolved =
|
|
3713
|
-
const nm = resolved.endsWith(`${
|
|
4622
|
+
const resolved = path24.resolve(targetPath);
|
|
4623
|
+
const nm = resolved.endsWith(`${path24.sep}node_modules`) ? resolved : null;
|
|
3714
4624
|
if (!nm) return "path_outside_harness";
|
|
3715
|
-
const rel =
|
|
3716
|
-
if (rel.startsWith("..") ||
|
|
3717
|
-
const parts = rel.split(
|
|
4625
|
+
const rel = path24.relative(worktreesDir, nm);
|
|
4626
|
+
if (rel.startsWith("..") || path24.isAbsolute(rel)) return "path_outside_harness";
|
|
4627
|
+
const parts = rel.split(path24.sep);
|
|
3718
4628
|
if (parts.length < 3 || parts[parts.length - 1] !== "node_modules") return "path_outside_harness";
|
|
3719
|
-
if (!resolved.startsWith(
|
|
4629
|
+
if (!resolved.startsWith(path24.resolve(harnessRoot))) return "path_outside_harness";
|
|
3720
4630
|
return null;
|
|
3721
4631
|
}
|
|
3722
4632
|
|
|
3723
4633
|
// src/cleanup-scan.ts
|
|
3724
|
-
import { existsSync as
|
|
3725
|
-
import
|
|
4634
|
+
import { existsSync as existsSync17, readdirSync as readdirSync6, statSync as statSync3 } from "node:fs";
|
|
4635
|
+
import path25 from "node:path";
|
|
3726
4636
|
function pathAgeMs(target, now) {
|
|
3727
4637
|
try {
|
|
3728
4638
|
const mtime = statSync3(target).mtimeMs;
|
|
@@ -3732,17 +4642,17 @@ function pathAgeMs(target, now) {
|
|
|
3732
4642
|
}
|
|
3733
4643
|
}
|
|
3734
4644
|
function isPathInside(child, parent) {
|
|
3735
|
-
const rel =
|
|
3736
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
4645
|
+
const rel = path25.relative(parent, child);
|
|
4646
|
+
return rel === "" || !rel.startsWith("..") && !path25.isAbsolute(rel);
|
|
3737
4647
|
}
|
|
3738
4648
|
function scanNodeModulesCandidates(opts) {
|
|
3739
4649
|
const candidates = [];
|
|
3740
4650
|
const seen = /* @__PURE__ */ new Set();
|
|
3741
4651
|
for (const entry of opts.index.values()) {
|
|
3742
4652
|
if (opts.runIdFilter && entry.runId !== opts.runIdFilter) continue;
|
|
3743
|
-
const nm =
|
|
3744
|
-
if (!
|
|
3745
|
-
const resolved =
|
|
4653
|
+
const nm = path25.join(entry.worktreePath, "node_modules");
|
|
4654
|
+
if (!existsSync17(nm)) continue;
|
|
4655
|
+
const resolved = path25.resolve(nm);
|
|
3746
4656
|
if (seen.has(resolved)) continue;
|
|
3747
4657
|
seen.add(resolved);
|
|
3748
4658
|
candidates.push({
|
|
@@ -3755,16 +4665,16 @@ function scanNodeModulesCandidates(opts) {
|
|
|
3755
4665
|
ageMs: pathAgeMs(resolved, opts.now)
|
|
3756
4666
|
});
|
|
3757
4667
|
}
|
|
3758
|
-
if (!opts.includeOrphans || !
|
|
3759
|
-
for (const runEntry of
|
|
4668
|
+
if (!opts.includeOrphans || !existsSync17(opts.worktreesDir)) return candidates;
|
|
4669
|
+
for (const runEntry of readdirSync6(opts.worktreesDir, { withFileTypes: true })) {
|
|
3760
4670
|
if (!runEntry.isDirectory()) continue;
|
|
3761
|
-
const runPath =
|
|
3762
|
-
for (const workerEntry of
|
|
4671
|
+
const runPath = path25.join(opts.worktreesDir, runEntry.name);
|
|
4672
|
+
for (const workerEntry of readdirSync6(runPath, { withFileTypes: true })) {
|
|
3763
4673
|
if (!workerEntry.isDirectory()) continue;
|
|
3764
|
-
const worktreePath =
|
|
3765
|
-
const nm =
|
|
3766
|
-
if (!
|
|
3767
|
-
const resolved =
|
|
4674
|
+
const worktreePath = path25.join(runPath, workerEntry.name);
|
|
4675
|
+
const nm = path25.join(worktreePath, "node_modules");
|
|
4676
|
+
if (!existsSync17(nm)) continue;
|
|
4677
|
+
const resolved = path25.resolve(nm);
|
|
3768
4678
|
if (seen.has(resolved)) continue;
|
|
3769
4679
|
if (!isPathInside(resolved, opts.harnessRoot)) continue;
|
|
3770
4680
|
seen.add(resolved);
|
|
@@ -3787,7 +4697,7 @@ function scanWorktreeCandidates(opts) {
|
|
|
3787
4697
|
for (const entry of opts.index.values()) {
|
|
3788
4698
|
if (opts.runIdFilter && entry.runId !== opts.runIdFilter) continue;
|
|
3789
4699
|
const resolved = entry.worktreePath;
|
|
3790
|
-
if (!
|
|
4700
|
+
if (!existsSync17(resolved)) continue;
|
|
3791
4701
|
if (seen.has(resolved)) continue;
|
|
3792
4702
|
seen.add(resolved);
|
|
3793
4703
|
candidates.push({
|
|
@@ -3804,17 +4714,17 @@ function scanWorktreeCandidates(opts) {
|
|
|
3804
4714
|
}
|
|
3805
4715
|
|
|
3806
4716
|
// src/cleanup-worktree-index.ts
|
|
3807
|
-
import
|
|
4717
|
+
import path26 from "node:path";
|
|
3808
4718
|
function buildWorktreeIndex() {
|
|
3809
4719
|
const index = /* @__PURE__ */ new Map();
|
|
3810
4720
|
for (const run of listRunRecords()) {
|
|
3811
4721
|
for (const name of Object.keys(run.workers || {})) {
|
|
3812
|
-
const workerPath =
|
|
4722
|
+
const workerPath = path26.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
|
|
3813
4723
|
const worker = readJson(workerPath, void 0);
|
|
3814
4724
|
if (!worker?.worktreePath) continue;
|
|
3815
4725
|
const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
|
|
3816
|
-
index.set(
|
|
3817
|
-
worktreePath:
|
|
4726
|
+
index.set(path26.resolve(worker.worktreePath), {
|
|
4727
|
+
worktreePath: path26.resolve(worker.worktreePath),
|
|
3818
4728
|
runId: run.id,
|
|
3819
4729
|
workerName: name,
|
|
3820
4730
|
run,
|
|
@@ -3828,8 +4738,8 @@ function buildWorktreeIndex() {
|
|
|
3828
4738
|
|
|
3829
4739
|
// src/cleanup.ts
|
|
3830
4740
|
function resolveOptions(options = {}) {
|
|
3831
|
-
const harnessRoot = options.harnessRoot ?
|
|
3832
|
-
const { worktreesDir } = options.harnessRoot ? { worktreesDir:
|
|
4741
|
+
const harnessRoot = options.harnessRoot ? path27.resolve(options.harnessRoot) : resolveHarnessRoot();
|
|
4742
|
+
const { worktreesDir } = options.harnessRoot ? { worktreesDir: path27.join(harnessRoot, "worktrees") } : getHarnessPaths();
|
|
3833
4743
|
const execute = options.execute === true;
|
|
3834
4744
|
const nodeModulesAgeMs = options.nodeModulesAgeMs ?? DEFAULT_NODE_MODULES_AGE_MS;
|
|
3835
4745
|
const worktreesAgeMs = options.worktreesAgeMs ?? 0;
|
|
@@ -3873,7 +4783,7 @@ function runHarnessCleanup(options = {}) {
|
|
|
3873
4783
|
actions.push({ ...candidate, executed: false, skipped: true, skipReason: pathSkip });
|
|
3874
4784
|
continue;
|
|
3875
4785
|
}
|
|
3876
|
-
const worktreePath =
|
|
4786
|
+
const worktreePath = path27.resolve(candidate.path, "..");
|
|
3877
4787
|
const indexed = index.get(worktreePath) ?? null;
|
|
3878
4788
|
const guardReason = skipNodeModulesRemoval({
|
|
3879
4789
|
indexed,
|
|
@@ -3889,7 +4799,7 @@ function runHarnessCleanup(options = {}) {
|
|
|
3889
4799
|
actions.push(removeNodeModules(candidate, resolved.execute));
|
|
3890
4800
|
}
|
|
3891
4801
|
for (const candidate of scanWorktreeCandidates(scanOpts)) {
|
|
3892
|
-
const indexed = index.get(
|
|
4802
|
+
const indexed = index.get(path27.resolve(candidate.path)) ?? null;
|
|
3893
4803
|
const guardReason = skipWorktreeRemoval({
|
|
3894
4804
|
indexed,
|
|
3895
4805
|
includeOrphans: resolved.includeOrphans,
|
|
@@ -3958,7 +4868,7 @@ async function completeFinishedWorkers(runId, args) {
|
|
|
3958
4868
|
const outcomes = [];
|
|
3959
4869
|
for (const name of Object.keys(run.workers || {})) {
|
|
3960
4870
|
const worker = readJson(
|
|
3961
|
-
|
|
4871
|
+
path28.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
3962
4872
|
void 0
|
|
3963
4873
|
);
|
|
3964
4874
|
if (!worker?.taskId || worker.localOnly) continue;
|
|
@@ -4009,6 +4919,7 @@ async function runPipelineTick(args) {
|
|
|
4009
4919
|
configuredMaxWorkersOverride: workspacePrefs?.maxConcurrentWorkers
|
|
4010
4920
|
});
|
|
4011
4921
|
const operatorTick = await postOperatorTick(agentOsId, runId, resourceGate, args);
|
|
4922
|
+
const completionAckSync = syncCompletionAcknowledgedFromOperatorTick(runId, operatorTick);
|
|
4012
4923
|
const leaseRenewal = await renewActiveTaskLeases(runId, args);
|
|
4013
4924
|
const completedWorkers = await completeFinishedWorkers(runId, args);
|
|
4014
4925
|
const staleReconcile = reconcileStaleWorkers();
|
|
@@ -4023,14 +4934,14 @@ async function runPipelineTick(args) {
|
|
|
4023
4934
|
const sweep = await sweepRun({ run: runId, agentOsId, pipeline: true, ...args });
|
|
4024
4935
|
let dispatch = null;
|
|
4025
4936
|
if (execute && maxStarts > 0) {
|
|
4026
|
-
dispatch = await
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4937
|
+
dispatch = await runPipelineDispatch(
|
|
4938
|
+
{
|
|
4939
|
+
...args,
|
|
4940
|
+
run: runId,
|
|
4941
|
+
agentOsId
|
|
4942
|
+
},
|
|
4943
|
+
maxStarts
|
|
4944
|
+
);
|
|
4034
4945
|
} else {
|
|
4035
4946
|
dispatch = {
|
|
4036
4947
|
ok: true,
|
|
@@ -4051,6 +4962,7 @@ async function runPipelineTick(args) {
|
|
|
4051
4962
|
staleReconcile,
|
|
4052
4963
|
harnessCleanup,
|
|
4053
4964
|
planProgressSync,
|
|
4965
|
+
completionAckSync,
|
|
4054
4966
|
operatorTick,
|
|
4055
4967
|
sweep,
|
|
4056
4968
|
dispatch,
|
|
@@ -4122,6 +5034,8 @@ async function emitPlanProgress(args) {
|
|
|
4122
5034
|
const url = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/plans/${encodeURIComponent(planId)}/progress-events`;
|
|
4123
5035
|
const cfg = loadUserConfig();
|
|
4124
5036
|
const provider = cfg.workerProvider ? `provider:${cfg.workerProvider}` : void 0;
|
|
5037
|
+
const explicitProposed = args.proposed === true || args.proposed === "true" ? true : args.proposed === false || args.proposed === "false" ? false : void 0;
|
|
5038
|
+
const proposed = explicitProposed ?? (status !== "done" && (roleLane === "implementer" || roleLane === "repair_implementer"));
|
|
4125
5039
|
const body = {
|
|
4126
5040
|
rowKey: args.row ? String(args.row) : void 0,
|
|
4127
5041
|
rowId: args.rowId ? String(args.rowId) : void 0,
|
|
@@ -4132,9 +5046,9 @@ async function emitPlanProgress(args) {
|
|
|
4132
5046
|
note: args.note ? String(args.note) : void 0,
|
|
4133
5047
|
remainingWork: args.remaining ? String(args.remaining) : void 0,
|
|
4134
5048
|
evidence: evidence.length ? evidence : void 0,
|
|
4135
|
-
proposed: args.proposed === true || args.proposed === "true",
|
|
4136
5049
|
executorRef: args.executorRef ? String(args.executorRef) : provider
|
|
4137
5050
|
};
|
|
5051
|
+
if (proposed !== void 0) body.proposed = proposed;
|
|
4138
5052
|
const res = await fetch(url, {
|
|
4139
5053
|
method: "POST",
|
|
4140
5054
|
headers: buildHarnessCallbackHeaders(secret),
|
|
@@ -4188,6 +5102,86 @@ async function verifyPlan(args) {
|
|
|
4188
5102
|
console.log(JSON.stringify(parsed, null, 2));
|
|
4189
5103
|
}
|
|
4190
5104
|
|
|
5105
|
+
// src/plan-persist-cli.ts
|
|
5106
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
5107
|
+
var OPERATIONS = ["create", "add_version", "update_metadata"];
|
|
5108
|
+
var FAILURE_KINDS = [
|
|
5109
|
+
"approval_guard",
|
|
5110
|
+
"auth",
|
|
5111
|
+
"network",
|
|
5112
|
+
"server",
|
|
5113
|
+
"tool_interruption"
|
|
5114
|
+
];
|
|
5115
|
+
function readBodyArg(args) {
|
|
5116
|
+
const bodyFile = args.bodyFile ? String(args.bodyFile) : void 0;
|
|
5117
|
+
if (bodyFile) {
|
|
5118
|
+
return { body: readFileSync8(bodyFile, "utf8"), bodyPathHint: bodyFile };
|
|
5119
|
+
}
|
|
5120
|
+
const inline = args.body ? String(args.body) : void 0;
|
|
5121
|
+
if (inline) return { body: inline };
|
|
5122
|
+
throw new Error("requires --body-file PATH or --body TEXT");
|
|
5123
|
+
}
|
|
5124
|
+
async function runPlanPersist(args) {
|
|
5125
|
+
const operationRaw = required(args.operation ? String(args.operation) : void 0, "operation");
|
|
5126
|
+
if (!OPERATIONS.includes(operationRaw)) {
|
|
5127
|
+
throw new Error(`invalid --operation ${operationRaw}`);
|
|
5128
|
+
}
|
|
5129
|
+
const operation = operationRaw;
|
|
5130
|
+
const cfg = loadUserConfig();
|
|
5131
|
+
const agentOsSlug = required(
|
|
5132
|
+
args.slug ? String(args.slug) : cfg.agentOsSlug,
|
|
5133
|
+
"slug (or agentOsSlug in ~/.kynver/config.json)"
|
|
5134
|
+
);
|
|
5135
|
+
const title = required(args.title ? String(args.title) : void 0, "title");
|
|
5136
|
+
const { body, bodyPathHint } = readBodyArg(args);
|
|
5137
|
+
if (bodyPathHint && isTmpOnlyPath(bodyPathHint)) {
|
|
5138
|
+
console.warn(
|
|
5139
|
+
JSON.stringify({
|
|
5140
|
+
warning: "/tmp-only body path is not durable; AgentOS persistence requires outbox or successful API write",
|
|
5141
|
+
bodyPathHint
|
|
5142
|
+
})
|
|
5143
|
+
);
|
|
5144
|
+
}
|
|
5145
|
+
const input = {
|
|
5146
|
+
operation,
|
|
5147
|
+
agentOsSlug,
|
|
5148
|
+
title,
|
|
5149
|
+
body,
|
|
5150
|
+
bodyPathHint,
|
|
5151
|
+
summary: args.summary ? String(args.summary) : void 0,
|
|
5152
|
+
planId: args.plan ? String(args.plan) : void 0,
|
|
5153
|
+
planSlug: args.planSlug ? String(args.planSlug) : void 0,
|
|
5154
|
+
changeSummary: args.changeSummary ? String(args.changeSummary) : void 0,
|
|
5155
|
+
author: args.author ? String(args.author) : void 0,
|
|
5156
|
+
model: args.model ? String(args.model) : void 0,
|
|
5157
|
+
maxRetries: args.maxRetries ? Number(args.maxRetries) : void 0,
|
|
5158
|
+
immediateFailure: parseImmediateFailure(args)
|
|
5159
|
+
};
|
|
5160
|
+
const result = await persistPlan(input);
|
|
5161
|
+
console.log(JSON.stringify(result, null, 2));
|
|
5162
|
+
if (result.userStatus === "failed and needs action") process.exit(1);
|
|
5163
|
+
}
|
|
5164
|
+
function parseImmediateFailure(args) {
|
|
5165
|
+
const kind = args.failureKind ? String(args.failureKind) : void 0;
|
|
5166
|
+
if (!kind) return void 0;
|
|
5167
|
+
if (!FAILURE_KINDS.includes(kind)) {
|
|
5168
|
+
throw new Error(`invalid --failure-kind ${kind}`);
|
|
5169
|
+
}
|
|
5170
|
+
const message = args.failureMessage ? String(args.failureMessage) : `immediate failure (${kind})`;
|
|
5171
|
+
return { kind, message };
|
|
5172
|
+
}
|
|
5173
|
+
async function runPlanOutboxList() {
|
|
5174
|
+
const items = listOutboxItems();
|
|
5175
|
+
console.log(JSON.stringify({ count: items.length, items }, null, 2));
|
|
5176
|
+
}
|
|
5177
|
+
async function runPlanOutboxDrain(args) {
|
|
5178
|
+
const max = args.max ? Number(args.max) : void 0;
|
|
5179
|
+
const outboxId = args.id ? String(args.id) : void 0;
|
|
5180
|
+
const result = await drainPlanOutbox({ max, outboxId });
|
|
5181
|
+
console.log(JSON.stringify(result, null, 2));
|
|
5182
|
+
if (result.failed > 0) process.exit(1);
|
|
5183
|
+
}
|
|
5184
|
+
|
|
4191
5185
|
// src/cleanup-cli.ts
|
|
4192
5186
|
function runCleanupCli(args) {
|
|
4193
5187
|
const execute = args.execute === true || args.execute === "true";
|
|
@@ -4208,6 +5202,519 @@ function runCleanupCli(args) {
|
|
|
4208
5202
|
}
|
|
4209
5203
|
}
|
|
4210
5204
|
|
|
5205
|
+
// src/monitor/monitor.service.ts
|
|
5206
|
+
import path30 from "node:path";
|
|
5207
|
+
|
|
5208
|
+
// src/monitor/monitor.classify.ts
|
|
5209
|
+
function expectedLeaseOwner(runId) {
|
|
5210
|
+
return `kynver-harness:${runId}`;
|
|
5211
|
+
}
|
|
5212
|
+
function classifyWorkerHealth(input) {
|
|
5213
|
+
const { worker, status, taskLease } = input;
|
|
5214
|
+
const leaseOwner = taskLease?.leaseOwner ?? null;
|
|
5215
|
+
const expectedOwner = expectedLeaseOwner(worker.runId);
|
|
5216
|
+
if (worker.dispatched && taskLease) {
|
|
5217
|
+
if (taskLease.status === "running" && leaseOwner && leaseOwner !== expectedOwner) {
|
|
5218
|
+
return {
|
|
5219
|
+
health: "orphaned",
|
|
5220
|
+
reason: `task lease held by ${leaseOwner}, expected ${expectedOwner}`
|
|
5221
|
+
};
|
|
5222
|
+
}
|
|
5223
|
+
if (taskLease.status === "running" && !status.alive && !status.finalResult) {
|
|
5224
|
+
return {
|
|
5225
|
+
health: "orphaned",
|
|
5226
|
+
reason: "board task running but worker process is not alive"
|
|
5227
|
+
};
|
|
5228
|
+
}
|
|
5229
|
+
}
|
|
5230
|
+
if (worker.status === "running" && !status.alive && !status.finalResult) {
|
|
5231
|
+
return {
|
|
5232
|
+
health: "orphaned",
|
|
5233
|
+
reason: "worker.json still running but process is dead"
|
|
5234
|
+
};
|
|
5235
|
+
}
|
|
5236
|
+
if (status.attention.state === "stale") {
|
|
5237
|
+
return { health: "stale", reason: status.attention.reason };
|
|
5238
|
+
}
|
|
5239
|
+
const hbMs = status.lastHeartbeatAt ? Date.parse(status.lastHeartbeatAt) : NaN;
|
|
5240
|
+
if (status.alive && Number.isFinite(hbMs) && Date.now() - hbMs > STALE_MS) {
|
|
5241
|
+
return {
|
|
5242
|
+
health: "stale",
|
|
5243
|
+
reason: `heartbeat older than ${Math.floor(STALE_MS / 1e3)}s`
|
|
5244
|
+
};
|
|
5245
|
+
}
|
|
5246
|
+
if (status.alive && worker.pid && !isPidAlive(worker.pid)) {
|
|
5247
|
+
return { health: "orphaned", reason: "pid recorded but process is not alive" };
|
|
5248
|
+
}
|
|
5249
|
+
if (taskLease?.status === "running" && !status.alive && status.finalResult) {
|
|
5250
|
+
return {
|
|
5251
|
+
health: "healthy",
|
|
5252
|
+
reason: "finished worker awaiting completion replay"
|
|
5253
|
+
};
|
|
5254
|
+
}
|
|
5255
|
+
return {
|
|
5256
|
+
health: "healthy",
|
|
5257
|
+
reason: status.attention.reason || "worker within expected lifecycle bounds"
|
|
5258
|
+
};
|
|
5259
|
+
}
|
|
5260
|
+
|
|
5261
|
+
// src/monitor/monitor.store.ts
|
|
5262
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync6, readdirSync as readdirSync7, unlinkSync as unlinkSync2 } from "node:fs";
|
|
5263
|
+
import path29 from "node:path";
|
|
5264
|
+
function monitorsDir() {
|
|
5265
|
+
const { harnessRoot } = getHarnessPaths();
|
|
5266
|
+
const dir = path29.join(harnessRoot, "monitors");
|
|
5267
|
+
mkdirSync6(dir, { recursive: true });
|
|
5268
|
+
return dir;
|
|
5269
|
+
}
|
|
5270
|
+
function monitorIdFor(runId, workerName) {
|
|
5271
|
+
return workerName ? `${safeSlug(runId)}--${safeSlug(workerName)}` : safeSlug(runId);
|
|
5272
|
+
}
|
|
5273
|
+
function monitorPath(monitorId) {
|
|
5274
|
+
return path29.join(monitorsDir(), `${monitorId}.json`);
|
|
5275
|
+
}
|
|
5276
|
+
function loadMonitorSession(monitorId) {
|
|
5277
|
+
return readJson(monitorPath(monitorId), void 0);
|
|
5278
|
+
}
|
|
5279
|
+
function saveMonitorSession(session) {
|
|
5280
|
+
writeJson(monitorPath(session.monitorId), session);
|
|
5281
|
+
}
|
|
5282
|
+
function deleteMonitorSession(monitorId) {
|
|
5283
|
+
const file = monitorPath(monitorId);
|
|
5284
|
+
if (!existsSync18(file)) return false;
|
|
5285
|
+
unlinkSync2(file);
|
|
5286
|
+
return true;
|
|
5287
|
+
}
|
|
5288
|
+
function listMonitorSessions() {
|
|
5289
|
+
const dir = monitorsDir();
|
|
5290
|
+
if (!existsSync18(dir)) return [];
|
|
5291
|
+
const entries = [];
|
|
5292
|
+
for (const name of readdirSync7(dir)) {
|
|
5293
|
+
if (!name.endsWith(".json")) continue;
|
|
5294
|
+
const session = readJson(
|
|
5295
|
+
path29.join(dir, name),
|
|
5296
|
+
void 0
|
|
5297
|
+
);
|
|
5298
|
+
if (!session?.monitorId) continue;
|
|
5299
|
+
entries.push({
|
|
5300
|
+
monitorId: session.monitorId,
|
|
5301
|
+
runId: session.runId,
|
|
5302
|
+
workerName: session.workerName,
|
|
5303
|
+
agentOsId: session.agentOsId,
|
|
5304
|
+
pid: session.pid,
|
|
5305
|
+
alive: session.pid ? isPidAlive(session.pid) : false,
|
|
5306
|
+
startedAt: session.startedAt,
|
|
5307
|
+
pollMs: session.pollMs,
|
|
5308
|
+
logPath: session.logPath
|
|
5309
|
+
});
|
|
5310
|
+
}
|
|
5311
|
+
return entries.sort((a, b) => a.startedAt.localeCompare(b.startedAt));
|
|
5312
|
+
}
|
|
5313
|
+
|
|
5314
|
+
// src/monitor/monitor.terminal.ts
|
|
5315
|
+
function assessAutoCompleteEligibility(input) {
|
|
5316
|
+
const { worker, status } = input;
|
|
5317
|
+
const blockers = [];
|
|
5318
|
+
if (worker.localOnly) {
|
|
5319
|
+
blockers.push("local-only worker (no board linkage)");
|
|
5320
|
+
}
|
|
5321
|
+
if (!worker.agentOsId || !worker.taskId) {
|
|
5322
|
+
blockers.push("missing agentOsId/taskId linkage");
|
|
5323
|
+
}
|
|
5324
|
+
if (hasCompletionAck(worker)) {
|
|
5325
|
+
blockers.push("completion already acknowledged");
|
|
5326
|
+
}
|
|
5327
|
+
if (worker.completionBlocker) {
|
|
5328
|
+
blockers.push(worker.completionBlocker);
|
|
5329
|
+
}
|
|
5330
|
+
if (status.heartbeatBlocker && status.alive) {
|
|
5331
|
+
blockers.push(`worker heartbeat blocker: ${status.heartbeatBlocker}`);
|
|
5332
|
+
}
|
|
5333
|
+
if (status.attention.state === "blocked") {
|
|
5334
|
+
blockers.push(status.attention.reason || "worker attention blocked");
|
|
5335
|
+
}
|
|
5336
|
+
if (isLandingBlockedWorkerStatus(status)) {
|
|
5337
|
+
blockers.push(status.attention.reason || "landing gate blocked");
|
|
5338
|
+
}
|
|
5339
|
+
const terminalVerified = isFinishedWorkerStatus(status);
|
|
5340
|
+
let terminalReason;
|
|
5341
|
+
if (terminalVerified) {
|
|
5342
|
+
if (status.finalResult) terminalReason = "final_result";
|
|
5343
|
+
else if (!status.alive) terminalReason = "process_exited";
|
|
5344
|
+
else terminalReason = "terminal_status";
|
|
5345
|
+
} else {
|
|
5346
|
+
blockers.push("worker has not reached a terminal condition");
|
|
5347
|
+
}
|
|
5348
|
+
const eligible = terminalVerified && blockers.length === 0;
|
|
5349
|
+
return {
|
|
5350
|
+
eligible,
|
|
5351
|
+
terminalVerified,
|
|
5352
|
+
terminalReason,
|
|
5353
|
+
blockers
|
|
5354
|
+
};
|
|
5355
|
+
}
|
|
5356
|
+
|
|
5357
|
+
// src/monitor/monitor.task-lease.ts
|
|
5358
|
+
async function fetchTaskLeasesForWorkers(input) {
|
|
5359
|
+
const out = /* @__PURE__ */ new Map();
|
|
5360
|
+
const agentOsId = input.agentOsId?.trim();
|
|
5361
|
+
if (!agentOsId || input.taskIds.length === 0) return out;
|
|
5362
|
+
const base = resolveBaseUrl(input.baseUrl);
|
|
5363
|
+
try {
|
|
5364
|
+
const secret = await resolveCallbackSecretWithMint(input.secret, agentOsId, { baseUrl: base });
|
|
5365
|
+
const url = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/harness/monitor/task-leases`;
|
|
5366
|
+
const res = await postJsonWithCredentialRefresh(
|
|
5367
|
+
url,
|
|
5368
|
+
secret,
|
|
5369
|
+
{ taskIds: [...new Set(input.taskIds)] },
|
|
5370
|
+
{ agentOsId, baseUrl: base }
|
|
5371
|
+
);
|
|
5372
|
+
if (!res.ok || !res.response || typeof res.response !== "object") return out;
|
|
5373
|
+
const rows = res.response.tasks;
|
|
5374
|
+
if (!Array.isArray(rows)) return out;
|
|
5375
|
+
for (const row of rows) {
|
|
5376
|
+
if (row?.taskId) out.set(row.taskId, row);
|
|
5377
|
+
}
|
|
5378
|
+
} catch {
|
|
5379
|
+
}
|
|
5380
|
+
return out;
|
|
5381
|
+
}
|
|
5382
|
+
|
|
5383
|
+
// src/monitor/monitor.service.ts
|
|
5384
|
+
function workerRecord2(runId, name) {
|
|
5385
|
+
return readJson(
|
|
5386
|
+
path30.join(runDirectory(runId), "workers", safeSlug(name), "worker.json"),
|
|
5387
|
+
void 0
|
|
5388
|
+
);
|
|
5389
|
+
}
|
|
5390
|
+
function workerNamesForRun(runId, scope) {
|
|
5391
|
+
const run = loadRun(runId);
|
|
5392
|
+
const names = Object.keys(run.workers || {});
|
|
5393
|
+
if (!scope) return names;
|
|
5394
|
+
const wanted = safeSlug(scope);
|
|
5395
|
+
return names.filter((n) => safeSlug(n) === wanted);
|
|
5396
|
+
}
|
|
5397
|
+
function buildWorkerView(worker, taskLeases) {
|
|
5398
|
+
const run = loadRun(worker.runId);
|
|
5399
|
+
const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
|
|
5400
|
+
const taskLease = worker.taskId ? taskLeases.get(worker.taskId) ?? null : null;
|
|
5401
|
+
const health = classifyWorkerHealth({ worker, status, taskLease });
|
|
5402
|
+
const autoComplete = assessAutoCompleteEligibility({ worker, status });
|
|
5403
|
+
return {
|
|
5404
|
+
runId: worker.runId,
|
|
5405
|
+
worker: worker.name,
|
|
5406
|
+
health: health.health,
|
|
5407
|
+
healthReason: health.reason,
|
|
5408
|
+
workerStatus: status.status,
|
|
5409
|
+
attentionState: status.attention.state,
|
|
5410
|
+
attentionReason: status.attention.reason,
|
|
5411
|
+
alive: status.alive,
|
|
5412
|
+
taskId: worker.taskId,
|
|
5413
|
+
leaseOwner: taskLease?.leaseOwner ?? void 0,
|
|
5414
|
+
taskStatus: taskLease?.status,
|
|
5415
|
+
autoComplete,
|
|
5416
|
+
status
|
|
5417
|
+
};
|
|
5418
|
+
}
|
|
5419
|
+
async function runMonitorTick(args) {
|
|
5420
|
+
const runId = String(args.run || "");
|
|
5421
|
+
required(runId, "--run");
|
|
5422
|
+
const scope = args.name ? String(args.name) : void 0;
|
|
5423
|
+
const agentOsId = args.agentOsId ? String(args.agentOsId) : void 0;
|
|
5424
|
+
const run = loadRun(runId);
|
|
5425
|
+
const names = workerNamesForRun(runId, scope);
|
|
5426
|
+
const workers = [];
|
|
5427
|
+
for (const name of names) {
|
|
5428
|
+
const worker = workerRecord2(runId, name);
|
|
5429
|
+
if (worker) workers.push(worker);
|
|
5430
|
+
}
|
|
5431
|
+
const resolvedAgentOsId = agentOsId || workers.map((w) => w.agentOsId).find((id) => typeof id === "string" && id.trim()) || void 0;
|
|
5432
|
+
const taskIds = workers.map((w) => w.taskId).filter((id) => Boolean(id));
|
|
5433
|
+
const taskLeases = await fetchTaskLeasesForWorkers({
|
|
5434
|
+
agentOsId: resolvedAgentOsId,
|
|
5435
|
+
taskIds,
|
|
5436
|
+
baseUrl: args.baseUrl ? String(args.baseUrl) : void 0,
|
|
5437
|
+
secret: args.secret ? String(args.secret) : void 0
|
|
5438
|
+
});
|
|
5439
|
+
const views = workers.map((w) => buildWorkerView(w, taskLeases));
|
|
5440
|
+
let leaseRenewal;
|
|
5441
|
+
if (resolvedAgentOsId && args.renewLeases !== false && args.renewLeases !== "false") {
|
|
5442
|
+
leaseRenewal = await renewActiveTaskLeases(runId, {
|
|
5443
|
+
...args,
|
|
5444
|
+
agentOsId: resolvedAgentOsId
|
|
5445
|
+
});
|
|
5446
|
+
}
|
|
5447
|
+
const autoCompleted = [];
|
|
5448
|
+
const shouldAutoComplete = args.autoComplete === true || args.autoComplete === "true";
|
|
5449
|
+
if (shouldAutoComplete) {
|
|
5450
|
+
for (const view of views) {
|
|
5451
|
+
if (!view.autoComplete.eligible) {
|
|
5452
|
+
autoCompleted.push({
|
|
5453
|
+
worker: view.worker,
|
|
5454
|
+
outcome: "skipped",
|
|
5455
|
+
ok: false,
|
|
5456
|
+
reason: view.autoComplete.blockers.join("; ") || "not eligible"
|
|
5457
|
+
});
|
|
5458
|
+
continue;
|
|
5459
|
+
}
|
|
5460
|
+
const outcome = await autoCompleteWorker({
|
|
5461
|
+
run: runId,
|
|
5462
|
+
name: view.worker,
|
|
5463
|
+
...resolvedAgentOsId ? { agentOsId: resolvedAgentOsId } : {},
|
|
5464
|
+
...args.baseUrl ? { baseUrl: String(args.baseUrl) } : {},
|
|
5465
|
+
...args.secret ? { secret: String(args.secret) } : {}
|
|
5466
|
+
});
|
|
5467
|
+
autoCompleted.push({
|
|
5468
|
+
worker: view.worker,
|
|
5469
|
+
outcome: outcome.outcome,
|
|
5470
|
+
ok: outcome.outcome === "completed",
|
|
5471
|
+
reason: outcome.reason
|
|
5472
|
+
});
|
|
5473
|
+
}
|
|
5474
|
+
}
|
|
5475
|
+
return {
|
|
5476
|
+
runId,
|
|
5477
|
+
agentOsId: resolvedAgentOsId,
|
|
5478
|
+
workers: views,
|
|
5479
|
+
leaseRenewal,
|
|
5480
|
+
autoCompleted
|
|
5481
|
+
};
|
|
5482
|
+
}
|
|
5483
|
+
function getMonitorStatus(args) {
|
|
5484
|
+
const runId = String(args.run || "");
|
|
5485
|
+
required(runId, "--run");
|
|
5486
|
+
const scope = args.name ? String(args.name) : void 0;
|
|
5487
|
+
const names = workerNamesForRun(runId, scope);
|
|
5488
|
+
const workers = [];
|
|
5489
|
+
for (const name of names) {
|
|
5490
|
+
const worker = workerRecord2(runId, name);
|
|
5491
|
+
if (!worker) continue;
|
|
5492
|
+
workers.push(buildWorkerView(worker, /* @__PURE__ */ new Map()));
|
|
5493
|
+
}
|
|
5494
|
+
return { runId, workers, autoCompleted: [] };
|
|
5495
|
+
}
|
|
5496
|
+
function listMonitors() {
|
|
5497
|
+
return listMonitorSessions();
|
|
5498
|
+
}
|
|
5499
|
+
function stopMonitor(args) {
|
|
5500
|
+
const runId = String(args.run || "");
|
|
5501
|
+
required(runId, "--run");
|
|
5502
|
+
const monitorId = monitorIdFor(runId, args.name ? String(args.name) : void 0);
|
|
5503
|
+
const session = loadMonitorSession(monitorId);
|
|
5504
|
+
if (!session) {
|
|
5505
|
+
return { monitorId, stopped: false };
|
|
5506
|
+
}
|
|
5507
|
+
if (session.pid && isPidAlive(session.pid)) {
|
|
5508
|
+
try {
|
|
5509
|
+
process.kill(session.pid, "SIGTERM");
|
|
5510
|
+
} catch {
|
|
5511
|
+
}
|
|
5512
|
+
}
|
|
5513
|
+
session.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5514
|
+
saveMonitorSession(session);
|
|
5515
|
+
deleteMonitorSession(monitorId);
|
|
5516
|
+
return { monitorId, stopped: true, pid: session.pid };
|
|
5517
|
+
}
|
|
5518
|
+
async function monitorAutoCompleteCli(args) {
|
|
5519
|
+
const runId = String(args.run || "");
|
|
5520
|
+
const name = String(args.name || "");
|
|
5521
|
+
required(runId, "--run");
|
|
5522
|
+
required(name, "--name");
|
|
5523
|
+
const worker = loadWorker(runId, name);
|
|
5524
|
+
const run = loadRun(runId);
|
|
5525
|
+
const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
|
|
5526
|
+
const assessment = assessAutoCompleteEligibility({ worker, status });
|
|
5527
|
+
if (!assessment.eligible) {
|
|
5528
|
+
console.log(
|
|
5529
|
+
JSON.stringify(
|
|
5530
|
+
{
|
|
5531
|
+
runId,
|
|
5532
|
+
worker: name,
|
|
5533
|
+
outcome: "blocked",
|
|
5534
|
+
blockers: assessment.blockers,
|
|
5535
|
+
terminalVerified: assessment.terminalVerified
|
|
5536
|
+
},
|
|
5537
|
+
null,
|
|
5538
|
+
2
|
|
5539
|
+
)
|
|
5540
|
+
);
|
|
5541
|
+
process.exitCode = 1;
|
|
5542
|
+
return;
|
|
5543
|
+
}
|
|
5544
|
+
const outcome = await autoCompleteWorker({
|
|
5545
|
+
...args,
|
|
5546
|
+
run: runId,
|
|
5547
|
+
name
|
|
5548
|
+
});
|
|
5549
|
+
console.log(JSON.stringify(outcome, null, 2));
|
|
5550
|
+
if (outcome.outcome !== "completed" && outcome.outcome !== "blocked") {
|
|
5551
|
+
process.exitCode = 1;
|
|
5552
|
+
}
|
|
5553
|
+
}
|
|
5554
|
+
|
|
5555
|
+
// src/monitor/monitor-loop.ts
|
|
5556
|
+
var DEFAULT_POLL_MS2 = 5e3;
|
|
5557
|
+
var DEFAULT_MAX_TOTAL_MS2 = 6 * 60 * 60 * 1e3;
|
|
5558
|
+
async function runMonitorLoop(args) {
|
|
5559
|
+
const monitorId = String(args.monitorId || "");
|
|
5560
|
+
const pollMs = Number(args.pollMs) > 0 ? Math.floor(Number(args.pollMs)) : DEFAULT_POLL_MS2;
|
|
5561
|
+
const maxTotalMs = Number(args.maxTotalMs) > 0 ? Math.floor(Number(args.maxTotalMs)) : DEFAULT_MAX_TOTAL_MS2;
|
|
5562
|
+
const startMs = Date.now();
|
|
5563
|
+
while (Date.now() - startMs <= maxTotalMs) {
|
|
5564
|
+
const session = monitorId ? loadMonitorSession(monitorId) : void 0;
|
|
5565
|
+
if (session?.stoppedAt) break;
|
|
5566
|
+
const tick = await runMonitorTick({
|
|
5567
|
+
...args,
|
|
5568
|
+
autoComplete: args.autoComplete ?? true,
|
|
5569
|
+
renewLeases: args.renewLeases ?? true
|
|
5570
|
+
});
|
|
5571
|
+
console.log(JSON.stringify({ monitorId, phase: "tick", ...tick }));
|
|
5572
|
+
const allTerminal = tick.workers.length > 0 && tick.workers.every(
|
|
5573
|
+
(w) => w.autoComplete.terminalVerified && (w.autoComplete.eligible || w.autoComplete.blockers.some((b) => b.includes("already acknowledged")))
|
|
5574
|
+
);
|
|
5575
|
+
if (allTerminal && tick.autoCompleted.every((a) => a.ok || a.outcome === "skipped")) {
|
|
5576
|
+
if (monitorId && session) {
|
|
5577
|
+
session.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5578
|
+
saveMonitorSession(session);
|
|
5579
|
+
}
|
|
5580
|
+
break;
|
|
5581
|
+
}
|
|
5582
|
+
sleepMs(pollMs);
|
|
5583
|
+
}
|
|
5584
|
+
}
|
|
5585
|
+
|
|
5586
|
+
// src/monitor/monitor-spawn.ts
|
|
5587
|
+
import { spawn as spawn4 } from "node:child_process";
|
|
5588
|
+
import { closeSync as closeSync4, existsSync as existsSync19, openSync as openSync4 } from "node:fs";
|
|
5589
|
+
import path31 from "node:path";
|
|
5590
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
5591
|
+
function resolveDefaultCliPath2() {
|
|
5592
|
+
return path31.join(fileURLToPath3(new URL(".", import.meta.url)), "..", "cli.js");
|
|
5593
|
+
}
|
|
5594
|
+
function spawnMonitorSidecar(opts) {
|
|
5595
|
+
const cliPath = opts.cliPath ?? resolveDefaultCliPath2();
|
|
5596
|
+
if (!existsSync19(cliPath)) return void 0;
|
|
5597
|
+
const monitorId = monitorIdFor(opts.runId, opts.workerName);
|
|
5598
|
+
const { harnessRoot } = getHarnessPaths();
|
|
5599
|
+
const logPath = path31.join(harnessRoot, "monitors", `${monitorId}.log`);
|
|
5600
|
+
let logFd;
|
|
5601
|
+
try {
|
|
5602
|
+
logFd = openSync4(logPath, "a");
|
|
5603
|
+
} catch {
|
|
5604
|
+
logFd = void 0;
|
|
5605
|
+
}
|
|
5606
|
+
const nodeExecutable = opts.nodeExecutable ?? process.execPath;
|
|
5607
|
+
const pollMs = opts.pollMs ?? 5e3;
|
|
5608
|
+
const args = [
|
|
5609
|
+
cliPath,
|
|
5610
|
+
"monitor",
|
|
5611
|
+
"run-loop",
|
|
5612
|
+
"--run",
|
|
5613
|
+
opts.runId,
|
|
5614
|
+
"--monitor-id",
|
|
5615
|
+
monitorId,
|
|
5616
|
+
"--poll-ms",
|
|
5617
|
+
String(pollMs),
|
|
5618
|
+
"--auto-complete",
|
|
5619
|
+
"true",
|
|
5620
|
+
"--renew-leases",
|
|
5621
|
+
"true"
|
|
5622
|
+
];
|
|
5623
|
+
if (opts.workerName) args.push("--name", opts.workerName);
|
|
5624
|
+
if (opts.agentOsId) args.push("--agent-os-id", opts.agentOsId);
|
|
5625
|
+
if (opts.baseUrl) args.push("--base-url", opts.baseUrl);
|
|
5626
|
+
if (opts.secret) args.push("--secret", opts.secret);
|
|
5627
|
+
const stdio = [
|
|
5628
|
+
"ignore",
|
|
5629
|
+
logFd ?? "ignore",
|
|
5630
|
+
logFd ?? "ignore"
|
|
5631
|
+
];
|
|
5632
|
+
try {
|
|
5633
|
+
const child = spawn4(
|
|
5634
|
+
nodeExecutable,
|
|
5635
|
+
args,
|
|
5636
|
+
hiddenSpawnOptions({
|
|
5637
|
+
detached: true,
|
|
5638
|
+
stdio,
|
|
5639
|
+
env: process.env
|
|
5640
|
+
})
|
|
5641
|
+
);
|
|
5642
|
+
if (logFd !== void 0) closeSync4(logFd);
|
|
5643
|
+
child.unref();
|
|
5644
|
+
const session = {
|
|
5645
|
+
monitorId,
|
|
5646
|
+
runId: opts.runId,
|
|
5647
|
+
workerName: opts.workerName,
|
|
5648
|
+
agentOsId: opts.agentOsId,
|
|
5649
|
+
pid: child.pid,
|
|
5650
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5651
|
+
pollMs,
|
|
5652
|
+
logPath
|
|
5653
|
+
};
|
|
5654
|
+
saveMonitorSession(session);
|
|
5655
|
+
return { monitorId, pid: child.pid, logPath, session };
|
|
5656
|
+
} catch {
|
|
5657
|
+
if (logFd !== void 0) {
|
|
5658
|
+
try {
|
|
5659
|
+
closeSync4(logFd);
|
|
5660
|
+
} catch {
|
|
5661
|
+
}
|
|
5662
|
+
}
|
|
5663
|
+
return void 0;
|
|
5664
|
+
}
|
|
5665
|
+
}
|
|
5666
|
+
|
|
5667
|
+
// src/monitor/monitor-cli.ts
|
|
5668
|
+
async function startMonitorCli(args) {
|
|
5669
|
+
const runId = String(args.run || "");
|
|
5670
|
+
required(runId, "--run");
|
|
5671
|
+
const workerName = args.name ? String(args.name) : void 0;
|
|
5672
|
+
const monitorId = monitorIdFor(runId, workerName);
|
|
5673
|
+
const existing = loadMonitorSession(monitorId);
|
|
5674
|
+
if (existing?.pid && !existing.stoppedAt) {
|
|
5675
|
+
return { monitorId, session: existing, spawned: false, pid: existing.pid };
|
|
5676
|
+
}
|
|
5677
|
+
const spawned = spawnMonitorSidecar({
|
|
5678
|
+
runId,
|
|
5679
|
+
workerName,
|
|
5680
|
+
agentOsId: args.agentOsId ? String(args.agentOsId) : void 0,
|
|
5681
|
+
pollMs: Number(args.pollMs) > 0 ? Math.floor(Number(args.pollMs)) : void 0,
|
|
5682
|
+
baseUrl: args.baseUrl ? String(args.baseUrl) : void 0,
|
|
5683
|
+
secret: args.secret ? String(args.secret) : void 0
|
|
5684
|
+
});
|
|
5685
|
+
if (!spawned) {
|
|
5686
|
+
throw new Error("failed to spawn monitor sidecar (cli.js missing or spawn error)");
|
|
5687
|
+
}
|
|
5688
|
+
return {
|
|
5689
|
+
monitorId,
|
|
5690
|
+
session: spawned.session,
|
|
5691
|
+
spawned: true,
|
|
5692
|
+
pid: spawned.pid
|
|
5693
|
+
};
|
|
5694
|
+
}
|
|
5695
|
+
async function monitorStatusCli(args) {
|
|
5696
|
+
const runId = String(args.run || "");
|
|
5697
|
+
if (runId) {
|
|
5698
|
+
const tick = args.tick === true || args.tick === "true" ? await runMonitorTick({ ...args, autoComplete: false }) : getMonitorStatus(args);
|
|
5699
|
+
console.log(JSON.stringify(tick, null, 2));
|
|
5700
|
+
return;
|
|
5701
|
+
}
|
|
5702
|
+
console.log(JSON.stringify({ monitors: listMonitors() }, null, 2));
|
|
5703
|
+
}
|
|
5704
|
+
function monitorStopCli(args) {
|
|
5705
|
+
console.log(JSON.stringify(stopMonitor(args), null, 2));
|
|
5706
|
+
}
|
|
5707
|
+
function monitorListCli() {
|
|
5708
|
+
console.log(JSON.stringify({ monitors: listMonitors() }, null, 2));
|
|
5709
|
+
}
|
|
5710
|
+
async function monitorRunLoopCli(args) {
|
|
5711
|
+
await runMonitorLoop(args);
|
|
5712
|
+
}
|
|
5713
|
+
async function monitorTickCli(args) {
|
|
5714
|
+
const tick = await runMonitorTick(args);
|
|
5715
|
+
console.log(JSON.stringify(tick, null, 2));
|
|
5716
|
+
}
|
|
5717
|
+
|
|
4211
5718
|
// src/cli.ts
|
|
4212
5719
|
function isHelpFlag(arg) {
|
|
4213
5720
|
return arg === "help" || arg === "--help" || arg === "-h";
|
|
@@ -4239,17 +5746,28 @@ function usage(code = 0) {
|
|
|
4239
5746
|
" 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
5747
|
" 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
5748
|
" kynver plan verify --plan PLAN_ID [--worktree PATH] [--task TASK_ID] [--human-override]",
|
|
4242
|
-
" kynver
|
|
5749
|
+
" 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]",
|
|
5750
|
+
" kynver plan outbox list",
|
|
5751
|
+
" kynver plan outbox drain [--max N] [--id OUTBOX_ID]",
|
|
5752
|
+
" kynver cleanup [--execute] [--node-modules-age-ms MS] [--worktrees-age-ms MS] [--harness-root PATH] [--include-orphans]",
|
|
5753
|
+
" kynver monitor start --run RUN_ID [--name worker] [--agent-os-id AOS_ID] [--poll-ms MS]",
|
|
5754
|
+
" kynver monitor status [--run RUN_ID] [--name worker] [--tick]",
|
|
5755
|
+
" kynver monitor stop --run RUN_ID [--name worker]",
|
|
5756
|
+
" kynver monitor list",
|
|
5757
|
+
" kynver monitor tick --run RUN_ID [--name worker] [--agent-os-id AOS_ID] [--auto-complete] [--renew-leases]",
|
|
5758
|
+
" kynver monitor auto-complete --run RUN_ID --name worker [--agent-os-id AOS_ID] [--base-url URL] [--secret SECRET]",
|
|
5759
|
+
" 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
5760
|
].join("\n")
|
|
4244
5761
|
);
|
|
4245
5762
|
process.exit(code);
|
|
4246
5763
|
}
|
|
4247
5764
|
async function main(argv = process.argv.slice(2)) {
|
|
5765
|
+
if (handleCliVersionFlag(argv, import.meta.url, "kynver")) return;
|
|
4248
5766
|
if (argv.length === 0 || isHelpFlag(argv[0])) return usage(0);
|
|
4249
5767
|
const scope = argv.shift();
|
|
4250
5768
|
let action;
|
|
4251
5769
|
let rest;
|
|
4252
|
-
if (scope === "run" || scope === "worker" || scope === "plan" || scope === "runner") {
|
|
5770
|
+
if (scope === "run" || scope === "worker" || scope === "plan" || scope === "runner" || scope === "monitor") {
|
|
4253
5771
|
action = argv.shift();
|
|
4254
5772
|
rest = argv;
|
|
4255
5773
|
} else {
|
|
@@ -4258,14 +5776,21 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
4258
5776
|
if (action && isHelpFlag(action) || rest.some(isHelpFlag)) return usage(0);
|
|
4259
5777
|
const args = parseArgs(rest);
|
|
4260
5778
|
const { runsDir, worktreesDir } = getPaths();
|
|
4261
|
-
|
|
4262
|
-
|
|
5779
|
+
mkdirSync7(runsDir, { recursive: true });
|
|
5780
|
+
mkdirSync7(worktreesDir, { recursive: true });
|
|
4263
5781
|
if (scope === "login") return void await runLogin(args);
|
|
4264
5782
|
if (scope === "runner" && action === "credential") return void await mintRunnerCredential(args);
|
|
4265
5783
|
if (scope === "setup") return void await runSetup(args);
|
|
4266
5784
|
if (scope === "daemon") return void await runDaemon(args);
|
|
4267
5785
|
if (scope === "plan" && action === "progress") return void await emitPlanProgress(args);
|
|
4268
5786
|
if (scope === "plan" && action === "verify") return void await verifyPlan(args);
|
|
5787
|
+
if (scope === "plan" && action === "persist") return void await runPlanPersist(args);
|
|
5788
|
+
if (scope === "plan" && action === "outbox") {
|
|
5789
|
+
const outboxAction = rest.shift();
|
|
5790
|
+
if (outboxAction === "list") return void await runPlanOutboxList();
|
|
5791
|
+
if (outboxAction === "drain") return void await runPlanOutboxDrain(parseArgs(rest));
|
|
5792
|
+
unknownCommand("plan", `outbox ${outboxAction ?? ""}`.trim());
|
|
5793
|
+
}
|
|
4269
5794
|
if (scope === "cleanup") return runCleanupCli(args);
|
|
4270
5795
|
if (scope === "run" && action === "create") return createRun(args);
|
|
4271
5796
|
if (scope === "run" && action === "list") return listRuns();
|
|
@@ -4278,9 +5803,20 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
4278
5803
|
if (scope === "worker" && action === "stop") return stopWorker(args);
|
|
4279
5804
|
if (scope === "worker" && action === "complete") return void await completeWorker(args);
|
|
4280
5805
|
if (scope === "worker" && action === "auto-complete") return void await autoCompleteWorkerCli(args);
|
|
5806
|
+
if (scope === "monitor" && action === "start") {
|
|
5807
|
+
const result = await startMonitorCli(args);
|
|
5808
|
+
console.log(JSON.stringify(result, null, 2));
|
|
5809
|
+
return;
|
|
5810
|
+
}
|
|
5811
|
+
if (scope === "monitor" && action === "status") return void await monitorStatusCli(args);
|
|
5812
|
+
if (scope === "monitor" && action === "stop") return monitorStopCli(args);
|
|
5813
|
+
if (scope === "monitor" && action === "list") return monitorListCli();
|
|
5814
|
+
if (scope === "monitor" && action === "tick") return void await monitorTickCli(args);
|
|
5815
|
+
if (scope === "monitor" && action === "auto-complete") return void await monitorAutoCompleteCli(args);
|
|
5816
|
+
if (scope === "monitor" && action === "run-loop") return void await monitorRunLoopCli(args);
|
|
4281
5817
|
unknownCommand(scope, action);
|
|
4282
5818
|
}
|
|
4283
|
-
var isCliEntry = process.argv[1] && realpathSync.native(process.argv[1]) === realpathSync.native(
|
|
5819
|
+
var isCliEntry = process.argv[1] && realpathSync.native(process.argv[1]) === realpathSync.native(fileURLToPath4(import.meta.url));
|
|
4284
5820
|
if (isCliEntry) {
|
|
4285
5821
|
void main().catch((error) => {
|
|
4286
5822
|
console.error(error);
|
|
@@ -4289,25 +5825,37 @@ if (isCliEntry) {
|
|
|
4289
5825
|
}
|
|
4290
5826
|
export {
|
|
4291
5827
|
DEFAULT_DISPATCH_LEASE_MS,
|
|
5828
|
+
PACKAGE_VERSION,
|
|
5829
|
+
assessAutoCompleteEligibility,
|
|
4292
5830
|
assessExitedWorkerSalvage,
|
|
4293
5831
|
assessPrHandoffRequirement,
|
|
4294
5832
|
assessWorkerLanding,
|
|
5833
|
+
assessWorkerLandingContract,
|
|
4295
5834
|
autoCompleteWorker,
|
|
4296
5835
|
autoCompleteWorkerCli,
|
|
4297
5836
|
buildDispatchTaskText,
|
|
4298
5837
|
buildPrompt,
|
|
5838
|
+
classifyWorkerHealth,
|
|
4299
5839
|
completeWorker,
|
|
4300
5840
|
computeAttention,
|
|
4301
5841
|
computeWorkerStatus,
|
|
4302
5842
|
createRun,
|
|
4303
5843
|
deriveRunStatus,
|
|
4304
5844
|
dispatchRun,
|
|
5845
|
+
drainPlanOutbox,
|
|
4305
5846
|
ensurePrReadyHandoff,
|
|
5847
|
+
extractPlanOutboxFromTask,
|
|
4306
5848
|
extractPrUrlFromText,
|
|
5849
|
+
formatPlanOutboxHandoffBlock,
|
|
4307
5850
|
getHarnessPaths,
|
|
5851
|
+
getMonitorStatus,
|
|
5852
|
+
hashPlanBody,
|
|
4308
5853
|
isFinishedWorkerStatus,
|
|
4309
5854
|
isLandingBlockedWorkerStatus,
|
|
4310
5855
|
isTerminalHeartbeatPhase,
|
|
5856
|
+
landingContractAttentionReason,
|
|
5857
|
+
listMonitors,
|
|
5858
|
+
listOutboxItems,
|
|
4311
5859
|
listRuns,
|
|
4312
5860
|
loadUserConfig,
|
|
4313
5861
|
main,
|
|
@@ -4317,6 +5865,7 @@ export {
|
|
|
4317
5865
|
parseClaudeStream,
|
|
4318
5866
|
parseHarnessStream,
|
|
4319
5867
|
parseHeartbeat,
|
|
5868
|
+
persistPlan,
|
|
4320
5869
|
postJson,
|
|
4321
5870
|
preflightCursorModel,
|
|
4322
5871
|
redactHarness,
|
|
@@ -4325,9 +5874,11 @@ export {
|
|
|
4325
5874
|
resolveCallbackSecretWithMint,
|
|
4326
5875
|
resolveHarnessRoot,
|
|
4327
5876
|
runDaemon,
|
|
5877
|
+
runMonitorTick,
|
|
4328
5878
|
runStatus,
|
|
4329
5879
|
saveUserConfig,
|
|
4330
5880
|
spawnCompletionSidecar,
|
|
5881
|
+
spawnMonitorSidecar,
|
|
4331
5882
|
spawnWorkerProcess,
|
|
4332
5883
|
startWorker,
|
|
4333
5884
|
stopWorker,
|