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 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 the latest incomplete run, or null. */
321
- function findIncompleteRun(rootDir) {
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(); // newest first
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
- return { dir: join(runsDir, d), state };
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
- let completedRuns = [];
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 && s.phase === "done")
634
- completedRuns.push({ dir: join(runsDir, d), state: s });
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 incomplete = findIncompleteRun(rootDir);
661
- if (incomplete && incomplete.state.cwd === cwd && !noTTY && tasks.length === 0) {
662
- const prev = incomplete.state;
663
- const merged = prev.branches.filter(b => b.status === "merged").length;
664
- const unmerged = prev.branches.filter(b => b.status === "unmerged").length;
665
- const failed = prev.branches.filter(b => b.status === "failed" || b.status === "merge-failed").length;
666
- const obj = prev.objective?.slice(0, 50) || "";
667
- // Read last status for context
668
- let lastStatus = "";
669
- try {
670
- lastStatus = readFileSync(join(incomplete.dir, "status.md"), "utf-8").trim().slice(0, 120);
671
- }
672
- catch { }
673
- const label = "Unfinished run";
674
- console.log(chalk.yellow(`\n ⚠ ${label}`));
675
- const boxLines = [
676
- `${obj}${obj.length >= 50 ? "…" : ""}`,
677
- `${prev.accCompleted}/${prev.budget} sessions · ${prev.remaining} remaining · $${prev.accCost.toFixed(2)}`,
678
- ];
679
- if (lastStatus)
680
- boxLines.push(lastStatus);
681
- if (merged + unmerged + failed > 0)
682
- boxLines.push(`${merged} merged · ${unmerged} unmerged · ${failed} failed branches`);
683
- const boxW = Math.max(...boxLines.map(l => l.length)) + 4;
684
- console.log(chalk.dim(` ╭${"─".repeat(boxW)}╮`));
685
- for (const line of boxLines)
686
- console.log(chalk.dim(" │") + ` ${line.padEnd(boxW - 2)}` + chalk.dim("│"));
687
- console.log(chalk.dim(` ╰${"─".repeat(boxW)}╯`));
688
- const action = await selectKey("", [
689
- { key: "r", desc: "esume" },
690
- { key: "f", desc: "resh" },
691
- { key: "q", desc: "uit" },
692
- ]);
693
- if (action === "q") {
694
- process.exit(0);
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 (action === "r") {
697
- resuming = true;
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, prev.branches, (msg) => console.log(chalk.dim(` ${msg}`)));
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(incomplete.dir, prev);
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 ALL config from saved state
1133
- remaining = resumeState.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
- objective = resumeState.objective;
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
- console.log(chalk.cyan(`\n ◆ Assessing...\n`));
1216
- process.stdout.write("\x1B[?25l");
1217
- try {
1218
- const memory = readRunMemory(runDir, previousKnowledge || undefined);
1219
- const steer = await steerWave(objective, waveHistory, remaining, cwd, plannerModel, workerModel, permissionMode, concurrency, makeProgressLog(), memory);
1220
- process.stdout.write(`\x1B[2K\r`);
1221
- process.stdout.write("\x1B[?25h");
1222
- if (steer.statusUpdate)
1223
- writeStatus(runDir, steer.statusUpdate);
1224
- if (steer.goalUpdate)
1225
- writeGoalUpdate(runDir, steer.goalUpdate);
1226
- if (!steer.done && steer.tasks.length > 0) {
1227
- console.log(chalk.dim(` ${steer.reasoning}\n`));
1228
- currentTasks = steer.tasks.map(t => ({
1229
- ...t,
1230
- model: t.model === "planner" ? plannerModel : t.model === "worker" ? workerModel : t.model,
1231
- }));
1232
- lastWaveKind = steer.waveKind;
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
- else if (steer.done) {
1235
- console.log(chalk.green(` \u2713 ${steer.reasoning}\n`));
1236
- objectiveComplete = true;
1237
- remaining = 0;
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 -= swarm.completed + swarm.failed;
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 repeatHint = lastKind && lastKind !== "execute"
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: "executing" | "steering" | "reflecting" | "verifying" | "capped" | "done";
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.0",
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": {