@kynver-app/runtime 0.1.62 → 0.1.66
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/README.md +3 -3
- package/dist/box-resource-snapshot-shared.d.ts +2 -0
- package/dist/box-resource-snapshot.d.ts +45 -0
- package/dist/cleanup-active-worktrees.d.ts +14 -0
- package/dist/cleanup-build-cache-paths.d.ts +5 -0
- package/dist/cleanup-dependency-scan.d.ts +12 -0
- package/dist/cleanup-disk-pressure.d.ts +15 -0
- package/dist/cleanup-duplicate-worktrees.d.ts +7 -0
- package/dist/cleanup-execute.d.ts +4 -0
- package/dist/cleanup-guards-helpers.d.ts +1 -1
- package/dist/cleanup-guards.d.ts +13 -0
- package/dist/cleanup-harness-roots.d.ts +6 -0
- package/dist/cleanup-retention-config.d.ts +3 -0
- package/dist/cleanup-scan.d.ts +1 -0
- package/dist/cleanup-types.d.ts +12 -1
- package/dist/cleanup-worktree-index.d.ts +2 -0
- package/dist/cli.js +1639 -558
- package/dist/cli.js.map +4 -4
- package/dist/harness-notice/harness-notice.auto-complete.d.ts +3 -0
- package/dist/harness-notice/harness-notice.monitor-tick.d.ts +6 -0
- package/dist/harness-notice/harness-notice.parse.d.ts +3 -0
- package/dist/harness-notice/harness-notice.tool-response.d.ts +3 -0
- package/dist/harness-notice/harness-notice.types.d.ts +17 -0
- package/dist/harness-notice/harness-notice.worker-complete.d.ts +3 -0
- package/dist/harness-notice/harness-notice.worker-status.d.ts +2 -0
- package/dist/harness-notice/index.d.ts +7 -0
- package/dist/harness-repair-target.d.ts +23 -0
- package/dist/heartbeat.d.ts +3 -0
- package/dist/index.d.ts +9 -2
- package/dist/index.js +2079 -700
- package/dist/index.js.map +4 -4
- package/dist/landing-contract-gate.d.ts +3 -1
- package/dist/model-routing.d.ts +2 -0
- package/dist/pr-handoff/pr-handoff-assess.d.ts +2 -0
- package/dist/pr-handoff/pr-handoff.types.d.ts +1 -1
- package/dist/prompt.d.ts +2 -0
- package/dist/repair-target-worktree.d.ts +4 -0
- package/dist/resource-gate.d.ts +5 -0
- package/dist/run-list.d.ts +38 -0
- package/dist/run-store.d.ts +2 -0
- package/dist/runner-identity.d.ts +12 -0
- package/dist/scheduler-cutover-cli.d.ts +2 -0
- package/dist/scheduler-cutover.d.ts +22 -0
- package/dist/status.d.ts +3 -0
- package/dist/supervisor.d.ts +2 -0
- package/dist/vercel/index.d.ts +1 -1
- package/dist/vercel/vercel-url.d.ts +2 -0
- package/dist/worker-provider-policy.d.ts +26 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -120,10 +120,6 @@ function tailFile(file, lines) {
|
|
|
120
120
|
function readMaybeFile(file) {
|
|
121
121
|
return file ? readFileSync2(path.resolve(file), "utf8") : "";
|
|
122
122
|
}
|
|
123
|
-
function listRunIds(runsDir) {
|
|
124
|
-
if (!existsSync2(runsDir)) return [];
|
|
125
|
-
return readdirSync(runsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
126
|
-
}
|
|
127
123
|
function sleepMs(ms) {
|
|
128
124
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
129
125
|
}
|
|
@@ -661,7 +657,7 @@ async function runSetup(args) {
|
|
|
661
657
|
...existing,
|
|
662
658
|
...inferSetupFields(existing, args),
|
|
663
659
|
...maxWorkersRaw ? { maxConcurrentWorkers: Math.max(1, Math.floor(Number(maxWorkersRaw))) } : {},
|
|
664
|
-
workerProvider: typeof args.provider === "string" ? args.provider : existing.workerProvider || "
|
|
660
|
+
workerProvider: typeof args.provider === "string" ? args.provider : existing.workerProvider || "cursor"
|
|
665
661
|
});
|
|
666
662
|
saveUserConfig(config);
|
|
667
663
|
let runnerCredentialNote;
|
|
@@ -780,23 +776,23 @@ function isWslHost() {
|
|
|
780
776
|
function observeWslHostDisk(options = {}) {
|
|
781
777
|
const wsl = options.forceWsl === void 0 ? isWslHost() : options.forceWsl;
|
|
782
778
|
if (!wsl) return null;
|
|
783
|
-
const
|
|
779
|
+
const path51 = options.wslHostMount?.trim() || process.env.KYNVER_WSL_HOST_MOUNT?.trim() || DEFAULT_WSL_HOST_MOUNT;
|
|
784
780
|
const warnBelowBytes = options.wslHostFreeWarnBytes ?? DEFAULT_WSL_HOST_WARN_FREE_BYTES;
|
|
785
781
|
const criticalBelowBytes = options.wslHostFreeCriticalBytes ?? DEFAULT_WSL_HOST_CRITICAL_FREE_BYTES;
|
|
786
782
|
const statfs = options.statfs ?? statfsSync;
|
|
787
783
|
let stats;
|
|
788
784
|
try {
|
|
789
|
-
stats = statfs(
|
|
785
|
+
stats = statfs(path51);
|
|
790
786
|
} catch (error) {
|
|
791
787
|
return {
|
|
792
788
|
ok: false,
|
|
793
|
-
path:
|
|
789
|
+
path: path51,
|
|
794
790
|
freeBytes: 0,
|
|
795
791
|
totalBytes: 0,
|
|
796
792
|
usedPercent: 100,
|
|
797
793
|
warnBelowBytes,
|
|
798
794
|
criticalBelowBytes,
|
|
799
|
-
reason: `Windows host disk probe failed at ${
|
|
795
|
+
reason: `Windows host disk probe failed at ${path51}: ${error.message}`,
|
|
800
796
|
probeError: error.message
|
|
801
797
|
};
|
|
802
798
|
}
|
|
@@ -810,11 +806,11 @@ function observeWslHostDisk(options = {}) {
|
|
|
810
806
|
let reason = null;
|
|
811
807
|
if (!ok) {
|
|
812
808
|
const tag = criticalFree ? "critical" : "warning";
|
|
813
|
-
reason = `Windows host disk ${
|
|
809
|
+
reason = `Windows host disk ${path51} at ${tag}: ${freeGiB} GiB free (<${(criticalFree ? criticalBelowBytes : warnBelowBytes) / 1024 / 1024 / 1024} GiB); WSL VHDX cannot grow safely. ${summarizeWslRecoverySteps()}`;
|
|
814
810
|
}
|
|
815
811
|
return {
|
|
816
812
|
ok,
|
|
817
|
-
path:
|
|
813
|
+
path: path51,
|
|
818
814
|
freeBytes,
|
|
819
815
|
totalBytes,
|
|
820
816
|
usedPercent,
|
|
@@ -834,12 +830,12 @@ var DEFAULT_CRITICAL_FREE_BYTES = 15 * 1024 * 1024 * 1024;
|
|
|
834
830
|
var DEFAULT_MAX_USED_PERCENT = 80;
|
|
835
831
|
var DEFAULT_HARD_MAX_USED_PERCENT = 90;
|
|
836
832
|
function observeRunnerDiskGate(input = {}) {
|
|
837
|
-
const
|
|
833
|
+
const path51 = input.diskPath?.trim() || "/";
|
|
838
834
|
const warnBelowBytes = input.diskFreeWarnBytes ?? DEFAULT_WARN_FREE_BYTES;
|
|
839
835
|
const criticalBelowBytes = input.diskFreeCriticalBytes ?? DEFAULT_CRITICAL_FREE_BYTES;
|
|
840
836
|
const maxUsedPercent = input.diskMaxUsedPercent ?? DEFAULT_MAX_USED_PERCENT;
|
|
841
837
|
const hardMaxUsedPercent = input.diskHardMaxUsedPercent ?? DEFAULT_HARD_MAX_USED_PERCENT;
|
|
842
|
-
const stats = statfsSync2(
|
|
838
|
+
const stats = statfsSync2(path51);
|
|
843
839
|
const freeBytes = Number(stats.bavail) * Number(stats.bsize);
|
|
844
840
|
const totalBytes = Number(stats.blocks) * Number(stats.bsize);
|
|
845
841
|
const usedPercent = totalBytes > 0 ? (totalBytes - freeBytes) / totalBytes * 100 : 100;
|
|
@@ -862,7 +858,7 @@ function observeRunnerDiskGate(input = {}) {
|
|
|
862
858
|
}
|
|
863
859
|
return {
|
|
864
860
|
ok,
|
|
865
|
-
path:
|
|
861
|
+
path: path51,
|
|
866
862
|
freeBytes,
|
|
867
863
|
totalBytes,
|
|
868
864
|
usedPercent,
|
|
@@ -942,6 +938,12 @@ function loadRun(id) {
|
|
|
942
938
|
}
|
|
943
939
|
function listRunRecords() {
|
|
944
940
|
const { runsDir } = getPaths();
|
|
941
|
+
return listRunRecordsAt(runsDir);
|
|
942
|
+
}
|
|
943
|
+
function listRunRecordsForHarnessRoot(harnessRoot) {
|
|
944
|
+
return listRunRecordsAt(path6.join(harnessRoot, "runs"));
|
|
945
|
+
}
|
|
946
|
+
function listRunRecordsAt(runsDir) {
|
|
945
947
|
if (!existsSync7(runsDir)) return [];
|
|
946
948
|
const runs = [];
|
|
947
949
|
for (const entry of readdirSync2(runsDir, { withFileTypes: true })) {
|
|
@@ -970,7 +972,10 @@ function saveWorker(runId, worker) {
|
|
|
970
972
|
}
|
|
971
973
|
function runDirectory(id) {
|
|
972
974
|
const { runsDir } = getPaths();
|
|
973
|
-
return
|
|
975
|
+
return runDirectoryAt(runsDir, id);
|
|
976
|
+
}
|
|
977
|
+
function runDirectoryAt(harnessRoot, id) {
|
|
978
|
+
return runDir(path6.join(harnessRoot, "runs"), safeSlug(id));
|
|
974
979
|
}
|
|
975
980
|
|
|
976
981
|
// src/heartbeat.ts
|
|
@@ -991,7 +996,9 @@ function parseHeartbeat(file) {
|
|
|
991
996
|
lastHeartbeatPhase: null,
|
|
992
997
|
lastHeartbeatSummary: null,
|
|
993
998
|
heartbeatBlocker: null,
|
|
994
|
-
timestampAnomalies: []
|
|
999
|
+
timestampAnomalies: [],
|
|
1000
|
+
lastBoxResourceSnapshot: null,
|
|
1001
|
+
lastPrEvidence: []
|
|
995
1002
|
};
|
|
996
1003
|
if (!existsSync8(file)) return result;
|
|
997
1004
|
const maxFutureMs = Date.now() + HEARTBEAT_FUTURE_SKEW_MS;
|
|
@@ -1018,6 +1025,14 @@ function parseHeartbeat(file) {
|
|
|
1018
1025
|
if (row.phase !== void 0 && row.phase !== null) result.lastHeartbeatPhase = String(row.phase);
|
|
1019
1026
|
if (row.summary !== void 0 && row.summary !== null) result.lastHeartbeatSummary = String(row.summary);
|
|
1020
1027
|
result.heartbeatBlocker = row.blocker ? String(row.blocker) : null;
|
|
1028
|
+
if (row.boxResourceSnapshot && typeof row.boxResourceSnapshot === "object" && !Array.isArray(row.boxResourceSnapshot)) {
|
|
1029
|
+
result.lastBoxResourceSnapshot = row.boxResourceSnapshot;
|
|
1030
|
+
}
|
|
1031
|
+
if (Array.isArray(row.prEvidence)) {
|
|
1032
|
+
result.lastPrEvidence = row.prEvidence.filter(
|
|
1033
|
+
(entry2) => !!entry2 && typeof entry2 === "object" && typeof entry2.prUrl === "string"
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1021
1036
|
}
|
|
1022
1037
|
return result;
|
|
1023
1038
|
}
|
|
@@ -1506,8 +1521,8 @@ function normalizePrUrl(url) {
|
|
|
1506
1521
|
}
|
|
1507
1522
|
function parseReconciliation(finalResult) {
|
|
1508
1523
|
if (!finalResult || typeof finalResult !== "object" || Array.isArray(finalResult)) return [];
|
|
1509
|
-
const
|
|
1510
|
-
const raw =
|
|
1524
|
+
const record3 = finalResult;
|
|
1525
|
+
const raw = record3.targetPrReconciliation ?? record3.target_pr_reconciliation;
|
|
1511
1526
|
if (!Array.isArray(raw)) return [];
|
|
1512
1527
|
const out = [];
|
|
1513
1528
|
for (const item of raw) {
|
|
@@ -1538,10 +1553,35 @@ function workerPrUrls(snapshot, finalResult) {
|
|
|
1538
1553
|
function assessWorkerLandingContract(input) {
|
|
1539
1554
|
const { contract, snapshot } = input;
|
|
1540
1555
|
const finalResult = input.finalResult ?? snapshot.finalResult;
|
|
1541
|
-
if (!contract.landingOnly && contract.targetPrUrls.length === 0) {
|
|
1556
|
+
if (!contract.landingOnly && contract.targetPrUrls.length === 0 && !contract.repairEnforceOriginalPr) {
|
|
1542
1557
|
return { blocked: false };
|
|
1543
1558
|
}
|
|
1544
1559
|
if (!hasFinalResult3(finalResult)) return { blocked: false };
|
|
1560
|
+
const repairTarget = contract.repairEnforceOriginalPr ? normalizePrUrl(trimOrNull3(contract.targetPrUrl) ?? "") ?? (contract.targetPrUrls.length === 1 ? normalizePrUrl(contract.targetPrUrls[0]) : null) : null;
|
|
1561
|
+
if (repairTarget) {
|
|
1562
|
+
const workerPrs2 = workerPrUrls(snapshot, finalResult);
|
|
1563
|
+
const supersedes = finalResult && typeof finalResult === "object" && !Array.isArray(finalResult) && finalResult.supersedesOriginalTargetPr === true;
|
|
1564
|
+
if (!supersedes) {
|
|
1565
|
+
for (const pr of workerPrs2) {
|
|
1566
|
+
if (pr !== repairTarget) {
|
|
1567
|
+
return {
|
|
1568
|
+
blocked: true,
|
|
1569
|
+
reason: "duplicate_repair_pr",
|
|
1570
|
+
detail: `Repair worker opened or attached PR ${pr} instead of canonical target ${repairTarget}`
|
|
1571
|
+
};
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
const reconciliation2 = parseReconciliation(finalResult);
|
|
1576
|
+
const entry = reconciliation2.find((r) => r.prUrl === repairTarget);
|
|
1577
|
+
if (!entry || entry.outcome !== "merged" && !(entry.reason?.trim() && (entry.outcome === "skipped" || entry.outcome === "blocked"))) {
|
|
1578
|
+
return {
|
|
1579
|
+
blocked: true,
|
|
1580
|
+
reason: "missing_repair_target_reconciliation",
|
|
1581
|
+
detail: `Repair worker must reconcile target PR ${repairTarget}`
|
|
1582
|
+
};
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1545
1585
|
const reconciliation = parseReconciliation(finalResult);
|
|
1546
1586
|
const byUrl = new Map(reconciliation.map((r) => [r.prUrl, r]));
|
|
1547
1587
|
const targetSet = new Set(
|
|
@@ -1696,6 +1736,12 @@ function computeWorkerStatus(worker, options = {}) {
|
|
|
1696
1736
|
]);
|
|
1697
1737
|
const error = parsed.error || (!alive && !finalResult ? tailFile(worker.stderrPath, 10).trim() || void 0 : void 0);
|
|
1698
1738
|
const completionBlocker = typeof worker.completionBlocker === "string" && worker.completionBlocker.trim() ? worker.completionBlocker.trim() : null;
|
|
1739
|
+
const landingContract = worker.repairTargetPrUrl ? {
|
|
1740
|
+
landingOnly: false,
|
|
1741
|
+
targetPrUrls: [worker.repairTargetPrUrl],
|
|
1742
|
+
targetPrUrl: worker.repairTargetPrUrl,
|
|
1743
|
+
repairEnforceOriginalPr: true
|
|
1744
|
+
} : null;
|
|
1699
1745
|
const attention = computeAttention({
|
|
1700
1746
|
alive,
|
|
1701
1747
|
finalResult,
|
|
@@ -1708,7 +1754,9 @@ function computeWorkerStatus(worker, options = {}) {
|
|
|
1708
1754
|
error,
|
|
1709
1755
|
changedFiles,
|
|
1710
1756
|
gitAncestry,
|
|
1711
|
-
completionBlocker
|
|
1757
|
+
completionBlocker,
|
|
1758
|
+
landingContract,
|
|
1759
|
+
prUrl: worker.repairTargetPrUrl ?? worker.taskPrUrl ?? null
|
|
1712
1760
|
});
|
|
1713
1761
|
const workerStatusLabel = completionBlocker || attention.state === "blocked" ? "blocked" : completionAcknowledged || attention.state === "done" ? "done" : finalResult ? "exited" : alive ? "running" : "exited";
|
|
1714
1762
|
return {
|
|
@@ -1830,9 +1878,16 @@ function observeRunnerResourceGate(input) {
|
|
|
1830
1878
|
const slotsByCapacity = Math.max(0, maxConcurrentWorkers - activeWorkers);
|
|
1831
1879
|
const slotsByFreeMem = capacityFromFree;
|
|
1832
1880
|
let slotsAvailable = Math.min(slotsByCapacity, slotsByFreeMem);
|
|
1881
|
+
const skipDisk = input.skipDiskGate || process.env.KYNVER_RESOURCE_GATE_SKIP_DISK === "1";
|
|
1882
|
+
const diskGate = skipDisk ? void 0 : observeRunnerDiskGate({
|
|
1883
|
+
diskPath: input.diskPath?.trim() || process.env.KYNVER_DISK_GUARD_PATH?.trim() || "/"
|
|
1884
|
+
});
|
|
1885
|
+
if (diskGate && !diskGate.ok) slotsAvailable = 0;
|
|
1833
1886
|
let reason = null;
|
|
1834
1887
|
if (slotsAvailable <= 0) {
|
|
1835
|
-
if (
|
|
1888
|
+
if (diskGate && !diskGate.ok) {
|
|
1889
|
+
reason = diskGate.reason ?? "disk gate blocked worker admission";
|
|
1890
|
+
} else if (activeWorkers >= maxConcurrentWorkers) {
|
|
1836
1891
|
reason = `at worker limit (${activeWorkers}/${maxConcurrentWorkers} running)`;
|
|
1837
1892
|
} else if (capacityFromFree <= 0) {
|
|
1838
1893
|
reason = "insufficient free memory \u2014 waiting for workers to finish";
|
|
@@ -1852,7 +1907,8 @@ function observeRunnerResourceGate(input) {
|
|
|
1852
1907
|
maxConcurrentWorkers,
|
|
1853
1908
|
activeWorkers,
|
|
1854
1909
|
slotsAvailable,
|
|
1855
|
-
reason
|
|
1910
|
+
reason,
|
|
1911
|
+
...diskGate ? { diskGate } : {}
|
|
1856
1912
|
};
|
|
1857
1913
|
}
|
|
1858
1914
|
|
|
@@ -2040,10 +2096,86 @@ var claudeProvider = {
|
|
|
2040
2096
|
}
|
|
2041
2097
|
};
|
|
2042
2098
|
|
|
2099
|
+
// src/worker-provider-policy.ts
|
|
2100
|
+
var DEFAULT_WORKER_PROVIDER = "cursor";
|
|
2101
|
+
var CLAUDE_FAMILY = /* @__PURE__ */ new Set(["claude", "opus", "anthropic"]);
|
|
2102
|
+
var TASK_OVERRIDE_MARKERS = [
|
|
2103
|
+
/\[worker-provider:\s*claude\]/i,
|
|
2104
|
+
/\[use-claude-worker\]/i,
|
|
2105
|
+
/\[operator-worker-provider:\s*claude\]/i
|
|
2106
|
+
];
|
|
2107
|
+
function taskString2(task, key) {
|
|
2108
|
+
const v = task[key];
|
|
2109
|
+
return typeof v === "string" ? v.trim() : "";
|
|
2110
|
+
}
|
|
2111
|
+
function isClaudeFamilyProvider(provider) {
|
|
2112
|
+
if (!provider?.trim()) return false;
|
|
2113
|
+
const normalized = provider.trim().toLowerCase();
|
|
2114
|
+
if (CLAUDE_FAMILY.has(normalized)) return true;
|
|
2115
|
+
return normalized.includes("claude") || normalized.includes("opus");
|
|
2116
|
+
}
|
|
2117
|
+
function taskAllowsClaudeWorker(task) {
|
|
2118
|
+
if (!task) return false;
|
|
2119
|
+
const override = task.workerProviderOverride;
|
|
2120
|
+
if (typeof override === "string" && isClaudeFamilyProvider(override)) return true;
|
|
2121
|
+
const ref = taskString2(task, "executorRef").toLowerCase();
|
|
2122
|
+
if (ref === "provider:claude" || ref.startsWith("provider:claude:")) return true;
|
|
2123
|
+
if (ref.includes("claude-worker-override") || ref.includes("operator-claude")) return true;
|
|
2124
|
+
const description = taskString2(task, "description");
|
|
2125
|
+
if (TASK_OVERRIDE_MARKERS.some((re) => re.test(description))) return true;
|
|
2126
|
+
const title = taskString2(task, "title");
|
|
2127
|
+
if (/\[use-claude-worker\]/i.test(title)) return true;
|
|
2128
|
+
return false;
|
|
2129
|
+
}
|
|
2130
|
+
function coerceCursorModel(model, ruleSuffix) {
|
|
2131
|
+
const coerced = {
|
|
2132
|
+
provider: DEFAULT_WORKER_PROVIDER,
|
|
2133
|
+
model: CURSOR_DEFAULT_MODEL,
|
|
2134
|
+
rule: `policy:cursor_default${ruleSuffix}`,
|
|
2135
|
+
requestedModel: model
|
|
2136
|
+
};
|
|
2137
|
+
return coerced;
|
|
2138
|
+
}
|
|
2139
|
+
function enforceCursorWorkerProvider(input) {
|
|
2140
|
+
const { routing, task } = input;
|
|
2141
|
+
const explicit = input.explicitProvider?.trim().toLowerCase();
|
|
2142
|
+
if (input.explicitProviderIsOperatorOverride && isClaudeFamilyProvider(explicit)) {
|
|
2143
|
+
return {
|
|
2144
|
+
...routing,
|
|
2145
|
+
provider: "claude",
|
|
2146
|
+
rule: routing.rule.startsWith("explicit:") ? routing.rule : "explicit:operator_provider"
|
|
2147
|
+
};
|
|
2148
|
+
}
|
|
2149
|
+
if (taskAllowsClaudeWorker(task)) {
|
|
2150
|
+
return routing;
|
|
2151
|
+
}
|
|
2152
|
+
if (!isClaudeFamilyProvider(routing.provider)) {
|
|
2153
|
+
return routing;
|
|
2154
|
+
}
|
|
2155
|
+
const suffix = routing.rule && routing.rule !== "default:global" ? `:${routing.rule.replace(/:/g, "_")}` : "";
|
|
2156
|
+
return coerceCursorModel(routing.model, suffix);
|
|
2157
|
+
}
|
|
2158
|
+
function resolveConfiguredWorkerProvider(configured, fallback = DEFAULT_WORKER_PROVIDER) {
|
|
2159
|
+
const trimmed = configured?.trim();
|
|
2160
|
+
if (!trimmed) return fallback;
|
|
2161
|
+
if (isClaudeFamilyProvider(trimmed)) return DEFAULT_WORKER_PROVIDER;
|
|
2162
|
+
return trimmed;
|
|
2163
|
+
}
|
|
2164
|
+
function preferCursorExecutor(executors) {
|
|
2165
|
+
const unique2 = [...new Set(executors.map((e) => e.trim().toLowerCase()).filter(Boolean))];
|
|
2166
|
+
if (unique2.includes(DEFAULT_WORKER_PROVIDER)) {
|
|
2167
|
+
return [...new Set(unique2.map((e) => isClaudeFamilyProvider(e) ? DEFAULT_WORKER_PROVIDER : e))];
|
|
2168
|
+
}
|
|
2169
|
+
if (unique2.every((e) => isClaudeFamilyProvider(e))) {
|
|
2170
|
+
return [DEFAULT_WORKER_PROVIDER];
|
|
2171
|
+
}
|
|
2172
|
+
return unique2;
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2043
2175
|
// src/model-routing.ts
|
|
2044
2176
|
var GLOBAL_DEFAULT_MODEL = "claude-sonnet-4-6";
|
|
2045
2177
|
var CURSOR_DEFAULT_MODEL = "composer-2.5";
|
|
2046
|
-
function
|
|
2178
|
+
function taskString3(task, key) {
|
|
2047
2179
|
const v = task[key];
|
|
2048
2180
|
return typeof v === "string" ? v.trim() : "";
|
|
2049
2181
|
}
|
|
@@ -2059,11 +2191,14 @@ function resolveGlobalDefaultModel(config = loadUserConfig()) {
|
|
|
2059
2191
|
}
|
|
2060
2192
|
function inferProviderFromModel(model) {
|
|
2061
2193
|
const m = (model ?? "").toLowerCase();
|
|
2062
|
-
if (!m) return "
|
|
2194
|
+
if (!m) return "cursor";
|
|
2063
2195
|
if (m.includes("composer") || m.includes("cursor") || m.includes("codex") || m.startsWith("gpt-") || m.startsWith("gpt5")) {
|
|
2064
2196
|
return "cursor";
|
|
2065
2197
|
}
|
|
2066
|
-
|
|
2198
|
+
if (/^claude[-_]/i.test(m) || /^(?:opus|sonnet|haiku)\b/i.test(m)) {
|
|
2199
|
+
return "claude";
|
|
2200
|
+
}
|
|
2201
|
+
return "cursor";
|
|
2067
2202
|
}
|
|
2068
2203
|
function normalizeProviderAliasModel(model, explicitProvider) {
|
|
2069
2204
|
const alias = model.trim().toLowerCase();
|
|
@@ -2097,41 +2232,33 @@ function isOpusLane(ref, title) {
|
|
|
2097
2232
|
return false;
|
|
2098
2233
|
}
|
|
2099
2234
|
function inferModelRoutingFromTask(task) {
|
|
2100
|
-
const ref = normalizeRef(
|
|
2101
|
-
const title =
|
|
2102
|
-
const priority =
|
|
2103
|
-
const roleLane = normalizeRef(
|
|
2235
|
+
const ref = normalizeRef(taskString3(task, "executorRef"));
|
|
2236
|
+
const title = taskString3(task, "title").toLowerCase();
|
|
2237
|
+
const priority = taskString3(task, "priority") || "normal";
|
|
2238
|
+
const roleLane = normalizeRef(taskString3(task, "roleLane"));
|
|
2104
2239
|
if (ref.includes("cursor") || ref.includes("codex") || ref.includes("composer") || ref.includes("copilot") || roleLane === "implementer" || roleLane === "repair_implementer") {
|
|
2105
2240
|
return { provider: "cursor", rule: "lane:implementation" };
|
|
2106
2241
|
}
|
|
2107
2242
|
if (ref.includes("landing") || title.startsWith("land:") || title.includes(" merge")) {
|
|
2108
|
-
return {
|
|
2109
|
-
model: "claude-haiku-4-5-20251001",
|
|
2110
|
-
provider: "claude",
|
|
2111
|
-
rule: "lane:landing"
|
|
2112
|
-
};
|
|
2243
|
+
return { provider: "cursor", rule: "lane:landing" };
|
|
2113
2244
|
}
|
|
2114
2245
|
if (ref.includes("review") || /^review[\s:]/.test(title) || roleLane.includes("review")) {
|
|
2115
2246
|
if (isOpusLane(ref, title) || roleLane === "deep_reviewer") {
|
|
2116
|
-
return {
|
|
2247
|
+
return { provider: "cursor", rule: "lane:deep_review" };
|
|
2117
2248
|
}
|
|
2118
|
-
return {
|
|
2249
|
+
return { provider: "cursor", rule: "lane:review" };
|
|
2119
2250
|
}
|
|
2120
2251
|
if (isOpusLane(ref, title) || roleLane === "plan_author") {
|
|
2121
|
-
return {
|
|
2252
|
+
return { provider: "cursor", rule: "lane:planning" };
|
|
2122
2253
|
}
|
|
2123
2254
|
if (priority === "critical") {
|
|
2124
|
-
return {
|
|
2255
|
+
return { provider: "cursor", rule: "priority:critical" };
|
|
2125
2256
|
}
|
|
2126
2257
|
if (priority === "high") {
|
|
2127
|
-
return {
|
|
2258
|
+
return { provider: "cursor", rule: "priority:high" };
|
|
2128
2259
|
}
|
|
2129
2260
|
if (priority === "low") {
|
|
2130
|
-
return {
|
|
2131
|
-
model: "claude-haiku-4-5-20251001",
|
|
2132
|
-
provider: "claude",
|
|
2133
|
-
rule: "priority:low"
|
|
2134
|
-
};
|
|
2261
|
+
return { provider: "cursor", rule: "priority:low" };
|
|
2135
2262
|
}
|
|
2136
2263
|
const model = resolveGlobalDefaultModel();
|
|
2137
2264
|
return {
|
|
@@ -2141,31 +2268,41 @@ function inferModelRoutingFromTask(task) {
|
|
|
2141
2268
|
};
|
|
2142
2269
|
}
|
|
2143
2270
|
function resolveWorkerLaunch(input) {
|
|
2271
|
+
let decision;
|
|
2144
2272
|
if (input.explicitModel?.trim()) {
|
|
2145
|
-
const
|
|
2146
|
-
const providerAlias = normalizeProviderAliasModel(
|
|
2147
|
-
if (providerAlias)
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2273
|
+
const model = input.explicitModel.trim();
|
|
2274
|
+
const providerAlias = normalizeProviderAliasModel(model, input.explicitProvider);
|
|
2275
|
+
if (providerAlias) {
|
|
2276
|
+
decision = providerAlias;
|
|
2277
|
+
} else {
|
|
2278
|
+
decision = {
|
|
2279
|
+
model,
|
|
2280
|
+
provider: input.explicitProvider?.trim() || inferProviderFromModel(model),
|
|
2281
|
+
rule: "explicit:cli",
|
|
2282
|
+
requestedModel: model
|
|
2283
|
+
};
|
|
2284
|
+
}
|
|
2285
|
+
} else if (input.task && Object.keys(input.task).length > 0) {
|
|
2156
2286
|
const inferred = inferModelRoutingFromTask(input.task);
|
|
2157
|
-
|
|
2287
|
+
decision = {
|
|
2158
2288
|
...inferred,
|
|
2159
2289
|
requestedModel: inferred.model
|
|
2160
2290
|
};
|
|
2291
|
+
} else {
|
|
2292
|
+
const model = resolveGlobalDefaultModel();
|
|
2293
|
+
decision = {
|
|
2294
|
+
model,
|
|
2295
|
+
provider: input.explicitProvider?.trim() || inferProviderFromModel(model),
|
|
2296
|
+
rule: "default:global",
|
|
2297
|
+
requestedModel: model
|
|
2298
|
+
};
|
|
2161
2299
|
}
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
};
|
|
2300
|
+
return enforceCursorWorkerProvider({
|
|
2301
|
+
routing: decision,
|
|
2302
|
+
task: input.task,
|
|
2303
|
+
explicitProvider: input.explicitProvider,
|
|
2304
|
+
explicitProviderIsOperatorOverride: input.explicitProviderIsOperatorOverride
|
|
2305
|
+
});
|
|
2169
2306
|
}
|
|
2170
2307
|
function resolveModelFallback(startedModel, launchModel, providerDefault) {
|
|
2171
2308
|
return startedModel || launchModel || providerDefault || resolveGlobalDefaultModel() || CLAUDE_DEFAULT_MODEL;
|
|
@@ -2257,6 +2394,76 @@ function hasLiveWorkerForTask(runId, taskId) {
|
|
|
2257
2394
|
import { existsSync as existsSync13, mkdirSync as mkdirSync3 } from "node:fs";
|
|
2258
2395
|
import path14 from "node:path";
|
|
2259
2396
|
|
|
2397
|
+
// src/harness-repair-target.ts
|
|
2398
|
+
var HARNESS_CONTRACT_RE = /<!--\s*harness-contract:\s*(\{[\s\S]*?\})\s*-->/i;
|
|
2399
|
+
var FIX_EXECUTOR_REF_PREFIX = "next-action-fix:";
|
|
2400
|
+
function trimOrNull4(value) {
|
|
2401
|
+
if (typeof value !== "string") return null;
|
|
2402
|
+
const t = value.trim();
|
|
2403
|
+
return t.length ? t : null;
|
|
2404
|
+
}
|
|
2405
|
+
function normalizePrUrl2(url) {
|
|
2406
|
+
const m = url.trim().match(/github\.com\/([^/]+\/[^/]+)\/(?:pull|pulls)\/(\d+)/i);
|
|
2407
|
+
if (!m) return trimOrNull4(url);
|
|
2408
|
+
return `https://github.com/${m[1]}/pull/${m[2]}`;
|
|
2409
|
+
}
|
|
2410
|
+
function isHarnessRepairTask(task) {
|
|
2411
|
+
const title = (task.title ?? "").trim().toLowerCase();
|
|
2412
|
+
if (title.startsWith("fix:") || title.startsWith("repair:")) return true;
|
|
2413
|
+
const ref = (task.executorRef ?? "").toLowerCase();
|
|
2414
|
+
if (ref.startsWith(FIX_EXECUTOR_REF_PREFIX)) return true;
|
|
2415
|
+
if (ref.includes("repair") || ref.includes("unblock")) return true;
|
|
2416
|
+
return false;
|
|
2417
|
+
}
|
|
2418
|
+
function parseRepairTargetContractFromDescription(description) {
|
|
2419
|
+
const empty = {
|
|
2420
|
+
repairEnforceOriginalPr: false,
|
|
2421
|
+
targetPrUrl: null,
|
|
2422
|
+
targetPrBranch: null
|
|
2423
|
+
};
|
|
2424
|
+
if (!description) return empty;
|
|
2425
|
+
const m = description.match(HARNESS_CONTRACT_RE);
|
|
2426
|
+
if (!m?.[1]) return empty;
|
|
2427
|
+
try {
|
|
2428
|
+
const parsed = JSON.parse(m[1]);
|
|
2429
|
+
const url = trimOrNull4(
|
|
2430
|
+
String(parsed.targetPrUrl ?? parsed.target_pr_url ?? "")
|
|
2431
|
+
);
|
|
2432
|
+
const branch = trimOrNull4(
|
|
2433
|
+
String(parsed.targetPrBranch ?? parsed.target_pr_branch ?? "")
|
|
2434
|
+
);
|
|
2435
|
+
const enforce = parsed.repairEnforceOriginalPr === true || parsed.repair_enforce_original_pr === true;
|
|
2436
|
+
return {
|
|
2437
|
+
repairEnforceOriginalPr: enforce || Boolean(url),
|
|
2438
|
+
targetPrUrl: url ? normalizePrUrl2(url) : null,
|
|
2439
|
+
targetPrBranch: branch
|
|
2440
|
+
};
|
|
2441
|
+
} catch {
|
|
2442
|
+
return empty;
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
function resolveHarnessRepairTargetFromTask(task) {
|
|
2446
|
+
if (!isHarnessRepairTask(task)) return null;
|
|
2447
|
+
const block = parseRepairTargetContractFromDescription(task.description);
|
|
2448
|
+
const taskPr = task.prUrl ? normalizePrUrl2(task.prUrl) : null;
|
|
2449
|
+
const targetPrUrl = block.targetPrUrl ?? taskPr;
|
|
2450
|
+
if (!targetPrUrl) return null;
|
|
2451
|
+
return {
|
|
2452
|
+
targetPrUrl,
|
|
2453
|
+
targetPrBranch: block.targetPrBranch ?? trimOrNull4(task.branch)
|
|
2454
|
+
};
|
|
2455
|
+
}
|
|
2456
|
+
function repairTargetPromptLines(target) {
|
|
2457
|
+
return [
|
|
2458
|
+
"Repair target PR policy:",
|
|
2459
|
+
`- Work on the existing target PR branch \u2014 do not open a duplicate repair PR by default.`,
|
|
2460
|
+
`- Canonical target PR: ${target.targetPrUrl}`,
|
|
2461
|
+
...target.targetPrBranch ? [`- Canonical target branch: \`${target.targetPrBranch}\` (checkout is already on this branch).`] : [],
|
|
2462
|
+
`- Reconcile ${target.targetPrUrl} in structured finalResult.targetPrReconciliation.`,
|
|
2463
|
+
`- Only supersede the original when the branch is inaccessible: set supersedesOriginalTargetPr: true with reason and close/comment on the original PR.`
|
|
2464
|
+
];
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2260
2467
|
// src/prompt.ts
|
|
2261
2468
|
function buildPrompt(input) {
|
|
2262
2469
|
const ownership = input.ownedPaths.length ? `Owned paths: ${input.ownedPaths.join(", ")}. Do not edit outside these paths without stopping and reporting why.` : "Owned paths: unrestricted for this worker, but keep edits tightly scoped.";
|
|
@@ -2301,7 +2508,7 @@ function buildPrompt(input) {
|
|
|
2301
2508
|
`Progress heartbeat file: ${input.heartbeatPath}`,
|
|
2302
2509
|
"After each major step, append one JSON line to the heartbeat file with fields: ts, phase, summary, changedFiles, blocker.",
|
|
2303
2510
|
"Final response must include files changed, verification commands, and unresolved risks.",
|
|
2304
|
-
"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.",
|
|
2511
|
+
"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. Persona-attributed tasks: put repeatable lane lessons in lessonsLearned/laneGuidance (with evidence); substantive rows auto-persist as persona-scoped Lane A rules \u2014 global cross-lane policy stays in owner memory, not worker lessons.",
|
|
2305
2512
|
"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). One-off helper scripts must be removed (`kynver worker discard-disposable --path <file>`) or committed before completion \u2014 maintenance/board-drain workers are not exempt. Exiting with only dirty files and no PR routes to salvage review, not production review.",
|
|
2306
2513
|
"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.",
|
|
2307
2514
|
"Expert review / production-review workers (Dalton/Lorentz, plan-review-task, scheduledJob reviewer children): do NOT open new implementation PRs \u2014 review the parent task's existing PR and record reviewVerdict in finalResult; landing-contract targetPrReconciliation does not apply.",
|
|
@@ -2318,6 +2525,13 @@ function buildPrompt(input) {
|
|
|
2318
2525
|
"",
|
|
2319
2526
|
...input.personaMarkdown?.trim() ? [input.personaMarkdown.trim(), ""] : [],
|
|
2320
2527
|
...input.instructionPolicyMarkdown?.trim() ? ["Operating rules (Lane A \u2014 from AgentOS memory policy):", input.instructionPolicyMarkdown.trim(), ""] : [],
|
|
2528
|
+
...input.repairTargetPrUrl ? [
|
|
2529
|
+
...repairTargetPromptLines({
|
|
2530
|
+
targetPrUrl: input.repairTargetPrUrl,
|
|
2531
|
+
targetPrBranch: input.repairTargetBranch ?? null
|
|
2532
|
+
}),
|
|
2533
|
+
""
|
|
2534
|
+
] : [],
|
|
2321
2535
|
"Task:",
|
|
2322
2536
|
input.task
|
|
2323
2537
|
].join("\n");
|
|
@@ -2483,7 +2697,15 @@ var BUILTIN = {
|
|
|
2483
2697
|
var overrideProvider = null;
|
|
2484
2698
|
function resolveWorkerProvider(name) {
|
|
2485
2699
|
if (overrideProvider) return overrideProvider;
|
|
2486
|
-
const
|
|
2700
|
+
const explicit = name?.trim();
|
|
2701
|
+
if (explicit) {
|
|
2702
|
+
const provider2 = BUILTIN[explicit];
|
|
2703
|
+
if (!provider2) {
|
|
2704
|
+
throw new Error(`unknown worker provider "${explicit}" \u2014 supported: ${Object.keys(BUILTIN).join(", ")}`);
|
|
2705
|
+
}
|
|
2706
|
+
return provider2;
|
|
2707
|
+
}
|
|
2708
|
+
const configured = resolveConfiguredWorkerProvider(loadUserConfig().workerProvider);
|
|
2487
2709
|
const provider = BUILTIN[configured];
|
|
2488
2710
|
if (!provider) {
|
|
2489
2711
|
throw new Error(`unknown worker provider "${configured}" \u2014 supported: ${Object.keys(BUILTIN).join(", ")}`);
|
|
@@ -2527,13 +2749,13 @@ var ADVANCED_OUTCOMES = /* @__PURE__ */ new Set([
|
|
|
2527
2749
|
"review_already_scheduled"
|
|
2528
2750
|
]);
|
|
2529
2751
|
function summarizeHarnessCompletionResponse(parsed) {
|
|
2530
|
-
const
|
|
2531
|
-
if (!
|
|
2752
|
+
const record3 = asRecord(parsed);
|
|
2753
|
+
if (!record3) {
|
|
2532
2754
|
return { routeOutcome: null, taskAdvanced: false, detail: null };
|
|
2533
2755
|
}
|
|
2534
|
-
const outcome = asString(
|
|
2535
|
-
const detail = asString(
|
|
2536
|
-
const task = asRecord(
|
|
2756
|
+
const outcome = asString(record3.outcome);
|
|
2757
|
+
const detail = asString(record3.detail) ?? asString(record3.error);
|
|
2758
|
+
const task = asRecord(record3.task);
|
|
2537
2759
|
const taskStatus = task ? asString(task.status) : null;
|
|
2538
2760
|
const taskAdvanced = outcome !== null && ADVANCED_OUTCOMES.has(outcome) || taskStatus === "awaiting_review" || taskStatus === "done";
|
|
2539
2761
|
return {
|
|
@@ -2579,7 +2801,7 @@ var NO_PR_COMMITS_BETWEEN_RE = /no commits between/i;
|
|
|
2579
2801
|
function isGhNoCommitsBetweenError(detail) {
|
|
2580
2802
|
return Boolean(detail && NO_PR_COMMITS_BETWEEN_RE.test(detail));
|
|
2581
2803
|
}
|
|
2582
|
-
function
|
|
2804
|
+
function trimOrNull5(value) {
|
|
2583
2805
|
if (typeof value !== "string") return null;
|
|
2584
2806
|
const trimmed = value.trim();
|
|
2585
2807
|
return trimmed.length ? trimmed : null;
|
|
@@ -2587,7 +2809,7 @@ function trimOrNull4(value) {
|
|
|
2587
2809
|
function committedHead(ancestry) {
|
|
2588
2810
|
if (!ancestry?.checked) return null;
|
|
2589
2811
|
if (ancestry.headIsAncestorOfBase !== false) return null;
|
|
2590
|
-
return
|
|
2812
|
+
return trimOrNull5(ancestry.head);
|
|
2591
2813
|
}
|
|
2592
2814
|
function extractPrUrlFromText(value) {
|
|
2593
2815
|
if (value === void 0 || value === null) return null;
|
|
@@ -2595,7 +2817,7 @@ function extractPrUrlFromText(value) {
|
|
|
2595
2817
|
const m = text.match(
|
|
2596
2818
|
/https?:\/\/[^\s)>"]+\/(?:pull|pulls|merge_requests|pull-requests)\/\d+/i
|
|
2597
2819
|
);
|
|
2598
|
-
return m ?
|
|
2820
|
+
return m ? trimOrNull5(m[0]) : null;
|
|
2599
2821
|
}
|
|
2600
2822
|
function countCommitsAheadOfBase(worktreePath, baseRef, exec) {
|
|
2601
2823
|
const base = baseRef.trim();
|
|
@@ -2607,21 +2829,21 @@ function countCommitsAheadOfBase(worktreePath, baseRef, exec) {
|
|
|
2607
2829
|
}
|
|
2608
2830
|
function isReviewArtifactWorker(worker, snapshot) {
|
|
2609
2831
|
if (snapshot.changedFiles.length > 0) return false;
|
|
2610
|
-
const persona =
|
|
2832
|
+
const persona = trimOrNull5(worker.personaSlug)?.toLowerCase();
|
|
2611
2833
|
if (persona && REVIEW_PERSONA_SLUGS.has(persona)) return true;
|
|
2612
|
-
const rule =
|
|
2834
|
+
const rule = trimOrNull5(worker.routingRule) ?? "";
|
|
2613
2835
|
if (rule && REVIEW_LANE_RULE.test(rule)) return true;
|
|
2614
2836
|
return false;
|
|
2615
2837
|
}
|
|
2616
2838
|
function hasWorkProduct(snapshot, options) {
|
|
2617
2839
|
if (snapshot.changedFiles.length > 0) return true;
|
|
2618
|
-
const baseRef =
|
|
2840
|
+
const baseRef = trimOrNull5(options?.baseRef);
|
|
2619
2841
|
if (baseRef && options?.exec && options.worktreePath) {
|
|
2620
2842
|
const ahead = countCommitsAheadOfBase(options.worktreePath, baseRef, options.exec);
|
|
2621
2843
|
if (ahead === 0) return false;
|
|
2622
2844
|
if (ahead !== null && ahead > 0) return true;
|
|
2623
2845
|
}
|
|
2624
|
-
if (
|
|
2846
|
+
if (trimOrNull5(snapshot.headCommit)) return true;
|
|
2625
2847
|
if (committedHead(snapshot.gitAncestry)) return true;
|
|
2626
2848
|
return false;
|
|
2627
2849
|
}
|
|
@@ -2637,7 +2859,7 @@ function assessPrHandoffRequirement(input) {
|
|
|
2637
2859
|
})) {
|
|
2638
2860
|
return { required: false, reason: "expert_review_task" };
|
|
2639
2861
|
}
|
|
2640
|
-
const rule =
|
|
2862
|
+
const rule = trimOrNull5(input.routingRule) ?? "";
|
|
2641
2863
|
if (rule && REVIEW_LANE_RULE.test(rule)) {
|
|
2642
2864
|
return { required: false, reason: "review_lane" };
|
|
2643
2865
|
}
|
|
@@ -2648,10 +2870,14 @@ function assessPrHandoffRequirement(input) {
|
|
|
2648
2870
|
if (isReviewArtifactWorker(workerCtx, input.snapshot)) {
|
|
2649
2871
|
return { required: false, reason: "review_artifact" };
|
|
2650
2872
|
}
|
|
2651
|
-
if (
|
|
2873
|
+
if (trimOrNull5(input.patchPath) || trimOrNull5(input.artifactBundlePath)) {
|
|
2652
2874
|
return { required: false, reason: "patch_or_bundle" };
|
|
2653
2875
|
}
|
|
2654
|
-
const
|
|
2876
|
+
const repairTarget = trimOrNull5(input.repairTargetPrUrl);
|
|
2877
|
+
if (repairTarget) {
|
|
2878
|
+
return { required: false, reason: "repair_target_pr" };
|
|
2879
|
+
}
|
|
2880
|
+
const prUrl = trimOrNull5(input.prUrl) ?? trimOrNull5(input.taskPrUrl) ?? trimOrNull5(input.snapshot.prUrl);
|
|
2655
2881
|
if (prUrl) {
|
|
2656
2882
|
return { required: false, reason: "already_has_pr" };
|
|
2657
2883
|
}
|
|
@@ -2672,8 +2898,8 @@ function buildPrHandoffSnapshotFromStatus(status, extras) {
|
|
|
2672
2898
|
worktreePath: status.worktreePath,
|
|
2673
2899
|
gitAncestry: status.gitAncestry,
|
|
2674
2900
|
finalResult: status.finalResult,
|
|
2675
|
-
headCommit:
|
|
2676
|
-
prUrl:
|
|
2901
|
+
headCommit: trimOrNull5(extras?.headCommit) ?? committedHead(status.gitAncestry),
|
|
2902
|
+
prUrl: trimOrNull5(extras?.prUrl) ?? null
|
|
2677
2903
|
};
|
|
2678
2904
|
}
|
|
2679
2905
|
|
|
@@ -2879,6 +3105,7 @@ function ensurePrReadyHandoff(input, exec = defaultPrHandoffExec) {
|
|
|
2879
3105
|
executorRef: input.worker.executorRef,
|
|
2880
3106
|
parentTaskId: input.worker.parentTaskId,
|
|
2881
3107
|
taskPrUrl: input.worker.taskPrUrl,
|
|
3108
|
+
repairTargetPrUrl: input.worker.repairTargetPrUrl,
|
|
2882
3109
|
baseRef,
|
|
2883
3110
|
exec,
|
|
2884
3111
|
worker: input.worker,
|
|
@@ -2907,6 +3134,48 @@ function ensurePrReadyHandoff(input, exec = defaultPrHandoffExec) {
|
|
|
2907
3134
|
nextAction: "Ensure `origin` points at GitHub, push the branch, open a PR, and rerun `kynver worker complete`."
|
|
2908
3135
|
};
|
|
2909
3136
|
}
|
|
3137
|
+
const repairTarget = input.worker.repairTargetPrUrl?.trim();
|
|
3138
|
+
if (repairTarget) {
|
|
3139
|
+
let committed2 = false;
|
|
3140
|
+
let pushed2 = false;
|
|
3141
|
+
let headCommit2 = snapshot.headCommit ?? resolveHeadCommit(snapshot.worktreePath, exec) ?? void 0;
|
|
3142
|
+
if (snapshot.changedFiles.length > 0) {
|
|
3143
|
+
const pushResult2 = commitAndPushBranch({
|
|
3144
|
+
worktreePath: snapshot.worktreePath,
|
|
3145
|
+
branch: snapshot.branch,
|
|
3146
|
+
commitMessage: `fix(harness): repair target PR ${repairTarget}`,
|
|
3147
|
+
hasDirtyFiles: true,
|
|
3148
|
+
exec
|
|
3149
|
+
});
|
|
3150
|
+
if (!pushResult2.ok) {
|
|
3151
|
+
return {
|
|
3152
|
+
ok: false,
|
|
3153
|
+
reason: `PR-ready handoff blocked: ${pushResult2.detail ?? "git commit/push failed"}`,
|
|
3154
|
+
nextAction: "Commit and push to the target PR branch, then rerun `kynver worker complete`."
|
|
3155
|
+
};
|
|
3156
|
+
}
|
|
3157
|
+
committed2 = pushResult2.committed;
|
|
3158
|
+
pushed2 = pushResult2.pushed;
|
|
3159
|
+
headCommit2 = pushResult2.headCommit ?? headCommit2;
|
|
3160
|
+
} else {
|
|
3161
|
+
const pushOnly = exec.git(snapshot.worktreePath, ["push", "-u", "origin", snapshot.branch]);
|
|
3162
|
+
if (pushOnly.status !== 0 && !/already up to date/i.test(pushOnly.stderr || pushOnly.stdout)) {
|
|
3163
|
+
return {
|
|
3164
|
+
ok: false,
|
|
3165
|
+
reason: `PR-ready handoff blocked: ${pushOnly.stderr || pushOnly.stdout || "git push failed"}`,
|
|
3166
|
+
nextAction: "Push the target branch to origin, then rerun `kynver worker complete`."
|
|
3167
|
+
};
|
|
3168
|
+
}
|
|
3169
|
+
pushed2 = pushOnly.status === 0;
|
|
3170
|
+
}
|
|
3171
|
+
return {
|
|
3172
|
+
ok: true,
|
|
3173
|
+
prUrl: repairTarget,
|
|
3174
|
+
headCommit: headCommit2,
|
|
3175
|
+
committed: committed2,
|
|
3176
|
+
pushed: pushed2
|
|
3177
|
+
};
|
|
3178
|
+
}
|
|
2910
3179
|
const existing = findOpenPrUrl(snapshot.worktreePath, repo, snapshot.branch, exec);
|
|
2911
3180
|
if (existing) {
|
|
2912
3181
|
return {
|
|
@@ -3018,12 +3287,12 @@ function asRecord2(value) {
|
|
|
3018
3287
|
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
3019
3288
|
}
|
|
3020
3289
|
function extractDisposableArtifactsRemoved(finalResult) {
|
|
3021
|
-
const
|
|
3022
|
-
if (!
|
|
3023
|
-
const nested = asRecord2(
|
|
3290
|
+
const record3 = asRecord2(finalResult);
|
|
3291
|
+
if (!record3) return [];
|
|
3292
|
+
const nested = asRecord2(record3.worktreeHandoff);
|
|
3024
3293
|
const fromNested = stringList(nested?.disposableArtifactsRemoved);
|
|
3025
3294
|
if (fromNested.length) return fromNested;
|
|
3026
|
-
return stringList(
|
|
3295
|
+
return stringList(record3.disposableArtifactsRemoved);
|
|
3027
3296
|
}
|
|
3028
3297
|
function normalizeRelativePath(value) {
|
|
3029
3298
|
return value.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
@@ -3034,13 +3303,13 @@ function dirtyPathsCoveredByDisposableRemoval(changedFiles, removed) {
|
|
|
3034
3303
|
if (removed.length === 0) return false;
|
|
3035
3304
|
const removedSet = new Set(removed.map((p) => normalizeRelativePath(p)));
|
|
3036
3305
|
return material.every((line) => {
|
|
3037
|
-
const
|
|
3038
|
-
return removedSet.has(
|
|
3306
|
+
const path51 = normalizeRelativePath(pathFromGitStatusLine(line));
|
|
3307
|
+
return removedSet.has(path51);
|
|
3039
3308
|
});
|
|
3040
3309
|
}
|
|
3041
3310
|
|
|
3042
3311
|
// src/worktree-completion-handoff.ts
|
|
3043
|
-
function
|
|
3312
|
+
function trimOrNull6(value) {
|
|
3044
3313
|
if (typeof value !== "string") return null;
|
|
3045
3314
|
const t = value.trim();
|
|
3046
3315
|
return t.length ? t : null;
|
|
@@ -3067,7 +3336,7 @@ function assessWorktreeCompletionHandoff(input) {
|
|
|
3067
3336
|
const materialDirty = materialWorktreeChanges(rawDirty);
|
|
3068
3337
|
const removed = mergedDisposableRemoved(input);
|
|
3069
3338
|
const effectivelyClean = materialDirty.length === 0 || dirtyPathsCoveredByDisposableRemoval(rawDirty, removed);
|
|
3070
|
-
if (
|
|
3339
|
+
if (trimOrNull6(input.prUrl)) {
|
|
3071
3340
|
if (!effectivelyClean) {
|
|
3072
3341
|
return {
|
|
3073
3342
|
allowed: false,
|
|
@@ -3078,7 +3347,7 @@ function assessWorktreeCompletionHandoff(input) {
|
|
|
3078
3347
|
}
|
|
3079
3348
|
return { allowed: true, state: "pr_handoff", materialDirtyCount: 0 };
|
|
3080
3349
|
}
|
|
3081
|
-
if (
|
|
3350
|
+
if (trimOrNull6(input.headCommit)) {
|
|
3082
3351
|
if (!effectivelyClean) {
|
|
3083
3352
|
return {
|
|
3084
3353
|
allowed: false,
|
|
@@ -3089,7 +3358,7 @@ function assessWorktreeCompletionHandoff(input) {
|
|
|
3089
3358
|
}
|
|
3090
3359
|
return { allowed: true, state: "commit_handoff", materialDirtyCount: 0 };
|
|
3091
3360
|
}
|
|
3092
|
-
if (
|
|
3361
|
+
if (trimOrNull6(input.artifactBundlePath) || trimOrNull6(input.patchPath)) {
|
|
3093
3362
|
if (!effectivelyClean) {
|
|
3094
3363
|
return {
|
|
3095
3364
|
allowed: false,
|
|
@@ -3819,6 +4088,19 @@ function spawnCompletionSidecar(opts) {
|
|
|
3819
4088
|
}
|
|
3820
4089
|
}
|
|
3821
4090
|
|
|
4091
|
+
// src/repair-target-worktree.ts
|
|
4092
|
+
function addWorktreeForRepairBranch(repo, worktreePath, branch) {
|
|
4093
|
+
git(repo, ["fetch", "origin", branch, "--prune"], { allowFailure: true });
|
|
4094
|
+
const remoteRef = `origin/${branch}`;
|
|
4095
|
+
const added = gitCapture(repo, ["worktree", "add", "-B", branch, worktreePath, remoteRef]);
|
|
4096
|
+
if (added.status === 0) return;
|
|
4097
|
+
const fallback = gitCapture(repo, ["worktree", "add", "-b", branch, worktreePath, "HEAD"]);
|
|
4098
|
+
if (fallback.status !== 0) {
|
|
4099
|
+
const detail = added.stderr || added.stdout || fallback.stderr || fallback.stdout || "git worktree add failed for repair target branch";
|
|
4100
|
+
throw new Error(detail);
|
|
4101
|
+
}
|
|
4102
|
+
}
|
|
4103
|
+
|
|
3822
4104
|
// src/supervisor.ts
|
|
3823
4105
|
function spawnWorkerProcess(run, opts) {
|
|
3824
4106
|
const rawName = typeof opts.name === "string" ? opts.name.trim() : "";
|
|
@@ -3829,13 +4111,14 @@ function spawnWorkerProcess(run, opts) {
|
|
|
3829
4111
|
if (run.workers?.[name]) throw new Error(`worker already exists in run ${run.id}: ${name}`);
|
|
3830
4112
|
if (!opts.task) throw new Error(`missing task text for worker ${name}`);
|
|
3831
4113
|
const routing = opts.routingRule || opts.requestedModel ? {
|
|
3832
|
-
provider: opts.provider ||
|
|
4114
|
+
provider: opts.provider || DEFAULT_WORKER_PROVIDER,
|
|
3833
4115
|
model: opts.model,
|
|
3834
4116
|
rule: opts.routingRule || "explicit:spawn",
|
|
3835
4117
|
requestedModel: opts.requestedModel ?? opts.model
|
|
3836
4118
|
} : resolveWorkerLaunch({
|
|
3837
4119
|
explicitModel: opts.model,
|
|
3838
|
-
explicitProvider: opts.provider
|
|
4120
|
+
explicitProvider: opts.provider,
|
|
4121
|
+
explicitProviderIsOperatorOverride: Boolean(opts.provider?.trim())
|
|
3839
4122
|
});
|
|
3840
4123
|
const provider = resolveWorkerProvider(routing.provider);
|
|
3841
4124
|
let launchModel = routing.model;
|
|
@@ -3855,10 +4138,15 @@ function spawnWorkerProcess(run, opts) {
|
|
|
3855
4138
|
const workerDir = path14.join(runDirectory(run.id), "workers", name);
|
|
3856
4139
|
mkdirSync3(workerDir, { recursive: true });
|
|
3857
4140
|
const worktreePath = path14.join(worktreesDir, run.id, name);
|
|
3858
|
-
const
|
|
4141
|
+
const repairBranch = opts.repairTargetBranch?.trim() || void 0;
|
|
4142
|
+
const branch = repairBranch || opts.branch || `agent/${run.id}/${name}`;
|
|
3859
4143
|
if (existsSync13(worktreePath)) throw new Error(`worktree path already exists: ${worktreePath}`);
|
|
3860
4144
|
git(run.repo, ["fetch", "origin", "--prune"], { allowFailure: true });
|
|
3861
|
-
|
|
4145
|
+
if (repairBranch) {
|
|
4146
|
+
addWorktreeForRepairBranch(run.repo, worktreePath, repairBranch);
|
|
4147
|
+
} else {
|
|
4148
|
+
git(run.repo, ["worktree", "add", "-b", branch, worktreePath, run.baseCommit], { throwError: true });
|
|
4149
|
+
}
|
|
3862
4150
|
const stdoutPath = path14.join(workerDir, "stdout.jsonl");
|
|
3863
4151
|
const stderrPath = path14.join(workerDir, "stderr.log");
|
|
3864
4152
|
const heartbeatPath = path14.join(workerDir, "heartbeat.jsonl");
|
|
@@ -3871,7 +4159,9 @@ function spawnWorkerProcess(run, opts) {
|
|
|
3871
4159
|
taskId: opts.taskId,
|
|
3872
4160
|
instructionPolicyMarkdown: opts.instructionPolicyMarkdown,
|
|
3873
4161
|
personaMarkdown: opts.personaMarkdown,
|
|
3874
|
-
model: launchModel
|
|
4162
|
+
model: launchModel,
|
|
4163
|
+
repairTargetPrUrl: opts.repairTargetPrUrl,
|
|
4164
|
+
repairTargetBranch: opts.repairTargetBranch ?? (repairBranch || void 0)
|
|
3875
4165
|
});
|
|
3876
4166
|
let started;
|
|
3877
4167
|
try {
|
|
@@ -3924,6 +4214,8 @@ function spawnWorkerProcess(run, opts) {
|
|
|
3924
4214
|
...opts.parentTaskId ? { parentTaskId: String(opts.parentTaskId) } : {},
|
|
3925
4215
|
...opts.taskTitle ? { taskTitle: String(opts.taskTitle) } : {},
|
|
3926
4216
|
...opts.taskPrUrl ? { taskPrUrl: String(opts.taskPrUrl) } : {},
|
|
4217
|
+
...opts.repairTargetPrUrl ? { repairTargetPrUrl: String(opts.repairTargetPrUrl) } : {},
|
|
4218
|
+
...opts.repairTargetBranch ? { repairTargetBranch: String(opts.repairTargetBranch) } : {},
|
|
3927
4219
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3928
4220
|
};
|
|
3929
4221
|
saveWorker(run.id, worker);
|
|
@@ -4595,6 +4887,24 @@ function extractPlanOutboxFromTask(task) {
|
|
|
4595
4887
|
};
|
|
4596
4888
|
}
|
|
4597
4889
|
|
|
4890
|
+
// src/runner-identity.ts
|
|
4891
|
+
import os3 from "node:os";
|
|
4892
|
+
function trimOrNull7(value) {
|
|
4893
|
+
if (!value?.trim()) return null;
|
|
4894
|
+
return value.trim();
|
|
4895
|
+
}
|
|
4896
|
+
function resolveRunnerPresencePayload(input = {}) {
|
|
4897
|
+
const env = input.env ?? process.env;
|
|
4898
|
+
const runnerId = trimOrNull7(env.KYNVER_RUNTIME_ID) ?? trimOrNull7(env.OPENCLAW_RUNTIME_ID) ?? trimOrNull7(env.HOSTNAME) ?? os3.hostname();
|
|
4899
|
+
return {
|
|
4900
|
+
runnerId,
|
|
4901
|
+
hostname: trimOrNull7(env.HOSTNAME) ?? os3.hostname(),
|
|
4902
|
+
profile: trimOrNull7(env.KYNVER_RUNNER_PROFILE) ?? trimOrNull7(env.OPENCLAW_RUNNER_PROFILE),
|
|
4903
|
+
harnessRepo: trimOrNull7(env.KYNVER_HARNESS_REPO) ?? trimOrNull7(env.KYNVER_DEFAULT_REPO),
|
|
4904
|
+
runId: input.runId ?? null
|
|
4905
|
+
};
|
|
4906
|
+
}
|
|
4907
|
+
|
|
4598
4908
|
// src/dispatch.ts
|
|
4599
4909
|
var DEFAULT_DISPATCH_LEASE_MS = 60 * 60 * 1e3;
|
|
4600
4910
|
function readHarnessWorkerContext(decision) {
|
|
@@ -4659,7 +4969,8 @@ async function dispatchRun(args) {
|
|
|
4659
4969
|
const secret = await resolveCallbackSecretWithMint(args.secret ? String(args.secret) : void 0, agentOsId, { baseUrl: base });
|
|
4660
4970
|
const execute = args.execute === true || args.execute === "true";
|
|
4661
4971
|
const dryRun = !execute;
|
|
4662
|
-
const
|
|
4972
|
+
const runnerPresence = resolveRunnerPresencePayload({ runId: run.id });
|
|
4973
|
+
const leaseOwner = `kynver-harness:${run.id}@runner:${runnerPresence.runnerId}`;
|
|
4663
4974
|
const runnerDiskGate = args.diskPath ? observeRunnerDiskGate({ diskPath: String(args.diskPath) }) : observeRunnerDiskGate({ diskPath: run.repo });
|
|
4664
4975
|
const runnerResourceGate = observeRunnerResourceGate({ runId: run.id });
|
|
4665
4976
|
const requestedStarts = Number(args.maxStarts) > 0 ? Math.floor(Number(args.maxStarts)) : 1;
|
|
@@ -4671,11 +4982,18 @@ async function dispatchRun(args) {
|
|
|
4671
4982
|
void 0
|
|
4672
4983
|
);
|
|
4673
4984
|
if (!worker?.taskId || !isPidAlive(worker.pid)) continue;
|
|
4985
|
+
const ownedPaths = Array.isArray(worker.ownedPaths) ? worker.ownedPaths.filter((p) => typeof p === "string") : [];
|
|
4986
|
+
const writeSetPrefixes = Array.isArray(
|
|
4987
|
+
worker.writeSetPrefixes
|
|
4988
|
+
) ? (worker.writeSetPrefixes ?? []).filter((p) => typeof p === "string") : [];
|
|
4674
4989
|
activeHarnessWorkers.push({
|
|
4675
4990
|
runId: run.id,
|
|
4676
4991
|
workerName: name,
|
|
4677
4992
|
taskId: worker.taskId,
|
|
4678
|
-
pid: worker.pid
|
|
4993
|
+
pid: worker.pid,
|
|
4994
|
+
...ownedPaths.length ? { ownedPaths } : {},
|
|
4995
|
+
...writeSetPrefixes.length ? { writeSetPrefixes } : {},
|
|
4996
|
+
...worker.allowConcurrentHotspot ? { allowConcurrentHotspot: true } : {}
|
|
4679
4997
|
});
|
|
4680
4998
|
}
|
|
4681
4999
|
const dispatchUrl = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/tasks/dispatch-next`;
|
|
@@ -4688,6 +5006,7 @@ async function dispatchRun(args) {
|
|
|
4688
5006
|
runnerDiskGate,
|
|
4689
5007
|
runnerResourceGate,
|
|
4690
5008
|
activeHarnessWorkers,
|
|
5009
|
+
runnerPresence,
|
|
4691
5010
|
harnessBoardSnapshot: buildRunBoard(run.id),
|
|
4692
5011
|
...args.lane ? { lane: String(args.lane) } : {},
|
|
4693
5012
|
executor: args.executor ? String(args.executor) : "harness",
|
|
@@ -4780,10 +5099,19 @@ async function dispatchRun(args) {
|
|
|
4780
5099
|
const name = safeSlug(`t-${task.id}-a${task.attempt}`);
|
|
4781
5100
|
const routing = resolveWorkerLaunch({
|
|
4782
5101
|
explicitModel: args.model ? String(args.model) : void 0,
|
|
5102
|
+
explicitProvider: args.provider ? String(args.provider) : void 0,
|
|
5103
|
+
explicitProviderIsOperatorOverride: Boolean(args.provider),
|
|
4783
5104
|
task: enrichTaskForModelRouting(task)
|
|
4784
5105
|
});
|
|
4785
5106
|
try {
|
|
4786
5107
|
const planId = task.planId ? String(task.planId) : void 0;
|
|
5108
|
+
const repairTarget = resolveHarnessRepairTargetFromTask({
|
|
5109
|
+
title: task.title ? String(task.title) : void 0,
|
|
5110
|
+
description: task.description ? String(task.description) : null,
|
|
5111
|
+
executorRef: task.executorRef ? String(task.executorRef) : null,
|
|
5112
|
+
prUrl: task.prUrl ? String(task.prUrl) : null,
|
|
5113
|
+
branch: task.branch ? String(task.branch) : null
|
|
5114
|
+
});
|
|
4787
5115
|
const worker = spawnWorkerProcess(run, {
|
|
4788
5116
|
name,
|
|
4789
5117
|
task: buildDispatchTaskText(task, agentOsId),
|
|
@@ -4795,10 +5123,13 @@ async function dispatchRun(args) {
|
|
|
4795
5123
|
agentOsId,
|
|
4796
5124
|
taskId: String(task.id),
|
|
4797
5125
|
planId,
|
|
5126
|
+
branch: repairTarget?.targetPrBranch ?? void 0,
|
|
4798
5127
|
executorRef: task.executorRef ? String(task.executorRef) : void 0,
|
|
4799
5128
|
parentTaskId: task.parentTaskId ? String(task.parentTaskId) : void 0,
|
|
4800
5129
|
taskTitle: task.title ? String(task.title) : void 0,
|
|
4801
|
-
taskPrUrl: task.prUrl ? String(task.prUrl) : void 0,
|
|
5130
|
+
taskPrUrl: repairTarget?.targetPrUrl ?? (task.prUrl ? String(task.prUrl) : void 0),
|
|
5131
|
+
repairTargetPrUrl: repairTarget?.targetPrUrl,
|
|
5132
|
+
repairTargetBranch: repairTarget?.targetPrBranch ?? void 0,
|
|
4802
5133
|
instructionPolicyMarkdown: harnessContext?.instructionPolicyMarkdown ?? null,
|
|
4803
5134
|
instructionPolicyFingerprint: harnessContext?.instructionPolicyFingerprint ?? null,
|
|
4804
5135
|
instructionPolicyEvidence: harnessContext?.instructionPolicyEvidence ?? null,
|
|
@@ -4876,6 +5207,58 @@ async function dispatchRun(args) {
|
|
|
4876
5207
|
}
|
|
4877
5208
|
}
|
|
4878
5209
|
|
|
5210
|
+
// src/box-resource-snapshot.ts
|
|
5211
|
+
import os5 from "node:os";
|
|
5212
|
+
|
|
5213
|
+
// src/box-resource-snapshot-shared.ts
|
|
5214
|
+
import os4 from "node:os";
|
|
5215
|
+
function resolveBoxKindFromEnv(env = process.env) {
|
|
5216
|
+
const kind = (env.KYNVER_BOX_KIND ?? env.KYNVER_AGENT_OS_SLUG ?? "forge").trim().toLowerCase();
|
|
5217
|
+
if (kind === "ghost" || kind === "forge") return kind;
|
|
5218
|
+
return kind || "forge";
|
|
5219
|
+
}
|
|
5220
|
+
function defaultBoxId(boxKind, hostLabel) {
|
|
5221
|
+
const host = (hostLabel ?? os4.hostname()).trim().toLowerCase().replace(/\s+/g, "-") || "unknown-host";
|
|
5222
|
+
return `${boxKind}:${host}`;
|
|
5223
|
+
}
|
|
5224
|
+
|
|
5225
|
+
// src/box-resource-snapshot.ts
|
|
5226
|
+
function buildBoxResourceSnapshotFromGate(gate, input = {}) {
|
|
5227
|
+
const boxKind = (input.boxKind ?? resolveBoxKindFromEnv()).trim().toLowerCase() || "forge";
|
|
5228
|
+
const hostLabel = input.hostLabel ?? os5.hostname();
|
|
5229
|
+
const boxId = input.boxId ?? defaultBoxId(boxKind, hostLabel);
|
|
5230
|
+
return {
|
|
5231
|
+
boxId,
|
|
5232
|
+
boxKind,
|
|
5233
|
+
displayName: input.displayName ?? null,
|
|
5234
|
+
hostLabel,
|
|
5235
|
+
observedAt: input.observedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
5236
|
+
totalMemBytes: gate.totalMemBytes,
|
|
5237
|
+
freeMemBytes: gate.freeMemBytes,
|
|
5238
|
+
activeWorkers: gate.activeWorkers,
|
|
5239
|
+
maxConcurrentWorkers: gate.maxConcurrentWorkers,
|
|
5240
|
+
autoCap: gate.autoCap,
|
|
5241
|
+
slotsAvailable: gate.slotsAvailable,
|
|
5242
|
+
harnessRunId: input.harnessRunId ?? null,
|
|
5243
|
+
queuedTasks: input.queuedTasks ?? null,
|
|
5244
|
+
reason: gate.reason,
|
|
5245
|
+
...input.prEvidence?.length ? { prEvidence: input.prEvidence } : {}
|
|
5246
|
+
};
|
|
5247
|
+
}
|
|
5248
|
+
function formatHeartbeatLine(input) {
|
|
5249
|
+
const row = {
|
|
5250
|
+
ts: input.ts ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
5251
|
+
phase: input.phase,
|
|
5252
|
+
summary: input.summary,
|
|
5253
|
+
changedFiles: input.changedFiles ?? [],
|
|
5254
|
+
blocker: input.blocker ?? null
|
|
5255
|
+
};
|
|
5256
|
+
if (input.boxResourceSnapshot) row.boxResourceSnapshot = input.boxResourceSnapshot;
|
|
5257
|
+
if (input.prEvidence?.length) row.prEvidence = input.prEvidence;
|
|
5258
|
+
return `${JSON.stringify(row)}
|
|
5259
|
+
`;
|
|
5260
|
+
}
|
|
5261
|
+
|
|
4879
5262
|
// src/fortress-engagement-gate.ts
|
|
4880
5263
|
function isEngagementRequiredSkip(skip) {
|
|
4881
5264
|
if (skip.skipReason === "engagement_required") return true;
|
|
@@ -5094,79 +5477,365 @@ function validateTailLines(lines) {
|
|
|
5094
5477
|
}
|
|
5095
5478
|
|
|
5096
5479
|
// src/worktree.ts
|
|
5097
|
-
import { existsSync as
|
|
5480
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync5 } from "node:fs";
|
|
5481
|
+
import path25 from "node:path";
|
|
5482
|
+
|
|
5483
|
+
// src/run-list.ts
|
|
5484
|
+
import { existsSync as existsSync17, readFileSync as readFileSync11 } from "node:fs";
|
|
5485
|
+
import path23 from "node:path";
|
|
5486
|
+
|
|
5487
|
+
// src/stale-reconcile.ts
|
|
5098
5488
|
import path22 from "node:path";
|
|
5099
5489
|
|
|
5100
|
-
// src/
|
|
5490
|
+
// src/finalize.ts
|
|
5101
5491
|
import path21 from "node:path";
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
const
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
}
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
};
|
|
5139
|
-
saveUserConfig(config);
|
|
5140
|
-
return config;
|
|
5141
|
-
}
|
|
5142
|
-
function remediateDefaultRepo(opts) {
|
|
5143
|
-
const existing = opts?.config ?? loadUserConfig();
|
|
5144
|
-
const resolved = resolveDefaultRepo({ ...opts, config: existing });
|
|
5145
|
-
if (!resolved) {
|
|
5146
|
-
return {
|
|
5147
|
-
ok: false,
|
|
5148
|
-
reason: "No Kynver git checkout found. Clone the repo, cd into it, then run `kynver setup --repo /path/to/Kynver` (or export KYNVER_DEFAULT_REPO)."
|
|
5149
|
-
};
|
|
5150
|
-
}
|
|
5151
|
-
if (resolved.persistedInConfig) {
|
|
5152
|
-
return { ok: true, resolved, config: existing };
|
|
5492
|
+
var ACTIVE_RUN_STATUSES = /* @__PURE__ */ new Set([
|
|
5493
|
+
"running",
|
|
5494
|
+
"dispatching",
|
|
5495
|
+
"pending",
|
|
5496
|
+
"queued",
|
|
5497
|
+
"needs_attention"
|
|
5498
|
+
]);
|
|
5499
|
+
var TERMINAL_RUN_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "cancelled", "done"]);
|
|
5500
|
+
function deriveTerminalRunStatus(run) {
|
|
5501
|
+
const names = Object.keys(run.workers || {});
|
|
5502
|
+
if (names.length === 0) return "failed";
|
|
5503
|
+
let anyAlive = false;
|
|
5504
|
+
let anyResult = false;
|
|
5505
|
+
let anyCompletionBlocked = false;
|
|
5506
|
+
let anyLandingBlocked = false;
|
|
5507
|
+
for (const name of names) {
|
|
5508
|
+
const worker = readJson(
|
|
5509
|
+
path21.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
5510
|
+
void 0
|
|
5511
|
+
);
|
|
5512
|
+
if (!worker) continue;
|
|
5513
|
+
const status = computeWorkerStatus(worker, {
|
|
5514
|
+
base: run.base,
|
|
5515
|
+
baseCommit: run.baseCommit
|
|
5516
|
+
});
|
|
5517
|
+
if (status.alive && !status.finalResult) {
|
|
5518
|
+
anyAlive = true;
|
|
5519
|
+
break;
|
|
5520
|
+
}
|
|
5521
|
+
if (typeof worker.completionBlocker === "string" && worker.completionBlocker) {
|
|
5522
|
+
anyCompletionBlocked = true;
|
|
5523
|
+
}
|
|
5524
|
+
if (isLandingBlockedWorkerStatus(status)) {
|
|
5525
|
+
anyLandingBlocked = true;
|
|
5526
|
+
}
|
|
5527
|
+
if (status.finalResult && status.attention.state === "done") anyResult = true;
|
|
5153
5528
|
}
|
|
5154
|
-
|
|
5155
|
-
return
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
config
|
|
5159
|
-
};
|
|
5529
|
+
if (anyAlive) return null;
|
|
5530
|
+
if (anyCompletionBlocked) return null;
|
|
5531
|
+
if (anyLandingBlocked) return null;
|
|
5532
|
+
return anyResult ? "completed" : "failed";
|
|
5160
5533
|
}
|
|
5161
|
-
function
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5534
|
+
function finalizeStaleRuns() {
|
|
5535
|
+
const finalized = [];
|
|
5536
|
+
for (const run of listRunRecords()) {
|
|
5537
|
+
if (!ACTIVE_RUN_STATUSES.has(run.status)) continue;
|
|
5538
|
+
const next = deriveTerminalRunStatus(run);
|
|
5539
|
+
if (!next || next === run.status) continue;
|
|
5540
|
+
const from = run.status;
|
|
5541
|
+
run.status = next;
|
|
5542
|
+
saveRun(run);
|
|
5543
|
+
finalized.push({ runId: run.id, from, to: next });
|
|
5544
|
+
}
|
|
5545
|
+
return finalized;
|
|
5167
5546
|
}
|
|
5168
5547
|
|
|
5169
|
-
// src/
|
|
5548
|
+
// src/stale-reconcile.ts
|
|
5549
|
+
var STALE_RECONCILE_HEARTBEAT_MS = 15 * 60 * 1e3;
|
|
5550
|
+
function staleReconcileDisabled() {
|
|
5551
|
+
return process.env.KYNVER_NO_STALE_CLEANUP === "1";
|
|
5552
|
+
}
|
|
5553
|
+
function reconcileStaleWorkers() {
|
|
5554
|
+
if (staleReconcileDisabled()) {
|
|
5555
|
+
return { workers: [], finalizedRuns: finalizeStaleRuns() };
|
|
5556
|
+
}
|
|
5557
|
+
const outcomes = [];
|
|
5558
|
+
const now = Date.now();
|
|
5559
|
+
for (const run of listRunRecords()) {
|
|
5560
|
+
for (const name of Object.keys(run.workers || {})) {
|
|
5561
|
+
const workerPath = path22.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
|
|
5562
|
+
const worker = readJson(workerPath, void 0);
|
|
5563
|
+
if (!worker || worker.status !== "running") {
|
|
5564
|
+
outcomes.push({
|
|
5565
|
+
runId: run.id,
|
|
5566
|
+
worker: name,
|
|
5567
|
+
action: "skipped",
|
|
5568
|
+
reason: worker ? `worker status is ${worker.status}` : "worker.json missing"
|
|
5569
|
+
});
|
|
5570
|
+
continue;
|
|
5571
|
+
}
|
|
5572
|
+
const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
|
|
5573
|
+
if (status.finalResult) {
|
|
5574
|
+
if (worker.status === "running") {
|
|
5575
|
+
const nextStatus = status.attention.state === "blocked" ? "blocked" : status.attention.state === "done" || status.status === "done" ? "done" : "exited";
|
|
5576
|
+
worker.status = nextStatus;
|
|
5577
|
+
worker.reconciledAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5578
|
+
worker.reconcileReason = "synced finished worker record after terminal stdout/heartbeat";
|
|
5579
|
+
saveWorker(run.id, worker);
|
|
5580
|
+
outcomes.push({
|
|
5581
|
+
runId: run.id,
|
|
5582
|
+
worker: name,
|
|
5583
|
+
action: "marked_exited",
|
|
5584
|
+
reason: worker.reconcileReason
|
|
5585
|
+
});
|
|
5586
|
+
} else {
|
|
5587
|
+
outcomes.push({ runId: run.id, worker: name, action: "skipped", reason: "final result present" });
|
|
5588
|
+
}
|
|
5589
|
+
continue;
|
|
5590
|
+
}
|
|
5591
|
+
if (!status.alive) {
|
|
5592
|
+
const nextStatus = status.attention.state === "blocked" ? "blocked" : status.status === "done" ? "done" : "exited";
|
|
5593
|
+
worker.status = nextStatus;
|
|
5594
|
+
worker.reconciledAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5595
|
+
worker.reconcileReason = status.attention.reason;
|
|
5596
|
+
saveWorker(run.id, worker);
|
|
5597
|
+
outcomes.push({
|
|
5598
|
+
runId: run.id,
|
|
5599
|
+
worker: name,
|
|
5600
|
+
action: "marked_exited",
|
|
5601
|
+
reason: status.attention.reason
|
|
5602
|
+
});
|
|
5603
|
+
continue;
|
|
5604
|
+
}
|
|
5605
|
+
if (status.attention.state === "stale" && worker.pid && isPidAlive(worker.pid)) {
|
|
5606
|
+
const hbMs = status.lastHeartbeatAt ? Date.parse(status.lastHeartbeatAt) : NaN;
|
|
5607
|
+
const actMs = status.lastActivityAt ? Date.parse(status.lastActivityAt) : NaN;
|
|
5608
|
+
const hbStale = !Number.isFinite(hbMs) || now - hbMs > STALE_RECONCILE_HEARTBEAT_MS;
|
|
5609
|
+
const actStale = Number.isFinite(actMs) && now - actMs > STALE_MS;
|
|
5610
|
+
if (hbStale && actStale) {
|
|
5611
|
+
killWorkerProcess(worker.pid, "SIGTERM");
|
|
5612
|
+
worker.status = "exited";
|
|
5613
|
+
worker.reconciledAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5614
|
+
worker.reconcileReason = `reconciled stale worker: ${status.attention.reason}`;
|
|
5615
|
+
saveWorker(run.id, worker);
|
|
5616
|
+
outcomes.push({
|
|
5617
|
+
runId: run.id,
|
|
5618
|
+
worker: name,
|
|
5619
|
+
action: "killed_stale",
|
|
5620
|
+
reason: status.attention.reason
|
|
5621
|
+
});
|
|
5622
|
+
continue;
|
|
5623
|
+
}
|
|
5624
|
+
}
|
|
5625
|
+
outcomes.push({
|
|
5626
|
+
runId: run.id,
|
|
5627
|
+
worker: name,
|
|
5628
|
+
action: "skipped",
|
|
5629
|
+
reason: status.attention.reason
|
|
5630
|
+
});
|
|
5631
|
+
}
|
|
5632
|
+
}
|
|
5633
|
+
return { workers: outcomes, finalizedRuns: finalizeStaleRuns() };
|
|
5634
|
+
}
|
|
5635
|
+
function reconcileRunsCli() {
|
|
5636
|
+
const result = reconcileStaleWorkers();
|
|
5637
|
+
const markedExited = result.workers.filter((w) => w.action === "marked_exited").length;
|
|
5638
|
+
const killedStale = result.workers.filter((w) => w.action === "killed_stale").length;
|
|
5639
|
+
const skipped = result.workers.filter((w) => w.action === "skipped").length;
|
|
5640
|
+
console.log(
|
|
5641
|
+
JSON.stringify(
|
|
5642
|
+
{
|
|
5643
|
+
ok: true,
|
|
5644
|
+
workers: { markedExited, killedStale, skipped, total: result.workers.length },
|
|
5645
|
+
finalizedRuns: result.finalizedRuns.length,
|
|
5646
|
+
details: { workers: result.workers, finalizedRuns: result.finalizedRuns }
|
|
5647
|
+
},
|
|
5648
|
+
null,
|
|
5649
|
+
2
|
|
5650
|
+
)
|
|
5651
|
+
);
|
|
5652
|
+
}
|
|
5653
|
+
|
|
5654
|
+
// src/run-list.ts
|
|
5655
|
+
function heartbeatByteLength(heartbeatPath) {
|
|
5656
|
+
if (!heartbeatPath || !existsSync17(heartbeatPath)) return 0;
|
|
5657
|
+
try {
|
|
5658
|
+
return readFileSync11(heartbeatPath, "utf8").trim().length;
|
|
5659
|
+
} catch {
|
|
5660
|
+
return 0;
|
|
5661
|
+
}
|
|
5662
|
+
}
|
|
5663
|
+
function workerEvidence(run, workerName) {
|
|
5664
|
+
const workerPath = path23.join(runDirectory(run.id), "workers", safeSlug(workerName), "worker.json");
|
|
5665
|
+
const worker = readJson(workerPath, void 0);
|
|
5666
|
+
if (!worker) {
|
|
5667
|
+
return {
|
|
5668
|
+
worker: workerName,
|
|
5669
|
+
workerStatus: "missing",
|
|
5670
|
+
attention: "needs_attention",
|
|
5671
|
+
attentionReason: "worker.json missing",
|
|
5672
|
+
missingHeartbeat: true,
|
|
5673
|
+
missingFinalResult: true,
|
|
5674
|
+
landingBlocked: false,
|
|
5675
|
+
completionBlocked: false
|
|
5676
|
+
};
|
|
5677
|
+
}
|
|
5678
|
+
const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
|
|
5679
|
+
const missingHeartbeat = heartbeatByteLength(worker.heartbeatPath) === 0;
|
|
5680
|
+
const missingFinalResult = !status.finalResult && !status.alive;
|
|
5681
|
+
const completionBlocked = typeof worker.completionBlocker === "string" && worker.completionBlocker.length > 0;
|
|
5682
|
+
return {
|
|
5683
|
+
worker: workerName,
|
|
5684
|
+
workerStatus: worker.status,
|
|
5685
|
+
attention: status.attention.state,
|
|
5686
|
+
attentionReason: status.attention.reason,
|
|
5687
|
+
missingHeartbeat,
|
|
5688
|
+
missingFinalResult,
|
|
5689
|
+
landingBlocked: isLandingBlockedWorkerStatus(status),
|
|
5690
|
+
completionBlocked
|
|
5691
|
+
};
|
|
5692
|
+
}
|
|
5693
|
+
function deriveFinalizeBlockedReason(input) {
|
|
5694
|
+
if (input.openWorkerCount > 0) return "active_workers";
|
|
5695
|
+
if (input.workers.some((w) => w.completionBlocked)) return "completion_blocked";
|
|
5696
|
+
if (input.workers.some((w) => w.landingBlocked)) return "landing_blocked";
|
|
5697
|
+
return null;
|
|
5698
|
+
}
|
|
5699
|
+
function aggregateRunAttention(workers) {
|
|
5700
|
+
const rank = {
|
|
5701
|
+
blocked: 5,
|
|
5702
|
+
needs_attention: 4,
|
|
5703
|
+
stale: 3,
|
|
5704
|
+
done: 2,
|
|
5705
|
+
ok: 1
|
|
5706
|
+
};
|
|
5707
|
+
let best = "ok";
|
|
5708
|
+
let reason;
|
|
5709
|
+
for (const w of workers) {
|
|
5710
|
+
const state = w.attention;
|
|
5711
|
+
if ((rank[state] ?? 0) >= (rank[best] ?? 0)) {
|
|
5712
|
+
best = state;
|
|
5713
|
+
reason = w.attentionReason;
|
|
5714
|
+
}
|
|
5715
|
+
}
|
|
5716
|
+
return { attention: best, attentionReason: reason };
|
|
5717
|
+
}
|
|
5718
|
+
function countOpenWorkers(run) {
|
|
5719
|
+
let open = 0;
|
|
5720
|
+
for (const name of Object.keys(run.workers || {})) {
|
|
5721
|
+
const workerPath = path23.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
|
|
5722
|
+
const worker = readJson(workerPath, void 0);
|
|
5723
|
+
if (!worker) continue;
|
|
5724
|
+
const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
|
|
5725
|
+
if (status.alive && !status.finalResult) open += 1;
|
|
5726
|
+
}
|
|
5727
|
+
return open;
|
|
5728
|
+
}
|
|
5729
|
+
function buildRunListRows() {
|
|
5730
|
+
reconcileStaleWorkers();
|
|
5731
|
+
return listRunRecords().map((run) => {
|
|
5732
|
+
const workerNames = Object.keys(run.workers || {});
|
|
5733
|
+
const workers = workerNames.map((name) => workerEvidence(run, name));
|
|
5734
|
+
const missingHeartbeatWorkers = workers.filter((w) => w.missingHeartbeat).map((w) => w.worker);
|
|
5735
|
+
const missingFinalResultWorkers = workers.filter((w) => w.missingFinalResult).map((w) => w.worker);
|
|
5736
|
+
const landingBlockedWorkers = workers.filter((w) => w.landingBlocked).map((w) => w.worker);
|
|
5737
|
+
const completionBlockedWorkers = workers.filter((w) => w.completionBlocked).map((w) => w.worker);
|
|
5738
|
+
const { attention, attentionReason } = aggregateRunAttention(workers);
|
|
5739
|
+
const openWorkerCount = countOpenWorkers(run);
|
|
5740
|
+
const effectiveStatus = deriveRunStatus(
|
|
5741
|
+
run.status,
|
|
5742
|
+
workers.map((w) => ({ attention: w.attention, status: w.workerStatus }))
|
|
5743
|
+
);
|
|
5744
|
+
return {
|
|
5745
|
+
id: run.id,
|
|
5746
|
+
name: run.name,
|
|
5747
|
+
status: run.status,
|
|
5748
|
+
effectiveStatus,
|
|
5749
|
+
repo: run.repo,
|
|
5750
|
+
createdAt: run.createdAt,
|
|
5751
|
+
openWorkerCount,
|
|
5752
|
+
attention,
|
|
5753
|
+
attentionReason,
|
|
5754
|
+
finalizeBlockedReason: deriveFinalizeBlockedReason({ run, workers, openWorkerCount }),
|
|
5755
|
+
evidence: {
|
|
5756
|
+
missingHeartbeatWorkers,
|
|
5757
|
+
missingFinalResultWorkers,
|
|
5758
|
+
landingBlockedWorkers,
|
|
5759
|
+
completionBlockedWorkers,
|
|
5760
|
+
workers
|
|
5761
|
+
}
|
|
5762
|
+
};
|
|
5763
|
+
});
|
|
5764
|
+
}
|
|
5765
|
+
function listRunsCli() {
|
|
5766
|
+
console.log(JSON.stringify(buildRunListRows(), null, 2));
|
|
5767
|
+
}
|
|
5768
|
+
|
|
5769
|
+
// src/default-repo.ts
|
|
5770
|
+
import path24 from "node:path";
|
|
5771
|
+
function expandConfiguredRepo(value) {
|
|
5772
|
+
return path24.resolve(resolveUserPath(value.trim()));
|
|
5773
|
+
}
|
|
5774
|
+
function fromConfigured(value, source, persistedInConfig) {
|
|
5775
|
+
const trimmed = value?.trim();
|
|
5776
|
+
if (!trimmed) return null;
|
|
5777
|
+
return {
|
|
5778
|
+
repo: expandConfiguredRepo(trimmed),
|
|
5779
|
+
source,
|
|
5780
|
+
persistedInConfig
|
|
5781
|
+
};
|
|
5782
|
+
}
|
|
5783
|
+
function resolveDefaultRepo(opts = {}) {
|
|
5784
|
+
const env = opts.env ?? process.env;
|
|
5785
|
+
const config = opts.config ?? loadUserConfig();
|
|
5786
|
+
const fromConfig = fromConfigured(config.defaultRepo, "config", true);
|
|
5787
|
+
if (fromConfig) return fromConfig;
|
|
5788
|
+
const fromDefaultEnv = fromConfigured(env.KYNVER_DEFAULT_REPO, "env_default_repo", false);
|
|
5789
|
+
if (fromDefaultEnv) return fromDefaultEnv;
|
|
5790
|
+
const fromHarnessEnv = fromConfigured(env.KYNVER_HARNESS_REPO, "env_harness_repo", false);
|
|
5791
|
+
if (fromHarnessEnv) return fromHarnessEnv;
|
|
5792
|
+
const discovered = discoverDefaultRepo({
|
|
5793
|
+
cwd: opts.cwd,
|
|
5794
|
+
runtimeModuleUrl: opts.runtimeModuleUrl
|
|
5795
|
+
});
|
|
5796
|
+
if (!discovered) return null;
|
|
5797
|
+
return {
|
|
5798
|
+
repo: discovered.repo,
|
|
5799
|
+
source: discovered.source,
|
|
5800
|
+
persistedInConfig: false
|
|
5801
|
+
};
|
|
5802
|
+
}
|
|
5803
|
+
function persistDefaultRepo(repo, existing) {
|
|
5804
|
+
const config = {
|
|
5805
|
+
...existing ?? loadUserConfig(),
|
|
5806
|
+
defaultRepo: redactHomePath(path24.resolve(repo))
|
|
5807
|
+
};
|
|
5808
|
+
saveUserConfig(config);
|
|
5809
|
+
return config;
|
|
5810
|
+
}
|
|
5811
|
+
function remediateDefaultRepo(opts) {
|
|
5812
|
+
const existing = opts?.config ?? loadUserConfig();
|
|
5813
|
+
const resolved = resolveDefaultRepo({ ...opts, config: existing });
|
|
5814
|
+
if (!resolved) {
|
|
5815
|
+
return {
|
|
5816
|
+
ok: false,
|
|
5817
|
+
reason: "No Kynver git checkout found. Clone the repo, cd into it, then run `kynver setup --repo /path/to/Kynver` (or export KYNVER_DEFAULT_REPO)."
|
|
5818
|
+
};
|
|
5819
|
+
}
|
|
5820
|
+
if (resolved.persistedInConfig) {
|
|
5821
|
+
return { ok: true, resolved, config: existing };
|
|
5822
|
+
}
|
|
5823
|
+
const config = persistDefaultRepo(resolved.repo, existing);
|
|
5824
|
+
return {
|
|
5825
|
+
ok: true,
|
|
5826
|
+
resolved: { ...resolved, persistedInConfig: true, source: "config" },
|
|
5827
|
+
config
|
|
5828
|
+
};
|
|
5829
|
+
}
|
|
5830
|
+
function formatResolvedDefaultRepo(resolved) {
|
|
5831
|
+
return {
|
|
5832
|
+
defaultRepo: displayUserPath(resolved.repo),
|
|
5833
|
+
source: resolved.source,
|
|
5834
|
+
persistedInConfig: resolved.persistedInConfig
|
|
5835
|
+
};
|
|
5836
|
+
}
|
|
5837
|
+
|
|
5838
|
+
// src/worktree.ts
|
|
5170
5839
|
function resolveCreateRunRepo(args) {
|
|
5171
5840
|
const explicit = typeof args.repo === "string" ? args.repo.trim() : "";
|
|
5172
5841
|
if (explicit) return explicit;
|
|
@@ -5180,7 +5849,7 @@ function createRun(args) {
|
|
|
5180
5849
|
ensureGitRepo(repo);
|
|
5181
5850
|
const id = args.id ? validateRunId(String(args.id)) : timestampSlug(String(args.name || "run"));
|
|
5182
5851
|
const dir = runDirectory(id);
|
|
5183
|
-
if (
|
|
5852
|
+
if (existsSync18(dir)) failExists(`run already exists: ${id}`);
|
|
5184
5853
|
mkdirSync5(dir, { recursive: true });
|
|
5185
5854
|
const base = String(args.base || "origin/main");
|
|
5186
5855
|
const baseCommit = git(repo, ["rev-parse", base]).trim();
|
|
@@ -5194,19 +5863,11 @@ function createRun(args) {
|
|
|
5194
5863
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5195
5864
|
workers: {}
|
|
5196
5865
|
};
|
|
5197
|
-
writeJson(
|
|
5866
|
+
writeJson(path25.join(dir, "run.json"), run);
|
|
5198
5867
|
console.log(JSON.stringify({ runId: id, runDir: dir, repo, base, baseCommit }, null, 2));
|
|
5199
5868
|
}
|
|
5200
5869
|
function listRuns() {
|
|
5201
|
-
|
|
5202
|
-
const rows = listRunIds(runsDir).map((id) => readJson(path22.join(runDirectory(id), "run.json"), void 0)).filter(Boolean).map((run) => ({
|
|
5203
|
-
id: run.id,
|
|
5204
|
-
name: run.name,
|
|
5205
|
-
status: run.status,
|
|
5206
|
-
repo: run.repo,
|
|
5207
|
-
createdAt: run.createdAt
|
|
5208
|
-
}));
|
|
5209
|
-
console.log(JSON.stringify(rows, null, 2));
|
|
5870
|
+
listRunsCli();
|
|
5210
5871
|
}
|
|
5211
5872
|
function failExists(message) {
|
|
5212
5873
|
console.error(message);
|
|
@@ -5214,7 +5875,7 @@ function failExists(message) {
|
|
|
5214
5875
|
}
|
|
5215
5876
|
|
|
5216
5877
|
// src/sweep.ts
|
|
5217
|
-
import
|
|
5878
|
+
import path26 from "node:path";
|
|
5218
5879
|
async function sweepRun(args) {
|
|
5219
5880
|
const pipeline = args.pipeline === true || args.pipeline === "true";
|
|
5220
5881
|
try {
|
|
@@ -5227,7 +5888,7 @@ async function sweepRun(args) {
|
|
|
5227
5888
|
const releasedLocalOrphans = [];
|
|
5228
5889
|
for (const name of Object.keys(run.workers || {})) {
|
|
5229
5890
|
const worker = readJson(
|
|
5230
|
-
|
|
5891
|
+
path26.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
5231
5892
|
void 0
|
|
5232
5893
|
);
|
|
5233
5894
|
if (!worker || !worker.dispatched || !worker.taskId) continue;
|
|
@@ -5270,14 +5931,14 @@ async function sweepRun(args) {
|
|
|
5270
5931
|
}
|
|
5271
5932
|
|
|
5272
5933
|
// src/harness-storage-snapshot.ts
|
|
5273
|
-
import { existsSync as
|
|
5274
|
-
import
|
|
5934
|
+
import { existsSync as existsSync20, readdirSync as readdirSync6, statSync as statSync3 } from "node:fs";
|
|
5935
|
+
import path28 from "node:path";
|
|
5275
5936
|
|
|
5276
5937
|
// src/cleanup-dir-size.ts
|
|
5277
|
-
import { existsSync as
|
|
5278
|
-
import
|
|
5938
|
+
import { existsSync as existsSync19, readdirSync as readdirSync5, statSync as statSync2 } from "node:fs";
|
|
5939
|
+
import path27 from "node:path";
|
|
5279
5940
|
function directorySizeBytes(root, maxEntries = 5e4) {
|
|
5280
|
-
if (!
|
|
5941
|
+
if (!existsSync19(root)) return 0;
|
|
5281
5942
|
let total = 0;
|
|
5282
5943
|
let seen = 0;
|
|
5283
5944
|
const stack = [root];
|
|
@@ -5291,7 +5952,7 @@ function directorySizeBytes(root, maxEntries = 5e4) {
|
|
|
5291
5952
|
}
|
|
5292
5953
|
for (const name of entries) {
|
|
5293
5954
|
if (seen++ > maxEntries) return null;
|
|
5294
|
-
const full =
|
|
5955
|
+
const full = path27.join(current, name);
|
|
5295
5956
|
let st;
|
|
5296
5957
|
try {
|
|
5297
5958
|
st = statSync2(full);
|
|
@@ -5308,10 +5969,10 @@ function directorySizeBytes(root, maxEntries = 5e4) {
|
|
|
5308
5969
|
// src/harness-storage-snapshot.ts
|
|
5309
5970
|
function harnessStorageSnapshot(opts = {}) {
|
|
5310
5971
|
const harnessRoot = opts.harnessRoot ?? resolveHarnessRoot();
|
|
5311
|
-
const worktreesDir =
|
|
5972
|
+
const worktreesDir = path28.join(harnessRoot, "worktrees");
|
|
5312
5973
|
const now = opts.now ?? Date.now();
|
|
5313
5974
|
const scannedAt = new Date(now).toISOString();
|
|
5314
|
-
if (!
|
|
5975
|
+
if (!existsSync20(worktreesDir)) {
|
|
5315
5976
|
return {
|
|
5316
5977
|
harnessRoot,
|
|
5317
5978
|
worktreesDir,
|
|
@@ -5343,7 +6004,7 @@ function harnessStorageSnapshot(opts = {}) {
|
|
|
5343
6004
|
for (const runEntry of entries) {
|
|
5344
6005
|
if (!runEntry.isDirectory()) continue;
|
|
5345
6006
|
runCount += 1;
|
|
5346
|
-
const runPath =
|
|
6007
|
+
const runPath = path28.join(worktreesDir, runEntry.name);
|
|
5347
6008
|
try {
|
|
5348
6009
|
const st = statSync3(runPath);
|
|
5349
6010
|
oldestMs = oldestMs === null ? st.mtimeMs : Math.min(oldestMs, st.mtimeMs);
|
|
@@ -5377,149 +6038,58 @@ function harnessStorageSnapshot(opts = {}) {
|
|
|
5377
6038
|
};
|
|
5378
6039
|
}
|
|
5379
6040
|
|
|
5380
|
-
// src/cleanup
|
|
5381
|
-
import
|
|
5382
|
-
|
|
6041
|
+
// src/cleanup.ts
|
|
6042
|
+
import path38 from "node:path";
|
|
6043
|
+
|
|
6044
|
+
// src/cleanup-guards.ts
|
|
6045
|
+
import path29 from "node:path";
|
|
6046
|
+
|
|
6047
|
+
// src/cleanup-run-liveness.ts
|
|
6048
|
+
function isWorkerProcessLive(indexed) {
|
|
6049
|
+
if (indexed.status.alive) return true;
|
|
6050
|
+
if (indexed.worker.status === "running") return true;
|
|
6051
|
+
return false;
|
|
6052
|
+
}
|
|
6053
|
+
function isRunStaleActive(indexed) {
|
|
6054
|
+
if (TERMINAL_RUN_STATUSES.has(indexed.run.status)) return false;
|
|
6055
|
+
return deriveTerminalRunStatus(indexed.run) !== null;
|
|
6056
|
+
}
|
|
6057
|
+
function runBlocksWorktreeRemoval(indexed) {
|
|
6058
|
+
if (isWorkerProcessLive(indexed)) return true;
|
|
6059
|
+
if (indexed.worker.completionBlocker) return true;
|
|
6060
|
+
if (TERMINAL_RUN_STATUSES.has(indexed.run.status)) return false;
|
|
6061
|
+
if (isRunStaleActive(indexed)) return false;
|
|
6062
|
+
if (!isFinishedWorkerStatus(indexed.status)) return true;
|
|
6063
|
+
return deriveTerminalRunStatus(indexed.run) === null;
|
|
6064
|
+
}
|
|
6065
|
+
|
|
6066
|
+
// src/cleanup-build-cache-paths.ts
|
|
6067
|
+
var HARNESS_BUILD_CACHE_RELATIVE_PATHS = [
|
|
6068
|
+
".next",
|
|
6069
|
+
".turbo",
|
|
6070
|
+
"dist",
|
|
6071
|
+
"build",
|
|
6072
|
+
".cache",
|
|
6073
|
+
"node_modules/.cache"
|
|
6074
|
+
];
|
|
6075
|
+
function isGeneratedHarnessPath(pathPart) {
|
|
6076
|
+
const normalized = pathPart.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
6077
|
+
if (normalized === "node_modules" || normalized.startsWith("node_modules/")) return true;
|
|
6078
|
+
for (const rel of HARNESS_BUILD_CACHE_RELATIVE_PATHS) {
|
|
6079
|
+
if (normalized === rel || normalized.startsWith(`${rel}/`)) return true;
|
|
6080
|
+
}
|
|
6081
|
+
return false;
|
|
6082
|
+
}
|
|
5383
6083
|
|
|
5384
6084
|
// src/cleanup-guards-helpers.ts
|
|
5385
6085
|
function materialWorktreeChanges2(changedFiles) {
|
|
5386
6086
|
return changedFiles.filter((line) => {
|
|
5387
6087
|
const trimmed = line.trim();
|
|
5388
6088
|
const pathPart = trimmed.startsWith("??") ? trimmed.slice(2).trim() : trimmed.length > 3 ? trimmed.slice(3).trim() : trimmed;
|
|
5389
|
-
return
|
|
6089
|
+
return !isGeneratedHarnessPath(pathPart);
|
|
5390
6090
|
});
|
|
5391
6091
|
}
|
|
5392
6092
|
|
|
5393
|
-
// src/cleanup-orphan-safety.ts
|
|
5394
|
-
var DEFAULT_HEARTBEAT_FRESH_MS = 30 * 60 * 1e3;
|
|
5395
|
-
function assessOrphanWorktreeSafety(input) {
|
|
5396
|
-
const now = input.now ?? Date.now();
|
|
5397
|
-
const heartbeatFreshMs = input.heartbeatFreshMs ?? DEFAULT_HEARTBEAT_FRESH_MS;
|
|
5398
|
-
if (!existsSync20(input.worktreePath)) return null;
|
|
5399
|
-
if (input.runId && input.workerName) {
|
|
5400
|
-
const heartbeatPath = path26.join(
|
|
5401
|
-
input.harnessRoot,
|
|
5402
|
-
"runs",
|
|
5403
|
-
input.runId,
|
|
5404
|
-
"workers",
|
|
5405
|
-
input.workerName,
|
|
5406
|
-
"heartbeat.jsonl"
|
|
5407
|
-
);
|
|
5408
|
-
try {
|
|
5409
|
-
const mtime = statSync4(heartbeatPath).mtimeMs;
|
|
5410
|
-
if (now - mtime < heartbeatFreshMs) return "active_worker";
|
|
5411
|
-
} catch {
|
|
5412
|
-
}
|
|
5413
|
-
}
|
|
5414
|
-
const gitDir = path26.join(input.worktreePath, ".git");
|
|
5415
|
-
if (!existsSync20(gitDir)) return null;
|
|
5416
|
-
const porcelain = gitCapture(input.worktreePath, ["status", "--porcelain"]);
|
|
5417
|
-
if (porcelain.status !== 0) return "pr_or_unmerged_commits";
|
|
5418
|
-
const dirtyLines = porcelain.stdout.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
|
5419
|
-
if (materialWorktreeChanges2(dirtyLines).length > 0) return "dirty_worktree";
|
|
5420
|
-
const upstreamAhead = gitCapture(input.worktreePath, [
|
|
5421
|
-
"rev-list",
|
|
5422
|
-
"--count",
|
|
5423
|
-
"@{u}..HEAD"
|
|
5424
|
-
]);
|
|
5425
|
-
if (upstreamAhead.status === 0) {
|
|
5426
|
-
const count = Number(upstreamAhead.stdout.trim());
|
|
5427
|
-
if (Number.isFinite(count) && count > 0) return "pr_or_unmerged_commits";
|
|
5428
|
-
}
|
|
5429
|
-
const mainAhead = gitCapture(input.worktreePath, [
|
|
5430
|
-
"rev-list",
|
|
5431
|
-
"--count",
|
|
5432
|
-
"origin/main..HEAD"
|
|
5433
|
-
]);
|
|
5434
|
-
if (mainAhead.status !== 0) {
|
|
5435
|
-
if (upstreamAhead.status !== 0) return "pr_or_unmerged_commits";
|
|
5436
|
-
return null;
|
|
5437
|
-
}
|
|
5438
|
-
const mainCount = Number(mainAhead.stdout.trim());
|
|
5439
|
-
if (Number.isFinite(mainCount) && mainCount > 0) return "pr_or_unmerged_commits";
|
|
5440
|
-
return null;
|
|
5441
|
-
}
|
|
5442
|
-
|
|
5443
|
-
// src/cleanup.ts
|
|
5444
|
-
import path31 from "node:path";
|
|
5445
|
-
|
|
5446
|
-
// src/finalize.ts
|
|
5447
|
-
import path27 from "node:path";
|
|
5448
|
-
var ACTIVE_RUN_STATUSES = /* @__PURE__ */ new Set([
|
|
5449
|
-
"running",
|
|
5450
|
-
"dispatching",
|
|
5451
|
-
"pending",
|
|
5452
|
-
"queued",
|
|
5453
|
-
"needs_attention"
|
|
5454
|
-
]);
|
|
5455
|
-
var TERMINAL_RUN_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "cancelled", "done"]);
|
|
5456
|
-
function deriveTerminalRunStatus(run) {
|
|
5457
|
-
const names = Object.keys(run.workers || {});
|
|
5458
|
-
if (names.length === 0) return "failed";
|
|
5459
|
-
let anyAlive = false;
|
|
5460
|
-
let anyResult = false;
|
|
5461
|
-
let anyCompletionBlocked = false;
|
|
5462
|
-
let anyLandingBlocked = false;
|
|
5463
|
-
for (const name of names) {
|
|
5464
|
-
const worker = readJson(
|
|
5465
|
-
path27.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
5466
|
-
void 0
|
|
5467
|
-
);
|
|
5468
|
-
if (!worker) continue;
|
|
5469
|
-
const status = computeWorkerStatus(worker, {
|
|
5470
|
-
base: run.base,
|
|
5471
|
-
baseCommit: run.baseCommit
|
|
5472
|
-
});
|
|
5473
|
-
if (status.alive && !status.finalResult) {
|
|
5474
|
-
anyAlive = true;
|
|
5475
|
-
break;
|
|
5476
|
-
}
|
|
5477
|
-
if (typeof worker.completionBlocker === "string" && worker.completionBlocker) {
|
|
5478
|
-
anyCompletionBlocked = true;
|
|
5479
|
-
}
|
|
5480
|
-
if (isLandingBlockedWorkerStatus(status)) {
|
|
5481
|
-
anyLandingBlocked = true;
|
|
5482
|
-
}
|
|
5483
|
-
if (status.finalResult && status.attention.state === "done") anyResult = true;
|
|
5484
|
-
}
|
|
5485
|
-
if (anyAlive) return null;
|
|
5486
|
-
if (anyCompletionBlocked) return null;
|
|
5487
|
-
if (anyLandingBlocked) return null;
|
|
5488
|
-
return anyResult ? "completed" : "failed";
|
|
5489
|
-
}
|
|
5490
|
-
function finalizeStaleRuns() {
|
|
5491
|
-
const finalized = [];
|
|
5492
|
-
for (const run of listRunRecords()) {
|
|
5493
|
-
if (!ACTIVE_RUN_STATUSES.has(run.status)) continue;
|
|
5494
|
-
const next = deriveTerminalRunStatus(run);
|
|
5495
|
-
if (!next || next === run.status) continue;
|
|
5496
|
-
const from = run.status;
|
|
5497
|
-
run.status = next;
|
|
5498
|
-
saveRun(run);
|
|
5499
|
-
finalized.push({ runId: run.id, from, to: next });
|
|
5500
|
-
}
|
|
5501
|
-
return finalized;
|
|
5502
|
-
}
|
|
5503
|
-
|
|
5504
|
-
// src/cleanup-run-liveness.ts
|
|
5505
|
-
function isWorkerProcessLive(indexed) {
|
|
5506
|
-
if (indexed.status.alive) return true;
|
|
5507
|
-
if (indexed.worker.status === "running") return true;
|
|
5508
|
-
return false;
|
|
5509
|
-
}
|
|
5510
|
-
function isRunStaleActive(indexed) {
|
|
5511
|
-
if (TERMINAL_RUN_STATUSES.has(indexed.run.status)) return false;
|
|
5512
|
-
return deriveTerminalRunStatus(indexed.run) !== null;
|
|
5513
|
-
}
|
|
5514
|
-
function runBlocksWorktreeRemoval(indexed) {
|
|
5515
|
-
if (isWorkerProcessLive(indexed)) return true;
|
|
5516
|
-
if (indexed.worker.completionBlocker) return true;
|
|
5517
|
-
if (TERMINAL_RUN_STATUSES.has(indexed.run.status)) return false;
|
|
5518
|
-
if (isRunStaleActive(indexed)) return false;
|
|
5519
|
-
if (!isFinishedWorkerStatus(indexed.status)) return true;
|
|
5520
|
-
return deriveTerminalRunStatus(indexed.run) === null;
|
|
5521
|
-
}
|
|
5522
|
-
|
|
5523
6093
|
// src/cleanup-guards.ts
|
|
5524
6094
|
function prUrlFromFinalResult(finalResult) {
|
|
5525
6095
|
if (typeof finalResult === "string") {
|
|
@@ -5581,30 +6151,22 @@ function skipWorktreeRemoval(input) {
|
|
|
5581
6151
|
}
|
|
5582
6152
|
return null;
|
|
5583
6153
|
}
|
|
5584
|
-
function
|
|
5585
|
-
const { indexed,
|
|
5586
|
-
if (ageMs < nodeModulesAgeMs) return "below_age_threshold";
|
|
5587
|
-
if (
|
|
5588
|
-
if (isWorkerProcessLive(indexed)) return "active_worker";
|
|
5589
|
-
if (indexed.
|
|
5590
|
-
if (!isFinishedWorkerStatus(indexed.status)) return "run_still_active";
|
|
5591
|
-
if (hasUnrestorableWorktreeChanges(indexed.status)) return "dirty_worktree";
|
|
5592
|
-
const landing = assessWorkerLanding({
|
|
5593
|
-
finalResult: indexed.status.finalResult,
|
|
5594
|
-
changedFiles: indexed.status.changedFiles,
|
|
5595
|
-
gitAncestry: indexed.status.gitAncestry,
|
|
5596
|
-
prUrl: prUrlFromFinalResult(indexed.status.finalResult)
|
|
5597
|
-
});
|
|
5598
|
-
if (landing.blocked && materialWorktreeChanges2(indexed.status.changedFiles).length > 0) {
|
|
5599
|
-
return "landing_blocked";
|
|
5600
|
-
}
|
|
6154
|
+
function skipDependencyCacheRemoval(input) {
|
|
6155
|
+
const { indexed, nodeModulesAgeMs, ageMs, worktreePath, activeWorktreePaths, diskPressure } = input;
|
|
6156
|
+
if (!diskPressure && ageMs < nodeModulesAgeMs) return "below_age_threshold";
|
|
6157
|
+
if (activeWorktreePaths.has(path29.resolve(worktreePath))) return "active_worker";
|
|
6158
|
+
if (indexed && isWorkerProcessLive(indexed)) return "active_worker";
|
|
6159
|
+
if (indexed && hasUnrestorableWorktreeChanges(indexed.status)) return "dirty_worktree";
|
|
5601
6160
|
return null;
|
|
5602
6161
|
}
|
|
6162
|
+
function skipBuildCacheRemoval(input) {
|
|
6163
|
+
return skipDependencyCacheRemoval(input);
|
|
6164
|
+
}
|
|
5603
6165
|
|
|
5604
6166
|
// src/cleanup-execute.ts
|
|
5605
6167
|
import { existsSync as existsSync21, rmSync } from "node:fs";
|
|
5606
|
-
import
|
|
5607
|
-
function
|
|
6168
|
+
import path30 from "node:path";
|
|
6169
|
+
function removeDependencyCache(candidate, execute) {
|
|
5608
6170
|
if (!existsSync21(candidate.path)) {
|
|
5609
6171
|
return {
|
|
5610
6172
|
...candidate,
|
|
@@ -5635,6 +6197,15 @@ function removeNodeModules(candidate, execute) {
|
|
|
5635
6197
|
};
|
|
5636
6198
|
}
|
|
5637
6199
|
}
|
|
6200
|
+
function removeNodeModules(candidate, execute) {
|
|
6201
|
+
return removeDependencyCache(candidate, execute);
|
|
6202
|
+
}
|
|
6203
|
+
function removeNextCache(candidate, execute) {
|
|
6204
|
+
return removeDependencyCache(candidate, execute);
|
|
6205
|
+
}
|
|
6206
|
+
function removeBuildCache(candidate, execute) {
|
|
6207
|
+
return removeDependencyCache(candidate, execute);
|
|
6208
|
+
}
|
|
5638
6209
|
function removeWorktree(candidate, execute) {
|
|
5639
6210
|
if (!existsSync21(candidate.path)) {
|
|
5640
6211
|
return {
|
|
@@ -5672,74 +6243,97 @@ function removeWorktree(candidate, execute) {
|
|
|
5672
6243
|
};
|
|
5673
6244
|
}
|
|
5674
6245
|
}
|
|
6246
|
+
function isHarnessDependencyCachePath(targetPath, harnessRoot, worktreesDir, cacheDirName) {
|
|
6247
|
+
const resolved = path30.resolve(targetPath);
|
|
6248
|
+
const suffix = `${path30.sep}${cacheDirName}`;
|
|
6249
|
+
const cachePath = resolved.endsWith(suffix) ? resolved : null;
|
|
6250
|
+
if (!cachePath) return "path_outside_harness";
|
|
6251
|
+
const rel = path30.relative(worktreesDir, cachePath);
|
|
6252
|
+
if (rel.startsWith("..") || path30.isAbsolute(rel)) return "path_outside_harness";
|
|
6253
|
+
const parts = rel.split(path30.sep);
|
|
6254
|
+
if (parts.length < 3 || parts[parts.length - 1] !== cacheDirName) return "path_outside_harness";
|
|
6255
|
+
if (!resolved.startsWith(path30.resolve(harnessRoot))) return "path_outside_harness";
|
|
6256
|
+
return null;
|
|
6257
|
+
}
|
|
5675
6258
|
function isHarnessNodeModulesPath(targetPath, harnessRoot, worktreesDir) {
|
|
5676
|
-
|
|
5677
|
-
|
|
5678
|
-
|
|
5679
|
-
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
|
|
5683
|
-
|
|
6259
|
+
return isHarnessDependencyCachePath(targetPath, harnessRoot, worktreesDir, "node_modules");
|
|
6260
|
+
}
|
|
6261
|
+
function isHarnessNextCachePath(targetPath, harnessRoot, worktreesDir) {
|
|
6262
|
+
return isHarnessDependencyCachePath(targetPath, harnessRoot, worktreesDir, ".next");
|
|
6263
|
+
}
|
|
6264
|
+
function isHarnessBuildCachePath(targetPath, harnessRoot, worktreesDir) {
|
|
6265
|
+
const resolved = path30.resolve(targetPath);
|
|
6266
|
+
const relToWt = path30.relative(worktreesDir, resolved);
|
|
6267
|
+
if (relToWt.startsWith("..") || path30.isAbsolute(relToWt)) return "path_outside_harness";
|
|
6268
|
+
const parts = relToWt.split(path30.sep);
|
|
6269
|
+
if (parts.length < 3) return "path_outside_harness";
|
|
6270
|
+
if (!resolved.startsWith(path30.resolve(harnessRoot))) return "path_outside_harness";
|
|
5684
6271
|
return null;
|
|
5685
6272
|
}
|
|
5686
6273
|
|
|
5687
6274
|
// src/cleanup-scan.ts
|
|
5688
|
-
import { existsSync as existsSync22, readdirSync as readdirSync7, statSync as
|
|
5689
|
-
import
|
|
6275
|
+
import { existsSync as existsSync22, readdirSync as readdirSync7, statSync as statSync4 } from "node:fs";
|
|
6276
|
+
import path31 from "node:path";
|
|
5690
6277
|
function pathAgeMs(target, now) {
|
|
5691
6278
|
try {
|
|
5692
|
-
const mtime =
|
|
6279
|
+
const mtime = statSync4(target).mtimeMs;
|
|
5693
6280
|
return Math.max(0, now - mtime);
|
|
5694
6281
|
} catch {
|
|
5695
6282
|
return 0;
|
|
5696
6283
|
}
|
|
5697
6284
|
}
|
|
5698
6285
|
function isPathInside(child, parent) {
|
|
5699
|
-
const rel =
|
|
5700
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
6286
|
+
const rel = path31.relative(parent, child);
|
|
6287
|
+
return rel === "" || !rel.startsWith("..") && !path31.isAbsolute(rel);
|
|
5701
6288
|
}
|
|
5702
|
-
function
|
|
5703
|
-
const
|
|
5704
|
-
const
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
|
|
5708
|
-
|
|
5709
|
-
const resolved = path29.resolve(nm);
|
|
6289
|
+
function collectBuildCacheForWorktree(worktreePath, opts, seen, meta) {
|
|
6290
|
+
const out = [];
|
|
6291
|
+
for (const rel of HARNESS_BUILD_CACHE_RELATIVE_PATHS) {
|
|
6292
|
+
if (rel === ".next") continue;
|
|
6293
|
+
const target = path31.join(worktreePath, rel);
|
|
6294
|
+
if (!existsSync22(target)) continue;
|
|
6295
|
+
const resolved = path31.resolve(target);
|
|
5710
6296
|
if (seen.has(resolved)) continue;
|
|
6297
|
+
if (!isPathInside(resolved, opts.harnessRoot)) continue;
|
|
5711
6298
|
seen.add(resolved);
|
|
5712
|
-
|
|
5713
|
-
kind: "
|
|
6299
|
+
out.push({
|
|
6300
|
+
kind: "remove_build_cache",
|
|
5714
6301
|
path: resolved,
|
|
5715
6302
|
bytes: null,
|
|
5716
|
-
runId:
|
|
5717
|
-
worker:
|
|
5718
|
-
repo:
|
|
6303
|
+
runId: meta.runId,
|
|
6304
|
+
worker: meta.worker,
|
|
6305
|
+
repo: meta.repo,
|
|
5719
6306
|
ageMs: pathAgeMs(resolved, opts.now)
|
|
5720
6307
|
});
|
|
5721
6308
|
}
|
|
6309
|
+
return out;
|
|
6310
|
+
}
|
|
6311
|
+
function scanBuildCacheCandidates(opts) {
|
|
6312
|
+
const candidates = [];
|
|
6313
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6314
|
+
for (const entry of opts.index.values()) {
|
|
6315
|
+
if (opts.runIdFilter && entry.runId !== opts.runIdFilter) continue;
|
|
6316
|
+
candidates.push(
|
|
6317
|
+
...collectBuildCacheForWorktree(entry.worktreePath, opts, seen, {
|
|
6318
|
+
runId: entry.runId,
|
|
6319
|
+
worker: entry.workerName,
|
|
6320
|
+
repo: entry.run.repo
|
|
6321
|
+
})
|
|
6322
|
+
);
|
|
6323
|
+
}
|
|
5722
6324
|
if (!opts.includeOrphans || !existsSync22(opts.worktreesDir)) return candidates;
|
|
5723
6325
|
for (const runEntry of readdirSync7(opts.worktreesDir, { withFileTypes: true })) {
|
|
5724
6326
|
if (!runEntry.isDirectory()) continue;
|
|
5725
|
-
const runPath =
|
|
6327
|
+
const runPath = path31.join(opts.worktreesDir, runEntry.name);
|
|
5726
6328
|
for (const workerEntry of readdirSync7(runPath, { withFileTypes: true })) {
|
|
5727
6329
|
if (!workerEntry.isDirectory()) continue;
|
|
5728
|
-
const worktreePath =
|
|
5729
|
-
|
|
5730
|
-
|
|
5731
|
-
|
|
5732
|
-
|
|
5733
|
-
|
|
5734
|
-
|
|
5735
|
-
candidates.push({
|
|
5736
|
-
kind: "remove_node_modules",
|
|
5737
|
-
path: resolved,
|
|
5738
|
-
bytes: null,
|
|
5739
|
-
runId: runEntry.name,
|
|
5740
|
-
worker: workerEntry.name,
|
|
5741
|
-
ageMs: pathAgeMs(resolved, opts.now)
|
|
5742
|
-
});
|
|
6330
|
+
const worktreePath = path31.join(runPath, workerEntry.name);
|
|
6331
|
+
candidates.push(
|
|
6332
|
+
...collectBuildCacheForWorktree(worktreePath, opts, seen, {
|
|
6333
|
+
runId: runEntry.name,
|
|
6334
|
+
worker: workerEntry.name
|
|
6335
|
+
})
|
|
6336
|
+
);
|
|
5743
6337
|
}
|
|
5744
6338
|
}
|
|
5745
6339
|
return candidates;
|
|
@@ -5771,12 +6365,12 @@ function scanWorktreeCandidates(opts) {
|
|
|
5771
6365
|
if (!orphanEnabled || !existsSync22(opts.worktreesDir)) return candidates;
|
|
5772
6366
|
const indexedPaths = /* @__PURE__ */ new Set();
|
|
5773
6367
|
for (const entry of opts.index.values()) {
|
|
5774
|
-
indexedPaths.add(
|
|
6368
|
+
indexedPaths.add(path31.resolve(entry.worktreePath));
|
|
5775
6369
|
}
|
|
5776
6370
|
for (const runEntry of readdirSync7(opts.worktreesDir, { withFileTypes: true })) {
|
|
5777
6371
|
if (!runEntry.isDirectory()) continue;
|
|
5778
6372
|
if (opts.runIdFilter && runEntry.name !== opts.runIdFilter) continue;
|
|
5779
|
-
const runPath =
|
|
6373
|
+
const runPath = path31.join(opts.worktreesDir, runEntry.name);
|
|
5780
6374
|
let workerEntries;
|
|
5781
6375
|
try {
|
|
5782
6376
|
workerEntries = readdirSync7(runPath, { withFileTypes: true });
|
|
@@ -5785,7 +6379,7 @@ function scanWorktreeCandidates(opts) {
|
|
|
5785
6379
|
}
|
|
5786
6380
|
for (const workerEntry of workerEntries) {
|
|
5787
6381
|
if (!workerEntry.isDirectory()) continue;
|
|
5788
|
-
const worktreePath =
|
|
6382
|
+
const worktreePath = path31.resolve(path31.join(runPath, workerEntry.name));
|
|
5789
6383
|
if (seen.has(worktreePath)) continue;
|
|
5790
6384
|
if (indexedPaths.has(worktreePath)) continue;
|
|
5791
6385
|
if (!isPathInside(worktreePath, opts.harnessRoot)) continue;
|
|
@@ -5803,18 +6397,191 @@ function scanWorktreeCandidates(opts) {
|
|
|
5803
6397
|
return candidates;
|
|
5804
6398
|
}
|
|
5805
6399
|
|
|
6400
|
+
// src/cleanup-dependency-scan.ts
|
|
6401
|
+
import { existsSync as existsSync23, readdirSync as readdirSync8, statSync as statSync5 } from "node:fs";
|
|
6402
|
+
import path32 from "node:path";
|
|
6403
|
+
var DEPENDENCY_CACHE_DIRS = [
|
|
6404
|
+
{ dirName: "node_modules", kind: "remove_node_modules" },
|
|
6405
|
+
{ dirName: ".next", kind: "remove_next_cache" }
|
|
6406
|
+
];
|
|
6407
|
+
function pathAgeMs2(target, now) {
|
|
6408
|
+
try {
|
|
6409
|
+
const mtime = statSync5(target).mtimeMs;
|
|
6410
|
+
return Math.max(0, now - mtime);
|
|
6411
|
+
} catch {
|
|
6412
|
+
return 0;
|
|
6413
|
+
}
|
|
6414
|
+
}
|
|
6415
|
+
function isPathInside2(child, parent) {
|
|
6416
|
+
const rel = path32.relative(parent, child);
|
|
6417
|
+
return rel === "" || !rel.startsWith("..") && !path32.isAbsolute(rel);
|
|
6418
|
+
}
|
|
6419
|
+
function pushCandidate2(candidates, seen, opts, targetPath, kind, meta) {
|
|
6420
|
+
if (!existsSync23(targetPath)) return;
|
|
6421
|
+
const resolved = path32.resolve(targetPath);
|
|
6422
|
+
if (seen.has(resolved)) return;
|
|
6423
|
+
if (!isPathInside2(resolved, opts.harnessRoot)) return;
|
|
6424
|
+
seen.add(resolved);
|
|
6425
|
+
candidates.push({
|
|
6426
|
+
kind,
|
|
6427
|
+
path: resolved,
|
|
6428
|
+
bytes: null,
|
|
6429
|
+
harnessRoot: opts.harnessRoot,
|
|
6430
|
+
runId: meta.runId,
|
|
6431
|
+
worker: meta.worker,
|
|
6432
|
+
repo: meta.repo,
|
|
6433
|
+
ageMs: pathAgeMs2(resolved, opts.now)
|
|
6434
|
+
});
|
|
6435
|
+
}
|
|
6436
|
+
function scanWorktreeDependencyCaches(candidates, seen, opts, worktreePath, meta) {
|
|
6437
|
+
for (const entry of DEPENDENCY_CACHE_DIRS) {
|
|
6438
|
+
pushCandidate2(candidates, seen, opts, path32.join(worktreePath, entry.dirName), entry.kind, meta);
|
|
6439
|
+
}
|
|
6440
|
+
}
|
|
6441
|
+
function scanDependencyCacheCandidates(opts) {
|
|
6442
|
+
const candidates = [];
|
|
6443
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6444
|
+
for (const entry of opts.index.values()) {
|
|
6445
|
+
if (opts.runIdFilter && entry.runId !== opts.runIdFilter) continue;
|
|
6446
|
+
scanWorktreeDependencyCaches(candidates, seen, opts, entry.worktreePath, {
|
|
6447
|
+
runId: entry.runId,
|
|
6448
|
+
worker: entry.workerName,
|
|
6449
|
+
repo: entry.run.repo
|
|
6450
|
+
});
|
|
6451
|
+
}
|
|
6452
|
+
if (!existsSync23(opts.worktreesDir)) return candidates;
|
|
6453
|
+
for (const runEntry of readdirSync8(opts.worktreesDir, { withFileTypes: true })) {
|
|
6454
|
+
if (!runEntry.isDirectory()) continue;
|
|
6455
|
+
if (opts.runIdFilter && runEntry.name !== opts.runIdFilter) continue;
|
|
6456
|
+
const runPath = path32.join(opts.worktreesDir, runEntry.name);
|
|
6457
|
+
let workerEntries;
|
|
6458
|
+
try {
|
|
6459
|
+
workerEntries = readdirSync8(runPath, { withFileTypes: true });
|
|
6460
|
+
} catch {
|
|
6461
|
+
continue;
|
|
6462
|
+
}
|
|
6463
|
+
for (const workerEntry of workerEntries) {
|
|
6464
|
+
if (!workerEntry.isDirectory()) continue;
|
|
6465
|
+
const worktreePath = path32.join(runPath, workerEntry.name);
|
|
6466
|
+
scanWorktreeDependencyCaches(candidates, seen, opts, worktreePath, {
|
|
6467
|
+
runId: runEntry.name,
|
|
6468
|
+
worker: workerEntry.name
|
|
6469
|
+
});
|
|
6470
|
+
}
|
|
6471
|
+
}
|
|
6472
|
+
return candidates;
|
|
6473
|
+
}
|
|
6474
|
+
|
|
6475
|
+
// src/cleanup-duplicate-worktrees.ts
|
|
6476
|
+
import { existsSync as existsSync24, statSync as statSync6 } from "node:fs";
|
|
6477
|
+
import path33 from "node:path";
|
|
6478
|
+
function pathAgeMs3(target, now) {
|
|
6479
|
+
try {
|
|
6480
|
+
const mtime = statSync6(target).mtimeMs;
|
|
6481
|
+
return Math.max(0, now - mtime);
|
|
6482
|
+
} catch {
|
|
6483
|
+
return 0;
|
|
6484
|
+
}
|
|
6485
|
+
}
|
|
6486
|
+
function parseWorktreePorcelain(output) {
|
|
6487
|
+
const records = [];
|
|
6488
|
+
let current = null;
|
|
6489
|
+
for (const line of output.split("\n")) {
|
|
6490
|
+
if (!line.trim()) continue;
|
|
6491
|
+
const [key, ...rest] = line.split(" ");
|
|
6492
|
+
const value = rest.join(" ");
|
|
6493
|
+
if (key === "worktree") {
|
|
6494
|
+
if (current) records.push(current);
|
|
6495
|
+
current = { path: value };
|
|
6496
|
+
continue;
|
|
6497
|
+
}
|
|
6498
|
+
if (!current) continue;
|
|
6499
|
+
if (key === "branch") current.branch = value;
|
|
6500
|
+
if (key === "HEAD") current.head = value;
|
|
6501
|
+
if (key === "bare") current.bare = true;
|
|
6502
|
+
}
|
|
6503
|
+
if (current) records.push(current);
|
|
6504
|
+
return records;
|
|
6505
|
+
}
|
|
6506
|
+
function isUnderWorktreesDir(worktreePath, worktreesDir) {
|
|
6507
|
+
const rel = path33.relative(path33.resolve(worktreesDir), path33.resolve(worktreePath));
|
|
6508
|
+
return rel !== "" && !rel.startsWith("..") && !path33.isAbsolute(rel);
|
|
6509
|
+
}
|
|
6510
|
+
function isCleanWorktree(worktreePath, repoRoot) {
|
|
6511
|
+
try {
|
|
6512
|
+
const porcelain = git(repoRoot, ["-C", worktreePath, "status", "--porcelain"], {
|
|
6513
|
+
allowFailure: true
|
|
6514
|
+
});
|
|
6515
|
+
return !String(porcelain || "").trim();
|
|
6516
|
+
} catch {
|
|
6517
|
+
return false;
|
|
6518
|
+
}
|
|
6519
|
+
}
|
|
6520
|
+
function scanDuplicateWorktreeCandidates(opts) {
|
|
6521
|
+
if (!opts.includeOrphans || !existsSync24(opts.worktreesDir)) return [];
|
|
6522
|
+
const repos = /* @__PURE__ */ new Set();
|
|
6523
|
+
for (const entry of opts.index.values()) {
|
|
6524
|
+
if (entry.run.repo) repos.add(path33.resolve(entry.run.repo));
|
|
6525
|
+
}
|
|
6526
|
+
const indexedPaths = /* @__PURE__ */ new Set();
|
|
6527
|
+
for (const entry of opts.index.values()) {
|
|
6528
|
+
indexedPaths.add(path33.resolve(entry.worktreePath));
|
|
6529
|
+
}
|
|
6530
|
+
const candidates = [];
|
|
6531
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6532
|
+
for (const repoRoot of repos) {
|
|
6533
|
+
let porcelain;
|
|
6534
|
+
try {
|
|
6535
|
+
porcelain = git(repoRoot, ["worktree", "list", "--porcelain"], { allowFailure: true });
|
|
6536
|
+
} catch {
|
|
6537
|
+
continue;
|
|
6538
|
+
}
|
|
6539
|
+
const worktrees = parseWorktreePorcelain(porcelain);
|
|
6540
|
+
for (const wt of worktrees) {
|
|
6541
|
+
const resolved = path33.resolve(wt.path);
|
|
6542
|
+
if (resolved === path33.resolve(repoRoot)) continue;
|
|
6543
|
+
if (!isUnderWorktreesDir(resolved, opts.worktreesDir)) continue;
|
|
6544
|
+
if (indexedPaths.has(resolved)) continue;
|
|
6545
|
+
if (seen.has(resolved)) continue;
|
|
6546
|
+
if (!existsSync24(resolved)) continue;
|
|
6547
|
+
if (!isCleanWorktree(resolved, repoRoot)) continue;
|
|
6548
|
+
const rel = path33.relative(opts.worktreesDir, resolved);
|
|
6549
|
+
const parts = rel.split(path33.sep);
|
|
6550
|
+
const runId = parts[0];
|
|
6551
|
+
const worker = parts[1] ?? "unknown";
|
|
6552
|
+
seen.add(resolved);
|
|
6553
|
+
candidates.push({
|
|
6554
|
+
kind: "remove_worktree",
|
|
6555
|
+
path: resolved,
|
|
6556
|
+
bytes: null,
|
|
6557
|
+
runId,
|
|
6558
|
+
worker,
|
|
6559
|
+
repo: repoRoot,
|
|
6560
|
+
ageMs: pathAgeMs3(resolved, opts.now)
|
|
6561
|
+
});
|
|
6562
|
+
}
|
|
6563
|
+
}
|
|
6564
|
+
return candidates;
|
|
6565
|
+
}
|
|
6566
|
+
|
|
5806
6567
|
// src/cleanup-worktree-index.ts
|
|
5807
|
-
import
|
|
5808
|
-
function
|
|
6568
|
+
import path34 from "node:path";
|
|
6569
|
+
function buildWorktreeIndexAt(harnessRoot) {
|
|
5809
6570
|
const index = /* @__PURE__ */ new Map();
|
|
5810
|
-
for (const run of
|
|
6571
|
+
for (const run of listRunRecordsForHarnessRoot(harnessRoot)) {
|
|
5811
6572
|
for (const name of Object.keys(run.workers || {})) {
|
|
5812
|
-
const workerPath =
|
|
6573
|
+
const workerPath = path34.join(
|
|
6574
|
+
runDirectoryAt(harnessRoot, run.id),
|
|
6575
|
+
"workers",
|
|
6576
|
+
safeSlug(name),
|
|
6577
|
+
"worker.json"
|
|
6578
|
+
);
|
|
5813
6579
|
const worker = readJson(workerPath, void 0);
|
|
5814
6580
|
if (!worker?.worktreePath) continue;
|
|
5815
6581
|
const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
|
|
5816
|
-
index.set(
|
|
5817
|
-
|
|
6582
|
+
index.set(path34.resolve(worker.worktreePath), {
|
|
6583
|
+
harnessRoot,
|
|
6584
|
+
worktreePath: path34.resolve(worker.worktreePath),
|
|
5818
6585
|
runId: run.id,
|
|
5819
6586
|
workerName: name,
|
|
5820
6587
|
run,
|
|
@@ -5872,12 +6639,162 @@ function resolvePipelineHarnessRetention(runId) {
|
|
|
5872
6639
|
});
|
|
5873
6640
|
}
|
|
5874
6641
|
|
|
5875
|
-
// src/cleanup.ts
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
6642
|
+
// src/cleanup-orphan-safety.ts
|
|
6643
|
+
import { existsSync as existsSync25, statSync as statSync7 } from "node:fs";
|
|
6644
|
+
import path35 from "node:path";
|
|
6645
|
+
var DEFAULT_HEARTBEAT_FRESH_MS = 30 * 60 * 1e3;
|
|
6646
|
+
function assessOrphanWorktreeSafety(input) {
|
|
6647
|
+
const now = input.now ?? Date.now();
|
|
6648
|
+
const heartbeatFreshMs = input.heartbeatFreshMs ?? DEFAULT_HEARTBEAT_FRESH_MS;
|
|
6649
|
+
if (!existsSync25(input.worktreePath)) return null;
|
|
6650
|
+
if (input.runId && input.workerName) {
|
|
6651
|
+
const heartbeatPath = path35.join(
|
|
6652
|
+
input.harnessRoot,
|
|
6653
|
+
"runs",
|
|
6654
|
+
input.runId,
|
|
6655
|
+
"workers",
|
|
6656
|
+
input.workerName,
|
|
6657
|
+
"heartbeat.jsonl"
|
|
6658
|
+
);
|
|
6659
|
+
try {
|
|
6660
|
+
const mtime = statSync7(heartbeatPath).mtimeMs;
|
|
6661
|
+
if (now - mtime < heartbeatFreshMs) return "active_worker";
|
|
6662
|
+
} catch {
|
|
6663
|
+
}
|
|
6664
|
+
}
|
|
6665
|
+
const gitDir = path35.join(input.worktreePath, ".git");
|
|
6666
|
+
if (!existsSync25(gitDir)) return null;
|
|
6667
|
+
const porcelain = gitCapture(input.worktreePath, ["status", "--porcelain"]);
|
|
6668
|
+
if (porcelain.status !== 0) return "pr_or_unmerged_commits";
|
|
6669
|
+
const dirtyLines = porcelain.stdout.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
|
6670
|
+
if (materialWorktreeChanges2(dirtyLines).length > 0) return "dirty_worktree";
|
|
6671
|
+
const upstreamAhead = gitCapture(input.worktreePath, [
|
|
6672
|
+
"rev-list",
|
|
6673
|
+
"--count",
|
|
6674
|
+
"@{u}..HEAD"
|
|
6675
|
+
]);
|
|
6676
|
+
if (upstreamAhead.status === 0) {
|
|
6677
|
+
const count = Number(upstreamAhead.stdout.trim());
|
|
6678
|
+
if (Number.isFinite(count) && count > 0) return "pr_or_unmerged_commits";
|
|
6679
|
+
}
|
|
6680
|
+
const mainAhead = gitCapture(input.worktreePath, [
|
|
6681
|
+
"rev-list",
|
|
6682
|
+
"--count",
|
|
6683
|
+
"origin/main..HEAD"
|
|
6684
|
+
]);
|
|
6685
|
+
if (mainAhead.status !== 0) {
|
|
6686
|
+
if (upstreamAhead.status !== 0) return "pr_or_unmerged_commits";
|
|
6687
|
+
return null;
|
|
6688
|
+
}
|
|
6689
|
+
const mainCount = Number(mainAhead.stdout.trim());
|
|
6690
|
+
if (Number.isFinite(mainCount) && mainCount > 0) return "pr_or_unmerged_commits";
|
|
6691
|
+
return null;
|
|
6692
|
+
}
|
|
6693
|
+
|
|
6694
|
+
// src/cleanup-harness-roots.ts
|
|
6695
|
+
import { existsSync as existsSync26 } from "node:fs";
|
|
6696
|
+
import { homedir as homedir6 } from "node:os";
|
|
6697
|
+
import path36 from "node:path";
|
|
6698
|
+
var WELL_KNOWN_HARNESS_SCAN_ROOTS = [
|
|
6699
|
+
"/var/tmp/kynver-harness",
|
|
6700
|
+
path36.join(homedir6(), ".openclaw", "harness")
|
|
6701
|
+
];
|
|
6702
|
+
function addRoot(seen, roots, candidate) {
|
|
6703
|
+
if (!candidate?.trim()) return;
|
|
6704
|
+
const resolved = path36.resolve(resolveUserPath(candidate.trim()));
|
|
6705
|
+
if (seen.has(resolved)) return;
|
|
6706
|
+
seen.add(resolved);
|
|
6707
|
+
roots.push(resolved);
|
|
6708
|
+
}
|
|
6709
|
+
function shouldScanWellKnownRoots(options) {
|
|
6710
|
+
if (options.scanWellKnown != null) return options.scanWellKnown;
|
|
6711
|
+
if (process.env.VITEST === "true") return false;
|
|
6712
|
+
return process.env.KYNVER_CLEANUP_SCAN_WELL_KNOWN !== "0" && !["0", "false", "no"].includes((process.env.KYNVER_CLEANUP_SCAN_WELL_KNOWN ?? "").toLowerCase());
|
|
6713
|
+
}
|
|
6714
|
+
function resolveHarnessScanRoots(options = {}) {
|
|
6715
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6716
|
+
const roots = [];
|
|
6717
|
+
addRoot(seen, roots, options.harnessRoot ?? resolveHarnessRoot());
|
|
6718
|
+
const extra = process.env.KYNVER_CLEANUP_EXTRA_ROOTS?.split(",").map((part) => part.trim()).filter(Boolean);
|
|
6719
|
+
for (const candidate of extra ?? []) addRoot(seen, roots, candidate);
|
|
6720
|
+
if (shouldScanWellKnownRoots(options)) {
|
|
6721
|
+
for (const candidate of WELL_KNOWN_HARNESS_SCAN_ROOTS) {
|
|
6722
|
+
const resolved = path36.resolve(candidate);
|
|
6723
|
+
if (!seen.has(resolved) && existsSync26(resolved)) addRoot(seen, roots, resolved);
|
|
6724
|
+
}
|
|
6725
|
+
}
|
|
6726
|
+
return roots;
|
|
6727
|
+
}
|
|
6728
|
+
|
|
6729
|
+
// src/cleanup-active-worktrees.ts
|
|
6730
|
+
import path37 from "node:path";
|
|
6731
|
+
function isActiveHarnessWorker2(worker, runBase, runBaseCommit) {
|
|
6732
|
+
const status = computeWorkerStatus(worker, { base: runBase, baseCommit: runBaseCommit });
|
|
6733
|
+
return status.alive && !status.finalResult && status.attention.state !== "done";
|
|
6734
|
+
}
|
|
6735
|
+
function collectActiveWorktreeGuards(harnessRoots) {
|
|
6736
|
+
const activeWorktreePaths = /* @__PURE__ */ new Set();
|
|
6737
|
+
const liveRunKeys = /* @__PURE__ */ new Set();
|
|
6738
|
+
for (const harnessRoot of harnessRoots) {
|
|
6739
|
+
for (const run of listRunRecordsForHarnessRoot(harnessRoot)) {
|
|
6740
|
+
let runHasLive = false;
|
|
6741
|
+
for (const name of Object.keys(run.workers || {})) {
|
|
6742
|
+
const worker = readJson(
|
|
6743
|
+
path37.join(runDirectoryAt(harnessRoot, run.id), "workers", safeSlug(name), "worker.json"),
|
|
6744
|
+
void 0
|
|
6745
|
+
);
|
|
6746
|
+
if (!worker?.worktreePath) continue;
|
|
6747
|
+
const worktreePath = path37.resolve(worker.worktreePath);
|
|
6748
|
+
if (!isActiveHarnessWorker2(worker, run.base, run.baseCommit)) continue;
|
|
6749
|
+
runHasLive = true;
|
|
6750
|
+
activeWorktreePaths.add(worktreePath);
|
|
6751
|
+
}
|
|
6752
|
+
if (runHasLive) liveRunKeys.add(`${harnessRoot}\0${run.id}`);
|
|
6753
|
+
}
|
|
6754
|
+
}
|
|
6755
|
+
return { activeWorktreePaths, liveRunKeys };
|
|
6756
|
+
}
|
|
6757
|
+
|
|
6758
|
+
// src/cleanup-disk-pressure.ts
|
|
6759
|
+
function envFlag2(name) {
|
|
6760
|
+
const v = process.env[name];
|
|
6761
|
+
return v === "1" || v === "true" || v === "yes";
|
|
6762
|
+
}
|
|
6763
|
+
function envNumber(name, fallback) {
|
|
6764
|
+
const raw = process.env[name];
|
|
6765
|
+
if (!raw) return fallback;
|
|
6766
|
+
const n = Number(raw);
|
|
6767
|
+
return Number.isFinite(n) ? n : fallback;
|
|
6768
|
+
}
|
|
6769
|
+
function observeCleanupDiskPressure(input = {}) {
|
|
6770
|
+
const diskPath = input.diskPath?.trim() || process.env.KYNVER_DISK_GUARD_PATH?.trim() || "/";
|
|
6771
|
+
const maxUsedPercent = envNumber("KYNVER_DISK_GUARD_MAX_USED_PERCENT", 75);
|
|
6772
|
+
const diskGate = observeRunnerDiskGate({
|
|
6773
|
+
...input,
|
|
6774
|
+
diskPath,
|
|
6775
|
+
diskMaxUsedPercent: input.diskMaxUsedPercent ?? maxUsedPercent
|
|
6776
|
+
});
|
|
6777
|
+
const pressured = !diskGate.ok || diskGate.usedPercent >= maxUsedPercent;
|
|
6778
|
+
return { diskGate, pressured, maxUsedPercent };
|
|
6779
|
+
}
|
|
6780
|
+
function applyDiskPressureToRetention(retention, pressure) {
|
|
6781
|
+
if (!pressure.pressured) return retention;
|
|
6782
|
+
const executeOnPressure = retention.execute || envFlag2("KYNVER_CLEANUP_EXECUTE_ON_PRESSURE");
|
|
6783
|
+
return {
|
|
6784
|
+
...retention,
|
|
6785
|
+
execute: executeOnPressure,
|
|
6786
|
+
nodeModulesAgeMs: 0,
|
|
6787
|
+
diskPressure: true,
|
|
6788
|
+
diskGate: pressure.diskGate
|
|
6789
|
+
};
|
|
6790
|
+
}
|
|
6791
|
+
|
|
6792
|
+
// src/cleanup.ts
|
|
6793
|
+
function resolvePaths(options = {}) {
|
|
6794
|
+
const harnessRoot = options.harnessRoot ? resolveUserPath(options.harnessRoot) : resolveHarnessRoot();
|
|
6795
|
+
const scanRoots = resolveHarnessScanRoots({ harnessRoot });
|
|
6796
|
+
const now = options.now ?? Date.now();
|
|
6797
|
+
return { harnessRoot, scanRoots, now };
|
|
5881
6798
|
}
|
|
5882
6799
|
function normalizeGuardSkip(skip) {
|
|
5883
6800
|
if (typeof skip === "string") return { reason: skip };
|
|
@@ -5902,72 +6819,145 @@ function tallySkipReasons(actions, skips) {
|
|
|
5902
6819
|
}
|
|
5903
6820
|
return counts;
|
|
5904
6821
|
}
|
|
6822
|
+
function removeDependencyCacheAction(candidate, execute) {
|
|
6823
|
+
if (candidate.kind === "remove_next_cache") return removeNextCache(candidate, execute);
|
|
6824
|
+
return removeNodeModules(candidate, execute);
|
|
6825
|
+
}
|
|
6826
|
+
function pathGuardForDependencyCache(candidate, harnessRoot, worktreesDir) {
|
|
6827
|
+
if (candidate.kind === "remove_next_cache") {
|
|
6828
|
+
return isHarnessNextCachePath(candidate.path, harnessRoot, worktreesDir);
|
|
6829
|
+
}
|
|
6830
|
+
return isHarnessNodeModulesPath(candidate.path, harnessRoot, worktreesDir);
|
|
6831
|
+
}
|
|
6832
|
+
function mergeWorktreeIndexes(scanRoots) {
|
|
6833
|
+
const merged = /* @__PURE__ */ new Map();
|
|
6834
|
+
for (const root of scanRoots) {
|
|
6835
|
+
for (const [key, value] of buildWorktreeIndexAt(root)) merged.set(key, value);
|
|
6836
|
+
}
|
|
6837
|
+
return merged;
|
|
6838
|
+
}
|
|
6839
|
+
function worktreePathForCandidate(candidate, worktreesDir) {
|
|
6840
|
+
if (candidate.runId && candidate.worker) {
|
|
6841
|
+
return path38.join(worktreesDir, candidate.runId, candidate.worker);
|
|
6842
|
+
}
|
|
6843
|
+
return path38.resolve(candidate.path, "..");
|
|
6844
|
+
}
|
|
5905
6845
|
function runHarnessCleanup(options = {}) {
|
|
5906
|
-
|
|
6846
|
+
let retention = resolveHarnessRetention(options);
|
|
6847
|
+
const diskPressure = observeCleanupDiskPressure();
|
|
6848
|
+
retention = applyDiskPressureToRetention(retention, diskPressure);
|
|
5907
6849
|
const paths = resolvePaths(options);
|
|
6850
|
+
const activeGuards = collectActiveWorktreeGuards(paths.scanRoots);
|
|
6851
|
+
const index = mergeWorktreeIndexes(paths.scanRoots);
|
|
5908
6852
|
const finalizedRuns = retention.finalizeStaleRuns ? finalizeStaleRuns().map((f) => ({ runId: f.runId, from: f.from, to: f.to })) : [];
|
|
5909
|
-
const index = buildWorktreeIndex();
|
|
5910
|
-
const scanOpts = {
|
|
5911
|
-
harnessRoot: paths.harnessRoot,
|
|
5912
|
-
worktreesDir: paths.worktreesDir,
|
|
5913
|
-
nodeModulesAgeMs: retention.nodeModulesAgeMs,
|
|
5914
|
-
worktreesAgeMs: retention.worktreesAgeMs,
|
|
5915
|
-
includeOrphans: retention.includeOrphans,
|
|
5916
|
-
runIdFilter: retention.runIdFilter,
|
|
5917
|
-
index,
|
|
5918
|
-
now: paths.now
|
|
5919
|
-
};
|
|
5920
6853
|
const skips = [];
|
|
5921
6854
|
const actions = [];
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
const
|
|
5925
|
-
|
|
5926
|
-
|
|
5927
|
-
|
|
5928
|
-
continue;
|
|
5929
|
-
}
|
|
5930
|
-
const worktreePath = path31.resolve(candidate.path, "..");
|
|
5931
|
-
const indexed = index.get(worktreePath) ?? null;
|
|
5932
|
-
const guardReason = skipNodeModulesRemoval({
|
|
5933
|
-
indexed,
|
|
5934
|
-
includeOrphans: retention.includeOrphans,
|
|
6855
|
+
const processedPaths = /* @__PURE__ */ new Set();
|
|
6856
|
+
for (const harnessRoot of paths.scanRoots) {
|
|
6857
|
+
const worktreesDir = path38.join(harnessRoot, "worktrees");
|
|
6858
|
+
const scanOpts = {
|
|
6859
|
+
harnessRoot,
|
|
6860
|
+
worktreesDir,
|
|
5935
6861
|
nodeModulesAgeMs: retention.nodeModulesAgeMs,
|
|
5936
|
-
ageMs: candidate.ageMs
|
|
5937
|
-
});
|
|
5938
|
-
if (guardReason) {
|
|
5939
|
-
recordSkip(skips, candidate.path, guardReason);
|
|
5940
|
-
actions.push({ ...candidate, executed: false, skipped: true, skipReason: guardReason });
|
|
5941
|
-
continue;
|
|
5942
|
-
}
|
|
5943
|
-
actions.push(removeNodeModules(candidate, retention.execute));
|
|
5944
|
-
}
|
|
5945
|
-
for (const raw of scanWorktreeCandidates(scanOpts)) {
|
|
5946
|
-
const candidate = attachCandidateBytes(raw, retention.accountBytes);
|
|
5947
|
-
const indexed = index.get(path31.resolve(candidate.path)) ?? null;
|
|
5948
|
-
const orphanSafety = indexed ? null : assessOrphanWorktreeSafety({
|
|
5949
|
-
worktreePath: candidate.path,
|
|
5950
|
-
harnessRoot: paths.harnessRoot,
|
|
5951
|
-
runId: candidate.runId,
|
|
5952
|
-
workerName: candidate.worker,
|
|
5953
|
-
now: paths.now
|
|
5954
|
-
});
|
|
5955
|
-
const guardSkip = skipWorktreeRemoval({
|
|
5956
|
-
indexed,
|
|
5957
|
-
worktreePath: path31.resolve(candidate.path),
|
|
5958
|
-
includeOrphans: retention.includeOrphans,
|
|
5959
6862
|
worktreesAgeMs: retention.worktreesAgeMs,
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
5965
|
-
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
continue;
|
|
6863
|
+
includeOrphans: retention.includeOrphans,
|
|
6864
|
+
runIdFilter: retention.runIdFilter,
|
|
6865
|
+
index,
|
|
6866
|
+
now: paths.now
|
|
6867
|
+
};
|
|
6868
|
+
for (const raw of scanDependencyCacheCandidates(scanOpts)) {
|
|
6869
|
+
const candidate = attachCandidateBytes(raw, retention.accountBytes);
|
|
6870
|
+
const resolved = path38.resolve(candidate.path);
|
|
6871
|
+
if (processedPaths.has(resolved)) continue;
|
|
6872
|
+
processedPaths.add(resolved);
|
|
6873
|
+
const pathSkip = pathGuardForDependencyCache(candidate, harnessRoot, worktreesDir);
|
|
6874
|
+
if (pathSkip) {
|
|
6875
|
+
recordSkip(skips, candidate.path, pathSkip);
|
|
6876
|
+
actions.push({ ...candidate, executed: false, skipped: true, skipReason: pathSkip });
|
|
6877
|
+
continue;
|
|
6878
|
+
}
|
|
6879
|
+
const worktreePath = worktreePathForCandidate(candidate, worktreesDir);
|
|
6880
|
+
const indexed = index.get(path38.resolve(worktreePath)) ?? null;
|
|
6881
|
+
const guardReason = skipDependencyCacheRemoval({
|
|
6882
|
+
indexed,
|
|
6883
|
+
includeOrphans: true,
|
|
6884
|
+
nodeModulesAgeMs: retention.nodeModulesAgeMs,
|
|
6885
|
+
ageMs: candidate.ageMs,
|
|
6886
|
+
worktreePath,
|
|
6887
|
+
activeWorktreePaths: activeGuards.activeWorktreePaths,
|
|
6888
|
+
diskPressure: retention.diskPressure
|
|
6889
|
+
});
|
|
6890
|
+
if (guardReason) {
|
|
6891
|
+
recordSkip(skips, candidate.path, guardReason);
|
|
6892
|
+
actions.push({ ...candidate, executed: false, skipped: true, skipReason: guardReason });
|
|
6893
|
+
continue;
|
|
6894
|
+
}
|
|
6895
|
+
actions.push(removeDependencyCacheAction(candidate, retention.execute));
|
|
6896
|
+
}
|
|
6897
|
+
for (const raw of scanBuildCacheCandidates(scanOpts)) {
|
|
6898
|
+
const candidate = attachCandidateBytes(raw, retention.accountBytes);
|
|
6899
|
+
const resolved = path38.resolve(candidate.path);
|
|
6900
|
+
if (processedPaths.has(resolved)) continue;
|
|
6901
|
+
processedPaths.add(resolved);
|
|
6902
|
+
const pathSkip = isHarnessBuildCachePath(candidate.path, harnessRoot, worktreesDir);
|
|
6903
|
+
if (pathSkip) {
|
|
6904
|
+
recordSkip(skips, candidate.path, pathSkip);
|
|
6905
|
+
actions.push({ ...candidate, executed: false, skipped: true, skipReason: pathSkip });
|
|
6906
|
+
continue;
|
|
6907
|
+
}
|
|
6908
|
+
const worktreePath = worktreePathForCandidate(candidate, worktreesDir);
|
|
6909
|
+
const indexed = index.get(path38.resolve(worktreePath)) ?? null;
|
|
6910
|
+
const guardReason = skipBuildCacheRemoval({
|
|
6911
|
+
indexed,
|
|
6912
|
+
includeOrphans: true,
|
|
6913
|
+
nodeModulesAgeMs: retention.nodeModulesAgeMs,
|
|
6914
|
+
ageMs: candidate.ageMs,
|
|
6915
|
+
worktreePath,
|
|
6916
|
+
activeWorktreePaths: activeGuards.activeWorktreePaths,
|
|
6917
|
+
diskPressure: retention.diskPressure
|
|
6918
|
+
});
|
|
6919
|
+
if (guardReason) {
|
|
6920
|
+
recordSkip(skips, candidate.path, guardReason);
|
|
6921
|
+
actions.push({ ...candidate, executed: false, skipped: true, skipReason: guardReason });
|
|
6922
|
+
continue;
|
|
6923
|
+
}
|
|
6924
|
+
actions.push(removeBuildCache(candidate, retention.execute));
|
|
6925
|
+
}
|
|
6926
|
+
const worktreeCandidates = [
|
|
6927
|
+
...scanWorktreeCandidates(scanOpts),
|
|
6928
|
+
...scanDuplicateWorktreeCandidates(scanOpts)
|
|
6929
|
+
];
|
|
6930
|
+
const worktreeSeen = /* @__PURE__ */ new Set();
|
|
6931
|
+
for (const raw of worktreeCandidates) {
|
|
6932
|
+
const resolved = path38.resolve(raw.path);
|
|
6933
|
+
if (worktreeSeen.has(resolved)) continue;
|
|
6934
|
+
worktreeSeen.add(resolved);
|
|
6935
|
+
const candidate = attachCandidateBytes({ ...raw, path: resolved }, retention.accountBytes);
|
|
6936
|
+
const indexed = index.get(path38.resolve(candidate.path)) ?? null;
|
|
6937
|
+
const orphanSafety = indexed ? null : assessOrphanWorktreeSafety({
|
|
6938
|
+
worktreePath: candidate.path,
|
|
6939
|
+
harnessRoot,
|
|
6940
|
+
runId: candidate.runId,
|
|
6941
|
+
workerName: candidate.worker,
|
|
6942
|
+
now: paths.now
|
|
6943
|
+
});
|
|
6944
|
+
const guardSkip = skipWorktreeRemoval({
|
|
6945
|
+
indexed,
|
|
6946
|
+
worktreePath: path38.resolve(candidate.path),
|
|
6947
|
+
includeOrphans: retention.includeOrphans,
|
|
6948
|
+
worktreesAgeMs: retention.worktreesAgeMs,
|
|
6949
|
+
ageMs: candidate.ageMs,
|
|
6950
|
+
orphanSafety,
|
|
6951
|
+
worktreeRemovalGuard: options.worktreeRemovalGuard
|
|
6952
|
+
});
|
|
6953
|
+
if (guardSkip) {
|
|
6954
|
+
const { reason: guardReason, detail: guardDetail } = normalizeGuardSkip(guardSkip);
|
|
6955
|
+
recordSkip(skips, candidate.path, guardReason, guardDetail);
|
|
6956
|
+
actions.push({ ...candidate, executed: false, skipped: true, skipReason: guardReason });
|
|
6957
|
+
continue;
|
|
6958
|
+
}
|
|
6959
|
+
actions.push(removeWorktree(candidate, retention.execute));
|
|
5969
6960
|
}
|
|
5970
|
-
actions.push(removeWorktree(candidate, retention.execute));
|
|
5971
6961
|
}
|
|
5972
6962
|
let candidateBytes = 0;
|
|
5973
6963
|
let reclaimableBytes = 0;
|
|
@@ -5988,11 +6978,20 @@ function runHarnessCleanup(options = {}) {
|
|
|
5988
6978
|
const storage = retention.accountBytes ? harnessStorageSnapshot({ harnessRoot: paths.harnessRoot, now: paths.now }) : void 0;
|
|
5989
6979
|
return {
|
|
5990
6980
|
harnessRoot: paths.harnessRoot,
|
|
6981
|
+
scanRoots: paths.scanRoots,
|
|
5991
6982
|
dryRun: !retention.execute,
|
|
5992
6983
|
execute: retention.execute,
|
|
5993
6984
|
nodeModulesAgeMs: retention.nodeModulesAgeMs,
|
|
5994
6985
|
worktreesAgeMs: retention.worktreesAgeMs,
|
|
5995
6986
|
includeOrphans: retention.includeOrphans,
|
|
6987
|
+
diskPressure: retention.diskPressure,
|
|
6988
|
+
diskGate: retention.diskGate ? {
|
|
6989
|
+
ok: retention.diskGate.ok,
|
|
6990
|
+
path: retention.diskGate.path,
|
|
6991
|
+
freeBytes: retention.diskGate.freeBytes,
|
|
6992
|
+
usedPercent: retention.diskGate.usedPercent,
|
|
6993
|
+
reason: retention.diskGate.reason
|
|
6994
|
+
} : void 0,
|
|
5996
6995
|
scannedAt: new Date(paths.now).toISOString(),
|
|
5997
6996
|
finalizedRuns,
|
|
5998
6997
|
actions,
|
|
@@ -6029,8 +7028,8 @@ import { mkdirSync as mkdirSync7, realpathSync } from "node:fs";
|
|
|
6029
7028
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
6030
7029
|
|
|
6031
7030
|
// src/discard-disposable.ts
|
|
6032
|
-
import { existsSync as
|
|
6033
|
-
import
|
|
7031
|
+
import { existsSync as existsSync27, rmSync as rmSync2 } from "node:fs";
|
|
7032
|
+
import path39 from "node:path";
|
|
6034
7033
|
function normalizeRelativePath2(value) {
|
|
6035
7034
|
const normalized = value.replace(/\\/g, "/").replace(/^\.\//, "").trim();
|
|
6036
7035
|
if (!normalized || normalized.startsWith("/") || normalized.includes("..")) {
|
|
@@ -6051,15 +7050,15 @@ function discardDisposableArtifacts(args) {
|
|
|
6051
7050
|
if (paths.length === 0) {
|
|
6052
7051
|
return { ok: false, removed: [], reason: "requires at least one --path" };
|
|
6053
7052
|
}
|
|
6054
|
-
const worktreeRoot =
|
|
7053
|
+
const worktreeRoot = path39.resolve(worker.worktreePath);
|
|
6055
7054
|
const removed = [];
|
|
6056
7055
|
for (const raw of paths) {
|
|
6057
7056
|
const rel = normalizeRelativePath2(raw);
|
|
6058
|
-
const abs =
|
|
6059
|
-
if (!abs.startsWith(worktreeRoot +
|
|
7057
|
+
const abs = path39.resolve(worktreeRoot, rel);
|
|
7058
|
+
if (!abs.startsWith(worktreeRoot + path39.sep) && abs !== worktreeRoot) {
|
|
6060
7059
|
return { ok: false, removed, reason: `path escapes worktree: ${raw}` };
|
|
6061
7060
|
}
|
|
6062
|
-
if (!
|
|
7061
|
+
if (!existsSync27(abs)) {
|
|
6063
7062
|
return { ok: false, removed, reason: `path not found: ${raw}` };
|
|
6064
7063
|
}
|
|
6065
7064
|
rmSync2(abs, { recursive: true, force: true });
|
|
@@ -6082,195 +7081,88 @@ function discardDisposableCli(args) {
|
|
|
6082
7081
|
}
|
|
6083
7082
|
|
|
6084
7083
|
// src/pipeline-tick.ts
|
|
6085
|
-
import
|
|
7084
|
+
import path42 from "node:path";
|
|
6086
7085
|
|
|
6087
7086
|
// src/pipeline-dispatch.ts
|
|
6088
7087
|
var RESERVED_REVIEW_STARTS = 1;
|
|
6089
|
-
function countDispatchStarts(result) {
|
|
6090
|
-
if (!result || typeof result !== "object") return 0;
|
|
6091
|
-
const startedCount = result.startedCount;
|
|
6092
|
-
if (typeof startedCount === "number") return startedCount;
|
|
6093
|
-
const outcomes = result.outcomes;
|
|
6094
|
-
if (!Array.isArray(outcomes)) return 0;
|
|
6095
|
-
return outcomes.filter((o) => o.started).length;
|
|
6096
|
-
}
|
|
6097
|
-
function stripCliMaxStarts(args) {
|
|
6098
|
-
const { maxStarts: _maxStarts, ...rest } = args;
|
|
6099
|
-
return rest;
|
|
6100
|
-
}
|
|
6101
|
-
async function runPipelineDispatch(args, slots) {
|
|
6102
|
-
if (slots <= 0) {
|
|
6103
|
-
return { ok: true, skipped: true, reason: "no slots", maxStarts: 0, startedCount: 0 };
|
|
6104
|
-
}
|
|
6105
|
-
const base = stripCliMaxStarts(args);
|
|
6106
|
-
const reviewBudget = Math.min(slots, RESERVED_REVIEW_STARTS);
|
|
6107
|
-
const workBudget = Math.max(0, slots - reviewBudget);
|
|
6108
|
-
const review = await dispatchRun({
|
|
6109
|
-
...base,
|
|
6110
|
-
execute: true,
|
|
6111
|
-
pipeline: true,
|
|
6112
|
-
lane: "review",
|
|
6113
|
-
maxStarts: String(reviewBudget)
|
|
6114
|
-
});
|
|
6115
|
-
const reviewStarted = countDispatchStarts(review);
|
|
6116
|
-
const workSlots = workBudget + (reviewBudget - reviewStarted);
|
|
6117
|
-
if (workSlots <= 0) {
|
|
6118
|
-
return {
|
|
6119
|
-
...typeof review === "object" && review !== null ? review : {},
|
|
6120
|
-
passes: { review },
|
|
6121
|
-
startedCount: reviewStarted
|
|
6122
|
-
};
|
|
6123
|
-
}
|
|
6124
|
-
const work = await dispatchRun({
|
|
6125
|
-
...base,
|
|
6126
|
-
execute: true,
|
|
6127
|
-
pipeline: true,
|
|
6128
|
-
maxStarts: String(workSlots)
|
|
6129
|
-
});
|
|
6130
|
-
const workStarted = countDispatchStarts(work);
|
|
6131
|
-
return {
|
|
6132
|
-
passes: { review, work },
|
|
6133
|
-
startedCount: reviewStarted + workStarted,
|
|
6134
|
-
ok: true
|
|
6135
|
-
};
|
|
6136
|
-
}
|
|
6137
|
-
|
|
6138
|
-
// src/pipeline-max-starts.ts
|
|
6139
|
-
function operatorDispatchFromTick(operatorTick) {
|
|
6140
|
-
const body = operatorTick;
|
|
6141
|
-
const dispatch = body.response?.dispatch;
|
|
6142
|
-
return dispatch && typeof dispatch === "object" ? dispatch : null;
|
|
6143
|
-
}
|
|
6144
|
-
function resolvePipelineMaxStarts(resourceGate, operatorTick) {
|
|
6145
|
-
const dispatch = operatorDispatchFromTick(operatorTick);
|
|
6146
|
-
const advised = typeof dispatch?.recommendedMaxStarts === "number" ? Math.max(0, dispatch.recommendedMaxStarts) : null;
|
|
6147
|
-
let maxStarts = resourceGate.slotsAvailable;
|
|
6148
|
-
if (advised !== null) {
|
|
6149
|
-
maxStarts = Math.min(maxStarts, advised);
|
|
6150
|
-
}
|
|
6151
|
-
const underutilized = dispatch?.underutilized === true;
|
|
6152
|
-
const boardAdvancedThisTick = typeof dispatch?.boardAdvancedThisTick === "number" ? dispatch.boardAdvancedThisTick : 0;
|
|
6153
|
-
if (underutilized && resourceGate.slotsAvailable > 0 && maxStarts === 0) {
|
|
6154
|
-
const ready = dispatch?.actionableReady ?? dispatch?.queuedTasks ?? (boardAdvancedThisTick > 0 ? boardAdvancedThisTick : 1);
|
|
6155
|
-
maxStarts = Math.min(resourceGate.slotsAvailable, Math.max(1, ready));
|
|
6156
|
-
}
|
|
6157
|
-
return {
|
|
6158
|
-
maxStarts: Math.max(0, maxStarts),
|
|
6159
|
-
underutilized,
|
|
6160
|
-
advisedStarts: advised,
|
|
6161
|
-
boardAdvancedThisTick
|
|
6162
|
-
};
|
|
6163
|
-
}
|
|
6164
|
-
|
|
6165
|
-
// src/stale-reconcile.ts
|
|
6166
|
-
import path33 from "node:path";
|
|
6167
|
-
var STALE_RECONCILE_HEARTBEAT_MS = 15 * 60 * 1e3;
|
|
6168
|
-
function staleReconcileDisabled() {
|
|
6169
|
-
return process.env.KYNVER_NO_STALE_CLEANUP === "1";
|
|
6170
|
-
}
|
|
6171
|
-
function reconcileStaleWorkers() {
|
|
6172
|
-
if (staleReconcileDisabled()) {
|
|
6173
|
-
return { workers: [], finalizedRuns: finalizeStaleRuns() };
|
|
6174
|
-
}
|
|
6175
|
-
const outcomes = [];
|
|
6176
|
-
const now = Date.now();
|
|
6177
|
-
for (const run of listRunRecords()) {
|
|
6178
|
-
for (const name of Object.keys(run.workers || {})) {
|
|
6179
|
-
const workerPath = path33.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
|
|
6180
|
-
const worker = readJson(workerPath, void 0);
|
|
6181
|
-
if (!worker || worker.status !== "running") {
|
|
6182
|
-
outcomes.push({
|
|
6183
|
-
runId: run.id,
|
|
6184
|
-
worker: name,
|
|
6185
|
-
action: "skipped",
|
|
6186
|
-
reason: worker ? `worker status is ${worker.status}` : "worker.json missing"
|
|
6187
|
-
});
|
|
6188
|
-
continue;
|
|
6189
|
-
}
|
|
6190
|
-
const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
|
|
6191
|
-
if (status.finalResult) {
|
|
6192
|
-
if (worker.status === "running") {
|
|
6193
|
-
const nextStatus = status.attention.state === "blocked" ? "blocked" : status.attention.state === "done" || status.status === "done" ? "done" : "exited";
|
|
6194
|
-
worker.status = nextStatus;
|
|
6195
|
-
worker.reconciledAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6196
|
-
worker.reconcileReason = "synced finished worker record after terminal stdout/heartbeat";
|
|
6197
|
-
saveWorker(run.id, worker);
|
|
6198
|
-
outcomes.push({
|
|
6199
|
-
runId: run.id,
|
|
6200
|
-
worker: name,
|
|
6201
|
-
action: "marked_exited",
|
|
6202
|
-
reason: worker.reconcileReason
|
|
6203
|
-
});
|
|
6204
|
-
} else {
|
|
6205
|
-
outcomes.push({ runId: run.id, worker: name, action: "skipped", reason: "final result present" });
|
|
6206
|
-
}
|
|
6207
|
-
continue;
|
|
6208
|
-
}
|
|
6209
|
-
if (!status.alive) {
|
|
6210
|
-
const nextStatus = status.attention.state === "blocked" ? "blocked" : status.status === "done" ? "done" : "exited";
|
|
6211
|
-
worker.status = nextStatus;
|
|
6212
|
-
worker.reconciledAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6213
|
-
worker.reconcileReason = status.attention.reason;
|
|
6214
|
-
saveWorker(run.id, worker);
|
|
6215
|
-
outcomes.push({
|
|
6216
|
-
runId: run.id,
|
|
6217
|
-
worker: name,
|
|
6218
|
-
action: "marked_exited",
|
|
6219
|
-
reason: status.attention.reason
|
|
6220
|
-
});
|
|
6221
|
-
continue;
|
|
6222
|
-
}
|
|
6223
|
-
if (status.attention.state === "stale" && worker.pid && isPidAlive(worker.pid)) {
|
|
6224
|
-
const hbMs = status.lastHeartbeatAt ? Date.parse(status.lastHeartbeatAt) : NaN;
|
|
6225
|
-
const actMs = status.lastActivityAt ? Date.parse(status.lastActivityAt) : NaN;
|
|
6226
|
-
const hbStale = !Number.isFinite(hbMs) || now - hbMs > STALE_RECONCILE_HEARTBEAT_MS;
|
|
6227
|
-
const actStale = Number.isFinite(actMs) && now - actMs > STALE_MS;
|
|
6228
|
-
if (hbStale && actStale) {
|
|
6229
|
-
killWorkerProcess(worker.pid, "SIGTERM");
|
|
6230
|
-
worker.status = "exited";
|
|
6231
|
-
worker.reconciledAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6232
|
-
worker.reconcileReason = `reconciled stale worker: ${status.attention.reason}`;
|
|
6233
|
-
saveWorker(run.id, worker);
|
|
6234
|
-
outcomes.push({
|
|
6235
|
-
runId: run.id,
|
|
6236
|
-
worker: name,
|
|
6237
|
-
action: "killed_stale",
|
|
6238
|
-
reason: status.attention.reason
|
|
6239
|
-
});
|
|
6240
|
-
continue;
|
|
6241
|
-
}
|
|
6242
|
-
}
|
|
6243
|
-
outcomes.push({
|
|
6244
|
-
runId: run.id,
|
|
6245
|
-
worker: name,
|
|
6246
|
-
action: "skipped",
|
|
6247
|
-
reason: status.attention.reason
|
|
6248
|
-
});
|
|
6249
|
-
}
|
|
7088
|
+
function countDispatchStarts(result) {
|
|
7089
|
+
if (!result || typeof result !== "object") return 0;
|
|
7090
|
+
const startedCount = result.startedCount;
|
|
7091
|
+
if (typeof startedCount === "number") return startedCount;
|
|
7092
|
+
const outcomes = result.outcomes;
|
|
7093
|
+
if (!Array.isArray(outcomes)) return 0;
|
|
7094
|
+
return outcomes.filter((o) => o.started).length;
|
|
7095
|
+
}
|
|
7096
|
+
function stripCliMaxStarts(args) {
|
|
7097
|
+
const { maxStarts: _maxStarts, ...rest } = args;
|
|
7098
|
+
return rest;
|
|
7099
|
+
}
|
|
7100
|
+
async function runPipelineDispatch(args, slots) {
|
|
7101
|
+
if (slots <= 0) {
|
|
7102
|
+
return { ok: true, skipped: true, reason: "no slots", maxStarts: 0, startedCount: 0 };
|
|
6250
7103
|
}
|
|
6251
|
-
|
|
7104
|
+
const base = stripCliMaxStarts(args);
|
|
7105
|
+
const reviewBudget = Math.min(slots, RESERVED_REVIEW_STARTS);
|
|
7106
|
+
const workBudget = Math.max(0, slots - reviewBudget);
|
|
7107
|
+
const review = await dispatchRun({
|
|
7108
|
+
...base,
|
|
7109
|
+
execute: true,
|
|
7110
|
+
pipeline: true,
|
|
7111
|
+
lane: "review",
|
|
7112
|
+
maxStarts: String(reviewBudget)
|
|
7113
|
+
});
|
|
7114
|
+
const reviewStarted = countDispatchStarts(review);
|
|
7115
|
+
const workSlots = workBudget + (reviewBudget - reviewStarted);
|
|
7116
|
+
if (workSlots <= 0) {
|
|
7117
|
+
return {
|
|
7118
|
+
...typeof review === "object" && review !== null ? review : {},
|
|
7119
|
+
passes: { review },
|
|
7120
|
+
startedCount: reviewStarted
|
|
7121
|
+
};
|
|
7122
|
+
}
|
|
7123
|
+
const work = await dispatchRun({
|
|
7124
|
+
...base,
|
|
7125
|
+
execute: true,
|
|
7126
|
+
pipeline: true,
|
|
7127
|
+
maxStarts: String(workSlots)
|
|
7128
|
+
});
|
|
7129
|
+
const workStarted = countDispatchStarts(work);
|
|
7130
|
+
return {
|
|
7131
|
+
passes: { review, work },
|
|
7132
|
+
startedCount: reviewStarted + workStarted,
|
|
7133
|
+
ok: true
|
|
7134
|
+
};
|
|
6252
7135
|
}
|
|
6253
|
-
|
|
6254
|
-
|
|
6255
|
-
|
|
6256
|
-
const
|
|
6257
|
-
const
|
|
6258
|
-
|
|
6259
|
-
|
|
6260
|
-
|
|
6261
|
-
|
|
6262
|
-
|
|
6263
|
-
|
|
6264
|
-
|
|
6265
|
-
|
|
6266
|
-
|
|
6267
|
-
|
|
6268
|
-
|
|
6269
|
-
)
|
|
7136
|
+
|
|
7137
|
+
// src/pipeline-max-starts.ts
|
|
7138
|
+
function operatorDispatchFromTick(operatorTick) {
|
|
7139
|
+
const body = operatorTick;
|
|
7140
|
+
const dispatch = body.response?.dispatch;
|
|
7141
|
+
return dispatch && typeof dispatch === "object" ? dispatch : null;
|
|
7142
|
+
}
|
|
7143
|
+
function resolvePipelineMaxStarts(resourceGate, operatorTick) {
|
|
7144
|
+
const dispatch = operatorDispatchFromTick(operatorTick);
|
|
7145
|
+
const advised = typeof dispatch?.recommendedMaxStarts === "number" ? Math.max(0, dispatch.recommendedMaxStarts) : null;
|
|
7146
|
+
let maxStarts = resourceGate.slotsAvailable;
|
|
7147
|
+
if (advised !== null) {
|
|
7148
|
+
maxStarts = Math.min(maxStarts, advised);
|
|
7149
|
+
}
|
|
7150
|
+
const underutilized = dispatch?.underutilized === true;
|
|
7151
|
+
const boardAdvancedThisTick = typeof dispatch?.boardAdvancedThisTick === "number" ? dispatch.boardAdvancedThisTick : 0;
|
|
7152
|
+
if (underutilized && resourceGate.slotsAvailable > 0 && maxStarts === 0) {
|
|
7153
|
+
const ready = dispatch?.actionableReady ?? dispatch?.queuedTasks ?? (boardAdvancedThisTick > 0 ? boardAdvancedThisTick : 1);
|
|
7154
|
+
maxStarts = Math.min(resourceGate.slotsAvailable, Math.max(1, ready));
|
|
7155
|
+
}
|
|
7156
|
+
return {
|
|
7157
|
+
maxStarts: Math.max(0, maxStarts),
|
|
7158
|
+
underutilized,
|
|
7159
|
+
advisedStarts: advised,
|
|
7160
|
+
boardAdvancedThisTick
|
|
7161
|
+
};
|
|
6270
7162
|
}
|
|
6271
7163
|
|
|
6272
7164
|
// src/plan-progress-daemon-sync.ts
|
|
6273
|
-
import
|
|
7165
|
+
import path40 from "node:path";
|
|
6274
7166
|
|
|
6275
7167
|
// src/plan-progress-sync.ts
|
|
6276
7168
|
async function syncPlanProgress(args) {
|
|
@@ -6294,7 +7186,7 @@ async function syncActiveWorkerPlanProgress(runId, args) {
|
|
|
6294
7186
|
const outcomes = [];
|
|
6295
7187
|
for (const name of Object.keys(run.workers || {})) {
|
|
6296
7188
|
const worker = readJson(
|
|
6297
|
-
|
|
7189
|
+
path40.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
6298
7190
|
void 0
|
|
6299
7191
|
);
|
|
6300
7192
|
if (!worker?.dispatched || !worker.taskId) continue;
|
|
@@ -6344,8 +7236,8 @@ async function fetchWorkspaceRuntimePreferences(agentOsId, args) {
|
|
|
6344
7236
|
|
|
6345
7237
|
// src/installed-package-versions.ts
|
|
6346
7238
|
import { readFile } from "node:fs/promises";
|
|
6347
|
-
import { homedir as
|
|
6348
|
-
import
|
|
7239
|
+
import { homedir as homedir7 } from "node:os";
|
|
7240
|
+
import path41 from "node:path";
|
|
6349
7241
|
var MANAGED_PACKAGES = [
|
|
6350
7242
|
"@kynver-app/runtime",
|
|
6351
7243
|
"@kynver-app/openclaw-agent-os",
|
|
@@ -6359,13 +7251,13 @@ function unique(values) {
|
|
|
6359
7251
|
return [...new Set(values.filter((value) => Boolean(value)))];
|
|
6360
7252
|
}
|
|
6361
7253
|
function moduleRoots() {
|
|
6362
|
-
const home =
|
|
6363
|
-
const openClawPrefix = trim(process.env.KYNVER_OPENCLAW_NPM_ROOT) ?? trim(process.env.OPENCLAW_NPM_ROOT) ??
|
|
6364
|
-
const npmGlobalRoot = trim(process.env.KYNVER_NPM_GLOBAL_ROOT) ?? trim(process.env.KYNVER_NPM_GLOBAL_MODULES_ROOT) ?? (trim(process.env.NPM_CONFIG_PREFIX) ?
|
|
7254
|
+
const home = homedir7();
|
|
7255
|
+
const openClawPrefix = trim(process.env.KYNVER_OPENCLAW_NPM_ROOT) ?? trim(process.env.OPENCLAW_NPM_ROOT) ?? path41.join(home, ".openclaw", "npm");
|
|
7256
|
+
const npmGlobalRoot = trim(process.env.KYNVER_NPM_GLOBAL_ROOT) ?? trim(process.env.KYNVER_NPM_GLOBAL_MODULES_ROOT) ?? (trim(process.env.NPM_CONFIG_PREFIX) ? path41.join(trim(process.env.NPM_CONFIG_PREFIX), "lib", "node_modules") : path41.join(home, ".npm-global", "lib", "node_modules"));
|
|
6365
7257
|
return unique([
|
|
6366
|
-
|
|
6367
|
-
|
|
6368
|
-
npmGlobalRoot.endsWith("node_modules") ? npmGlobalRoot :
|
|
7258
|
+
path41.join(openClawPrefix, "lib", "node_modules"),
|
|
7259
|
+
path41.join(openClawPrefix, "node_modules"),
|
|
7260
|
+
npmGlobalRoot.endsWith("node_modules") ? npmGlobalRoot : path41.join(npmGlobalRoot, "lib", "node_modules")
|
|
6369
7261
|
]);
|
|
6370
7262
|
}
|
|
6371
7263
|
async function readVersion(packageJsonPath) {
|
|
@@ -6381,7 +7273,7 @@ async function collectInstalledPackageVersions(observedAt = (/* @__PURE__ */ new
|
|
|
6381
7273
|
const out = {};
|
|
6382
7274
|
for (const packageName of MANAGED_PACKAGES) {
|
|
6383
7275
|
for (const root of roots) {
|
|
6384
|
-
const packageJsonPath =
|
|
7276
|
+
const packageJsonPath = path41.join(root, packageName, "package.json");
|
|
6385
7277
|
const version = await readVersion(packageJsonPath);
|
|
6386
7278
|
if (!version) continue;
|
|
6387
7279
|
out[packageName] = { version, observedAt, path: packageJsonPath };
|
|
@@ -6397,7 +7289,7 @@ async function completeFinishedWorkers(runId, args) {
|
|
|
6397
7289
|
const outcomes = [];
|
|
6398
7290
|
for (const name of Object.keys(run.workers || {})) {
|
|
6399
7291
|
const worker = readJson(
|
|
6400
|
-
|
|
7292
|
+
path42.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
6401
7293
|
void 0
|
|
6402
7294
|
);
|
|
6403
7295
|
if (!worker?.taskId || worker.localOnly) continue;
|
|
@@ -6424,18 +7316,38 @@ async function completeFinishedWorkers(runId, args) {
|
|
|
6424
7316
|
}
|
|
6425
7317
|
return outcomes;
|
|
6426
7318
|
}
|
|
6427
|
-
async function postOperatorTick(agentOsId, runId, resourceGate, args) {
|
|
7319
|
+
async function postOperatorTick(agentOsId, runId, resourceGate, args, harnessCleanup) {
|
|
6428
7320
|
const base = resolveBaseUrl(args.baseUrl ? String(args.baseUrl) : void 0);
|
|
6429
7321
|
const secret = await resolveCallbackSecretWithMint(args.secret ? String(args.secret) : void 0, agentOsId, { baseUrl: base });
|
|
6430
7322
|
const url = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/operator/tick`;
|
|
6431
7323
|
const packageVersions = await collectInstalledPackageVersions();
|
|
7324
|
+
const activeHarnessWorkers = [];
|
|
7325
|
+
const run = loadRun(runId);
|
|
7326
|
+
for (const name of Object.keys(run.workers || {})) {
|
|
7327
|
+
const worker = readJson(
|
|
7328
|
+
path42.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
7329
|
+
void 0
|
|
7330
|
+
);
|
|
7331
|
+
if (!worker?.taskId) continue;
|
|
7332
|
+
activeHarnessWorkers.push({
|
|
7333
|
+
runId: run.id,
|
|
7334
|
+
workerName: name,
|
|
7335
|
+
taskId: worker.taskId,
|
|
7336
|
+
pid: worker.pid
|
|
7337
|
+
});
|
|
7338
|
+
}
|
|
6432
7339
|
const res = await postJson(url, secret, {
|
|
6433
7340
|
agentOsId,
|
|
6434
7341
|
runId,
|
|
6435
7342
|
ingestHarness: true,
|
|
6436
7343
|
harnessBoardSnapshot: buildRunBoard(runId),
|
|
6437
7344
|
resourceGate,
|
|
6438
|
-
|
|
7345
|
+
boxResourceSnapshot: buildBoxResourceSnapshotFromGate(resourceGate, { harnessRunId: runId }),
|
|
7346
|
+
packageVersions,
|
|
7347
|
+
...harnessCleanup ? { harnessCleanup } : {},
|
|
7348
|
+
runnerPresence: resolveRunnerPresencePayload({ runId }),
|
|
7349
|
+
activeHarnessWorkers,
|
|
7350
|
+
...harnessCleanup ? { harnessCleanup } : {}
|
|
6439
7351
|
});
|
|
6440
7352
|
return { ok: res.ok, httpStatus: res.status, response: res.response };
|
|
6441
7353
|
}
|
|
@@ -6449,12 +7361,12 @@ async function runPipelineTick(args) {
|
|
|
6449
7361
|
runId,
|
|
6450
7362
|
configuredMaxWorkersOverride: workspacePrefs?.maxConcurrentWorkers
|
|
6451
7363
|
});
|
|
6452
|
-
const
|
|
7364
|
+
const harnessCleanup = isPipelineCleanupEnabled() ? runPipelineHarnessCleanup(runId) : void 0;
|
|
7365
|
+
const operatorTick = await postOperatorTick(agentOsId, runId, resourceGate, args, harnessCleanup);
|
|
6453
7366
|
const completionAckSync = syncCompletionAcknowledgedFromOperatorTick(runId, operatorTick);
|
|
6454
7367
|
const leaseRenewal = await renewActiveTaskLeases(runId, args);
|
|
6455
7368
|
const completedWorkers = await completeFinishedWorkers(runId, args);
|
|
6456
7369
|
const staleReconcile = reconcileStaleWorkers();
|
|
6457
|
-
const harnessCleanup = isPipelineCleanupEnabled() ? runPipelineHarnessCleanup(runId) : void 0;
|
|
6458
7370
|
const planProgressSync = await syncActiveWorkerPlanProgress(runId, args);
|
|
6459
7371
|
const maxStartsAdvice = resolvePipelineMaxStarts(resourceGate, operatorTick);
|
|
6460
7372
|
let maxStarts = maxStartsAdvice.maxStarts;
|
|
@@ -6535,7 +7447,7 @@ async function runDaemon(args) {
|
|
|
6535
7447
|
}
|
|
6536
7448
|
|
|
6537
7449
|
// src/plan-progress.ts
|
|
6538
|
-
import
|
|
7450
|
+
import path43 from "node:path";
|
|
6539
7451
|
|
|
6540
7452
|
// src/bounded-build/constants.ts
|
|
6541
7453
|
var DEFAULT_BUILD_MEM_BUDGET_BYTES = 1536 * 1024 * 1024;
|
|
@@ -6785,7 +7697,8 @@ async function emitPlanProgress(args) {
|
|
|
6785
7697
|
const secret = await resolveCallbackSecretWithMint(args.secret ? String(args.secret) : void 0, agentOsId, { baseUrl: base });
|
|
6786
7698
|
const url = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/plans/${encodeURIComponent(planId)}/progress-events`;
|
|
6787
7699
|
const cfg = loadUserConfig();
|
|
6788
|
-
const
|
|
7700
|
+
const workerProvider = resolveConfiguredWorkerProvider(cfg.workerProvider, DEFAULT_WORKER_PROVIDER);
|
|
7701
|
+
const provider = `provider:${workerProvider}`;
|
|
6789
7702
|
const explicitProposed = args.proposed === true || args.proposed === "true" ? true : args.proposed === false || args.proposed === "false" ? false : void 0;
|
|
6790
7703
|
const proposed = explicitProposed ?? (status !== "done" && (roleLane === "implementer" || roleLane === "repair_implementer"));
|
|
6791
7704
|
const body = {
|
|
@@ -6821,7 +7734,7 @@ async function emitPlanProgress(args) {
|
|
|
6821
7734
|
}
|
|
6822
7735
|
function verifyPlanLocal(args) {
|
|
6823
7736
|
const worktree = required(args.worktree ? String(args.worktree) : void 0, "worktree");
|
|
6824
|
-
const cwd =
|
|
7737
|
+
const cwd = path43.resolve(worktree);
|
|
6825
7738
|
const summary = runHarnessVerifyCommands(cwd);
|
|
6826
7739
|
const emitJson = args.json === true || args.json === "true";
|
|
6827
7740
|
const payload = { passed: summary.passed, worktree: cwd, steps: summary.steps };
|
|
@@ -6870,9 +7783,9 @@ async function verifyPlan(args) {
|
|
|
6870
7783
|
}
|
|
6871
7784
|
|
|
6872
7785
|
// src/harness-verify-cli.ts
|
|
6873
|
-
import
|
|
7786
|
+
import path44 from "node:path";
|
|
6874
7787
|
function runHarnessVerifyCli(args) {
|
|
6875
|
-
const cwd =
|
|
7788
|
+
const cwd = path44.resolve(required(args.worktree ? String(args.worktree) : void 0, "worktree"));
|
|
6876
7789
|
const emitJson = args.json === true || args.json === "true" || args.emitJson === true || args.emitJson === "true";
|
|
6877
7790
|
const commands = [];
|
|
6878
7791
|
const rawCmd = args.command;
|
|
@@ -6916,7 +7829,7 @@ function runHarnessVerifyCli(args) {
|
|
|
6916
7829
|
}
|
|
6917
7830
|
|
|
6918
7831
|
// src/plan-persist-cli.ts
|
|
6919
|
-
import { readFileSync as
|
|
7832
|
+
import { readFileSync as readFileSync12 } from "node:fs";
|
|
6920
7833
|
var OPERATIONS = ["create", "add_version", "update_metadata"];
|
|
6921
7834
|
var FAILURE_KINDS = [
|
|
6922
7835
|
"approval_guard",
|
|
@@ -6928,7 +7841,7 @@ var FAILURE_KINDS = [
|
|
|
6928
7841
|
function readBodyArg(args) {
|
|
6929
7842
|
const bodyFile = args.bodyFile ? String(args.bodyFile) : void 0;
|
|
6930
7843
|
if (bodyFile) {
|
|
6931
|
-
return { body:
|
|
7844
|
+
return { body: readFileSync12(bodyFile, "utf8"), bodyPathHint: bodyFile };
|
|
6932
7845
|
}
|
|
6933
7846
|
const inline = args.body ? String(args.body) : void 0;
|
|
6934
7847
|
if (inline) return { body: inline };
|
|
@@ -7017,8 +7930,291 @@ function runCleanupCli(args) {
|
|
|
7017
7930
|
}
|
|
7018
7931
|
}
|
|
7019
7932
|
|
|
7933
|
+
// src/harness-notice/harness-notice.parse.ts
|
|
7934
|
+
var MAX_DIAGNOSTIC_CHARS = 2400;
|
|
7935
|
+
function tryParseJsonValue(text) {
|
|
7936
|
+
const trimmed = text.trim();
|
|
7937
|
+
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return null;
|
|
7938
|
+
try {
|
|
7939
|
+
return JSON.parse(trimmed);
|
|
7940
|
+
} catch {
|
|
7941
|
+
return null;
|
|
7942
|
+
}
|
|
7943
|
+
}
|
|
7944
|
+
function diagnosticJson(value, maxChars = MAX_DIAGNOSTIC_CHARS) {
|
|
7945
|
+
if (value === void 0 || value === null) return void 0;
|
|
7946
|
+
const raw = typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
7947
|
+
const trimmed = raw.trim();
|
|
7948
|
+
if (!trimmed) return void 0;
|
|
7949
|
+
if (trimmed.length <= maxChars) return trimmed;
|
|
7950
|
+
return `${trimmed.slice(0, maxChars - 1).trimEnd()}\u2026`;
|
|
7951
|
+
}
|
|
7952
|
+
function firstJsonFromStdout(stdout) {
|
|
7953
|
+
const trimmed = stdout.trim();
|
|
7954
|
+
if (!trimmed) return null;
|
|
7955
|
+
const direct = tryParseJsonValue(trimmed);
|
|
7956
|
+
if (direct !== null) return direct;
|
|
7957
|
+
for (const line of trimmed.split("\n")) {
|
|
7958
|
+
const parsed = tryParseJsonValue(line);
|
|
7959
|
+
if (parsed !== null) return parsed;
|
|
7960
|
+
}
|
|
7961
|
+
return null;
|
|
7962
|
+
}
|
|
7963
|
+
|
|
7964
|
+
// src/harness-notice/harness-notice.auto-complete.ts
|
|
7965
|
+
function formatAutoCompleteOutcomeNotice(outcome) {
|
|
7966
|
+
const lines = [];
|
|
7967
|
+
lines.push(`Background auto-complete \xB7 ${outcome.runId} / ${outcome.worker}`);
|
|
7968
|
+
switch (outcome.outcome) {
|
|
7969
|
+
case "completed":
|
|
7970
|
+
lines.push("Outcome: harness completion posted to AgentOS successfully.");
|
|
7971
|
+
lines.push("AgentOS task should close or advance to review per completion routing.");
|
|
7972
|
+
lines.push("Next: check Command Center \u2014 no manual complete needed unless the board still shows running.");
|
|
7973
|
+
break;
|
|
7974
|
+
case "blocked":
|
|
7975
|
+
lines.push(
|
|
7976
|
+
`Outcome: worker finished but completion was blocked${outcome.httpStatus ? ` (HTTP ${outcome.httpStatus})` : ""}.`
|
|
7977
|
+
);
|
|
7978
|
+
if (outcome.reason) lines.push(`Blocker: ${outcome.reason}`);
|
|
7979
|
+
lines.push("Next: fix the blocker (auth, landing gate, dirty worktree) and replay completion from Command Center.");
|
|
7980
|
+
break;
|
|
7981
|
+
case "timed_out":
|
|
7982
|
+
lines.push(`Outcome: monitor gave up waiting \u2014 ${outcome.reason ?? "worker did not finish in time"}.`);
|
|
7983
|
+
lines.push("Next: inspect the worker process/logs; stop or unblock the worker, then retry auto-complete.");
|
|
7984
|
+
break;
|
|
7985
|
+
case "missing_link":
|
|
7986
|
+
lines.push(`Outcome: cannot complete \u2014 ${outcome.reason ?? "worker missing agentOsId/taskId"}.`);
|
|
7987
|
+
lines.push("Next: re-dispatch with board linkage or run `kynver worker complete` with --agent-os-id.");
|
|
7988
|
+
break;
|
|
7989
|
+
default:
|
|
7990
|
+
lines.push(`Outcome: ${outcome.outcome}`);
|
|
7991
|
+
}
|
|
7992
|
+
return { primary: lines.join("\n"), diagnostic: diagnosticJson(outcome) };
|
|
7993
|
+
}
|
|
7994
|
+
|
|
7995
|
+
// src/harness-notice/harness-notice.monitor-tick.ts
|
|
7996
|
+
function formatMonitorTickNotice(tick) {
|
|
7997
|
+
const lines = [];
|
|
7998
|
+
const monitorId = typeof tick.monitorId === "string" ? tick.monitorId : void 0;
|
|
7999
|
+
lines.push(
|
|
8000
|
+
monitorId ? `Harness monitor tick \xB7 ${tick.runId} (${monitorId})` : `Harness monitor tick \xB7 ${tick.runId}`
|
|
8001
|
+
);
|
|
8002
|
+
if (!tick.workers.length) {
|
|
8003
|
+
lines.push("No workers in scope for this poll.");
|
|
8004
|
+
return { primary: lines.join("\n"), diagnostic: diagnosticJson(tick) };
|
|
8005
|
+
}
|
|
8006
|
+
for (const view of tick.workers) {
|
|
8007
|
+
const auto = view.autoComplete.eligible ? "eligible for auto-complete" : "not auto-completing";
|
|
8008
|
+
const blockers = view.autoComplete.blockers.length > 0 ? ` (${view.autoComplete.blockers.slice(0, 2).join("; ")})` : "";
|
|
8009
|
+
lines.push(
|
|
8010
|
+
`\u2022 ${view.worker}: ${view.workerStatus}, ${view.health}${view.healthReason ? ` \u2014 ${view.healthReason}` : ""}; ${auto}${blockers}`
|
|
8011
|
+
);
|
|
8012
|
+
if (view.taskStatus) {
|
|
8013
|
+
lines.push(` Board task: ${view.taskStatus}${view.leaseOwner ? ` (lease: ${view.leaseOwner})` : ""}`);
|
|
8014
|
+
}
|
|
8015
|
+
}
|
|
8016
|
+
const completed = tick.autoCompleted?.filter((a) => a.outcome === "completed" && a.ok) ?? [];
|
|
8017
|
+
const blocked = tick.autoCompleted?.filter((a) => !a.ok && a.outcome !== "skipped") ?? [];
|
|
8018
|
+
if (completed.length) {
|
|
8019
|
+
lines.push(
|
|
8020
|
+
`Auto-completed: ${completed.map((c) => c.worker).join(", ")} \u2014 AgentOS completion should be posted.`
|
|
8021
|
+
);
|
|
8022
|
+
}
|
|
8023
|
+
if (blocked.length) {
|
|
8024
|
+
lines.push(
|
|
8025
|
+
`Auto-complete blocked: ${blocked.map((c) => `${c.worker}${c.reason ? ` (${c.reason})` : ""}`).join("; ")}`
|
|
8026
|
+
);
|
|
8027
|
+
}
|
|
8028
|
+
if (tick.leaseRenewal?.failed?.length) {
|
|
8029
|
+
lines.push(`Lease renew failed for: ${tick.leaseRenewal.failed.map((f) => f.worker).join(", ")}`);
|
|
8030
|
+
}
|
|
8031
|
+
const allDone = tick.workers.length > 0 && tick.workers.every((w) => w.autoComplete.terminalVerified) && (tick.autoCompleted?.every((a) => a.ok || a.outcome === "skipped") ?? true);
|
|
8032
|
+
lines.push(
|
|
8033
|
+
allDone ? "Next: monitor loop should stop \u2014 all workers terminal and handled." : "Next: monitor will poll again until workers are terminal-verified or max time elapses."
|
|
8034
|
+
);
|
|
8035
|
+
return { primary: lines.join("\n"), diagnostic: diagnosticJson(tick) };
|
|
8036
|
+
}
|
|
8037
|
+
|
|
8038
|
+
// src/harness-notice/harness-notice.worker-complete.ts
|
|
8039
|
+
function record(value) {
|
|
8040
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
8041
|
+
}
|
|
8042
|
+
function str(value) {
|
|
8043
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
8044
|
+
}
|
|
8045
|
+
function formatWorkerCompleteNotice(data) {
|
|
8046
|
+
const rec = record(data);
|
|
8047
|
+
if (!rec) {
|
|
8048
|
+
return {
|
|
8049
|
+
primary: "Harness worker complete finished with no parseable result.",
|
|
8050
|
+
diagnostic: diagnosticJson(data)
|
|
8051
|
+
};
|
|
8052
|
+
}
|
|
8053
|
+
const worker = str(rec.worker) ?? "worker";
|
|
8054
|
+
const runId = str(rec.runId);
|
|
8055
|
+
const skipped = str(rec.status) === "skipped";
|
|
8056
|
+
const httpStatus = typeof rec.httpStatus === "number" ? rec.httpStatus : null;
|
|
8057
|
+
const response = record(rec.response);
|
|
8058
|
+
const lines = [];
|
|
8059
|
+
lines.push(`Harness worker complete \xB7 ${worker}${runId ? ` (${runId})` : ""}`);
|
|
8060
|
+
if (skipped) {
|
|
8061
|
+
lines.push(`Outcome: skipped \u2014 ${str(rec.reason) ?? "worker not finished yet"}.`);
|
|
8062
|
+
lines.push("Next: wait for the worker to exit or post a finalResult, then retry complete.");
|
|
8063
|
+
return { primary: lines.join("\n"), diagnostic: diagnosticJson(data) };
|
|
8064
|
+
}
|
|
8065
|
+
const routeOutcome = str(response?.outcome);
|
|
8066
|
+
const taskRec = record(response?.task);
|
|
8067
|
+
const taskStatus = str(taskRec?.status);
|
|
8068
|
+
const prUrl = str(taskRec?.prUrl) ?? str(response?.prUrl);
|
|
8069
|
+
if (httpStatus && httpStatus >= 200 && httpStatus < 300) {
|
|
8070
|
+
lines.push("Outcome: completion callback accepted by AgentOS.");
|
|
8071
|
+
if (routeOutcome) lines.push(`Routing: ${routeOutcome.replace(/_/g, " ")}`);
|
|
8072
|
+
if (taskStatus) lines.push(`AgentOS task status: ${taskStatus}`);
|
|
8073
|
+
if (prUrl) lines.push(`PR: ${prUrl}`);
|
|
8074
|
+
lines.push("Next: check Command Center for review scheduling or blockers.");
|
|
8075
|
+
} else {
|
|
8076
|
+
lines.push(
|
|
8077
|
+
`Outcome: completion failed${httpStatus != null ? ` (HTTP ${httpStatus})` : ""}.`
|
|
8078
|
+
);
|
|
8079
|
+
const detail = str(response?.detail) ?? str(response?.error);
|
|
8080
|
+
if (detail) lines.push(`Blocker: ${detail}`);
|
|
8081
|
+
lines.push("Next: fix the reported blocker and replay completion from the board.");
|
|
8082
|
+
}
|
|
8083
|
+
return { primary: lines.join("\n"), diagnostic: diagnosticJson(data) };
|
|
8084
|
+
}
|
|
8085
|
+
|
|
8086
|
+
// src/harness-notice/harness-notice.worker-status.ts
|
|
8087
|
+
function str2(value) {
|
|
8088
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
8089
|
+
}
|
|
8090
|
+
function record2(value) {
|
|
8091
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
8092
|
+
}
|
|
8093
|
+
function prFromFinalResult(finalResult) {
|
|
8094
|
+
const fr = record2(finalResult);
|
|
8095
|
+
if (!fr) return null;
|
|
8096
|
+
return str2(fr.prUrl) ?? str2(fr.pr);
|
|
8097
|
+
}
|
|
8098
|
+
function formatWorkerStatusNotice(status) {
|
|
8099
|
+
const rec = record2(status);
|
|
8100
|
+
if (!rec) {
|
|
8101
|
+
return {
|
|
8102
|
+
primary: "Harness worker status unavailable.",
|
|
8103
|
+
diagnostic: diagnosticJson(status)
|
|
8104
|
+
};
|
|
8105
|
+
}
|
|
8106
|
+
const worker = str2(rec.worker) ?? str2(rec.name) ?? "worker";
|
|
8107
|
+
const runId = str2(rec.runId);
|
|
8108
|
+
const workerStatus2 = str2(rec.status) ?? "unknown";
|
|
8109
|
+
const alive = rec.alive === true;
|
|
8110
|
+
const attention = record2(rec.attention);
|
|
8111
|
+
const attentionState = str2(attention?.state) ?? str2(rec.attentionState);
|
|
8112
|
+
const attentionReason = str2(attention?.reason) ?? str2(rec.attentionReason);
|
|
8113
|
+
const taskId = str2(rec.taskId);
|
|
8114
|
+
const prUrl = str2(rec.prUrl) ?? prFromFinalResult(rec.finalResult);
|
|
8115
|
+
const branch = str2(rec.branch);
|
|
8116
|
+
const headCommit = str2(rec.headCommit);
|
|
8117
|
+
const lines = [];
|
|
8118
|
+
lines.push(`Harness worker ${worker}${runId ? ` (${runId})` : ""}`);
|
|
8119
|
+
lines.push(`Process: ${alive ? "running" : "stopped"} \xB7 harness status: ${workerStatus2}`);
|
|
8120
|
+
if (attentionState) {
|
|
8121
|
+
lines.push(
|
|
8122
|
+
attentionReason ? `Attention: ${attentionState} \u2014 ${attentionReason}` : `Attention: ${attentionState}`
|
|
8123
|
+
);
|
|
8124
|
+
}
|
|
8125
|
+
if (taskId) lines.push(`AgentOS task: ${taskId}`);
|
|
8126
|
+
if (prUrl) lines.push(`PR: ${prUrl}`);
|
|
8127
|
+
if (branch) lines.push(`Branch: ${branch}`);
|
|
8128
|
+
if (headCommit) lines.push(`Commit: ${headCommit.slice(0, 12)}`);
|
|
8129
|
+
if (workerStatus2 === "done" || workerStatus2 === "exited") {
|
|
8130
|
+
lines.push(
|
|
8131
|
+
prUrl ? "Outcome: worker finished \u2014 open the PR or check Command Center for review routing." : "Outcome: worker finished \u2014 check Command Center for task status and next action."
|
|
8132
|
+
);
|
|
8133
|
+
} else if (attentionState === "blocked" || attentionState === "needs_attention") {
|
|
8134
|
+
lines.push("Next: resolve the blocker on the board or wait for the monitor to auto-complete when terminal.");
|
|
8135
|
+
} else if (alive) {
|
|
8136
|
+
lines.push("Next: wait for completion or poll again; background monitor will auto-complete when eligible.");
|
|
8137
|
+
}
|
|
8138
|
+
return { primary: lines.join("\n"), diagnostic: diagnosticJson(status) };
|
|
8139
|
+
}
|
|
8140
|
+
|
|
8141
|
+
// src/harness-notice/harness-notice.tool-response.ts
|
|
8142
|
+
var DIVIDER = "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 Diagnostic (JSON) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500";
|
|
8143
|
+
function joinHarnessNotice(notice) {
|
|
8144
|
+
if (!notice.diagnostic?.trim()) return notice.primary;
|
|
8145
|
+
return `${notice.primary}
|
|
8146
|
+
|
|
8147
|
+
${DIVIDER}
|
|
8148
|
+
${notice.diagnostic}`;
|
|
8149
|
+
}
|
|
8150
|
+
function failureNotice(ctx) {
|
|
8151
|
+
const lines = [`Harness ${ctx.scope} ${ctx.action} failed.`];
|
|
8152
|
+
if (ctx.timedOut) lines.push("Reason: command timed out on the runner host.");
|
|
8153
|
+
else if (ctx.error) lines.push(`Reason: ${ctx.error}`);
|
|
8154
|
+
else if (ctx.exitCode != null) lines.push(`Exit code: ${ctx.exitCode}`);
|
|
8155
|
+
const errText = ctx.stderr.trim();
|
|
8156
|
+
if (errText) lines.push(`Stderr: ${errText.split("\n").slice(-3).join(" ")}`);
|
|
8157
|
+
lines.push("Next: retry on the runner host or inspect harness logs.");
|
|
8158
|
+
const parsed = firstJsonFromStdout(ctx.stdout);
|
|
8159
|
+
return {
|
|
8160
|
+
primary: lines.join("\n"),
|
|
8161
|
+
diagnostic: diagnosticJson(parsed ?? { stdout: ctx.stdout, stderr: ctx.stderr })
|
|
8162
|
+
};
|
|
8163
|
+
}
|
|
8164
|
+
function formatHarnessToolReadable(ctx) {
|
|
8165
|
+
if (!ctx.ok) return failureNotice(ctx);
|
|
8166
|
+
const parsed = firstJsonFromStdout(ctx.stdout);
|
|
8167
|
+
if (parsed === null) {
|
|
8168
|
+
const text = ctx.stdout.trim() || ctx.stderr.trim() || "(no output)";
|
|
8169
|
+
return {
|
|
8170
|
+
primary: `Harness ${ctx.scope} ${ctx.action} finished.
|
|
8171
|
+
${text.slice(0, 800)}`,
|
|
8172
|
+
diagnostic: diagnosticJson({ stdout: ctx.stdout, stderr: ctx.stderr })
|
|
8173
|
+
};
|
|
8174
|
+
}
|
|
8175
|
+
if (ctx.scope === "worker" && ctx.action === "status") {
|
|
8176
|
+
return formatWorkerStatusNotice(parsed);
|
|
8177
|
+
}
|
|
8178
|
+
if (ctx.scope === "worker" && ctx.action === "complete") {
|
|
8179
|
+
return formatWorkerCompleteNotice(parsed);
|
|
8180
|
+
}
|
|
8181
|
+
if (ctx.scope === "monitor" && (ctx.action === "tick" || ctx.action === "run-loop")) {
|
|
8182
|
+
return formatMonitorTickNotice(parsed);
|
|
8183
|
+
}
|
|
8184
|
+
if (ctx.scope === "monitor" && ctx.action === "auto-complete") {
|
|
8185
|
+
const rec = parsed;
|
|
8186
|
+
if (rec.outcome && rec.worker && rec.runId) {
|
|
8187
|
+
return formatAutoCompleteOutcomeNotice(parsed);
|
|
8188
|
+
}
|
|
8189
|
+
if (Array.isArray(rec.blockers)) {
|
|
8190
|
+
return {
|
|
8191
|
+
primary: [
|
|
8192
|
+
`Monitor auto-complete blocked \xB7 ${rec.runId ?? "run"} / ${rec.worker ?? "worker"}`,
|
|
8193
|
+
`Blockers: ${rec.blockers.join("; ")}`,
|
|
8194
|
+
"Next: resolve blockers on the runner, then retry auto-complete."
|
|
8195
|
+
].join("\n"),
|
|
8196
|
+
diagnostic: diagnosticJson(parsed)
|
|
8197
|
+
};
|
|
8198
|
+
}
|
|
8199
|
+
}
|
|
8200
|
+
if (ctx.scope === "run" && ctx.action === "status") {
|
|
8201
|
+
const workers = parsed.workers;
|
|
8202
|
+
if (Array.isArray(workers) && workers.length === 1) {
|
|
8203
|
+
return formatWorkerStatusNotice(workers[0]);
|
|
8204
|
+
}
|
|
8205
|
+
return {
|
|
8206
|
+
primary: `Harness run status \xB7 ${parsed.runId ?? "run"} (${Array.isArray(workers) ? workers.length : 0} workers).`,
|
|
8207
|
+
diagnostic: diagnosticJson(parsed)
|
|
8208
|
+
};
|
|
8209
|
+
}
|
|
8210
|
+
return {
|
|
8211
|
+
primary: `Harness ${ctx.scope} ${ctx.action} completed successfully on the runner.`,
|
|
8212
|
+
diagnostic: diagnosticJson(parsed)
|
|
8213
|
+
};
|
|
8214
|
+
}
|
|
8215
|
+
|
|
7020
8216
|
// src/monitor/monitor.service.ts
|
|
7021
|
-
import
|
|
8217
|
+
import path46 from "node:path";
|
|
7022
8218
|
|
|
7023
8219
|
// src/monitor/monitor.classify.ts
|
|
7024
8220
|
function expectedLeaseOwner(runId) {
|
|
@@ -7074,11 +8270,11 @@ function classifyWorkerHealth(input) {
|
|
|
7074
8270
|
}
|
|
7075
8271
|
|
|
7076
8272
|
// src/monitor/monitor.store.ts
|
|
7077
|
-
import { existsSync as
|
|
7078
|
-
import
|
|
8273
|
+
import { existsSync as existsSync28, mkdirSync as mkdirSync6, readdirSync as readdirSync9, unlinkSync as unlinkSync2 } from "node:fs";
|
|
8274
|
+
import path45 from "node:path";
|
|
7079
8275
|
function monitorsDir() {
|
|
7080
8276
|
const { harnessRoot } = getHarnessPaths();
|
|
7081
|
-
const dir =
|
|
8277
|
+
const dir = path45.join(harnessRoot, "monitors");
|
|
7082
8278
|
mkdirSync6(dir, { recursive: true });
|
|
7083
8279
|
return dir;
|
|
7084
8280
|
}
|
|
@@ -7086,7 +8282,7 @@ function monitorIdFor(runId, workerName) {
|
|
|
7086
8282
|
return workerName ? `${safeSlug(runId)}--${safeSlug(workerName)}` : safeSlug(runId);
|
|
7087
8283
|
}
|
|
7088
8284
|
function monitorPath(monitorId) {
|
|
7089
|
-
return
|
|
8285
|
+
return path45.join(monitorsDir(), `${monitorId}.json`);
|
|
7090
8286
|
}
|
|
7091
8287
|
function loadMonitorSession(monitorId) {
|
|
7092
8288
|
return readJson(monitorPath(monitorId), void 0);
|
|
@@ -7096,18 +8292,18 @@ function saveMonitorSession(session) {
|
|
|
7096
8292
|
}
|
|
7097
8293
|
function deleteMonitorSession(monitorId) {
|
|
7098
8294
|
const file = monitorPath(monitorId);
|
|
7099
|
-
if (!
|
|
8295
|
+
if (!existsSync28(file)) return false;
|
|
7100
8296
|
unlinkSync2(file);
|
|
7101
8297
|
return true;
|
|
7102
8298
|
}
|
|
7103
8299
|
function listMonitorSessions() {
|
|
7104
8300
|
const dir = monitorsDir();
|
|
7105
|
-
if (!
|
|
8301
|
+
if (!existsSync28(dir)) return [];
|
|
7106
8302
|
const entries = [];
|
|
7107
|
-
for (const name of
|
|
8303
|
+
for (const name of readdirSync9(dir)) {
|
|
7108
8304
|
if (!name.endsWith(".json")) continue;
|
|
7109
8305
|
const session = readJson(
|
|
7110
|
-
|
|
8306
|
+
path45.join(dir, name),
|
|
7111
8307
|
void 0
|
|
7112
8308
|
);
|
|
7113
8309
|
if (!session?.monitorId) continue;
|
|
@@ -7198,7 +8394,7 @@ async function fetchTaskLeasesForWorkers(input) {
|
|
|
7198
8394
|
// src/monitor/monitor.service.ts
|
|
7199
8395
|
function workerRecord2(runId, name) {
|
|
7200
8396
|
return readJson(
|
|
7201
|
-
|
|
8397
|
+
path46.join(runDirectory(runId), "workers", safeSlug(name), "worker.json"),
|
|
7202
8398
|
void 0
|
|
7203
8399
|
);
|
|
7204
8400
|
}
|
|
@@ -7383,7 +8579,11 @@ async function runMonitorLoop(args) {
|
|
|
7383
8579
|
autoComplete: args.autoComplete ?? true,
|
|
7384
8580
|
renewLeases: args.renewLeases ?? true
|
|
7385
8581
|
});
|
|
7386
|
-
|
|
8582
|
+
const notice = formatMonitorTickNotice({ monitorId, phase: "tick", ...tick });
|
|
8583
|
+
console.log(notice.primary);
|
|
8584
|
+
if (notice.diagnostic) {
|
|
8585
|
+
console.error(`[monitor diagnostic] ${notice.diagnostic}`);
|
|
8586
|
+
}
|
|
7387
8587
|
const allTerminal = tick.workers.length > 0 && tick.workers.every(
|
|
7388
8588
|
(w) => w.autoComplete.terminalVerified && (w.autoComplete.eligible || w.autoComplete.blockers.some((b) => b.includes("already acknowledged")))
|
|
7389
8589
|
);
|
|
@@ -7400,18 +8600,18 @@ async function runMonitorLoop(args) {
|
|
|
7400
8600
|
|
|
7401
8601
|
// src/monitor/monitor-spawn.ts
|
|
7402
8602
|
import { spawn as spawn4 } from "node:child_process";
|
|
7403
|
-
import { closeSync as closeSync4, existsSync as
|
|
7404
|
-
import
|
|
8603
|
+
import { closeSync as closeSync4, existsSync as existsSync29, openSync as openSync4 } from "node:fs";
|
|
8604
|
+
import path47 from "node:path";
|
|
7405
8605
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
7406
8606
|
function resolveDefaultCliPath2() {
|
|
7407
|
-
return
|
|
8607
|
+
return path47.join(fileURLToPath4(new URL(".", import.meta.url)), "cli.js");
|
|
7408
8608
|
}
|
|
7409
8609
|
function spawnMonitorSidecar(opts) {
|
|
7410
8610
|
const cliPath = opts.cliPath ?? resolveDefaultCliPath2();
|
|
7411
|
-
if (!
|
|
8611
|
+
if (!existsSync29(cliPath)) return void 0;
|
|
7412
8612
|
const monitorId = monitorIdFor(opts.runId, opts.workerName);
|
|
7413
8613
|
const { harnessRoot } = getHarnessPaths();
|
|
7414
|
-
const logPath =
|
|
8614
|
+
const logPath = path47.join(harnessRoot, "monitors", `${monitorId}.log`);
|
|
7415
8615
|
let logFd;
|
|
7416
8616
|
try {
|
|
7417
8617
|
logFd = openSync4(logPath, "a");
|
|
@@ -7531,12 +8731,12 @@ async function monitorTickCli(args) {
|
|
|
7531
8731
|
}
|
|
7532
8732
|
|
|
7533
8733
|
// src/doctor/runtime-takeover.ts
|
|
7534
|
-
import
|
|
8734
|
+
import path49 from "node:path";
|
|
7535
8735
|
|
|
7536
8736
|
// src/doctor/runtime-takeover.probes.ts
|
|
7537
|
-
import { accessSync, constants, existsSync as
|
|
7538
|
-
import { homedir as
|
|
7539
|
-
import
|
|
8737
|
+
import { accessSync, constants, existsSync as existsSync30, readFileSync as readFileSync13 } from "node:fs";
|
|
8738
|
+
import { homedir as homedir8 } from "node:os";
|
|
8739
|
+
import path48 from "node:path";
|
|
7540
8740
|
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
7541
8741
|
function captureCommand(bin, args) {
|
|
7542
8742
|
try {
|
|
@@ -7565,7 +8765,7 @@ function tokenPrefix(token) {
|
|
|
7565
8765
|
return trimmed.length <= 12 ? `${trimmed}\u2026` : `${trimmed.slice(0, 12)}\u2026`;
|
|
7566
8766
|
}
|
|
7567
8767
|
function isWritable(target) {
|
|
7568
|
-
if (!
|
|
8768
|
+
if (!existsSync30(target)) return false;
|
|
7569
8769
|
try {
|
|
7570
8770
|
accessSync(target, constants.W_OK);
|
|
7571
8771
|
return true;
|
|
@@ -7578,15 +8778,15 @@ var defaultRuntimeTakeoverProbes = {
|
|
|
7578
8778
|
commandOnPath: (bin) => captureCommand(process.platform === "win32" ? "where" : "which", [bin]),
|
|
7579
8779
|
kynverVersion: (bin) => captureCommand(bin, ["--version"]),
|
|
7580
8780
|
loadConfig: () => loadUserConfig(),
|
|
7581
|
-
configFilePath: () =>
|
|
7582
|
-
credentialsFilePath: () =>
|
|
8781
|
+
configFilePath: () => path48.join(homedir8(), ".kynver", "config.json"),
|
|
8782
|
+
credentialsFilePath: () => path48.join(homedir8(), ".kynver", "credentials"),
|
|
7583
8783
|
readCredentials: () => {
|
|
7584
|
-
const credPath =
|
|
7585
|
-
if (!
|
|
8784
|
+
const credPath = path48.join(homedir8(), ".kynver", "credentials");
|
|
8785
|
+
if (!existsSync30(credPath)) {
|
|
7586
8786
|
return { hasApiKey: false };
|
|
7587
8787
|
}
|
|
7588
8788
|
try {
|
|
7589
|
-
const parsed = JSON.parse(
|
|
8789
|
+
const parsed = JSON.parse(readFileSync13(credPath, "utf8"));
|
|
7590
8790
|
return {
|
|
7591
8791
|
hasApiKey: Boolean(parsed.apiKey?.trim()),
|
|
7592
8792
|
runnerTokenPrefix: tokenPrefix(parsed.runnerToken),
|
|
@@ -7613,8 +8813,8 @@ var defaultRuntimeTakeoverProbes = {
|
|
|
7613
8813
|
})()
|
|
7614
8814
|
}),
|
|
7615
8815
|
harnessRoot: () => resolveHarnessRoot(),
|
|
7616
|
-
legacyOpenclawHarnessRoot: () =>
|
|
7617
|
-
pathExists: (target) =>
|
|
8816
|
+
legacyOpenclawHarnessRoot: () => path48.join(homedir8(), ".openclaw", "harness"),
|
|
8817
|
+
pathExists: (target) => existsSync30(target),
|
|
7618
8818
|
pathWritable: (target) => isWritable(target),
|
|
7619
8819
|
vercelVersion: () => captureCommand("vercel", ["--version"]),
|
|
7620
8820
|
vercelWhoami: () => captureCommand("vercel", ["whoami"])
|
|
@@ -7662,7 +8862,7 @@ function assessRuntimeTakeoverScheduler(env, ctx) {
|
|
|
7662
8862
|
label: "Scheduler provider (runtime daemon vs OpenClaw cron)",
|
|
7663
8863
|
status: "warn",
|
|
7664
8864
|
summary: `OpenClaw local cron still active (${parts.join("; ")})`,
|
|
7665
|
-
remediation: "On the Kynver deployment: set KYNVER_SCHEDULER_PROVIDER=qstash with QSTASH_TOKEN configured. On user runners: unset KYNVER_SCHEDULER_PROVIDER and OPENCLAW_CRON_STORE_PATH;
|
|
8865
|
+
remediation: "On the Kynver deployment: set KYNVER_SCHEDULER_PROVIDER=qstash with QSTASH_TOKEN configured. On user runners: unset KYNVER_SCHEDULER_PROVIDER and OPENCLAW_CRON_STORE_PATH; after Vercel env is updated run `kynver scheduler attest-cutover`.",
|
|
7666
8866
|
details: schedulerDetails
|
|
7667
8867
|
});
|
|
7668
8868
|
}
|
|
@@ -7942,8 +9142,8 @@ function assessVercelCli(probes) {
|
|
|
7942
9142
|
}
|
|
7943
9143
|
function assessHarnessDirs(probes) {
|
|
7944
9144
|
const harnessRoot = probes.harnessRoot();
|
|
7945
|
-
const runsDir =
|
|
7946
|
-
const worktreesDir =
|
|
9145
|
+
const runsDir = path49.join(harnessRoot, "runs");
|
|
9146
|
+
const worktreesDir = path49.join(harnessRoot, "worktrees");
|
|
7947
9147
|
const displayHarnessRoot = redactHomePath(harnessRoot);
|
|
7948
9148
|
const displayRunsDir = redactHomePath(runsDir);
|
|
7949
9149
|
const displayWorktreesDir = redactHomePath(worktreesDir);
|
|
@@ -8131,6 +9331,131 @@ async function runCommandCenterContractCli(args) {
|
|
|
8131
9331
|
console.log(JSON.stringify(res.response, null, 2));
|
|
8132
9332
|
}
|
|
8133
9333
|
|
|
9334
|
+
// src/scheduler-cutover.ts
|
|
9335
|
+
var DEPLOYMENT_SCHEDULER_CUTOVER_STEPS = [
|
|
9336
|
+
"Vercel/hosted: set KYNVER_SCHEDULER_PROVIDER=qstash",
|
|
9337
|
+
"Vercel/hosted: ensure QSTASH_TOKEN (and QStash signing keys) are configured",
|
|
9338
|
+
"Vercel/hosted: unset OPENCLAW_CRON_STORE_PATH if present"
|
|
9339
|
+
];
|
|
9340
|
+
var RUNNER_SCHEDULER_CUTOVER_STEPS = [
|
|
9341
|
+
"User runner: unset KYNVER_SCHEDULER_PROVIDER (scheduling is deployment-owned)",
|
|
9342
|
+
"User runner: unset OPENCLAW_CRON_STORE_PATH, OPENCLAW_CRON_SECRET, OPENCLAW_CRON_FIRE_BASE_URL",
|
|
9343
|
+
'User runner: after deployment cutover, run `kynver scheduler attest-cutover` (or set deploymentSchedulerProvider to "qstash" in ~/.kynver/config.json)',
|
|
9344
|
+
"Verify: kynver doctor runtime-takeover \u2014 hotspot_openclaw_scheduler should pass"
|
|
9345
|
+
];
|
|
9346
|
+
function readSchedulerCutoverEnv(env = process.env) {
|
|
9347
|
+
return {
|
|
9348
|
+
kynverSchedulerProvider: env.KYNVER_SCHEDULER_PROVIDER?.trim() || null,
|
|
9349
|
+
openclawCronStorePath: env.OPENCLAW_CRON_STORE_PATH?.trim() || null,
|
|
9350
|
+
openclawCronSecret: Boolean(env.OPENCLAW_CRON_SECRET?.trim()),
|
|
9351
|
+
openclawCronFireBaseUrl: env.OPENCLAW_CRON_FIRE_BASE_URL?.trim() || null
|
|
9352
|
+
};
|
|
9353
|
+
}
|
|
9354
|
+
function assessSchedulerCutover(config, env = readSchedulerCutoverEnv()) {
|
|
9355
|
+
const blockers = [];
|
|
9356
|
+
if (env.kynverSchedulerProvider === "openclaw-cron") {
|
|
9357
|
+
blockers.push("Runner still has KYNVER_SCHEDULER_PROVIDER=openclaw-cron");
|
|
9358
|
+
}
|
|
9359
|
+
if (env.openclawCronStorePath) {
|
|
9360
|
+
blockers.push("Runner still has OPENCLAW_CRON_STORE_PATH");
|
|
9361
|
+
}
|
|
9362
|
+
if (config.deploymentSchedulerProvider === "openclaw-cron") {
|
|
9363
|
+
blockers.push("~/.kynver/config.json deploymentSchedulerProvider is still openclaw-cron");
|
|
9364
|
+
}
|
|
9365
|
+
return {
|
|
9366
|
+
ok: blockers.length === 0,
|
|
9367
|
+
blockers,
|
|
9368
|
+
runnerEnv: env,
|
|
9369
|
+
deploymentSchedulerProvider: config.deploymentSchedulerProvider ?? null,
|
|
9370
|
+
deploymentSteps: [...DEPLOYMENT_SCHEDULER_CUTOVER_STEPS],
|
|
9371
|
+
runnerSteps: [...RUNNER_SCHEDULER_CUTOVER_STEPS]
|
|
9372
|
+
};
|
|
9373
|
+
}
|
|
9374
|
+
function applySchedulerCutoverAttestation(config) {
|
|
9375
|
+
return {
|
|
9376
|
+
...config,
|
|
9377
|
+
deploymentSchedulerProvider: "qstash"
|
|
9378
|
+
};
|
|
9379
|
+
}
|
|
9380
|
+
|
|
9381
|
+
// src/scheduler-cutover-cli.ts
|
|
9382
|
+
import path50 from "node:path";
|
|
9383
|
+
import { homedir as homedir9 } from "node:os";
|
|
9384
|
+
var CONFIG_FILE2 = path50.join(homedir9(), ".kynver", "config.json");
|
|
9385
|
+
function runSchedulerCutoverCheckCli(json = false) {
|
|
9386
|
+
const config = loadUserConfig();
|
|
9387
|
+
const report = assessSchedulerCutover(config);
|
|
9388
|
+
const payload = {
|
|
9389
|
+
...report,
|
|
9390
|
+
configPath: displayUserPath(CONFIG_FILE2),
|
|
9391
|
+
configAttestationExample: { deploymentSchedulerProvider: "qstash" }
|
|
9392
|
+
};
|
|
9393
|
+
if (json) {
|
|
9394
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
9395
|
+
if (!report.ok) process.exitCode = 1;
|
|
9396
|
+
return;
|
|
9397
|
+
}
|
|
9398
|
+
console.log("AgentOS scheduler provider cutover checklist\n");
|
|
9399
|
+
console.log("Deployment (Vercel):");
|
|
9400
|
+
for (const step of DEPLOYMENT_SCHEDULER_CUTOVER_STEPS) console.log(` - ${step}`);
|
|
9401
|
+
console.log("\nUser runner:");
|
|
9402
|
+
for (const step of RUNNER_SCHEDULER_CUTOVER_STEPS) console.log(` - ${step}`);
|
|
9403
|
+
console.log("\nThis host:");
|
|
9404
|
+
console.log(` config: ${payload.configPath}`);
|
|
9405
|
+
console.log(
|
|
9406
|
+
` deploymentSchedulerProvider: ${report.deploymentSchedulerProvider ?? "(unset)"}`
|
|
9407
|
+
);
|
|
9408
|
+
console.log(
|
|
9409
|
+
` KYNVER_SCHEDULER_PROVIDER: ${report.runnerEnv.kynverSchedulerProvider ?? "(unset)"}`
|
|
9410
|
+
);
|
|
9411
|
+
console.log(
|
|
9412
|
+
` OPENCLAW_CRON_STORE_PATH: ${report.runnerEnv.openclawCronStorePath ?? "(unset)"}`
|
|
9413
|
+
);
|
|
9414
|
+
if (report.blockers.length) {
|
|
9415
|
+
console.log("\nBlockers:");
|
|
9416
|
+
for (const b of report.blockers) console.log(` ! ${b}`);
|
|
9417
|
+
process.exitCode = 1;
|
|
9418
|
+
return;
|
|
9419
|
+
}
|
|
9420
|
+
console.log("\nNo local blockers detected on this runner.");
|
|
9421
|
+
}
|
|
9422
|
+
function runSchedulerAttestCutoverCli(json = false) {
|
|
9423
|
+
const existing = loadUserConfig();
|
|
9424
|
+
const report = assessSchedulerCutover(existing);
|
|
9425
|
+
if (!report.ok) {
|
|
9426
|
+
const payload2 = {
|
|
9427
|
+
ok: false,
|
|
9428
|
+
attested: false,
|
|
9429
|
+
blockers: report.blockers,
|
|
9430
|
+
remediation: "Clear local OpenClaw scheduler blockers before attesting qstash cutover."
|
|
9431
|
+
};
|
|
9432
|
+
if (json) {
|
|
9433
|
+
console.log(JSON.stringify(payload2, null, 2));
|
|
9434
|
+
} else {
|
|
9435
|
+
console.error("Cannot attest scheduler cutover \u2014 local blockers remain:");
|
|
9436
|
+
for (const b of report.blockers) console.error(` ! ${b}`);
|
|
9437
|
+
}
|
|
9438
|
+
process.exitCode = 1;
|
|
9439
|
+
return;
|
|
9440
|
+
}
|
|
9441
|
+
const next = applySchedulerCutoverAttestation(existing);
|
|
9442
|
+
saveUserConfig(next);
|
|
9443
|
+
const payload = {
|
|
9444
|
+
ok: true,
|
|
9445
|
+
attested: true,
|
|
9446
|
+
configPath: displayUserPath(CONFIG_FILE2),
|
|
9447
|
+
deploymentSchedulerProvider: "qstash",
|
|
9448
|
+
config: presentUserConfig(next),
|
|
9449
|
+
note: "Recorded deploymentSchedulerProvider=qstash in ~/.kynver/config.json. Confirm Vercel has KYNVER_SCHEDULER_PROVIDER=qstash and QSTASH_TOKEN before relying on hosted schedules."
|
|
9450
|
+
};
|
|
9451
|
+
if (json) {
|
|
9452
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
9453
|
+
return;
|
|
9454
|
+
}
|
|
9455
|
+
console.log(payload.note);
|
|
9456
|
+
console.log(` config: ${payload.configPath}`);
|
|
9457
|
+
}
|
|
9458
|
+
|
|
8134
9459
|
// src/cli.ts
|
|
8135
9460
|
function isHelpFlag(arg) {
|
|
8136
9461
|
return arg === "help" || arg === "--help" || arg === "-h";
|
|
@@ -8178,6 +9503,8 @@ function usage(code = 0) {
|
|
|
8178
9503
|
" kynver monitor auto-complete --run RUN_ID --name worker [--agent-os-id AOS_ID] [--base-url URL] [--secret SECRET]",
|
|
8179
9504
|
" kynver monitor run-loop --run RUN_ID --monitor-id ID [--name worker] [--agent-os-id AOS_ID] [--poll-ms MS] [--auto-complete] [--renew-leases]",
|
|
8180
9505
|
" kynver doctor runtime-takeover",
|
|
9506
|
+
" kynver scheduler cutover-check [--json]",
|
|
9507
|
+
" kynver scheduler attest-cutover [--json]",
|
|
8181
9508
|
" kynver board contract [--agent-os-id ID] [--base-url URL] [--since ISO] [--limit N]"
|
|
8182
9509
|
].join("\n")
|
|
8183
9510
|
);
|
|
@@ -8189,7 +9516,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
8189
9516
|
const scope = argv.shift();
|
|
8190
9517
|
let action;
|
|
8191
9518
|
let rest;
|
|
8192
|
-
if (scope === "run" || scope === "worker" || scope === "plan" || scope === "runner" || scope === "harness" || scope === "monitor" || scope === "doctor" || scope === "board") {
|
|
9519
|
+
if (scope === "run" || scope === "worker" || scope === "plan" || scope === "runner" || scope === "harness" || scope === "monitor" || scope === "doctor" || scope === "scheduler" || scope === "board") {
|
|
8193
9520
|
action = argv.shift();
|
|
8194
9521
|
rest = argv;
|
|
8195
9522
|
} else {
|
|
@@ -8216,6 +9543,12 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
8216
9543
|
}
|
|
8217
9544
|
if (scope === "cleanup") return runCleanupCli(args);
|
|
8218
9545
|
if (scope === "doctor" && action === "runtime-takeover") return runRuntimeTakeoverDoctorCli();
|
|
9546
|
+
if (scope === "scheduler" && action === "cutover-check") {
|
|
9547
|
+
return runSchedulerCutoverCheckCli(args.json === true);
|
|
9548
|
+
}
|
|
9549
|
+
if (scope === "scheduler" && action === "attest-cutover") {
|
|
9550
|
+
return runSchedulerAttestCutoverCli(args.json === true);
|
|
9551
|
+
}
|
|
8219
9552
|
if (scope === "board" && action === "contract") {
|
|
8220
9553
|
return void await runCommandCenterContractCli(args);
|
|
8221
9554
|
}
|
|
@@ -8255,6 +9588,19 @@ if (isCliEntry) {
|
|
|
8255
9588
|
|
|
8256
9589
|
// src/vercel/vercel-url.ts
|
|
8257
9590
|
var VERCEL_HOST_RE = /(^|\.)vercel\.app$/i;
|
|
9591
|
+
var DPL_ID_RE = /^dpl_[a-z0-9]+$/i;
|
|
9592
|
+
function isInspectableVercelTarget(target) {
|
|
9593
|
+
const trimmed = target.trim();
|
|
9594
|
+
if (!trimmed) return false;
|
|
9595
|
+
if (/vercel\.com/i.test(trimmed)) return false;
|
|
9596
|
+
if (DPL_ID_RE.test(trimmed)) return true;
|
|
9597
|
+
try {
|
|
9598
|
+
const url = new URL(trimmed.startsWith("http") ? trimmed : `https://${trimmed}`);
|
|
9599
|
+
return VERCEL_HOST_RE.test(url.hostname);
|
|
9600
|
+
} catch {
|
|
9601
|
+
return false;
|
|
9602
|
+
}
|
|
9603
|
+
}
|
|
8258
9604
|
function tryParseUrl(raw) {
|
|
8259
9605
|
const trimmed = raw.trim();
|
|
8260
9606
|
if (!trimmed) return null;
|
|
@@ -8308,10 +9654,11 @@ function classifyVercelUrl(raw) {
|
|
|
8308
9654
|
}
|
|
8309
9655
|
if (url.hostname === "vercel.com" || url.hostname.endsWith(".vercel.com")) {
|
|
8310
9656
|
const deploymentId = parseDeploymentsSegment(url) ?? parseDashboardDeployment(url);
|
|
9657
|
+
const inspectTarget = deploymentId && DPL_ID_RE.test(deploymentId) ? deploymentId : null;
|
|
8311
9658
|
return {
|
|
8312
9659
|
kind: "dashboard",
|
|
8313
9660
|
previewUrl: null,
|
|
8314
|
-
inspectTarget
|
|
9661
|
+
inspectTarget,
|
|
8315
9662
|
deploymentId
|
|
8316
9663
|
};
|
|
8317
9664
|
}
|
|
@@ -8386,14 +9733,14 @@ function resolveVercelInspectTarget(rawUrl) {
|
|
|
8386
9733
|
return { target: null, classified: null, reason: "missing target_url" };
|
|
8387
9734
|
}
|
|
8388
9735
|
const classified = classifyVercelUrl(trimmed);
|
|
8389
|
-
if (classified.inspectTarget) {
|
|
9736
|
+
if (classified.inspectTarget && isInspectableVercelTarget(classified.inspectTarget)) {
|
|
8390
9737
|
return { target: classified.inspectTarget, classified, reason: null };
|
|
8391
9738
|
}
|
|
8392
9739
|
if (classified.kind === "dashboard") {
|
|
8393
9740
|
return {
|
|
8394
9741
|
target: null,
|
|
8395
9742
|
classified,
|
|
8396
|
-
reason: "dashboard URL is not valid for vercel inspect"
|
|
9743
|
+
reason: classified.deploymentId ? "dashboard deployment id is not CLI-inspectable; trust GitHub Vercel status" : "dashboard URL is not valid for vercel inspect"
|
|
8397
9744
|
};
|
|
8398
9745
|
}
|
|
8399
9746
|
return { target: null, classified, reason: "unrecognized Vercel URL" };
|
|
@@ -8434,6 +9781,15 @@ function evidenceFromGitHubVercelStatus(statuses, options = {}) {
|
|
|
8434
9781
|
};
|
|
8435
9782
|
}
|
|
8436
9783
|
function defaultRunVercelInspect(target, waitSeconds) {
|
|
9784
|
+
if (!isInspectableVercelTarget(target)) {
|
|
9785
|
+
return {
|
|
9786
|
+
ok: false,
|
|
9787
|
+
exitCode: 1,
|
|
9788
|
+
stdout: "",
|
|
9789
|
+
stderr: "",
|
|
9790
|
+
error: "refusing vercel inspect on non-inspectable target (dashboard URLs use GitHub status)"
|
|
9791
|
+
};
|
|
9792
|
+
}
|
|
8437
9793
|
const args = ["inspect", target, "--wait", String(waitSeconds)];
|
|
8438
9794
|
const result = spawnSync8("vercel", args, {
|
|
8439
9795
|
encoding: "utf8",
|
|
@@ -8544,10 +9900,14 @@ function collectVercelEvidence(input) {
|
|
|
8544
9900
|
export {
|
|
8545
9901
|
DEFAULT_DISPATCH_LEASE_MS,
|
|
8546
9902
|
DEFAULT_HARNESS_VERIFY_COMMANDS,
|
|
9903
|
+
DEFAULT_NODE_MODULES_AGE_MS,
|
|
9904
|
+
DEFAULT_WORKER_PROVIDER,
|
|
9905
|
+
DEFAULT_WORKTREES_AGE_MS,
|
|
8547
9906
|
DEFAULT_WSL_HOST_CRITICAL_FREE_BYTES,
|
|
8548
9907
|
DEFAULT_WSL_HOST_MOUNT,
|
|
8549
9908
|
DEFAULT_WSL_HOST_WARN_FREE_BYTES,
|
|
8550
9909
|
FORBIDDEN_WORKER_ENV_KEYS,
|
|
9910
|
+
HARNESS_BUILD_CACHE_RELATIVE_PATHS,
|
|
8551
9911
|
PACKAGE_VERSION,
|
|
8552
9912
|
applyProductionDatabaseToProcess,
|
|
8553
9913
|
assessAutoCompleteEligibility,
|
|
@@ -8560,8 +9920,10 @@ export {
|
|
|
8560
9920
|
auditWorkerEnv,
|
|
8561
9921
|
autoCompleteWorker,
|
|
8562
9922
|
autoCompleteWorkerCli,
|
|
9923
|
+
buildBoxResourceSnapshotFromGate,
|
|
8563
9924
|
buildDispatchTaskText,
|
|
8564
9925
|
buildPrompt,
|
|
9926
|
+
buildRunListRows,
|
|
8565
9927
|
buildSystemdRunArgv,
|
|
8566
9928
|
classifyNpmAuditOutcome,
|
|
8567
9929
|
classifyShellCommandOutcome,
|
|
@@ -8572,26 +9934,37 @@ export {
|
|
|
8572
9934
|
computeAttention,
|
|
8573
9935
|
computeWorkerStatus,
|
|
8574
9936
|
createRun,
|
|
9937
|
+
defaultBoxId,
|
|
8575
9938
|
deriveRunStatus,
|
|
8576
9939
|
discoverDefaultRepo,
|
|
8577
9940
|
discoverDefaultRepoCandidates,
|
|
8578
9941
|
dispatchRun,
|
|
8579
9942
|
drainPlanOutbox,
|
|
9943
|
+
enforceCursorWorkerProvider,
|
|
8580
9944
|
ensurePrReadyHandoff,
|
|
8581
9945
|
evidenceFromGitHubVercelStatus,
|
|
8582
9946
|
extractPlanOutboxFromTask,
|
|
8583
9947
|
extractPrUrlFromText,
|
|
9948
|
+
formatAutoCompleteOutcomeNotice,
|
|
9949
|
+
formatHarnessToolReadable,
|
|
9950
|
+
formatHeartbeatLine,
|
|
9951
|
+
formatMonitorTickNotice,
|
|
8584
9952
|
formatPlanOutboxHandoffBlock,
|
|
8585
9953
|
formatResolvedDefaultRepo,
|
|
9954
|
+
formatWorkerCompleteNotice,
|
|
9955
|
+
formatWorkerStatusNotice,
|
|
8586
9956
|
getHarnessPaths,
|
|
8587
9957
|
getMonitorStatus,
|
|
8588
9958
|
gitRepoRoot,
|
|
8589
9959
|
harnessStorageSnapshot,
|
|
8590
9960
|
hashPlanBody,
|
|
9961
|
+
isClaudeFamilyProvider,
|
|
8591
9962
|
isDashboardVercelUrl,
|
|
8592
9963
|
isEngagementRequiredSkip,
|
|
8593
9964
|
isFinishedWorkerStatus,
|
|
8594
9965
|
isForbiddenWorkerEnvKey,
|
|
9966
|
+
isGeneratedHarnessPath,
|
|
9967
|
+
isInspectableVercelTarget,
|
|
8595
9968
|
isKynverMonorepoRoot,
|
|
8596
9969
|
isLandingBlockedWorkerStatus,
|
|
8597
9970
|
isPipelineCleanupEnabled,
|
|
@@ -8599,11 +9972,13 @@ export {
|
|
|
8599
9972
|
isTerminalHeartbeatPhase,
|
|
8600
9973
|
isVercelStatusContext,
|
|
8601
9974
|
isWslHost,
|
|
9975
|
+
joinHarnessNotice,
|
|
8602
9976
|
landingContractAttentionReason,
|
|
8603
9977
|
listForbiddenWorkerEnvKeys,
|
|
8604
9978
|
listMonitors,
|
|
8605
9979
|
listOutboxItems,
|
|
8606
9980
|
listRuns,
|
|
9981
|
+
listRunsCli,
|
|
8607
9982
|
loadUserConfig,
|
|
8608
9983
|
main,
|
|
8609
9984
|
mergeNodeOptionsForBuildCheck,
|
|
@@ -8618,6 +9993,7 @@ export {
|
|
|
8618
9993
|
persistPlan,
|
|
8619
9994
|
pickVercelStatusContext,
|
|
8620
9995
|
postJson,
|
|
9996
|
+
preferCursorExecutor,
|
|
8621
9997
|
preflightCursorModel,
|
|
8622
9998
|
readMemAvailableBytes,
|
|
8623
9999
|
readProductionDbKeysFromEnvFile,
|
|
@@ -8626,8 +10002,10 @@ export {
|
|
|
8626
10002
|
redactHarness,
|
|
8627
10003
|
remediateDefaultRepo,
|
|
8628
10004
|
resolveBaseUrl,
|
|
10005
|
+
resolveBoxKindFromEnv,
|
|
8629
10006
|
resolveCallbackSecret,
|
|
8630
10007
|
resolveCallbackSecretWithMint,
|
|
10008
|
+
resolveConfiguredWorkerProvider,
|
|
8631
10009
|
resolveDefaultRepo,
|
|
8632
10010
|
resolveHarnessRoot,
|
|
8633
10011
|
resolveProductionDatabaseUrl,
|
|
@@ -8654,6 +10032,7 @@ export {
|
|
|
8654
10032
|
summarizeWslRecoverySteps,
|
|
8655
10033
|
sweepRun,
|
|
8656
10034
|
tailWorker,
|
|
10035
|
+
taskAllowsClaudeWorker,
|
|
8657
10036
|
terminalFinalResultFromHeartbeat,
|
|
8658
10037
|
usage,
|
|
8659
10038
|
validateOwnedPaths,
|