claude-overnight 1.5.1 → 1.6.1
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/index.js +308 -78
- package/dist/planner.js +8 -2
- package/dist/types.d.ts +4 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readFileSync, existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from "fs";
|
|
2
|
+
import { readFileSync, existsSync, mkdirSync, readdirSync, rmSync, writeFileSync, symlinkSync, unlinkSync } from "fs";
|
|
3
3
|
import { resolve, dirname, join } from "path";
|
|
4
4
|
import { fileURLToPath } from "url";
|
|
5
5
|
import { execSync } from "child_process";
|
|
@@ -317,19 +317,23 @@ function loadRunState(runDir) {
|
|
|
317
317
|
return null;
|
|
318
318
|
}
|
|
319
319
|
}
|
|
320
|
-
/** Find
|
|
321
|
-
function
|
|
320
|
+
/** Find all incomplete runs for a given working directory, newest first. */
|
|
321
|
+
function findIncompleteRuns(rootDir, filterCwd) {
|
|
322
322
|
const runsDir = join(rootDir, "runs");
|
|
323
323
|
try {
|
|
324
|
-
const dirs = readdirSync(runsDir).sort().reverse();
|
|
324
|
+
const dirs = readdirSync(runsDir).sort().reverse();
|
|
325
|
+
const results = [];
|
|
325
326
|
for (const d of dirs) {
|
|
326
327
|
const state = loadRunState(join(runsDir, d));
|
|
327
|
-
if (state && state.phase !== "done")
|
|
328
|
-
|
|
328
|
+
if (state && state.phase !== "done" && state.cwd === filterCwd) {
|
|
329
|
+
results.push({ dir: join(runsDir, d), state });
|
|
330
|
+
}
|
|
329
331
|
}
|
|
332
|
+
return results;
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
return [];
|
|
330
336
|
}
|
|
331
|
-
catch { }
|
|
332
|
-
return null;
|
|
333
337
|
}
|
|
334
338
|
/** Find orphaned designs: a run where thinking succeeded but orchestration crashed (has designs, no run.json). */
|
|
335
339
|
function findOrphanedDesigns(rootDir) {
|
|
@@ -349,6 +353,49 @@ function findOrphanedDesigns(rootDir) {
|
|
|
349
353
|
catch { }
|
|
350
354
|
return null;
|
|
351
355
|
}
|
|
356
|
+
function formatTimeAgo(isoStr) {
|
|
357
|
+
const ms = Date.now() - new Date(isoStr).getTime();
|
|
358
|
+
const mins = Math.floor(ms / 60000);
|
|
359
|
+
if (mins < 1)
|
|
360
|
+
return "just now";
|
|
361
|
+
if (mins < 60)
|
|
362
|
+
return `${mins}m ago`;
|
|
363
|
+
const hours = Math.floor(mins / 60);
|
|
364
|
+
if (hours < 24)
|
|
365
|
+
return `${hours}h ago`;
|
|
366
|
+
const days = Math.floor(hours / 24);
|
|
367
|
+
return `${days}d ago`;
|
|
368
|
+
}
|
|
369
|
+
function showRunHistory(allRuns, filterCwd) {
|
|
370
|
+
const runs = allRuns.filter(r => r.state.cwd === filterCwd);
|
|
371
|
+
if (runs.length === 0) {
|
|
372
|
+
console.log(chalk.dim("\n No run history.\n"));
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
const w = Math.min((process.stdout.columns ?? 80) - 6, 50);
|
|
376
|
+
console.log(chalk.dim(`\n ── Run History ${"─".repeat(Math.max(0, w - 16))}\n`));
|
|
377
|
+
let resumeIdx = 0;
|
|
378
|
+
for (const run of runs) {
|
|
379
|
+
const s = run.state;
|
|
380
|
+
const done = s.phase === "done";
|
|
381
|
+
const icon = done ? chalk.green("✓") : chalk.yellow("⚠");
|
|
382
|
+
const date = s.startedAt?.slice(0, 16).replace("T", " ") || "unknown";
|
|
383
|
+
const cost = s.accCost > 0 ? ` · $${s.accCost.toFixed(2)}` : "";
|
|
384
|
+
const obj = s.objective?.slice(0, 50) || "";
|
|
385
|
+
const num = done ? " " : chalk.cyan(String(++resumeIdx));
|
|
386
|
+
const merged = s.branches.filter(b => b.status === "merged").length;
|
|
387
|
+
console.log(` ${icon} ${num} ${chalk.dim(date)} · ${s.phase} · ${s.accCompleted}/${s.budget}${cost}${merged ? ` · ${merged} merged` : ""}`);
|
|
388
|
+
console.log(` ${obj}${obj.length >= 50 ? "…" : ""}`);
|
|
389
|
+
let status = "";
|
|
390
|
+
try {
|
|
391
|
+
status = readFileSync(join(run.dir, "status.md"), "utf-8").trim().split("\n")[0].slice(0, 70);
|
|
392
|
+
}
|
|
393
|
+
catch { }
|
|
394
|
+
if (status)
|
|
395
|
+
console.log(chalk.dim(` ${status}`));
|
|
396
|
+
console.log("");
|
|
397
|
+
}
|
|
398
|
+
}
|
|
352
399
|
/** Read final status + goal from all completed previous runs (newest first, max 5). */
|
|
353
400
|
function readPreviousRunKnowledge(rootDir) {
|
|
354
401
|
const runsDir = join(rootDir, "runs");
|
|
@@ -389,8 +436,20 @@ function createRunDir(rootDir) {
|
|
|
389
436
|
mkdirSync(join(runDir, "verifications"), { recursive: true });
|
|
390
437
|
mkdirSync(join(runDir, "milestones"), { recursive: true });
|
|
391
438
|
mkdirSync(join(runDir, "sessions"), { recursive: true });
|
|
439
|
+
updateLatestSymlink(rootDir, runDir);
|
|
392
440
|
return runDir;
|
|
393
441
|
}
|
|
442
|
+
function updateLatestSymlink(rootDir, runDir) {
|
|
443
|
+
const link = join(rootDir, "latest");
|
|
444
|
+
try {
|
|
445
|
+
unlinkSync(link);
|
|
446
|
+
}
|
|
447
|
+
catch { }
|
|
448
|
+
try {
|
|
449
|
+
symlinkSync(runDir, link);
|
|
450
|
+
}
|
|
451
|
+
catch { }
|
|
452
|
+
}
|
|
394
453
|
function saveWaveSession(baseDir, waveNum, kind, swarm) {
|
|
395
454
|
const dir = join(baseDir, "sessions");
|
|
396
455
|
mkdirSync(dir, { recursive: true });
|
|
@@ -410,6 +469,35 @@ function saveWaveSession(baseDir, waveNum, kind, swarm) {
|
|
|
410
469
|
totalCost: swarm.totalCostUsd,
|
|
411
470
|
}, null, 2), "utf-8");
|
|
412
471
|
}
|
|
472
|
+
/** Rebuild waveHistory from saved session files on resume. */
|
|
473
|
+
function loadWaveHistory(runDir) {
|
|
474
|
+
const dir = join(runDir, "sessions");
|
|
475
|
+
try {
|
|
476
|
+
return readdirSync(dir)
|
|
477
|
+
.filter(f => f.startsWith("wave-") && f.endsWith(".json"))
|
|
478
|
+
.sort((a, b) => {
|
|
479
|
+
const numA = parseInt(a.replace("wave-", "").replace(".json", ""));
|
|
480
|
+
const numB = parseInt(b.replace("wave-", "").replace(".json", ""));
|
|
481
|
+
return numA - numB;
|
|
482
|
+
})
|
|
483
|
+
.map(f => {
|
|
484
|
+
const data = JSON.parse(readFileSync(join(dir, f), "utf-8"));
|
|
485
|
+
return {
|
|
486
|
+
wave: data.wave,
|
|
487
|
+
kind: data.kind,
|
|
488
|
+
tasks: (data.agents || []).map((a) => ({
|
|
489
|
+
prompt: a.prompt,
|
|
490
|
+
status: a.status,
|
|
491
|
+
filesChanged: a.filesChanged,
|
|
492
|
+
error: a.error,
|
|
493
|
+
})),
|
|
494
|
+
};
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
catch {
|
|
498
|
+
return [];
|
|
499
|
+
}
|
|
500
|
+
}
|
|
413
501
|
function recordBranches(swarm, branches) {
|
|
414
502
|
for (const a of swarm.agents) {
|
|
415
503
|
if (a.branch) {
|
|
@@ -596,16 +684,17 @@ async function main() {
|
|
|
596
684
|
// ── Show run history ──
|
|
597
685
|
const rootDir = join(cwd, ".claude-overnight");
|
|
598
686
|
const runsDir = join(rootDir, "runs");
|
|
599
|
-
|
|
687
|
+
const allRuns = [];
|
|
600
688
|
try {
|
|
601
689
|
const dirs = readdirSync(runsDir).sort().reverse();
|
|
602
690
|
for (const d of dirs) {
|
|
603
691
|
const s = loadRunState(join(runsDir, d));
|
|
604
|
-
if (s
|
|
605
|
-
|
|
692
|
+
if (s)
|
|
693
|
+
allRuns.push({ dir: join(runsDir, d), state: s });
|
|
606
694
|
}
|
|
607
695
|
}
|
|
608
696
|
catch { }
|
|
697
|
+
const completedRuns = allRuns.filter(r => r.state.phase === "done" && r.state.cwd === cwd);
|
|
609
698
|
if (completedRuns.length > 0 && !noTTY) {
|
|
610
699
|
console.log(chalk.dim(`\n ${completedRuns.length} previous run${completedRuns.length > 1 ? "s" : ""}`));
|
|
611
700
|
for (const r of completedRuns.slice(0, 3)) {
|
|
@@ -614,7 +703,6 @@ async function main() {
|
|
|
614
703
|
const cost = r.state.accCost > 0 ? ` · $${r.state.accCost.toFixed(0)}` : "";
|
|
615
704
|
const merged = r.state.branches.filter(b => b.status === "merged").length;
|
|
616
705
|
console.log(chalk.dim(` ${date} · ${r.state.accCompleted} done · ${merged} merged${cost}${obj ? ` · ${obj}` : ""}${obj.length >= 50 ? "…" : ""}`));
|
|
617
|
-
// Show status if available
|
|
618
706
|
let status = "";
|
|
619
707
|
try {
|
|
620
708
|
status = readFileSync(join(r.dir, "status.md"), "utf-8").trim().split("\n")[0].slice(0, 80);
|
|
@@ -628,49 +716,115 @@ async function main() {
|
|
|
628
716
|
let resuming = false;
|
|
629
717
|
let resumeState = null;
|
|
630
718
|
let resumeRunDir;
|
|
631
|
-
const
|
|
632
|
-
if (
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
719
|
+
const incompleteRuns = findIncompleteRuns(rootDir, cwd);
|
|
720
|
+
if (incompleteRuns.length > 0 && !noTTY && tasks.length === 0) {
|
|
721
|
+
let decided = false;
|
|
722
|
+
while (!decided) {
|
|
723
|
+
if (incompleteRuns.length === 1) {
|
|
724
|
+
// Single incomplete run — show detailed box
|
|
725
|
+
const run = incompleteRuns[0];
|
|
726
|
+
const prev = run.state;
|
|
727
|
+
const merged = prev.branches.filter(b => b.status === "merged").length;
|
|
728
|
+
const unmerged = prev.branches.filter(b => b.status === "unmerged").length;
|
|
729
|
+
const failed = prev.branches.filter(b => b.status === "failed" || b.status === "merge-failed").length;
|
|
730
|
+
const obj = prev.objective?.slice(0, 50) || "";
|
|
731
|
+
const ago = formatTimeAgo(prev.startedAt);
|
|
732
|
+
let lastStatus = "";
|
|
733
|
+
try {
|
|
734
|
+
lastStatus = readFileSync(join(run.dir, "status.md"), "utf-8").trim().slice(0, 120);
|
|
735
|
+
}
|
|
736
|
+
catch { }
|
|
737
|
+
console.log(chalk.yellow(`\n ⚠ Unfinished run`) + chalk.dim(` · ${ago}`));
|
|
738
|
+
const boxLines = [
|
|
739
|
+
`${obj}${obj.length >= 50 ? "…" : ""}`,
|
|
740
|
+
`${prev.accCompleted}/${prev.budget} sessions · ${Math.max(1, (prev.budget ?? 0) - prev.accCompleted)} remaining · $${prev.accCost.toFixed(2)}`,
|
|
741
|
+
`Wave ${prev.waveNum + 1} · ${prev.phase}`,
|
|
742
|
+
];
|
|
743
|
+
if (lastStatus)
|
|
744
|
+
boxLines.push(lastStatus);
|
|
745
|
+
if (merged + unmerged + failed > 0)
|
|
746
|
+
boxLines.push(`${merged} merged · ${unmerged} unmerged · ${failed} failed`);
|
|
747
|
+
const boxW = Math.max(...boxLines.map(l => l.length)) + 4;
|
|
748
|
+
console.log(chalk.dim(` ╭${"─".repeat(boxW)}╮`));
|
|
749
|
+
for (const line of boxLines)
|
|
750
|
+
console.log(chalk.dim(" │") + ` ${line.padEnd(boxW - 2)}` + chalk.dim("│"));
|
|
751
|
+
console.log(chalk.dim(` ╰${"─".repeat(boxW)}╯`));
|
|
752
|
+
const action = await selectKey("", [
|
|
753
|
+
{ key: "r", desc: "esume" },
|
|
754
|
+
{ key: "h", desc: "istory" },
|
|
755
|
+
{ key: "f", desc: "resh" },
|
|
756
|
+
{ key: "q", desc: "uit" },
|
|
757
|
+
]);
|
|
758
|
+
if (action === "q")
|
|
759
|
+
process.exit(0);
|
|
760
|
+
if (action === "f") {
|
|
761
|
+
decided = true;
|
|
762
|
+
break;
|
|
763
|
+
}
|
|
764
|
+
if (action === "h") {
|
|
765
|
+
showRunHistory(allRuns, cwd);
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
resuming = true;
|
|
769
|
+
resumeState = prev;
|
|
770
|
+
resumeRunDir = run.dir;
|
|
771
|
+
decided = true;
|
|
772
|
+
}
|
|
773
|
+
else {
|
|
774
|
+
// Multiple incomplete runs — show numbered list (cap at 9 for single-keystroke selection)
|
|
775
|
+
const shown = incompleteRuns.slice(0, 9);
|
|
776
|
+
console.log(chalk.yellow(`\n ⚠ ${incompleteRuns.length} unfinished runs${incompleteRuns.length > 9 ? ` (showing newest 9)` : ""}\n`));
|
|
777
|
+
for (let i = 0; i < shown.length; i++) {
|
|
778
|
+
const run = shown[i];
|
|
779
|
+
const s = run.state;
|
|
780
|
+
const ago = formatTimeAgo(s.startedAt);
|
|
781
|
+
const obj = s.objective?.slice(0, 50) || "";
|
|
782
|
+
const merged = s.branches.filter(b => b.status === "merged").length;
|
|
783
|
+
let lastStatus = "";
|
|
784
|
+
try {
|
|
785
|
+
lastStatus = readFileSync(join(run.dir, "status.md"), "utf-8").trim().split("\n")[0].slice(0, 70);
|
|
786
|
+
}
|
|
787
|
+
catch { }
|
|
788
|
+
console.log(chalk.cyan(` ${i + 1}`) + ` ${obj}${obj.length >= 50 ? "…" : ""}`);
|
|
789
|
+
console.log(chalk.dim(` ${s.accCompleted}/${s.budget} · $${s.accCost.toFixed(2)} · ${ago} · ${s.phase} at wave ${s.waveNum + 1}${merged ? ` · ${merged} merged` : ""}`));
|
|
790
|
+
if (lastStatus)
|
|
791
|
+
console.log(chalk.dim(` ${lastStatus}`));
|
|
792
|
+
console.log("");
|
|
793
|
+
}
|
|
794
|
+
const action = await selectKey(` ${chalk.dim(`[1-${shown.length}] resume`)}`, [
|
|
795
|
+
...shown.map((_, i) => ({ key: String(i + 1), desc: "" })),
|
|
796
|
+
{ key: "h", desc: "istory" },
|
|
797
|
+
{ key: "f", desc: "resh" },
|
|
798
|
+
{ key: "q", desc: "uit" },
|
|
799
|
+
]);
|
|
800
|
+
if (action === "q")
|
|
801
|
+
process.exit(0);
|
|
802
|
+
if (action === "f") {
|
|
803
|
+
decided = true;
|
|
804
|
+
break;
|
|
805
|
+
}
|
|
806
|
+
if (action === "h") {
|
|
807
|
+
showRunHistory(allRuns, cwd);
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
const idx = parseInt(action) - 1;
|
|
811
|
+
if (idx >= 0 && idx < shown.length) {
|
|
812
|
+
resuming = true;
|
|
813
|
+
resumeState = shown[idx].state;
|
|
814
|
+
resumeRunDir = shown[idx].dir;
|
|
815
|
+
decided = true;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
666
818
|
}
|
|
667
|
-
if (
|
|
668
|
-
|
|
669
|
-
resumeState = prev;
|
|
670
|
-
resumeRunDir = incomplete.dir;
|
|
819
|
+
if (resuming && resumeState && resumeRunDir) {
|
|
820
|
+
const unmerged = resumeState.branches.filter(b => b.status === "unmerged").length;
|
|
671
821
|
if (unmerged > 0) {
|
|
672
822
|
console.log("");
|
|
673
|
-
autoMergeBranches(cwd,
|
|
823
|
+
autoMergeBranches(cwd, resumeState.branches, (msg) => console.log(chalk.dim(` ${msg}`)));
|
|
824
|
+
try {
|
|
825
|
+
saveRunState(resumeRunDir, resumeState);
|
|
826
|
+
}
|
|
827
|
+
catch { }
|
|
674
828
|
}
|
|
675
829
|
}
|
|
676
830
|
}
|
|
@@ -683,7 +837,18 @@ async function main() {
|
|
|
683
837
|
let usageCap;
|
|
684
838
|
let allowExtraUsage = false;
|
|
685
839
|
let extraUsageBudget;
|
|
686
|
-
if (
|
|
840
|
+
if (resuming) {
|
|
841
|
+
// Skip interactive flow entirely — all config is restored from saved state later
|
|
842
|
+
workerModel = resumeState.workerModel;
|
|
843
|
+
plannerModel = resumeState.plannerModel;
|
|
844
|
+
budget = resumeState.budget;
|
|
845
|
+
concurrency = resumeState.concurrency;
|
|
846
|
+
objective = resumeState.objective;
|
|
847
|
+
usageCap = resumeState.usageCap;
|
|
848
|
+
allowExtraUsage = resumeState.allowExtraUsage ?? false;
|
|
849
|
+
extraUsageBudget = resumeState.extraUsageBudget;
|
|
850
|
+
}
|
|
851
|
+
else if (!nonInteractive) {
|
|
687
852
|
// ① Objective
|
|
688
853
|
while (true) {
|
|
689
854
|
objective = await ask(`\n ${chalk.cyan("①")} ${chalk.bold("What should the agents do?")}\n ${chalk.cyan(">")} `);
|
|
@@ -819,11 +984,11 @@ async function main() {
|
|
|
819
984
|
}
|
|
820
985
|
}
|
|
821
986
|
validateConcurrency(concurrency);
|
|
822
|
-
|
|
823
|
-
|
|
987
|
+
let permissionMode = resuming ? resumeState.permissionMode : (fileCfg?.permissionMode ?? "auto");
|
|
988
|
+
let useWorktrees = resuming ? resumeState.useWorktrees : (fileCfg?.useWorktrees ?? isGitRepo(cwd));
|
|
824
989
|
if (useWorktrees)
|
|
825
990
|
validateGitRepo(cwd);
|
|
826
|
-
|
|
991
|
+
let mergeStrategy = resuming ? resumeState.mergeStrategy : (fileCfg?.mergeStrategy ?? "yolo");
|
|
827
992
|
if (nonInteractive) {
|
|
828
993
|
const capStr = usageCap != null ? ` cap=${Math.round(usageCap * 100)}%` : "";
|
|
829
994
|
const extraStr = allowExtraUsage ? (extraUsageBudget ? ` extra=$${extraUsageBudget}` : " extra=∞") : " extra=off";
|
|
@@ -838,9 +1003,11 @@ async function main() {
|
|
|
838
1003
|
// Create run directory — reuse orphaned run (thinking succeeded, orchestration crashed) if available
|
|
839
1004
|
const orphanedDir = !resuming ? findOrphanedDesigns(rootDir) : null;
|
|
840
1005
|
const runDir = resuming && resumeRunDir ? resumeRunDir : (orphanedDir ?? createRunDir(rootDir));
|
|
1006
|
+
if (resuming && resumeRunDir)
|
|
1007
|
+
updateLatestSymlink(rootDir, resumeRunDir);
|
|
841
1008
|
const previousKnowledge = readPreviousRunKnowledge(rootDir);
|
|
842
1009
|
// ── Plan phase (interactive: review loop, non-interactive: auto-plan or skip) ──
|
|
843
|
-
const needsPlan = tasks.length === 0;
|
|
1010
|
+
const needsPlan = tasks.length === 0 && !resuming;
|
|
844
1011
|
const designDir = join(runDir, "designs");
|
|
845
1012
|
if (needsPlan) {
|
|
846
1013
|
if (noTTY) {
|
|
@@ -1054,7 +1221,7 @@ async function main() {
|
|
|
1054
1221
|
process.exit(1);
|
|
1055
1222
|
}
|
|
1056
1223
|
}
|
|
1057
|
-
if (tasks.length === 0) {
|
|
1224
|
+
if (tasks.length === 0 && !resuming) {
|
|
1058
1225
|
console.error("No tasks provided.");
|
|
1059
1226
|
process.exit(1);
|
|
1060
1227
|
}
|
|
@@ -1066,7 +1233,7 @@ async function main() {
|
|
|
1066
1233
|
// ── Run (wave loop) ──
|
|
1067
1234
|
process.stdout.write("\x1B[?25l");
|
|
1068
1235
|
const restore = () => process.stdout.write("\x1B[?25h\n");
|
|
1069
|
-
const runStartedAt = Date.now();
|
|
1236
|
+
const runStartedAt = resuming && resumeState?.startedAt ? new Date(resumeState.startedAt).getTime() : Date.now();
|
|
1070
1237
|
// Wave-loop state — either fresh or resumed
|
|
1071
1238
|
mkdirSync(join(runDir, "reflections"), { recursive: true });
|
|
1072
1239
|
mkdirSync(join(runDir, "milestones"), { recursive: true });
|
|
@@ -1084,27 +1251,26 @@ async function main() {
|
|
|
1084
1251
|
let overheadBudgetUsed;
|
|
1085
1252
|
const branches = [];
|
|
1086
1253
|
if (resuming && resumeState) {
|
|
1087
|
-
// Restore
|
|
1088
|
-
remaining
|
|
1254
|
+
// Restore wave-loop state (config already restored in the early resume block + ternaries above)
|
|
1255
|
+
// Recalculate remaining from budget — don't trust saved value which counts failures
|
|
1256
|
+
// and may have been zeroed by a premature "done" signal from the steerer.
|
|
1257
|
+
// The user explicitly chose Resume, so give back budget not yet successfully used.
|
|
1258
|
+
remaining = Math.max(1, (resumeState.budget ?? 0) - resumeState.accCompleted);
|
|
1089
1259
|
currentTasks = resumeState.currentTasks;
|
|
1090
1260
|
waveNum = resumeState.waveNum;
|
|
1091
1261
|
accCost = resumeState.accCost;
|
|
1092
1262
|
accCompleted = resumeState.accCompleted;
|
|
1093
1263
|
accFailed = resumeState.accFailed;
|
|
1094
|
-
accTools = 0;
|
|
1264
|
+
accTools = resumeState.accTools ?? 0;
|
|
1265
|
+
accIn = resumeState.accIn ?? 0;
|
|
1266
|
+
accOut = resumeState.accOut ?? 0;
|
|
1095
1267
|
lastWaveKind = resumeState.lastWaveKind;
|
|
1096
1268
|
overheadBudgetUsed = resumeState.overheadBudgetUsed ?? (resumeState.reflectionBudgetUsed ?? 0) + (resumeState.verificationBudgetUsed ?? 0);
|
|
1097
1269
|
branches.push(...resumeState.branches);
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
concurrency = resumeState.concurrency;
|
|
1103
|
-
flex = resumeState.flex;
|
|
1104
|
-
usageCap = resumeState.usageCap;
|
|
1105
|
-
allowExtraUsage = resumeState.allowExtraUsage ?? false;
|
|
1106
|
-
extraUsageBudget = resumeState.extraUsageBudget;
|
|
1107
|
-
console.log(chalk.green(`\n ✓ Resumed`) + chalk.dim(` · wave ${waveNum + 1} · ${remaining} remaining · $${accCost.toFixed(2)} spent\n`));
|
|
1270
|
+
flex = resumeState.flex; // override computed flex with saved value
|
|
1271
|
+
waveHistory.push(...loadWaveHistory(runDir));
|
|
1272
|
+
console.log(chalk.green(`\n ✓ Resumed`) + chalk.dim(` · wave ${waveNum + 1} · ${remaining} remaining · $${accCost.toFixed(2)} spent · ${waveHistory.length} prior waves\n`));
|
|
1273
|
+
waveNum++; // advance past last completed wave so next session file doesn't overwrite
|
|
1108
1274
|
}
|
|
1109
1275
|
else {
|
|
1110
1276
|
// Fresh run
|
|
@@ -1131,7 +1297,7 @@ async function main() {
|
|
|
1131
1297
|
// For flex + branch strategy: create one target branch, waves merge via yolo into it
|
|
1132
1298
|
let runBranch;
|
|
1133
1299
|
let originalRef;
|
|
1134
|
-
if (flex && mergeStrategy === "branch" && useWorktrees) {
|
|
1300
|
+
if (flex && mergeStrategy === "branch" && useWorktrees && !resuming) {
|
|
1135
1301
|
try {
|
|
1136
1302
|
originalRef = execSync("git rev-parse --abbrev-ref HEAD", { cwd, encoding: "utf-8", stdio: "pipe" }).trim();
|
|
1137
1303
|
if (originalRef === "HEAD")
|
|
@@ -1160,6 +1326,60 @@ async function main() {
|
|
|
1160
1326
|
process.on("SIGTERM", () => gracefulStop("SIGTERM"));
|
|
1161
1327
|
process.on("uncaughtException", (err) => { currentSwarm?.abort(); currentSwarm?.cleanup(); restore(); console.error(chalk.red(`\n Uncaught: ${err.message}`)); process.exit(1); });
|
|
1162
1328
|
process.on("unhandledRejection", (reason) => { currentSwarm?.abort(); currentSwarm?.cleanup(); restore(); console.error(chalk.red(`\n Unhandled: ${reason instanceof Error ? reason.message : reason}`)); process.exit(1); });
|
|
1329
|
+
// When resuming a flex run with no queued tasks, steer immediately to get the next wave
|
|
1330
|
+
if (resuming && flex && currentTasks.length === 0 && remaining > 0) {
|
|
1331
|
+
let steerAttempts = 0;
|
|
1332
|
+
while (currentTasks.length === 0 && remaining > 0 && !objectiveComplete && steerAttempts < 3) {
|
|
1333
|
+
steerAttempts++;
|
|
1334
|
+
console.log(chalk.cyan(`\n ◆ Assessing...\n`));
|
|
1335
|
+
process.stdout.write("\x1B[?25l");
|
|
1336
|
+
try {
|
|
1337
|
+
const memory = readRunMemory(runDir, previousKnowledge || undefined);
|
|
1338
|
+
const steer = await steerWave(objective, waveHistory, remaining, cwd, plannerModel, workerModel, permissionMode, concurrency, makeProgressLog(), memory);
|
|
1339
|
+
process.stdout.write(`\x1B[2K\r`);
|
|
1340
|
+
process.stdout.write("\x1B[?25h");
|
|
1341
|
+
if (steer.statusUpdate)
|
|
1342
|
+
writeStatus(runDir, steer.statusUpdate);
|
|
1343
|
+
if (steer.goalUpdate)
|
|
1344
|
+
writeGoalUpdate(runDir, steer.goalUpdate);
|
|
1345
|
+
if (steer.done || steer.tasks.length === 0) {
|
|
1346
|
+
const hasVerification = waveHistory.some(w => w.kind.includes("verif"));
|
|
1347
|
+
if (!hasVerification && remaining >= 1) {
|
|
1348
|
+
console.log(chalk.dim(` ${steer.reasoning}`));
|
|
1349
|
+
console.log(chalk.yellow(` Done blocked — verification required before completion\n`));
|
|
1350
|
+
lastWaveKind = "done-blocked";
|
|
1351
|
+
continue;
|
|
1352
|
+
}
|
|
1353
|
+
console.log(chalk.green(` \u2713 ${steer.reasoning}\n`));
|
|
1354
|
+
objectiveComplete = true;
|
|
1355
|
+
remaining = 0;
|
|
1356
|
+
}
|
|
1357
|
+
else {
|
|
1358
|
+
const isOverhead = steer.waveKind !== "execute";
|
|
1359
|
+
if (isOverhead && overheadBudgetUsed + steer.tasks.length > maxOverheadBudget) {
|
|
1360
|
+
console.log(chalk.dim(` ${steer.reasoning}`));
|
|
1361
|
+
console.log(chalk.yellow(` Overhead budget exhausted (${overheadBudgetUsed}/${maxOverheadBudget}) — re-assessing\n`));
|
|
1362
|
+
lastWaveKind = "overhead-capped";
|
|
1363
|
+
continue;
|
|
1364
|
+
}
|
|
1365
|
+
console.log(chalk.dim(` ${steer.reasoning}\n`));
|
|
1366
|
+
currentTasks = steer.tasks.map(t => ({
|
|
1367
|
+
...t,
|
|
1368
|
+
model: t.model === "planner" ? plannerModel : t.model === "worker" ? workerModel
|
|
1369
|
+
: isOverhead && !t.model ? plannerModel : t.model,
|
|
1370
|
+
}));
|
|
1371
|
+
lastWaveKind = steer.waveKind;
|
|
1372
|
+
if (isOverhead)
|
|
1373
|
+
overheadBudgetUsed += currentTasks.length;
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
catch (err) {
|
|
1377
|
+
process.stdout.write("\x1B[?25h");
|
|
1378
|
+
console.log(chalk.yellow(` Steering failed: ${err.message?.slice(0, 80)} \u2014 stopping\n`));
|
|
1379
|
+
break;
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1163
1383
|
while (remaining > 0 && currentTasks.length > 0 && !stopping) {
|
|
1164
1384
|
if (currentTasks.length > remaining)
|
|
1165
1385
|
currentTasks = currentTasks.slice(0, remaining);
|
|
@@ -1197,7 +1417,11 @@ async function main() {
|
|
|
1197
1417
|
accCompleted += swarm.completed;
|
|
1198
1418
|
accFailed += swarm.failed;
|
|
1199
1419
|
accTools += swarm.agents.reduce((sum, a) => sum + a.toolCalls, 0);
|
|
1200
|
-
remaining
|
|
1420
|
+
remaining = Math.max(0, remaining - swarm.completed - swarm.failed);
|
|
1421
|
+
// Sanity check: remaining should never drop below budget - total consumed
|
|
1422
|
+
const expectedFloor = Math.max(0, (budget ?? 0) - accCompleted - accFailed);
|
|
1423
|
+
if (remaining < expectedFloor)
|
|
1424
|
+
remaining = expectedFloor;
|
|
1201
1425
|
// Apply live config changes if user adjusted budget/threshold mid-wave
|
|
1202
1426
|
if (liveConfig.dirty) {
|
|
1203
1427
|
remaining = liveConfig.remaining;
|
|
@@ -1212,8 +1436,8 @@ async function main() {
|
|
|
1212
1436
|
saveRunState(runDir, {
|
|
1213
1437
|
id: `run-${new Date().toISOString().slice(0, 19)}`, objective: objective, budget: budget ?? tasks.length,
|
|
1214
1438
|
remaining, workerModel, plannerModel, concurrency, permissionMode,
|
|
1215
|
-
usageCap, allowExtraUsage, extraUsageBudget, flex, useWorktrees, mergeStrategy, waveNum, currentTasks,
|
|
1216
|
-
lastWaveKind, overheadBudgetUsed, accCost, accCompleted, accFailed,
|
|
1439
|
+
usageCap, allowExtraUsage, extraUsageBudget, flex, useWorktrees, mergeStrategy, waveNum, currentTasks: [],
|
|
1440
|
+
lastWaveKind, overheadBudgetUsed, accCost, accCompleted, accFailed, accIn, accOut, accTools,
|
|
1217
1441
|
branches, phase: "steering", startedAt: new Date(runStartedAt).toISOString(), cwd,
|
|
1218
1442
|
});
|
|
1219
1443
|
waveHistory.push({
|
|
@@ -1284,10 +1508,12 @@ async function main() {
|
|
|
1284
1508
|
catch (err) {
|
|
1285
1509
|
process.stdout.write("\x1B[?25h");
|
|
1286
1510
|
console.log(chalk.yellow(` Steering failed: ${err.message?.slice(0, 80)} \u2014 stopping\n`));
|
|
1287
|
-
remaining
|
|
1511
|
+
// Don't zero out remaining — preserve unspent budget so resume works
|
|
1288
1512
|
break;
|
|
1289
1513
|
}
|
|
1290
1514
|
}
|
|
1515
|
+
if (!steered)
|
|
1516
|
+
break; // steering failed — stop, don't re-run old tasks
|
|
1291
1517
|
waveNum++;
|
|
1292
1518
|
}
|
|
1293
1519
|
// Only truly "done" if steering explicitly completed the objective (or non-flex single wave with budget exhausted)
|
|
@@ -1297,7 +1523,7 @@ async function main() {
|
|
|
1297
1523
|
id: `run-${new Date().toISOString().slice(0, 19)}`, objective: objective ?? "", budget: budget ?? tasks.length,
|
|
1298
1524
|
remaining, workerModel, plannerModel, concurrency, permissionMode,
|
|
1299
1525
|
usageCap, allowExtraUsage, extraUsageBudget, flex, useWorktrees, mergeStrategy, waveNum, currentTasks: [],
|
|
1300
|
-
lastWaveKind, overheadBudgetUsed, accCost, accCompleted, accFailed,
|
|
1526
|
+
lastWaveKind, overheadBudgetUsed, accCost, accCompleted, accFailed, accIn, accOut, accTools,
|
|
1301
1527
|
branches, phase: finalPhase, startedAt: new Date(runStartedAt).toISOString(), cwd,
|
|
1302
1528
|
});
|
|
1303
1529
|
if (trulyDone) {
|
|
@@ -1309,6 +1535,10 @@ async function main() {
|
|
|
1309
1535
|
rmSync(join(runDir, "reflections"), { recursive: true, force: true });
|
|
1310
1536
|
}
|
|
1311
1537
|
catch { }
|
|
1538
|
+
try {
|
|
1539
|
+
rmSync(join(runDir, "verifications"), { recursive: true, force: true });
|
|
1540
|
+
}
|
|
1541
|
+
catch { }
|
|
1312
1542
|
}
|
|
1313
1543
|
// Switch back if we created a run branch
|
|
1314
1544
|
if (runBranch && originalRef) {
|
package/dist/planner.js
CHANGED
|
@@ -643,7 +643,8 @@ export async function steerWave(objective, history, remainingBudget, cwd, planne
|
|
|
643
643
|
return `Wave ${w.wave + 1} (${w.kind}):\n${lines}`;
|
|
644
644
|
}).join("\n\n") : "(first wave)";
|
|
645
645
|
const lastKind = history.length > 0 ? history[history.length - 1].kind : "";
|
|
646
|
-
const
|
|
646
|
+
const isSyntheticKind = lastKind.includes("blocked") || lastKind.includes("capped");
|
|
647
|
+
const repeatHint = lastKind && lastKind !== "execute" && !isSyntheticKind
|
|
647
648
|
? `\nThe previous wave was "${lastKind}". Don't repeat the same wave kind unless you have a strong reason.\n`
|
|
648
649
|
: "";
|
|
649
650
|
const cap = (s, max) => s.length > max ? s.slice(0, max) + "\n...(truncated)" : s;
|
|
@@ -725,7 +726,12 @@ If done: {"done": true, "waveKind": "done", "reasoning": "...", "statusUpdate":
|
|
|
725
726
|
return first;
|
|
726
727
|
onLog("Retrying...");
|
|
727
728
|
const retryText = await runPlannerQuery(`Your previous response was not valid JSON. Respond with ONLY a JSON object {"done":false,"waveKind":"execute","reasoning":"...","statusUpdate":"...","tasks":[{"prompt":"..."}]}.\n\n${prompt}`, { cwd, model: plannerModel, permissionMode }, onLog);
|
|
728
|
-
|
|
729
|
+
const retryParsed = attemptJsonParse(retryText);
|
|
730
|
+
if (retryParsed)
|
|
731
|
+
return retryParsed;
|
|
732
|
+
// Don't return done:true on parse failure — that permanently marks the run complete.
|
|
733
|
+
// Throw so the caller's catch block handles it as a transient steering failure.
|
|
734
|
+
throw new Error("Could not parse steering response after retry");
|
|
729
735
|
})();
|
|
730
736
|
const isDone = parsed.done === true;
|
|
731
737
|
const waveKind = parsed.waveKind || parsed.action || (isDone ? "done" : "execute");
|
package/dist/types.d.ts
CHANGED
|
@@ -138,8 +138,11 @@ export interface RunState {
|
|
|
138
138
|
accCost: number;
|
|
139
139
|
accCompleted: number;
|
|
140
140
|
accFailed: number;
|
|
141
|
+
accIn?: number;
|
|
142
|
+
accOut?: number;
|
|
143
|
+
accTools?: number;
|
|
141
144
|
branches: BranchRecord[];
|
|
142
|
-
phase: "
|
|
145
|
+
phase: "steering" | "capped" | "done";
|
|
143
146
|
startedAt: string;
|
|
144
147
|
cwd: string;
|
|
145
148
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-overnight",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.1",
|
|
4
4
|
"description": "Run 10, 100, or 1000 Claude agents overnight. Parallel autonomous AI coding with thinking waves, iterative quality steering, crash recovery, and rate limit handling. Built on the Claude Agent SDK.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|