@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/index.js
CHANGED
|
@@ -176,7 +176,7 @@ function resolveConfiguredBaseUrl(argsBaseUrl) {
|
|
|
176
176
|
return baseUrl ? trimTrailingSlash(String(baseUrl)) : void 0;
|
|
177
177
|
}
|
|
178
178
|
function resolveConfiguredCallbackSecret(argsSecret, agentOsId) {
|
|
179
|
-
const scoped = argsSecret || loadRunnerToken(agentOsId) || loadRunnerToken(loadUserConfig().agentOsId);
|
|
179
|
+
const scoped = argsSecret || loadRunnerToken(agentOsId) || (agentOsId ? void 0 : loadRunnerToken(loadUserConfig().agentOsId));
|
|
180
180
|
if (scoped) return String(scoped);
|
|
181
181
|
const globalSecret = process.env.KYNVER_RUNTIME_SECRET || process.env.OPENCLAW_CRON_SECRET;
|
|
182
182
|
if (globalSecret) {
|
|
@@ -224,6 +224,23 @@ async function refreshRunnerToken(agentOsId, opts) {
|
|
|
224
224
|
return null;
|
|
225
225
|
}
|
|
226
226
|
}
|
|
227
|
+
async function refreshRunnerTokenForAuthFailure(rejectedSecret, agentOsId, opts) {
|
|
228
|
+
const apiKey = loadApiKey();
|
|
229
|
+
const baseUrl = resolveConfiguredBaseUrl(opts?.baseUrl);
|
|
230
|
+
if (!apiKey) return { ok: false, reason: "KYNVER_API_KEY is required to refresh a rejected runner token" };
|
|
231
|
+
if (!agentOsId) return { ok: false, reason: "agentOsId is required to refresh a rejected runner token" };
|
|
232
|
+
if (!baseUrl) return { ok: false, reason: "KYNVER_API_URL or --base-url is required to refresh a rejected runner token" };
|
|
233
|
+
try {
|
|
234
|
+
const token = await fetchRunnerCredential(agentOsId, { baseUrl, apiKey });
|
|
235
|
+
if (token && token !== rejectedSecret) {
|
|
236
|
+
saveRunnerToken(agentOsId, token);
|
|
237
|
+
return { ok: true, token };
|
|
238
|
+
}
|
|
239
|
+
return { ok: false, reason: "runner credential refresh returned the rejected token" };
|
|
240
|
+
} catch (error) {
|
|
241
|
+
return { ok: false, reason: error.message };
|
|
242
|
+
}
|
|
243
|
+
}
|
|
227
244
|
async function fetchRunnerCredential(agentOsId, opts) {
|
|
228
245
|
const apiKey = opts?.apiKey || loadApiKey();
|
|
229
246
|
if (!apiKey) throw new Error("API key required \u2014 run `kynver login` first");
|
|
@@ -376,6 +393,14 @@ async function postJson(url, secret, body) {
|
|
|
376
393
|
}
|
|
377
394
|
return { ok: res.ok, status: res.status, response };
|
|
378
395
|
}
|
|
396
|
+
async function postJsonWithCredentialRefresh(url, secret, body, opts) {
|
|
397
|
+
const first = await postJson(url, secret, body);
|
|
398
|
+
if (first.ok || first.status !== 401) return first;
|
|
399
|
+
const refreshed = await refreshRunnerTokenForAuthFailure(secret, opts.agentOsId, { baseUrl: opts.baseUrl });
|
|
400
|
+
if (!refreshed.ok) return { ...first, authRefreshFailure: refreshed.reason };
|
|
401
|
+
const retry = await postJson(url, refreshed.token, body);
|
|
402
|
+
return { ...retry, refreshedAuth: true };
|
|
403
|
+
}
|
|
379
404
|
async function getJson(url, secret) {
|
|
380
405
|
const res = await fetch(url, {
|
|
381
406
|
method: "GET",
|
|
@@ -397,12 +422,12 @@ var DEFAULT_CRITICAL_FREE_BYTES = 15 * 1024 * 1024 * 1024;
|
|
|
397
422
|
var DEFAULT_MAX_USED_PERCENT = 80;
|
|
398
423
|
var DEFAULT_HARD_MAX_USED_PERCENT = 90;
|
|
399
424
|
function observeRunnerDiskGate(input = {}) {
|
|
400
|
-
const
|
|
425
|
+
const path18 = input.diskPath?.trim() || "/";
|
|
401
426
|
const warnBelowBytes = input.diskFreeWarnBytes ?? DEFAULT_WARN_FREE_BYTES;
|
|
402
427
|
const criticalBelowBytes = input.diskFreeCriticalBytes ?? DEFAULT_CRITICAL_FREE_BYTES;
|
|
403
428
|
const maxUsedPercent = input.diskMaxUsedPercent ?? DEFAULT_MAX_USED_PERCENT;
|
|
404
429
|
const hardMaxUsedPercent = input.diskHardMaxUsedPercent ?? DEFAULT_HARD_MAX_USED_PERCENT;
|
|
405
|
-
const stats = statfsSync(
|
|
430
|
+
const stats = statfsSync(path18);
|
|
406
431
|
const freeBytes = Number(stats.bavail) * Number(stats.bsize);
|
|
407
432
|
const totalBytes = Number(stats.blocks) * Number(stats.bsize);
|
|
408
433
|
const usedPercent = totalBytes > 0 ? (totalBytes - freeBytes) / totalBytes * 100 : 100;
|
|
@@ -422,7 +447,7 @@ function observeRunnerDiskGate(input = {}) {
|
|
|
422
447
|
}
|
|
423
448
|
return {
|
|
424
449
|
ok,
|
|
425
|
-
path:
|
|
450
|
+
path: path18,
|
|
426
451
|
freeBytes,
|
|
427
452
|
totalBytes,
|
|
428
453
|
usedPercent,
|
|
@@ -696,22 +721,36 @@ function gitIsAncestor(cwd, ancestor, descendant) {
|
|
|
696
721
|
if (res.status === 1) return { isAncestor: false, error: null };
|
|
697
722
|
return { isAncestor: null, error: res.error || res.stderr || res.stdout || `git exited ${res.status}` };
|
|
698
723
|
}
|
|
699
|
-
function computeGitAncestry(worktreePath,
|
|
724
|
+
function computeGitAncestry(worktreePath, baseOrOptions = "origin/main") {
|
|
725
|
+
const options = typeof baseOrOptions === "string" ? { base: baseOrOptions } : baseOrOptions;
|
|
726
|
+
const baseLabel = options.baseCommit?.trim() || options.base?.trim() || "origin/main";
|
|
727
|
+
const pinnedBaseCommit = options.baseCommit?.trim() || null;
|
|
700
728
|
if (!worktreePath) {
|
|
701
|
-
return unknownAncestry(
|
|
729
|
+
return unknownAncestry(baseLabel, "missing worktree path");
|
|
702
730
|
}
|
|
703
731
|
const head = gitCapture(worktreePath, ["rev-parse", "HEAD"]);
|
|
704
|
-
if (head.status !== 0)
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
732
|
+
if (head.status !== 0) {
|
|
733
|
+
return unknownAncestry(baseLabel, head.error || head.stderr || head.stdout || "failed to resolve HEAD");
|
|
734
|
+
}
|
|
735
|
+
let baseSha;
|
|
736
|
+
if (pinnedBaseCommit) {
|
|
737
|
+
baseSha = pinnedBaseCommit;
|
|
738
|
+
} else {
|
|
739
|
+
const baseHead = gitCapture(worktreePath, ["rev-parse", baseLabel]);
|
|
740
|
+
if (baseHead.status !== 0) {
|
|
741
|
+
return unknownAncestry(
|
|
742
|
+
baseLabel,
|
|
743
|
+
baseHead.error || baseHead.stderr || baseHead.stdout || `failed to resolve ${baseLabel}`,
|
|
744
|
+
head.stdout.trim()
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
baseSha = baseHead.stdout.trim();
|
|
708
748
|
}
|
|
709
749
|
const headSha = head.stdout.trim();
|
|
710
|
-
const baseSha = baseHead.stdout.trim();
|
|
711
750
|
if (headSha === baseSha) {
|
|
712
751
|
return {
|
|
713
752
|
checked: true,
|
|
714
|
-
base,
|
|
753
|
+
base: baseLabel,
|
|
715
754
|
head: headSha,
|
|
716
755
|
baseHead: baseSha,
|
|
717
756
|
baseIsAncestorOfHead: true,
|
|
@@ -725,7 +764,7 @@ function computeGitAncestry(worktreePath, base = "origin/main") {
|
|
|
725
764
|
if (baseIsAncestorOfHead.isAncestor == null || headIsAncestorOfBase.isAncestor == null) {
|
|
726
765
|
return {
|
|
727
766
|
checked: false,
|
|
728
|
-
base,
|
|
767
|
+
base: baseLabel,
|
|
729
768
|
head: headSha,
|
|
730
769
|
baseHead: baseSha,
|
|
731
770
|
baseIsAncestorOfHead: baseIsAncestorOfHead.isAncestor,
|
|
@@ -737,7 +776,7 @@ function computeGitAncestry(worktreePath, base = "origin/main") {
|
|
|
737
776
|
const relation = baseIsAncestorOfHead.isAncestor ? "ahead" : headIsAncestorOfBase.isAncestor ? "merged" : "diverged";
|
|
738
777
|
return {
|
|
739
778
|
checked: true,
|
|
740
|
-
base,
|
|
779
|
+
base: baseLabel,
|
|
741
780
|
head: headSha,
|
|
742
781
|
baseHead: baseSha,
|
|
743
782
|
baseIsAncestorOfHead: baseIsAncestorOfHead.isAncestor,
|
|
@@ -764,12 +803,74 @@ function scrubClaudeEnv(env) {
|
|
|
764
803
|
return next;
|
|
765
804
|
}
|
|
766
805
|
|
|
806
|
+
// src/landing-gate.ts
|
|
807
|
+
function trimOrNull(value) {
|
|
808
|
+
if (typeof value !== "string") return null;
|
|
809
|
+
const trimmed = value.trim();
|
|
810
|
+
return trimmed.length ? trimmed : null;
|
|
811
|
+
}
|
|
812
|
+
function hasFinalResult(value) {
|
|
813
|
+
if (value === void 0 || value === null) return false;
|
|
814
|
+
if (typeof value === "string") return value.trim().length > 0;
|
|
815
|
+
if (typeof value === "boolean") return value;
|
|
816
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
817
|
+
if (typeof value === "object") return Object.keys(value).length > 0;
|
|
818
|
+
return true;
|
|
819
|
+
}
|
|
820
|
+
function hasCommittedLandingRef(snapshot) {
|
|
821
|
+
if (trimOrNull(snapshot.headCommit)) return true;
|
|
822
|
+
if (trimOrNull(snapshot.prUrl)) return true;
|
|
823
|
+
if (trimOrNull(snapshot.artifactBundlePath)) return true;
|
|
824
|
+
if (trimOrNull(snapshot.patchPath)) return true;
|
|
825
|
+
const ancestry = snapshot.gitAncestry;
|
|
826
|
+
if (ancestry?.checked && ancestry.headIsAncestorOfBase === false && trimOrNull(ancestry.head)) {
|
|
827
|
+
return true;
|
|
828
|
+
}
|
|
829
|
+
return false;
|
|
830
|
+
}
|
|
831
|
+
function assessWorkerLanding(snapshot) {
|
|
832
|
+
if (!hasFinalResult(snapshot.finalResult)) return { blocked: false };
|
|
833
|
+
if (snapshot.changedFiles.length === 0) return { blocked: false };
|
|
834
|
+
if (!hasCommittedLandingRef(snapshot)) {
|
|
835
|
+
return {
|
|
836
|
+
blocked: true,
|
|
837
|
+
reason: "dirty_worktree_no_pr",
|
|
838
|
+
detail: `Worktree has ${snapshot.changedFiles.length} uncommitted change(s) with no commit or PR; commit, open a PR, or discard before landing`
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
return {
|
|
842
|
+
blocked: true,
|
|
843
|
+
detail: `Worktree has ${snapshot.changedFiles.length} uncommitted change(s); commit or discard before landing`
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
function landingAttentionReason(verdict) {
|
|
847
|
+
if (!verdict.blocked) return void 0;
|
|
848
|
+
return verdict.detail ?? verdict.reason ?? "dirty_worktree_no_pr";
|
|
849
|
+
}
|
|
850
|
+
|
|
767
851
|
// src/status.ts
|
|
768
852
|
var NO_START_MS = 18e4;
|
|
769
853
|
var STALE_MS = 6e5;
|
|
770
854
|
function computeAttention(input) {
|
|
771
855
|
const now = Date.now();
|
|
772
|
-
if (input.
|
|
856
|
+
if (input.completionBlocker) {
|
|
857
|
+
return { state: "blocked", reason: input.completionBlocker };
|
|
858
|
+
}
|
|
859
|
+
if (input.finalResult) {
|
|
860
|
+
const landing = assessWorkerLanding({
|
|
861
|
+
finalResult: input.finalResult,
|
|
862
|
+
changedFiles: input.changedFiles ?? [],
|
|
863
|
+
gitAncestry: input.gitAncestry ?? null
|
|
864
|
+
});
|
|
865
|
+
if (landing.blocked) {
|
|
866
|
+
const detail = landingAttentionReason(landing);
|
|
867
|
+
return {
|
|
868
|
+
state: "needs_attention",
|
|
869
|
+
reason: landing.reason ? `landing blocked (${landing.reason}): ${detail}` : `landing blocked: ${detail}`
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
return { state: "done", reason: "final result recorded" };
|
|
873
|
+
}
|
|
773
874
|
if (!input.alive) {
|
|
774
875
|
const classified = classifyExitFailure(input.error);
|
|
775
876
|
if (classified) return { state: "blocked", reason: classified.reason };
|
|
@@ -800,7 +901,10 @@ function computeWorkerStatus(worker, options = {}) {
|
|
|
800
901
|
const stderrBytes = fileSize(worker.stderrPath);
|
|
801
902
|
const heartbeatBytes = fileSize(worker.heartbeatPath);
|
|
802
903
|
const changedFiles = gitStatusShort(worker.worktreePath);
|
|
803
|
-
const gitAncestry = computeGitAncestry(worker.worktreePath,
|
|
904
|
+
const gitAncestry = computeGitAncestry(worker.worktreePath, {
|
|
905
|
+
base: options.base,
|
|
906
|
+
baseCommit: options.baseCommit
|
|
907
|
+
});
|
|
804
908
|
const lastActivityAt = latestIso([
|
|
805
909
|
parsed.lastEventAt,
|
|
806
910
|
heartbeat.lastHeartbeatAt,
|
|
@@ -809,6 +913,7 @@ function computeWorkerStatus(worker, options = {}) {
|
|
|
809
913
|
fileMtime(worker.heartbeatPath)
|
|
810
914
|
]);
|
|
811
915
|
const error = parsed.error || (!alive && !parsed.finalResult ? tailFile(worker.stderrPath, 10).trim() || void 0 : void 0);
|
|
916
|
+
const completionBlocker = typeof worker.completionBlocker === "string" && worker.completionBlocker.trim() ? worker.completionBlocker.trim() : null;
|
|
812
917
|
const attention = computeAttention({
|
|
813
918
|
alive,
|
|
814
919
|
finalResult: parsed.finalResult,
|
|
@@ -818,14 +923,18 @@ function computeWorkerStatus(worker, options = {}) {
|
|
|
818
923
|
lastActivityAt,
|
|
819
924
|
heartbeatBlocker: heartbeat.heartbeatBlocker,
|
|
820
925
|
startedAt: worker.startedAt,
|
|
821
|
-
error
|
|
926
|
+
error,
|
|
927
|
+
changedFiles,
|
|
928
|
+
gitAncestry,
|
|
929
|
+
completionBlocker
|
|
822
930
|
});
|
|
931
|
+
const workerStatusLabel = completionBlocker || attention.state === "blocked" ? "blocked" : attention.state === "done" ? "done" : parsed.finalResult ? "exited" : alive ? "running" : "exited";
|
|
823
932
|
return {
|
|
824
933
|
runId: worker.runId,
|
|
825
934
|
worker: worker.name,
|
|
826
935
|
pid: worker.pid,
|
|
827
936
|
alive,
|
|
828
|
-
status:
|
|
937
|
+
status: workerStatusLabel,
|
|
829
938
|
attention,
|
|
830
939
|
branch: worker.branch,
|
|
831
940
|
worktreePath: worker.worktreePath,
|
|
@@ -854,6 +963,10 @@ function isFinishedWorkerStatus(status) {
|
|
|
854
963
|
if (status.status === "exited" || status.status === "done") return true;
|
|
855
964
|
return false;
|
|
856
965
|
}
|
|
966
|
+
function isLandingBlockedWorkerStatus(status) {
|
|
967
|
+
if (!status.finalResult) return false;
|
|
968
|
+
return status.attention.state === "needs_attention" || status.attention.state === "blocked";
|
|
969
|
+
}
|
|
857
970
|
function deriveRunStatus(fallback, workers) {
|
|
858
971
|
if (workers.length === 0) return fallback;
|
|
859
972
|
if (workers.some((w) => w.attention === "needs_attention" || w.attention === "stale" || w.attention === "blocked")) {
|
|
@@ -904,6 +1017,10 @@ function readAvailableMemBytes() {
|
|
|
904
1017
|
}
|
|
905
1018
|
return os.freemem();
|
|
906
1019
|
}
|
|
1020
|
+
function isActiveHarnessWorker(worker) {
|
|
1021
|
+
const status = computeWorkerStatus(worker);
|
|
1022
|
+
return status.alive && !status.finalResult && status.attention.state !== "done";
|
|
1023
|
+
}
|
|
907
1024
|
function countActiveWorkersForRun(run) {
|
|
908
1025
|
let active = 0;
|
|
909
1026
|
for (const name of Object.keys(run.workers || {})) {
|
|
@@ -911,11 +1028,8 @@ function countActiveWorkersForRun(run) {
|
|
|
911
1028
|
path5.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
912
1029
|
void 0
|
|
913
1030
|
);
|
|
914
|
-
if (!worker) continue;
|
|
915
|
-
|
|
916
|
-
if (status.alive && !status.finalResult && status.attention.state !== "done") {
|
|
917
|
-
active++;
|
|
918
|
-
}
|
|
1031
|
+
if (!worker || !isActiveHarnessWorker(worker)) continue;
|
|
1032
|
+
active++;
|
|
919
1033
|
}
|
|
920
1034
|
return active;
|
|
921
1035
|
}
|
|
@@ -940,7 +1054,7 @@ function observeRunnerResourceGate(input) {
|
|
|
940
1054
|
const maxConcurrentWorkers = Math.max(0, Math.min(targetCap, capacityFromTotal));
|
|
941
1055
|
const slotsByCapacity = Math.max(0, maxConcurrentWorkers - activeWorkers);
|
|
942
1056
|
const slotsByFreeMem = capacityFromFree;
|
|
943
|
-
|
|
1057
|
+
let slotsAvailable = Math.min(slotsByCapacity, slotsByFreeMem);
|
|
944
1058
|
let reason = null;
|
|
945
1059
|
if (slotsAvailable <= 0) {
|
|
946
1060
|
if (activeWorkers >= maxConcurrentWorkers) {
|
|
@@ -967,37 +1081,57 @@ function observeRunnerResourceGate(input) {
|
|
|
967
1081
|
};
|
|
968
1082
|
}
|
|
969
1083
|
|
|
970
|
-
// src/
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
function
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1084
|
+
// src/model-routing-task-enrich.ts
|
|
1085
|
+
function taskString(task, key) {
|
|
1086
|
+
const v = task[key];
|
|
1087
|
+
return typeof v === "string" ? v.trim() : "";
|
|
1088
|
+
}
|
|
1089
|
+
function normalize(value) {
|
|
1090
|
+
return value.toLowerCase();
|
|
1091
|
+
}
|
|
1092
|
+
var PERSONA_DEFAULT_LANE = {
|
|
1093
|
+
dalton: "implementer",
|
|
1094
|
+
lorentz: "report_reviewer"
|
|
1095
|
+
};
|
|
1096
|
+
function inferRoleLaneFromTask(task) {
|
|
1097
|
+
const existing = taskString(task, "roleLane");
|
|
1098
|
+
if (existing) return existing;
|
|
1099
|
+
const ref = normalize(taskString(task, "executorRef"));
|
|
1100
|
+
const title = normalize(taskString(task, "title"));
|
|
1101
|
+
const persona = normalize(taskString(task, "personaSlug"));
|
|
1102
|
+
const combined = `${ref} ${title}`;
|
|
1103
|
+
if (combined.includes("deep review") || combined.includes("security review") || ref.includes("deep-reviewer")) {
|
|
1104
|
+
return "deep_reviewer";
|
|
1105
|
+
}
|
|
1106
|
+
if (combined.includes("plan author") || combined.includes("plan-author") || title.includes("strategy plan")) {
|
|
1107
|
+
return "plan_author";
|
|
1108
|
+
}
|
|
1109
|
+
if (combined.includes("plan review") || ref.includes("plan-reviewer")) {
|
|
1110
|
+
return "plan_reviewer";
|
|
1111
|
+
}
|
|
1112
|
+
if (combined.includes("report review") || combined.includes("completion report")) {
|
|
1113
|
+
return "report_reviewer";
|
|
1114
|
+
}
|
|
1115
|
+
if (combined.includes("repair") || title.startsWith("fix ") || ref.includes("repair")) {
|
|
1116
|
+
return "repair_implementer";
|
|
1117
|
+
}
|
|
1118
|
+
if (ref.includes("cursor") || ref.includes("codex") || ref.includes("composer") || title.includes("implement") || title.includes("land:")) {
|
|
1119
|
+
return "implementer";
|
|
1120
|
+
}
|
|
1121
|
+
if (persona && PERSONA_DEFAULT_LANE[persona]) {
|
|
1122
|
+
const base = PERSONA_DEFAULT_LANE[persona];
|
|
1123
|
+
if (persona === "lorentz" && (combined.includes("deep") || combined.includes("security"))) {
|
|
1124
|
+
return "deep_reviewer";
|
|
1125
|
+
}
|
|
1126
|
+
return base;
|
|
1127
|
+
}
|
|
1128
|
+
if (combined.includes("review")) return "report_reviewer";
|
|
1129
|
+
return void 0;
|
|
1130
|
+
}
|
|
1131
|
+
function enrichTaskForModelRouting(task) {
|
|
1132
|
+
const roleLane = inferRoleLaneFromTask(task);
|
|
1133
|
+
if (!roleLane) return task;
|
|
1134
|
+
return { ...task, roleLane };
|
|
1001
1135
|
}
|
|
1002
1136
|
|
|
1003
1137
|
// src/providers/claude.ts
|
|
@@ -1058,7 +1192,7 @@ function preflightCursorModel(model, defaultModel) {
|
|
|
1058
1192
|
}
|
|
1059
1193
|
|
|
1060
1194
|
// src/providers/claude.ts
|
|
1061
|
-
var CLAUDE_DEFAULT_MODEL = "claude-
|
|
1195
|
+
var CLAUDE_DEFAULT_MODEL = "claude-sonnet-4-6";
|
|
1062
1196
|
var claudeProvider = {
|
|
1063
1197
|
name: "claude",
|
|
1064
1198
|
defaultModel: CLAUDE_DEFAULT_MODEL,
|
|
@@ -1104,6 +1238,172 @@ var claudeProvider = {
|
|
|
1104
1238
|
}
|
|
1105
1239
|
};
|
|
1106
1240
|
|
|
1241
|
+
// src/model-routing.ts
|
|
1242
|
+
var GLOBAL_DEFAULT_MODEL = "claude-sonnet-4-6";
|
|
1243
|
+
function taskString2(task, key) {
|
|
1244
|
+
const v = task[key];
|
|
1245
|
+
return typeof v === "string" ? v.trim() : "";
|
|
1246
|
+
}
|
|
1247
|
+
function normalizeRef(ref) {
|
|
1248
|
+
return ref.toLowerCase();
|
|
1249
|
+
}
|
|
1250
|
+
function resolveGlobalDefaultModel(config = loadUserConfig()) {
|
|
1251
|
+
const fromConfig = config.defaultModel?.trim();
|
|
1252
|
+
if (fromConfig) return fromConfig;
|
|
1253
|
+
const fromEnv = process.env.KYNVER_DEFAULT_MODEL?.trim();
|
|
1254
|
+
if (fromEnv) return fromEnv;
|
|
1255
|
+
return GLOBAL_DEFAULT_MODEL;
|
|
1256
|
+
}
|
|
1257
|
+
function inferProviderFromModel(model) {
|
|
1258
|
+
const m = (model ?? "").toLowerCase();
|
|
1259
|
+
if (!m) return "claude";
|
|
1260
|
+
if (m.includes("composer") || m.includes("cursor") || m.includes("codex") || m.startsWith("gpt-") || m.startsWith("gpt5")) {
|
|
1261
|
+
return "cursor";
|
|
1262
|
+
}
|
|
1263
|
+
return "claude";
|
|
1264
|
+
}
|
|
1265
|
+
function isOpusLane(ref, title) {
|
|
1266
|
+
if (ref.includes("deep") && ref.includes("review")) return true;
|
|
1267
|
+
if (ref.includes("security")) return true;
|
|
1268
|
+
if (ref.includes("plan_author") || ref.includes("plan-author")) return true;
|
|
1269
|
+
if (title.includes("deep review") || title.includes("security review")) return true;
|
|
1270
|
+
if (ref.includes("plan") && !ref.includes("review") && (ref.includes("author") || ref.includes("strategy"))) {
|
|
1271
|
+
return true;
|
|
1272
|
+
}
|
|
1273
|
+
return false;
|
|
1274
|
+
}
|
|
1275
|
+
function inferModelRoutingFromTask(task) {
|
|
1276
|
+
const ref = normalizeRef(taskString2(task, "executorRef"));
|
|
1277
|
+
const title = taskString2(task, "title").toLowerCase();
|
|
1278
|
+
const priority = taskString2(task, "priority") || "normal";
|
|
1279
|
+
const roleLane = normalizeRef(taskString2(task, "roleLane"));
|
|
1280
|
+
if (ref.includes("cursor") || ref.includes("codex") || ref.includes("composer") || ref.includes("copilot") || roleLane === "implementer" || roleLane === "repair_implementer") {
|
|
1281
|
+
return { provider: "cursor", rule: "lane:implementation" };
|
|
1282
|
+
}
|
|
1283
|
+
if (ref.includes("landing") || title.startsWith("land:") || title.includes(" merge")) {
|
|
1284
|
+
return {
|
|
1285
|
+
model: "claude-haiku-4-5-20251001",
|
|
1286
|
+
provider: "claude",
|
|
1287
|
+
rule: "lane:landing"
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
if (ref.includes("review") || title.startsWith("review ") || roleLane.includes("review")) {
|
|
1291
|
+
if (isOpusLane(ref, title) || roleLane === "deep_reviewer") {
|
|
1292
|
+
return { model: "claude-opus-4-7", provider: "claude", rule: "lane:deep_review" };
|
|
1293
|
+
}
|
|
1294
|
+
return { model: "claude-sonnet-4-6", provider: "claude", rule: "lane:review" };
|
|
1295
|
+
}
|
|
1296
|
+
if (isOpusLane(ref, title) || roleLane === "plan_author") {
|
|
1297
|
+
return { model: "claude-opus-4-7", provider: "claude", rule: "lane:planning" };
|
|
1298
|
+
}
|
|
1299
|
+
if (priority === "critical") {
|
|
1300
|
+
return { model: "claude-opus-4-7", provider: "claude", rule: "priority:critical" };
|
|
1301
|
+
}
|
|
1302
|
+
if (priority === "high") {
|
|
1303
|
+
return { model: "claude-sonnet-4-6", provider: "claude", rule: "priority:high" };
|
|
1304
|
+
}
|
|
1305
|
+
if (priority === "low") {
|
|
1306
|
+
return {
|
|
1307
|
+
model: "claude-haiku-4-5-20251001",
|
|
1308
|
+
provider: "claude",
|
|
1309
|
+
rule: "priority:low"
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
return {
|
|
1313
|
+
model: resolveGlobalDefaultModel(),
|
|
1314
|
+
provider: "claude",
|
|
1315
|
+
rule: "default:sonnet"
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
function resolveWorkerLaunch(input) {
|
|
1319
|
+
if (input.explicitModel?.trim()) {
|
|
1320
|
+
const model2 = input.explicitModel.trim();
|
|
1321
|
+
return {
|
|
1322
|
+
model: model2,
|
|
1323
|
+
provider: input.explicitProvider?.trim() || inferProviderFromModel(model2),
|
|
1324
|
+
rule: "explicit:cli",
|
|
1325
|
+
requestedModel: model2
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
if (input.task && Object.keys(input.task).length > 0) {
|
|
1329
|
+
const inferred = inferModelRoutingFromTask(input.task);
|
|
1330
|
+
return {
|
|
1331
|
+
...inferred,
|
|
1332
|
+
requestedModel: inferred.model
|
|
1333
|
+
};
|
|
1334
|
+
}
|
|
1335
|
+
const model = resolveGlobalDefaultModel();
|
|
1336
|
+
return {
|
|
1337
|
+
model,
|
|
1338
|
+
provider: input.explicitProvider?.trim() || inferProviderFromModel(model),
|
|
1339
|
+
rule: "default:global",
|
|
1340
|
+
requestedModel: model
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
function resolveModelFallback(startedModel, launchModel, providerDefault) {
|
|
1344
|
+
return startedModel || launchModel || providerDefault || resolveGlobalDefaultModel() || CLAUDE_DEFAULT_MODEL;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// src/retry-limits.ts
|
|
1348
|
+
function positiveInt2(value, fallback) {
|
|
1349
|
+
const n = Number(value);
|
|
1350
|
+
if (!Number.isFinite(n) || n <= 0) return fallback;
|
|
1351
|
+
return Math.floor(n);
|
|
1352
|
+
}
|
|
1353
|
+
function readHarnessRetryLimits() {
|
|
1354
|
+
return {
|
|
1355
|
+
maxTaskAttempts: positiveInt2(process.env.KYNVER_MAX_TASK_ATTEMPTS, 3),
|
|
1356
|
+
dispatchCooldownMs: positiveInt2(process.env.KYNVER_DISPATCH_COOLDOWN_MS, 5e3)
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
// src/supervisor.ts
|
|
1361
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync3 } from "node:fs";
|
|
1362
|
+
import path10 from "node:path";
|
|
1363
|
+
|
|
1364
|
+
// src/prompt.ts
|
|
1365
|
+
function buildPrompt(input) {
|
|
1366
|
+
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.";
|
|
1367
|
+
const compact = Boolean(input.model?.toLowerCase().includes("haiku"));
|
|
1368
|
+
const progressLines = compact ? [
|
|
1369
|
+
"Plan progress: when planId is set, use `kynver plan progress` for running|partial|blocked only; row `done` is MCP/session only.",
|
|
1370
|
+
input.planId ? `Active planId: ${input.planId}` : "No planId on this worker."
|
|
1371
|
+
] : [
|
|
1372
|
+
"Structured plan progress (required when planId is set):",
|
|
1373
|
+
"- 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).",
|
|
1374
|
+
"- 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.",
|
|
1375
|
+
"- 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`).",
|
|
1376
|
+
"- 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>`.",
|
|
1377
|
+
"- Before the completion report: mark completion-report rows partial with evidence; do not skip report review.",
|
|
1378
|
+
"- After implementation: wait for report_reviewer then deep_reviewer confirmation (via MCP/session agents) before follow-up rows close.",
|
|
1379
|
+
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."
|
|
1380
|
+
];
|
|
1381
|
+
const planArtifactLines = compact ? [
|
|
1382
|
+
"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."
|
|
1383
|
+
] : [
|
|
1384
|
+
"PR-first plan artifacts (when authoring or revising docs/superpowers/plans/):",
|
|
1385
|
+
"- 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.",
|
|
1386
|
+
"- Iterate review on that PR branch; link prUrl on the AgentOS task and plan progress evidence (`--evidence pr:<url>`).",
|
|
1387
|
+
"- See docs/superpowers/plans/2026-05-25-pr-first-plan-artifact-preservation.md for the full checklist."
|
|
1388
|
+
];
|
|
1389
|
+
return [
|
|
1390
|
+
"You are running under the Kynver AgentOS runtime.",
|
|
1391
|
+
"Immediately state your plan before editing.",
|
|
1392
|
+
ownership,
|
|
1393
|
+
`Worktree: ${input.worktreePath}`,
|
|
1394
|
+
`Progress heartbeat file: ${input.heartbeatPath}`,
|
|
1395
|
+
"After each major step, append one JSON line to the heartbeat file with fields: ts, phase, summary, changedFiles, blocker.",
|
|
1396
|
+
"Final response must include files changed, verification commands, and unresolved risks.",
|
|
1397
|
+
"",
|
|
1398
|
+
...progressLines,
|
|
1399
|
+
"",
|
|
1400
|
+
...planArtifactLines,
|
|
1401
|
+
"",
|
|
1402
|
+
"Task:",
|
|
1403
|
+
input.task
|
|
1404
|
+
].join("\n");
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1107
1407
|
// src/providers/cursor.ts
|
|
1108
1408
|
import { closeSync as closeSync2, existsSync as existsSync8, openSync as openSync2 } from "node:fs";
|
|
1109
1409
|
import { spawn as spawn2 } from "node:child_process";
|
|
@@ -1307,9 +1607,13 @@ function persistCompletionBlocker(worker, reason) {
|
|
|
1307
1607
|
else delete worker.completionBlocker;
|
|
1308
1608
|
saveWorker(worker.runId, worker);
|
|
1309
1609
|
}
|
|
1610
|
+
function workerStatusOptions(run) {
|
|
1611
|
+
return run ? { base: run.base, baseCommit: run.baseCommit } : {};
|
|
1612
|
+
}
|
|
1310
1613
|
async function tryCompleteWorker(args) {
|
|
1311
1614
|
const worker = loadWorker(String(args.run), String(args.name));
|
|
1312
|
-
const
|
|
1615
|
+
const run = loadRun(worker.runId);
|
|
1616
|
+
const status = computeWorkerStatus(worker, workerStatusOptions(run));
|
|
1313
1617
|
const agentOsId = (args.agentOsId ? String(args.agentOsId) : worker.agentOsId) || "";
|
|
1314
1618
|
const taskId = (args.taskId ? String(args.taskId) : worker.taskId) || null;
|
|
1315
1619
|
if (!agentOsId) {
|
|
@@ -1353,7 +1657,8 @@ async function tryCompleteWorker(args) {
|
|
|
1353
1657
|
async function completeWorker(args) {
|
|
1354
1658
|
try {
|
|
1355
1659
|
const worker = loadWorker(String(args.run), String(args.name));
|
|
1356
|
-
const
|
|
1660
|
+
const run = loadRun(worker.runId);
|
|
1661
|
+
const status = computeWorkerStatus(worker, workerStatusOptions(run));
|
|
1357
1662
|
const agentOsId = (args.agentOsId ? String(args.agentOsId) : worker.agentOsId) || "";
|
|
1358
1663
|
const taskId = (args.taskId ? String(args.taskId) : worker.taskId) || null;
|
|
1359
1664
|
if (!agentOsId) {
|
|
@@ -1400,12 +1705,13 @@ async function completeWorker(args) {
|
|
|
1400
1705
|
}
|
|
1401
1706
|
function workerStatus(args) {
|
|
1402
1707
|
const worker = loadWorker(String(args.run), String(args.name));
|
|
1403
|
-
const
|
|
1708
|
+
const run = loadRun(worker.runId);
|
|
1709
|
+
const status = computeWorkerStatus(worker, workerStatusOptions(run));
|
|
1404
1710
|
writeJson(path8.join(worker.workerDir, "last-status.json"), status);
|
|
1405
1711
|
console.log(JSON.stringify(status, null, 2));
|
|
1406
1712
|
}
|
|
1407
|
-
function
|
|
1408
|
-
const run = loadRun(
|
|
1713
|
+
function buildRunBoard(runId) {
|
|
1714
|
+
const run = loadRun(runId);
|
|
1409
1715
|
const names = Object.keys(run.workers || {});
|
|
1410
1716
|
const workers = names.map((name) => {
|
|
1411
1717
|
const worker = readJson(
|
|
@@ -1415,14 +1721,21 @@ function runStatus(args) {
|
|
|
1415
1721
|
if (!worker) {
|
|
1416
1722
|
return { worker: name, status: "missing", attention: "needs_attention", attentionReason: "worker.json not found" };
|
|
1417
1723
|
}
|
|
1418
|
-
const status = computeWorkerStatus(worker, {
|
|
1724
|
+
const status = computeWorkerStatus(worker, {
|
|
1725
|
+
base: run.base,
|
|
1726
|
+
baseCommit: run.baseCommit
|
|
1727
|
+
});
|
|
1728
|
+
const headCommit = status.gitAncestry.headIsAncestorOfBase === false && status.gitAncestry.head ? status.gitAncestry.head : void 0;
|
|
1419
1729
|
const rawBlocker = worker.completionBlocker;
|
|
1420
1730
|
const completionBlocker = typeof rawBlocker === "string" && rawBlocker ? rawBlocker : void 0;
|
|
1731
|
+
const boardStatus = completionBlocker ? "blocked" : status.status;
|
|
1732
|
+
const boardAttention = completionBlocker ? "blocked" : status.attention.state;
|
|
1421
1733
|
return {
|
|
1422
1734
|
worker: status.worker,
|
|
1423
|
-
status:
|
|
1424
|
-
attention:
|
|
1735
|
+
status: boardStatus,
|
|
1736
|
+
attention: boardAttention,
|
|
1425
1737
|
attentionReason: completionBlocker ?? status.attention.reason,
|
|
1738
|
+
landingBlocked: status.finalResult ? boardAttention === "needs_attention" || boardAttention === "blocked" : false,
|
|
1426
1739
|
pid: status.pid,
|
|
1427
1740
|
alive: status.alive,
|
|
1428
1741
|
currentTool: status.currentTool,
|
|
@@ -1431,7 +1744,14 @@ function runStatus(args) {
|
|
|
1431
1744
|
lastHeartbeatSummary: status.lastHeartbeatSummary,
|
|
1432
1745
|
heartbeatBlocker: status.heartbeatBlocker,
|
|
1433
1746
|
changedFileCount: status.changedFiles.length,
|
|
1747
|
+
changedFiles: status.changedFiles,
|
|
1434
1748
|
branch: status.branch,
|
|
1749
|
+
model: typeof worker.model === "string" ? worker.model : void 0,
|
|
1750
|
+
routingRule: typeof worker.routingRule === "string" ? worker.routingRule : void 0,
|
|
1751
|
+
requestedModel: typeof worker.requestedModel === "string" ? worker.requestedModel : void 0,
|
|
1752
|
+
headCommit,
|
|
1753
|
+
gitAncestry: status.gitAncestry,
|
|
1754
|
+
finalResult: status.finalResult,
|
|
1435
1755
|
ancestry: status.gitAncestry.relation,
|
|
1436
1756
|
ancestryChecked: status.gitAncestry.checked
|
|
1437
1757
|
};
|
|
@@ -1446,6 +1766,34 @@ function runStatus(args) {
|
|
|
1446
1766
|
workers
|
|
1447
1767
|
};
|
|
1448
1768
|
writeJson(path8.join(runDirectory(run.id), "last-board.json"), board);
|
|
1769
|
+
return board;
|
|
1770
|
+
}
|
|
1771
|
+
async function publishHarnessBoardSnapshot(args, source) {
|
|
1772
|
+
const runId = String(args.run || "");
|
|
1773
|
+
const agentOsId = String(args.agentOsId || "");
|
|
1774
|
+
if (!runId || !agentOsId) return null;
|
|
1775
|
+
const board = buildRunBoard(runId);
|
|
1776
|
+
const base = resolveBaseUrl(args.baseUrl ? String(args.baseUrl) : void 0);
|
|
1777
|
+
const secret = await resolveCallbackSecretWithMint(args.secret ? String(args.secret) : void 0, agentOsId, {
|
|
1778
|
+
baseUrl: base
|
|
1779
|
+
});
|
|
1780
|
+
const url = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/harness/snapshot`;
|
|
1781
|
+
const res = await postJsonWithCredentialRefresh(
|
|
1782
|
+
url,
|
|
1783
|
+
secret,
|
|
1784
|
+
{ agentOsId, runId, source, snapshot: board },
|
|
1785
|
+
{ agentOsId, baseUrl: base }
|
|
1786
|
+
);
|
|
1787
|
+
return {
|
|
1788
|
+
ok: res.ok,
|
|
1789
|
+
httpStatus: res.status,
|
|
1790
|
+
response: res.response,
|
|
1791
|
+
authRefreshed: res.refreshedAuth,
|
|
1792
|
+
authRefreshFailure: res.authRefreshFailure
|
|
1793
|
+
};
|
|
1794
|
+
}
|
|
1795
|
+
function runStatus(args) {
|
|
1796
|
+
const board = buildRunBoard(String(args.run));
|
|
1449
1797
|
console.log(JSON.stringify(board, null, 2));
|
|
1450
1798
|
}
|
|
1451
1799
|
function tailWorker(args) {
|
|
@@ -1647,8 +1995,17 @@ function spawnWorkerProcess(run, opts) {
|
|
|
1647
1995
|
const name = safeSlug(rawName);
|
|
1648
1996
|
if (run.workers?.[name]) throw new Error(`worker already exists in run ${run.id}: ${name}`);
|
|
1649
1997
|
if (!opts.task) throw new Error(`missing task text for worker ${name}`);
|
|
1650
|
-
const
|
|
1651
|
-
|
|
1998
|
+
const routing = opts.routingRule || opts.requestedModel ? {
|
|
1999
|
+
provider: opts.provider || "claude",
|
|
2000
|
+
model: opts.model,
|
|
2001
|
+
rule: opts.routingRule || "explicit:spawn",
|
|
2002
|
+
requestedModel: opts.requestedModel ?? opts.model
|
|
2003
|
+
} : resolveWorkerLaunch({
|
|
2004
|
+
explicitModel: opts.model,
|
|
2005
|
+
explicitProvider: opts.provider
|
|
2006
|
+
});
|
|
2007
|
+
const provider = resolveWorkerProvider(routing.provider);
|
|
2008
|
+
let launchModel = routing.model;
|
|
1652
2009
|
if (provider.preflightModel) {
|
|
1653
2010
|
const preflight = provider.preflightModel(opts.model);
|
|
1654
2011
|
if (!preflight.ok) {
|
|
@@ -1676,7 +2033,10 @@ function spawnWorkerProcess(run, opts) {
|
|
|
1676
2033
|
task: opts.task,
|
|
1677
2034
|
ownedPaths: opts.ownedPaths || [],
|
|
1678
2035
|
worktreePath,
|
|
1679
|
-
heartbeatPath
|
|
2036
|
+
heartbeatPath,
|
|
2037
|
+
planId: opts.planId,
|
|
2038
|
+
taskId: opts.taskId,
|
|
2039
|
+
model: launchModel
|
|
1680
2040
|
});
|
|
1681
2041
|
let started;
|
|
1682
2042
|
try {
|
|
@@ -1698,7 +2058,7 @@ function spawnWorkerProcess(run, opts) {
|
|
|
1698
2058
|
git(run.repo, ["branch", "-D", branch], { allowFailure: true });
|
|
1699
2059
|
throw error;
|
|
1700
2060
|
}
|
|
1701
|
-
const model = started.model
|
|
2061
|
+
const model = resolveModelFallback(started.model, launchModel, provider.defaultModel);
|
|
1702
2062
|
const worker = {
|
|
1703
2063
|
name,
|
|
1704
2064
|
runId: run.id,
|
|
@@ -1717,6 +2077,8 @@ function spawnWorkerProcess(run, opts) {
|
|
|
1717
2077
|
...opts.planId ? { planId: String(opts.planId) } : {},
|
|
1718
2078
|
...opts.leaseOwner ? { leaseOwner: String(opts.leaseOwner) } : {},
|
|
1719
2079
|
...opts.dispatched ? { dispatched: true } : {},
|
|
2080
|
+
routingRule: routing.rule,
|
|
2081
|
+
...routing.requestedModel ? { requestedModel: routing.requestedModel } : {},
|
|
1720
2082
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1721
2083
|
};
|
|
1722
2084
|
saveWorker(run.id, worker);
|
|
@@ -1724,14 +2086,23 @@ function spawnWorkerProcess(run, opts) {
|
|
|
1724
2086
|
run.status = "running";
|
|
1725
2087
|
saveRun(run);
|
|
1726
2088
|
if (worker.agentOsId && worker.taskId) {
|
|
2089
|
+
let sidecarSpawned;
|
|
1727
2090
|
try {
|
|
1728
|
-
spawnCompletionSidecar({
|
|
2091
|
+
sidecarSpawned = spawnCompletionSidecar({
|
|
1729
2092
|
runId: run.id,
|
|
1730
2093
|
workerName: name,
|
|
1731
2094
|
workerDir,
|
|
1732
2095
|
agentOsId: worker.agentOsId
|
|
1733
2096
|
});
|
|
1734
|
-
} catch {
|
|
2097
|
+
} catch (error) {
|
|
2098
|
+
const reason = `completion sidecar failed to spawn: ${error.message}`;
|
|
2099
|
+
worker.completionBlocker = reason;
|
|
2100
|
+
saveWorker(run.id, worker);
|
|
2101
|
+
}
|
|
2102
|
+
if (!sidecarSpawned) {
|
|
2103
|
+
const reason = "completion sidecar failed to spawn (CLI not found or spawn error)";
|
|
2104
|
+
worker.completionBlocker = reason;
|
|
2105
|
+
saveWorker(run.id, worker);
|
|
1735
2106
|
}
|
|
1736
2107
|
}
|
|
1737
2108
|
return worker;
|
|
@@ -1815,9 +2186,10 @@ async function dispatchRun(args) {
|
|
|
1815
2186
|
runnerDiskGate,
|
|
1816
2187
|
runnerResourceGate,
|
|
1817
2188
|
...args.lane ? { lane: String(args.lane) } : {},
|
|
2189
|
+
executor: args.executor ? String(args.executor) : "harness",
|
|
1818
2190
|
...args.diskPath ? { diskPath: String(args.diskPath) } : {}
|
|
1819
2191
|
};
|
|
1820
|
-
const dispatch = await
|
|
2192
|
+
const dispatch = await postJsonWithCredentialRefresh(dispatchUrl, secret, body, { agentOsId, baseUrl: base });
|
|
1821
2193
|
const responseBody = dispatch.response;
|
|
1822
2194
|
if (!dispatch.ok || !responseBody?.result) {
|
|
1823
2195
|
const failure = {
|
|
@@ -1825,7 +2197,9 @@ async function dispatchRun(args) {
|
|
|
1825
2197
|
agentOsId,
|
|
1826
2198
|
action: "dispatch",
|
|
1827
2199
|
httpStatus: dispatch.status,
|
|
1828
|
-
response: dispatch.response
|
|
2200
|
+
response: dispatch.response,
|
|
2201
|
+
authRefreshed: dispatch.refreshedAuth === true,
|
|
2202
|
+
authRefreshFailure: dispatch.authRefreshFailure
|
|
1829
2203
|
};
|
|
1830
2204
|
if (pipeline) return { ok: false, ...failure };
|
|
1831
2205
|
console.log(JSON.stringify(failure, null, 2));
|
|
@@ -1866,17 +2240,34 @@ async function dispatchRun(args) {
|
|
|
1866
2240
|
console.log(JSON.stringify(summary2, null, 2));
|
|
1867
2241
|
return;
|
|
1868
2242
|
}
|
|
2243
|
+
const retryLimits = readHarnessRetryLimits();
|
|
1869
2244
|
const outcomes = [];
|
|
1870
2245
|
for (const decision of result.started) {
|
|
1871
2246
|
const task = decision.task;
|
|
2247
|
+
const attempt = Number(task.attempt) || 1;
|
|
2248
|
+
if (attempt > retryLimits.maxTaskAttempts) {
|
|
2249
|
+
outcomes.push({
|
|
2250
|
+
taskId: task.id,
|
|
2251
|
+
started: false,
|
|
2252
|
+
error: `task attempt ${attempt} exceeds KYNVER_MAX_TASK_ATTEMPTS (${retryLimits.maxTaskAttempts})`
|
|
2253
|
+
});
|
|
2254
|
+
continue;
|
|
2255
|
+
}
|
|
1872
2256
|
const name = safeSlug(`t-${task.id}-a${task.attempt}`);
|
|
2257
|
+
const routing = resolveWorkerLaunch({
|
|
2258
|
+
explicitModel: args.model ? String(args.model) : void 0,
|
|
2259
|
+
task: enrichTaskForModelRouting(task)
|
|
2260
|
+
});
|
|
1873
2261
|
try {
|
|
1874
2262
|
const planId = task.planId ? String(task.planId) : void 0;
|
|
1875
2263
|
const worker = spawnWorkerProcess(run, {
|
|
1876
2264
|
name,
|
|
1877
2265
|
task: buildDispatchTaskText(task, agentOsId),
|
|
1878
2266
|
ownedPaths: args.owned ? String(args.owned).split(",").map((s) => s.trim()).filter(Boolean) : [],
|
|
1879
|
-
model:
|
|
2267
|
+
model: routing.model,
|
|
2268
|
+
provider: routing.provider,
|
|
2269
|
+
routingRule: routing.rule,
|
|
2270
|
+
requestedModel: routing.requestedModel,
|
|
1880
2271
|
agentOsId,
|
|
1881
2272
|
taskId: String(task.id),
|
|
1882
2273
|
planId,
|
|
@@ -1888,13 +2279,16 @@ async function dispatchRun(args) {
|
|
|
1888
2279
|
started: true,
|
|
1889
2280
|
worker: worker.name,
|
|
1890
2281
|
pid: worker.pid,
|
|
1891
|
-
branch: worker.branch
|
|
2282
|
+
branch: worker.branch,
|
|
2283
|
+
model: worker.model,
|
|
2284
|
+
provider: routing.provider,
|
|
2285
|
+
routingRule: routing.rule
|
|
1892
2286
|
});
|
|
1893
2287
|
} catch (error) {
|
|
1894
2288
|
const releaseUrl = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/tasks/${encodeURIComponent(String(task.id))}/release`;
|
|
1895
2289
|
let release;
|
|
1896
2290
|
try {
|
|
1897
|
-
release = await
|
|
2291
|
+
release = await postJsonWithCredentialRefresh(releaseUrl, secret, { agentOsId, leaseOwner }, { agentOsId, baseUrl: base });
|
|
1898
2292
|
} catch (relErr) {
|
|
1899
2293
|
release = { ok: false, error: relErr.message };
|
|
1900
2294
|
}
|
|
@@ -2027,6 +2421,7 @@ async function sweepRun(args) {
|
|
|
2027
2421
|
const base = resolveBaseUrl(args.baseUrl ? String(args.baseUrl) : void 0);
|
|
2028
2422
|
const secret = await resolveCallbackSecretWithMint(args.secret ? String(args.secret) : void 0, agentOsId, { baseUrl: base });
|
|
2029
2423
|
const leaseOwner = `openclaw-harness:${run.id}`;
|
|
2424
|
+
const snapshotPublished = await publishHarnessBoardSnapshot({ run: run.id, agentOsId, ...args }, "run_sweep");
|
|
2030
2425
|
const releasedLocalOrphans = [];
|
|
2031
2426
|
for (const name of Object.keys(run.workers || {})) {
|
|
2032
2427
|
const worker = readJson(
|
|
@@ -2036,11 +2431,11 @@ async function sweepRun(args) {
|
|
|
2036
2431
|
if (!worker || !worker.dispatched || !worker.taskId) continue;
|
|
2037
2432
|
const status = computeWorkerStatus(worker);
|
|
2038
2433
|
if (status.alive) continue;
|
|
2039
|
-
if (status.finalResult) continue;
|
|
2434
|
+
if (status.finalResult || worker.completionReportedAt) continue;
|
|
2040
2435
|
const releaseUrl = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/tasks/${encodeURIComponent(String(worker.taskId))}/release`;
|
|
2041
2436
|
let release;
|
|
2042
2437
|
try {
|
|
2043
|
-
release = await
|
|
2438
|
+
release = await postJsonWithCredentialRefresh(releaseUrl, secret, { agentOsId, leaseOwner }, { agentOsId, baseUrl: base });
|
|
2044
2439
|
} catch (relErr) {
|
|
2045
2440
|
release = { ok: false, error: relErr.message };
|
|
2046
2441
|
}
|
|
@@ -2055,14 +2450,14 @@ async function sweepRun(args) {
|
|
|
2055
2450
|
const reapUrl = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/tasks/reap`;
|
|
2056
2451
|
let reap;
|
|
2057
2452
|
try {
|
|
2058
|
-
reap = await
|
|
2453
|
+
reap = await postJsonWithCredentialRefresh(reapUrl, secret, {
|
|
2059
2454
|
agentOsId,
|
|
2060
2455
|
...Number(args.graceMs) >= 0 && args.graceMs !== void 0 && args.graceMs !== true ? { graceMs: Math.floor(Number(args.graceMs)) } : {}
|
|
2061
|
-
});
|
|
2456
|
+
}, { agentOsId, baseUrl: base });
|
|
2062
2457
|
} catch (reapErr) {
|
|
2063
2458
|
reap = { ok: false, error: reapErr.message };
|
|
2064
2459
|
}
|
|
2065
|
-
const summary = { runId: run.id, agentOsId, leaseOwner, releasedLocalOrphans, reap: reap.response ?? reap };
|
|
2460
|
+
const summary = { runId: run.id, agentOsId, leaseOwner, snapshotPublished, releasedLocalOrphans, reap: reap.response ?? reap };
|
|
2066
2461
|
if (pipeline) return { ok: true, ...summary };
|
|
2067
2462
|
console.log(JSON.stringify(summary, null, 2));
|
|
2068
2463
|
} catch (error) {
|
|
@@ -2077,7 +2472,10 @@ import { mkdirSync as mkdirSync5, realpathSync } from "node:fs";
|
|
|
2077
2472
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
2078
2473
|
|
|
2079
2474
|
// src/pipeline-tick.ts
|
|
2080
|
-
import
|
|
2475
|
+
import path17 from "node:path";
|
|
2476
|
+
|
|
2477
|
+
// src/stale-reconcile.ts
|
|
2478
|
+
import path15 from "node:path";
|
|
2081
2479
|
|
|
2082
2480
|
// src/finalize.ts
|
|
2083
2481
|
import path14 from "node:path";
|
|
@@ -2088,13 +2486,17 @@ function terminalStatusFor(run) {
|
|
|
2088
2486
|
let anyAlive = false;
|
|
2089
2487
|
let anyResult = false;
|
|
2090
2488
|
let anyCompletionBlocked = false;
|
|
2489
|
+
let anyLandingBlocked = false;
|
|
2091
2490
|
for (const name of names) {
|
|
2092
2491
|
const worker = readJson(
|
|
2093
2492
|
path14.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
2094
2493
|
void 0
|
|
2095
2494
|
);
|
|
2096
2495
|
if (!worker) continue;
|
|
2097
|
-
const status = computeWorkerStatus(worker
|
|
2496
|
+
const status = computeWorkerStatus(worker, {
|
|
2497
|
+
base: run.base,
|
|
2498
|
+
baseCommit: run.baseCommit
|
|
2499
|
+
});
|
|
2098
2500
|
if (status.alive && !status.finalResult) {
|
|
2099
2501
|
anyAlive = true;
|
|
2100
2502
|
break;
|
|
@@ -2102,10 +2504,14 @@ function terminalStatusFor(run) {
|
|
|
2102
2504
|
if (typeof worker.completionBlocker === "string" && worker.completionBlocker) {
|
|
2103
2505
|
anyCompletionBlocked = true;
|
|
2104
2506
|
}
|
|
2105
|
-
if (status
|
|
2507
|
+
if (isLandingBlockedWorkerStatus(status)) {
|
|
2508
|
+
anyLandingBlocked = true;
|
|
2509
|
+
}
|
|
2510
|
+
if (status.finalResult && status.attention.state === "done") anyResult = true;
|
|
2106
2511
|
}
|
|
2107
2512
|
if (anyAlive) return null;
|
|
2108
2513
|
if (anyCompletionBlocked) return null;
|
|
2514
|
+
if (anyLandingBlocked) return null;
|
|
2109
2515
|
return anyResult ? "completed" : "failed";
|
|
2110
2516
|
}
|
|
2111
2517
|
function finalizeStaleRuns() {
|
|
@@ -2122,20 +2528,94 @@ function finalizeStaleRuns() {
|
|
|
2122
2528
|
return finalized;
|
|
2123
2529
|
}
|
|
2124
2530
|
|
|
2531
|
+
// src/stale-reconcile.ts
|
|
2532
|
+
var STALE_RECONCILE_HEARTBEAT_MS = 15 * 60 * 1e3;
|
|
2533
|
+
function staleReconcileDisabled() {
|
|
2534
|
+
return process.env.KYNVER_NO_STALE_CLEANUP === "1";
|
|
2535
|
+
}
|
|
2536
|
+
function reconcileStaleWorkers() {
|
|
2537
|
+
if (staleReconcileDisabled()) {
|
|
2538
|
+
return { workers: [], finalizedRuns: finalizeStaleRuns() };
|
|
2539
|
+
}
|
|
2540
|
+
const outcomes = [];
|
|
2541
|
+
const now = Date.now();
|
|
2542
|
+
for (const run of listRunRecords()) {
|
|
2543
|
+
for (const name of Object.keys(run.workers || {})) {
|
|
2544
|
+
const workerPath = path15.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
|
|
2545
|
+
const worker = readJson(workerPath, void 0);
|
|
2546
|
+
if (!worker || worker.status !== "running") {
|
|
2547
|
+
outcomes.push({
|
|
2548
|
+
runId: run.id,
|
|
2549
|
+
worker: name,
|
|
2550
|
+
action: "skipped",
|
|
2551
|
+
reason: worker ? `worker status is ${worker.status}` : "worker.json missing"
|
|
2552
|
+
});
|
|
2553
|
+
continue;
|
|
2554
|
+
}
|
|
2555
|
+
const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
|
|
2556
|
+
if (status.finalResult) {
|
|
2557
|
+
outcomes.push({ runId: run.id, worker: name, action: "skipped", reason: "final result present" });
|
|
2558
|
+
continue;
|
|
2559
|
+
}
|
|
2560
|
+
if (!status.alive) {
|
|
2561
|
+
const nextStatus = status.attention.state === "blocked" ? "blocked" : status.status === "done" ? "done" : "exited";
|
|
2562
|
+
worker.status = nextStatus;
|
|
2563
|
+
worker.reconciledAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2564
|
+
worker.reconcileReason = status.attention.reason;
|
|
2565
|
+
saveWorker(run.id, worker);
|
|
2566
|
+
outcomes.push({
|
|
2567
|
+
runId: run.id,
|
|
2568
|
+
worker: name,
|
|
2569
|
+
action: "marked_exited",
|
|
2570
|
+
reason: status.attention.reason
|
|
2571
|
+
});
|
|
2572
|
+
continue;
|
|
2573
|
+
}
|
|
2574
|
+
if (status.attention.state === "stale" && worker.pid && isPidAlive(worker.pid)) {
|
|
2575
|
+
const hbMs = status.lastHeartbeatAt ? Date.parse(status.lastHeartbeatAt) : NaN;
|
|
2576
|
+
const actMs = status.lastActivityAt ? Date.parse(status.lastActivityAt) : NaN;
|
|
2577
|
+
const hbStale = !Number.isFinite(hbMs) || now - hbMs > STALE_RECONCILE_HEARTBEAT_MS;
|
|
2578
|
+
const actStale = Number.isFinite(actMs) && now - actMs > STALE_MS;
|
|
2579
|
+
if (hbStale && actStale) {
|
|
2580
|
+
killWorkerProcess(worker.pid, "SIGTERM");
|
|
2581
|
+
worker.status = "exited";
|
|
2582
|
+
worker.reconciledAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2583
|
+
worker.reconcileReason = `reconciled stale worker: ${status.attention.reason}`;
|
|
2584
|
+
saveWorker(run.id, worker);
|
|
2585
|
+
outcomes.push({
|
|
2586
|
+
runId: run.id,
|
|
2587
|
+
worker: name,
|
|
2588
|
+
action: "killed_stale",
|
|
2589
|
+
reason: status.attention.reason
|
|
2590
|
+
});
|
|
2591
|
+
continue;
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
outcomes.push({
|
|
2595
|
+
runId: run.id,
|
|
2596
|
+
worker: name,
|
|
2597
|
+
action: "skipped",
|
|
2598
|
+
reason: status.attention.reason
|
|
2599
|
+
});
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
return { workers: outcomes, finalizedRuns: finalizeStaleRuns() };
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2125
2605
|
// src/plan-progress-daemon-sync.ts
|
|
2126
|
-
import
|
|
2606
|
+
import path16 from "node:path";
|
|
2127
2607
|
|
|
2128
2608
|
// src/plan-progress-sync.ts
|
|
2129
2609
|
async function syncPlanProgress(args) {
|
|
2130
2610
|
const base = resolveBaseUrl(args.baseUrl);
|
|
2131
2611
|
const secret = await resolveCallbackSecretWithMint(args.secret, args.agentOsId, { baseUrl: base });
|
|
2132
2612
|
const url = `${base}/api/agent-os/by-id/${encodeURIComponent(args.agentOsId)}/tasks/${encodeURIComponent(args.taskId)}/plan-progress-sync`;
|
|
2133
|
-
const res = await
|
|
2613
|
+
const res = await postJsonWithCredentialRefresh(url, secret, {
|
|
2134
2614
|
phase: args.phase,
|
|
2135
2615
|
taskId: args.taskId,
|
|
2136
2616
|
blocker: args.blocker,
|
|
2137
2617
|
artifact: args.artifact
|
|
2138
|
-
});
|
|
2618
|
+
}, { agentOsId: args.agentOsId, baseUrl: base });
|
|
2139
2619
|
return { ok: res.ok, status: res.status, response: res.response };
|
|
2140
2620
|
}
|
|
2141
2621
|
|
|
@@ -2147,7 +2627,7 @@ async function syncActiveWorkerPlanProgress(runId, args) {
|
|
|
2147
2627
|
const outcomes = [];
|
|
2148
2628
|
for (const name of Object.keys(run.workers || {})) {
|
|
2149
2629
|
const worker = readJson(
|
|
2150
|
-
|
|
2630
|
+
path16.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
2151
2631
|
void 0
|
|
2152
2632
|
);
|
|
2153
2633
|
if (!worker?.dispatched || !worker.taskId) continue;
|
|
@@ -2201,7 +2681,7 @@ async function completeFinishedWorkers(runId, args) {
|
|
|
2201
2681
|
const outcomes = [];
|
|
2202
2682
|
for (const name of Object.keys(run.workers || {})) {
|
|
2203
2683
|
const worker = readJson(
|
|
2204
|
-
|
|
2684
|
+
path17.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
2205
2685
|
void 0
|
|
2206
2686
|
);
|
|
2207
2687
|
if (!worker?.taskId) continue;
|
|
@@ -2226,6 +2706,7 @@ async function postOperatorTick(agentOsId, runId, resourceGate, args) {
|
|
|
2226
2706
|
agentOsId,
|
|
2227
2707
|
runId,
|
|
2228
2708
|
ingestHarness: true,
|
|
2709
|
+
harnessBoardSnapshot: buildRunBoard(runId),
|
|
2229
2710
|
resourceGate
|
|
2230
2711
|
});
|
|
2231
2712
|
return { ok: res.ok, httpStatus: res.status, response: res.response };
|
|
@@ -2236,7 +2717,7 @@ async function runPipelineTick(args) {
|
|
|
2236
2717
|
const execute = args.execute !== false && args.execute !== "false";
|
|
2237
2718
|
runStatus({ run: runId });
|
|
2238
2719
|
const completedWorkers = await completeFinishedWorkers(runId, args);
|
|
2239
|
-
const
|
|
2720
|
+
const staleReconcile = reconcileStaleWorkers();
|
|
2240
2721
|
const planProgressSync = await syncActiveWorkerPlanProgress(runId, args);
|
|
2241
2722
|
const workspacePrefs = await fetchWorkspaceRuntimePreferences(agentOsId, args);
|
|
2242
2723
|
const resourceGate = observeRunnerResourceGate({
|
|
@@ -2277,7 +2758,7 @@ async function runPipelineTick(args) {
|
|
|
2277
2758
|
execute,
|
|
2278
2759
|
resourceGate,
|
|
2279
2760
|
completedWorkers,
|
|
2280
|
-
|
|
2761
|
+
staleReconcile,
|
|
2281
2762
|
planProgressSync,
|
|
2282
2763
|
operatorTick,
|
|
2283
2764
|
sweep,
|
|
@@ -2437,7 +2918,7 @@ function usage(code = 0) {
|
|
|
2437
2918
|
" kynver run create --repo /path/repo [--name name] [--base origin/main]",
|
|
2438
2919
|
" kynver run list",
|
|
2439
2920
|
" kynver run status --run RUN_ID",
|
|
2440
|
-
" 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 /]",
|
|
2921
|
+
" 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 /]",
|
|
2441
2922
|
" kynver run sweep --run RUN_ID --agent-os-id AOS_ID [--base-url URL] [--secret SECRET] [--grace-ms MS]",
|
|
2442
2923
|
' 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]',
|
|
2443
2924
|
" kynver worker status --run RUN_ID --name worker",
|
|
@@ -2495,6 +2976,7 @@ if (isCliEntry) {
|
|
|
2495
2976
|
}
|
|
2496
2977
|
export {
|
|
2497
2978
|
DEFAULT_DISPATCH_LEASE_MS,
|
|
2979
|
+
assessWorkerLanding,
|
|
2498
2980
|
autoCompleteWorker,
|
|
2499
2981
|
autoCompleteWorkerCli,
|
|
2500
2982
|
buildDispatchTaskText,
|
|
@@ -2507,6 +2989,7 @@ export {
|
|
|
2507
2989
|
dispatchRun,
|
|
2508
2990
|
getHarnessPaths,
|
|
2509
2991
|
isFinishedWorkerStatus,
|
|
2992
|
+
isLandingBlockedWorkerStatus,
|
|
2510
2993
|
listRuns,
|
|
2511
2994
|
loadUserConfig,
|
|
2512
2995
|
main,
|