claude-overnight 1.0.0 → 1.1.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/README.md CHANGED
@@ -52,7 +52,7 @@ claude-overnight
52
52
  ◆ Assessing... ✓ Vision met
53
53
  ```
54
54
 
55
- You interact once (objective, budget, model, review themes), then everything runs autonomously — thinking, planning, executing, reflecting, steering. Rate-limited? It waits and retries. Crash? Resume where you left off.
55
+ You interact once (objective, budget, model, review themes), then everything runs autonomously — thinking, planning, executing, reflecting, steering. Rate-limited? It waits and retries. Crash? Resume where you left off. Capped at usage limit? Pick up next time with full context preserved.
56
56
 
57
57
  ## How it works
58
58
 
@@ -62,7 +62,7 @@ For budgets > 15, the tool launches **architect agents** that explore your codeb
62
62
 
63
63
  ### 2. Orchestration
64
64
 
65
- An orchestrator agent reads all design documents and synthesizes concrete execution tasks — grounded in real files and patterns the architects found. No guesswork.
65
+ An orchestrator agent reads all design documents and synthesizes concrete execution tasks — grounded in real files and patterns the architects found. No guesswork. The task plan is also written to a file for resilience — if orchestration is interrupted, partial results survive.
66
66
 
67
67
  ### 3. Iterative execution
68
68
 
@@ -97,20 +97,30 @@ Every run gets its own folder in `.claude-overnight/runs/`. Nothing is ever over
97
97
  run.json, sessions/
98
98
  ```
99
99
 
100
- If a run crashes, gets rate-limited, or you Ctrl+C:
100
+ Any run that stops before the steering system declares the objective complete — capped at usage limit, Ctrl+C, crash, rate limit timeout, steering failure — is automatically resumable:
101
101
 
102
102
  ```
103
- Interrupted run
103
+ Unfinished run
104
104
  ╭──────────────────────────────────────────────────╮
105
105
  │ refactor auth, add tests, update docs │
106
- │ 50/200 sessions · 3 waves · $69.16
106
+ │ 50/200 sessions · 150 remaining · $69.16
107
107
  │ 34 merged · 16 unmerged · 0 failed branches │
108
108
  ╰──────────────────────────────────────────────────╯
109
109
 
110
110
  Resume │ Fresh │ Quit
111
111
  ```
112
112
 
113
- On resume: unmerged branches auto-merge, the wave loop continues, all context is preserved.
113
+ On resume: unmerged branches auto-merge, the wave loop continues, all context is preserved. Designs and reflections stay on disk until the objective is truly complete.
114
+
115
+ If the thinking phase succeeds but orchestration crashes, the next run detects the orphaned design docs and reuses them — no re-running $9 worth of architect agents:
116
+
117
+ ```
118
+ ✓ Reusing 5 design docs (from prior attempt)
119
+ Focus 0: Project Wizard UI vs VISION.md Flow
120
+ Focus 1: Team Load and Rebalancer Surface
121
+ Focus 2: Code Health After Swarm Wave
122
+ ...
123
+ ```
114
124
 
115
125
  **Knowledge carries forward** — new runs inherit knowledge from completed previous runs. Thinking agents and steering see what past runs built. Run 2 knows run 1 already built the auth system.
116
126
 
@@ -186,9 +196,10 @@ Built for unattended runs lasting hours or days.
186
196
 
187
197
  - **Hard block**: pauses until the rate limit window resets, then resumes
188
198
  - **Soft throttle**: slows dispatch at >75% utilization
199
+ - **Cooldown between phases**: waits for rate limit reset after thinking before starting orchestration
189
200
  - **Retry with backoff**: transient errors (429, overloaded) retry automatically
190
- - **Usage cap**: set a ceiling, active agents finish, no new ones start
191
- - **Planner retries**: steering and orchestration also retry on rate limits (30s/60s/120s backoff)
201
+ - **Usage cap**: set a ceiling, active agents finish, no new ones start — run is resumable
202
+ - **Planner retries**: steering and orchestration retry on rate limits (30s/60s/120s backoff) with full context
192
203
 
