@kynver-app/runtime 0.1.18 → 0.1.21
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 +559 -138
- package/dist/cli.js.map +4 -4
- package/dist/index.js +564 -141
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -10,6 +10,10 @@ function fail(message) {
|
|
|
10
10
|
console.error(message);
|
|
11
11
|
process.exit(1);
|
|
12
12
|
}
|
|
13
|
+
function hiddenSpawnOptions(opts) {
|
|
14
|
+
if (process.platform !== "win32") return opts;
|
|
15
|
+
return { windowsHide: true, ...opts };
|
|
16
|
+
}
|
|
13
17
|
function required(value, name) {
|
|
14
18
|
if (!value) fail(`missing ${name}`);
|
|
15
19
|
return value;
|
|
@@ -393,12 +397,12 @@ var DEFAULT_CRITICAL_FREE_BYTES = 15 * 1024 * 1024 * 1024;
|
|
|
393
397
|
var DEFAULT_MAX_USED_PERCENT = 80;
|
|
394
398
|
var DEFAULT_HARD_MAX_USED_PERCENT = 90;
|
|
395
399
|
function observeRunnerDiskGate(input = {}) {
|
|
396
|
-
const
|
|
400
|
+
const path18 = input.diskPath?.trim() || "/";
|
|
397
401
|
const warnBelowBytes = input.diskFreeWarnBytes ?? DEFAULT_WARN_FREE_BYTES;
|
|
398
402
|
const criticalBelowBytes = input.diskFreeCriticalBytes ?? DEFAULT_CRITICAL_FREE_BYTES;
|
|
399
403
|
const maxUsedPercent = input.diskMaxUsedPercent ?? DEFAULT_MAX_USED_PERCENT;
|
|
400
404
|
const hardMaxUsedPercent = input.diskHardMaxUsedPercent ?? DEFAULT_HARD_MAX_USED_PERCENT;
|
|
401
|
-
const stats = statfsSync(
|
|
405
|
+
const stats = statfsSync(path18);
|
|
402
406
|
const freeBytes = Number(stats.bavail) * Number(stats.bsize);
|
|
403
407
|
const totalBytes = Number(stats.blocks) * Number(stats.bsize);
|
|
404
408
|
const usedPercent = totalBytes > 0 ? (totalBytes - freeBytes) / totalBytes * 100 : 100;
|
|
@@ -418,7 +422,7 @@ function observeRunnerDiskGate(input = {}) {
|
|
|
418
422
|
}
|
|
419
423
|
return {
|
|
420
424
|
ok,
|
|
421
|
-
path:
|
|
425
|
+
path: path18,
|
|
422
426
|
freeBytes,
|
|
423
427
|
totalBytes,
|
|
424
428
|
usedPercent,
|
|
@@ -692,22 +696,36 @@ function gitIsAncestor(cwd, ancestor, descendant) {
|
|
|
692
696
|
if (res.status === 1) return { isAncestor: false, error: null };
|
|
693
697
|
return { isAncestor: null, error: res.error || res.stderr || res.stdout || `git exited ${res.status}` };
|
|
694
698
|
}
|
|
695
|
-
function computeGitAncestry(worktreePath,
|
|
699
|
+
function computeGitAncestry(worktreePath, baseOrOptions = "origin/main") {
|
|
700
|
+
const options = typeof baseOrOptions === "string" ? { base: baseOrOptions } : baseOrOptions;
|
|
701
|
+
const baseLabel = options.baseCommit?.trim() || options.base?.trim() || "origin/main";
|
|
702
|
+
const pinnedBaseCommit = options.baseCommit?.trim() || null;
|
|
696
703
|
if (!worktreePath) {
|
|
697
|
-
return unknownAncestry(
|
|
704
|
+
return unknownAncestry(baseLabel, "missing worktree path");
|
|
698
705
|
}
|
|
699
706
|
const head = gitCapture(worktreePath, ["rev-parse", "HEAD"]);
|
|
700
|
-
if (head.status !== 0)
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
707
|
+
if (head.status !== 0) {
|
|
708
|
+
return unknownAncestry(baseLabel, head.error || head.stderr || head.stdout || "failed to resolve HEAD");
|
|
709
|
+
}
|
|
710
|
+
let baseSha;
|
|
711
|
+
if (pinnedBaseCommit) {
|
|
712
|
+
baseSha = pinnedBaseCommit;
|
|
713
|
+
} else {
|
|
714
|
+
const baseHead = gitCapture(worktreePath, ["rev-parse", baseLabel]);
|
|
715
|
+
if (baseHead.status !== 0) {
|
|
716
|
+
return unknownAncestry(
|
|
717
|
+
baseLabel,
|
|
718
|
+
baseHead.error || baseHead.stderr || baseHead.stdout || `failed to resolve ${baseLabel}`,
|
|
719
|
+
head.stdout.trim()
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
baseSha = baseHead.stdout.trim();
|
|
704
723
|
}
|
|
705
724
|
const headSha = head.stdout.trim();
|
|
706
|
-
const baseSha = baseHead.stdout.trim();
|
|
707
725
|
if (headSha === baseSha) {
|
|
708
726
|
return {
|
|
709
727
|
checked: true,
|
|
710
|
-
base,
|
|
728
|
+
base: baseLabel,
|
|
711
729
|
head: headSha,
|
|
712
730
|
baseHead: baseSha,
|
|
713
731
|
baseIsAncestorOfHead: true,
|
|
@@ -721,7 +739,7 @@ function computeGitAncestry(worktreePath, base = "origin/main") {
|
|
|
721
739
|
if (baseIsAncestorOfHead.isAncestor == null || headIsAncestorOfBase.isAncestor == null) {
|
|
722
740
|
return {
|
|
723
741
|
checked: false,
|
|
724
|
-
base,
|
|
742
|
+
base: baseLabel,
|
|
725
743
|
head: headSha,
|
|
726
744
|
baseHead: baseSha,
|
|
727
745
|
baseIsAncestorOfHead: baseIsAncestorOfHead.isAncestor,
|
|
@@ -733,7 +751,7 @@ function computeGitAncestry(worktreePath, base = "origin/main") {
|
|
|
733
751
|
const relation = baseIsAncestorOfHead.isAncestor ? "ahead" : headIsAncestorOfBase.isAncestor ? "merged" : "diverged";
|
|
734
752
|
return {
|
|
735
753
|
checked: true,
|
|
736
|
-
base,
|
|
754
|
+
base: baseLabel,
|
|
737
755
|
head: headSha,
|
|
738
756
|
baseHead: baseSha,
|
|
739
757
|
baseIsAncestorOfHead: baseIsAncestorOfHead.isAncestor,
|
|
@@ -760,12 +778,74 @@ function scrubClaudeEnv(env) {
|
|
|
760
778
|
return next;
|
|
761
779
|
}
|
|
762
780
|
|
|
781
|
+
// src/landing-gate.ts
|
|
782
|
+
function trimOrNull(value) {
|
|
783
|
+
if (typeof value !== "string") return null;
|
|
784
|
+
const trimmed = value.trim();
|
|
785
|
+
return trimmed.length ? trimmed : null;
|
|
786
|
+
}
|
|
787
|
+
function hasFinalResult(value) {
|
|
788
|
+
if (value === void 0 || value === null) return false;
|
|
789
|
+
if (typeof value === "string") return value.trim().length > 0;
|
|
790
|
+
if (typeof value === "boolean") return value;
|
|
791
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
792
|
+
if (typeof value === "object") return Object.keys(value).length > 0;
|
|
793
|
+
return true;
|
|
794
|
+
}
|
|
795
|
+
function hasCommittedLandingRef(snapshot) {
|
|
796
|
+
if (trimOrNull(snapshot.headCommit)) return true;
|
|
797
|
+
if (trimOrNull(snapshot.prUrl)) return true;
|
|
798
|
+
if (trimOrNull(snapshot.artifactBundlePath)) return true;
|
|
799
|
+
if (trimOrNull(snapshot.patchPath)) return true;
|
|
800
|
+
const ancestry = snapshot.gitAncestry;
|
|
801
|
+
if (ancestry?.checked && ancestry.headIsAncestorOfBase === false && trimOrNull(ancestry.head)) {
|
|
802
|
+
return true;
|
|
803
|
+
}
|
|
804
|
+
return false;
|
|
805
|
+
}
|
|
806
|
+
function assessWorkerLanding(snapshot) {
|
|
807
|
+
if (!hasFinalResult(snapshot.finalResult)) return { blocked: false };
|
|
808
|
+
if (snapshot.changedFiles.length === 0) return { blocked: false };
|
|
809
|
+
if (!hasCommittedLandingRef(snapshot)) {
|
|
810
|
+
return {
|
|
811
|
+
blocked: true,
|
|
812
|
+
reason: "dirty_worktree_no_pr",
|
|
813
|
+
detail: `Worktree has ${snapshot.changedFiles.length} uncommitted change(s) with no commit or PR; commit, open a PR, or discard before landing`
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
return {
|
|
817
|
+
blocked: true,
|
|
818
|
+
detail: `Worktree has ${snapshot.changedFiles.length} uncommitted change(s); commit or discard before landing`
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
function landingAttentionReason(verdict) {
|
|
822
|
+
if (!verdict.blocked) return void 0;
|
|
823
|
+
return verdict.detail ?? verdict.reason ?? "dirty_worktree_no_pr";
|
|
824
|
+
}
|
|
825
|
+
|
|
763
826
|
// src/status.ts
|
|
764
827
|
var NO_START_MS = 18e4;
|
|
765
828
|
var STALE_MS = 6e5;
|
|
766
829
|
function computeAttention(input) {
|
|
767
830
|
const now = Date.now();
|
|
768
|
-
if (input.
|
|
831
|
+
if (input.completionBlocker) {
|
|
832
|
+
return { state: "blocked", reason: input.completionBlocker };
|
|
833
|
+
}
|
|
834
|
+
if (input.finalResult) {
|
|
835
|
+
const landing = assessWorkerLanding({
|
|
836
|
+
finalResult: input.finalResult,
|
|
837
|
+
changedFiles: input.changedFiles ?? [],
|
|
838
|
+
gitAncestry: input.gitAncestry ?? null
|
|
839
|
+
});
|
|
840
|
+
if (landing.blocked) {
|
|
841
|
+
const detail = landingAttentionReason(landing);
|
|
842
|
+
return {
|
|
843
|
+
state: "needs_attention",
|
|
844
|
+
reason: landing.reason ? `landing blocked (${landing.reason}): ${detail}` : `landing blocked: ${detail}`
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
return { state: "done", reason: "final result recorded" };
|
|
848
|
+
}
|
|
769
849
|
if (!input.alive) {
|
|
770
850
|
const classified = classifyExitFailure(input.error);
|
|
771
851
|
if (classified) return { state: "blocked", reason: classified.reason };
|
|
@@ -796,7 +876,10 @@ function computeWorkerStatus(worker, options = {}) {
|
|
|
796
876
|
const stderrBytes = fileSize(worker.stderrPath);
|
|
797
877
|
const heartbeatBytes = fileSize(worker.heartbeatPath);
|
|
798
878
|
const changedFiles = gitStatusShort(worker.worktreePath);
|
|
799
|
-
const gitAncestry = computeGitAncestry(worker.worktreePath,
|
|
879
|
+
const gitAncestry = computeGitAncestry(worker.worktreePath, {
|
|
880
|
+
base: options.base,
|
|
881
|
+
baseCommit: options.baseCommit
|
|
882
|
+
});
|
|
800
883
|
const lastActivityAt = latestIso([
|
|
801
884
|
parsed.lastEventAt,
|
|
802
885
|
heartbeat.lastHeartbeatAt,
|
|
@@ -805,6 +888,7 @@ function computeWorkerStatus(worker, options = {}) {
|
|
|
805
888
|
fileMtime(worker.heartbeatPath)
|
|
806
889
|
]);
|
|
807
890
|
const error = parsed.error || (!alive && !parsed.finalResult ? tailFile(worker.stderrPath, 10).trim() || void 0 : void 0);
|
|
891
|
+
const completionBlocker = typeof worker.completionBlocker === "string" && worker.completionBlocker.trim() ? worker.completionBlocker.trim() : null;
|
|
808
892
|
const attention = computeAttention({
|
|
809
893
|
alive,
|
|
810
894
|
finalResult: parsed.finalResult,
|
|
@@ -814,14 +898,18 @@ function computeWorkerStatus(worker, options = {}) {
|
|
|
814
898
|
lastActivityAt,
|
|
815
899
|
heartbeatBlocker: heartbeat.heartbeatBlocker,
|
|
816
900
|
startedAt: worker.startedAt,
|
|
817
|
-
error
|
|
901
|
+
error,
|
|
902
|
+
changedFiles,
|
|
903
|
+
gitAncestry,
|
|
904
|
+
completionBlocker
|
|
818
905
|
});
|
|
906
|
+
const workerStatusLabel = completionBlocker || attention.state === "blocked" ? "blocked" : attention.state === "done" ? "done" : parsed.finalResult ? "exited" : alive ? "running" : "exited";
|
|
819
907
|
return {
|
|
820
908
|
runId: worker.runId,
|
|
821
909
|
worker: worker.name,
|
|
822
910
|
pid: worker.pid,
|
|
823
911
|
alive,
|
|
824
|
-
status:
|
|
912
|
+
status: workerStatusLabel,
|
|
825
913
|
attention,
|
|
826
914
|
branch: worker.branch,
|
|
827
915
|
worktreePath: worker.worktreePath,
|
|
@@ -850,6 +938,10 @@ function isFinishedWorkerStatus(status) {
|
|
|
850
938
|
if (status.status === "exited" || status.status === "done") return true;
|
|
851
939
|
return false;
|
|
852
940
|
}
|
|
941
|
+
function isLandingBlockedWorkerStatus(status) {
|
|
942
|
+
if (!status.finalResult) return false;
|
|
943
|
+
return status.attention.state === "needs_attention" || status.attention.state === "blocked";
|
|
944
|
+
}
|
|
853
945
|
function deriveRunStatus(fallback, workers) {
|
|
854
946
|
if (workers.length === 0) return fallback;
|
|
855
947
|
if (workers.some((w) => w.attention === "needs_attention" || w.attention === "stale" || w.attention === "blocked")) {
|
|
@@ -900,6 +992,10 @@ function readAvailableMemBytes() {
|
|
|
900
992
|
}
|
|
901
993
|
return os.freemem();
|
|
902
994
|
}
|
|
995
|
+
function isActiveHarnessWorker(worker) {
|
|
996
|
+
const status = computeWorkerStatus(worker);
|
|
997
|
+
return status.alive && !status.finalResult && status.attention.state !== "done";
|
|
998
|
+
}
|
|
903
999
|
function countActiveWorkersForRun(run) {
|
|
904
1000
|
let active = 0;
|
|
905
1001
|
for (const name of Object.keys(run.workers || {})) {
|
|
@@ -907,11 +1003,8 @@ function countActiveWorkersForRun(run) {
|
|
|
907
1003
|
path5.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
908
1004
|
void 0
|
|
909
1005
|
);
|
|
910
|
-
if (!worker) continue;
|
|
911
|
-
|
|
912
|
-
if (status.alive && !status.finalResult && status.attention.state !== "done") {
|
|
913
|
-
active++;
|
|
914
|
-
}
|
|
1006
|
+
if (!worker || !isActiveHarnessWorker(worker)) continue;
|
|
1007
|
+
active++;
|
|
915
1008
|
}
|
|
916
1009
|
return active;
|
|
917
1010
|
}
|
|
@@ -936,7 +1029,7 @@ function observeRunnerResourceGate(input) {
|
|
|
936
1029
|
const maxConcurrentWorkers = Math.max(0, Math.min(targetCap, capacityFromTotal));
|
|
937
1030
|
const slotsByCapacity = Math.max(0, maxConcurrentWorkers - activeWorkers);
|
|
938
1031
|
const slotsByFreeMem = capacityFromFree;
|
|
939
|
-
|
|
1032
|
+
let slotsAvailable = Math.min(slotsByCapacity, slotsByFreeMem);
|
|
940
1033
|
let reason = null;
|
|
941
1034
|
if (slotsAvailable <= 0) {
|
|
942
1035
|
if (activeWorkers >= maxConcurrentWorkers) {
|
|
@@ -963,39 +1056,6 @@ function observeRunnerResourceGate(input) {
|
|
|
963
1056
|
};
|
|
964
1057
|
}
|
|
965
1058
|
|
|
966
|
-
// src/supervisor.ts
|
|
967
|
-
import { existsSync as existsSync9, mkdirSync as mkdirSync3 } from "node:fs";
|
|
968
|
-
import path9 from "node:path";
|
|
969
|
-
|
|
970
|
-
// src/prompt.ts
|
|
971
|
-
function buildPrompt(input) {
|
|
972
|
-
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.";
|
|
973
|
-
const progressLines = [
|
|
974
|
-
"Structured plan progress (required when planId is set):",
|
|
975
|
-
"- 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).",
|
|
976
|
-
"- 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.",
|
|
977
|
-
"- 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`).",
|
|
978
|
-
"- 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>`.",
|
|
979
|
-
"- Before the completion report: mark completion-report rows partial with evidence; do not skip report review.",
|
|
980
|
-
"- After implementation: wait for report_reviewer then deep_reviewer confirmation (via MCP/session agents) before follow-up rows close.",
|
|
981
|
-
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."
|
|
982
|
-
];
|
|
983
|
-
return [
|
|
984
|
-
"You are running under the Kynver AgentOS runtime.",
|
|
985
|
-
"Immediately state your plan before editing.",
|
|
986
|
-
ownership,
|
|
987
|
-
`Worktree: ${input.worktreePath}`,
|
|
988
|
-
`Progress heartbeat file: ${input.heartbeatPath}`,
|
|
989
|
-
"After each major step, append one JSON line to the heartbeat file with fields: ts, phase, summary, changedFiles, blocker.",
|
|
990
|
-
"Final response must include files changed, verification commands, and unresolved risks.",
|
|
991
|
-
"",
|
|
992
|
-
...progressLines,
|
|
993
|
-
"",
|
|
994
|
-
"Task:",
|
|
995
|
-
input.task
|
|
996
|
-
].join("\n");
|
|
997
|
-
}
|
|
998
|
-
|
|
999
1059
|
// src/providers/claude.ts
|
|
1000
1060
|
import { closeSync, openSync } from "node:fs";
|
|
1001
1061
|
import { spawn } from "node:child_process";
|
|
@@ -1054,7 +1114,7 @@ function preflightCursorModel(model, defaultModel) {
|
|
|
1054
1114
|
}
|
|
1055
1115
|
|
|
1056
1116
|
// src/providers/claude.ts
|
|
1057
|
-
var CLAUDE_DEFAULT_MODEL = "claude-
|
|
1117
|
+
var CLAUDE_DEFAULT_MODEL = "claude-sonnet-4-6";
|
|
1058
1118
|
var claudeProvider = {
|
|
1059
1119
|
name: "claude",
|
|
1060
1120
|
defaultModel: CLAUDE_DEFAULT_MODEL,
|
|
@@ -1083,12 +1143,12 @@ var claudeProvider = {
|
|
|
1083
1143
|
"--include-partial-messages",
|
|
1084
1144
|
opts.prompt
|
|
1085
1145
|
],
|
|
1086
|
-
{
|
|
1146
|
+
hiddenSpawnOptions({
|
|
1087
1147
|
cwd: opts.worktreePath,
|
|
1088
1148
|
detached: true,
|
|
1089
1149
|
stdio: ["ignore", stdoutFd, stderrFd],
|
|
1090
1150
|
env: scrubClaudeEnv(process.env)
|
|
1091
|
-
}
|
|
1151
|
+
})
|
|
1092
1152
|
);
|
|
1093
1153
|
closeSync(stdoutFd);
|
|
1094
1154
|
closeSync(stderrFd);
|
|
@@ -1100,34 +1160,234 @@ var claudeProvider = {
|
|
|
1100
1160
|
}
|
|
1101
1161
|
};
|
|
1102
1162
|
|
|
1163
|
+
// src/model-routing.ts
|
|
1164
|
+
var GLOBAL_DEFAULT_MODEL = "claude-sonnet-4-6";
|
|
1165
|
+
function taskString(task, key) {
|
|
1166
|
+
const v = task[key];
|
|
1167
|
+
return typeof v === "string" ? v.trim() : "";
|
|
1168
|
+
}
|
|
1169
|
+
function normalizeRef(ref) {
|
|
1170
|
+
return ref.toLowerCase();
|
|
1171
|
+
}
|
|
1172
|
+
function resolveGlobalDefaultModel(config = loadUserConfig()) {
|
|
1173
|
+
const fromConfig = config.defaultModel?.trim();
|
|
1174
|
+
if (fromConfig) return fromConfig;
|
|
1175
|
+
const fromEnv = process.env.KYNVER_DEFAULT_MODEL?.trim();
|
|
1176
|
+
if (fromEnv) return fromEnv;
|
|
1177
|
+
return GLOBAL_DEFAULT_MODEL;
|
|
1178
|
+
}
|
|
1179
|
+
function inferProviderFromModel(model) {
|
|
1180
|
+
const m = (model ?? "").toLowerCase();
|
|
1181
|
+
if (!m) return "claude";
|
|
1182
|
+
if (m.includes("composer") || m.includes("cursor") || m.includes("codex") || m.startsWith("gpt-") || m.startsWith("gpt5")) {
|
|
1183
|
+
return "cursor";
|
|
1184
|
+
}
|
|
1185
|
+
return "claude";
|
|
1186
|
+
}
|
|
1187
|
+
function isOpusLane(ref, title) {
|
|
1188
|
+
if (ref.includes("deep") && ref.includes("review")) return true;
|
|
1189
|
+
if (ref.includes("security")) return true;
|
|
1190
|
+
if (ref.includes("plan_author") || ref.includes("plan-author")) return true;
|
|
1191
|
+
if (title.includes("deep review") || title.includes("security review")) return true;
|
|
1192
|
+
if (ref.includes("plan") && !ref.includes("review") && (ref.includes("author") || ref.includes("strategy"))) {
|
|
1193
|
+
return true;
|
|
1194
|
+
}
|
|
1195
|
+
return false;
|
|
1196
|
+
}
|
|
1197
|
+
function inferModelRoutingFromTask(task) {
|
|
1198
|
+
const ref = normalizeRef(taskString(task, "executorRef"));
|
|
1199
|
+
const title = taskString(task, "title").toLowerCase();
|
|
1200
|
+
const priority = taskString(task, "priority") || "normal";
|
|
1201
|
+
const roleLane = normalizeRef(taskString(task, "roleLane"));
|
|
1202
|
+
if (ref.includes("cursor") || ref.includes("codex") || ref.includes("composer") || ref.includes("copilot") || roleLane === "implementer" || roleLane === "repair_implementer") {
|
|
1203
|
+
return { provider: "cursor", rule: "lane:implementation" };
|
|
1204
|
+
}
|
|
1205
|
+
if (ref.includes("landing") || title.startsWith("land:") || title.includes(" merge")) {
|
|
1206
|
+
return {
|
|
1207
|
+
model: "claude-haiku-4-5-20251001",
|
|
1208
|
+
provider: "claude",
|
|
1209
|
+
rule: "lane:landing"
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
if (ref.includes("review") || title.startsWith("review ") || roleLane.includes("review")) {
|
|
1213
|
+
if (isOpusLane(ref, title) || roleLane === "deep_reviewer") {
|
|
1214
|
+
return { model: "claude-opus-4-7", provider: "claude", rule: "lane:deep_review" };
|
|
1215
|
+
}
|
|
1216
|
+
return { model: "claude-sonnet-4-6", provider: "claude", rule: "lane:review" };
|
|
1217
|
+
}
|
|
1218
|
+
if (isOpusLane(ref, title) || roleLane === "plan_author") {
|
|
1219
|
+
return { model: "claude-opus-4-7", provider: "claude", rule: "lane:planning" };
|
|
1220
|
+
}
|
|
1221
|
+
if (priority === "critical") {
|
|
1222
|
+
return { model: "claude-opus-4-7", provider: "claude", rule: "priority:critical" };
|
|
1223
|
+
}
|
|
1224
|
+
if (priority === "low") {
|
|
1225
|
+
return {
|
|
1226
|
+
model: "claude-haiku-4-5-20251001",
|
|
1227
|
+
provider: "claude",
|
|
1228
|
+
rule: "priority:low"
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
return {
|
|
1232
|
+
model: resolveGlobalDefaultModel(),
|
|
1233
|
+
provider: "claude",
|
|
1234
|
+
rule: "default:sonnet"
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
function resolveWorkerLaunch(input) {
|
|
1238
|
+
if (input.explicitModel?.trim()) {
|
|
1239
|
+
const model2 = input.explicitModel.trim();
|
|
1240
|
+
return {
|
|
1241
|
+
model: model2,
|
|
1242
|
+
provider: input.explicitProvider?.trim() || inferProviderFromModel(model2),
|
|
1243
|
+
rule: "explicit:cli",
|
|
1244
|
+
requestedModel: model2
|
|
1245
|
+
};
|
|
1246
|
+
}
|
|
1247
|
+
if (input.task && Object.keys(input.task).length > 0) {
|
|
1248
|
+
const inferred = inferModelRoutingFromTask(input.task);
|
|
1249
|
+
return {
|
|
1250
|
+
...inferred,
|
|
1251
|
+
requestedModel: inferred.model
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
const model = resolveGlobalDefaultModel();
|
|
1255
|
+
return {
|
|
1256
|
+
model,
|
|
1257
|
+
provider: input.explicitProvider?.trim() || inferProviderFromModel(model),
|
|
1258
|
+
rule: "default:global",
|
|
1259
|
+
requestedModel: model
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
function resolveModelFallback(startedModel, launchModel, providerDefault) {
|
|
1263
|
+
return startedModel || launchModel || providerDefault || resolveGlobalDefaultModel() || CLAUDE_DEFAULT_MODEL;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// src/retry-limits.ts
|
|
1267
|
+
function positiveInt2(value, fallback) {
|
|
1268
|
+
const n = Number(value);
|
|
1269
|
+
if (!Number.isFinite(n) || n <= 0) return fallback;
|
|
1270
|
+
return Math.floor(n);
|
|
1271
|
+
}
|
|
1272
|
+
function readHarnessRetryLimits() {
|
|
1273
|
+
return {
|
|
1274
|
+
maxTaskAttempts: positiveInt2(process.env.KYNVER_MAX_TASK_ATTEMPTS, 3),
|
|
1275
|
+
dispatchCooldownMs: positiveInt2(process.env.KYNVER_DISPATCH_COOLDOWN_MS, 5e3)
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
// src/supervisor.ts
|
|
1280
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync3 } from "node:fs";
|
|
1281
|
+
import path10 from "node:path";
|
|
1282
|
+
|
|
1283
|
+
// src/prompt.ts
|
|
1284
|
+
function buildPrompt(input) {
|
|
1285
|
+
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.";
|
|
1286
|
+
const compact = Boolean(input.model?.toLowerCase().includes("haiku"));
|
|
1287
|
+
const progressLines = compact ? [
|
|
1288
|
+
"Plan progress: when planId is set, use `kynver plan progress` for running|partial|blocked only; row `done` is MCP/session only.",
|
|
1289
|
+
input.planId ? `Active planId: ${input.planId}` : "No planId on this worker."
|
|
1290
|
+
] : [
|
|
1291
|
+
"Structured plan progress (required when planId is set):",
|
|
1292
|
+
"- 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).",
|
|
1293
|
+
"- 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.",
|
|
1294
|
+
"- 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`).",
|
|
1295
|
+
"- 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>`.",
|
|
1296
|
+
"- Before the completion report: mark completion-report rows partial with evidence; do not skip report review.",
|
|
1297
|
+
"- After implementation: wait for report_reviewer then deep_reviewer confirmation (via MCP/session agents) before follow-up rows close.",
|
|
1298
|
+
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."
|
|
1299
|
+
];
|
|
1300
|
+
return [
|
|
1301
|
+
"You are running under the Kynver AgentOS runtime.",
|
|
1302
|
+
"Immediately state your plan before editing.",
|
|
1303
|
+
ownership,
|
|
1304
|
+
`Worktree: ${input.worktreePath}`,
|
|
1305
|
+
`Progress heartbeat file: ${input.heartbeatPath}`,
|
|
1306
|
+
"After each major step, append one JSON line to the heartbeat file with fields: ts, phase, summary, changedFiles, blocker.",
|
|
1307
|
+
"Final response must include files changed, verification commands, and unresolved risks.",
|
|
1308
|
+
"",
|
|
1309
|
+
...progressLines,
|
|
1310
|
+
"",
|
|
1311
|
+
"Task:",
|
|
1312
|
+
input.task
|
|
1313
|
+
].join("\n");
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1103
1316
|
// src/providers/cursor.ts
|
|
1104
|
-
import { closeSync as closeSync2, existsSync as
|
|
1317
|
+
import { closeSync as closeSync2, existsSync as existsSync8, openSync as openSync2 } from "node:fs";
|
|
1105
1318
|
import { spawn as spawn2 } from "node:child_process";
|
|
1319
|
+
import path7 from "node:path";
|
|
1320
|
+
|
|
1321
|
+
// src/providers/cursor-windows.ts
|
|
1322
|
+
import { existsSync as existsSync7, readdirSync as readdirSync3 } from "node:fs";
|
|
1106
1323
|
import path6 from "node:path";
|
|
1107
|
-
var
|
|
1108
|
-
function
|
|
1324
|
+
var CURSOR_VERSION_DIR = /^\d{4}\.\d{1,2}\.\d{1,2}-[a-f0-9]+$/i;
|
|
1325
|
+
function parseCursorVersionSortKey(versionName) {
|
|
1326
|
+
const datePart = versionName.split("-")[0];
|
|
1327
|
+
const parts = datePart.split(".");
|
|
1328
|
+
if (parts.length !== 3) return null;
|
|
1329
|
+
const [year, month, day] = parts;
|
|
1330
|
+
if (!year || !month || !day) return null;
|
|
1331
|
+
return Number(`${year}${month.padStart(2, "0")}${day.padStart(2, "0")}`);
|
|
1332
|
+
}
|
|
1333
|
+
function pickLatestCursorVersionDir(agentRoot) {
|
|
1334
|
+
const versionsRoot = path6.join(agentRoot, "versions");
|
|
1109
1335
|
if (!existsSync7(versionsRoot)) return null;
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1336
|
+
let bestDir = null;
|
|
1337
|
+
let bestKey = -1;
|
|
1338
|
+
for (const entry of readdirSync3(versionsRoot, { withFileTypes: true })) {
|
|
1339
|
+
if (!entry.isDirectory() || !CURSOR_VERSION_DIR.test(entry.name)) continue;
|
|
1340
|
+
const key = parseCursorVersionSortKey(entry.name);
|
|
1341
|
+
if (key == null || key <= bestKey) continue;
|
|
1342
|
+
bestKey = key;
|
|
1343
|
+
bestDir = path6.join(versionsRoot, entry.name);
|
|
1344
|
+
}
|
|
1345
|
+
return bestDir;
|
|
1346
|
+
}
|
|
1347
|
+
function resolveWindowsCursorBundled(agentRoot) {
|
|
1348
|
+
const root = agentRoot?.trim() || path6.join(process.env.LOCALAPPDATA || "", "cursor-agent");
|
|
1349
|
+
const directNode = path6.join(root, "node.exe");
|
|
1350
|
+
const directIndex = path6.join(root, "index.js");
|
|
1351
|
+
if (existsSync7(directNode) && existsSync7(directIndex)) {
|
|
1352
|
+
return { nodeExe: directNode, indexJs: directIndex, versionDir: root };
|
|
1353
|
+
}
|
|
1354
|
+
const versionDir = pickLatestCursorVersionDir(root);
|
|
1355
|
+
if (!versionDir) return null;
|
|
1114
1356
|
const nodeExe = path6.join(versionDir, "node.exe");
|
|
1115
1357
|
const indexJs = path6.join(versionDir, "index.js");
|
|
1116
1358
|
if (!existsSync7(nodeExe) || !existsSync7(indexJs)) return null;
|
|
1117
|
-
return {
|
|
1359
|
+
return { nodeExe, indexJs, versionDir };
|
|
1118
1360
|
}
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1361
|
+
|
|
1362
|
+
// src/providers/cursor.ts
|
|
1363
|
+
var DEFAULT_CURSOR_MODEL = "composer-2.5";
|
|
1364
|
+
function bundledSpawnTarget(nodeExe, indexJs, versionDir) {
|
|
1365
|
+
return {
|
|
1366
|
+
executable: nodeExe,
|
|
1367
|
+
prefixArgs: [indexJs],
|
|
1368
|
+
shell: false,
|
|
1369
|
+
detached: true,
|
|
1370
|
+
bundledVersionDir: versionDir
|
|
1371
|
+
};
|
|
1125
1372
|
}
|
|
1126
1373
|
function resolveCursorSpawn(agentBin) {
|
|
1127
|
-
if (process.platform === "win32"
|
|
1128
|
-
const
|
|
1129
|
-
|
|
1130
|
-
|
|
1374
|
+
if (process.platform === "win32") {
|
|
1375
|
+
const isCursorWrapper = /\.(cmd|bat)$/i.test(agentBin);
|
|
1376
|
+
const isBundledNode = /node\.exe$/i.test(agentBin) && existsSync8(path7.join(path7.dirname(agentBin), "index.js"));
|
|
1377
|
+
const isDefaultShim = agentBin === "agent";
|
|
1378
|
+
if (isCursorWrapper || isBundledNode || isDefaultShim) {
|
|
1379
|
+
const bundled = isCursorWrapper ? resolveWindowsCursorBundled(path7.dirname(agentBin)) : isBundledNode ? {
|
|
1380
|
+
nodeExe: agentBin,
|
|
1381
|
+
indexJs: path7.join(path7.dirname(agentBin), "index.js"),
|
|
1382
|
+
versionDir: path7.dirname(agentBin)
|
|
1383
|
+
} : resolveWindowsCursorBundled();
|
|
1384
|
+
if (bundled) {
|
|
1385
|
+
return bundledSpawnTarget(bundled.nodeExe, bundled.indexJs, bundled.versionDir);
|
|
1386
|
+
}
|
|
1387
|
+
throw new Error(
|
|
1388
|
+
"Cursor Agent on Windows has no headless bundled node.exe under %LOCALAPPDATA%\\cursor-agent\\versions\\\u2026. Run `agent login` / update Cursor Agent CLI, use `--provider claude`, or set KYNVER_CURSOR_AGENT_ROOT to the cursor-agent folder."
|
|
1389
|
+
);
|
|
1390
|
+
}
|
|
1131
1391
|
}
|
|
1132
1392
|
return { executable: agentBin, prefixArgs: [], shell: false, detached: true };
|
|
1133
1393
|
}
|
|
@@ -1135,11 +1395,23 @@ function resolveAgentBin() {
|
|
|
1135
1395
|
const configured = process.env.KYNVER_CURSOR_AGENT_BIN?.trim() || process.env.CURSOR_AGENT_BIN?.trim();
|
|
1136
1396
|
if (configured) return configured;
|
|
1137
1397
|
if (process.platform === "win32") {
|
|
1138
|
-
const
|
|
1139
|
-
|
|
1398
|
+
const bundled = resolveWindowsCursorBundled(
|
|
1399
|
+
process.env.KYNVER_CURSOR_AGENT_ROOT?.trim() || void 0
|
|
1400
|
+
);
|
|
1401
|
+
if (bundled) return bundled.nodeExe;
|
|
1402
|
+
const localAgent = path7.join(process.env.LOCALAPPDATA || "", "cursor-agent", "agent.cmd");
|
|
1403
|
+
if (existsSync8(localAgent)) return localAgent;
|
|
1140
1404
|
}
|
|
1141
1405
|
return "agent";
|
|
1142
1406
|
}
|
|
1407
|
+
function cursorWorkerEnv(agentBin, spawnTarget) {
|
|
1408
|
+
return {
|
|
1409
|
+
...process.env,
|
|
1410
|
+
CI: "1",
|
|
1411
|
+
NO_COLOR: "1",
|
|
1412
|
+
...spawnTarget.bundledVersionDir ? { CURSOR_INVOKED_AS: path7.basename(agentBin) || "agent.cmd" } : {}
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1143
1415
|
var cursorProvider = {
|
|
1144
1416
|
name: "cursor",
|
|
1145
1417
|
defaultModel: DEFAULT_CURSOR_MODEL,
|
|
@@ -1172,16 +1444,13 @@ var cursorProvider = {
|
|
|
1172
1444
|
model,
|
|
1173
1445
|
opts.prompt
|
|
1174
1446
|
],
|
|
1175
|
-
{
|
|
1447
|
+
hiddenSpawnOptions({
|
|
1176
1448
|
cwd: opts.worktreePath,
|
|
1177
1449
|
detached: spawnTarget.detached,
|
|
1178
1450
|
shell: spawnTarget.shell,
|
|
1179
1451
|
stdio: ["ignore", stdoutFd, stderrFd],
|
|
1180
|
-
env:
|
|
1181
|
-
|
|
1182
|
-
...spawnTarget.prefixArgs.length > 0 ? { CURSOR_INVOKED_AS: path6.basename(agentBin) } : {}
|
|
1183
|
-
}
|
|
1184
|
-
}
|
|
1452
|
+
env: cursorWorkerEnv(agentBin, spawnTarget)
|
|
1453
|
+
})
|
|
1185
1454
|
);
|
|
1186
1455
|
closeSync2(stdoutFd);
|
|
1187
1456
|
closeSync2(stderrFd);
|
|
@@ -1213,12 +1482,12 @@ function resolveWorkerProvider(name) {
|
|
|
1213
1482
|
|
|
1214
1483
|
// src/auto-complete.ts
|
|
1215
1484
|
import { spawn as spawn3 } from "node:child_process";
|
|
1216
|
-
import { existsSync as
|
|
1217
|
-
import
|
|
1485
|
+
import { existsSync as existsSync9, openSync as openSync3, closeSync as closeSync3 } from "node:fs";
|
|
1486
|
+
import path9 from "node:path";
|
|
1218
1487
|
import { fileURLToPath } from "node:url";
|
|
1219
1488
|
|
|
1220
1489
|
// src/worker-ops.ts
|
|
1221
|
-
import
|
|
1490
|
+
import path8 from "node:path";
|
|
1222
1491
|
async function postCompletion(url, secret, body) {
|
|
1223
1492
|
const res = await fetch(url, {
|
|
1224
1493
|
method: "POST",
|
|
@@ -1247,9 +1516,13 @@ function persistCompletionBlocker(worker, reason) {
|
|
|
1247
1516
|
else delete worker.completionBlocker;
|
|
1248
1517
|
saveWorker(worker.runId, worker);
|
|
1249
1518
|
}
|
|
1519
|
+
function workerStatusOptions(run) {
|
|
1520
|
+
return run ? { base: run.base, baseCommit: run.baseCommit } : {};
|
|
1521
|
+
}
|
|
1250
1522
|
async function tryCompleteWorker(args) {
|
|
1251
1523
|
const worker = loadWorker(String(args.run), String(args.name));
|
|
1252
|
-
const
|
|
1524
|
+
const run = loadRun(worker.runId);
|
|
1525
|
+
const status = computeWorkerStatus(worker, workerStatusOptions(run));
|
|
1253
1526
|
const agentOsId = (args.agentOsId ? String(args.agentOsId) : worker.agentOsId) || "";
|
|
1254
1527
|
const taskId = (args.taskId ? String(args.taskId) : worker.taskId) || null;
|
|
1255
1528
|
if (!agentOsId) {
|
|
@@ -1293,7 +1566,8 @@ async function tryCompleteWorker(args) {
|
|
|
1293
1566
|
async function completeWorker(args) {
|
|
1294
1567
|
try {
|
|
1295
1568
|
const worker = loadWorker(String(args.run), String(args.name));
|
|
1296
|
-
const
|
|
1569
|
+
const run = loadRun(worker.runId);
|
|
1570
|
+
const status = computeWorkerStatus(worker, workerStatusOptions(run));
|
|
1297
1571
|
const agentOsId = (args.agentOsId ? String(args.agentOsId) : worker.agentOsId) || "";
|
|
1298
1572
|
const taskId = (args.taskId ? String(args.taskId) : worker.taskId) || null;
|
|
1299
1573
|
if (!agentOsId) {
|
|
@@ -1340,8 +1614,9 @@ async function completeWorker(args) {
|
|
|
1340
1614
|
}
|
|
1341
1615
|
function workerStatus(args) {
|
|
1342
1616
|
const worker = loadWorker(String(args.run), String(args.name));
|
|
1343
|
-
const
|
|
1344
|
-
|
|
1617
|
+
const run = loadRun(worker.runId);
|
|
1618
|
+
const status = computeWorkerStatus(worker, workerStatusOptions(run));
|
|
1619
|
+
writeJson(path8.join(worker.workerDir, "last-status.json"), status);
|
|
1345
1620
|
console.log(JSON.stringify(status, null, 2));
|
|
1346
1621
|
}
|
|
1347
1622
|
function runStatus(args) {
|
|
@@ -1349,20 +1624,27 @@ function runStatus(args) {
|
|
|
1349
1624
|
const names = Object.keys(run.workers || {});
|
|
1350
1625
|
const workers = names.map((name) => {
|
|
1351
1626
|
const worker = readJson(
|
|
1352
|
-
|
|
1627
|
+
path8.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
1353
1628
|
void 0
|
|
1354
1629
|
);
|
|
1355
1630
|
if (!worker) {
|
|
1356
1631
|
return { worker: name, status: "missing", attention: "needs_attention", attentionReason: "worker.json not found" };
|
|
1357
1632
|
}
|
|
1358
|
-
const status = computeWorkerStatus(worker, {
|
|
1633
|
+
const status = computeWorkerStatus(worker, {
|
|
1634
|
+
base: run.base,
|
|
1635
|
+
baseCommit: run.baseCommit
|
|
1636
|
+
});
|
|
1637
|
+
const headCommit = status.gitAncestry.headIsAncestorOfBase === false && status.gitAncestry.head ? status.gitAncestry.head : void 0;
|
|
1359
1638
|
const rawBlocker = worker.completionBlocker;
|
|
1360
1639
|
const completionBlocker = typeof rawBlocker === "string" && rawBlocker ? rawBlocker : void 0;
|
|
1640
|
+
const boardStatus = completionBlocker ? "blocked" : status.status;
|
|
1641
|
+
const boardAttention = completionBlocker ? "blocked" : status.attention.state;
|
|
1361
1642
|
return {
|
|
1362
1643
|
worker: status.worker,
|
|
1363
|
-
status:
|
|
1364
|
-
attention:
|
|
1644
|
+
status: boardStatus,
|
|
1645
|
+
attention: boardAttention,
|
|
1365
1646
|
attentionReason: completionBlocker ?? status.attention.reason,
|
|
1647
|
+
landingBlocked: status.finalResult ? boardAttention === "needs_attention" || boardAttention === "blocked" : false,
|
|
1366
1648
|
pid: status.pid,
|
|
1367
1649
|
alive: status.alive,
|
|
1368
1650
|
currentTool: status.currentTool,
|
|
@@ -1371,7 +1653,14 @@ function runStatus(args) {
|
|
|
1371
1653
|
lastHeartbeatSummary: status.lastHeartbeatSummary,
|
|
1372
1654
|
heartbeatBlocker: status.heartbeatBlocker,
|
|
1373
1655
|
changedFileCount: status.changedFiles.length,
|
|
1656
|
+
changedFiles: status.changedFiles,
|
|
1374
1657
|
branch: status.branch,
|
|
1658
|
+
model: typeof worker.model === "string" ? worker.model : void 0,
|
|
1659
|
+
routingRule: typeof worker.routingRule === "string" ? worker.routingRule : void 0,
|
|
1660
|
+
requestedModel: typeof worker.requestedModel === "string" ? worker.requestedModel : void 0,
|
|
1661
|
+
headCommit,
|
|
1662
|
+
gitAncestry: status.gitAncestry,
|
|
1663
|
+
finalResult: status.finalResult,
|
|
1375
1664
|
ancestry: status.gitAncestry.relation,
|
|
1376
1665
|
ancestryChecked: status.gitAncestry.checked
|
|
1377
1666
|
};
|
|
@@ -1385,7 +1674,7 @@ function runStatus(args) {
|
|
|
1385
1674
|
needsAttention: workers.filter((w) => w.attention && w.attention !== "ok" && w.attention !== "done").map((w) => w.worker),
|
|
1386
1675
|
workers
|
|
1387
1676
|
};
|
|
1388
|
-
writeJson(
|
|
1677
|
+
writeJson(path8.join(runDirectory(run.id), "last-board.json"), board);
|
|
1389
1678
|
console.log(JSON.stringify(board, null, 2));
|
|
1390
1679
|
}
|
|
1391
1680
|
function tailWorker(args) {
|
|
@@ -1524,12 +1813,12 @@ async function autoCompleteWorkerCli(raw) {
|
|
|
1524
1813
|
}
|
|
1525
1814
|
}
|
|
1526
1815
|
function resolveDefaultCliPath() {
|
|
1527
|
-
return
|
|
1816
|
+
return path9.join(fileURLToPath(new URL(".", import.meta.url)), "cli.js");
|
|
1528
1817
|
}
|
|
1529
1818
|
function spawnCompletionSidecar(opts) {
|
|
1530
1819
|
const cliPath = opts.cliPath ?? resolveDefaultCliPath();
|
|
1531
|
-
if (!
|
|
1532
|
-
const logPath =
|
|
1820
|
+
if (!existsSync9(cliPath)) return void 0;
|
|
1821
|
+
const logPath = path9.join(opts.workerDir, "auto-complete.log");
|
|
1533
1822
|
let logFd;
|
|
1534
1823
|
try {
|
|
1535
1824
|
logFd = openSync3(logPath, "a");
|
|
@@ -1555,11 +1844,15 @@ function spawnCompletionSidecar(opts) {
|
|
|
1555
1844
|
if (opts.baseUrl) args.push("--base-url", opts.baseUrl);
|
|
1556
1845
|
if (opts.secret) args.push("--secret", opts.secret);
|
|
1557
1846
|
try {
|
|
1558
|
-
const child = spawn3(
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1847
|
+
const child = spawn3(
|
|
1848
|
+
nodeExecutable,
|
|
1849
|
+
args,
|
|
1850
|
+
hiddenSpawnOptions({
|
|
1851
|
+
detached: true,
|
|
1852
|
+
stdio,
|
|
1853
|
+
env: process.env
|
|
1854
|
+
})
|
|
1855
|
+
);
|
|
1563
1856
|
if (logFd !== void 0) closeSync3(logFd);
|
|
1564
1857
|
child.unref();
|
|
1565
1858
|
return { pid: child.pid, logPath, cliPath };
|
|
@@ -1583,8 +1876,17 @@ function spawnWorkerProcess(run, opts) {
|
|
|
1583
1876
|
const name = safeSlug(rawName);
|
|
1584
1877
|
if (run.workers?.[name]) throw new Error(`worker already exists in run ${run.id}: ${name}`);
|
|
1585
1878
|
if (!opts.task) throw new Error(`missing task text for worker ${name}`);
|
|
1586
|
-
const
|
|
1587
|
-
|
|
1879
|
+
const routing = opts.routingRule || opts.requestedModel ? {
|
|
1880
|
+
provider: opts.provider || "claude",
|
|
1881
|
+
model: opts.model,
|
|
1882
|
+
rule: opts.routingRule || "explicit:spawn",
|
|
1883
|
+
requestedModel: opts.requestedModel ?? opts.model
|
|
1884
|
+
} : resolveWorkerLaunch({
|
|
1885
|
+
explicitModel: opts.model,
|
|
1886
|
+
explicitProvider: opts.provider
|
|
1887
|
+
});
|
|
1888
|
+
const provider = resolveWorkerProvider(routing.provider);
|
|
1889
|
+
let launchModel = routing.model;
|
|
1588
1890
|
if (provider.preflightModel) {
|
|
1589
1891
|
const preflight = provider.preflightModel(opts.model);
|
|
1590
1892
|
if (!preflight.ok) {
|
|
@@ -1598,21 +1900,24 @@ function spawnWorkerProcess(run, opts) {
|
|
|
1598
1900
|
launchModel = preflight.model;
|
|
1599
1901
|
}
|
|
1600
1902
|
const { worktreesDir } = getPaths();
|
|
1601
|
-
const workerDir =
|
|
1903
|
+
const workerDir = path10.join(runDirectory(run.id), "workers", name);
|
|
1602
1904
|
mkdirSync3(workerDir, { recursive: true });
|
|
1603
|
-
const worktreePath =
|
|
1905
|
+
const worktreePath = path10.join(worktreesDir, run.id, name);
|
|
1604
1906
|
const branch = opts.branch || `agent/${run.id}/${name}`;
|
|
1605
|
-
if (
|
|
1907
|
+
if (existsSync10(worktreePath)) throw new Error(`worktree path already exists: ${worktreePath}`);
|
|
1606
1908
|
git(run.repo, ["fetch", "origin", "--prune"], { allowFailure: true });
|
|
1607
1909
|
git(run.repo, ["worktree", "add", "-b", branch, worktreePath, run.baseCommit], { throwError: true });
|
|
1608
|
-
const stdoutPath =
|
|
1609
|
-
const stderrPath =
|
|
1610
|
-
const heartbeatPath =
|
|
1910
|
+
const stdoutPath = path10.join(workerDir, "stdout.jsonl");
|
|
1911
|
+
const stderrPath = path10.join(workerDir, "stderr.log");
|
|
1912
|
+
const heartbeatPath = path10.join(workerDir, "heartbeat.jsonl");
|
|
1611
1913
|
const prompt = buildPrompt({
|
|
1612
1914
|
task: opts.task,
|
|
1613
1915
|
ownedPaths: opts.ownedPaths || [],
|
|
1614
1916
|
worktreePath,
|
|
1615
|
-
heartbeatPath
|
|
1917
|
+
heartbeatPath,
|
|
1918
|
+
planId: opts.planId,
|
|
1919
|
+
taskId: opts.taskId,
|
|
1920
|
+
model: launchModel
|
|
1616
1921
|
});
|
|
1617
1922
|
let started;
|
|
1618
1923
|
try {
|
|
@@ -1634,7 +1939,7 @@ function spawnWorkerProcess(run, opts) {
|
|
|
1634
1939
|
git(run.repo, ["branch", "-D", branch], { allowFailure: true });
|
|
1635
1940
|
throw error;
|
|
1636
1941
|
}
|
|
1637
|
-
const model = started.model
|
|
1942
|
+
const model = resolveModelFallback(started.model, launchModel, provider.defaultModel);
|
|
1638
1943
|
const worker = {
|
|
1639
1944
|
name,
|
|
1640
1945
|
runId: run.id,
|
|
@@ -1653,21 +1958,32 @@ function spawnWorkerProcess(run, opts) {
|
|
|
1653
1958
|
...opts.planId ? { planId: String(opts.planId) } : {},
|
|
1654
1959
|
...opts.leaseOwner ? { leaseOwner: String(opts.leaseOwner) } : {},
|
|
1655
1960
|
...opts.dispatched ? { dispatched: true } : {},
|
|
1961
|
+
routingRule: routing.rule,
|
|
1962
|
+
...routing.requestedModel ? { requestedModel: routing.requestedModel } : {},
|
|
1656
1963
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1657
1964
|
};
|
|
1658
1965
|
saveWorker(run.id, worker);
|
|
1659
|
-
run.workers = { ...run.workers || {}, [name]: { workerDir, statusPath:
|
|
1966
|
+
run.workers = { ...run.workers || {}, [name]: { workerDir, statusPath: path10.join(workerDir, "worker.json") } };
|
|
1660
1967
|
run.status = "running";
|
|
1661
1968
|
saveRun(run);
|
|
1662
1969
|
if (worker.agentOsId && worker.taskId) {
|
|
1970
|
+
let sidecarSpawned;
|
|
1663
1971
|
try {
|
|
1664
|
-
spawnCompletionSidecar({
|
|
1972
|
+
sidecarSpawned = spawnCompletionSidecar({
|
|
1665
1973
|
runId: run.id,
|
|
1666
1974
|
workerName: name,
|
|
1667
1975
|
workerDir,
|
|
1668
1976
|
agentOsId: worker.agentOsId
|
|
1669
1977
|
});
|
|
1670
|
-
} catch {
|
|
1978
|
+
} catch (error) {
|
|
1979
|
+
const reason = `completion sidecar failed to spawn: ${error.message}`;
|
|
1980
|
+
worker.completionBlocker = reason;
|
|
1981
|
+
saveWorker(run.id, worker);
|
|
1982
|
+
}
|
|
1983
|
+
if (!sidecarSpawned) {
|
|
1984
|
+
const reason = "completion sidecar failed to spawn (CLI not found or spawn error)";
|
|
1985
|
+
worker.completionBlocker = reason;
|
|
1986
|
+
saveWorker(run.id, worker);
|
|
1671
1987
|
}
|
|
1672
1988
|
}
|
|
1673
1989
|
return worker;
|
|
@@ -1802,17 +2118,34 @@ async function dispatchRun(args) {
|
|
|
1802
2118
|
console.log(JSON.stringify(summary2, null, 2));
|
|
1803
2119
|
return;
|
|
1804
2120
|
}
|
|
2121
|
+
const retryLimits = readHarnessRetryLimits();
|
|
1805
2122
|
const outcomes = [];
|
|
1806
2123
|
for (const decision of result.started) {
|
|
1807
2124
|
const task = decision.task;
|
|
2125
|
+
const attempt = Number(task.attempt) || 1;
|
|
2126
|
+
if (attempt > retryLimits.maxTaskAttempts) {
|
|
2127
|
+
outcomes.push({
|
|
2128
|
+
taskId: task.id,
|
|
2129
|
+
started: false,
|
|
2130
|
+
error: `task attempt ${attempt} exceeds KYNVER_MAX_TASK_ATTEMPTS (${retryLimits.maxTaskAttempts})`
|
|
2131
|
+
});
|
|
2132
|
+
continue;
|
|
2133
|
+
}
|
|
1808
2134
|
const name = safeSlug(`t-${task.id}-a${task.attempt}`);
|
|
2135
|
+
const routing = resolveWorkerLaunch({
|
|
2136
|
+
explicitModel: args.model ? String(args.model) : void 0,
|
|
2137
|
+
task
|
|
2138
|
+
});
|
|
1809
2139
|
try {
|
|
1810
2140
|
const planId = task.planId ? String(task.planId) : void 0;
|
|
1811
2141
|
const worker = spawnWorkerProcess(run, {
|
|
1812
2142
|
name,
|
|
1813
2143
|
task: buildDispatchTaskText(task, agentOsId),
|
|
1814
2144
|
ownedPaths: args.owned ? String(args.owned).split(",").map((s) => s.trim()).filter(Boolean) : [],
|
|
1815
|
-
model:
|
|
2145
|
+
model: routing.model,
|
|
2146
|
+
provider: routing.provider,
|
|
2147
|
+
routingRule: routing.rule,
|
|
2148
|
+
requestedModel: routing.requestedModel,
|
|
1816
2149
|
agentOsId,
|
|
1817
2150
|
taskId: String(task.id),
|
|
1818
2151
|
planId,
|
|
@@ -1824,7 +2157,10 @@ async function dispatchRun(args) {
|
|
|
1824
2157
|
started: true,
|
|
1825
2158
|
worker: worker.name,
|
|
1826
2159
|
pid: worker.pid,
|
|
1827
|
-
branch: worker.branch
|
|
2160
|
+
branch: worker.branch,
|
|
2161
|
+
model: worker.model,
|
|
2162
|
+
provider: routing.provider,
|
|
2163
|
+
routingRule: routing.rule
|
|
1828
2164
|
});
|
|
1829
2165
|
} catch (error) {
|
|
1830
2166
|
const releaseUrl = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/tasks/${encodeURIComponent(String(task.id))}/release`;
|
|
@@ -1879,7 +2215,7 @@ function redactHarness(text, secret) {
|
|
|
1879
2215
|
}
|
|
1880
2216
|
|
|
1881
2217
|
// src/validate.ts
|
|
1882
|
-
import
|
|
2218
|
+
import path11 from "node:path";
|
|
1883
2219
|
var RUN_ID_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/i;
|
|
1884
2220
|
var WORKER_NAME_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/i;
|
|
1885
2221
|
function validateRunId(runId) {
|
|
@@ -1893,15 +2229,15 @@ function validateWorkerName(name) {
|
|
|
1893
2229
|
return trimmed;
|
|
1894
2230
|
}
|
|
1895
2231
|
function validateRepo(repo) {
|
|
1896
|
-
const resolved =
|
|
2232
|
+
const resolved = path11.resolve(repo);
|
|
1897
2233
|
if (resolved.includes("..")) throw new Error("repo path must not contain .. segments");
|
|
1898
2234
|
return resolved;
|
|
1899
2235
|
}
|
|
1900
2236
|
function validateOwnedPaths(repoRoot, ownedPaths) {
|
|
1901
2237
|
return ownedPaths.map((owned) => {
|
|
1902
|
-
const resolved =
|
|
1903
|
-
const rel =
|
|
1904
|
-
if (rel.startsWith("..") ||
|
|
2238
|
+
const resolved = path11.resolve(repoRoot, owned);
|
|
2239
|
+
const rel = path11.relative(repoRoot, resolved);
|
|
2240
|
+
if (rel.startsWith("..") || path11.isAbsolute(rel)) {
|
|
1905
2241
|
throw new Error(`owned path escapes repo: ${owned}`);
|
|
1906
2242
|
}
|
|
1907
2243
|
return resolved;
|
|
@@ -1913,14 +2249,14 @@ function validateTailLines(lines) {
|
|
|
1913
2249
|
}
|
|
1914
2250
|
|
|
1915
2251
|
// src/worktree.ts
|
|
1916
|
-
import { existsSync as
|
|
1917
|
-
import
|
|
2252
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync4 } from "node:fs";
|
|
2253
|
+
import path12 from "node:path";
|
|
1918
2254
|
function createRun(args) {
|
|
1919
2255
|
const repo = validateRepo(required(String(args.repo || ""), "--repo"));
|
|
1920
2256
|
ensureGitRepo(repo);
|
|
1921
2257
|
const id = args.id ? validateRunId(String(args.id)) : timestampSlug(String(args.name || "run"));
|
|
1922
2258
|
const dir = runDirectory(id);
|
|
1923
|
-
if (
|
|
2259
|
+
if (existsSync11(dir)) failExists(`run already exists: ${id}`);
|
|
1924
2260
|
mkdirSync4(dir, { recursive: true });
|
|
1925
2261
|
const base = String(args.base || "origin/main");
|
|
1926
2262
|
const baseCommit = git(repo, ["rev-parse", base]).trim();
|
|
@@ -1934,12 +2270,12 @@ function createRun(args) {
|
|
|
1934
2270
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1935
2271
|
workers: {}
|
|
1936
2272
|
};
|
|
1937
|
-
writeJson(
|
|
2273
|
+
writeJson(path12.join(dir, "run.json"), run);
|
|
1938
2274
|
console.log(JSON.stringify({ runId: id, runDir: dir, repo, base, baseCommit }, null, 2));
|
|
1939
2275
|
}
|
|
1940
2276
|
function listRuns() {
|
|
1941
2277
|
const { runsDir } = getPaths();
|
|
1942
|
-
const rows = listRunIds(runsDir).map((id) => readJson(
|
|
2278
|
+
const rows = listRunIds(runsDir).map((id) => readJson(path12.join(runDirectory(id), "run.json"), void 0)).filter(Boolean).map((run) => ({
|
|
1943
2279
|
id: run.id,
|
|
1944
2280
|
name: run.name,
|
|
1945
2281
|
status: run.status,
|
|
@@ -1954,7 +2290,7 @@ function failExists(message) {
|
|
|
1954
2290
|
}
|
|
1955
2291
|
|
|
1956
2292
|
// src/sweep.ts
|
|
1957
|
-
import
|
|
2293
|
+
import path13 from "node:path";
|
|
1958
2294
|
async function sweepRun(args) {
|
|
1959
2295
|
const pipeline = args.pipeline === true || args.pipeline === "true";
|
|
1960
2296
|
try {
|
|
@@ -1966,7 +2302,7 @@ async function sweepRun(args) {
|
|
|
1966
2302
|
const releasedLocalOrphans = [];
|
|
1967
2303
|
for (const name of Object.keys(run.workers || {})) {
|
|
1968
2304
|
const worker = readJson(
|
|
1969
|
-
|
|
2305
|
+
path13.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
1970
2306
|
void 0
|
|
1971
2307
|
);
|
|
1972
2308
|
if (!worker || !worker.dispatched || !worker.taskId) continue;
|
|
@@ -2013,10 +2349,13 @@ import { mkdirSync as mkdirSync5, realpathSync } from "node:fs";
|
|
|
2013
2349
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
2014
2350
|
|
|
2015
2351
|
// src/pipeline-tick.ts
|
|
2352
|
+
import path17 from "node:path";
|
|
2353
|
+
|
|
2354
|
+
// src/stale-reconcile.ts
|
|
2016
2355
|
import path15 from "node:path";
|
|
2017
2356
|
|
|
2018
2357
|
// src/finalize.ts
|
|
2019
|
-
import
|
|
2358
|
+
import path14 from "node:path";
|
|
2020
2359
|
var ACTIVE_RUN_STATUSES = /* @__PURE__ */ new Set(["running", "dispatching", "pending", "queued"]);
|
|
2021
2360
|
function terminalStatusFor(run) {
|
|
2022
2361
|
const names = Object.keys(run.workers || {});
|
|
@@ -2024,13 +2363,17 @@ function terminalStatusFor(run) {
|
|
|
2024
2363
|
let anyAlive = false;
|
|
2025
2364
|
let anyResult = false;
|
|
2026
2365
|
let anyCompletionBlocked = false;
|
|
2366
|
+
let anyLandingBlocked = false;
|
|
2027
2367
|
for (const name of names) {
|
|
2028
2368
|
const worker = readJson(
|
|
2029
|
-
|
|
2369
|
+
path14.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
2030
2370
|
void 0
|
|
2031
2371
|
);
|
|
2032
2372
|
if (!worker) continue;
|
|
2033
|
-
const status = computeWorkerStatus(worker
|
|
2373
|
+
const status = computeWorkerStatus(worker, {
|
|
2374
|
+
base: run.base,
|
|
2375
|
+
baseCommit: run.baseCommit
|
|
2376
|
+
});
|
|
2034
2377
|
if (status.alive && !status.finalResult) {
|
|
2035
2378
|
anyAlive = true;
|
|
2036
2379
|
break;
|
|
@@ -2038,10 +2381,14 @@ function terminalStatusFor(run) {
|
|
|
2038
2381
|
if (typeof worker.completionBlocker === "string" && worker.completionBlocker) {
|
|
2039
2382
|
anyCompletionBlocked = true;
|
|
2040
2383
|
}
|
|
2041
|
-
if (status
|
|
2384
|
+
if (isLandingBlockedWorkerStatus(status)) {
|
|
2385
|
+
anyLandingBlocked = true;
|
|
2386
|
+
}
|
|
2387
|
+
if (status.finalResult && status.attention.state === "done") anyResult = true;
|
|
2042
2388
|
}
|
|
2043
2389
|
if (anyAlive) return null;
|
|
2044
2390
|
if (anyCompletionBlocked) return null;
|
|
2391
|
+
if (anyLandingBlocked) return null;
|
|
2045
2392
|
return anyResult ? "completed" : "failed";
|
|
2046
2393
|
}
|
|
2047
2394
|
function finalizeStaleRuns() {
|
|
@@ -2058,8 +2405,82 @@ function finalizeStaleRuns() {
|
|
|
2058
2405
|
return finalized;
|
|
2059
2406
|
}
|
|
2060
2407
|
|
|
2408
|
+
// src/stale-reconcile.ts
|
|
2409
|
+
var STALE_RECONCILE_HEARTBEAT_MS = 15 * 60 * 1e3;
|
|
2410
|
+
function staleReconcileDisabled() {
|
|
2411
|
+
return process.env.KYNVER_NO_STALE_CLEANUP === "1";
|
|
2412
|
+
}
|
|
2413
|
+
function reconcileStaleWorkers() {
|
|
2414
|
+
if (staleReconcileDisabled()) {
|
|
2415
|
+
return { workers: [], finalizedRuns: finalizeStaleRuns() };
|
|
2416
|
+
}
|
|
2417
|
+
const outcomes = [];
|
|
2418
|
+
const now = Date.now();
|
|
2419
|
+
for (const run of listRunRecords()) {
|
|
2420
|
+
for (const name of Object.keys(run.workers || {})) {
|
|
2421
|
+
const workerPath = path15.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
|
|
2422
|
+
const worker = readJson(workerPath, void 0);
|
|
2423
|
+
if (!worker || worker.status !== "running") {
|
|
2424
|
+
outcomes.push({
|
|
2425
|
+
runId: run.id,
|
|
2426
|
+
worker: name,
|
|
2427
|
+
action: "skipped",
|
|
2428
|
+
reason: worker ? `worker status is ${worker.status}` : "worker.json missing"
|
|
2429
|
+
});
|
|
2430
|
+
continue;
|
|
2431
|
+
}
|
|
2432
|
+
const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
|
|
2433
|
+
if (status.finalResult) {
|
|
2434
|
+
outcomes.push({ runId: run.id, worker: name, action: "skipped", reason: "final result present" });
|
|
2435
|
+
continue;
|
|
2436
|
+
}
|
|
2437
|
+
if (!status.alive) {
|
|
2438
|
+
const nextStatus = status.attention.state === "blocked" ? "blocked" : status.status === "done" ? "done" : "exited";
|
|
2439
|
+
worker.status = nextStatus;
|
|
2440
|
+
worker.reconciledAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2441
|
+
worker.reconcileReason = status.attention.reason;
|
|
2442
|
+
saveWorker(run.id, worker);
|
|
2443
|
+
outcomes.push({
|
|
2444
|
+
runId: run.id,
|
|
2445
|
+
worker: name,
|
|
2446
|
+
action: "marked_exited",
|
|
2447
|
+
reason: status.attention.reason
|
|
2448
|
+
});
|
|
2449
|
+
continue;
|
|
2450
|
+
}
|
|
2451
|
+
if (status.attention.state === "stale" && worker.pid && isPidAlive(worker.pid)) {
|
|
2452
|
+
const hbMs = status.lastHeartbeatAt ? Date.parse(status.lastHeartbeatAt) : NaN;
|
|
2453
|
+
const actMs = status.lastActivityAt ? Date.parse(status.lastActivityAt) : NaN;
|
|
2454
|
+
const hbStale = !Number.isFinite(hbMs) || now - hbMs > STALE_RECONCILE_HEARTBEAT_MS;
|
|
2455
|
+
const actStale = Number.isFinite(actMs) && now - actMs > STALE_MS;
|
|
2456
|
+
if (hbStale && actStale) {
|
|
2457
|
+
killWorkerProcess(worker.pid, "SIGTERM");
|
|
2458
|
+
worker.status = "exited";
|
|
2459
|
+
worker.reconciledAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2460
|
+
worker.reconcileReason = `reconciled stale worker: ${status.attention.reason}`;
|
|
2461
|
+
saveWorker(run.id, worker);
|
|
2462
|
+
outcomes.push({
|
|
2463
|
+
runId: run.id,
|
|
2464
|
+
worker: name,
|
|
2465
|
+
action: "killed_stale",
|
|
2466
|
+
reason: status.attention.reason
|
|
2467
|
+
});
|
|
2468
|
+
continue;
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
outcomes.push({
|
|
2472
|
+
runId: run.id,
|
|
2473
|
+
worker: name,
|
|
2474
|
+
action: "skipped",
|
|
2475
|
+
reason: status.attention.reason
|
|
2476
|
+
});
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
return { workers: outcomes, finalizedRuns: finalizeStaleRuns() };
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2061
2482
|
// src/plan-progress-daemon-sync.ts
|
|
2062
|
-
import
|
|
2483
|
+
import path16 from "node:path";
|
|
2063
2484
|
|
|
2064
2485
|
// src/plan-progress-sync.ts
|
|
2065
2486
|
async function syncPlanProgress(args) {
|
|
@@ -2083,7 +2504,7 @@ async function syncActiveWorkerPlanProgress(runId, args) {
|
|
|
2083
2504
|
const outcomes = [];
|
|
2084
2505
|
for (const name of Object.keys(run.workers || {})) {
|
|
2085
2506
|
const worker = readJson(
|
|
2086
|
-
|
|
2507
|
+
path16.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
2087
2508
|
void 0
|
|
2088
2509
|
);
|
|
2089
2510
|
if (!worker?.dispatched || !worker.taskId) continue;
|
|
@@ -2137,7 +2558,7 @@ async function completeFinishedWorkers(runId, args) {
|
|
|
2137
2558
|
const outcomes = [];
|
|
2138
2559
|
for (const name of Object.keys(run.workers || {})) {
|
|
2139
2560
|
const worker = readJson(
|
|
2140
|
-
|
|
2561
|
+
path17.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
2141
2562
|
void 0
|
|
2142
2563
|
);
|
|
2143
2564
|
if (!worker?.taskId) continue;
|
|
@@ -2172,7 +2593,7 @@ async function runPipelineTick(args) {
|
|
|
2172
2593
|
const execute = args.execute !== false && args.execute !== "false";
|
|
2173
2594
|
runStatus({ run: runId });
|
|
2174
2595
|
const completedWorkers = await completeFinishedWorkers(runId, args);
|
|
2175
|
-
const
|
|
2596
|
+
const staleReconcile = reconcileStaleWorkers();
|
|
2176
2597
|
const planProgressSync = await syncActiveWorkerPlanProgress(runId, args);
|
|
2177
2598
|
const workspacePrefs = await fetchWorkspaceRuntimePreferences(agentOsId, args);
|
|
2178
2599
|
const resourceGate = observeRunnerResourceGate({
|
|
@@ -2213,7 +2634,7 @@ async function runPipelineTick(args) {
|
|
|
2213
2634
|
execute,
|
|
2214
2635
|
resourceGate,
|
|
2215
2636
|
completedWorkers,
|
|
2216
|
-
|
|
2637
|
+
staleReconcile,
|
|
2217
2638
|
planProgressSync,
|
|
2218
2639
|
operatorTick,
|
|
2219
2640
|
sweep,
|
|
@@ -2431,6 +2852,7 @@ if (isCliEntry) {
|
|
|
2431
2852
|
}
|
|
2432
2853
|
export {
|
|
2433
2854
|
DEFAULT_DISPATCH_LEASE_MS,
|
|
2855
|
+
assessWorkerLanding,
|
|
2434
2856
|
autoCompleteWorker,
|
|
2435
2857
|
autoCompleteWorkerCli,
|
|
2436
2858
|
buildDispatchTaskText,
|
|
@@ -2443,6 +2865,7 @@ export {
|
|
|
2443
2865
|
dispatchRun,
|
|
2444
2866
|
getHarnessPaths,
|
|
2445
2867
|
isFinishedWorkerStatus,
|
|
2868
|
+
isLandingBlockedWorkerStatus,
|
|
2446
2869
|
listRuns,
|
|
2447
2870
|
loadUserConfig,
|
|
2448
2871
|
main,
|