claude-overnight 1.6.0 → 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 +247 -99
- package/dist/planner.js +2 -1
- 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 });
|
|
@@ -625,16 +684,17 @@ async function main() {
|
|
|
625
684
|
// ── Show run history ──
|
|
626
685
|
const rootDir = join(cwd, ".claude-overnight");
|
|
627
686
|
const runsDir = join(rootDir, "runs");
|
|
628
|
-
|
|
687
|
+
const allRuns = [];
|
|
629
688
|
try {
|
|
630
689
|
const dirs = readdirSync(runsDir).sort().reverse();
|
|
631
690
|
for (const d of dirs) {
|
|
632
691
|
const s = loadRunState(join(runsDir, d));
|
|
633
|
-
if (s
|
|
634
|
-
|
|
692
|
+
if (s)
|
|
693
|
+
allRuns.push({ dir: join(runsDir, d), state: s });
|
|
635
694
|
}
|
|
636
695
|
}
|
|
637
696
|
catch { }
|
|
697
|
+
const completedRuns = allRuns.filter(r => r.state.phase === "done" && r.state.cwd === cwd);
|
|
638
698
|
if (completedRuns.length > 0 && !noTTY) {
|
|
639
699
|
console.log(chalk.dim(`\n ${completedRuns.length} previous run${completedRuns.length > 1 ? "s" : ""}`));
|
|
640
700
|
for (const r of completedRuns.slice(0, 3)) {
|
|
@@ -643,7 +703,6 @@ async function main() {
|
|
|
643
703
|
const cost = r.state.accCost > 0 ? ` · $${r.state.accCost.toFixed(0)}` : "";
|
|
644
704
|
const merged = r.state.branches.filter(b => b.status === "merged").length;
|
|
645
705
|
console.log(chalk.dim(` ${date} · ${r.state.accCompleted} done · ${merged} merged${cost}${obj ? ` · ${obj}` : ""}${obj.length >= 50 ? "…" : ""}`));
|
|
646
|
-
// Show status if available
|
|
647
706
|
let status = "";
|
|
648
707
|
try {
|
|
649
708
|
status = readFileSync(join(r.dir, "status.md"), "utf-8").trim().split("\n")[0].slice(0, 80);
|
|
@@ -657,52 +716,113 @@ async function main() {
|
|
|
657
716
|
let resuming = false;
|
|
658
717
|
let resumeState = null;
|
|
659
718
|
let resumeRunDir;
|
|
660
|
-
const
|
|
661
|
-
if (
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
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
|
+
}
|
|
695
818
|
}
|
|
696
|
-
if (
|
|
697
|
-
|
|
698
|
-
resumeState = prev;
|
|
699
|
-
resumeRunDir = incomplete.dir;
|
|
819
|
+
if (resuming && resumeState && resumeRunDir) {
|
|
820
|
+
const unmerged = resumeState.branches.filter(b => b.status === "unmerged").length;
|
|
700
821
|
if (unmerged > 0) {
|
|
701
822
|
console.log("");
|
|
702
|
-
autoMergeBranches(cwd,
|
|
703
|
-
// Persist merged branch statuses immediately so they survive a crash before next saveRunState
|
|
823
|
+
autoMergeBranches(cwd, resumeState.branches, (msg) => console.log(chalk.dim(` ${msg}`)));
|
|
704
824
|
try {
|
|
705
|
-
saveRunState(
|
|
825
|
+
saveRunState(resumeRunDir, resumeState);
|
|
706
826
|
}
|
|
707
827
|
catch { }
|
|
708
828
|
}
|
|
@@ -883,6 +1003,8 @@ async function main() {
|
|
|
883
1003
|
// Create run directory — reuse orphaned run (thinking succeeded, orchestration crashed) if available
|
|
884
1004
|
const orphanedDir = !resuming ? findOrphanedDesigns(rootDir) : null;
|
|
885
1005
|
const runDir = resuming && resumeRunDir ? resumeRunDir : (orphanedDir ?? createRunDir(rootDir));
|
|
1006
|
+
if (resuming && resumeRunDir)
|
|
1007
|
+
updateLatestSymlink(rootDir, resumeRunDir);
|
|
886
1008
|
const previousKnowledge = readPreviousRunKnowledge(rootDir);
|
|
887
1009
|
// ── Plan phase (interactive: review loop, non-interactive: auto-plan or skip) ──
|
|
888
1010
|
const needsPlan = tasks.length === 0 && !resuming;
|
|
@@ -1129,32 +1251,26 @@ async function main() {
|
|
|
1129
1251
|
let overheadBudgetUsed;
|
|
1130
1252
|
const branches = [];
|
|
1131
1253
|
if (resuming && resumeState) {
|
|
1132
|
-
// Restore
|
|
1133
|
-
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);
|
|
1134
1259
|
currentTasks = resumeState.currentTasks;
|
|
1135
1260
|
waveNum = resumeState.waveNum;
|
|
1136
1261
|
accCost = resumeState.accCost;
|
|
1137
1262
|
accCompleted = resumeState.accCompleted;
|
|
1138
1263
|
accFailed = resumeState.accFailed;
|
|
1139
|
-
accTools = 0;
|
|
1264
|
+
accTools = resumeState.accTools ?? 0;
|
|
1265
|
+
accIn = resumeState.accIn ?? 0;
|
|
1266
|
+
accOut = resumeState.accOut ?? 0;
|
|
1140
1267
|
lastWaveKind = resumeState.lastWaveKind;
|
|
1141
1268
|
overheadBudgetUsed = resumeState.overheadBudgetUsed ?? (resumeState.reflectionBudgetUsed ?? 0) + (resumeState.verificationBudgetUsed ?? 0);
|
|
1142
1269
|
branches.push(...resumeState.branches);
|
|
1143
|
-
|
|
1144
|
-
workerModel = resumeState.workerModel;
|
|
1145
|
-
plannerModel = resumeState.plannerModel;
|
|
1146
|
-
budget = resumeState.budget;
|
|
1147
|
-
concurrency = resumeState.concurrency;
|
|
1148
|
-
flex = resumeState.flex;
|
|
1149
|
-
usageCap = resumeState.usageCap;
|
|
1150
|
-
allowExtraUsage = resumeState.allowExtraUsage ?? false;
|
|
1151
|
-
extraUsageBudget = resumeState.extraUsageBudget;
|
|
1152
|
-
permissionMode = resumeState.permissionMode;
|
|
1153
|
-
useWorktrees = resumeState.useWorktrees;
|
|
1154
|
-
mergeStrategy = resumeState.mergeStrategy;
|
|
1155
|
-
// Restore wave history from saved session files so steerer has full context
|
|
1270
|
+
flex = resumeState.flex; // override computed flex with saved value
|
|
1156
1271
|
waveHistory.push(...loadWaveHistory(runDir));
|
|
1157
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
|
|
1158
1274
|
}
|
|
1159
1275
|
else {
|
|
1160
1276
|
// Fresh run
|
|
@@ -1212,35 +1328,57 @@ async function main() {
|
|
|
1212
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); });
|
|
1213
1329
|
// When resuming a flex run with no queued tasks, steer immediately to get the next wave
|
|
1214
1330
|
if (resuming && flex && currentTasks.length === 0 && remaining > 0) {
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
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
|
+
}
|
|
1233
1375
|
}
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
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;
|
|
1238
1380
|
}
|
|
1239
1381
|
}
|
|
1240
|
-
catch (err) {
|
|
1241
|
-
process.stdout.write("\x1B[?25h");
|
|
1242
|
-
console.log(chalk.yellow(` Steering failed: ${err.message?.slice(0, 80)} \u2014 stopping\n`));
|
|
1243
|
-
}
|
|
1244
1382
|
}
|
|
1245
1383
|
while (remaining > 0 && currentTasks.length > 0 && !stopping) {
|
|
1246
1384
|
if (currentTasks.length > remaining)
|
|
@@ -1279,7 +1417,11 @@ async function main() {
|
|
|
1279
1417
|
accCompleted += swarm.completed;
|
|
1280
1418
|
accFailed += swarm.failed;
|
|
1281
1419
|
accTools += swarm.agents.reduce((sum, a) => sum + a.toolCalls, 0);
|
|
1282
|
-
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;
|
|
1283
1425
|
// Apply live config changes if user adjusted budget/threshold mid-wave
|
|
1284
1426
|
if (liveConfig.dirty) {
|
|
1285
1427
|
remaining = liveConfig.remaining;
|
|
@@ -1295,7 +1437,7 @@ async function main() {
|
|
|
1295
1437
|
id: `run-${new Date().toISOString().slice(0, 19)}`, objective: objective, budget: budget ?? tasks.length,
|
|
1296
1438
|
remaining, workerModel, plannerModel, concurrency, permissionMode,
|
|
1297
1439
|
usageCap, allowExtraUsage, extraUsageBudget, flex, useWorktrees, mergeStrategy, waveNum, currentTasks: [],
|
|
1298
|
-
lastWaveKind, overheadBudgetUsed, accCost, accCompleted, accFailed,
|
|
1440
|
+
lastWaveKind, overheadBudgetUsed, accCost, accCompleted, accFailed, accIn, accOut, accTools,
|
|
1299
1441
|
branches, phase: "steering", startedAt: new Date(runStartedAt).toISOString(), cwd,
|
|
1300
1442
|
});
|
|
1301
1443
|
waveHistory.push({
|
|
@@ -1370,6 +1512,8 @@ async function main() {
|
|
|
1370
1512
|
break;
|
|
1371
1513
|
}
|
|
1372
1514
|
}
|
|
1515
|
+
if (!steered)
|
|
1516
|
+
break; // steering failed — stop, don't re-run old tasks
|
|
1373
1517
|
waveNum++;
|
|
1374
1518
|
}
|
|
1375
1519
|
// Only truly "done" if steering explicitly completed the objective (or non-flex single wave with budget exhausted)
|
|
@@ -1379,7 +1523,7 @@ async function main() {
|
|
|
1379
1523
|
id: `run-${new Date().toISOString().slice(0, 19)}`, objective: objective ?? "", budget: budget ?? tasks.length,
|
|
1380
1524
|
remaining, workerModel, plannerModel, concurrency, permissionMode,
|
|
1381
1525
|
usageCap, allowExtraUsage, extraUsageBudget, flex, useWorktrees, mergeStrategy, waveNum, currentTasks: [],
|
|
1382
|
-
lastWaveKind, overheadBudgetUsed, accCost, accCompleted, accFailed,
|
|
1526
|
+
lastWaveKind, overheadBudgetUsed, accCost, accCompleted, accFailed, accIn, accOut, accTools,
|
|
1383
1527
|
branches, phase: finalPhase, startedAt: new Date(runStartedAt).toISOString(), cwd,
|
|
1384
1528
|
});
|
|
1385
1529
|
if (trulyDone) {
|
|
@@ -1391,6 +1535,10 @@ async function main() {
|
|
|
1391
1535
|
rmSync(join(runDir, "reflections"), { recursive: true, force: true });
|
|
1392
1536
|
}
|
|
1393
1537
|
catch { }
|
|
1538
|
+
try {
|
|
1539
|
+
rmSync(join(runDir, "verifications"), { recursive: true, force: true });
|
|
1540
|
+
}
|
|
1541
|
+
catch { }
|
|
1394
1542
|
}
|
|
1395
1543
|
// Switch back if we created a run branch
|
|
1396
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;
|
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.6.
|
|
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": {
|