claude-overnight 1.0.0 → 1.0.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
@@ -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");
@@ -769,8 +787,9 @@ async function main() {
769
787
  let thinkingUsed = 0;
770
788
  let thinkingCost = 0, thinkingIn = 0, thinkingOut = 0, thinkingTools = 0;
771
789
  let thinkingHistory;
772
- // Create run directory early so thinking wave can use it
773
- const runDir = resuming && resumeRunDir ? resumeRunDir : createRunDir(rootDir);
790
+ // Create run directory reuse orphaned run (thinking succeeded, orchestration crashed) if available
791
+ const orphanedDir = !resuming ? findOrphanedDesigns(rootDir) : null;
792
+ const runDir = resuming && resumeRunDir ? resumeRunDir : (orphanedDir ?? createRunDir(rootDir));
774
793
  const previousKnowledge = readPreviousRunKnowledge(rootDir);
775
794
  // ── Plan phase (interactive: review loop, non-interactive: auto-plan or skip) ──
776
795
  const needsPlan = tasks.length === 0;
@@ -839,56 +858,71 @@ async function main() {
839
858
  }
840
859
  // ── From here, fully autonomous — no more user interaction ──
841
860
  process.stdout.write("\x1B[?25l");
842
- // Phase 2: Thinking wave
861
+ // Phase 2: Thinking wave — skip if design docs already exist (e.g. previous orchestration failed)
843
862
  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();
863
+ const existingDesigns = readMdDir(designDir);
864
+ if (existingDesigns) {
865
+ console.log(chalk.green(`\n ✓ Reusing ${readdirSync(designDir).filter(f => f.endsWith(".md")).length} existing design docs`) + chalk.dim(` (from prior attempt)\n`));
858
866
  }
859
- finally {
860
- stopThinkRender();
867
+ else {
868
+ const thinkingTasks = buildThinkingTasks(objective, themes, designDir, plannerModel, previousKnowledge || undefined);
869
+ console.log(chalk.cyan(`\n ◆ Thinking: ${thinkingTasks.length} agents exploring...\n`));
870
+ const thinkingSwarm = new Swarm({
871
+ tasks: thinkingTasks, concurrency, cwd,
872
+ model: plannerModel,
873
+ permissionMode,
874
+ useWorktrees: false,
875
+ mergeStrategy: "yolo",
876
+ agentTimeoutMs,
877
+ usageCap,
878
+ });
879
+ const stopThinkRender = startRenderLoop(thinkingSwarm);
880
+ try {
881
+ await thinkingSwarm.run();
882
+ }
883
+ finally {
884
+ stopThinkRender();
885
+ }
886
+ console.log(renderSummary(thinkingSwarm));
887
+ thinkingUsed = thinkingSwarm.completed + thinkingSwarm.failed;
888
+ thinkingCost = thinkingSwarm.totalCostUsd;
889
+ thinkingIn = thinkingSwarm.totalInputTokens;
890
+ thinkingOut = thinkingSwarm.totalOutputTokens;
891
+ thinkingTools = thinkingSwarm.agents.reduce((sum, a) => sum + a.toolCalls, 0);
892
+ // Record thinking wave so steering knows what happened
893
+ thinkingHistory = {
894
+ wave: -1,
895
+ kind: "think",
896
+ tasks: thinkingSwarm.agents.map(a => ({
897
+ prompt: a.task.prompt.slice(0, 200),
898
+ status: a.status,
899
+ filesChanged: a.filesChanged,
900
+ error: a.error,
901
+ })),
902
+ };
903
+ // Wait for rate limit reset before orchestration
904
+ if (thinkingSwarm.rateLimitResetsAt) {
905
+ const waitMs = thinkingSwarm.rateLimitResetsAt - Date.now();
906
+ if (waitMs > 0) {
907
+ console.log(chalk.dim(` Waiting ${Math.ceil(waitMs / 1000)}s for rate limit reset...`));
908
+ await new Promise(r => setTimeout(r, waitMs + 2000));
909
+ }
910
+ }
861
911
  }
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
912
  // Phase 3: Orchestrate from design docs
880
913
  const designs = readMdDir(designDir);
914
+ const taskFile = join(runDir, "tasks.json");
881
915
  if (designs) {
882
916
  const orchBudget = Math.min(50, Math.max(concurrency, Math.ceil(((budget ?? 10) - thinkingUsed) * 0.5)));
883
917
  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
918
  console.log(chalk.cyan(`\n ◆ Orchestrating plan...\n`));
885
- tasks = await orchestrate(objective, designs, cwd, plannerModel, workerModel, permissionMode, orchBudget, concurrency, makeProgressLog(), flexNote);
919
+ tasks = await orchestrate(objective, designs, cwd, plannerModel, workerModel, permissionMode, orchBudget, concurrency, makeProgressLog(), flexNote, taskFile);
886
920
  process.stdout.write(`\x1B[2K\r ${chalk.green(`\u2713 ${tasks.length} tasks`)}\n\n`);
887
921
  }
888
922
  else {
889
923
  console.log(chalk.yellow(`\n No design docs — falling back to direct planning\n`));
890
924
  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());
925
+ tasks = await planTasks(objective, cwd, plannerModel, workerModel, permissionMode, waveBudget, concurrency, makeProgressLog(), undefined, taskFile);
892
926
  process.stdout.write(`\x1B[2K\r ${chalk.green(`\u2713 ${tasks.length} tasks`)}\n\n`);
893
927
  }
894
928
  }
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-overnight",
3
- "version": "1.0.0",
3
+ "version": "1.0.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": {