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