@kynver-app/runtime 0.1.19 → 0.1.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +572 -91
- package/dist/cli.js.map +4 -4
- package/dist/index.js +574 -91
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -182,7 +182,7 @@ function resolveConfiguredBaseUrl(argsBaseUrl) {
|
|
|
182
182
|
return baseUrl ? trimTrailingSlash(String(baseUrl)) : void 0;
|
|
183
183
|
}
|
|
184
184
|
function resolveConfiguredCallbackSecret(argsSecret, agentOsId) {
|
|
185
|
-
const scoped = argsSecret || loadRunnerToken(agentOsId) || loadRunnerToken(loadUserConfig().agentOsId);
|
|
185
|
+
const scoped = argsSecret || loadRunnerToken(agentOsId) || (agentOsId ? void 0 : loadRunnerToken(loadUserConfig().agentOsId));
|
|
186
186
|
if (scoped) return String(scoped);
|
|
187
187
|
const globalSecret = process.env.KYNVER_RUNTIME_SECRET || process.env.OPENCLAW_CRON_SECRET;
|
|
188
188
|
if (globalSecret) {
|
|
@@ -223,6 +223,23 @@ async function refreshRunnerToken(agentOsId, opts) {
|
|
|
223
223
|
return null;
|
|
224
224
|
}
|
|
225
225
|
}
|
|
226
|
+
async function refreshRunnerTokenForAuthFailure(rejectedSecret, agentOsId, opts) {
|
|
227
|
+
const apiKey = loadApiKey();
|
|
228
|
+
const baseUrl = resolveConfiguredBaseUrl(opts?.baseUrl);
|
|
229
|
+
if (!apiKey) return { ok: false, reason: "KYNVER_API_KEY is required to refresh a rejected runner token" };
|
|
230
|
+
if (!agentOsId) return { ok: false, reason: "agentOsId is required to refresh a rejected runner token" };
|
|
231
|
+
if (!baseUrl) return { ok: false, reason: "KYNVER_API_URL or --base-url is required to refresh a rejected runner token" };
|
|
232
|
+
try {
|
|
233
|
+
const token = await fetchRunnerCredential(agentOsId, { baseUrl, apiKey });
|
|
234
|
+
if (token && token !== rejectedSecret) {
|
|
235
|
+
saveRunnerToken(agentOsId, token);
|
|
236
|
+
return { ok: true, token };
|
|
237
|
+
}
|
|
238
|
+
return { ok: false, reason: "runner credential refresh returned the rejected token" };
|
|
239
|
+
} catch (error) {
|
|
240
|
+
return { ok: false, reason: error.message };
|
|
241
|
+
}
|
|
242
|
+
}
|
|
226
243
|
async function fetchRunnerCredential(agentOsId, opts) {
|
|
227
244
|
const apiKey = opts?.apiKey || loadApiKey();
|
|
228
245
|
if (!apiKey) throw new Error("API key required \u2014 run `kynver login` first");
|
|
@@ -375,6 +392,14 @@ async function postJson(url, secret, body) {
|
|
|
375
392
|
}
|
|
376
393
|
return { ok: res.ok, status: res.status, response };
|
|
377
394
|
}
|
|
395
|
+
async function postJsonWithCredentialRefresh(url, secret, body, opts) {
|
|
396
|
+
const first = await postJson(url, secret, body);
|
|
397
|
+
if (first.ok || first.status !== 401) return first;
|
|
398
|
+
const refreshed = await refreshRunnerTokenForAuthFailure(secret, opts.agentOsId, { baseUrl: opts.baseUrl });
|
|
399
|
+
if (!refreshed.ok) return { ...first, authRefreshFailure: refreshed.reason };
|
|
400
|
+
const retry = await postJson(url, refreshed.token, body);
|
|
401
|
+
return { ...retry, refreshedAuth: true };
|
|
402
|
+
}
|
|
378
403
|
async function getJson(url, secret) {
|
|
379
404
|
const res = await fetch(url, {
|
|
380
405
|
method: "GET",
|
|
@@ -396,12 +421,12 @@ var DEFAULT_CRITICAL_FREE_BYTES = 15 * 1024 * 1024 * 1024;
|
|
|
396
421
|
var DEFAULT_MAX_USED_PERCENT = 80;
|
|
397
422
|
var DEFAULT_HARD_MAX_USED_PERCENT = 90;
|
|
398
423
|
function observeRunnerDiskGate(input = {}) {
|
|
399
|
-
const
|
|
424
|
+
const path18 = input.diskPath?.trim() || "/";
|
|
400
425
|
const warnBelowBytes = input.diskFreeWarnBytes ?? DEFAULT_WARN_FREE_BYTES;
|
|
401
426
|
const criticalBelowBytes = input.diskFreeCriticalBytes ?? DEFAULT_CRITICAL_FREE_BYTES;
|
|
402
427
|
const maxUsedPercent = input.diskMaxUsedPercent ?? DEFAULT_MAX_USED_PERCENT;
|
|
403
428
|
const hardMaxUsedPercent = input.diskHardMaxUsedPercent ?? DEFAULT_HARD_MAX_USED_PERCENT;
|
|
404
|
-
const stats = statfsSync(
|
|
429
|
+
const stats = statfsSync(path18);
|
|
405
430
|
const freeBytes = Number(stats.bavail) * Number(stats.bsize);
|
|
406
431
|
const totalBytes = Number(stats.blocks) * Number(stats.bsize);
|
|
407
432
|
const usedPercent = totalBytes > 0 ? (totalBytes - freeBytes) / totalBytes * 100 : 100;
|
|
@@ -421,7 +446,7 @@ function observeRunnerDiskGate(input = {}) {
|
|
|
421
446
|
}
|
|
422
447
|
return {
|
|
423
448
|
ok,
|
|
424
|
-
path:
|
|
449
|
+
path: path18,
|
|
425
450
|
freeBytes,
|
|
426
451
|
totalBytes,
|
|
427
452
|
usedPercent,
|
|
@@ -695,22 +720,36 @@ function gitIsAncestor(cwd, ancestor, descendant) {
|
|
|
695
720
|
if (res.status === 1) return { isAncestor: false, error: null };
|
|
696
721
|
return { isAncestor: null, error: res.error || res.stderr || res.stdout || `git exited ${res.status}` };
|
|
697
722
|
}
|
|
698
|
-
function computeGitAncestry(worktreePath,
|
|
723
|
+
function computeGitAncestry(worktreePath, baseOrOptions = "origin/main") {
|
|
724
|
+
const options = typeof baseOrOptions === "string" ? { base: baseOrOptions } : baseOrOptions;
|
|
725
|
+
const baseLabel = options.baseCommit?.trim() || options.base?.trim() || "origin/main";
|
|
726
|
+
const pinnedBaseCommit = options.baseCommit?.trim() || null;
|
|
699
727
|
if (!worktreePath) {
|
|
700
|
-
return unknownAncestry(
|
|
728
|
+
return unknownAncestry(baseLabel, "missing worktree path");
|
|
701
729
|
}
|
|
702
730
|
const head = gitCapture(worktreePath, ["rev-parse", "HEAD"]);
|
|
703
|
-
if (head.status !== 0)
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
731
|
+
if (head.status !== 0) {
|
|
732
|
+
return unknownAncestry(baseLabel, head.error || head.stderr || head.stdout || "failed to resolve HEAD");
|
|
733
|
+
}
|
|
734
|
+
let baseSha;
|
|
735
|
+
if (pinnedBaseCommit) {
|
|
736
|
+
baseSha = pinnedBaseCommit;
|
|
737
|
+
} else {
|
|
738
|
+
const baseHead = gitCapture(worktreePath, ["rev-parse", baseLabel]);
|
|
739
|
+
if (baseHead.status !== 0) {
|
|
740
|
+
return unknownAncestry(
|
|
741
|
+
baseLabel,
|
|
742
|
+
baseHead.error || baseHead.stderr || baseHead.stdout || `failed to resolve ${baseLabel}`,
|
|
743
|
+
head.stdout.trim()
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
baseSha = baseHead.stdout.trim();
|
|
707
747
|
}
|
|
708
748
|
const headSha = head.stdout.trim();
|
|
709
|
-
const baseSha = baseHead.stdout.trim();
|
|
710
749
|
if (headSha === baseSha) {
|
|
711
750
|
return {
|
|
712
751
|
checked: true,
|
|
713
|
-
base,
|
|
752
|
+
base: baseLabel,
|
|
714
753
|
head: headSha,
|
|
715
754
|
baseHead: baseSha,
|
|
716
755
|
baseIsAncestorOfHead: true,
|
|
@@ -724,7 +763,7 @@ function computeGitAncestry(worktreePath, base = "origin/main") {
|
|
|
724
763
|
if (baseIsAncestorOfHead.isAncestor == null || headIsAncestorOfBase.isAncestor == null) {
|
|
725
764
|
return {
|
|
726
765
|
checked: false,
|
|
727
|
-
base,
|
|
766
|
+
base: baseLabel,
|
|
728
767
|
head: headSha,
|
|
729
768
|
baseHead: baseSha,
|
|
730
769
|
baseIsAncestorOfHead: baseIsAncestorOfHead.isAncestor,
|
|
@@ -736,7 +775,7 @@ function computeGitAncestry(worktreePath, base = "origin/main") {
|
|
|
736
775
|
const relation = baseIsAncestorOfHead.isAncestor ? "ahead" : headIsAncestorOfBase.isAncestor ? "merged" : "diverged";
|
|
737
776
|
return {
|
|
738
777
|
checked: true,
|
|
739
|
-
base,
|
|
778
|
+
base: baseLabel,
|
|
740
779
|
head: headSha,
|
|
741
780
|
baseHead: baseSha,
|
|
742
781
|
baseIsAncestorOfHead: baseIsAncestorOfHead.isAncestor,
|
|
@@ -763,12 +802,74 @@ function scrubClaudeEnv(env) {
|
|
|
763
802
|
return next;
|
|
764
803
|
}
|
|
765
804
|
|
|
805
|
+
// src/landing-gate.ts
|
|
806
|
+
function trimOrNull(value) {
|
|
807
|
+
if (typeof value !== "string") return null;
|
|
808
|
+
const trimmed = value.trim();
|
|
809
|
+
return trimmed.length ? trimmed : null;
|
|
810
|
+
}
|
|
811
|
+
function hasFinalResult(value) {
|
|
812
|
+
if (value === void 0 || value === null) return false;
|
|
813
|
+
if (typeof value === "string") return value.trim().length > 0;
|
|
814
|
+
if (typeof value === "boolean") return value;
|
|
815
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
816
|
+
if (typeof value === "object") return Object.keys(value).length > 0;
|
|
817
|
+
return true;
|
|
818
|
+
}
|
|
819
|
+
function hasCommittedLandingRef(snapshot) {
|
|
820
|
+
if (trimOrNull(snapshot.headCommit)) return true;
|
|
821
|
+
if (trimOrNull(snapshot.prUrl)) return true;
|
|
822
|
+
if (trimOrNull(snapshot.artifactBundlePath)) return true;
|
|
823
|
+
if (trimOrNull(snapshot.patchPath)) return true;
|
|
824
|
+
const ancestry = snapshot.gitAncestry;
|
|
825
|
+
if (ancestry?.checked && ancestry.headIsAncestorOfBase === false && trimOrNull(ancestry.head)) {
|
|
826
|
+
return true;
|
|
827
|
+
}
|
|
828
|
+
return false;
|
|
829
|
+
}
|
|
830
|
+
function assessWorkerLanding(snapshot) {
|
|
831
|
+
if (!hasFinalResult(snapshot.finalResult)) return { blocked: false };
|
|
832
|
+
if (snapshot.changedFiles.length === 0) return { blocked: false };
|
|
833
|
+
if (!hasCommittedLandingRef(snapshot)) {
|
|
834
|
+
return {
|
|
835
|
+
blocked: true,
|
|
836
|
+
reason: "dirty_worktree_no_pr",
|
|
837
|
+
detail: `Worktree has ${snapshot.changedFiles.length} uncommitted change(s) with no commit or PR; commit, open a PR, or discard before landing`
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
return {
|
|
841
|
+
blocked: true,
|
|
842
|
+
detail: `Worktree has ${snapshot.changedFiles.length} uncommitted change(s); commit or discard before landing`
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
function landingAttentionReason(verdict) {
|
|
846
|
+
if (!verdict.blocked) return void 0;
|
|
847
|
+
return verdict.detail ?? verdict.reason ?? "dirty_worktree_no_pr";
|
|
848
|
+
}
|
|
849
|
+
|
|
766
850
|
// src/status.ts
|
|
767
851
|
var NO_START_MS = 18e4;
|
|
768
852
|
var STALE_MS = 6e5;
|
|
769
853
|
function computeAttention(input) {
|
|
770
854
|
const now = Date.now();
|
|
771
|
-
if (input.
|
|
855
|
+
if (input.completionBlocker) {
|
|
856
|
+
return { state: "blocked", reason: input.completionBlocker };
|
|
857
|
+
}
|
|
858
|
+
if (input.finalResult) {
|
|
859
|
+
const landing = assessWorkerLanding({
|
|
860
|
+
finalResult: input.finalResult,
|
|
861
|
+
changedFiles: input.changedFiles ?? [],
|
|
862
|
+
gitAncestry: input.gitAncestry ?? null
|
|
863
|
+
});
|
|
864
|
+
if (landing.blocked) {
|
|
865
|
+
const detail = landingAttentionReason(landing);
|
|
866
|
+
return {
|
|
867
|
+
state: "needs_attention",
|
|
868
|
+
reason: landing.reason ? `landing blocked (${landing.reason}): ${detail}` : `landing blocked: ${detail}`
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
return { state: "done", reason: "final result recorded" };
|
|
872
|
+
}
|
|
772
873
|
if (!input.alive) {
|
|
773
874
|
const classified = classifyExitFailure(input.error);
|
|
774
875
|
if (classified) return { state: "blocked", reason: classified.reason };
|
|
@@ -799,7 +900,10 @@ function computeWorkerStatus(worker, options = {}) {
|
|
|
799
900
|
const stderrBytes = fileSize(worker.stderrPath);
|
|
800
901
|
const heartbeatBytes = fileSize(worker.heartbeatPath);
|
|
801
902
|
const changedFiles = gitStatusShort(worker.worktreePath);
|
|
802
|
-
const gitAncestry = computeGitAncestry(worker.worktreePath,
|
|
903
|
+
const gitAncestry = computeGitAncestry(worker.worktreePath, {
|
|
904
|
+
base: options.base,
|
|
905
|
+
baseCommit: options.baseCommit
|
|
906
|
+
});
|
|
803
907
|
const lastActivityAt = latestIso([
|
|
804
908
|
parsed.lastEventAt,
|
|
805
909
|
heartbeat.lastHeartbeatAt,
|
|
@@ -808,6 +912,7 @@ function computeWorkerStatus(worker, options = {}) {
|
|
|
808
912
|
fileMtime(worker.heartbeatPath)
|
|
809
913
|
]);
|
|
810
914
|
const error = parsed.error || (!alive && !parsed.finalResult ? tailFile(worker.stderrPath, 10).trim() || void 0 : void 0);
|
|
915
|
+
const completionBlocker = typeof worker.completionBlocker === "string" && worker.completionBlocker.trim() ? worker.completionBlocker.trim() : null;
|
|
811
916
|
const attention = computeAttention({
|
|
812
917
|
alive,
|
|
813
918
|
finalResult: parsed.finalResult,
|
|
@@ -817,14 +922,18 @@ function computeWorkerStatus(worker, options = {}) {
|
|
|
817
922
|
lastActivityAt,
|
|
818
923
|
heartbeatBlocker: heartbeat.heartbeatBlocker,
|
|
819
924
|
startedAt: worker.startedAt,
|
|
820
|
-
error
|
|
925
|
+
error,
|
|
926
|
+
changedFiles,
|
|
927
|
+
gitAncestry,
|
|
928
|
+
completionBlocker
|
|
821
929
|
});
|
|
930
|
+
const workerStatusLabel = completionBlocker || attention.state === "blocked" ? "blocked" : attention.state === "done" ? "done" : parsed.finalResult ? "exited" : alive ? "running" : "exited";
|
|
822
931
|
return {
|
|
823
932
|
runId: worker.runId,
|
|
824
933
|
worker: worker.name,
|
|
825
934
|
pid: worker.pid,
|
|
826
935
|
alive,
|
|
827
|
-
status:
|
|
936
|
+
status: workerStatusLabel,
|
|
828
937
|
attention,
|
|
829
938
|
branch: worker.branch,
|
|
830
939
|
worktreePath: worker.worktreePath,
|
|
@@ -853,6 +962,10 @@ function isFinishedWorkerStatus(status) {
|
|
|
853
962
|
if (status.status === "exited" || status.status === "done") return true;
|
|
854
963
|
return false;
|
|
855
964
|
}
|
|
965
|
+
function isLandingBlockedWorkerStatus(status) {
|
|
966
|
+
if (!status.finalResult) return false;
|
|
967
|
+
return status.attention.state === "needs_attention" || status.attention.state === "blocked";
|
|
968
|
+
}
|
|
856
969
|
function deriveRunStatus(fallback, workers) {
|
|
857
970
|
if (workers.length === 0) return fallback;
|
|
858
971
|
if (workers.some((w) => w.attention === "needs_attention" || w.attention === "stale" || w.attention === "blocked")) {
|
|
@@ -903,6 +1016,10 @@ function readAvailableMemBytes() {
|
|
|
903
1016
|
}
|
|
904
1017
|
return os.freemem();
|
|
905
1018
|
}
|
|
1019
|
+
function isActiveHarnessWorker(worker) {
|
|
1020
|
+
const status = computeWorkerStatus(worker);
|
|
1021
|
+
return status.alive && !status.finalResult && status.attention.state !== "done";
|
|
1022
|
+
}
|
|
906
1023
|
function countActiveWorkersForRun(run) {
|
|
907
1024
|
let active = 0;
|
|
908
1025
|
for (const name of Object.keys(run.workers || {})) {
|
|
@@ -910,11 +1027,8 @@ function countActiveWorkersForRun(run) {
|
|
|
910
1027
|
path5.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
911
1028
|
void 0
|
|
912
1029
|
);
|
|
913
|
-
if (!worker) continue;
|
|
914
|
-
|
|
915
|
-
if (status.alive && !status.finalResult && status.attention.state !== "done") {
|
|
916
|
-
active++;
|
|
917
|
-
}
|
|
1030
|
+
if (!worker || !isActiveHarnessWorker(worker)) continue;
|
|
1031
|
+
active++;
|
|
918
1032
|
}
|
|
919
1033
|
return active;
|
|
920
1034
|
}
|
|
@@ -939,7 +1053,7 @@ function observeRunnerResourceGate(input) {
|
|
|
939
1053
|
const maxConcurrentWorkers = Math.max(0, Math.min(targetCap, capacityFromTotal));
|
|
940
1054
|
const slotsByCapacity = Math.max(0, maxConcurrentWorkers - activeWorkers);
|
|
941
1055
|
const slotsByFreeMem = capacityFromFree;
|
|
942
|
-
|
|
1056
|
+
let slotsAvailable = Math.min(slotsByCapacity, slotsByFreeMem);
|
|
943
1057
|
let reason = null;
|
|
944
1058
|
if (slotsAvailable <= 0) {
|
|
945
1059
|
if (activeWorkers >= maxConcurrentWorkers) {
|
|
@@ -966,37 +1080,57 @@ function observeRunnerResourceGate(input) {
|
|
|
966
1080
|
};
|
|
967
1081
|
}
|
|
968
1082
|
|
|
969
|
-
// src/
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
function
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1083
|
+
// src/model-routing-task-enrich.ts
|
|
1084
|
+
function taskString(task, key) {
|
|
1085
|
+
const v = task[key];
|
|
1086
|
+
return typeof v === "string" ? v.trim() : "";
|
|
1087
|
+
}
|
|
1088
|
+
function normalize(value) {
|
|
1089
|
+
return value.toLowerCase();
|
|
1090
|
+
}
|
|
1091
|
+
var PERSONA_DEFAULT_LANE = {
|
|
1092
|
+
dalton: "implementer",
|
|
1093
|
+
lorentz: "report_reviewer"
|
|
1094
|
+
};
|
|
1095
|
+
function inferRoleLaneFromTask(task) {
|
|
1096
|
+
const existing = taskString(task, "roleLane");
|
|
1097
|
+
if (existing) return existing;
|
|
1098
|
+
const ref = normalize(taskString(task, "executorRef"));
|
|
1099
|
+
const title = normalize(taskString(task, "title"));
|
|
1100
|
+
const persona = normalize(taskString(task, "personaSlug"));
|
|
1101
|
+
const combined = `${ref} ${title}`;
|
|
1102
|
+
if (combined.includes("deep review") || combined.includes("security review") || ref.includes("deep-reviewer")) {
|
|
1103
|
+
return "deep_reviewer";
|
|
1104
|
+
}
|
|
1105
|
+
if (combined.includes("plan author") || combined.includes("plan-author") || title.includes("strategy plan")) {
|
|
1106
|
+
return "plan_author";
|
|
1107
|
+
}
|
|
1108
|
+
if (combined.includes("plan review") || ref.includes("plan-reviewer")) {
|
|
1109
|
+
return "plan_reviewer";
|
|
1110
|
+
}
|
|
1111
|
+
if (combined.includes("report review") || combined.includes("completion report")) {
|
|
1112
|
+
return "report_reviewer";
|
|
1113
|
+
}
|
|
1114
|
+
if (combined.includes("repair") || title.startsWith("fix ") || ref.includes("repair")) {
|
|
1115
|
+
return "repair_implementer";
|
|
1116
|
+
}
|
|
1117
|
+
if (ref.includes("cursor") || ref.includes("codex") || ref.includes("composer") || title.includes("implement") || title.includes("land:")) {
|
|
1118
|
+
return "implementer";
|
|
1119
|
+
}
|
|
1120
|
+
if (persona && PERSONA_DEFAULT_LANE[persona]) {
|
|
1121
|
+
const base = PERSONA_DEFAULT_LANE[persona];
|
|
1122
|
+
if (persona === "lorentz" && (combined.includes("deep") || combined.includes("security"))) {
|
|
1123
|
+
return "deep_reviewer";
|
|
1124
|
+
}
|
|
1125
|
+
return base;
|
|
1126
|
+
}
|
|
1127
|
+
if (combined.includes("review")) return "report_reviewer";
|
|
1128
|
+
return void 0;
|
|
1129
|
+
}
|
|
1130
|
+
function enrichTaskForModelRouting(task) {
|
|
1131
|
+
const roleLane = inferRoleLaneFromTask(task);
|
|
1132
|
+
if (!roleLane) return task;
|
|
1133
|
+
return { ...task, roleLane };
|
|
1000
1134
|
}
|
|
1001
1135
|
|
|
1002
1136
|
// src/providers/claude.ts
|
|
@@ -1057,7 +1191,7 @@ function preflightCursorModel(model, defaultModel) {
|
|
|
1057
1191
|
}
|
|
1058
1192
|
|
|
1059
1193
|
// src/providers/claude.ts
|
|
1060
|
-
var CLAUDE_DEFAULT_MODEL = "claude-
|
|
1194
|
+
var CLAUDE_DEFAULT_MODEL = "claude-sonnet-4-6";
|
|
1061
1195
|
var claudeProvider = {
|
|
1062
1196
|
name: "claude",
|
|
1063
1197
|
defaultModel: CLAUDE_DEFAULT_MODEL,
|
|
@@ -1103,6 +1237,172 @@ var claudeProvider = {
|
|
|
1103
1237
|
}
|
|
1104
1238
|
};
|
|
1105
1239
|
|
|
1240
|
+
// src/model-routing.ts
|
|
1241
|
+
var GLOBAL_DEFAULT_MODEL = "claude-sonnet-4-6";
|
|
1242
|
+
function taskString2(task, key) {
|
|
1243
|
+
const v = task[key];
|
|
1244
|
+
return typeof v === "string" ? v.trim() : "";
|
|
1245
|
+
}
|
|
1246
|
+
function normalizeRef(ref) {
|
|
1247
|
+
return ref.toLowerCase();
|
|
1248
|
+
}
|
|
1249
|
+
function resolveGlobalDefaultModel(config = loadUserConfig()) {
|
|
1250
|
+
const fromConfig = config.defaultModel?.trim();
|
|
1251
|
+
if (fromConfig) return fromConfig;
|
|
1252
|
+
const fromEnv = process.env.KYNVER_DEFAULT_MODEL?.trim();
|
|
1253
|
+
if (fromEnv) return fromEnv;
|
|
1254
|
+
return GLOBAL_DEFAULT_MODEL;
|
|
1255
|
+
}
|
|
1256
|
+
function inferProviderFromModel(model) {
|
|
1257
|
+
const m = (model ?? "").toLowerCase();
|
|
1258
|
+
if (!m) return "claude";
|
|
1259
|
+
if (m.includes("composer") || m.includes("cursor") || m.includes("codex") || m.startsWith("gpt-") || m.startsWith("gpt5")) {
|
|
1260
|
+
return "cursor";
|
|
1261
|
+
}
|
|
1262
|
+
return "claude";
|
|
1263
|
+
}
|
|
1264
|
+
function isOpusLane(ref, title) {
|
|
1265
|
+
if (ref.includes("deep") && ref.includes("review")) return true;
|
|
1266
|
+
if (ref.includes("security")) return true;
|
|
1267
|
+
if (ref.includes("plan_author") || ref.includes("plan-author")) return true;
|
|
1268
|
+
if (title.includes("deep review") || title.includes("security review")) return true;
|
|
1269
|
+
if (ref.includes("plan") && !ref.includes("review") && (ref.includes("author") || ref.includes("strategy"))) {
|
|
1270
|
+
return true;
|
|
1271
|
+
}
|
|
1272
|
+
return false;
|
|
1273
|
+
}
|
|
1274
|
+
function inferModelRoutingFromTask(task) {
|
|
1275
|
+
const ref = normalizeRef(taskString2(task, "executorRef"));
|
|
1276
|
+
const title = taskString2(task, "title").toLowerCase();
|
|
1277
|
+
const priority = taskString2(task, "priority") || "normal";
|
|
1278
|
+
const roleLane = normalizeRef(taskString2(task, "roleLane"));
|
|
1279
|
+
if (ref.includes("cursor") || ref.includes("codex") || ref.includes("composer") || ref.includes("copilot") || roleLane === "implementer" || roleLane === "repair_implementer") {
|
|
1280
|
+
return { provider: "cursor", rule: "lane:implementation" };
|
|
1281
|
+
}
|
|
1282
|
+
if (ref.includes("landing") || title.startsWith("land:") || title.includes(" merge")) {
|
|
1283
|
+
return {
|
|
1284
|
+
model: "claude-haiku-4-5-20251001",
|
|
1285
|
+
provider: "claude",
|
|
1286
|
+
rule: "lane:landing"
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
if (ref.includes("review") || title.startsWith("review ") || roleLane.includes("review")) {
|
|
1290
|
+
if (isOpusLane(ref, title) || roleLane === "deep_reviewer") {
|
|
1291
|
+
return { model: "claude-opus-4-7", provider: "claude", rule: "lane:deep_review" };
|
|
1292
|
+
}
|
|
1293
|
+
return { model: "claude-sonnet-4-6", provider: "claude", rule: "lane:review" };
|
|
1294
|
+
}
|
|
1295
|
+
if (isOpusLane(ref, title) || roleLane === "plan_author") {
|
|
1296
|
+
return { model: "claude-opus-4-7", provider: "claude", rule: "lane:planning" };
|
|
1297
|
+
}
|
|
1298
|
+
if (priority === "critical") {
|
|
1299
|
+
return { model: "claude-opus-4-7", provider: "claude", rule: "priority:critical" };
|
|
1300
|
+
}
|
|
1301
|
+
if (priority === "high") {
|
|
1302
|
+
return { model: "claude-sonnet-4-6", provider: "claude", rule: "priority:high" };
|
|
1303
|
+
}
|
|
1304
|
+
if (priority === "low") {
|
|
1305
|
+
return {
|
|
1306
|
+
model: "claude-haiku-4-5-20251001",
|
|
1307
|
+
provider: "claude",
|
|
1308
|
+
rule: "priority:low"
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
return {
|
|
1312
|
+
model: resolveGlobalDefaultModel(),
|
|
1313
|
+
provider: "claude",
|
|
1314
|
+
rule: "default:sonnet"
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
function resolveWorkerLaunch(input) {
|
|
1318
|
+
if (input.explicitModel?.trim()) {
|
|
1319
|
+
const model2 = input.explicitModel.trim();
|
|
1320
|
+
return {
|
|
1321
|
+
model: model2,
|
|
1322
|
+
provider: input.explicitProvider?.trim() || inferProviderFromModel(model2),
|
|
1323
|
+
rule: "explicit:cli",
|
|
1324
|
+
requestedModel: model2
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
if (input.task && Object.keys(input.task).length > 0) {
|
|
1328
|
+
const inferred = inferModelRoutingFromTask(input.task);
|
|
1329
|
+
return {
|
|
1330
|
+
...inferred,
|
|
1331
|
+
requestedModel: inferred.model
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
const model = resolveGlobalDefaultModel();
|
|
1335
|
+
return {
|
|
1336
|
+
model,
|
|
1337
|
+
provider: input.explicitProvider?.trim() || inferProviderFromModel(model),
|
|
1338
|
+
rule: "default:global",
|
|
1339
|
+
requestedModel: model
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
function resolveModelFallback(startedModel, launchModel, providerDefault) {
|
|
1343
|
+
return startedModel || launchModel || providerDefault || resolveGlobalDefaultModel() || CLAUDE_DEFAULT_MODEL;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
// src/retry-limits.ts
|
|
1347
|
+
function positiveInt2(value, fallback) {
|
|
1348
|
+
const n = Number(value);
|
|
1349
|
+
if (!Number.isFinite(n) || n <= 0) return fallback;
|
|
1350
|
+
return Math.floor(n);
|
|
1351
|
+
}
|
|
1352
|
+
function readHarnessRetryLimits() {
|
|
1353
|
+
return {
|
|
1354
|
+
maxTaskAttempts: positiveInt2(process.env.KYNVER_MAX_TASK_ATTEMPTS, 3),
|
|
1355
|
+
dispatchCooldownMs: positiveInt2(process.env.KYNVER_DISPATCH_COOLDOWN_MS, 5e3)
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
// src/supervisor.ts
|
|
1360
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync3 } from "node:fs";
|
|
1361
|
+
import path10 from "node:path";
|
|
1362
|
+
|
|
1363
|
+
// src/prompt.ts
|
|
1364
|
+
function buildPrompt(input) {
|
|
1365
|
+
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.";
|
|
1366
|
+
const compact = Boolean(input.model?.toLowerCase().includes("haiku"));
|
|
1367
|
+
const progressLines = compact ? [
|
|
1368
|
+
"Plan progress: when planId is set, use `kynver plan progress` for running|partial|blocked only; row `done` is MCP/session only.",
|
|
1369
|
+
input.planId ? `Active planId: ${input.planId}` : "No planId on this worker."
|
|
1370
|
+
] : [
|
|
1371
|
+
"Structured plan progress (required when planId is set):",
|
|
1372
|
+
"- Harness checkpoints only: `kynver plan progress --plan <planId> --row <rowKey> --role implementer --status running|partial|blocked` (the by-id harness route rejects `done` and confirm events).",
|
|
1373
|
+
"- When a slice is finished, emit `partial` with evidence (`--evidence pr:<url>`, `--evidence path:<file>`, or `--evidence command:<cmd>`). Do not propose or confirm row `done` from the worker CLI.",
|
|
1374
|
+
"- Propose/confirm row `done` is MCP/session only: chat agents use `agent_os_plan_progress_event_append` on the slug route (implementer proposes with `proposed: true`; report_reviewer/deep_reviewer confirm with `proposed: false`).",
|
|
1375
|
+
"- When blocked on operator/Ghost/runtime review, create a linked review task (MCP `agent_os_plan_review_task_create` or API) and pass `--review-task <taskId>`.",
|
|
1376
|
+
"- Before the completion report: mark completion-report rows partial with evidence; do not skip report review.",
|
|
1377
|
+
"- After implementation: wait for report_reviewer then deep_reviewer confirmation (via MCP/session agents) before follow-up rows close.",
|
|
1378
|
+
input.planId ? `Active planId: ${input.planId}${input.taskId ? ` \xB7 taskId: ${input.taskId}` : ""}` : "No planId on this worker \u2014 still emit progress when you touch plan-scoped work."
|
|
1379
|
+
];
|
|
1380
|
+
const planArtifactLines = compact ? [
|
|
1381
|
+
"Plan artifacts: when authoring/revising docs/superpowers/plans/, open a GitHub PR early and iterate from that PR branch; do not leave the canonical plan only in the harness worktree."
|
|
1382
|
+
] : [
|
|
1383
|
+
"PR-first plan artifacts (when authoring or revising docs/superpowers/plans/):",
|
|
1384
|
+
"- Before substantial plan drafting: create a feature branch, open a GitHub PR (draft OK), commit and push the plan file \u2014 do not leave the canonical plan only in this harness worktree.",
|
|
1385
|
+
"- Iterate review on that PR branch; link prUrl on the AgentOS task and plan progress evidence (`--evidence pr:<url>`).",
|
|
1386
|
+
"- See docs/superpowers/plans/2026-05-25-pr-first-plan-artifact-preservation.md for the full checklist."
|
|
1387
|
+
];
|
|
1388
|
+
return [
|
|
1389
|
+
"You are running under the Kynver AgentOS runtime.",
|
|
1390
|
+
"Immediately state your plan before editing.",
|
|
1391
|
+
ownership,
|
|
1392
|
+
`Worktree: ${input.worktreePath}`,
|
|
1393
|
+
`Progress heartbeat file: ${input.heartbeatPath}`,
|
|
1394
|
+
"After each major step, append one JSON line to the heartbeat file with fields: ts, phase, summary, changedFiles, blocker.",
|
|
1395
|
+
"Final response must include files changed, verification commands, and unresolved risks.",
|
|
1396
|
+
"",
|
|
1397
|
+
...progressLines,
|
|
1398
|
+
"",
|
|
1399
|
+
...planArtifactLines,
|
|
1400
|
+
"",
|
|
1401
|
+
"Task:",
|
|
1402
|
+
input.task
|
|
1403
|
+
].join("\n");
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1106
1406
|
// src/providers/cursor.ts
|
|
1107
1407
|
import { closeSync as closeSync2, existsSync as existsSync8, openSync as openSync2 } from "node:fs";
|
|
1108
1408
|
import { spawn as spawn2 } from "node:child_process";
|
|
@@ -1306,9 +1606,13 @@ function persistCompletionBlocker(worker, reason) {
|
|
|
1306
1606
|
else delete worker.completionBlocker;
|
|
1307
1607
|
saveWorker(worker.runId, worker);
|
|
1308
1608
|
}
|
|
1609
|
+
function workerStatusOptions(run) {
|
|
1610
|
+
return run ? { base: run.base, baseCommit: run.baseCommit } : {};
|
|
1611
|
+
}
|
|
1309
1612
|
async function tryCompleteWorker(args) {
|
|
1310
1613
|
const worker = loadWorker(String(args.run), String(args.name));
|
|
1311
|
-
const
|
|
1614
|
+
const run = loadRun(worker.runId);
|
|
1615
|
+
const status = computeWorkerStatus(worker, workerStatusOptions(run));
|
|
1312
1616
|
const agentOsId = (args.agentOsId ? String(args.agentOsId) : worker.agentOsId) || "";
|
|
1313
1617
|
const taskId = (args.taskId ? String(args.taskId) : worker.taskId) || null;
|
|
1314
1618
|
if (!agentOsId) {
|
|
@@ -1352,7 +1656,8 @@ async function tryCompleteWorker(args) {
|
|
|
1352
1656
|
async function completeWorker(args) {
|
|
1353
1657
|
try {
|
|
1354
1658
|
const worker = loadWorker(String(args.run), String(args.name));
|
|
1355
|
-
const
|
|
1659
|
+
const run = loadRun(worker.runId);
|
|
1660
|
+
const status = computeWorkerStatus(worker, workerStatusOptions(run));
|
|
1356
1661
|
const agentOsId = (args.agentOsId ? String(args.agentOsId) : worker.agentOsId) || "";
|
|
1357
1662
|
const taskId = (args.taskId ? String(args.taskId) : worker.taskId) || null;
|
|
1358
1663
|
if (!agentOsId) {
|
|
@@ -1399,12 +1704,13 @@ async function completeWorker(args) {
|
|
|
1399
1704
|
}
|
|
1400
1705
|
function workerStatus(args) {
|
|
1401
1706
|
const worker = loadWorker(String(args.run), String(args.name));
|
|
1402
|
-
const
|
|
1707
|
+
const run = loadRun(worker.runId);
|
|
1708
|
+
const status = computeWorkerStatus(worker, workerStatusOptions(run));
|
|
1403
1709
|
writeJson(path8.join(worker.workerDir, "last-status.json"), status);
|
|
1404
1710
|
console.log(JSON.stringify(status, null, 2));
|
|
1405
1711
|
}
|
|
1406
|
-
function
|
|
1407
|
-
const run = loadRun(
|
|
1712
|
+
function buildRunBoard(runId) {
|
|
1713
|
+
const run = loadRun(runId);
|
|
1408
1714
|
const names = Object.keys(run.workers || {});
|
|
1409
1715
|
const workers = names.map((name) => {
|
|
1410
1716
|
const worker = readJson(
|
|
@@ -1414,14 +1720,21 @@ function runStatus(args) {
|
|
|
1414
1720
|
if (!worker) {
|
|
1415
1721
|
return { worker: name, status: "missing", attention: "needs_attention", attentionReason: "worker.json not found" };
|
|
1416
1722
|
}
|
|
1417
|
-
const status = computeWorkerStatus(worker, {
|
|
1723
|
+
const status = computeWorkerStatus(worker, {
|
|
1724
|
+
base: run.base,
|
|
1725
|
+
baseCommit: run.baseCommit
|
|
1726
|
+
});
|
|
1727
|
+
const headCommit = status.gitAncestry.headIsAncestorOfBase === false && status.gitAncestry.head ? status.gitAncestry.head : void 0;
|
|
1418
1728
|
const rawBlocker = worker.completionBlocker;
|
|
1419
1729
|
const completionBlocker = typeof rawBlocker === "string" && rawBlocker ? rawBlocker : void 0;
|
|
1730
|
+
const boardStatus = completionBlocker ? "blocked" : status.status;
|
|
1731
|
+
const boardAttention = completionBlocker ? "blocked" : status.attention.state;
|
|
1420
1732
|
return {
|
|
1421
1733
|
worker: status.worker,
|
|
1422
|
-
status:
|
|
1423
|
-
attention:
|
|
1734
|
+
status: boardStatus,
|
|
1735
|
+
attention: boardAttention,
|
|
1424
1736
|
attentionReason: completionBlocker ?? status.attention.reason,
|
|
1737
|
+
landingBlocked: status.finalResult ? boardAttention === "needs_attention" || boardAttention === "blocked" : false,
|
|
1425
1738
|
pid: status.pid,
|
|
1426
1739
|
alive: status.alive,
|
|
1427
1740
|
currentTool: status.currentTool,
|
|
@@ -1430,7 +1743,14 @@ function runStatus(args) {
|
|
|
1430
1743
|
lastHeartbeatSummary: status.lastHeartbeatSummary,
|
|
1431
1744
|
heartbeatBlocker: status.heartbeatBlocker,
|
|
1432
1745
|
changedFileCount: status.changedFiles.length,
|
|
1746
|
+
changedFiles: status.changedFiles,
|
|
1433
1747
|
branch: status.branch,
|
|
1748
|
+
model: typeof worker.model === "string" ? worker.model : void 0,
|
|
1749
|
+
routingRule: typeof worker.routingRule === "string" ? worker.routingRule : void 0,
|
|
1750
|
+
requestedModel: typeof worker.requestedModel === "string" ? worker.requestedModel : void 0,
|
|
1751
|
+
headCommit,
|
|
1752
|
+
gitAncestry: status.gitAncestry,
|
|
1753
|
+
finalResult: status.finalResult,
|
|
1434
1754
|
ancestry: status.gitAncestry.relation,
|
|
1435
1755
|
ancestryChecked: status.gitAncestry.checked
|
|
1436
1756
|
};
|
|
@@ -1445,6 +1765,34 @@ function runStatus(args) {
|
|
|
1445
1765
|
workers
|
|
1446
1766
|
};
|
|
1447
1767
|
writeJson(path8.join(runDirectory(run.id), "last-board.json"), board);
|
|
1768
|
+
return board;
|
|
1769
|
+
}
|
|
1770
|
+
async function publishHarnessBoardSnapshot(args, source) {
|
|
1771
|
+
const runId = String(args.run || "");
|
|
1772
|
+
const agentOsId = String(args.agentOsId || "");
|
|
1773
|
+
if (!runId || !agentOsId) return null;
|
|
1774
|
+
const board = buildRunBoard(runId);
|
|
1775
|
+
const base = resolveBaseUrl(args.baseUrl ? String(args.baseUrl) : void 0);
|
|
1776
|
+
const secret = await resolveCallbackSecretWithMint(args.secret ? String(args.secret) : void 0, agentOsId, {
|
|
1777
|
+
baseUrl: base
|
|
1778
|
+
});
|
|
1779
|
+
const url = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/harness/snapshot`;
|
|
1780
|
+
const res = await postJsonWithCredentialRefresh(
|
|
1781
|
+
url,
|
|
1782
|
+
secret,
|
|
1783
|
+
{ agentOsId, runId, source, snapshot: board },
|
|
1784
|
+
{ agentOsId, baseUrl: base }
|
|
1785
|
+
);
|
|
1786
|
+
return {
|
|
1787
|
+
ok: res.ok,
|
|
1788
|
+
httpStatus: res.status,
|
|
1789
|
+
response: res.response,
|
|
1790
|
+
authRefreshed: res.refreshedAuth,
|
|
1791
|
+
authRefreshFailure: res.authRefreshFailure
|
|
1792
|
+
};
|
|
1793
|
+
}
|
|
1794
|
+
function runStatus(args) {
|
|
1795
|
+
const board = buildRunBoard(String(args.run));
|
|
1448
1796
|
console.log(JSON.stringify(board, null, 2));
|
|
1449
1797
|
}
|
|
1450
1798
|
function tailWorker(args) {
|
|
@@ -1646,8 +1994,17 @@ function spawnWorkerProcess(run, opts) {
|
|
|
1646
1994
|
const name = safeSlug(rawName);
|
|
1647
1995
|
if (run.workers?.[name]) throw new Error(`worker already exists in run ${run.id}: ${name}`);
|
|
1648
1996
|
if (!opts.task) throw new Error(`missing task text for worker ${name}`);
|
|
1649
|
-
const
|
|
1650
|
-
|
|
1997
|
+
const routing = opts.routingRule || opts.requestedModel ? {
|
|
1998
|
+
provider: opts.provider || "claude",
|
|
1999
|
+
model: opts.model,
|
|
2000
|
+
rule: opts.routingRule || "explicit:spawn",
|
|
2001
|
+
requestedModel: opts.requestedModel ?? opts.model
|
|
2002
|
+
} : resolveWorkerLaunch({
|
|
2003
|
+
explicitModel: opts.model,
|
|
2004
|
+
explicitProvider: opts.provider
|
|
2005
|
+
});
|
|
2006
|
+
const provider = resolveWorkerProvider(routing.provider);
|
|
2007
|
+
let launchModel = routing.model;
|
|
1651
2008
|
if (provider.preflightModel) {
|
|
1652
2009
|
const preflight = provider.preflightModel(opts.model);
|
|
1653
2010
|
if (!preflight.ok) {
|
|
@@ -1675,7 +2032,10 @@ function spawnWorkerProcess(run, opts) {
|
|
|
1675
2032
|
task: opts.task,
|
|
1676
2033
|
ownedPaths: opts.ownedPaths || [],
|
|
1677
2034
|
worktreePath,
|
|
1678
|
-
heartbeatPath
|
|
2035
|
+
heartbeatPath,
|
|
2036
|
+
planId: opts.planId,
|
|
2037
|
+
taskId: opts.taskId,
|
|
2038
|
+
model: launchModel
|
|
1679
2039
|
});
|
|
1680
2040
|
let started;
|
|
1681
2041
|
try {
|
|
@@ -1697,7 +2057,7 @@ function spawnWorkerProcess(run, opts) {
|
|
|
1697
2057
|
git(run.repo, ["branch", "-D", branch], { allowFailure: true });
|
|
1698
2058
|
throw error;
|
|
1699
2059
|
}
|
|
1700
|
-
const model = started.model
|
|
2060
|
+
const model = resolveModelFallback(started.model, launchModel, provider.defaultModel);
|
|
1701
2061
|
const worker = {
|
|
1702
2062
|
name,
|
|
1703
2063
|
runId: run.id,
|
|
@@ -1716,6 +2076,8 @@ function spawnWorkerProcess(run, opts) {
|
|
|
1716
2076
|
...opts.planId ? { planId: String(opts.planId) } : {},
|
|
1717
2077
|
...opts.leaseOwner ? { leaseOwner: String(opts.leaseOwner) } : {},
|
|
1718
2078
|
...opts.dispatched ? { dispatched: true } : {},
|
|
2079
|
+
routingRule: routing.rule,
|
|
2080
|
+
...routing.requestedModel ? { requestedModel: routing.requestedModel } : {},
|
|
1719
2081
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1720
2082
|
};
|
|
1721
2083
|
saveWorker(run.id, worker);
|
|
@@ -1723,14 +2085,23 @@ function spawnWorkerProcess(run, opts) {
|
|
|
1723
2085
|
run.status = "running";
|
|
1724
2086
|
saveRun(run);
|
|
1725
2087
|
if (worker.agentOsId && worker.taskId) {
|
|
2088
|
+
let sidecarSpawned;
|
|
1726
2089
|
try {
|
|
1727
|
-
spawnCompletionSidecar({
|
|
2090
|
+
sidecarSpawned = spawnCompletionSidecar({
|
|
1728
2091
|
runId: run.id,
|
|
1729
2092
|
workerName: name,
|
|
1730
2093
|
workerDir,
|
|
1731
2094
|
agentOsId: worker.agentOsId
|
|
1732
2095
|
});
|
|
1733
|
-
} catch {
|
|
2096
|
+
} catch (error) {
|
|
2097
|
+
const reason = `completion sidecar failed to spawn: ${error.message}`;
|
|
2098
|
+
worker.completionBlocker = reason;
|
|
2099
|
+
saveWorker(run.id, worker);
|
|
2100
|
+
}
|
|
2101
|
+
if (!sidecarSpawned) {
|
|
2102
|
+
const reason = "completion sidecar failed to spawn (CLI not found or spawn error)";
|
|
2103
|
+
worker.completionBlocker = reason;
|
|
2104
|
+
saveWorker(run.id, worker);
|
|
1734
2105
|
}
|
|
1735
2106
|
}
|
|
1736
2107
|
return worker;
|
|
@@ -1814,9 +2185,10 @@ async function dispatchRun(args) {
|
|
|
1814
2185
|
runnerDiskGate,
|
|
1815
2186
|
runnerResourceGate,
|
|
1816
2187
|
...args.lane ? { lane: String(args.lane) } : {},
|
|
2188
|
+
executor: args.executor ? String(args.executor) : "harness",
|
|
1817
2189
|
...args.diskPath ? { diskPath: String(args.diskPath) } : {}
|
|
1818
2190
|
};
|
|
1819
|
-
const dispatch = await
|
|
2191
|
+
const dispatch = await postJsonWithCredentialRefresh(dispatchUrl, secret, body, { agentOsId, baseUrl: base });
|
|
1820
2192
|
const responseBody = dispatch.response;
|
|
1821
2193
|
if (!dispatch.ok || !responseBody?.result) {
|
|
1822
2194
|
const failure = {
|
|
@@ -1824,7 +2196,9 @@ async function dispatchRun(args) {
|
|
|
1824
2196
|
agentOsId,
|
|
1825
2197
|
action: "dispatch",
|
|
1826
2198
|
httpStatus: dispatch.status,
|
|
1827
|
-
response: dispatch.response
|
|
2199
|
+
response: dispatch.response,
|
|
2200
|
+
authRefreshed: dispatch.refreshedAuth === true,
|
|
2201
|
+
authRefreshFailure: dispatch.authRefreshFailure
|
|
1828
2202
|
};
|
|
1829
2203
|
if (pipeline) return { ok: false, ...failure };
|
|
1830
2204
|
console.log(JSON.stringify(failure, null, 2));
|
|
@@ -1865,17 +2239,34 @@ async function dispatchRun(args) {
|
|
|
1865
2239
|
console.log(JSON.stringify(summary2, null, 2));
|
|
1866
2240
|
return;
|
|
1867
2241
|
}
|
|
2242
|
+
const retryLimits = readHarnessRetryLimits();
|
|
1868
2243
|
const outcomes = [];
|
|
1869
2244
|
for (const decision of result.started) {
|
|
1870
2245
|
const task = decision.task;
|
|
2246
|
+
const attempt = Number(task.attempt) || 1;
|
|
2247
|
+
if (attempt > retryLimits.maxTaskAttempts) {
|
|
2248
|
+
outcomes.push({
|
|
2249
|
+
taskId: task.id,
|
|
2250
|
+
started: false,
|
|
2251
|
+
error: `task attempt ${attempt} exceeds KYNVER_MAX_TASK_ATTEMPTS (${retryLimits.maxTaskAttempts})`
|
|
2252
|
+
});
|
|
2253
|
+
continue;
|
|
2254
|
+
}
|
|
1871
2255
|
const name = safeSlug(`t-${task.id}-a${task.attempt}`);
|
|
2256
|
+
const routing = resolveWorkerLaunch({
|
|
2257
|
+
explicitModel: args.model ? String(args.model) : void 0,
|
|
2258
|
+
task: enrichTaskForModelRouting(task)
|
|
2259
|
+
});
|
|
1872
2260
|
try {
|
|
1873
2261
|
const planId = task.planId ? String(task.planId) : void 0;
|
|
1874
2262
|
const worker = spawnWorkerProcess(run, {
|
|
1875
2263
|
name,
|
|
1876
2264
|
task: buildDispatchTaskText(task, agentOsId),
|
|
1877
2265
|
ownedPaths: args.owned ? String(args.owned).split(",").map((s) => s.trim()).filter(Boolean) : [],
|
|
1878
|
-
model:
|
|
2266
|
+
model: routing.model,
|
|
2267
|
+
provider: routing.provider,
|
|
2268
|
+
routingRule: routing.rule,
|
|
2269
|
+
requestedModel: routing.requestedModel,
|
|
1879
2270
|
agentOsId,
|
|
1880
2271
|
taskId: String(task.id),
|
|
1881
2272
|
planId,
|
|
@@ -1887,13 +2278,16 @@ async function dispatchRun(args) {
|
|
|
1887
2278
|
started: true,
|
|
1888
2279
|
worker: worker.name,
|
|
1889
2280
|
pid: worker.pid,
|
|
1890
|
-
branch: worker.branch
|
|
2281
|
+
branch: worker.branch,
|
|
2282
|
+
model: worker.model,
|
|
2283
|
+
provider: routing.provider,
|
|
2284
|
+
routingRule: routing.rule
|
|
1891
2285
|
});
|
|
1892
2286
|
} catch (error) {
|
|
1893
2287
|
const releaseUrl = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/tasks/${encodeURIComponent(String(task.id))}/release`;
|
|
1894
2288
|
let release;
|
|
1895
2289
|
try {
|
|
1896
|
-
release = await
|
|
2290
|
+
release = await postJsonWithCredentialRefresh(releaseUrl, secret, { agentOsId, leaseOwner }, { agentOsId, baseUrl: base });
|
|
1897
2291
|
} catch (relErr) {
|
|
1898
2292
|
release = { ok: false, error: relErr.message };
|
|
1899
2293
|
}
|
|
@@ -1942,6 +2336,7 @@ async function sweepRun(args) {
|
|
|
1942
2336
|
const base = resolveBaseUrl(args.baseUrl ? String(args.baseUrl) : void 0);
|
|
1943
2337
|
const secret = await resolveCallbackSecretWithMint(args.secret ? String(args.secret) : void 0, agentOsId, { baseUrl: base });
|
|
1944
2338
|
const leaseOwner = `openclaw-harness:${run.id}`;
|
|
2339
|
+
const snapshotPublished = await publishHarnessBoardSnapshot({ run: run.id, agentOsId, ...args }, "run_sweep");
|
|
1945
2340
|
const releasedLocalOrphans = [];
|
|
1946
2341
|
for (const name of Object.keys(run.workers || {})) {
|
|
1947
2342
|
const worker = readJson(
|
|
@@ -1951,11 +2346,11 @@ async function sweepRun(args) {
|
|
|
1951
2346
|
if (!worker || !worker.dispatched || !worker.taskId) continue;
|
|
1952
2347
|
const status = computeWorkerStatus(worker);
|
|
1953
2348
|
if (status.alive) continue;
|
|
1954
|
-
if (status.finalResult) continue;
|
|
2349
|
+
if (status.finalResult || worker.completionReportedAt) continue;
|
|
1955
2350
|
const releaseUrl = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/tasks/${encodeURIComponent(String(worker.taskId))}/release`;
|
|
1956
2351
|
let release;
|
|
1957
2352
|
try {
|
|
1958
|
-
release = await
|
|
2353
|
+
release = await postJsonWithCredentialRefresh(releaseUrl, secret, { agentOsId, leaseOwner }, { agentOsId, baseUrl: base });
|
|
1959
2354
|
} catch (relErr) {
|
|
1960
2355
|
release = { ok: false, error: relErr.message };
|
|
1961
2356
|
}
|
|
@@ -1970,14 +2365,14 @@ async function sweepRun(args) {
|
|
|
1970
2365
|
const reapUrl = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/tasks/reap`;
|
|
1971
2366
|
let reap;
|
|
1972
2367
|
try {
|
|
1973
|
-
reap = await
|
|
2368
|
+
reap = await postJsonWithCredentialRefresh(reapUrl, secret, {
|
|
1974
2369
|
agentOsId,
|
|
1975
2370
|
...Number(args.graceMs) >= 0 && args.graceMs !== void 0 && args.graceMs !== true ? { graceMs: Math.floor(Number(args.graceMs)) } : {}
|
|
1976
|
-
});
|
|
2371
|
+
}, { agentOsId, baseUrl: base });
|
|
1977
2372
|
} catch (reapErr) {
|
|
1978
2373
|
reap = { ok: false, error: reapErr.message };
|
|
1979
2374
|
}
|
|
1980
|
-
const summary = { runId: run.id, agentOsId, leaseOwner, releasedLocalOrphans, reap: reap.response ?? reap };
|
|
2375
|
+
const summary = { runId: run.id, agentOsId, leaseOwner, snapshotPublished, releasedLocalOrphans, reap: reap.response ?? reap };
|
|
1981
2376
|
if (pipeline) return { ok: true, ...summary };
|
|
1982
2377
|
console.log(JSON.stringify(summary, null, 2));
|
|
1983
2378
|
} catch (error) {
|
|
@@ -2045,7 +2440,10 @@ function failExists(message) {
|
|
|
2045
2440
|
}
|
|
2046
2441
|
|
|
2047
2442
|
// src/pipeline-tick.ts
|
|
2048
|
-
import
|
|
2443
|
+
import path17 from "node:path";
|
|
2444
|
+
|
|
2445
|
+
// src/stale-reconcile.ts
|
|
2446
|
+
import path15 from "node:path";
|
|
2049
2447
|
|
|
2050
2448
|
// src/finalize.ts
|
|
2051
2449
|
import path14 from "node:path";
|
|
@@ -2056,13 +2454,17 @@ function terminalStatusFor(run) {
|
|
|
2056
2454
|
let anyAlive = false;
|
|
2057
2455
|
let anyResult = false;
|
|
2058
2456
|
let anyCompletionBlocked = false;
|
|
2457
|
+
let anyLandingBlocked = false;
|
|
2059
2458
|
for (const name of names) {
|
|
2060
2459
|
const worker = readJson(
|
|
2061
2460
|
path14.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
2062
2461
|
void 0
|
|
2063
2462
|
);
|
|
2064
2463
|
if (!worker) continue;
|
|
2065
|
-
const status = computeWorkerStatus(worker
|
|
2464
|
+
const status = computeWorkerStatus(worker, {
|
|
2465
|
+
base: run.base,
|
|
2466
|
+
baseCommit: run.baseCommit
|
|
2467
|
+
});
|
|
2066
2468
|
if (status.alive && !status.finalResult) {
|
|
2067
2469
|
anyAlive = true;
|
|
2068
2470
|
break;
|
|
@@ -2070,10 +2472,14 @@ function terminalStatusFor(run) {
|
|
|
2070
2472
|
if (typeof worker.completionBlocker === "string" && worker.completionBlocker) {
|
|
2071
2473
|
anyCompletionBlocked = true;
|
|
2072
2474
|
}
|
|
2073
|
-
if (status
|
|
2475
|
+
if (isLandingBlockedWorkerStatus(status)) {
|
|
2476
|
+
anyLandingBlocked = true;
|
|
2477
|
+
}
|
|
2478
|
+
if (status.finalResult && status.attention.state === "done") anyResult = true;
|
|
2074
2479
|
}
|
|
2075
2480
|
if (anyAlive) return null;
|
|
2076
2481
|
if (anyCompletionBlocked) return null;
|
|
2482
|
+
if (anyLandingBlocked) return null;
|
|
2077
2483
|
return anyResult ? "completed" : "failed";
|
|
2078
2484
|
}
|
|
2079
2485
|
function finalizeStaleRuns() {
|
|
@@ -2090,20 +2496,94 @@ function finalizeStaleRuns() {
|
|
|
2090
2496
|
return finalized;
|
|
2091
2497
|
}
|
|
2092
2498
|
|
|
2499
|
+
// src/stale-reconcile.ts
|
|
2500
|
+
var STALE_RECONCILE_HEARTBEAT_MS = 15 * 60 * 1e3;
|
|
2501
|
+
function staleReconcileDisabled() {
|
|
2502
|
+
return process.env.KYNVER_NO_STALE_CLEANUP === "1";
|
|
2503
|
+
}
|
|
2504
|
+
function reconcileStaleWorkers() {
|
|
2505
|
+
if (staleReconcileDisabled()) {
|
|
2506
|
+
return { workers: [], finalizedRuns: finalizeStaleRuns() };
|
|
2507
|
+
}
|
|
2508
|
+
const outcomes = [];
|
|
2509
|
+
const now = Date.now();
|
|
2510
|
+
for (const run of listRunRecords()) {
|
|
2511
|
+
for (const name of Object.keys(run.workers || {})) {
|
|
2512
|
+
const workerPath = path15.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
|
|
2513
|
+
const worker = readJson(workerPath, void 0);
|
|
2514
|
+
if (!worker || worker.status !== "running") {
|
|
2515
|
+
outcomes.push({
|
|
2516
|
+
runId: run.id,
|
|
2517
|
+
worker: name,
|
|
2518
|
+
action: "skipped",
|
|
2519
|
+
reason: worker ? `worker status is ${worker.status}` : "worker.json missing"
|
|
2520
|
+
});
|
|
2521
|
+
continue;
|
|
2522
|
+
}
|
|
2523
|
+
const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
|
|
2524
|
+
if (status.finalResult) {
|
|
2525
|
+
outcomes.push({ runId: run.id, worker: name, action: "skipped", reason: "final result present" });
|
|
2526
|
+
continue;
|
|
2527
|
+
}
|
|
2528
|
+
if (!status.alive) {
|
|
2529
|
+
const nextStatus = status.attention.state === "blocked" ? "blocked" : status.status === "done" ? "done" : "exited";
|
|
2530
|
+
worker.status = nextStatus;
|
|
2531
|
+
worker.reconciledAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2532
|
+
worker.reconcileReason = status.attention.reason;
|
|
2533
|
+
saveWorker(run.id, worker);
|
|
2534
|
+
outcomes.push({
|
|
2535
|
+
runId: run.id,
|
|
2536
|
+
worker: name,
|
|
2537
|
+
action: "marked_exited",
|
|
2538
|
+
reason: status.attention.reason
|
|
2539
|
+
});
|
|
2540
|
+
continue;
|
|
2541
|
+
}
|
|
2542
|
+
if (status.attention.state === "stale" && worker.pid && isPidAlive(worker.pid)) {
|
|
2543
|
+
const hbMs = status.lastHeartbeatAt ? Date.parse(status.lastHeartbeatAt) : NaN;
|
|
2544
|
+
const actMs = status.lastActivityAt ? Date.parse(status.lastActivityAt) : NaN;
|
|
2545
|
+
const hbStale = !Number.isFinite(hbMs) || now - hbMs > STALE_RECONCILE_HEARTBEAT_MS;
|
|
2546
|
+
const actStale = Number.isFinite(actMs) && now - actMs > STALE_MS;
|
|
2547
|
+
if (hbStale && actStale) {
|
|
2548
|
+
killWorkerProcess(worker.pid, "SIGTERM");
|
|
2549
|
+
worker.status = "exited";
|
|
2550
|
+
worker.reconciledAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2551
|
+
worker.reconcileReason = `reconciled stale worker: ${status.attention.reason}`;
|
|
2552
|
+
saveWorker(run.id, worker);
|
|
2553
|
+
outcomes.push({
|
|
2554
|
+
runId: run.id,
|
|
2555
|
+
worker: name,
|
|
2556
|
+
action: "killed_stale",
|
|
2557
|
+
reason: status.attention.reason
|
|
2558
|
+
});
|
|
2559
|
+
continue;
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
outcomes.push({
|
|
2563
|
+
runId: run.id,
|
|
2564
|
+
worker: name,
|
|
2565
|
+
action: "skipped",
|
|
2566
|
+
reason: status.attention.reason
|
|
2567
|
+
});
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
return { workers: outcomes, finalizedRuns: finalizeStaleRuns() };
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2093
2573
|
// src/plan-progress-daemon-sync.ts
|
|
2094
|
-
import
|
|
2574
|
+
import path16 from "node:path";
|
|
2095
2575
|
|
|
2096
2576
|
// src/plan-progress-sync.ts
|
|
2097
2577
|
async function syncPlanProgress(args) {
|
|
2098
2578
|
const base = resolveBaseUrl(args.baseUrl);
|
|
2099
2579
|
const secret = await resolveCallbackSecretWithMint(args.secret, args.agentOsId, { baseUrl: base });
|
|
2100
2580
|
const url = `${base}/api/agent-os/by-id/${encodeURIComponent(args.agentOsId)}/tasks/${encodeURIComponent(args.taskId)}/plan-progress-sync`;
|
|
2101
|
-
const res = await
|
|
2581
|
+
const res = await postJsonWithCredentialRefresh(url, secret, {
|
|
2102
2582
|
phase: args.phase,
|
|
2103
2583
|
taskId: args.taskId,
|
|
2104
2584
|
blocker: args.blocker,
|
|
2105
2585
|
artifact: args.artifact
|
|
2106
|
-
});
|
|
2586
|
+
}, { agentOsId: args.agentOsId, baseUrl: base });
|
|
2107
2587
|
return { ok: res.ok, status: res.status, response: res.response };
|
|
2108
2588
|
}
|
|
2109
2589
|
|
|
@@ -2115,7 +2595,7 @@ async function syncActiveWorkerPlanProgress(runId, args) {
|
|
|
2115
2595
|
const outcomes = [];
|
|
2116
2596
|
for (const name of Object.keys(run.workers || {})) {
|
|
2117
2597
|
const worker = readJson(
|
|
2118
|
-
|
|
2598
|
+
path16.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
2119
2599
|
void 0
|
|
2120
2600
|
);
|
|
2121
2601
|
if (!worker?.dispatched || !worker.taskId) continue;
|
|
@@ -2169,7 +2649,7 @@ async function completeFinishedWorkers(runId, args) {
|
|
|
2169
2649
|
const outcomes = [];
|
|
2170
2650
|
for (const name of Object.keys(run.workers || {})) {
|
|
2171
2651
|
const worker = readJson(
|
|
2172
|
-
|
|
2652
|
+
path17.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
2173
2653
|
void 0
|
|
2174
2654
|
);
|
|
2175
2655
|
if (!worker?.taskId) continue;
|
|
@@ -2194,6 +2674,7 @@ async function postOperatorTick(agentOsId, runId, resourceGate, args) {
|
|
|
2194
2674
|
agentOsId,
|
|
2195
2675
|
runId,
|
|
2196
2676
|
ingestHarness: true,
|
|
2677
|
+
harnessBoardSnapshot: buildRunBoard(runId),
|
|
2197
2678
|
resourceGate
|
|
2198
2679
|
});
|
|
2199
2680
|
return { ok: res.ok, httpStatus: res.status, response: res.response };
|
|
@@ -2204,7 +2685,7 @@ async function runPipelineTick(args) {
|
|
|
2204
2685
|
const execute = args.execute !== false && args.execute !== "false";
|
|
2205
2686
|
runStatus({ run: runId });
|
|
2206
2687
|
const completedWorkers = await completeFinishedWorkers(runId, args);
|
|
2207
|
-
const
|
|
2688
|
+
const staleReconcile = reconcileStaleWorkers();
|
|
2208
2689
|
const planProgressSync = await syncActiveWorkerPlanProgress(runId, args);
|
|
2209
2690
|
const workspacePrefs = await fetchWorkspaceRuntimePreferences(agentOsId, args);
|
|
2210
2691
|
const resourceGate = observeRunnerResourceGate({
|
|
@@ -2245,7 +2726,7 @@ async function runPipelineTick(args) {
|
|
|
2245
2726
|
execute,
|
|
2246
2727
|
resourceGate,
|
|
2247
2728
|
completedWorkers,
|
|
2248
|
-
|
|
2729
|
+
staleReconcile,
|
|
2249
2730
|
planProgressSync,
|
|
2250
2731
|
operatorTick,
|
|
2251
2732
|
sweep,
|
|
@@ -2405,7 +2886,7 @@ function usage(code = 0) {
|
|
|
2405
2886
|
" kynver run create --repo /path/repo [--name name] [--base origin/main]",
|
|
2406
2887
|
" kynver run list",
|
|
2407
2888
|
" kynver run status --run RUN_ID",
|
|
2408
|
-
" kynver run dispatch --run RUN_ID --agent-os-id AOS_ID [--base-url URL] [--secret SECRET] [--execute] [--lane any|implementation|review|landing] [--max-starts 1] [--lease-ms MS] [--owned path[,path]] [--model claude-opus-4-7] [--disk-path /]",
|
|
2889
|
+
" kynver run dispatch --run RUN_ID --agent-os-id AOS_ID [--base-url URL] [--secret SECRET] [--execute] [--lane any|implementation|review|landing] [--executor harness] [--max-starts 1] [--lease-ms MS] [--owned path[,path]] [--model claude-opus-4-7] [--disk-path /]",
|
|
2409
2890
|
" kynver run sweep --run RUN_ID --agent-os-id AOS_ID [--base-url URL] [--secret SECRET] [--grace-ms MS]",
|
|
2410
2891
|
' kynver worker start --run RUN_ID --name worker --task "..." [--owned path[,path]] [--model MODEL] [--provider claude|cursor] [--agent-os-id AOS_ID] [--task-id TASK_ID]',
|
|
2411
2892
|
" kynver worker status --run RUN_ID --name worker",
|