@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/cli.js
CHANGED
|
@@ -380,12 +380,12 @@ var DEFAULT_CRITICAL_FREE_BYTES = 15 * 1024 * 1024 * 1024;
|
|
|
380
380
|
var DEFAULT_MAX_USED_PERCENT = 80;
|
|
381
381
|
var DEFAULT_HARD_MAX_USED_PERCENT = 90;
|
|
382
382
|
function observeRunnerDiskGate(input = {}) {
|
|
383
|
-
const
|
|
383
|
+
const path15 = input.diskPath?.trim() || "/";
|
|
384
384
|
const warnBelowBytes = input.diskFreeWarnBytes ?? DEFAULT_WARN_FREE_BYTES;
|
|
385
385
|
const criticalBelowBytes = input.diskFreeCriticalBytes ?? DEFAULT_CRITICAL_FREE_BYTES;
|
|
386
386
|
const maxUsedPercent = input.diskMaxUsedPercent ?? DEFAULT_MAX_USED_PERCENT;
|
|
387
387
|
const hardMaxUsedPercent = input.diskHardMaxUsedPercent ?? DEFAULT_HARD_MAX_USED_PERCENT;
|
|
388
|
-
const stats = statfsSync(
|
|
388
|
+
const stats = statfsSync(path15);
|
|
389
389
|
const freeBytes = Number(stats.bavail) * Number(stats.bsize);
|
|
390
390
|
const totalBytes = Number(stats.blocks) * Number(stats.bsize);
|
|
391
391
|
const usedPercent = totalBytes > 0 ? (totalBytes - freeBytes) / totalBytes * 100 : 100;
|
|
@@ -405,7 +405,7 @@ function observeRunnerDiskGate(input = {}) {
|
|
|
405
405
|
}
|
|
406
406
|
return {
|
|
407
407
|
ok,
|
|
408
|
-
path:
|
|
408
|
+
path: path15,
|
|
409
409
|
freeBytes,
|
|
410
410
|
totalBytes,
|
|
411
411
|
usedPercent,
|
|
@@ -418,10 +418,12 @@ function observeRunnerDiskGate(input = {}) {
|
|
|
418
418
|
}
|
|
419
419
|
|
|
420
420
|
// src/resource-gate.ts
|
|
421
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
421
422
|
import os from "node:os";
|
|
422
423
|
import path5 from "node:path";
|
|
423
424
|
|
|
424
425
|
// src/run-store.ts
|
|
426
|
+
import { existsSync as existsSync4, readdirSync as readdirSync2 } from "node:fs";
|
|
425
427
|
import path4 from "node:path";
|
|
426
428
|
|
|
427
429
|
// src/paths.ts
|
|
@@ -457,6 +459,20 @@ function loadRun(id) {
|
|
|
457
459
|
const { runsDir } = getPaths();
|
|
458
460
|
return readJson(path4.join(runDir(runsDir, safeSlug(id)), "run.json"));
|
|
459
461
|
}
|
|
462
|
+
function listRunRecords() {
|
|
463
|
+
const { runsDir } = getPaths();
|
|
464
|
+
if (!existsSync4(runsDir)) return [];
|
|
465
|
+
const runs = [];
|
|
466
|
+
for (const entry of readdirSync2(runsDir, { withFileTypes: true })) {
|
|
467
|
+
if (!entry.isDirectory()) continue;
|
|
468
|
+
const run = readJson(
|
|
469
|
+
path4.join(runsDir, entry.name, "run.json"),
|
|
470
|
+
void 0
|
|
471
|
+
);
|
|
472
|
+
if (run?.id) runs.push(run);
|
|
473
|
+
}
|
|
474
|
+
return runs;
|
|
475
|
+
}
|
|
460
476
|
function loadWorker(runId, name) {
|
|
461
477
|
const { runsDir } = getPaths();
|
|
462
478
|
return readJson(
|
|
@@ -477,7 +493,7 @@ function runDirectory(id) {
|
|
|
477
493
|
}
|
|
478
494
|
|
|
479
495
|
// src/heartbeat.ts
|
|
480
|
-
import { existsSync as
|
|
496
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3 } from "node:fs";
|
|
481
497
|
function parseHeartbeat(file) {
|
|
482
498
|
const result = {
|
|
483
499
|
heartbeatCount: 0,
|
|
@@ -486,7 +502,7 @@ function parseHeartbeat(file) {
|
|
|
486
502
|
lastHeartbeatSummary: null,
|
|
487
503
|
heartbeatBlocker: null
|
|
488
504
|
};
|
|
489
|
-
if (!
|
|
505
|
+
if (!existsSync5(file)) return result;
|
|
490
506
|
const lines = readFileSync3(file, "utf8").split("\n").filter(Boolean);
|
|
491
507
|
for (const line of lines) {
|
|
492
508
|
const entry = safeJson(line);
|
|
@@ -502,7 +518,7 @@ function parseHeartbeat(file) {
|
|
|
502
518
|
}
|
|
503
519
|
|
|
504
520
|
// src/stream.ts
|
|
505
|
-
import { existsSync as
|
|
521
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4 } from "node:fs";
|
|
506
522
|
function parseClaudeStream(file) {
|
|
507
523
|
const result = {
|
|
508
524
|
firstEventAt: null,
|
|
@@ -511,7 +527,7 @@ function parseClaudeStream(file) {
|
|
|
511
527
|
finalResult: null,
|
|
512
528
|
error: null
|
|
513
529
|
};
|
|
514
|
-
if (!
|
|
530
|
+
if (!existsSync6(file)) return result;
|
|
515
531
|
const lines = readFileSync4(file, "utf8").split("\n").filter(Boolean);
|
|
516
532
|
for (const line of lines) {
|
|
517
533
|
const event = safeJson(line);
|
|
@@ -593,6 +609,92 @@ function ensureGitRepo(repo) {
|
|
|
593
609
|
function gitStatusShort(worktreePath) {
|
|
594
610
|
return git(worktreePath, ["status", "--short"], { allowFailure: true }).split("\n").map((line) => line.trim()).filter(Boolean);
|
|
595
611
|
}
|
|
612
|
+
function gitCapture(cwd, args) {
|
|
613
|
+
try {
|
|
614
|
+
const res = spawnSync("git", args, { cwd, encoding: "utf8" });
|
|
615
|
+
return {
|
|
616
|
+
status: res.status,
|
|
617
|
+
stdout: res.stdout || "",
|
|
618
|
+
stderr: res.stderr || "",
|
|
619
|
+
error: res.error ? res.error.message : null
|
|
620
|
+
};
|
|
621
|
+
} catch (error) {
|
|
622
|
+
return {
|
|
623
|
+
status: null,
|
|
624
|
+
stdout: "",
|
|
625
|
+
stderr: "",
|
|
626
|
+
error: error.message
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
function gitIsAncestor(cwd, ancestor, descendant) {
|
|
631
|
+
const res = gitCapture(cwd, ["merge-base", "--is-ancestor", ancestor, descendant]);
|
|
632
|
+
if (res.status === 0) return { isAncestor: true, error: null };
|
|
633
|
+
if (res.status === 1) return { isAncestor: false, error: null };
|
|
634
|
+
return { isAncestor: null, error: res.error || res.stderr || res.stdout || `git exited ${res.status}` };
|
|
635
|
+
}
|
|
636
|
+
function computeGitAncestry(worktreePath, base = "origin/main") {
|
|
637
|
+
if (!worktreePath) {
|
|
638
|
+
return unknownAncestry(base, "missing worktree path");
|
|
639
|
+
}
|
|
640
|
+
const head = gitCapture(worktreePath, ["rev-parse", "HEAD"]);
|
|
641
|
+
if (head.status !== 0) return unknownAncestry(base, head.error || head.stderr || head.stdout || "failed to resolve HEAD");
|
|
642
|
+
const baseHead = gitCapture(worktreePath, ["rev-parse", base]);
|
|
643
|
+
if (baseHead.status !== 0) {
|
|
644
|
+
return unknownAncestry(base, baseHead.error || baseHead.stderr || baseHead.stdout || `failed to resolve ${base}`, head.stdout.trim());
|
|
645
|
+
}
|
|
646
|
+
const headSha = head.stdout.trim();
|
|
647
|
+
const baseSha = baseHead.stdout.trim();
|
|
648
|
+
if (headSha === baseSha) {
|
|
649
|
+
return {
|
|
650
|
+
checked: true,
|
|
651
|
+
base,
|
|
652
|
+
head: headSha,
|
|
653
|
+
baseHead: baseSha,
|
|
654
|
+
baseIsAncestorOfHead: true,
|
|
655
|
+
headIsAncestorOfBase: true,
|
|
656
|
+
relation: "synced"
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
const baseIsAncestorOfHead = gitIsAncestor(worktreePath, baseSha, headSha);
|
|
660
|
+
const headIsAncestorOfBase = gitIsAncestor(worktreePath, headSha, baseSha);
|
|
661
|
+
const error = baseIsAncestorOfHead.error || headIsAncestorOfBase.error || void 0;
|
|
662
|
+
if (baseIsAncestorOfHead.isAncestor == null || headIsAncestorOfBase.isAncestor == null) {
|
|
663
|
+
return {
|
|
664
|
+
checked: false,
|
|
665
|
+
base,
|
|
666
|
+
head: headSha,
|
|
667
|
+
baseHead: baseSha,
|
|
668
|
+
baseIsAncestorOfHead: baseIsAncestorOfHead.isAncestor,
|
|
669
|
+
headIsAncestorOfBase: headIsAncestorOfBase.isAncestor,
|
|
670
|
+
relation: "unknown",
|
|
671
|
+
...error ? { error } : {}
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
const relation = baseIsAncestorOfHead.isAncestor ? "ahead" : headIsAncestorOfBase.isAncestor ? "merged" : "diverged";
|
|
675
|
+
return {
|
|
676
|
+
checked: true,
|
|
677
|
+
base,
|
|
678
|
+
head: headSha,
|
|
679
|
+
baseHead: baseSha,
|
|
680
|
+
baseIsAncestorOfHead: baseIsAncestorOfHead.isAncestor,
|
|
681
|
+
headIsAncestorOfBase: headIsAncestorOfBase.isAncestor,
|
|
682
|
+
relation,
|
|
683
|
+
...error ? { error } : {}
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
function unknownAncestry(base, error, head = null) {
|
|
687
|
+
return {
|
|
688
|
+
checked: false,
|
|
689
|
+
base,
|
|
690
|
+
head,
|
|
691
|
+
baseHead: null,
|
|
692
|
+
baseIsAncestorOfHead: null,
|
|
693
|
+
headIsAncestorOfBase: null,
|
|
694
|
+
relation: "unknown",
|
|
695
|
+
error
|
|
696
|
+
};
|
|
697
|
+
}
|
|
596
698
|
function scrubClaudeEnv(env) {
|
|
597
699
|
const next = { ...env };
|
|
598
700
|
delete next.ANTHROPIC_API_KEY;
|
|
@@ -619,7 +721,7 @@ function computeAttention(input) {
|
|
|
619
721
|
}
|
|
620
722
|
return { state: "ok", reason: "recent activity" };
|
|
621
723
|
}
|
|
622
|
-
function computeWorkerStatus(worker) {
|
|
724
|
+
function computeWorkerStatus(worker, options = {}) {
|
|
623
725
|
const parsed = parseClaudeStream(worker.stdoutPath);
|
|
624
726
|
const heartbeat = parseHeartbeat(worker.heartbeatPath);
|
|
625
727
|
const alive = isPidAlive(worker.pid);
|
|
@@ -627,6 +729,7 @@ function computeWorkerStatus(worker) {
|
|
|
627
729
|
const stderrBytes = fileSize(worker.stderrPath);
|
|
628
730
|
const heartbeatBytes = fileSize(worker.heartbeatPath);
|
|
629
731
|
const changedFiles = gitStatusShort(worker.worktreePath);
|
|
732
|
+
const gitAncestry = computeGitAncestry(worker.worktreePath, options.base);
|
|
630
733
|
const lastActivityAt = latestIso([
|
|
631
734
|
parsed.lastEventAt,
|
|
632
735
|
heartbeat.lastHeartbeatAt,
|
|
@@ -668,7 +771,8 @@ function computeWorkerStatus(worker) {
|
|
|
668
771
|
heartbeatBlocker: heartbeat.heartbeatBlocker,
|
|
669
772
|
finalResult: parsed.finalResult,
|
|
670
773
|
error: parsed.error || (!alive && !parsed.finalResult ? tailFile(worker.stderrPath, 10).trim() || void 0 : void 0),
|
|
671
|
-
changedFiles
|
|
774
|
+
changedFiles,
|
|
775
|
+
gitAncestry
|
|
672
776
|
};
|
|
673
777
|
}
|
|
674
778
|
function isFinishedWorkerStatus(status) {
|
|
@@ -716,13 +820,23 @@ function computeAutoMaxWorkers(totalMemBytes, opts = {}) {
|
|
|
716
820
|
const raw = Math.max(1, Math.floor(budgetBytes / perWorkerMemBytes));
|
|
717
821
|
return Math.min(raw, AUTO_MAX_WORKERS_CEILING);
|
|
718
822
|
}
|
|
719
|
-
function
|
|
720
|
-
|
|
823
|
+
function readAvailableMemBytes() {
|
|
824
|
+
if (process.platform === "linux") {
|
|
825
|
+
try {
|
|
826
|
+
const meminfo = readFileSync5("/proc/meminfo", "utf8");
|
|
827
|
+
const match = meminfo.match(/^MemAvailable:\s+(\d+)\s*kB/m);
|
|
828
|
+
if (match) return Number(match[1]) * 1024;
|
|
829
|
+
} catch {
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
return os.freemem();
|
|
833
|
+
}
|
|
834
|
+
function countActiveWorkersForRun(run) {
|
|
721
835
|
let active = 0;
|
|
722
836
|
for (const name of Object.keys(run.workers || {})) {
|
|
723
837
|
const worker = readJson(
|
|
724
838
|
path5.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
725
|
-
|
|
839
|
+
void 0
|
|
726
840
|
);
|
|
727
841
|
if (!worker) continue;
|
|
728
842
|
const status = computeWorkerStatus(worker);
|
|
@@ -732,14 +846,19 @@ function countActiveWorkers(runId) {
|
|
|
732
846
|
}
|
|
733
847
|
return active;
|
|
734
848
|
}
|
|
849
|
+
function countActiveWorkersGlobal() {
|
|
850
|
+
let active = 0;
|
|
851
|
+
for (const run of listRunRecords()) active += countActiveWorkersForRun(run);
|
|
852
|
+
return active;
|
|
853
|
+
}
|
|
735
854
|
function observeRunnerResourceGate(input) {
|
|
736
855
|
const { perWorkerMemBytes, memReserveBytes, memUtilization, configuredMaxWorkers } = resolveResourceConfig(
|
|
737
856
|
input.config,
|
|
738
857
|
input.configuredMaxWorkersOverride
|
|
739
858
|
);
|
|
740
859
|
const totalMemBytes = input.totalMemBytes ?? os.totalmem();
|
|
741
|
-
const freeMemBytes = input.freeMemBytes ??
|
|
742
|
-
const activeWorkers = input.activeWorkers ??
|
|
860
|
+
const freeMemBytes = input.freeMemBytes ?? readAvailableMemBytes();
|
|
861
|
+
const activeWorkers = input.activeWorkers ?? countActiveWorkersGlobal();
|
|
743
862
|
const budgetBytes = Math.max(0, Math.floor(totalMemBytes * memUtilization) - memReserveBytes);
|
|
744
863
|
const capacityFromTotal = Math.max(0, Math.floor(budgetBytes / perWorkerMemBytes));
|
|
745
864
|
const capacityFromFree = Math.max(0, Math.floor(Math.max(0, freeMemBytes - memReserveBytes) / perWorkerMemBytes));
|
|
@@ -747,13 +866,13 @@ function observeRunnerResourceGate(input) {
|
|
|
747
866
|
const targetCap = configuredMaxWorkers ?? autoCap;
|
|
748
867
|
const maxConcurrentWorkers = Math.max(0, Math.min(targetCap, capacityFromTotal));
|
|
749
868
|
const slotsByCapacity = Math.max(0, maxConcurrentWorkers - activeWorkers);
|
|
750
|
-
const slotsByFreeMem =
|
|
869
|
+
const slotsByFreeMem = capacityFromFree;
|
|
751
870
|
const slotsAvailable = Math.min(slotsByCapacity, slotsByFreeMem);
|
|
752
871
|
let reason = null;
|
|
753
872
|
if (slotsAvailable <= 0) {
|
|
754
873
|
if (activeWorkers >= maxConcurrentWorkers) {
|
|
755
874
|
reason = `at worker limit (${activeWorkers}/${maxConcurrentWorkers} running)`;
|
|
756
|
-
} else if (capacityFromFree <=
|
|
875
|
+
} else if (capacityFromFree <= 0) {
|
|
757
876
|
reason = "insufficient free memory \u2014 waiting for workers to finish";
|
|
758
877
|
} else {
|
|
759
878
|
reason = "no worker slots available";
|
|
@@ -776,7 +895,7 @@ function observeRunnerResourceGate(input) {
|
|
|
776
895
|
}
|
|
777
896
|
|
|
778
897
|
// src/supervisor.ts
|
|
779
|
-
import { existsSync as
|
|
898
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync3 } from "node:fs";
|
|
780
899
|
import path7 from "node:path";
|
|
781
900
|
|
|
782
901
|
// src/prompt.ts
|
|
@@ -849,19 +968,19 @@ var claudeProvider = {
|
|
|
849
968
|
};
|
|
850
969
|
|
|
851
970
|
// src/providers/cursor.ts
|
|
852
|
-
import { closeSync as closeSync2, existsSync as
|
|
971
|
+
import { closeSync as closeSync2, existsSync as existsSync7, openSync as openSync2, readdirSync as readdirSync3 } from "node:fs";
|
|
853
972
|
import { spawn as spawn2 } from "node:child_process";
|
|
854
973
|
import path6 from "node:path";
|
|
855
974
|
var DEFAULT_CURSOR_MODEL = "composer-2.5";
|
|
856
975
|
function latestVersionDir(versionsRoot) {
|
|
857
|
-
if (!
|
|
858
|
-
const versions =
|
|
976
|
+
if (!existsSync7(versionsRoot)) return null;
|
|
977
|
+
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));
|
|
859
978
|
return versions[0] ? path6.join(versionsRoot, versions[0]) : null;
|
|
860
979
|
}
|
|
861
980
|
function resolveBundledCursor(versionDir) {
|
|
862
981
|
const nodeExe = path6.join(versionDir, "node.exe");
|
|
863
982
|
const indexJs = path6.join(versionDir, "index.js");
|
|
864
|
-
if (!
|
|
983
|
+
if (!existsSync7(nodeExe) || !existsSync7(indexJs)) return null;
|
|
865
984
|
return { executable: nodeExe, prefixArgs: [indexJs], shell: false, detached: true };
|
|
866
985
|
}
|
|
867
986
|
function resolveWindowsCursorSpawn(agentBin) {
|
|
@@ -884,7 +1003,7 @@ function resolveAgentBin() {
|
|
|
884
1003
|
if (configured) return configured;
|
|
885
1004
|
if (process.platform === "win32") {
|
|
886
1005
|
const localAgent = path6.join(process.env.LOCALAPPDATA || "", "cursor-agent", "agent.cmd");
|
|
887
|
-
if (
|
|
1006
|
+
if (existsSync7(localAgent)) return localAgent;
|
|
888
1007
|
}
|
|
889
1008
|
return "agent";
|
|
890
1009
|
}
|
|
@@ -953,8 +1072,11 @@ function resolveWorkerProvider(name) {
|
|
|
953
1072
|
|
|
954
1073
|
// src/supervisor.ts
|
|
955
1074
|
function spawnWorkerProcess(run, opts) {
|
|
956
|
-
const
|
|
957
|
-
if (!
|
|
1075
|
+
const rawName = typeof opts.name === "string" ? opts.name.trim() : "";
|
|
1076
|
+
if (!rawName || rawName === "undefined" || rawName === "null") {
|
|
1077
|
+
throw new Error(`worker name is required and must be a real identifier (got: ${JSON.stringify(opts.name)})`);
|
|
1078
|
+
}
|
|
1079
|
+
const name = safeSlug(rawName);
|
|
958
1080
|
if (run.workers?.[name]) throw new Error(`worker already exists in run ${run.id}: ${name}`);
|
|
959
1081
|
if (!opts.task) throw new Error(`missing task text for worker ${name}`);
|
|
960
1082
|
const { worktreesDir } = getPaths();
|
|
@@ -962,7 +1084,7 @@ function spawnWorkerProcess(run, opts) {
|
|
|
962
1084
|
mkdirSync3(workerDir, { recursive: true });
|
|
963
1085
|
const worktreePath = path7.join(worktreesDir, run.id, name);
|
|
964
1086
|
const branch = opts.branch || `agent/${run.id}/${name}`;
|
|
965
|
-
if (
|
|
1087
|
+
if (existsSync8(worktreePath)) throw new Error(`worktree path already exists: ${worktreePath}`);
|
|
966
1088
|
git(run.repo, ["fetch", "origin", "--prune"], { allowFailure: true });
|
|
967
1089
|
git(run.repo, ["worktree", "add", "-b", branch, worktreePath, run.baseCommit], { throwError: true });
|
|
968
1090
|
const stdoutPath = path7.join(workerDir, "stdout.jsonl");
|
|
@@ -1024,6 +1146,11 @@ function spawnWorkerProcess(run, opts) {
|
|
|
1024
1146
|
}
|
|
1025
1147
|
function startWorker(args) {
|
|
1026
1148
|
const run = loadRun(String(args.run));
|
|
1149
|
+
const name = typeof args.name === "string" ? args.name.trim() : "";
|
|
1150
|
+
if (!name) {
|
|
1151
|
+
console.error("worker start failed: --name is required");
|
|
1152
|
+
process.exit(1);
|
|
1153
|
+
}
|
|
1027
1154
|
const task = args.task ? String(args.task) : readMaybeFile(args.taskFile ? String(args.taskFile) : void 0);
|
|
1028
1155
|
if (!task) {
|
|
1029
1156
|
console.error("missing --task or --task-file");
|
|
@@ -1031,7 +1158,7 @@ function startWorker(args) {
|
|
|
1031
1158
|
}
|
|
1032
1159
|
try {
|
|
1033
1160
|
const worker = spawnWorkerProcess(run, {
|
|
1034
|
-
name
|
|
1161
|
+
name,
|
|
1035
1162
|
task,
|
|
1036
1163
|
ownedPaths: args.owned ? String(args.owned).split(",").map((s) => s.trim()).filter(Boolean) : [],
|
|
1037
1164
|
model: args.model ? String(args.model) : void 0,
|
|
@@ -1228,7 +1355,7 @@ async function sweepRun(args) {
|
|
|
1228
1355
|
for (const name of Object.keys(run.workers || {})) {
|
|
1229
1356
|
const worker = readJson(
|
|
1230
1357
|
path8.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
1231
|
-
|
|
1358
|
+
void 0
|
|
1232
1359
|
);
|
|
1233
1360
|
if (!worker || !worker.dispatched || !worker.taskId) continue;
|
|
1234
1361
|
const status = computeWorkerStatus(worker);
|
|
@@ -1270,7 +1397,7 @@ async function sweepRun(args) {
|
|
|
1270
1397
|
}
|
|
1271
1398
|
|
|
1272
1399
|
// src/worktree.ts
|
|
1273
|
-
import { existsSync as
|
|
1400
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync4 } from "node:fs";
|
|
1274
1401
|
import path10 from "node:path";
|
|
1275
1402
|
|
|
1276
1403
|
// src/validate.ts
|
|
@@ -1293,7 +1420,7 @@ function createRun(args) {
|
|
|
1293
1420
|
ensureGitRepo(repo);
|
|
1294
1421
|
const id = args.id ? validateRunId(String(args.id)) : timestampSlug(String(args.name || "run"));
|
|
1295
1422
|
const dir = runDirectory(id);
|
|
1296
|
-
if (
|
|
1423
|
+
if (existsSync9(dir)) failExists(`run already exists: ${id}`);
|
|
1297
1424
|
mkdirSync4(dir, { recursive: true });
|
|
1298
1425
|
const base = String(args.base || "origin/main");
|
|
1299
1426
|
const baseCommit = git(repo, ["rev-parse", base]).trim();
|
|
@@ -1312,7 +1439,7 @@ function createRun(args) {
|
|
|
1312
1439
|
}
|
|
1313
1440
|
function listRuns() {
|
|
1314
1441
|
const { runsDir } = getPaths();
|
|
1315
|
-
const rows = listRunIds(runsDir).map((id) => readJson(path10.join(runDirectory(id), "run.json"),
|
|
1442
|
+
const rows = listRunIds(runsDir).map((id) => readJson(path10.join(runDirectory(id), "run.json"), void 0)).filter(Boolean).map((run) => ({
|
|
1316
1443
|
id: run.id,
|
|
1317
1444
|
name: run.name,
|
|
1318
1445
|
status: run.status,
|
|
@@ -1425,12 +1552,12 @@ function runStatus(args) {
|
|
|
1425
1552
|
const workers = names.map((name) => {
|
|
1426
1553
|
const worker = readJson(
|
|
1427
1554
|
path11.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
1428
|
-
|
|
1555
|
+
void 0
|
|
1429
1556
|
);
|
|
1430
1557
|
if (!worker) {
|
|
1431
1558
|
return { worker: name, status: "missing", attention: "needs_attention", attentionReason: "worker.json not found" };
|
|
1432
1559
|
}
|
|
1433
|
-
const status = computeWorkerStatus(worker);
|
|
1560
|
+
const status = computeWorkerStatus(worker, { base: run.base });
|
|
1434
1561
|
return {
|
|
1435
1562
|
worker: status.worker,
|
|
1436
1563
|
status: status.status,
|
|
@@ -1444,7 +1571,9 @@ function runStatus(args) {
|
|
|
1444
1571
|
lastHeartbeatSummary: status.lastHeartbeatSummary,
|
|
1445
1572
|
heartbeatBlocker: status.heartbeatBlocker,
|
|
1446
1573
|
changedFileCount: status.changedFiles.length,
|
|
1447
|
-
branch: status.branch
|
|
1574
|
+
branch: status.branch,
|
|
1575
|
+
ancestry: status.gitAncestry.relation,
|
|
1576
|
+
ancestryChecked: status.gitAncestry.checked
|
|
1448
1577
|
};
|
|
1449
1578
|
});
|
|
1450
1579
|
const board = {
|
|
@@ -1489,10 +1618,48 @@ function stopWorker(args) {
|
|
|
1489
1618
|
}
|
|
1490
1619
|
|
|
1491
1620
|
// src/pipeline-tick.ts
|
|
1492
|
-
import
|
|
1621
|
+
import path14 from "node:path";
|
|
1493
1622
|
|
|
1494
|
-
// src/
|
|
1623
|
+
// src/finalize.ts
|
|
1495
1624
|
import path12 from "node:path";
|
|
1625
|
+
var ACTIVE_RUN_STATUSES = /* @__PURE__ */ new Set(["running", "dispatching", "pending", "queued"]);
|
|
1626
|
+
function terminalStatusFor(run) {
|
|
1627
|
+
const names = Object.keys(run.workers || {});
|
|
1628
|
+
if (names.length === 0) return "failed";
|
|
1629
|
+
let anyAlive = false;
|
|
1630
|
+
let anyResult = false;
|
|
1631
|
+
for (const name of names) {
|
|
1632
|
+
const worker = readJson(
|
|
1633
|
+
path12.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
1634
|
+
void 0
|
|
1635
|
+
);
|
|
1636
|
+
if (!worker) continue;
|
|
1637
|
+
const status = computeWorkerStatus(worker);
|
|
1638
|
+
if (status.alive && !status.finalResult) {
|
|
1639
|
+
anyAlive = true;
|
|
1640
|
+
break;
|
|
1641
|
+
}
|
|
1642
|
+
if (status.finalResult) anyResult = true;
|
|
1643
|
+
}
|
|
1644
|
+
if (anyAlive) return null;
|
|
1645
|
+
return anyResult ? "completed" : "failed";
|
|
1646
|
+
}
|
|
1647
|
+
function finalizeStaleRuns() {
|
|
1648
|
+
const finalized = [];
|
|
1649
|
+
for (const run of listRunRecords()) {
|
|
1650
|
+
if (!ACTIVE_RUN_STATUSES.has(run.status)) continue;
|
|
1651
|
+
const next = terminalStatusFor(run);
|
|
1652
|
+
if (!next || next === run.status) continue;
|
|
1653
|
+
const from = run.status;
|
|
1654
|
+
run.status = next;
|
|
1655
|
+
saveRun(run);
|
|
1656
|
+
finalized.push({ runId: run.id, from, to: next });
|
|
1657
|
+
}
|
|
1658
|
+
return finalized;
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
// src/plan-progress-daemon-sync.ts
|
|
1662
|
+
import path13 from "node:path";
|
|
1496
1663
|
|
|
1497
1664
|
// src/plan-progress-sync.ts
|
|
1498
1665
|
async function syncPlanProgress(args) {
|
|
@@ -1516,8 +1683,8 @@ async function syncActiveWorkerPlanProgress(runId, args) {
|
|
|
1516
1683
|
const outcomes = [];
|
|
1517
1684
|
for (const name of Object.keys(run.workers || {})) {
|
|
1518
1685
|
const worker = readJson(
|
|
1519
|
-
|
|
1520
|
-
|
|
1686
|
+
path13.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
1687
|
+
void 0
|
|
1521
1688
|
);
|
|
1522
1689
|
if (!worker?.dispatched || !worker.taskId) continue;
|
|
1523
1690
|
const status = computeWorkerStatus(worker);
|
|
@@ -1570,13 +1737,12 @@ async function completeFinishedWorkers(runId, args) {
|
|
|
1570
1737
|
const outcomes = [];
|
|
1571
1738
|
for (const name of Object.keys(run.workers || {})) {
|
|
1572
1739
|
const worker = readJson(
|
|
1573
|
-
|
|
1574
|
-
|
|
1740
|
+
path14.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
1741
|
+
void 0
|
|
1575
1742
|
);
|
|
1576
1743
|
if (!worker?.dispatched || !worker.taskId) continue;
|
|
1577
1744
|
const status = computeWorkerStatus(worker);
|
|
1578
1745
|
if (!isFinishedWorkerStatus(status)) continue;
|
|
1579
|
-
if (!status.finalResult) continue;
|
|
1580
1746
|
const result = await tryCompleteWorker({
|
|
1581
1747
|
run: runId,
|
|
1582
1748
|
name,
|
|
@@ -1604,6 +1770,7 @@ async function runPipelineTick(args) {
|
|
|
1604
1770
|
const agentOsId = String(required(String(args.agentOsId || ""), "--agent-os-id"));
|
|
1605
1771
|
const execute = args.execute !== false && args.execute !== "false";
|
|
1606
1772
|
runStatus({ run: runId });
|
|
1773
|
+
const finalizedStaleRuns = finalizeStaleRuns();
|
|
1607
1774
|
const completedWorkers = await completeFinishedWorkers(runId, args);
|
|
1608
1775
|
const planProgressSync = await syncActiveWorkerPlanProgress(runId, args);
|
|
1609
1776
|
const workspacePrefs = await fetchWorkspaceRuntimePreferences(agentOsId, args);
|
|
@@ -1645,6 +1812,7 @@ async function runPipelineTick(args) {
|
|
|
1645
1812
|
execute,
|
|
1646
1813
|
resourceGate,
|
|
1647
1814
|
completedWorkers,
|
|
1815
|
+
finalizedStaleRuns,
|
|
1648
1816
|
planProgressSync,
|
|
1649
1817
|
operatorTick,
|
|
1650
1818
|
sweep,
|