claude-overnight 1.5.1 → 1.6.0

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
@@ -410,6 +410,35 @@ function saveWaveSession(baseDir, waveNum, kind, swarm) {
410
410
  totalCost: swarm.totalCostUsd,
411
411
  }, null, 2), "utf-8");
412
412
  }
413
+ /** Rebuild waveHistory from saved session files on resume. */
414
+ function loadWaveHistory(runDir) {
415
+ const dir = join(runDir, "sessions");
416
+ try {
417
+ return readdirSync(dir)
418
+ .filter(f => f.startsWith("wave-") && f.endsWith(".json"))
419
+ .sort((a, b) => {
420
+ const numA = parseInt(a.replace("wave-", "").replace(".json", ""));
421
+ const numB = parseInt(b.replace("wave-", "").replace(".json", ""));
422
+ return numA - numB;
423
+ })
424
+ .map(f => {
425
+ const data = JSON.parse(readFileSync(join(dir, f), "utf-8"));
426
+ return {
427
+ wave: data.wave,
428
+ kind: data.kind,
429
+ tasks: (data.agents || []).map((a) => ({
430
+ prompt: a.prompt,
431
+ status: a.status,
432
+ filesChanged: a.filesChanged,
433
+ error: a.error,
434
+ })),
435
+ };
436
+ });
437
+ }
438
+ catch {
439
+ return [];
440
+ }
441
+ }
413
442
  function recordBranches(swarm, branches) {
414
443
  for (const a of swarm.agents) {
415
444
  if (a.branch) {
@@ -671,6 +700,11 @@ async function main() {
671
700
  if (unmerged > 0) {
672
701
  console.log("");
673
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
704
+ try {
705
+ saveRunState(incomplete.dir, prev);
706
+ }
707
+ catch { }
674
708
  }
675
709
  }
676
710
  }
@@ -683,7 +717,18 @@ async function main() {
683
717
  let usageCap;
684
718
  let allowExtraUsage = false;
685
719
  let extraUsageBudget;
686
- if (!nonInteractive) {
720
+ if (resuming) {
721
+ // Skip interactive flow entirely — all config is restored from saved state later
722
+ workerModel = resumeState.workerModel;
723
+ plannerModel = resumeState.plannerModel;
724
+ budget = resumeState.budget;
725
+ concurrency = resumeState.concurrency;
726
+ objective = resumeState.objective;
727
+ usageCap = resumeState.usageCap;
728
+ allowExtraUsage = resumeState.allowExtraUsage ?? false;
729
+ extraUsageBudget = resumeState.extraUsageBudget;
730
+ }
731
+ else if (!nonInteractive) {
687
732
  // ① Objective
688
733
  while (true) {
689
734
  objective = await ask(`\n ${chalk.cyan("①")} ${chalk.bold("What should the agents do?")}\n ${chalk.cyan(">")} `);
@@ -819,11 +864,11 @@ async function main() {
819
864
  }
820
865
  }
821
866
  validateConcurrency(concurrency);
822
- const permissionMode = fileCfg?.permissionMode ?? "auto";
823
- const useWorktrees = fileCfg?.useWorktrees ?? (isGitRepo(cwd));
867
+ let permissionMode = resuming ? resumeState.permissionMode : (fileCfg?.permissionMode ?? "auto");
868
+ let useWorktrees = resuming ? resumeState.useWorktrees : (fileCfg?.useWorktrees ?? isGitRepo(cwd));
824
869
  if (useWorktrees)
825
870
  validateGitRepo(cwd);
826
- const mergeStrategy = fileCfg?.mergeStrategy ?? "yolo";
871
+ let mergeStrategy = resuming ? resumeState.mergeStrategy : (fileCfg?.mergeStrategy ?? "yolo");
827
872
  if (nonInteractive) {
828
873
  const capStr = usageCap != null ? ` cap=${Math.round(usageCap * 100)}%` : "";
829
874
  const extraStr = allowExtraUsage ? (extraUsageBudget ? ` extra=$${extraUsageBudget}` : " extra=∞") : " extra=off";
@@ -840,7 +885,7 @@ async function main() {
840
885
  const runDir = resuming && resumeRunDir ? resumeRunDir : (orphanedDir ?? createRunDir(rootDir));
841
886
  const previousKnowledge = readPreviousRunKnowledge(rootDir);
842
887
  // ── Plan phase (interactive: review loop, non-interactive: auto-plan or skip) ──
843
- const needsPlan = tasks.length === 0;
888
+ const needsPlan = tasks.length === 0 && !resuming;
844
889
  const designDir = join(runDir, "designs");
845
890
  if (needsPlan) {
846
891
  if (noTTY) {
@@ -1054,7 +1099,7 @@ async function main() {
1054
1099
  process.exit(1);
1055
1100
  }
1056
1101
  }
1057
- if (tasks.length === 0) {
1102
+ if (tasks.length === 0 && !resuming) {
1058
1103
  console.error("No tasks provided.");
1059
1104
  process.exit(1);
1060
1105
  }
@@ -1066,7 +1111,7 @@ async function main() {
1066
1111
  // ── Run (wave loop) ──
1067
1112
  process.stdout.write("\x1B[?25l");
1068
1113
  const restore = () => process.stdout.write("\x1B[?25h\n");
1069
- const runStartedAt = Date.now();
1114
+ const runStartedAt = resuming && resumeState?.startedAt ? new Date(resumeState.startedAt).getTime() : Date.now();
1070
1115
  // Wave-loop state — either fresh or resumed
1071
1116
  mkdirSync(join(runDir, "reflections"), { recursive: true });
1072
1117
  mkdirSync(join(runDir, "milestones"), { recursive: true });
@@ -1104,7 +1149,12 @@ async function main() {
1104
1149
  usageCap = resumeState.usageCap;
1105
1150
  allowExtraUsage = resumeState.allowExtraUsage ?? false;
1106
1151
  extraUsageBudget = resumeState.extraUsageBudget;
1107
- console.log(chalk.green(`\n ✓ Resumed`) + chalk.dim(` · wave ${waveNum + 1} · ${remaining} remaining · $${accCost.toFixed(2)} spent\n`));
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
1156
+ waveHistory.push(...loadWaveHistory(runDir));
1157
+ console.log(chalk.green(`\n ✓ Resumed`) + chalk.dim(` · wave ${waveNum + 1} · ${remaining} remaining · $${accCost.toFixed(2)} spent · ${waveHistory.length} prior waves\n`));
1108
1158
  }
1109
1159
  else {
1110
1160
  // Fresh run
@@ -1131,7 +1181,7 @@ async function main() {
1131
1181
  // For flex + branch strategy: create one target branch, waves merge via yolo into it
1132
1182
  let runBranch;
1133
1183
  let originalRef;
1134
- if (flex && mergeStrategy === "branch" && useWorktrees) {
1184
+ if (flex && mergeStrategy === "branch" && useWorktrees && !resuming) {
1135
1185
  try {
1136
1186
  originalRef = execSync("git rev-parse --abbrev-ref HEAD", { cwd, encoding: "utf-8", stdio: "pipe" }).trim();
1137
1187
  if (originalRef === "HEAD")
@@ -1160,6 +1210,38 @@ async function main() {
1160
1210
  process.on("SIGTERM", () => gracefulStop("SIGTERM"));
1161
1211
  process.on("uncaughtException", (err) => { currentSwarm?.abort(); currentSwarm?.cleanup(); restore(); console.error(chalk.red(`\n Uncaught: ${err.message}`)); process.exit(1); });
1162
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); });
1213
+ // When resuming a flex run with no queued tasks, steer immediately to get the next wave
1214
+ 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;
1233
+ }
1234
+ else if (steer.done) {
1235
+ console.log(chalk.green(` \u2713 ${steer.reasoning}\n`));
1236
+ objectiveComplete = true;
1237
+ remaining = 0;
1238
+ }
1239
+ }
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
+ }
1163
1245
  while (remaining > 0 && currentTasks.length > 0 && !stopping) {
1164
1246
  if (currentTasks.length > remaining)
1165
1247
  currentTasks = currentTasks.slice(0, remaining);
@@ -1212,7 +1294,7 @@ async function main() {
1212
1294
  saveRunState(runDir, {
1213
1295
  id: `run-${new Date().toISOString().slice(0, 19)}`, objective: objective, budget: budget ?? tasks.length,
1214
1296
  remaining, workerModel, plannerModel, concurrency, permissionMode,
1215
- usageCap, allowExtraUsage, extraUsageBudget, flex, useWorktrees, mergeStrategy, waveNum, currentTasks,
1297
+ usageCap, allowExtraUsage, extraUsageBudget, flex, useWorktrees, mergeStrategy, waveNum, currentTasks: [],
1216
1298
  lastWaveKind, overheadBudgetUsed, accCost, accCompleted, accFailed,
1217
1299
  branches, phase: "steering", startedAt: new Date(runStartedAt).toISOString(), cwd,
1218
1300
  });
@@ -1284,7 +1366,7 @@ async function main() {
1284
1366
  catch (err) {
1285
1367
  process.stdout.write("\x1B[?25h");
1286
1368
  console.log(chalk.yellow(` Steering failed: ${err.message?.slice(0, 80)} \u2014 stopping\n`));
1287
- remaining = 0;
1369
+ // Don't zero out remaining preserve unspent budget so resume works
1288
1370
  break;
1289
1371
  }
1290
1372
  }
package/dist/planner.js CHANGED
@@ -725,7 +725,12 @@ If done: {"done": true, "waveKind": "done", "reasoning": "...", "statusUpdate":
725
725
  return first;
726
726
  onLog("Retrying...");
727
727
  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
- return attemptJsonParse(retryText) ?? { done: true, waveKind: "done", reasoning: "Could not parse steering response" };
728
+ const retryParsed = attemptJsonParse(retryText);
729
+ if (retryParsed)
730
+ return retryParsed;
731
+ // Don't return done:true on parse failure — that permanently marks the run complete.
732
+ // Throw so the caller's catch block handles it as a transient steering failure.
733
+ throw new Error("Could not parse steering response after retry");
729
734
  })();
730
735
  const isDone = parsed.done === true;
731
736
  const waveKind = parsed.waveKind || parsed.action || (isDone ? "done" : "execute");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-overnight",
3
- "version": "1.5.1",
3
+ "version": "1.6.0",
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": {