claude-overnight 1.11.6 → 1.11.7

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
@@ -5,13 +5,22 @@ import { fileURLToPath } from "url";
5
5
  import chalk from "chalk";
6
6
  import { query } from "@anthropic-ai/claude-agent-sdk";
7
7
  import { Swarm } from "./swarm.js";
8
- import { planTasks, refinePlan, identifyThemes, buildThinkingTasks, orchestrate } from "./planner.js";
8
+ import { planTasks, refinePlan, identifyThemes, buildThinkingTasks, orchestrate, salvageFromFile } from "./planner.js";
9
9
  import { detectModelTier } from "./planner-query.js";
10
10
  import { RunDisplay } from "./ui.js";
11
11
  import { renderSummary } from "./render.js";
12
12
  import { executeRun } from "./run.js";
13
13
  import { parseCliFlags, isAuthError, fetchModels, ask, select, selectKey, loadTaskFile, validateConcurrency, isGitRepo, validateGitRepo, showPlan, BRAILLE, makeProgressLog, } from "./cli.js";
14
14
  import { loadRunState, findIncompleteRuns, findOrphanedDesigns, formatTimeAgo, showRunHistory, readPreviousRunKnowledge, createRunDir, updateLatestSymlink, readMdDir, saveRunState, autoMergeBranches, } from "./state.js";
