@kynver-app/runtime 0.1.10 → 0.1.13
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 +208 -40
- package/dist/cli.js.map +4 -4
- package/dist/index.js +208 -40
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -381,12 +381,12 @@ var DEFAULT_CRITICAL_FREE_BYTES = 15 * 1024 * 1024 * 1024;
|
|
|
381
381
|
var DEFAULT_MAX_USED_PERCENT = 80;
|
|
382
382
|
var DEFAULT_HARD_MAX_USED_PERCENT = 90;
|
|
383
383
|
function observeRunnerDiskGate(input = {}) {
|
|
384
|
-
const
|
|
384
|
+
const path15 = input.diskPath?.trim() || "/";
|
|
385
385
|
const warnBelowBytes = input.diskFreeWarnBytes ?? DEFAULT_WARN_FREE_BYTES;
|
|
386
386
|
const criticalBelowBytes = input.diskFreeCriticalBytes ?? DEFAULT_CRITICAL_FREE_BYTES;
|
|
387
387
|
const maxUsedPercent = input.diskMaxUsedPercent ?? DEFAULT_MAX_USED_PERCENT;
|
|
388
388
|
const hardMaxUsedPercent = input.diskHardMaxUsedPercent ?? DEFAULT_HARD_MAX_USED_PERCENT;
|
|
389
|
-
const stats = statfsSync(
|
|
389
|
+
const stats = statfsSync(path15);
|
|
390
390
|
const freeBytes = Number(stats.bavail) * Number(stats.bsize);
|
|
391
391
|
const totalBytes = Number(stats.blocks) * Number(stats.bsize);
|
|
392
392
|
const usedPercent = totalBytes > 0 ? (totalBytes - freeBytes) / totalBytes * 100 : 100;
|
|
@@ -406,7 +406,7 @@ function observeRunnerDiskGate(input = {}) {
|
|
|
406
406
|
}
|
|
407
407
|
return {
|
|
408
408
|
ok,
|
|
409
|
-
path:
|
|
409
|
+
path: path15,
|
|
410
410
|
freeBytes,
|
|
411
411
|
totalBytes,
|
|
412
412
|
usedPercent,
|
|
@@ -419,10 +419,12 @@ function observeRunnerDiskGate(input = {}) {
|
|
|
419
419
|
}
|
|
420
420
|
|
|
421
421
|
// src/resource-gate.ts
|
|
422
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
422
423
|
import os from "node:os";
|
|
423
424
|
import path5 from "node:path";
|
|
424
425
|
|
|
425
426
|
// src/run-store.ts
|
|
427
|
+
import { existsSync as existsSync4, readdirSync as readdirSync2 } from "node:fs";
|
|
426
428
|
import path4 from "node:path";
|
|
427
429
|
|
|
428
430
|
// src/paths.ts
|
|
@@ -458,6 +460,20 @@ function loadRun(id) {
|
|
|
458
460
|
const { runsDir } = getPaths();
|
|
459
461
|
return readJson(path4.join(runDir(runsDir, safeSlug(id)), "run.json"));
|
|
460
462
|
}
|
|
463
|
+
function listRunRecords() {
|
|
464
|
+
const { runsDir } = getPaths();
|
|
465
|
+
if (!existsSync4(runsDir)) return [];
|
|
466
|
+
const runs = [];
|
|
467
|
+
for (const entry of readdirSync2(runsDir, { withFileTypes: true })) {
|
|
468
|
+
if (!entry.isDirectory()) continue;
|
|
469
|
+
const run = readJson(
|
|
470
|
+
path4.join(runsDir, entry.name, "run.json"),
|
|
471
|
+
void 0
|
|
472
|
+
);
|
|
473
|
+
if (run?.id) runs.push(run);
|
|
474
|
+
}
|
|
475
|
+
return runs;
|
|
476
|
+
}
|
|
461
477
|
function loadWorker(runId, name) {
|
|
462
478
|
const { runsDir } = getPaths();
|
|
463
479
|
return readJson(
|
|
@@ -478,7 +494,7 @@ function runDirectory(id) {
|
|
|
478
494
|
}
|
|
479
495
|
|
|
480
496
|
// src/heartbeat.ts
|
|
481
|
-
import { existsSync as
|
|
497
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3 } from "node:fs";
|
|
482
498
|
function parseHeartbeat(file) {
|
|
483
499
|
const result = {
|
|
484
500
|
heartbeatCount: 0,
|
|
@@ -487,7 +503,7 @@ function parseHeartbeat(file) {
|
|
|
487
503
|
lastHeartbeatSummary: null,
|
|
488
504
|
heartbeatBlocker: null
|
|
489
505
|
};
|
|
490
|
-
if (!
|
|
506
|
+
if (!existsSync5(file)) return result;
|
|
491
507
|
const lines = readFileSync3(file, "utf8").split("\n").filter(Boolean);
|
|
492
508
|
for (const line of lines) {
|
|
493
509
|
const entry = safeJson(line);
|
|
@@ -503,7 +519,7 @@ function parseHeartbeat(file) {
|
|
|
503
519
|
}
|
|
504
520
|
|
|
505
521
|
// src/stream.ts
|
|
506
|
-
import { existsSync as
|
|
522
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4 } from "node:fs";
|
|
507
523
|
function parseClaudeStream(file) {
|
|
508
524
|
const result = {
|
|
509
525
|
firstEventAt: null,
|
|
@@ -512,7 +528,7 @@ function parseClaudeStream(file) {
|
|
|
512
528
|
finalResult: null,
|
|
513
529
|
error: null
|
|
514
530
|
};
|
|
515
|
-
if (!
|
|
531
|
+
if (!existsSync6(file)) return result;
|
|
516
532
|
const lines = readFileSync4(file, "utf8").split("\n").filter(Boolean);
|
|
517
533
|
for (const line of lines) {
|
|
518
534
|
const event = safeJson(line);
|
|
@@ -594,6 +610,92 @@ function ensureGitRepo(repo) {
|
|
|
594
610
|
function gitStatusShort(worktreePath) {
|
|
595
611
|
return git(worktreePath, ["status", "--short"], { allowFailure: true }).split("\n").map((line) => line.trim()).filter(Boolean);
|
|
596
612
|
}
|
|
613
|
+
function gitCapture(cwd, args) {
|
|
614
|
+
try {
|
|
615
|
+
const res = spawnSync("git", args, { cwd, encoding: "utf8" });
|
|
616
|
+
return {
|
|
617
|
+
status: res.status,
|
|
618
|
+
stdout: res.stdout || "",
|
|
619
|
+
stderr: res.stderr || "",
|
|
620
|
+
error: res.error ? res.error.message : null
|
|
621
|
+
};
|
|
622
|
+
} catch (error) {
|
|
623
|
+
return {
|
|
624
|
+
status: null,
|
|
625
|
+
stdout: "",
|
|
626
|
+
stderr: "",
|
|
627
|
+
error: error.message
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
function gitIsAncestor(cwd, ancestor, descendant) {
|
|
632
|
+
const res = gitCapture(cwd, ["merge-base", "--is-ancestor", ancestor, descendant]);
|
|
633
|
+
if (res.status === 0) return { isAncestor: true, error: null };
|
|
634
|
+
if (res.status === 1) return { isAncestor: false, error: null };
|
|
635
|
+
return { isAncestor: null, error: res.error || res.stderr || res.stdout || `git exited ${res.status}` };
|
|
636
|
+
}
|
|
637
|
+
function computeGitAncestry(worktreePath, base = "origin/main") {
|
|
638
|
+
if (!worktreePath) {
|
|
639
|
+
return unknownAncestry(base, "missing worktree path");
|
|
640
|
+
}
|
|
641
|
+
const head = gitCapture(worktreePath, ["rev-parse", "HEAD"]);
|
|
642
|
+
if (head.status !== 0) return unknownAncestry(base, head.error || head.stderr || head.stdout || "failed to resolve HEAD");
|
|
643
|
+
const baseHead = gitCapture(worktreePath, ["rev-parse", base]);
|
|
644
|
+
if (baseHead.status !== 0) {
|
|
645
|
+
return unknownAncestry(base, baseHead.error || baseHead.stderr || baseHead.stdout || `failed to resolve ${base}`, head.stdout.trim());
|
|
646
|
+
}
|
|
647
|
+
const headSha = head.stdout.trim();
|
|
648
|
+
const baseSha = baseHead.stdout.trim();
|
|
649
|
+
if (headSha === baseSha) {
|
|
650
|
+
return {
|
|
651
|
+
checked: true,
|
|
652
|
+
base,
|
|
653
|
+
head: headSha,
|
|
654
|
+
baseHead: baseSha,
|
|
655
|
+
baseIsAncestorOfHead: true,
|
|
656
|
+
headIsAncestorOfBase: true,
|
|
657
|
+
relation: "synced"
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
const baseIsAncestorOfHead = gitIsAncestor(worktreePath, baseSha, headSha);
|
|
661
|
+
const headIsAncestorOfBase = gitIsAncestor(worktreePath, headSha, baseSha);
|
|
662
|
+
const error = baseIsAncestorOfHead.error || headIsAncestorOfBase.error || void 0;
|
|
663
|
+
if (baseIsAncestorOfHead.isAncestor == null || headIsAncestorOfBase.isAncestor == null) {
|
|
664
|
+
return {
|
|
665
|
+
checked: false,
|
|
666
|
+
base,
|
|
667
|
+
head: headSha,
|
|
668
|
+
baseHead: baseSha,
|
|
669
|
+
baseIsAncestorOfHead: baseIsAncestorOfHead.isAncestor,
|
|
670
|
+
headIsAncestorOfBase: headIsAncestorOfBase.isAncestor,
|
|
671
|
+
relation: "unknown",
|
|
672
|
+
...error ? { error } : {}
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
const relation = baseIsAncestorOfHead.isAncestor ? "ahead" : headIsAncestorOfBase.isAncestor ? "merged" : "diverged";
|
|
676
|
+
return {
|
|
677
|
+
checked: true,
|
|
678
|
+
base,
|
|
679
|
+
head: headSha,
|
|
680
|
+
baseHead: baseSha,
|
|
681
|
+
baseIsAncestorOfHead: baseIsAncestorOfHead.isAncestor,
|
|
682
|
+
headIsAncestorOfBase: headIsAncestorOfBase.isAncestor,
|
|
683
|
+
relation,
|
|
684
|
+
...error ? { error } : {}
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
function unknownAncestry(base, error, head = null) {
|
|
688
|
+
return {
|
|
689
|
+
checked: false,
|
|
690
|
+
base,
|
|
691
|
+
head,
|
|
692
|
+
baseHead: null,
|
|
693
|
+
baseIsAncestorOfHead: null,
|
|
694
|
+
headIsAncestorOfBase: null,
|
|
695
|
+
relation: "unknown",
|
|
696
|
+
error
|
|
697
|
+
};
|
|
698
|
+
}
|
|
597
699
|
function scrubClaudeEnv(env) {
|
|
598
700
|
const next = { ...env };
|
|
599
701
|
delete next.ANTHROPIC_API_KEY;
|
|
@@ -620,7 +722,7 @@ function computeAttention(input) {
|
|
|
620
722
|
}
|
|
621
723
|
return { state: "ok", reason: "recent activity" };
|
|
622
724
|
}
|
|
623
|
-
function computeWorkerStatus(worker) {
|
|
725
|
+
function computeWorkerStatus(worker, options = {}) {
|
|
624
726
|
const parsed = parseClaudeStream(worker.stdoutPath);
|
|
625
727
|
const heartbeat = parseHeartbeat(worker.heartbeatPath);
|
|
626
728
|
const alive = isPidAlive(worker.pid);
|
|
@@ -628,6 +730,7 @@ function computeWorkerStatus(worker) {
|
|
|
628
730
|
const stderrBytes = fileSize(worker.stderrPath);
|
|
629
731
|
const heartbeatBytes = fileSize(worker.heartbeatPath);
|
|
630
732
|
const changedFiles = gitStatusShort(worker.worktreePath);
|
|
733
|
+
const gitAncestry = computeGitAncestry(worker.worktreePath, options.base);
|
|
631
734
|
const lastActivityAt = latestIso([
|
|
632
735
|
parsed.lastEventAt,
|
|
633
736
|
heartbeat.lastHeartbeatAt,
|
|
@@ -669,7 +772,8 @@ function computeWorkerStatus(worker) {
|
|
|
669
772
|
heartbeatBlocker: heartbeat.heartbeatBlocker,
|
|
670
773
|
finalResult: parsed.finalResult,
|
|
671
774
|
error: parsed.error || (!alive && !parsed.finalResult ? tailFile(worker.stderrPath, 10).trim() || void 0 : void 0),
|
|
672
|
-
changedFiles
|
|
775
|
+
changedFiles,
|
|
776
|
+
gitAncestry
|
|
673
777
|
};
|
|
674
778
|
}
|
|
675
779
|
function isFinishedWorkerStatus(status) {
|
|
@@ -717,13 +821,23 @@ function computeAutoMaxWorkers(totalMemBytes, opts = {}) {
|
|
|
717
821
|
const raw = Math.max(1, Math.floor(budgetBytes / perWorkerMemBytes));
|
|
718
822
|
return Math.min(raw, AUTO_MAX_WORKERS_CEILING);
|
|
719
823
|
}
|
|
720
|
-
function
|
|
721
|
-
|
|
824
|
+
function readAvailableMemBytes() {
|
|
825
|
+
if (process.platform === "linux") {
|
|
826
|
+
try {
|
|
827
|
+
const meminfo = readFileSync5("/proc/meminfo", "utf8");
|
|
828
|
+
const match = meminfo.match(/^MemAvailable:\s+(\d+)\s*kB/m);
|
|
829
|
+
if (match) return Number(match[1]) * 1024;
|
|
830
|
+
} catch {
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
return os.freemem();
|
|
834
|
+
}
|
|
835
|
+
function countActiveWorkersForRun(run) {
|
|
722
836
|
let active = 0;
|
|
723
837
|
for (const name of Object.keys(run.workers || {})) {
|
|
724
838
|
const worker = readJson(
|
|
725
839
|
path5.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
726
|
-
|
|
840
|
+
void 0
|
|
727
841
|
);
|
|
728
842
|
if (!worker) continue;
|
|
729
843
|
const status = computeWorkerStatus(worker);
|
|
@@ -733,14 +847,19 @@ function countActiveWorkers(runId) {
|
|
|
733
847
|
}
|
|
734
848
|
return active;
|
|
735
849
|
}
|
|
850
|
+
function countActiveWorkersGlobal() {
|
|
851
|
+
let active = 0;
|
|
852
|
+
for (const run of listRunRecords()) active += countActiveWorkersForRun(run);
|
|
853
|
+
return active;
|
|
854
|
+
}
|
|
736
855
|
function observeRunnerResourceGate(input) {
|
|
737
856
|
const { perWorkerMemBytes, memReserveBytes, memUtilization, configuredMaxWorkers } = resolveResourceConfig(
|
|
738
857
|
input.config,
|
|
739
858
|
input.configuredMaxWorkersOverride
|
|
740
859
|
);
|
|
741
860
|
const totalMemBytes = input.totalMemBytes ?? os.totalmem();
|
|
742
|
-
const freeMemBytes = input.freeMemBytes ??
|
|
743
|
-
const activeWorkers = input.activeWorkers ??
|
|
861
|
+
const freeMemBytes = input.freeMemBytes ?? readAvailableMemBytes();
|
|
862
|
+
const activeWorkers = input.activeWorkers ?? countActiveWorkersGlobal();
|
|
744
863
|
const budgetBytes = Math.max(0, Math.floor(totalMemBytes * memUtilization) - memReserveBytes);
|
|
745
864
|
const capacityFromTotal = Math.max(0, Math.floor(budgetBytes / perWorkerMemBytes));
|
|
746
865
|
const capacityFromFree = Math.max(0, Math.floor(Math.max(0, freeMemBytes - memReserveBytes) / perWorkerMemBytes));
|
|
@@ -748,13 +867,13 @@ function observeRunnerResourceGate(input) {
|
|
|
748
867
|
const targetCap = configuredMaxWorkers ?? autoCap;
|
|
749
868
|
const maxConcurrentWorkers = Math.max(0, Math.min(targetCap, capacityFromTotal));
|
|
750
869
|
const slotsByCapacity = Math.max(0, maxConcurrentWorkers - activeWorkers);
|
|
751
|
-
const slotsByFreeMem =
|
|
870
|
+
const slotsByFreeMem = capacityFromFree;
|
|
752
871
|
const slotsAvailable = Math.min(slotsByCapacity, slotsByFreeMem);
|
|
753
872
|
let reason = null;
|
|
754
873
|
if (slotsAvailable <= 0) {
|
|
755
874
|
if (activeWorkers >= maxConcurrentWorkers) {
|
|
756
875
|
reason = `at worker limit (${activeWorkers}/${maxConcurrentWorkers} running)`;
|
|
757
|
-
} else if (capacityFromFree <=
|
|
876
|
+
} else if (capacityFromFree <= 0) {
|
|
758
877
|
reason = "insufficient free memory \u2014 waiting for workers to finish";
|
|
759
878
|
} else {
|
|
760
879
|
reason = "no worker slots available";
|
|
@@ -777,7 +896,7 @@ function observeRunnerResourceGate(input) {
|
|
|
777
896
|
}
|
|
778
897
|
|
|
779
898
|
// src/supervisor.ts
|
|
780
|
-
import { existsSync as
|
|
899
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync3 } from "node:fs";
|
|
781
900
|
import path7 from "node:path";
|
|
782
901
|
|
|
783
902
|
// src/prompt.ts
|
|
@@ -850,19 +969,19 @@ var claudeProvider = {
|
|
|
850
969
|
};
|
|
851
970
|
|
|
852
971
|
// src/providers/cursor.ts
|
|
853
|
-
import { closeSync as closeSync2, existsSync as
|
|
972
|
+
import { closeSync as closeSync2, existsSync as existsSync7, openSync as openSync2, readdirSync as readdirSync3 } from "node:fs";
|
|
854
973
|
import { spawn as spawn2 } from "node:child_process";
|
|
855
974
|
import path6 from "node:path";
|
|
856
975
|
var DEFAULT_CURSOR_MODEL = "composer-2.5";
|
|
857
976
|
function latestVersionDir(versionsRoot) {
|
|
858
|
-
if (!
|
|
859
|
-
const versions =
|
|
977
|
+
if (!existsSync7(versionsRoot)) return null;
|
|
978
|
+
const versions = readdirSync3(versionsRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory() && /^\d{4}\.\d/.test(entry.name)).map((entry) => entry.name).sort((a, b) => b.localeCompare(a));
|
|
860
979
|
return versions[0] ? path6.join(versionsRoot, versions[0]) : null;
|
|
861
980
|
}
|
|
862
981
|
function resolveBundledCursor(versionDir) {
|
|
863
982
|
const nodeExe = path6.join(versionDir, "node.exe");
|
|
864
983
|
const indexJs = path6.join(versionDir, "index.js");
|
|
865
|
-
if (!
|
|
984
|
+
if (!existsSync7(nodeExe) || !existsSync7(indexJs)) return null;
|
|
866
985
|
return { executable: nodeExe, prefixArgs: [indexJs], shell: false, detached: true };
|
|
867
986
|
}
|
|
868
987
|
function resolveWindowsCursorSpawn(agentBin) {
|
|
@@ -885,7 +1004,7 @@ function resolveAgentBin() {
|
|
|
885
1004
|
if (configured) return configured;
|
|
886
1005
|
if (process.platform === "win32") {
|
|
887
1006
|
const localAgent = path6.join(process.env.LOCALAPPDATA || "", "cursor-agent", "agent.cmd");
|
|
888
|
-
if (
|
|
1007
|
+
if (existsSync7(localAgent)) return localAgent;
|
|
889
1008
|
}
|
|
890
1009
|
return "agent";
|
|
891
1010
|
}
|
|
@@ -954,8 +1073,11 @@ function resolveWorkerProvider(name) {
|
|
|
954
1073
|
|
|
955
1074
|
// src/supervisor.ts
|
|
956
1075
|
function spawnWorkerProcess(run, opts) {
|
|
957
|
-
const
|
|
958
|
-
if (!
|
|
1076
|
+
const rawName = typeof opts.name === "string" ? opts.name.trim() : "";
|
|
1077
|
+
if (!rawName || rawName === "undefined" || rawName === "null") {
|
|
1078
|
+
throw new Error(`worker name is required and must be a real identifier (got: ${JSON.stringify(opts.name)})`);
|
|
1079
|
+
}
|
|
1080
|
+
const name = safeSlug(rawName);
|
|
959
1081
|
if (run.workers?.[name]) throw new Error(`worker already exists in run ${run.id}: ${name}`);
|
|
960
1082
|
if (!opts.task) throw new Error(`missing task text for worker ${name}`);
|
|
961
1083
|
const { worktreesDir } = getPaths();
|
|
@@ -963,7 +1085,7 @@ function spawnWorkerProcess(run, opts) {
|
|
|
963
1085
|
mkdirSync3(workerDir, { recursive: true });
|
|
964
1086
|
const worktreePath = path7.join(worktreesDir, run.id, name);
|
|
965
1087
|
const branch = opts.branch || `agent/${run.id}/${name}`;
|
|
966
|
-
if (
|
|
1088
|
+
if (existsSync8(worktreePath)) throw new Error(`worktree path already exists: ${worktreePath}`);
|
|
967
1089
|
git(run.repo, ["fetch", "origin", "--prune"], { allowFailure: true });
|
|
968
1090
|
git(run.repo, ["worktree", "add", "-b", branch, worktreePath, run.baseCommit], { throwError: true });
|
|
969
1091
|
const stdoutPath = path7.join(workerDir, "stdout.jsonl");
|
|
@@ -1025,6 +1147,11 @@ function spawnWorkerProcess(run, opts) {
|
|
|
1025
1147
|
}
|
|
1026
1148
|
function startWorker(args) {
|
|
1027
1149
|
const run = loadRun(String(args.run));
|
|
1150
|
+
const name = typeof args.name === "string" ? args.name.trim() : "";
|
|
1151
|
+
if (!name) {
|
|
1152
|
+
console.error("worker start failed: --name is required");
|
|
1153
|
+
process.exit(1);
|
|
1154
|
+
}
|
|
1028
1155
|
const task = args.task ? String(args.task) : readMaybeFile(args.taskFile ? String(args.taskFile) : void 0);
|
|
1029
1156
|
if (!task) {
|
|
1030
1157
|
console.error("missing --task or --task-file");
|
|
@@ -1032,7 +1159,7 @@ function startWorker(args) {
|
|
|
1032
1159
|
}
|
|
1033
1160
|
try {
|
|
1034
1161
|
const worker = spawnWorkerProcess(run, {
|
|
1035
|
-
name
|
|
1162
|
+
name,
|
|
1036
1163
|
task,
|
|
1037
1164
|
ownedPaths: args.owned ? String(args.owned).split(",").map((s) => s.trim()).filter(Boolean) : [],
|
|
1038
1165
|
model: args.model ? String(args.model) : void 0,
|
|
@@ -1259,14 +1386,14 @@ function validateTailLines(lines) {
|
|
|
1259
1386
|
}
|
|
1260
1387
|
|
|
1261
1388
|
// src/worktree.ts
|
|
1262
|
-
import { existsSync as
|
|
1389
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync4 } from "node:fs";
|
|
1263
1390
|
import path9 from "node:path";
|
|
1264
1391
|
function createRun(args) {
|
|
1265
1392
|
const repo = validateRepo(required(String(args.repo || ""), "--repo"));
|
|
1266
1393
|
ensureGitRepo(repo);
|
|
1267
1394
|
const id = args.id ? validateRunId(String(args.id)) : timestampSlug(String(args.name || "run"));
|
|
1268
1395
|
const dir = runDirectory(id);
|
|
1269
|
-
if (
|
|
1396
|
+
if (existsSync9(dir)) failExists(`run already exists: ${id}`);
|
|
1270
1397
|
mkdirSync4(dir, { recursive: true });
|
|
1271
1398
|
const base = String(args.base || "origin/main");
|
|
1272
1399
|
const baseCommit = git(repo, ["rev-parse", base]).trim();
|
|
@@ -1285,7 +1412,7 @@ function createRun(args) {
|
|
|
1285
1412
|
}
|
|
1286
1413
|
function listRuns() {
|
|
1287
1414
|
const { runsDir } = getPaths();
|
|
1288
|
-
const rows = listRunIds(runsDir).map((id) => readJson(path9.join(runDirectory(id), "run.json"),
|
|
1415
|
+
const rows = listRunIds(runsDir).map((id) => readJson(path9.join(runDirectory(id), "run.json"), void 0)).filter(Boolean).map((run) => ({
|
|
1289
1416
|
id: run.id,
|
|
1290
1417
|
name: run.name,
|
|
1291
1418
|
status: run.status,
|
|
@@ -1313,7 +1440,7 @@ async function sweepRun(args) {
|
|
|
1313
1440
|
for (const name of Object.keys(run.workers || {})) {
|
|
1314
1441
|
const worker = readJson(
|
|
1315
1442
|
path10.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
1316
|
-
|
|
1443
|
+
void 0
|
|
1317
1444
|
);
|
|
1318
1445
|
if (!worker || !worker.dispatched || !worker.taskId) continue;
|
|
1319
1446
|
const status = computeWorkerStatus(worker);
|
|
@@ -1453,12 +1580,12 @@ function runStatus(args) {
|
|
|
1453
1580
|
const workers = names.map((name) => {
|
|
1454
1581
|
const worker = readJson(
|
|
1455
1582
|
path11.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
1456
|
-
|
|
1583
|
+
void 0
|
|
1457
1584
|
);
|
|
1458
1585
|
if (!worker) {
|
|
1459
1586
|
return { worker: name, status: "missing", attention: "needs_attention", attentionReason: "worker.json not found" };
|
|
1460
1587
|
}
|
|
1461
|
-
const status = computeWorkerStatus(worker);
|
|
1588
|
+
const status = computeWorkerStatus(worker, { base: run.base });
|
|
1462
1589
|
return {
|
|
1463
1590
|
worker: status.worker,
|
|
1464
1591
|
status: status.status,
|
|
@@ -1472,7 +1599,9 @@ function runStatus(args) {
|
|
|
1472
1599
|
lastHeartbeatSummary: status.lastHeartbeatSummary,
|
|
1473
1600
|
heartbeatBlocker: status.heartbeatBlocker,
|
|
1474
1601
|
changedFileCount: status.changedFiles.length,
|
|
1475
|
-
branch: status.branch
|
|
1602
|
+
branch: status.branch,
|
|
1603
|
+
ancestry: status.gitAncestry.relation,
|
|
1604
|
+
ancestryChecked: status.gitAncestry.checked
|
|
1476
1605
|
};
|
|
1477
1606
|
});
|
|
1478
1607
|
const board = {
|
|
@@ -1521,10 +1650,48 @@ import { mkdirSync as mkdirSync5, realpathSync } from "node:fs";
|
|
|
1521
1650
|
import { fileURLToPath } from "node:url";
|
|
1522
1651
|
|
|
1523
1652
|
// src/pipeline-tick.ts
|
|
1524
|
-
import
|
|
1653
|
+
import path14 from "node:path";
|
|
1525
1654
|
|
|
1526
|
-
// src/
|
|
1655
|
+
// src/finalize.ts
|
|
1527
1656
|
import path12 from "node:path";
|
|
1657
|
+
var ACTIVE_RUN_STATUSES = /* @__PURE__ */ new Set(["running", "dispatching", "pending", "queued"]);
|
|
1658
|
+
function terminalStatusFor(run) {
|
|
1659
|
+
const names = Object.keys(run.workers || {});
|
|
1660
|
+
if (names.length === 0) return "failed";
|
|
1661
|
+
let anyAlive = false;
|
|
1662
|
+
let anyResult = false;
|
|
1663
|
+
for (const name of names) {
|
|
1664
|
+
const worker = readJson(
|
|
1665
|
+
path12.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
1666
|
+
void 0
|
|
1667
|
+
);
|
|
1668
|
+
if (!worker) continue;
|
|
1669
|
+
const status = computeWorkerStatus(worker);
|
|
1670
|
+
if (status.alive && !status.finalResult) {
|
|
1671
|
+
anyAlive = true;
|
|
1672
|
+
break;
|
|
1673
|
+
}
|
|
1674
|
+
if (status.finalResult) anyResult = true;
|
|
1675
|
+
}
|
|
1676
|
+
if (anyAlive) return null;
|
|
1677
|
+
return anyResult ? "completed" : "failed";
|
|
1678
|
+
}
|
|
1679
|
+
function finalizeStaleRuns() {
|
|
1680
|
+
const finalized = [];
|
|
1681
|
+
for (const run of listRunRecords()) {
|
|
1682
|
+
if (!ACTIVE_RUN_STATUSES.has(run.status)) continue;
|
|
1683
|
+
const next = terminalStatusFor(run);
|
|
1684
|
+
if (!next || next === run.status) continue;
|
|
1685
|
+
const from = run.status;
|
|
1686
|
+
run.status = next;
|
|
1687
|
+
saveRun(run);
|
|
1688
|
+
finalized.push({ runId: run.id, from, to: next });
|
|
1689
|
+
}
|
|
1690
|
+
return finalized;
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
// src/plan-progress-daemon-sync.ts
|
|
1694
|
+
import path13 from "node:path";
|
|
1528
1695
|
|
|
1529
1696
|
// src/plan-progress-sync.ts
|
|
1530
1697
|
async function syncPlanProgress(args) {
|
|
@@ -1548,8 +1715,8 @@ async function syncActiveWorkerPlanProgress(runId, args) {
|
|
|
1548
1715
|
const outcomes = [];
|
|
1549
1716
|
for (const name of Object.keys(run.workers || {})) {
|
|
1550
1717
|
const worker = readJson(
|
|
1551
|
-
|
|
1552
|
-
|
|
1718
|
+
path13.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
1719
|
+
void 0
|
|
1553
1720
|
);
|
|
1554
1721
|
if (!worker?.dispatched || !worker.taskId) continue;
|
|
1555
1722
|
const status = computeWorkerStatus(worker);
|
|
@@ -1602,13 +1769,12 @@ async function completeFinishedWorkers(runId, args) {
|
|
|
1602
1769
|
const outcomes = [];
|
|
1603
1770
|
for (const name of Object.keys(run.workers || {})) {
|
|
1604
1771
|
const worker = readJson(
|
|
1605
|
-
|
|
1606
|
-
|
|
1772
|
+
path14.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
1773
|
+
void 0
|
|
1607
1774
|
);
|
|
1608
1775
|
if (!worker?.dispatched || !worker.taskId) continue;
|
|
1609
1776
|
const status = computeWorkerStatus(worker);
|
|
1610
1777
|
if (!isFinishedWorkerStatus(status)) continue;
|
|
1611
|
-
if (!status.finalResult) continue;
|
|
1612
1778
|
const result = await tryCompleteWorker({
|
|
1613
1779
|
run: runId,
|
|
1614
1780
|
name,
|
|
@@ -1636,6 +1802,7 @@ async function runPipelineTick(args) {
|
|
|
1636
1802
|
const agentOsId = String(required(String(args.agentOsId || ""), "--agent-os-id"));
|
|
1637
1803
|
const execute = args.execute !== false && args.execute !== "false";
|
|
1638
1804
|
runStatus({ run: runId });
|
|
1805
|
+
const finalizedStaleRuns = finalizeStaleRuns();
|
|
1639
1806
|
const completedWorkers = await completeFinishedWorkers(runId, args);
|
|
1640
1807
|
const planProgressSync = await syncActiveWorkerPlanProgress(runId, args);
|
|
1641
1808
|
const workspacePrefs = await fetchWorkspaceRuntimePreferences(agentOsId, args);
|
|
@@ -1677,6 +1844,7 @@ async function runPipelineTick(args) {
|
|
|
1677
1844
|
execute,
|
|
1678
1845
|
resourceGate,
|
|
1679
1846
|
completedWorkers,
|
|
1847
|
+
finalizedStaleRuns,
|
|
1680
1848
|
planProgressSync,
|
|
1681
1849
|
operatorTick,
|
|
1682
1850
|
sweep,
|