claude-overnight 1.6.0 → 1.7.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 +307 -139
- package/dist/planner.d.ts +1 -0
- package/dist/planner.js +6 -1
- package/dist/types.d.ts +4 -1
- package/dist/ui.d.ts +56 -3
- package/dist/ui.js +389 -267
- 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";
|
|
@@ -7,8 +7,8 @@ import { createInterface } from "readline";
|
|
|
7
7
|
import chalk from "chalk";
|
|
8
8
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
9
9
|
import { Swarm } from "./swarm.js";
|
|
10
|
-
import { planTasks, refinePlan, detectModelTier, steerWave, identifyThemes, buildThinkingTasks, orchestrate } from "./planner.js";
|
|
11
|
-
import {
|
|
10
|
+
import { planTasks, refinePlan, detectModelTier, steerWave, identifyThemes, buildThinkingTasks, orchestrate, getTotalPlannerCost, getPlannerRateLimitInfo } from "./planner.js";
|
|
11
|
+
import { RunDisplay, renderSummary } from "./ui.js";
|
|
12
12
|
// ── CLI flag parsing ──
|
|
13
13
|
function parseCliFlags(argv) {
|
|
14
14
|
const known = new Set(["concurrency", "model", "timeout", "budget", "usage-cap", "extra-usage-budget"]);
|
|
@@ -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;
|
|
@@ -969,14 +1091,22 @@ async function main() {
|
|
|
969
1091
|
agentTimeoutMs,
|
|
970
1092
|
usageCap, allowExtraUsage, extraUsageBudget,
|
|
971
1093
|
});
|
|
972
|
-
const
|
|
1094
|
+
const thinkRunInfo = {
|
|
1095
|
+
accIn: 0, accOut: 0, accCost: 0, accCompleted: 0, accFailed: 0,
|
|
1096
|
+
sessionsBudget: budget ?? 10, waveNum: -1, remaining: (budget ?? 10),
|
|
1097
|
+
model: plannerModel, startedAt: Date.now(),
|
|
1098
|
+
};
|
|
1099
|
+
const thinkDisplay = new RunDisplay(thinkRunInfo, { remaining: 0, usageCap, dirty: false });
|
|
1100
|
+
thinkDisplay.setWave(thinkingSwarm);
|
|
1101
|
+
thinkDisplay.start();
|
|
973
1102
|
try {
|
|
974
1103
|
await thinkingSwarm.run();
|
|
975
1104
|
}
|
|
976
1105
|
finally {
|
|
977
|
-
|
|
1106
|
+
thinkDisplay.pause();
|
|
1107
|
+
console.log(renderSummary(thinkingSwarm));
|
|
1108
|
+
thinkDisplay.stop();
|
|
978
1109
|
}
|
|
979
|
-
console.log(renderSummary(thinkingSwarm));
|
|
980
1110
|
thinkingUsed = thinkingSwarm.completed + thinkingSwarm.failed;
|
|
981
1111
|
thinkingCost = thinkingSwarm.totalCostUsd;
|
|
982
1112
|
thinkingIn = thinkingSwarm.totalInputTokens;
|
|
@@ -1109,8 +1239,10 @@ async function main() {
|
|
|
1109
1239
|
process.exit(0);
|
|
1110
1240
|
}
|
|
1111
1241
|
// ── Run (wave loop) ──
|
|
1112
|
-
|
|
1113
|
-
|
|
1242
|
+
const restore = () => { try {
|
|
1243
|
+
process.stdout.write("\x1B[?25h\n");
|
|
1244
|
+
}
|
|
1245
|
+
catch { } };
|
|
1114
1246
|
const runStartedAt = resuming && resumeState?.startedAt ? new Date(resumeState.startedAt).getTime() : Date.now();
|
|
1115
1247
|
// Wave-loop state — either fresh or resumed
|
|
1116
1248
|
mkdirSync(join(runDir, "reflections"), { recursive: true });
|
|
@@ -1129,32 +1261,26 @@ async function main() {
|
|
|
1129
1261
|
let overheadBudgetUsed;
|
|
1130
1262
|
const branches = [];
|
|
1131
1263
|
if (resuming && resumeState) {
|
|
1132
|
-
// Restore
|
|
1133
|
-
remaining
|
|
1264
|
+
// Restore wave-loop state (config already restored in the early resume block + ternaries above)
|
|
1265
|
+
// Recalculate remaining from budget — don't trust saved value which counts failures
|
|
1266
|
+
// and may have been zeroed by a premature "done" signal from the steerer.
|
|
1267
|
+
// The user explicitly chose Resume, so give back budget not yet successfully used.
|
|
1268
|
+
remaining = Math.max(1, (resumeState.budget ?? 0) - resumeState.accCompleted);
|
|
1134
1269
|
currentTasks = resumeState.currentTasks;
|
|
1135
1270
|
waveNum = resumeState.waveNum;
|
|
1136
1271
|
accCost = resumeState.accCost;
|
|
1137
1272
|
accCompleted = resumeState.accCompleted;
|
|
1138
1273
|
accFailed = resumeState.accFailed;
|
|
1139
|
-
accTools = 0;
|
|
1274
|
+
accTools = resumeState.accTools ?? 0;
|
|
1275
|
+
accIn = resumeState.accIn ?? 0;
|
|
1276
|
+
accOut = resumeState.accOut ?? 0;
|
|
1140
1277
|
lastWaveKind = resumeState.lastWaveKind;
|
|
1141
1278
|
overheadBudgetUsed = resumeState.overheadBudgetUsed ?? (resumeState.reflectionBudgetUsed ?? 0) + (resumeState.verificationBudgetUsed ?? 0);
|
|
1142
1279
|
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
|
|
1280
|
+
flex = resumeState.flex; // override computed flex with saved value
|
|
1156
1281
|
waveHistory.push(...loadWaveHistory(runDir));
|
|
1157
1282
|
console.log(chalk.green(`\n ✓ Resumed`) + chalk.dim(` · wave ${waveNum + 1} · ${remaining} remaining · $${accCost.toFixed(2)} spent · ${waveHistory.length} prior waves\n`));
|
|
1283
|
+
waveNum++; // advance past last completed wave so next session file doesn't overwrite
|
|
1158
1284
|
}
|
|
1159
1285
|
else {
|
|
1160
1286
|
// Fresh run
|
|
@@ -1178,6 +1304,17 @@ async function main() {
|
|
|
1178
1304
|
liveConfig.remaining = remaining;
|
|
1179
1305
|
liveConfig.usageCap = usageCap;
|
|
1180
1306
|
const maxOverheadBudget = Math.max(4, Math.ceil((budget ?? 10) * 0.15));
|
|
1307
|
+
// Unified display — one instance for the entire run
|
|
1308
|
+
const runInfoRef = {
|
|
1309
|
+
accIn, accOut, accCost, accCompleted, accFailed,
|
|
1310
|
+
sessionsBudget: budget ?? tasks.length, waveNum, remaining,
|
|
1311
|
+
model: workerModel, startedAt: runStartedAt,
|
|
1312
|
+
};
|
|
1313
|
+
const display = new RunDisplay(runInfoRef, liveConfig);
|
|
1314
|
+
const rlGetter = () => {
|
|
1315
|
+
const rl = getPlannerRateLimitInfo();
|
|
1316
|
+
return { utilization: rl.utilization, isUsingOverage: rl.isUsingOverage, windows: rl.windows, resetsAt: rl.resetsAt };
|
|
1317
|
+
};
|
|
1181
1318
|
// For flex + branch strategy: create one target branch, waves merge via yolo into it
|
|
1182
1319
|
let runBranch;
|
|
1183
1320
|
let originalRef;
|
|
@@ -1199,79 +1336,104 @@ async function main() {
|
|
|
1199
1336
|
const gracefulStop = (signal) => {
|
|
1200
1337
|
if (stopping) {
|
|
1201
1338
|
currentSwarm?.cleanup();
|
|
1339
|
+
display.stop();
|
|
1202
1340
|
restore();
|
|
1203
1341
|
process.exit(0);
|
|
1204
1342
|
}
|
|
1205
1343
|
stopping = true;
|
|
1206
|
-
process.stdout.write(`\n ${chalk.yellow(`${signal}: stopping... (send again to force)`)}\n`);
|
|
1207
1344
|
currentSwarm?.abort();
|
|
1208
1345
|
};
|
|
1209
1346
|
process.on("SIGINT", () => gracefulStop("SIGINT"));
|
|
1210
1347
|
process.on("SIGTERM", () => gracefulStop("SIGTERM"));
|
|
1211
|
-
process.on("uncaughtException", (err) => { currentSwarm?.abort(); currentSwarm?.cleanup(); restore(); console.error(chalk.red(`\n Uncaught: ${err.message}`)); process.exit(1); });
|
|
1212
|
-
process.on("unhandledRejection", (reason) => { currentSwarm?.abort(); currentSwarm?.cleanup(); restore(); console.error(chalk.red(`\n Unhandled: ${reason instanceof Error ? reason.message : reason}`)); process.exit(1); });
|
|
1348
|
+
process.on("uncaughtException", (err) => { currentSwarm?.abort(); currentSwarm?.cleanup(); display.stop(); restore(); console.error(chalk.red(`\n Uncaught: ${err.message}`)); process.exit(1); });
|
|
1349
|
+
process.on("unhandledRejection", (reason) => { currentSwarm?.abort(); currentSwarm?.cleanup(); display.stop(); restore(); console.error(chalk.red(`\n Unhandled: ${reason instanceof Error ? reason.message : reason}`)); process.exit(1); });
|
|
1350
|
+
// Helper: sync mutable runInfo with accumulated state
|
|
1351
|
+
const syncRunInfo = () => Object.assign(runInfoRef, { accIn, accOut, accCost, accCompleted, accFailed, waveNum, remaining });
|
|
1213
1352
|
// When resuming a flex run with no queued tasks, steer immediately to get the next wave
|
|
1214
1353
|
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
|
-
|
|
1354
|
+
let steerAttempts = 0;
|
|
1355
|
+
display.setSteering(rlGetter);
|
|
1356
|
+
display.start();
|
|
1357
|
+
while (currentTasks.length === 0 && remaining > 0 && !objectiveComplete && steerAttempts < 3) {
|
|
1358
|
+
steerAttempts++;
|
|
1359
|
+
const plannerCostBefore = getTotalPlannerCost();
|
|
1360
|
+
try {
|
|
1361
|
+
const memory = readRunMemory(runDir, previousKnowledge || undefined);
|
|
1362
|
+
const steer = await steerWave(objective, waveHistory, remaining, cwd, plannerModel, workerModel, permissionMode, concurrency, (text) => display.updateText(text), memory);
|
|
1363
|
+
const steerCost = getTotalPlannerCost() - plannerCostBefore;
|
|
1364
|
+
accCost += steerCost;
|
|
1365
|
+
syncRunInfo();
|
|
1366
|
+
if (steer.statusUpdate)
|
|
1367
|
+
writeStatus(runDir, steer.statusUpdate);
|
|
1368
|
+
if (steer.goalUpdate)
|
|
1369
|
+
writeGoalUpdate(runDir, steer.goalUpdate);
|
|
1370
|
+
if (steer.done || steer.tasks.length === 0) {
|
|
1371
|
+
const hasVerification = waveHistory.some(w => w.kind.includes("verif"));
|
|
1372
|
+
if (!hasVerification && remaining >= 1) {
|
|
1373
|
+
display.updateText(`Done blocked \u2014 verification required`);
|
|
1374
|
+
lastWaveKind = "done-blocked";
|
|
1375
|
+
continue;
|
|
1376
|
+
}
|
|
1377
|
+
objectiveComplete = true;
|
|
1378
|
+
remaining = 0;
|
|
1379
|
+
}
|
|
1380
|
+
else {
|
|
1381
|
+
const isOverhead = steer.waveKind !== "execute";
|
|
1382
|
+
if (isOverhead && overheadBudgetUsed + steer.tasks.length > maxOverheadBudget) {
|
|
1383
|
+
display.updateText(`Overhead budget exhausted (${overheadBudgetUsed}/${maxOverheadBudget}) \u2014 re-assessing`);
|
|
1384
|
+
lastWaveKind = "overhead-capped";
|
|
1385
|
+
continue;
|
|
1386
|
+
}
|
|
1387
|
+
currentTasks = steer.tasks.map(t => ({
|
|
1388
|
+
...t,
|
|
1389
|
+
model: t.model === "planner" ? plannerModel : t.model === "worker" ? workerModel
|
|
1390
|
+
: isOverhead && !t.model ? plannerModel : t.model,
|
|
1391
|
+
}));
|
|
1392
|
+
lastWaveKind = steer.waveKind;
|
|
1393
|
+
if (isOverhead)
|
|
1394
|
+
overheadBudgetUsed += currentTasks.length;
|
|
1395
|
+
}
|
|
1233
1396
|
}
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1397
|
+
catch (err) {
|
|
1398
|
+
const steerCost = getTotalPlannerCost() - plannerCostBefore;
|
|
1399
|
+
accCost += steerCost;
|
|
1400
|
+
display.stop();
|
|
1401
|
+
console.log(chalk.yellow(` Steering failed: ${err.message?.slice(0, 80)} \u2014 stopping\n`));
|
|
1402
|
+
break;
|
|
1238
1403
|
}
|
|
1239
1404
|
}
|
|
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
1405
|
}
|
|
1406
|
+
// Start unified display (first call — idempotent)
|
|
1407
|
+
if (!display.runInfo.startedAt)
|
|
1408
|
+
display.runInfo.startedAt = runStartedAt;
|
|
1409
|
+
display.start();
|
|
1245
1410
|
while (remaining > 0 && currentTasks.length > 0 && !stopping) {
|
|
1246
1411
|
if (currentTasks.length > remaining)
|
|
1247
1412
|
currentTasks = currentTasks.slice(0, remaining);
|
|
1248
|
-
|
|
1249
|
-
const costSoFar = accCost > 0 ? ` · $${accCost.toFixed(2)} spent` : "";
|
|
1250
|
-
console.log(chalk.cyan(`\n ◆ Wave ${waveNum + 1}`) + chalk.dim(` · ${currentTasks.length} tasks · ${remaining} remaining${costSoFar}\n`));
|
|
1251
|
-
}
|
|
1413
|
+
syncRunInfo();
|
|
1252
1414
|
const swarm = new Swarm({
|
|
1253
1415
|
tasks: currentTasks, concurrency, cwd, model: workerModel, permissionMode, allowedTools,
|
|
1254
1416
|
useWorktrees, mergeStrategy: waveMerge, agentTimeoutMs, usageCap, allowExtraUsage, extraUsageBudget,
|
|
1255
1417
|
baseCostUsd: accCost,
|
|
1256
1418
|
});
|
|
1257
1419
|
currentSwarm = swarm;
|
|
1258
|
-
|
|
1420
|
+
display.setWave(swarm);
|
|
1421
|
+
display.resume();
|
|
1259
1422
|
try {
|
|
1260
1423
|
await swarm.run();
|
|
1261
1424
|
}
|
|
1262
1425
|
catch (err) {
|
|
1263
1426
|
if (isAuthError(err)) {
|
|
1264
|
-
|
|
1427
|
+
display.stop();
|
|
1265
1428
|
restore();
|
|
1266
1429
|
console.error(chalk.red(`\n Authentication failed — check your API key or run: claude auth\n`));
|
|
1267
1430
|
process.exit(1);
|
|
1268
1431
|
}
|
|
1269
1432
|
throw err;
|
|
1270
1433
|
}
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
}
|
|
1434
|
+
// Show wave summary (pauses render, prints, then we switch to steering)
|
|
1435
|
+
display.pause();
|
|
1436
|
+
console.log(renderSummary(swarm));
|
|
1275
1437
|
// Accumulate
|
|
1276
1438
|
accCost += swarm.totalCostUsd;
|
|
1277
1439
|
accIn += swarm.totalInputTokens;
|
|
@@ -1279,8 +1441,10 @@ async function main() {
|
|
|
1279
1441
|
accCompleted += swarm.completed;
|
|
1280
1442
|
accFailed += swarm.failed;
|
|
1281
1443
|
accTools += swarm.agents.reduce((sum, a) => sum + a.toolCalls, 0);
|
|
1282
|
-
remaining
|
|
1283
|
-
|
|
1444
|
+
remaining = Math.max(0, remaining - swarm.completed - swarm.failed);
|
|
1445
|
+
const expectedFloor = Math.max(0, (budget ?? 0) - accCompleted - accFailed);
|
|
1446
|
+
if (remaining < expectedFloor)
|
|
1447
|
+
remaining = expectedFloor;
|
|
1284
1448
|
if (liveConfig.dirty) {
|
|
1285
1449
|
remaining = liveConfig.remaining;
|
|
1286
1450
|
usageCap = liveConfig.usageCap;
|
|
@@ -1295,7 +1459,7 @@ async function main() {
|
|
|
1295
1459
|
id: `run-${new Date().toISOString().slice(0, 19)}`, objective: objective, budget: budget ?? tasks.length,
|
|
1296
1460
|
remaining, workerModel, plannerModel, concurrency, permissionMode,
|
|
1297
1461
|
usageCap, allowExtraUsage, extraUsageBudget, flex, useWorktrees, mergeStrategy, waveNum, currentTasks: [],
|
|
1298
|
-
lastWaveKind, overheadBudgetUsed, accCost, accCompleted, accFailed,
|
|
1462
|
+
lastWaveKind, overheadBudgetUsed, accCost, accCompleted, accFailed, accIn, accOut, accTools,
|
|
1299
1463
|
branches, phase: "steering", startedAt: new Date(runStartedAt).toISOString(), cwd,
|
|
1300
1464
|
});
|
|
1301
1465
|
waveHistory.push({
|
|
@@ -1311,48 +1475,44 @@ async function main() {
|
|
|
1311
1475
|
if (!flex || remaining <= 0 || swarm.aborted || swarm.cappedOut)
|
|
1312
1476
|
break;
|
|
1313
1477
|
// ── Steer: assess and compose the next wave ──
|
|
1478
|
+
syncRunInfo();
|
|
1479
|
+
display.setSteering(rlGetter);
|
|
1480
|
+
display.resume();
|
|
1314
1481
|
let steered = false;
|
|
1315
1482
|
let steerAttempts = 0;
|
|
1316
1483
|
while (!steered && remaining > 0 && !stopping && steerAttempts < 3) {
|
|
1317
1484
|
steerAttempts++;
|
|
1318
|
-
|
|
1319
|
-
process.stdout.write("\x1B[?25l");
|
|
1485
|
+
const plannerCostBefore = getTotalPlannerCost();
|
|
1320
1486
|
try {
|
|
1321
1487
|
const memory = readRunMemory(runDir, previousKnowledge || undefined);
|
|
1322
|
-
const steer = await steerWave(objective, waveHistory, remaining, cwd, plannerModel, workerModel, permissionMode, concurrency,
|
|
1323
|
-
|
|
1324
|
-
|
|
1488
|
+
const steer = await steerWave(objective, waveHistory, remaining, cwd, plannerModel, workerModel, permissionMode, concurrency, (text) => display.updateText(text), memory);
|
|
1489
|
+
const steerCost = getTotalPlannerCost() - plannerCostBefore;
|
|
1490
|
+
accCost += steerCost;
|
|
1491
|
+
syncRunInfo();
|
|
1325
1492
|
if (steer.statusUpdate)
|
|
1326
1493
|
writeStatus(runDir, steer.statusUpdate);
|
|
1327
|
-
if (steer.goalUpdate)
|
|
1494
|
+
if (steer.goalUpdate)
|
|
1328
1495
|
writeGoalUpdate(runDir, steer.goalUpdate);
|
|
1329
|
-
console.log(chalk.dim(` Goal refined: ${steer.goalUpdate.slice(0, 100)}\n`));
|
|
1330
|
-
}
|
|
1331
1496
|
const execWaves = waveHistory.filter(w => w.kind === "execute").length;
|
|
1332
1497
|
if (execWaves > 0 && execWaves % 5 === 0)
|
|
1333
1498
|
archiveMilestone(runDir, waveNum);
|
|
1334
1499
|
if (steer.done || steer.tasks.length === 0) {
|
|
1335
1500
|
const hasVerification = waveHistory.some(w => w.kind.includes("verif"));
|
|
1336
1501
|
if (!hasVerification && remaining >= 1) {
|
|
1337
|
-
|
|
1338
|
-
console.log(chalk.yellow(` Done blocked — verification required before completion\n`));
|
|
1502
|
+
display.updateText(`Done blocked \u2014 verification required`);
|
|
1339
1503
|
lastWaveKind = "done-blocked";
|
|
1340
|
-
continue;
|
|
1504
|
+
continue;
|
|
1341
1505
|
}
|
|
1342
|
-
console.log(chalk.green(` \u2713 ${steer.reasoning}\n`));
|
|
1343
1506
|
objectiveComplete = true;
|
|
1344
1507
|
remaining = 0;
|
|
1345
1508
|
break;
|
|
1346
1509
|
}
|
|
1347
1510
|
const isOverhead = steer.waveKind !== "execute";
|
|
1348
1511
|
if (isOverhead && overheadBudgetUsed + steer.tasks.length > maxOverheadBudget) {
|
|
1349
|
-
|
|
1350
|
-
console.log(chalk.yellow(` Overhead budget exhausted (${overheadBudgetUsed}/${maxOverheadBudget}) — re-assessing\n`));
|
|
1512
|
+
display.updateText(`Overhead budget exhausted (${overheadBudgetUsed}/${maxOverheadBudget}) \u2014 re-assessing`);
|
|
1351
1513
|
lastWaveKind = "overhead-capped";
|
|
1352
|
-
continue;
|
|
1514
|
+
continue;
|
|
1353
1515
|
}
|
|
1354
|
-
console.log(chalk.dim(` ${steer.reasoning}\n`));
|
|
1355
|
-
// Resolve model aliases: "planner" → plannerModel, "worker" → workerModel
|
|
1356
1516
|
currentTasks = steer.tasks.map(t => ({
|
|
1357
1517
|
...t,
|
|
1358
1518
|
model: t.model === "planner" ? plannerModel : t.model === "worker" ? workerModel
|
|
@@ -1364,14 +1524,18 @@ async function main() {
|
|
|
1364
1524
|
steered = true;
|
|
1365
1525
|
}
|
|
1366
1526
|
catch (err) {
|
|
1367
|
-
|
|
1527
|
+
const steerCost = getTotalPlannerCost() - plannerCostBefore;
|
|
1528
|
+
accCost += steerCost;
|
|
1529
|
+
display.stop();
|
|
1368
1530
|
console.log(chalk.yellow(` Steering failed: ${err.message?.slice(0, 80)} \u2014 stopping\n`));
|
|
1369
|
-
// Don't zero out remaining — preserve unspent budget so resume works
|
|
1370
1531
|
break;
|
|
1371
1532
|
}
|
|
1372
1533
|
}
|
|
1534
|
+
if (!steered)
|
|
1535
|
+
break;
|
|
1373
1536
|
waveNum++;
|
|
1374
1537
|
}
|
|
1538
|
+
display.stop();
|
|
1375
1539
|
// Only truly "done" if steering explicitly completed the objective (or non-flex single wave with budget exhausted)
|
|
1376
1540
|
const trulyDone = objectiveComplete || (!flex && remaining <= 0);
|
|
1377
1541
|
const finalPhase = trulyDone ? "done" : "capped";
|
|
@@ -1379,7 +1543,7 @@ async function main() {
|
|
|
1379
1543
|
id: `run-${new Date().toISOString().slice(0, 19)}`, objective: objective ?? "", budget: budget ?? tasks.length,
|
|
1380
1544
|
remaining, workerModel, plannerModel, concurrency, permissionMode,
|
|
1381
1545
|
usageCap, allowExtraUsage, extraUsageBudget, flex, useWorktrees, mergeStrategy, waveNum, currentTasks: [],
|
|
1382
|
-
lastWaveKind, overheadBudgetUsed, accCost, accCompleted, accFailed,
|
|
1546
|
+
lastWaveKind, overheadBudgetUsed, accCost, accCompleted, accFailed, accIn, accOut, accTools,
|
|
1383
1547
|
branches, phase: finalPhase, startedAt: new Date(runStartedAt).toISOString(), cwd,
|
|
1384
1548
|
});
|
|
1385
1549
|
if (trulyDone) {
|
|
@@ -1391,6 +1555,10 @@ async function main() {
|
|
|
1391
1555
|
rmSync(join(runDir, "reflections"), { recursive: true, force: true });
|
|
1392
1556
|
}
|
|
1393
1557
|
catch { }
|
|
1558
|
+
try {
|
|
1559
|
+
rmSync(join(runDir, "verifications"), { recursive: true, force: true });
|
|
1560
|
+
}
|
|
1561
|
+
catch { }
|
|
1394
1562
|
}
|
|
1395
1563
|
// Switch back if we created a run branch
|
|
1396
1564
|
if (runBranch && originalRef) {
|