15
+ function countTasksInFile(path) {
16
+ try {
17
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
18
+ return Array.isArray(parsed?.tasks) ? parsed.tasks.length : 0;
19
+ }
20
+ catch {
21
+ return 0;
22
+ }
23
+ }
15
24
  async function main() {
16
25
  const argv = process.argv.slice(2);
17
26
  if (argv.includes("-v") || argv.includes("--version")) {
@@ -162,8 +171,13 @@ async function main() {
162
171
  lastStatus = readFileSync(join(run.dir, "status.md"), "utf-8").trim().slice(0, 120);
163
172
  }
164
173
  catch { }
174
+ const planTaskCount = prev.phase === "planning" ? countTasksInFile(join(run.dir, "tasks.json")) : 0;
165
175
  console.log(chalk.yellow(`\n ⚠ Unfinished run`) + chalk.dim(` · ${ago}`));
166
- const boxLines = [
176
+ const boxLines = prev.phase === "planning" ? [
177
+ `${obj}${obj.length >= 50 ? "…" : ""}`,
178
+ `Plan ready · ${planTaskCount} tasks · budget ${prev.budget} · ${prev.concurrency}× concurrent`,
179
+ `Plan phase · not yet executing`,
180
+ ] : [
167
181
  `${obj}${obj.length >= 50 ? "…" : ""}`,
168
182
  `${prev.accCompleted}/${prev.budget} sessions · ${Math.max(1, (prev.budget ?? 0) - prev.accCompleted)} remaining · $${prev.accCost.toFixed(2)}`,
169
183
  `Wave ${prev.waveNum + 1} · ${prev.phase}`,
@@ -207,7 +221,13 @@ async function main() {
207
221
  }
208
222
  catch { }
209
223
  console.log(chalk.cyan(` ${i + 1}`) + ` ${obj}${obj.length >= 50 ? "…" : ""}`);
210
- console.log(chalk.dim(` ${s.accCompleted}/${s.budget} · $${s.accCost.toFixed(2)} · ${ago} · ${s.phase} at wave ${s.waveNum + 1}${merged ? ` · ${merged} merged` : ""}`));
224
+ if (s.phase === "planning") {
225
+ const n = countTasksInFile(join(shown[i].dir, "tasks.json"));
226
+ console.log(chalk.dim(` plan ready · ${n} tasks · budget ${s.budget} · ${ago} · not yet executing`));
227
+ }
228
+ else {
229
+ console.log(chalk.dim(` ${s.accCompleted}/${s.budget} · $${s.accCost.toFixed(2)} · ${ago} · ${s.phase} at wave ${s.waveNum + 1}${merged ? ` · ${merged} merged` : ""}`));
230
+ }
211
231
  if (lastStatus)
212
232
  console.log(chalk.dim(` ${lastStatus}`));
213
233
  console.log("");
@@ -236,6 +256,18 @@ async function main() {
236
256
  }
237
257
  }
238
258
  if (resuming && resumeState && resumeRunDir) {
259
+ // Planning-phase resume: the prior run died before executeRun ran any
260
+ // wave, but the orchestrate agent wrote tasks.json to disk. Load those
261
+ // tasks into currentTasks so executeRun can pick them up as wave 0.
262
+ if (resumeState.phase === "planning") {
263
+ const loaded = salvageFromFile(join(resumeRunDir, "tasks.json"), resumeState.budget, () => { }, "resume");
264
+ if (!loaded) {
265
+ console.error(chalk.red(`\n Planning-phase run has no usable tasks.json — start Fresh instead.\n`));
266
+ process.exit(1);
267
+ }
268
+ resumeState.currentTasks = loaded;
269
+ console.log(chalk.green(`\n ✓ Resuming plan · ${loaded.length} tasks loaded from tasks.json`));
270
+ }
239
271
  const unmerged = resumeState.branches.filter(b => b.status === "unmerged").length;
240
272
  if (unmerged > 0) {
241
273
  console.log("");
@@ -479,6 +511,29 @@ async function main() {
479
511
  const previousKnowledge = readPreviousRunKnowledge(rootDir);
480
512
  const needsPlan = tasks.length === 0 && !resuming;
481
513
  const designDir = join(runDir, "designs");
514
+ // Persist an early planning-phase state so the run is visible to the resume
515
+ // picker even if orchestrate dies before executeRun gets a chance to run.
516
+ // Without this, a crashed plan phase leaves no run.json and the run vanishes
517
+ // from findIncompleteRuns — you pay for orchestration and can't see it.
518
+ if (needsPlan && objective) {
519
+ try {
520
+ saveRunState(runDir, {
521
+ id: runDir.split(/[/\\]/).pop() ?? "",
522
+ objective, budget: budget ?? 10, remaining: budget ?? 10,
523
+ workerModel, plannerModel, concurrency, permissionMode,
524
+ usageCap, allowExtraUsage, extraUsageBudget,
525
+ flex, useWorktrees, mergeStrategy,
526
+ waveNum: 0, currentTasks: [],
527
+ accCost: 0, accCompleted: 0, accFailed: 0,
528
+ accIn: 0, accOut: 0, accTools: 0,
529
+ branches: [],
530
+ phase: "planning",
531
+ startedAt: new Date().toISOString(),
532
+ cwd,
533
+ });
534
+ }
535
+ catch { }
536
+ }
482
537
  if (needsPlan) {
483
538
  if (noTTY) {
484
539
  console.error(chalk.red(" No tasks provided and stdin is not a TTY."));
package/dist/planner.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { Task, PermMode } from "./types.js";
2
+ export declare function salvageFromFile(outFile: string | undefined, budget: number | undefined, onLog: (text: string, kind?: "status" | "event") => void, why: string): Task[] | null;
2
3
  export declare const DESIGN_THINKING = "\nHOW TO THINK ABOUT EVERY TASK:\n\nStart from the user's job. What is someone hiring this product to do? \"I need to send money abroad cheaply\" \u2014 not \"I need a currency conversion API.\" Every decision \u2014 what to build, how fast it responds, what happens on error \u2014 flows from the job.\n\nThe experience IS the product. A 200ms server response is not a \"performance metric\" \u2014 it's the difference between an app that feels alive and one that feels broken. A loading state is not \"polish\" \u2014 it's the user knowing the app heard them. An error message is not \"error handling\" \u2014 it's the app being honest. There is no line between backend and UX. The server, the API, the database query, the render \u2014 they're all one experience the user either trusts or doesn't.\n\nBuild the core, verify it works, learn, iterate. Don't plan 20 features and build them all. Build the ONE thing that matters most, run it, see if it actually works from a user's chair. What you learn from seeing it run will change what you build next. Each wave should make what exists better before adding what doesn't exist yet.\n\nConsistency is what makes complex things feel simple. One design system, rigid rules, no exceptions. This is how Revolut ships a super-app with 30+ features that doesn't feel like chaos.\n";
3
4
  export declare function planTasks(objective: string, cwd: string, plannerModel: string, workerModel: string, permissionMode: PermMode, budget: number | undefined, concurrency: number, onLog: (text: string) => void, flexNote?: string, outFile?: string): Promise<Task[]>;
4
5
  export declare function identifyThemes(objective: string, count: number, cwd: string, model: string, permissionMode: PermMode, onLog?: (text: string) => void): Promise<string[]>;
package/dist/planner.js CHANGED
@@ -1,4 +1,29 @@
1
+ import { readFileSync } from "fs";
1
2
  import { runPlannerQuery, extractTaskJson, attemptJsonParse, postProcess, detectModelTier, modelCapabilityBlock } from "./planner-query.js";
3
+ // Resilience: if the planner query throws but the agent already wrote valid
4
+ // tasks to `outFile` (via its Write tool), salvage them instead of discarding
5
+ // expensive work. Returns salvaged tasks on success, null if nothing usable on
6
+ // disk — caller should then re-throw the original error.
7
+ export function salvageFromFile(outFile, budget, onLog, why) {
8
+ if (!outFile)
9
+ return null;
10
+ try {
11
+ const parsed = attemptJsonParse(readFileSync(outFile, "utf-8"));
12
+ if (!parsed?.tasks?.length)
13
+ return null;
14
+ let tasks = parsed.tasks.map((t, i) => ({
15
+ id: String(i), prompt: typeof t === "string" ? t : t.prompt,
16
+ }));
17
+ tasks = postProcess(tasks, budget, onLog);
18
+ if (tasks.length === 0)
19
+ return null;
20
+ onLog(`Planner errored (${why}) — salvaged ${tasks.length} tasks from ${outFile}`, "event");
21
+ return tasks;
22
+ }
23
+ catch {
24
+ return null;
25
+ }
26
+ }
2
27
  // The core framing for all planning. Not a checklist — a way of thinking.
3
28
  export const DESIGN_THINKING = `
4
29
  HOW TO THINK ABOUT EVERY TASK:
@@ -152,7 +177,16 @@ export async function planTasks(objective, cwd, plannerModel, workerModel, permi
152
177
  onLog("Analyzing codebase...");
153
178
  const prompt = plannerPrompt(objective, workerModel, budget, concurrency, flexNote);
154
179
  const fileInstruction = outFile ? `\n\nAFTER generating the JSON, also write it to ${outFile} using the Write tool.` : "";
155
- const resultText = await runPlannerQuery(prompt + fileInstruction, { cwd, model: plannerModel, permissionMode, outputFormat: TASKS_SCHEMA }, onLog);
180
+ let resultText;
181
+ try {
182
+ resultText = await runPlannerQuery(prompt + fileInstruction, { cwd, model: plannerModel, permissionMode, outputFormat: TASKS_SCHEMA }, onLog);
183
+ }
184
+ catch (err) {
185
+ const salvaged = salvageFromFile(outFile, budget, onLog, err?.message ?? String(err));
186
+ if (salvaged)
187
+ return salvaged;
188
+ throw err;
189
+ }
156
190
  const parsed = await extractTaskJson(resultText, async () => {
157
191
  onLog("Retrying...");
158
192
  return runPlannerQuery(`Your previous response was not valid JSON. Respond with ONLY a JSON object {"tasks":[{"prompt":"..."}]}.\n\n${prompt}`, { cwd, model: plannerModel, permissionMode, outputFormat: TASKS_SCHEMA }, onLog);
@@ -234,7 +268,16 @@ Requirements:
234
268
  Respond with ONLY a JSON object (no markdown fences):
235
269
  {"tasks": [{"prompt": "..."}]}${fileInstruction}`;
236
270
  onLog("Synthesizing...");
237
- const resultText = await runPlannerQuery(prompt, { cwd, model: plannerModel, permissionMode, outputFormat: TASKS_SCHEMA }, onLog);
271
+ let resultText;
272
+ try {
273
+ resultText = await runPlannerQuery(prompt, { cwd, model: plannerModel, permissionMode, outputFormat: TASKS_SCHEMA }, onLog);
274
+ }
275
+ catch (err) {
276
+ const salvaged = salvageFromFile(outFile, budget, onLog, err?.message ?? String(err));
277
+ if (salvaged)
278
+ return salvaged;
279
+ throw err;
280
+ }
238
281
  const parsed = await extractTaskJson(resultText, async () => {
239
282
  onLog("Retrying...");
240
283
  return runPlannerQuery(`Your previous response was not valid JSON. Respond with ONLY a JSON object {"tasks":[{"prompt":"..."}]}.\n\n${prompt}`, { cwd, model: plannerModel, permissionMode, outputFormat: TASKS_SCHEMA }, onLog);
package/dist/run.js CHANGED
@@ -46,8 +46,16 @@ export async function executeRun(cfg) {
46
46
  branches.push(...rs.branches);
47
47
  flex = rs.flex;
48
48
  waveHistory.push(...loadWaveHistory(runDir));
49
- console.log(chalk.green(`\n ✓ Resumed`) + chalk.dim(` · wave ${waveNum + 1} · ${remaining} remaining · $${accCost.toFixed(2)} spent · ${waveHistory.length} prior waves\n`));
50
- waveNum++;
49
+ // Planning-phase resume starts at wave 0 (nothing ran before); all other
50
+ // resumes bump to the next wave since rs.waveNum is the last completed one.
51
+ const fromPlanning = rs.phase === "planning";
52
+ if (fromPlanning && !existsSync(join(runDir, "goal.md")) && objective) {
53
+ writeFileSync(join(runDir, "goal.md"), `## Original Objective\n${objective}`, "utf-8");
54
+ }
55
+ const detail = fromPlanning ? `${currentTasks.length} tasks from plan` : `${waveHistory.length} prior waves`;
56
+ console.log(chalk.green(`\n ✓ Resumed`) + chalk.dim(` · wave ${waveNum + 1} · ${remaining} remaining · $${accCost.toFixed(2)} spent · ${detail}\n`));
57
+ if (!fromPlanning)
58
+ waveNum++;
51
59
  }
52
60
  else {
53
61
  if (objective && !existsSync(join(runDir, "goal.md"))) {
package/dist/state.js CHANGED
@@ -187,10 +187,15 @@ export function findIncompleteRuns(rootDir, filterCwd) {
187
187
  const dirs = readdirSync(runsDir).sort().reverse();
188
188
  const results = [];
189
189
  for (const d of dirs) {
190
- const state = loadRunState(join(runsDir, d));
191
- if (state && state.phase !== "done" && state.cwd === filterCwd) {
192
- results.push({ dir: join(runsDir, d), state });
193
- }
190
+ const runDir = join(runsDir, d);
191
+ const state = loadRunState(runDir);
192
+ if (!state || state.phase === "done" || state.cwd !== filterCwd)
193
+ continue;
194
+ // Planning-phase runs are only resumable if tasks.json was actually
195
+ // written — resuming without tasks is nothing to resume.
196
+ if (state.phase === "planning" && !existsSync(join(runDir, "tasks.json")))
197
+ continue;
198
+ results.push({ dir: runDir, state });
194
199
  }
195
200
  return results;
196
201
  }
package/dist/types.d.ts CHANGED
@@ -172,7 +172,7 @@ export interface RunState {
172
172
  accOut?: number;
173
173
  accTools?: number;
174
174
  branches: BranchRecord[];
175
- phase: "steering" | "capped" | "done" | "stopped";
175
+ phase: "planning" | "steering" | "capped" | "done" | "stopped";
176
176
  startedAt: string;
177
177
  cwd: string;
178
178
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-overnight",
3
- "version": "1.11.6",
3
+ "version": "1.11.7",
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": {