193
204
  ## Worktrees and merging
194
205
 
package/dist/index.js CHANGED
@@ -330,6 +330,24 @@ function findIncompleteRun(rootDir) {
330
330
  catch { }
331
331
  return null;
332
332
  }
333
+ /** Find orphaned designs: a run where thinking succeeded but orchestration crashed (has designs, no run.json). */
334
+ function findOrphanedDesigns(rootDir) {
335
+ const runsDir = join(rootDir, "runs");
336
+ try {
337
+ const dirs = readdirSync(runsDir).sort().reverse();
338
+ for (const d of dirs) {
339
+ const runDir = join(runsDir, d);
340
+ const hasState = existsSync(join(runDir, "run.json"));
341
+ if (hasState)
342
+ continue; // has state — either complete or properly resumable
343
+ const designs = readMdDir(join(runDir, "designs"));
344
+ if (designs)
345
+ return runDir;
346
+ }
347
+ }
348
+ catch { }
349
+ return null;
350
+ }
333
351
  /** Read final status + goal from all completed previous runs (newest first, max 5). */
334
352
  function readPreviousRunKnowledge(rootDir) {
335
353
  const runsDir = join(rootDir, "runs");
@@ -588,9 +606,18 @@ async function main() {
588
606
  console.log(chalk.dim(`\n ${completedRuns.length} previous run${completedRuns.length > 1 ? "s" : ""}`));
589
607
  for (const r of completedRuns.slice(0, 3)) {
590
608
  const date = r.state.startedAt?.slice(0, 10) || "unknown";
591
- const obj = r.state.objective?.slice(0, 40) || "";
609
+ const obj = r.state.objective?.slice(0, 50) || "";
592
610
  const cost = r.state.accCost > 0 ? ` · $${r.state.accCost.toFixed(0)}` : "";
593
- console.log(chalk.dim(` ${date} · ${r.state.accCompleted} tasks${cost}${obj ? ` · ${obj}` : ""}${obj.length >= 40 ? "…" : ""}`));
611
+ const merged = r.state.branches.filter(b => b.status === "merged").length;
612
+ console.log(chalk.dim(` ${date} · ${r.state.accCompleted} done · ${merged} merged${cost}${obj ? ` · ${obj}` : ""}${obj.length >= 50 ? "…" : ""}`));
613
+ // Show status if available
614
+ let status = "";
615
+ try {
616
+ status = readFileSync(join(r.dir, "status.md"), "utf-8").trim().split("\n")[0].slice(0, 80);
617
+ }
618
+ catch { }
619
+ if (status)
620
+ console.log(chalk.dim(` ${status}`));
594
621
  }
595
622
  }
596
623
  // ── Resume detection ──
@@ -610,10 +637,11 @@ async function main() {
610
637
  lastStatus = readFileSync(join(incomplete.dir, "status.md"), "utf-8").trim().slice(0, 120);
611
638
  }
612
639
  catch { }
613
- console.log(chalk.yellow(`\n ⚠ Interrupted run`));
640
+ const label = "Unfinished run";
641
+ console.log(chalk.yellow(`\n ⚠ ${label}`));
614
642
  const boxLines = [
615
643
  `${obj}${obj.length >= 50 ? "…" : ""}`,
616
- `${prev.accCompleted}/${prev.budget} sessions · ${prev.waveNum + 1} waves · $${prev.accCost.toFixed(2)}`,
644
+ `${prev.accCompleted}/${prev.budget} sessions · ${prev.remaining} remaining · $${prev.accCost.toFixed(2)}`,
617
645
  ];
618
646
  if (lastStatus)
619
647
  boxLines.push(lastStatus);
@@ -769,8 +797,9 @@ async function main() {
769
797
  let thinkingUsed = 0;
770
798
  let thinkingCost = 0, thinkingIn = 0, thinkingOut = 0, thinkingTools = 0;
771
799
  let thinkingHistory;
772
- // Create run directory early so thinking wave can use it
773
- const runDir = resuming && resumeRunDir ? resumeRunDir : createRunDir(rootDir);
800
+ // Create run directory reuse orphaned run (thinking succeeded, orchestration crashed) if available
801
+ const orphanedDir = !resuming ? findOrphanedDesigns(rootDir) : null;
802
+ const runDir = resuming && resumeRunDir ? resumeRunDir : (orphanedDir ?? createRunDir(rootDir));
774
803
  const previousKnowledge = readPreviousRunKnowledge(rootDir);
775
804
  // ── Plan phase (interactive: review loop, non-interactive: auto-plan or skip) ──
776
805
  const needsPlan = tasks.length === 0;
@@ -839,56 +868,81 @@ async function main() {
839
868
  }
840
869
  // ── From here, fully autonomous — no more user interaction ──
841
870
  process.stdout.write("\x1B[?25l");
842
- // Phase 2: Thinking wave
871
+ // Phase 2: Thinking wave — skip if design docs already exist (e.g. previous orchestration failed)
843
872
  mkdirSync(designDir, { recursive: true });
844
- const thinkingTasks = buildThinkingTasks(objective, themes, designDir, plannerModel, previousKnowledge || undefined);
845
- console.log(chalk.cyan(`\n ◆ Thinking: ${thinkingTasks.length} agents exploring...\n`));
846
- const thinkingSwarm = new Swarm({
847
- tasks: thinkingTasks, concurrency, cwd,
848
- model: plannerModel,
849
- permissionMode,
850
- useWorktrees: false,
851
- mergeStrategy: "yolo",
852
- agentTimeoutMs,
853
- usageCap,
854
- });
855
- const stopThinkRender = startRenderLoop(thinkingSwarm);
856
- try {
857
- await thinkingSwarm.run();
873
+ const existingDesigns = readMdDir(designDir);
874
+ if (existingDesigns) {
875
+ const designFiles = readdirSync(designDir).filter(f => f.endsWith(".md")).sort();
876
+ console.log(chalk.green(`\n ✓ Reusing ${designFiles.length} design docs`) + chalk.dim(` (from prior attempt)`));
877
+ for (const f of designFiles) {
878
+ try {
879
+ const firstLine = readFileSync(join(designDir, f), "utf-8").split("\n")[0].replace(/^#+\s*/, "").trim();
880
+ if (firstLine)
881
+ console.log(chalk.dim(` ${firstLine.slice(0, 80)}`));
882
+ }
883
+ catch { }
884
+ }
885
+ console.log("");
858
886
  }
859
- finally {
860
- stopThinkRender();
887
+ else {
888
+ const thinkingTasks = buildThinkingTasks(objective, themes, designDir, plannerModel, previousKnowledge || undefined);
889
+ console.log(chalk.cyan(`\n ◆ Thinking: ${thinkingTasks.length} agents exploring...\n`));
890
+ const thinkingSwarm = new Swarm({
891
+ tasks: thinkingTasks, concurrency, cwd,
892
+ model: plannerModel,
893
+ permissionMode,
894
+ useWorktrees: false,
895
+ mergeStrategy: "yolo",
896
+ agentTimeoutMs,
897
+ usageCap,
898
+ });
899
+ const stopThinkRender = startRenderLoop(thinkingSwarm);
900
+ try {
901
+ await thinkingSwarm.run();
902
+ }
903
+ finally {
904
+ stopThinkRender();
905
+ }
906
+ console.log(renderSummary(thinkingSwarm));
907
+ thinkingUsed = thinkingSwarm.completed + thinkingSwarm.failed;
908
+ thinkingCost = thinkingSwarm.totalCostUsd;
909
+ thinkingIn = thinkingSwarm.totalInputTokens;
910
+ thinkingOut = thinkingSwarm.totalOutputTokens;
911
+ thinkingTools = thinkingSwarm.agents.reduce((sum, a) => sum + a.toolCalls, 0);
912
+ // Record thinking wave so steering knows what happened
913
+ thinkingHistory = {
914
+ wave: -1,
915
+ kind: "think",
916
+ tasks: thinkingSwarm.agents.map(a => ({
917
+ prompt: a.task.prompt.slice(0, 200),
918
+ status: a.status,
919
+ filesChanged: a.filesChanged,
920
+ error: a.error,
921
+ })),
922
+ };
923
+ // Wait for rate limit reset before orchestration
924
+ if (thinkingSwarm.rateLimitResetsAt) {
925
+ const waitMs = thinkingSwarm.rateLimitResetsAt - Date.now();
926
+ if (waitMs > 0) {
927
+ console.log(chalk.dim(` Waiting ${Math.ceil(waitMs / 1000)}s for rate limit reset...`));
928
+ await new Promise(r => setTimeout(r, waitMs + 2000));
929
+ }
930
+ }
861
931
  }
862
- console.log(renderSummary(thinkingSwarm));
863
- thinkingUsed = thinkingSwarm.completed + thinkingSwarm.failed;
864
- thinkingCost = thinkingSwarm.totalCostUsd;
865
- thinkingIn = thinkingSwarm.totalInputTokens;
866
- thinkingOut = thinkingSwarm.totalOutputTokens;
867
- thinkingTools = thinkingSwarm.agents.reduce((sum, a) => sum + a.toolCalls, 0);
868
- // Record thinking wave so steering knows what happened
869
- thinkingHistory = {
870
- wave: -1,
871
- kind: "think",
872
- tasks: thinkingSwarm.agents.map(a => ({
873
- prompt: a.task.prompt.slice(0, 200),
874
- status: a.status,
875
- filesChanged: a.filesChanged,
876
- error: a.error,
877
- })),
878
- };
879
932
  // Phase 3: Orchestrate from design docs
880
933
  const designs = readMdDir(designDir);
934
+ const taskFile = join(runDir, "tasks.json");
881
935
  if (designs) {
882
936
  const orchBudget = Math.min(50, Math.max(concurrency, Math.ceil(((budget ?? 10) - thinkingUsed) * 0.5)));
883
937
  const flexNote = `This is wave 1 of an adaptive multi-wave run (total budget: ${(budget ?? 10) - thinkingUsed}). Plan the highest-impact foundational work first. Future waves will iterate based on what's learned.`;
884
938
  console.log(chalk.cyan(`\n ◆ Orchestrating plan...\n`));
885
- tasks = await orchestrate(objective, designs, cwd, plannerModel, workerModel, permissionMode, orchBudget, concurrency, makeProgressLog(), flexNote);
939
+ tasks = await orchestrate(objective, designs, cwd, plannerModel, workerModel, permissionMode, orchBudget, concurrency, makeProgressLog(), flexNote, taskFile);
886
940
  process.stdout.write(`\x1B[2K\r ${chalk.green(`\u2713 ${tasks.length} tasks`)}\n\n`);
887
941
  }
888
942
  else {
889
943
  console.log(chalk.yellow(`\n No design docs — falling back to direct planning\n`));
890
944
  const waveBudget = Math.min(50, Math.max(concurrency, Math.ceil(((budget ?? 10) - thinkingUsed) * 0.5)));
891
- tasks = await planTasks(objective, cwd, plannerModel, workerModel, permissionMode, waveBudget, concurrency, makeProgressLog());
945
+ tasks = await planTasks(objective, cwd, plannerModel, workerModel, permissionMode, waveBudget, concurrency, makeProgressLog(), undefined, taskFile);
892
946
  process.stdout.write(`\x1B[2K\r ${chalk.green(`\u2713 ${tasks.length} tasks`)}\n\n`);
893
947
  }
894
948
  }
@@ -996,7 +1050,7 @@ async function main() {
996
1050
  const waveHistory = [];
997
1051
  let accCost, accCompleted, accFailed, accTools;
998
1052
  let accIn = 0, accOut = 0;
999
- let lastCapped = false, lastAborted = false;
1053
+ let lastCapped = false, lastAborted = false, objectiveComplete = false;
1000
1054
  let lastWaveKind;
1001
1055
  let reflectionBudgetUsed;
1002
1056
  const branches = [];
@@ -1159,6 +1213,7 @@ async function main() {
1159
1213
  if (steer.done || steer.action === "done") {
1160
1214
  console.log(chalk.green(` \u2713 ${steer.reasoning}\n`));
1161
1215
  steerDone = true;
1216
+ objectiveComplete = true;
1162
1217
  remaining = 0; // exit outer loop too
1163
1218
  break;
1164
1219
  }
@@ -1211,6 +1266,7 @@ async function main() {
1211
1266
  // action === "execute"
1212
1267
  if (steer.tasks.length === 0) {
1213
1268
  console.log(chalk.green(` \u2713 ${steer.reasoning}\n`));
1269
+ objectiveComplete = true;
1214
1270
  remaining = 0;
1215
1271
  break;
1216
1272
  }
@@ -1228,22 +1284,26 @@ async function main() {
1228
1284
  }
1229
1285
  waveNum++;
1230
1286
  }
1231
- // Mark run as done keep sessions/milestones/status/goal, clean transient files
1287
+ // Only truly "done" if steering explicitly completed the objective (or non-flex single wave with budget exhausted)
1288
+ const trulyDone = objectiveComplete || (!flex && remaining <= 0);
1289
+ const finalPhase = trulyDone ? "done" : "capped";
1232
1290
  saveRunState(runDir, {
1233
1291
  id: `run-${new Date().toISOString().slice(0, 19)}`, objective: objective ?? "", budget: budget ?? tasks.length,
1234
1292
  remaining, workerModel, plannerModel, concurrency, permissionMode,
1235
1293
  usageCap, flex, useWorktrees, mergeStrategy, waveNum, currentTasks: [],
1236
1294
  lastWaveKind, reflectionBudgetUsed, accCost, accCompleted, accFailed,
1237
- branches, phase: "done", startedAt: new Date(runStartedAt).toISOString(), cwd,
1295
+ branches, phase: finalPhase, startedAt: new Date(runStartedAt).toISOString(), cwd,
1238
1296
  });
1239
- try {
1240
- rmSync(join(runDir, "designs"), { recursive: true, force: true });
1241
- }
1242
- catch { }
1243
- try {
1244
- rmSync(join(runDir, "reflections"), { recursive: true, force: true });
1297
+ if (trulyDone) {
1298
+ try {
1299
+ rmSync(join(runDir, "designs"), { recursive: true, force: true });
1300
+ }
1301
+ catch { }
1302
+ try {
1303
+ rmSync(join(runDir, "reflections"), { recursive: true, force: true });
1304
+ }
1305
+ catch { }
1245
1306
  }
1246
- catch { }
1247
1307
  // Switch back if we created a run branch
1248
1308
  if (runBranch && originalRef) {
1249
1309
  try {
package/dist/planner.d.ts CHANGED
@@ -27,10 +27,10 @@ export interface RunMemory {
27
27
  }
28
28
  export type ModelTier = "opus" | "sonnet" | "haiku" | "unknown";
29
29
  export declare function detectModelTier(model: string): ModelTier;
30
- 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): Promise<Task[]>;
30
+ 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[]>;
31
31
  export declare function identifyThemes(objective: string, count: number, model: string, permissionMode: PermMode): Promise<string[]>;
32
32
  export declare function buildThinkingTasks(objective: string, themes: string[], designDir: string, plannerModel: string, previousKnowledge?: string): Task[];
33
33
  export declare function buildReflectionTasks(objective: string, goal: string, reflectionDir: string, waveNum: number, plannerModel: string): Task[];
34
- export declare function orchestrate(objective: string, designDocs: string, cwd: string, plannerModel: string, workerModel: string, permissionMode: PermMode, budget: number, concurrency: number, onLog: (text: string) => void, flexNote?: string): Promise<Task[]>;
34
+ export declare function orchestrate(objective: string, designDocs: string, cwd: string, plannerModel: string, workerModel: string, permissionMode: PermMode, budget: number, concurrency: number, onLog: (text: string) => void, flexNote?: string, outFile?: string): Promise<Task[]>;
35
35
  export declare function refinePlan(objective: string, previousTasks: Task[], feedback: string, cwd: string, plannerModel: string, workerModel: string, permissionMode: PermMode, budget: number | undefined, concurrency: number, onLog: (text: string) => void): Promise<Task[]>;
36
36
  export declare function steerWave(objective: string, history: WaveSummary[], remainingBudget: number, cwd: string, plannerModel: string, workerModel: string, permissionMode: PermMode, concurrency: number, onLog: (text: string) => void, runMemory?: RunMemory): Promise<SteerResult>;
package/dist/planner.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { query } from "@anthropic-ai/claude-agent-sdk";
2
+ import { readFileSync } from "fs";
2
3
  const INACTIVITY_MS = 5 * 60 * 1000;
3
4
  export function detectModelTier(model) {
4
5
  const m = model.toLowerCase();
@@ -179,8 +180,8 @@ async function runPlannerQueryOnce(prompt, opts, onLog) {
179
180
  options: {
180
181
  cwd: opts.cwd,
181
182
  model: opts.model,
182
- tools: ["Read", "Glob", "Grep"],
183
- allowedTools: ["Read", "Glob", "Grep"],
183
+ tools: ["Read", "Glob", "Grep", "Write"],
184
+ allowedTools: ["Read", "Glob", "Grep", "Write"],
184
185
  permissionMode: opts.permissionMode,
185
186
  ...(opts.permissionMode === "bypassPermissions" && { allowDangerouslySkipPermissions: true }),
186
187
  persistSession: false,
@@ -311,21 +312,15 @@ function postProcess(raw, budget, onLog) {
311
312
  tasks = tasks.map((t, i) => ({ ...t, id: String(i) }));
312
313
  return tasks;
313
314
  }
314
- export async function planTasks(objective, cwd, plannerModel, workerModel, permissionMode, budget, concurrency, onLog, flexNote) {
315
+ export async function planTasks(objective, cwd, plannerModel, workerModel, permissionMode, budget, concurrency, onLog, flexNote, outFile) {
315
316
  onLog("Analyzing codebase...");
316
- const resultText = await runPlannerQuery(plannerPrompt(objective, workerModel, budget, concurrency, flexNote), { cwd, model: plannerModel, permissionMode }, onLog);
317
+ const prompt = plannerPrompt(objective, workerModel, budget, concurrency, flexNote);
318
+ const fileInstruction = outFile ? `\n\nAFTER generating the JSON, also write it to ${outFile} using the Write tool.` : "";
319
+ const resultText = await runPlannerQuery(prompt + fileInstruction, { cwd, model: plannerModel, permissionMode }, onLog);
317
320
  const parsed = await extractTaskJson(resultText, async () => {
318
- onLog("Retrying for valid JSON...");
319
- let retryText = "";
320
- for await (const msg of query({
321
- prompt: `Your previous response did not contain valid JSON. Output ONLY a JSON object:\n{"tasks":[{"prompt":"..."}]}`,
322
- options: { cwd, model: plannerModel, permissionMode, ...(permissionMode === "bypassPermissions" && { allowDangerouslySkipPermissions: true }), persistSession: false },
323
- })) {
324
- if (msg.type === "result" && msg.subtype === "success")
325
- retryText = msg.result || "";
326
- }
327
- return retryText;
328
- });
321
+ onLog("Retrying...");
322
+ return runPlannerQuery(`Your previous response was not valid JSON. Respond with ONLY a JSON object {"tasks":[{"prompt":"..."}]}.\n\n${prompt}`, { cwd, model: plannerModel, permissionMode }, onLog);
323
+ }, onLog, outFile);
329
324
  let tasks = (parsed.tasks || []).map((t, i) => ({
330
325
  id: String(i),
331
326
  prompt: typeof t === "string" ? t : t.prompt,
@@ -428,9 +423,10 @@ End with ## Priorities: rank the top 3 things that would most improve the result
428
423
  },
429
424
  ];
430
425
  }
431
- export async function orchestrate(objective, designDocs, cwd, plannerModel, workerModel, permissionMode, budget, concurrency, onLog, flexNote) {
426
+ export async function orchestrate(objective, designDocs, cwd, plannerModel, workerModel, permissionMode, budget, concurrency, onLog, flexNote, outFile) {
432
427
  const capability = modelCapabilityBlock(workerModel);
433
428
  const flexLine = flexNote ? `\n\n${flexNote}` : "";
429
+ const fileInstruction = outFile ? `\n\nAFTER generating the JSON, also write it to ${outFile} using the Write tool.` : "";
434
430
  const prompt = `You are a tech lead planning a sprint based on your team's codebase research.
435
431
 
436
432
  Objective: ${objective}
@@ -452,21 +448,13 @@ Requirements:
452
448
  - Priority order: foundational first, polish last${flexLine}
453
449
 
454
450
  Respond with ONLY a JSON object (no markdown fences):
455
- {"tasks": [{"prompt": "..."}]}`;
451
+ {"tasks": [{"prompt": "..."}]}${fileInstruction}`;
456
452
  onLog("Synthesizing...");
457
453
  const resultText = await runPlannerQuery(prompt, { cwd, model: plannerModel, permissionMode }, onLog);
458
454
  const parsed = await extractTaskJson(resultText, async () => {
459
455
  onLog("Retrying...");
460
- let retryText = "";
461
- for await (const msg of query({
462
- prompt: `Output ONLY a JSON object:\n{"tasks":[{"prompt":"..."}]}`,
463
- options: { cwd, model: plannerModel, permissionMode, ...(permissionMode === "bypassPermissions" && { allowDangerouslySkipPermissions: true }), persistSession: false },
464
- })) {
465
- if (msg.type === "result" && msg.subtype === "success")
466
- retryText = msg.result || "";
467
- }
468
- return retryText;
469
- });
456
+ return runPlannerQuery(`Your previous response was not valid JSON. Respond with ONLY a JSON object {"tasks":[{"prompt":"..."}]}.\n\n${prompt}`, { cwd, model: plannerModel, permissionMode }, onLog);
457
+ }, onLog, outFile);
470
458
  let tasks = (parsed.tasks || []).map((t, i) => ({
471
459
  id: String(i),
472
460
  prompt: typeof t === "string" ? t : t.prompt,
@@ -505,16 +493,8 @@ Respond with ONLY a JSON object (no markdown):
505
493
  const resultText = await runPlannerQuery(prompt, { cwd, model: plannerModel, permissionMode }, onLog);
506
494
  const parsed = await extractTaskJson(resultText, async () => {
507
495
  onLog("Retrying...");
508
- let retryText = "";
509
- for await (const msg of query({
510
- prompt: `Output ONLY a JSON object:\n{"tasks":[{"prompt":"..."}]}`,
511
- options: { cwd, model: plannerModel, permissionMode, ...(permissionMode === "bypassPermissions" && { allowDangerouslySkipPermissions: true }), persistSession: false },
512
- })) {
513
- if (msg.type === "result" && msg.subtype === "success")
514
- retryText = msg.result || "";
515
- }
516
- return retryText;
517
- });
496
+ return runPlannerQuery(`Your previous response was not valid JSON. Respond with ONLY a JSON object {"tasks":[{"prompt":"..."}]}.\n\n${prompt}`, { cwd, model: plannerModel, permissionMode }, onLog);
497
+ }, onLog);
518
498
  let tasks = (parsed.tasks || []).map((t, i) => ({
519
499
  id: String(i),
520
500
  prompt: typeof t === "string" ? t : t.prompt,
@@ -574,17 +554,55 @@ function attemptJsonParse(text) {
574
554
  catch { }
575
555
  }
576
556
  }
557
+ // Salvage truncated task JSON — find last complete task object and close
558
+ const tasksMatch = text.match(/\{\s*"tasks"\s*:\s*\[/);
559
+ if (tasksMatch) {
560
+ const lastBrace = text.lastIndexOf("}");
561
+ if (lastBrace > tasksMatch.index) {
562
+ const salvaged = text.slice(tasksMatch.index, lastBrace + 1) + "]}";
563
+ try {
564
+ const obj = JSON.parse(salvaged);
565
+ if (obj?.tasks?.length > 0)
566
+ return obj;
567
+ }
568
+ catch { }
569
+ }
570
+ }
577
571
  return null;
578
572
  }
579
- /** Extract task JSON with validation and one retry. */
580
- async function extractTaskJson(raw, retry) {
573
+ /** Extract task JSON: try file first, then in-memory parse, then retry with context. */
574
+ async function extractTaskJson(raw, retry, onLog, outFile) {
575
+ // 1. Try reading from file (most resilient — survives truncated output)
576
+ if (outFile) {
577
+ try {
578
+ const fileContent = readFileSync(outFile, "utf-8");
579
+ const fromFile = attemptJsonParse(fileContent);
580
+ if (fromFile?.tasks)
581
+ return fromFile;
582
+ }
583
+ catch { }
584
+ }
585
+ // 2. Try parsing result text
581
586
  const first = attemptJsonParse(raw);
582
587
  if (first?.tasks)
583
588
  return first;
589
+ onLog?.(`Parse failed (${raw.length} chars): ${raw.slice(0, 300)}`);
590
+ // 3. Retry with full context
584
591
  const retryText = await retry();
592
+ // Re-check file in case retry wrote it
593
+ if (outFile) {
594
+ try {
595
+ const fileContent = readFileSync(outFile, "utf-8");
596
+ const fromFile = attemptJsonParse(fileContent);
597
+ if (fromFile?.tasks)
598
+ return fromFile;
599
+ }
600
+ catch { }
601
+ }
585
602
  const second = attemptJsonParse(retryText);
586
603
  if (second?.tasks)
587
604
  return second;
605
+ onLog?.(`Retry failed (${retryText.length} chars): ${retryText.slice(0, 300)}`);
588
606
  throw new Error("Planner did not return valid task JSON after retry");
589
607
  }
590
608
  // ── Wave steering ──
@@ -655,14 +673,7 @@ Respond with ONLY a JSON object (no markdown fences):
655
673
  if (first)
656
674
  return first;
657
675
  onLog("Retrying...");
658
- let retryText = "";
659
- for await (const msg of query({
660
- prompt: `Output ONLY a JSON object: {"action":"execute"|"reflect"|"done","done":true/false,"reasoning":"...","tasks":[{"prompt":"..."}]}`,
661
- options: { cwd, model: plannerModel, permissionMode, ...(permissionMode === "bypassPermissions" && { allowDangerouslySkipPermissions: true }), persistSession: false },
662
- })) {
663
- if (msg.type === "result" && msg.subtype === "success")
664
- retryText = msg.result || "";
665
- }
676
+ const retryText = await runPlannerQuery(`Your previous response was not valid JSON. Respond with ONLY a JSON object {"action":"execute"|"reflect"|"done","done":true/false,"reasoning":"...","tasks":[{"prompt":"..."}]}.\n\n${prompt}`, { cwd, model: plannerModel, permissionMode }, onLog);
666
677
  return attemptJsonParse(retryText) ?? { action: "done", done: true, reasoning: "Could not parse steering response" };
667
678
  })();
668
679
  const action = parsed.action || (parsed.done ? "done" : "execute");
package/dist/types.d.ts CHANGED
@@ -125,7 +125,7 @@ export interface RunState {
125
125
  accCompleted: number;
126
126
  accFailed: number;
127
127
  branches: BranchRecord[];
128
- phase: "executing" | "steering" | "reflecting" | "done";
128
+ phase: "executing" | "steering" | "reflecting" | "capped" | "done";
129
129
  startedAt: string;
130
130
  cwd: string;
131
131
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-overnight",
3
- "version": "1.0.0",
3
+ "version": "1.1.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": {