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 +93 -11
- package/dist/planner.js +6 -1
- package/package.json +1 -1
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 (
|
|
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
|
-
|
|
823
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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": {
|