claude-overnight 0.5.0 → 1.0.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
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { readFileSync, existsSync, mkdirSync, readdirSync, rmSync } from "fs";
2
+ import { readFileSync, existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from "fs";
3
3
  import { resolve, dirname, join } from "path";
4
4
  import { fileURLToPath } from "url";
5
5
  import { execSync } from "child_process";
@@ -7,7 +7,7 @@ import { createInterface } from "readline";
7
7
  import chalk from "chalk";
8
8
  import { query } from "@anthropic-ai/claude-agent-sdk";
9
9
  import { Swarm } from "./swarm.js";
10
- import { planTasks, refinePlan, detectModelTier, steerWave, identifyThemes, buildThinkingTasks, orchestrate } from "./planner.js";
10
+ import { planTasks, refinePlan, detectModelTier, steerWave, identifyThemes, buildThinkingTasks, buildReflectionTasks, orchestrate } from "./planner.js";
11
11
  import { startRenderLoop, renderSummary } from "./ui.js";
12
12
  // ── CLI flag parsing ──
13
13
  function parseCliFlags(argv) {
@@ -270,7 +270,7 @@ function showPlan(tasks) {
270
270
  }
271
271
  console.log(chalk.dim(` ${"─".repeat(ruleLen)}\n`));
272
272
  }
273
- function readDesignDocs(dir) {
273
+ function readMdDir(dir) {
274
274
  try {
275
275
  const files = readdirSync(dir).filter(f => f.endsWith(".md")).sort();
276
276
  return files.map(f => {
@@ -282,6 +282,191 @@ function readDesignDocs(dir) {
282
282
  return "";
283
283
  }
284
284
  }
285
+ function readRunMemory(runDir, previousRuns) {
286
+ let goal = "", status = "";
287
+ try {
288
+ goal = readFileSync(join(runDir, "goal.md"), "utf-8");
289
+ }
290
+ catch { }
291
+ try {
292
+ status = readFileSync(join(runDir, "status.md"), "utf-8");
293
+ }
294
+ catch { }
295
+ return {
296
+ designs: readMdDir(join(runDir, "designs")),
297
+ reflections: readMdDir(join(runDir, "reflections")),
298
+ milestones: readMdDir(join(runDir, "milestones")),
299
+ status,
300
+ goal,
301
+ previousRuns,
302
+ };
303
+ }
304
+ function writeStatus(baseDir, status) {
305
+ writeFileSync(join(baseDir, "status.md"), status, "utf-8");
306
+ }
307
+ function saveRunState(runDir, state) {
308
+ mkdirSync(runDir, { recursive: true });
309
+ writeFileSync(join(runDir, "run.json"), JSON.stringify(state, null, 2), "utf-8");
310
+ }
311
+ function loadRunState(runDir) {
312
+ try {
313
+ return JSON.parse(readFileSync(join(runDir, "run.json"), "utf-8"));
314
+ }
315
+ catch {
316
+ return null;
317
+ }
318
+ }
319
+ /** Find the latest incomplete run, or null. */
320
+ function findIncompleteRun(rootDir) {
321
+ const runsDir = join(rootDir, "runs");
322
+ try {
323
+ const dirs = readdirSync(runsDir).sort().reverse(); // newest first
324
+ for (const d of dirs) {
325
+ const state = loadRunState(join(runsDir, d));
326
+ if (state && state.phase !== "done")
327
+ return { dir: join(runsDir, d), state };
328
+ }
329
+ }
330
+ catch { }
331
+ return null;
332
+ }
333
+ /** Read final status + goal from all completed previous runs (newest first, max 5). */
334
+ function readPreviousRunKnowledge(rootDir) {
335
+ const runsDir = join(rootDir, "runs");
336
+ try {
337
+ const dirs = readdirSync(runsDir).sort().reverse();
338
+ const summaries = [];
339
+ for (const d of dirs) {
340
+ if (summaries.length >= 5)
341
+ break;
342
+ const state = loadRunState(join(runsDir, d));
343
+ if (!state || state.phase !== "done")
344
+ continue;
345
+ let status = "";
346
+ try {
347
+ status = readFileSync(join(runsDir, d, "status.md"), "utf-8");
348
+ }
349
+ catch { }
350
+ let goal = "";
351
+ try {
352
+ goal = readFileSync(join(runsDir, d, "goal.md"), "utf-8");
353
+ }
354
+ catch { }
355
+ const date = d.replace("T", " ").slice(0, 19);
356
+ const cost = state.accCost > 0 ? ` · $${state.accCost.toFixed(2)}` : "";
357
+ summaries.push(`### Run ${date} (${state.accCompleted} tasks${cost})\n${status || "(no status recorded)"}\n${goal ? `Goal: ${goal.slice(0, 500)}` : ""}`);
358
+ }
359
+ return summaries.join("\n\n");
360
+ }
361
+ catch {
362
+ return "";
363
+ }
364
+ }
365
+ function createRunDir(rootDir) {
366
+ const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
367
+ const runDir = join(rootDir, "runs", ts);
368
+ mkdirSync(join(runDir, "designs"), { recursive: true });
369
+ mkdirSync(join(runDir, "reflections"), { recursive: true });
370
+ mkdirSync(join(runDir, "milestones"), { recursive: true });
371
+ mkdirSync(join(runDir, "sessions"), { recursive: true });
372
+ return runDir;
373
+ }
374
+ function saveWaveSession(baseDir, waveNum, kind, swarm) {
375
+ const dir = join(baseDir, "sessions");
376
+ mkdirSync(dir, { recursive: true });
377
+ writeFileSync(join(dir, `wave-${waveNum}.json`), JSON.stringify({
378
+ wave: waveNum, kind,
379
+ agents: swarm.agents.map(a => ({
380
+ id: a.id,
381
+ prompt: a.task.prompt,
382
+ status: a.status,
383
+ error: a.error,
384
+ cost: a.costUsd,
385
+ toolCalls: a.toolCalls,
386
+ filesChanged: a.filesChanged,
387
+ duration: a.finishedAt && a.startedAt ? a.finishedAt - a.startedAt : 0,
388
+ branch: a.branch,
389
+ })),
390
+ totalCost: swarm.totalCostUsd,
391
+ }, null, 2), "utf-8");
392
+ }
393
+ function recordBranches(swarm, branches) {
394
+ for (const a of swarm.agents) {
395
+ if (a.branch) {
396
+ branches.push({
397
+ branch: a.branch,
398
+ taskPrompt: a.task.prompt.slice(0, 200),
399
+ status: a.status === "done" ? "unmerged" : "failed",
400
+ filesChanged: a.filesChanged ?? 0,
401
+ costUsd: a.costUsd ?? 0,
402
+ });
403
+ }
404
+ }
405
+ // Update with merge results
406
+ for (const mr of swarm.mergeResults) {
407
+ const br = branches.find(b => b.branch === mr.branch);
408
+ if (br)
409
+ br.status = mr.ok ? "merged" : "merge-failed";
410
+ }
411
+ }
412
+ function autoMergeBranches(cwd, branches, onLog) {
413
+ const unmerged = branches.filter(b => b.status === "unmerged" && b.filesChanged > 0);
414
+ if (unmerged.length === 0)
415
+ return;
416
+ onLog(`Merging ${unmerged.length} unmerged branches...`);
417
+ for (const br of unmerged) {
418
+ try {
419
+ execSync(`git merge --no-edit "${br.branch}"`, { cwd, encoding: "utf-8", stdio: "pipe" });
420
+ br.status = "merged";
421
+ onLog(` ✓ ${br.branch} (${br.filesChanged} files)`);
422
+ }
423
+ catch {
424
+ try {
425
+ try {
426
+ execSync("git merge --abort", { cwd, encoding: "utf-8", stdio: "pipe" });
427
+ }
428
+ catch { }
429
+ execSync(`git merge --no-edit -X theirs "${br.branch}"`, { cwd, encoding: "utf-8", stdio: "pipe" });
430
+ br.status = "merged";
431
+ onLog(` ✓ ${br.branch} (auto-resolved)`);
432
+ }
433
+ catch {
434
+ try {
435
+ execSync("git merge --abort", { cwd, encoding: "utf-8", stdio: "pipe" });
436
+ }
437
+ catch { }
438
+ br.status = "merge-failed";
439
+ onLog(` ✗ ${br.branch} (conflict — preserved for manual merge)`);
440
+ }
441
+ }
442
+ }
443
+ }
444
+ function archiveMilestone(baseDir, waveNum) {
445
+ const statusPath = join(baseDir, "status.md");
446
+ if (!existsSync(statusPath))
447
+ return;
448
+ const content = readFileSync(statusPath, "utf-8");
449
+ if (!content.trim())
450
+ return;
451
+ const milestoneDir = join(baseDir, "milestones");
452
+ mkdirSync(milestoneDir, { recursive: true });
453
+ const ts = new Date().toISOString().slice(0, 19).replace("T", " ");
454
+ writeFileSync(join(milestoneDir, `wave-${waveNum}.md`), `# Milestone — Wave ${waveNum} (${ts})\n\n${content}`, "utf-8");
455
+ }
456
+ function writeGoalUpdate(baseDir, update) {
457
+ const goalPath = join(baseDir, "goal.md");
458
+ let existing = "";
459
+ try {
460
+ existing = readFileSync(goalPath, "utf-8");
461
+ }
462
+ catch { }
463
+ const ts = new Date().toISOString().slice(0, 19).replace("T", " ");
464
+ const entry = `\n\n## Update — ${ts}\n${update}`;
465
+ const full = existing + entry;
466
+ // Keep it bounded: original + last ~3000 chars of updates
467
+ const trimmed = full.length > 4000 ? full.slice(0, 1000) + "\n\n...\n\n" + full.slice(-3000) : full;
468
+ writeFileSync(goalPath, trimmed, "utf-8");
469
+ }
285
470
  const BRAILLE = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
286
471
  function makeProgressLog() {
287
472
  let frame = 0;
@@ -386,6 +571,77 @@ async function main() {
386
571
  }
387
572
  if (noTTY)
388
573
  console.log(chalk.dim(" Non-interactive mode — using defaults\n"));
574
+ // ── Show run history ──
575
+ const rootDir = join(cwd, ".claude-overnight");
576
+ const runsDir = join(rootDir, "runs");
577
+ let completedRuns = [];
578
+ try {
579
+ const dirs = readdirSync(runsDir).sort().reverse();
580
+ for (const d of dirs) {
581
+ const s = loadRunState(join(runsDir, d));
582
+ if (s && s.phase === "done")
583
+ completedRuns.push({ dir: join(runsDir, d), state: s });
584
+ }
585
+ }
586
+ catch { }
587
+ if (completedRuns.length > 0 && !noTTY) {
588
+ console.log(chalk.dim(`\n ${completedRuns.length} previous run${completedRuns.length > 1 ? "s" : ""}`));
589
+ for (const r of completedRuns.slice(0, 3)) {
590
+ const date = r.state.startedAt?.slice(0, 10) || "unknown";
591
+ const obj = r.state.objective?.slice(0, 40) || "";
592
+ 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 ? "…" : ""}`));
594
+ }
595
+ }
596
+ // ── Resume detection ──
597
+ let resuming = false;
598
+ let resumeState = null;
599
+ let resumeRunDir;
600
+ const incomplete = findIncompleteRun(rootDir);
601
+ if (incomplete && incomplete.state.cwd === cwd && !noTTY && tasks.length === 0) {
602
+ const prev = incomplete.state;
603
+ const merged = prev.branches.filter(b => b.status === "merged").length;
604
+ const unmerged = prev.branches.filter(b => b.status === "unmerged").length;
605
+ const failed = prev.branches.filter(b => b.status === "failed" || b.status === "merge-failed").length;
606
+ const obj = prev.objective?.slice(0, 50) || "";
607
+ // Read last status for context
608
+ let lastStatus = "";
609
+ try {
610
+ lastStatus = readFileSync(join(incomplete.dir, "status.md"), "utf-8").trim().slice(0, 120);
611
+ }
612
+ catch { }
613
+ console.log(chalk.yellow(`\n ⚠ Interrupted run`));
614
+ const boxLines = [
615
+ `${obj}${obj.length >= 50 ? "…" : ""}`,
616
+ `${prev.accCompleted}/${prev.budget} sessions · ${prev.waveNum + 1} waves · $${prev.accCost.toFixed(2)}`,
617
+ ];
618
+ if (lastStatus)
619
+ boxLines.push(lastStatus);
620
+ if (merged + unmerged + failed > 0)
621
+ boxLines.push(`${merged} merged · ${unmerged} unmerged · ${failed} failed branches`);
622
+ const boxW = Math.max(...boxLines.map(l => l.length)) + 4;
623
+ console.log(chalk.dim(` ╭${"─".repeat(boxW)}╮`));
624
+ for (const line of boxLines)
625
+ console.log(chalk.dim(" │") + ` ${line.padEnd(boxW - 2)}` + chalk.dim("│"));
626
+ console.log(chalk.dim(` ╰${"─".repeat(boxW)}╯`));
627
+ const action = await selectKey("", [
628
+ { key: "r", desc: "esume" },
629
+ { key: "f", desc: "resh" },
630
+ { key: "q", desc: "uit" },
631
+ ]);
632
+ if (action === "q") {
633
+ process.exit(0);
634
+ }
635
+ if (action === "r") {
636
+ resuming = true;
637
+ resumeState = prev;
638
+ resumeRunDir = incomplete.dir;
639
+ if (unmerged > 0) {
640
+ console.log("");
641
+ autoMergeBranches(cwd, prev.branches, (msg) => console.log(chalk.dim(` ${msg}`)));
642
+ }
643
+ }
644
+ }
389
645
  // ── Interactive flow: Objective → Budget → Model → Usage cap → Plan → Review ──
390
646
  let workerModel;
391
647
  let plannerModel;
@@ -463,6 +719,8 @@ async function main() {
463
719
  parts.push("flex");
464
720
  if (usageCap != null)
465
721
  parts.push(`cap ${Math.round(usageCap * 100)}%`);
722
+ if (completedRuns.length > 0)
723
+ parts.push(`${completedRuns.length} prior`);
466
724
  const inner = parts.join(chalk.dim(" · "));
467
725
  const innerLen = parts.join(" · ").length;
468
726
  console.log(chalk.dim(`\n ╭${"─".repeat(innerLen + 4)}╮`));
@@ -506,13 +764,17 @@ async function main() {
506
764
  console.log(chalk.dim(` ${workerModel} concurrency=${concurrency} worktrees=${useWorktrees} merge=${mergeStrategy} perms=${permissionMode}${capStr}`));
507
765
  }
508
766
  // ── Flex mode: adaptive multi-wave planning ──
509
- const flex = !argv.includes("--no-flex") && (fileCfg?.flexiblePlan ?? objective != null) && objective != null && (budget ?? 10) > 2;
767
+ let flex = !argv.includes("--no-flex") && (fileCfg?.flexiblePlan ?? objective != null) && objective != null && (budget ?? 10) > 2;
510
768
  const agentTimeoutMs = cliFlags.timeout ? parseFloat(cliFlags.timeout) * 1000 : undefined;
511
769
  let thinkingUsed = 0;
512
770
  let thinkingCost = 0, thinkingIn = 0, thinkingOut = 0, thinkingTools = 0;
513
- let designContext;
771
+ let thinkingHistory;
772
+ // Create run directory early so thinking wave can use it
773
+ const runDir = resuming && resumeRunDir ? resumeRunDir : createRunDir(rootDir);
774
+ const previousKnowledge = readPreviousRunKnowledge(rootDir);
514
775
  // ── Plan phase (interactive: review loop, non-interactive: auto-plan or skip) ──
515
776
  const needsPlan = tasks.length === 0;
777
+ const designDir = join(runDir, "designs");
516
778
  if (needsPlan) {
517
779
  if (noTTY) {
518
780
  console.error(chalk.red(" No tasks provided and stdin is not a TTY. Provide tasks via args or a .json file."));
@@ -521,10 +783,10 @@ async function main() {
521
783
  process.stdout.write("\x1B[?25l");
522
784
  const planRestore = () => process.stdout.write("\x1B[?25h");
523
785
  const useThinking = flex && (budget ?? 10) > concurrency * 3;
524
- const designDir = join(cwd, ".claude-overnight", "designs");
786
+ const thinkingCount = useThinking ? Math.min(Math.max(concurrency, Math.ceil((budget ?? 10) * 0.005)), 10) : 0;
525
787
  try {
526
788
  if (useThinking) {
527
- // Phase 1: Quick theme identification
789
+ // Phase 1: Quick theme identification → review → then autonomous
528
790
  let themeFrame = 0;
529
791
  const themeSpinner = setInterval(() => {
530
792
  const spin = chalk.cyan(BRAILLE[themeFrame++ % BRAILLE.length]);
@@ -532,15 +794,54 @@ async function main() {
532
794
  }, 120);
533
795
  let themes;
534
796
  try {
535
- themes = await identifyThemes(objective, concurrency, plannerModel, permissionMode);
797
+ themes = await identifyThemes(objective, thinkingCount, plannerModel, permissionMode);
536
798
  }
537
799
  finally {
538
800
  clearInterval(themeSpinner);
539
801
  }
540
- process.stdout.write(`\x1B[2K\r ${chalk.green(`\u2713 ${themes.length} themes`)}\n`);
541
- // Phase 2: Thinking waveagents explore codebase
802
+ process.stdout.write(`\x1B[2K\r ${chalk.green(`\u2713 ${themes.length} themes`)}\n\n`);
803
+ // Show themes for reviewthis is the LAST user interaction
804
+ planRestore();
805
+ let reviewing = true;
806
+ while (reviewing) {
807
+ for (let i = 0; i < themes.length; i++) {
808
+ console.log(chalk.dim(` ${String(i + 1).padStart(3)}.`) + ` ${themes[i]}`);
809
+ }
810
+ console.log(chalk.dim(`\n ${thinkingCount} thinking agents → orchestrate → ${(budget ?? 10) - thinkingCount} execution sessions\n`));
811
+ const action = await selectKey(`${chalk.white(`${themes.length} themes`)} ${chalk.dim(`· ${thinkingCount} thinking · ${concurrency} concurrent`)}`, [
812
+ { key: "r", desc: "un" },
813
+ { key: "e", desc: "dit" },
814
+ { key: "q", desc: "uit" },
815
+ ]);
816
+ switch (action) {
817
+ case "r":
818
+ reviewing = false;
819
+ break;
820
+ case "e": {
821
+ const feedback = await ask(`\n ${chalk.bold("What should change?")}\n ${chalk.cyan(">")} `);
822
+ if (!feedback)
823
+ break;
824
+ process.stdout.write("\x1B[?25l");
825
+ try {
826
+ themes = await identifyThemes(`${objective}\n\nUser feedback: ${feedback}`, thinkingCount, plannerModel, permissionMode);
827
+ process.stdout.write(`\x1B[2K\r ${chalk.green(`\u2713 ${themes.length} themes`)}\n\n`);
828
+ }
829
+ catch (err) {
830
+ console.error(chalk.red(`\n Re-planning failed: ${err.message}\n`));
831
+ }
832
+ planRestore();
833
+ break;
834
+ }
835
+ case "q":
836
+ console.log(chalk.dim("\n Aborted.\n"));
837
+ process.exit(0);
838
+ }
839
+ }
840
+ // ── From here, fully autonomous — no more user interaction ──
841
+ process.stdout.write("\x1B[?25l");
842
+ // Phase 2: Thinking wave
542
843
  mkdirSync(designDir, { recursive: true });
543
- const thinkingTasks = buildThinkingTasks(objective, themes, designDir, plannerModel);
844
+ const thinkingTasks = buildThinkingTasks(objective, themes, designDir, plannerModel, previousKnowledge || undefined);
544
845
  console.log(chalk.cyan(`\n ◆ Thinking: ${thinkingTasks.length} agents exploring...\n`));
545
846
  const thinkingSwarm = new Swarm({
546
847
  tasks: thinkingTasks, concurrency, cwd,
@@ -564,34 +865,102 @@ async function main() {
564
865
  thinkingIn = thinkingSwarm.totalInputTokens;
565
866
  thinkingOut = thinkingSwarm.totalOutputTokens;
566
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
+ };
567
879
  // Phase 3: Orchestrate from design docs
568
- designContext = readDesignDocs(designDir);
569
- if (designContext) {
880
+ const designs = readMdDir(designDir);
881
+ if (designs) {
570
882
  const orchBudget = Math.min(50, Math.max(concurrency, Math.ceil(((budget ?? 10) - thinkingUsed) * 0.5)));
571
883
  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.`;
572
884
  console.log(chalk.cyan(`\n ◆ Orchestrating plan...\n`));
573
- tasks = await orchestrate(objective, designContext, cwd, plannerModel, workerModel, permissionMode, orchBudget, concurrency, makeProgressLog(), flexNote);
574
- const remaining = (budget ?? 10) - thinkingUsed - tasks.length;
575
- process.stdout.write(`\x1B[2K\r ${chalk.green(`\u2713 ${tasks.length} tasks`)}${chalk.dim(` · ${remaining} remaining`)}\n\n`);
885
+ tasks = await orchestrate(objective, designs, cwd, plannerModel, workerModel, permissionMode, orchBudget, concurrency, makeProgressLog(), flexNote);
886
+ process.stdout.write(`\x1B[2K\r ${chalk.green(`\u2713 ${tasks.length} tasks`)}\n\n`);
576
887
  }
577
888
  else {
578
- // Fallback: no design docs produced, use direct planner
579
- console.log(chalk.yellow(`\n No design docs produced — falling back to direct planning\n`));
889
+ console.log(chalk.yellow(`\n No design docs falling back to direct planning\n`));
580
890
  const waveBudget = Math.min(50, Math.max(concurrency, Math.ceil(((budget ?? 10) - thinkingUsed) * 0.5)));
581
891
  tasks = await planTasks(objective, cwd, plannerModel, workerModel, permissionMode, waveBudget, concurrency, makeProgressLog());
582
892
  process.stdout.write(`\x1B[2K\r ${chalk.green(`\u2713 ${tasks.length} tasks`)}\n\n`);
583
893
  }
584
894
  }
585
895
  else {
586
- // Small budget: direct planning (no thinking wave)
896
+ // Small budget: direct planning review → run
587
897
  const waveBudget = flex ? Math.min(50, Math.max(concurrency, Math.ceil((budget ?? 10) * 0.5))) : budget;
588
898
  const flexNote = flex
589
899
  ? `This is wave 1 of an adaptive multi-wave run (total budget: ${budget}). Plan the highest-impact foundational work first. Future waves will iterate, polish, and expand based on what's learned.`
590
900
  : undefined;
591
901
  console.log(chalk.cyan(`\n ◆ Planning${flex ? " wave 1" : ""}...\n`));
592
902
  tasks = await planTasks(objective, cwd, plannerModel, workerModel, permissionMode, waveBudget, concurrency, makeProgressLog(), flexNote);
593
- const flexHint = flex ? chalk.dim(` (wave 1, ${(budget ?? 10) - tasks.length} remaining)`) : "";
903
+ const flexHint = flex ? chalk.dim(` · wave 1`) : "";
594
904
  process.stdout.write(`\x1B[2K\r ${chalk.green(`\u2713 ${tasks.length} tasks`)}${flexHint}\n\n`);
905
+ // Review loop for small-budget path
906
+ planRestore();
907
+ let reviewing = true;
908
+ while (reviewing) {
909
+ showPlan(tasks);
910
+ const action = await selectKey(`${chalk.white(`${tasks.length} tasks`)} ${chalk.dim(`· ${concurrency} concurrent`)}`, [
911
+ { key: "r", desc: "un" },
912
+ { key: "e", desc: "dit" },
913
+ { key: "c", desc: "hat" },
914
+ { key: "q", desc: "uit" },
915
+ ]);
916
+ switch (action) {
917
+ case "r":
918
+ reviewing = false;
919
+ break;
920
+ case "e": {
921
+ const feedback = await ask(`\n ${chalk.bold("What should change?")}\n ${chalk.cyan(">")} `);
922
+ if (!feedback)
923
+ break;
924
+ console.log(chalk.cyan("\n ◆ Re-planning...\n"));
925
+ process.stdout.write("\x1B[?25l");
926
+ try {
927
+ tasks = await refinePlan(objective, tasks, feedback, cwd, plannerModel, workerModel, permissionMode, budget, concurrency, makeProgressLog());
928
+ process.stdout.write(`\x1B[2K\r ${chalk.green(`\u2713 ${tasks.length} tasks`)}\n\n`);
929
+ }
930
+ catch (err) {
931
+ console.error(chalk.red(`\n Re-planning failed: ${err.message}\n`));
932
+ }
933
+ planRestore();
934
+ break;
935
+ }
936
+ case "c": {
937
+ const question = await ask(`\n ${chalk.bold("Ask about the plan:")}\n ${chalk.cyan(">")} `);
938
+ if (!question)
939
+ break;
940
+ process.stdout.write("\x1B[?25l");
941
+ try {
942
+ let answer = "";
943
+ for await (const msg of query({
944
+ prompt: `You planned these tasks for the objective "${objective}":\n${tasks.map((t, i) => `${i + 1}. ${t.prompt}`).join("\n")}\n\nUser question: ${question}`,
945
+ options: { cwd, model: plannerModel, permissionMode, persistSession: false },
946
+ })) {
947
+ if (msg.type === "result" && msg.subtype === "success")
948
+ answer = msg.result || "";
949
+ }
950
+ planRestore();
951
+ if (answer)
952
+ console.log(chalk.dim(`\n ${answer.slice(0, 500)}\n`));
953
+ }
954
+ catch {
955
+ planRestore();
956
+ }
957
+ break;
958
+ }
959
+ case "q":
960
+ console.log(chalk.dim("\n Aborted.\n"));
961
+ process.exit(0);
962
+ }
963
+ }
595
964
  }
596
965
  }
597
966
  catch (err) {
@@ -602,65 +971,6 @@ async function main() {
602
971
  console.error(chalk.red(`\n Planning failed: ${err.message}\n`));
603
972
  process.exit(1);
604
973
  }
605
- // ── Review loop ──
606
- planRestore();
607
- let reviewing = true;
608
- while (reviewing) {
609
- showPlan(tasks);
610
- const action = await selectKey(`${chalk.white(`${tasks.length} tasks`)} ${chalk.dim(`· ${concurrency} concurrent`)}`, [
611
- { key: "r", desc: "un" },
612
- { key: "e", desc: "dit" },
613
- { key: "c", desc: "hat" },
614
- { key: "q", desc: "uit" },
615
- ]);
616
- switch (action) {
617
- case "r":
618
- reviewing = false;
619
- break;
620
- case "e": {
621
- const feedback = await ask(`\n ${chalk.bold("What should change?")}\n ${chalk.cyan(">")} `);
622
- if (!feedback)
623
- break;
624
- console.log(chalk.cyan("\n ◆ Re-planning...\n"));
625
- process.stdout.write("\x1B[?25l");
626
- try {
627
- tasks = await refinePlan(objective, tasks, feedback, cwd, plannerModel, workerModel, permissionMode, budget, concurrency, makeProgressLog());
628
- process.stdout.write(`\x1B[2K\r ${chalk.green(`\u2713 ${tasks.length} tasks`)}\n\n`);
629
- }
630
- catch (err) {
631
- console.error(chalk.red(`\n Re-planning failed: ${err.message}\n`));
632
- }
633
- planRestore();
634
- break;
635
- }
636
- case "c": {
637
- const question = await ask(`\n ${chalk.bold("Ask about the plan:")}\n ${chalk.cyan(">")} `);
638
- if (!question)
639
- break;
640
- process.stdout.write("\x1B[?25l");
641
- try {
642
- let answer = "";
643
- for await (const msg of query({
644
- prompt: `You planned these tasks for the objective "${objective}":\n${tasks.map((t, i) => `${i + 1}. ${t.prompt}`).join("\n")}\n\nUser question: ${question}`,
645
- options: { cwd, model: plannerModel, permissionMode, persistSession: false },
646
- })) {
647
- if (msg.type === "result" && msg.subtype === "success")
648
- answer = msg.result || "";
649
- }
650
- planRestore();
651
- if (answer)
652
- console.log(chalk.dim(`\n ${answer.slice(0, 500)}\n`));
653
- }
654
- catch {
655
- planRestore();
656
- }
657
- break;
658
- }
659
- case "q":
660
- console.log(chalk.dim("\n Aborted.\n"));
661
- process.exit(0);
662
- }
663
- }
664
974
  }
665
975
  if (tasks.length === 0) {
666
976
  console.error("No tasks provided.");
@@ -675,14 +985,62 @@ async function main() {
675
985
  process.stdout.write("\x1B[?25l");
676
986
  const restore = () => process.stdout.write("\x1B[?25h\n");
677
987
  const runStartedAt = Date.now();
678
- // Wave-loop state
988
+ // Wave-loop state — either fresh or resumed
989
+ mkdirSync(join(runDir, "reflections"), { recursive: true });
990
+ mkdirSync(join(runDir, "milestones"), { recursive: true });
991
+ mkdirSync(join(runDir, "sessions"), { recursive: true });
679
992
  let currentSwarm;
680
- let remaining = (budget ?? tasks.length) - thinkingUsed;
681
- let currentTasks = tasks;
682
- let waveNum = 0;
993
+ let remaining;
994
+ let currentTasks;
995
+ let waveNum;
683
996
  const waveHistory = [];
684
- let accCost = thinkingCost, accIn = thinkingIn, accOut = thinkingOut, accCompleted = 0, accFailed = 0, accTools = thinkingTools;
997
+ let accCost, accCompleted, accFailed, accTools;
998
+ let accIn = 0, accOut = 0;
685
999
  let lastCapped = false, lastAborted = false;
1000
+ let lastWaveKind;
1001
+ let reflectionBudgetUsed;
1002
+ const branches = [];
1003
+ if (resuming && resumeState) {
1004
+ // Restore ALL config from saved state
1005
+ remaining = resumeState.remaining;
1006
+ currentTasks = resumeState.currentTasks;
1007
+ waveNum = resumeState.waveNum;
1008
+ accCost = resumeState.accCost;
1009
+ accCompleted = resumeState.accCompleted;
1010
+ accFailed = resumeState.accFailed;
1011
+ accTools = 0;
1012
+ lastWaveKind = resumeState.lastWaveKind;
1013
+ reflectionBudgetUsed = resumeState.reflectionBudgetUsed;
1014
+ branches.push(...resumeState.branches);
1015
+ objective = resumeState.objective;
1016
+ workerModel = resumeState.workerModel;
1017
+ plannerModel = resumeState.plannerModel;
1018
+ budget = resumeState.budget;
1019
+ concurrency = resumeState.concurrency;
1020
+ flex = resumeState.flex;
1021
+ usageCap = resumeState.usageCap;
1022
+ console.log(chalk.green(`\n ✓ Resumed`) + chalk.dim(` · wave ${waveNum + 1} · ${remaining} remaining · $${accCost.toFixed(2)} spent\n`));
1023
+ }
1024
+ else {
1025
+ // Fresh run
1026
+ if (objective && !existsSync(join(runDir, "goal.md"))) {
1027
+ writeFileSync(join(runDir, "goal.md"), `## Original Objective\n${objective}`, "utf-8");
1028
+ }
1029
+ remaining = (budget ?? tasks.length) - thinkingUsed;
1030
+ currentTasks = tasks;
1031
+ waveNum = 0;
1032
+ if (thinkingHistory)
1033
+ waveHistory.push(thinkingHistory);
1034
+ accCost = thinkingCost;
1035
+ accCompleted = 0;
1036
+ accFailed = 0;
1037
+ accTools = thinkingTools;
1038
+ accIn = thinkingIn;
1039
+ accOut = thinkingOut;
1040
+ lastWaveKind = "execute";
1041
+ reflectionBudgetUsed = 0;
1042
+ }
1043
+ const maxReflectionBudget = Math.max(2, Math.ceil((budget ?? 10) * 0.05));
686
1044
  // For flex + branch strategy: create one target branch, waves merge via yolo into it
687
1045
  let runBranch;
688
1046
  let originalRef;
@@ -753,8 +1111,18 @@ async function main() {
753
1111
  remaining -= swarm.completed + swarm.failed;
754
1112
  lastCapped = swarm.cappedOut;
755
1113
  lastAborted = swarm.aborted;
1114
+ recordBranches(swarm, branches);
1115
+ saveWaveSession(runDir, waveNum, lastWaveKind, swarm);
1116
+ saveRunState(runDir, {
1117
+ id: `run-${new Date().toISOString().slice(0, 19)}`, objective: objective, budget: budget ?? tasks.length,
1118
+ remaining, workerModel, plannerModel, concurrency, permissionMode,
1119
+ usageCap, flex, useWorktrees, mergeStrategy, waveNum, currentTasks,
1120
+ lastWaveKind, reflectionBudgetUsed, accCost, accCompleted, accFailed,
1121
+ branches, phase: "steering", startedAt: new Date(runStartedAt).toISOString(), cwd,
1122
+ });
756
1123
  waveHistory.push({
757
1124
  wave: waveNum,
1125
+ kind: lastWaveKind,
758
1126
  tasks: swarm.agents.map(a => ({
759
1127
  prompt: a.task.prompt,
760
1128
  status: a.status,
@@ -764,30 +1132,116 @@ async function main() {
764
1132
  });
765
1133
  if (!flex || remaining <= 0 || swarm.aborted || swarm.cappedOut)
766
1134
  break;
767
- // ── Steer next wave ──
768
- console.log(chalk.cyan("\n ◆ Steering...\n"));
769
- process.stdout.write("\x1B[?25l");
770
- try {
771
- const steer = await steerWave(objective, waveHistory, remaining, cwd, plannerModel, workerModel, permissionMode, concurrency, makeProgressLog(), designContext);
772
- process.stdout.write(`\x1B[2K\r`);
773
- process.stdout.write("\x1B[?25h");
774
- if (steer.done) {
775
- console.log(chalk.green(` \u2713 ${steer.reasoning}\n`));
1135
+ // ── Steer: assess quality and decide next action ──
1136
+ // May loop through reflect→re-steer cycles before producing execution tasks
1137
+ let steerDone = false;
1138
+ let steerAttempts = 0;
1139
+ while (!steerDone && remaining > 0 && !stopping && steerAttempts < 4) {
1140
+ steerAttempts++;
1141
+ console.log(chalk.cyan(`\n ◆ Assessing...\n`));
1142
+ process.stdout.write("\x1B[?25l");
1143
+ try {
1144
+ const memory = readRunMemory(runDir, previousKnowledge || undefined);
1145
+ const steer = await steerWave(objective, waveHistory, remaining, cwd, plannerModel, workerModel, permissionMode, concurrency, makeProgressLog(), memory);
1146
+ process.stdout.write(`\x1B[2K\r`);
1147
+ process.stdout.write("\x1B[?25h");
1148
+ // Persist context layers
1149
+ if (steer.statusUpdate)
1150
+ writeStatus(runDir, steer.statusUpdate);
1151
+ if (steer.goalUpdate) {
1152
+ writeGoalUpdate(runDir, steer.goalUpdate);
1153
+ console.log(chalk.dim(` Goal refined: ${steer.goalUpdate.slice(0, 100)}\n`));
1154
+ }
1155
+ // Archive milestone every ~5 execution waves
1156
+ const execWaves = waveHistory.filter(w => w.kind === "execute").length;
1157
+ if (execWaves > 0 && execWaves % 5 === 0)
1158
+ archiveMilestone(runDir, waveNum);
1159
+ if (steer.done || steer.action === "done") {
1160
+ console.log(chalk.green(` \u2713 ${steer.reasoning}\n`));
1161
+ steerDone = true;
1162
+ remaining = 0; // exit outer loop too
1163
+ break;
1164
+ }
1165
+ if (steer.action === "reflect") {
1166
+ // Safety: no consecutive reflections, budget cap
1167
+ const canReflect = lastWaveKind !== "reflect" && reflectionBudgetUsed + 2 <= maxReflectionBudget;
1168
+ if (!canReflect) {
1169
+ console.log(chalk.dim(` ${steer.reasoning}`));
1170
+ console.log(chalk.yellow(` Reflection skipped (${lastWaveKind === "reflect" ? "consecutive" : "budget cap"}) — re-assessing\n`));
1171
+ lastWaveKind = "execute"; // allow next steer to see non-reflect
1172
+ continue; // re-steer in this inner loop
1173
+ }
1174
+ // Run reflection wave
1175
+ console.log(chalk.dim(` ${steer.reasoning}`));
1176
+ console.log(chalk.cyan(`\n ◆ Reflection: 2 agents reviewing...\n`));
1177
+ const reflectionDir = join(runDir, "reflections");
1178
+ waveNum++;
1179
+ const reflTasks = buildReflectionTasks(objective, memory.goal, reflectionDir, waveNum, plannerModel);
1180
+ const reflSwarm = new Swarm({
1181
+ tasks: reflTasks, concurrency: 2, cwd,
1182
+ model: plannerModel, permissionMode,
1183
+ useWorktrees: false, mergeStrategy: "yolo",
1184
+ agentTimeoutMs, usageCap,
1185
+ });
1186
+ currentSwarm = reflSwarm;
1187
+ const stopReflRender = startRenderLoop(reflSwarm);
1188
+ try {
1189
+ await reflSwarm.run();
1190
+ }
1191
+ finally {
1192
+ stopReflRender();
1193
+ }
1194
+ console.log(renderSummary(reflSwarm));
1195
+ accCost += reflSwarm.totalCostUsd;
1196
+ accIn += reflSwarm.totalInputTokens;
1197
+ accOut += reflSwarm.totalOutputTokens;
1198
+ accCompleted += reflSwarm.completed;
1199
+ accFailed += reflSwarm.failed;
1200
+ accTools += reflSwarm.agents.reduce((sum, a) => sum + a.toolCalls, 0);
1201
+ remaining -= reflSwarm.completed + reflSwarm.failed;
1202
+ reflectionBudgetUsed += reflSwarm.completed + reflSwarm.failed;
1203
+ waveHistory.push({
1204
+ wave: waveNum,
1205
+ kind: "reflect",
1206
+ tasks: reflSwarm.agents.map(a => ({ prompt: a.task.prompt, status: a.status, filesChanged: a.filesChanged, error: a.error })),
1207
+ });
1208
+ lastWaveKind = "reflect";
1209
+ continue; // re-steer with reflection artifacts
1210
+ }
1211
+ // action === "execute"
1212
+ if (steer.tasks.length === 0) {
1213
+ console.log(chalk.green(` \u2713 ${steer.reasoning}\n`));
1214
+ remaining = 0;
1215
+ break;
1216
+ }
1217
+ console.log(chalk.dim(` ${steer.reasoning}\n`));
1218
+ currentTasks = steer.tasks;
1219
+ lastWaveKind = "execute";
1220
+ steerDone = true; // exit inner loop, outer loop runs the tasks
1221
+ }
1222
+ catch (err) {
1223
+ process.stdout.write("\x1B[?25h");
1224
+ console.log(chalk.yellow(` Steering failed: ${err.message?.slice(0, 80)} \u2014 stopping\n`));
1225
+ remaining = 0;
776
1226
  break;
777
1227
  }
778
- console.log(chalk.dim(` ${steer.reasoning}\n`));
779
- currentTasks = steer.tasks;
780
- waveNum++;
781
- }
782
- catch (err) {
783
- process.stdout.write("\x1B[?25h");
784
- console.log(chalk.yellow(` Steering failed: ${err.message?.slice(0, 80)} \u2014 stopping\n`));
785
- break;
786
1228
  }
1229
+ waveNum++;
787
1230
  }
788
- // Clean up design docs
1231
+ // Mark run as done — keep sessions/milestones/status/goal, clean transient files
1232
+ saveRunState(runDir, {
1233
+ id: `run-${new Date().toISOString().slice(0, 19)}`, objective: objective ?? "", budget: budget ?? tasks.length,
1234
+ remaining, workerModel, plannerModel, concurrency, permissionMode,
1235
+ usageCap, flex, useWorktrees, mergeStrategy, waveNum, currentTasks: [],
1236
+ lastWaveKind, reflectionBudgetUsed, accCost, accCompleted, accFailed,
1237
+ branches, phase: "done", startedAt: new Date(runStartedAt).toISOString(), cwd,
1238
+ });
789
1239
  try {
790
- rmSync(join(cwd, ".claude-overnight", "designs"), { recursive: true, force: true });
1240
+ rmSync(join(runDir, "designs"), { recursive: true, force: true });
1241
+ }
1242
+ catch { }
1243
+ try {
1244
+ rmSync(join(runDir, "reflections"), { recursive: true, force: true });
791
1245
  }
792
1246
  catch { }
793
1247
  // Switch back if we created a run branch
@@ -799,48 +1253,40 @@ async function main() {
799
1253
  }
800
1254
  // ── Final summary ──
801
1255
  const waves = waveNum + 1;
802
- const cappedNote = lastCapped ? chalk.yellow(` (capped at ${usageCap != null ? Math.round(usageCap * 100) : 100}%)`) : "";
803
- const summaryText = accFailed > 0
804
- ? chalk.yellow(`${accCompleted} done, ${accFailed} failed`) + cappedNote
805
- : chalk.green(`${accCompleted} done`) + cappedNote;
806
- const costText = accCost > 0 ? chalk.dim(` · $${accCost.toFixed(3)}`) : "";
807
- const wavePart = waves > 1 ? chalk.dim(`${waves} waves · `) : "";
1256
+ const elapsed = Math.round((Date.now() - runStartedAt) / 1000);
1257
+ const elapsedStr = elapsed < 60 ? `${elapsed}s` : elapsed < 3600 ? `${Math.floor(elapsed / 60)}m ${elapsed % 60}s` : `${Math.floor(elapsed / 3600)}h ${Math.floor((elapsed % 3600) / 60)}m`;
1258
+ const totalMerged = branches.filter(b => b.status === "merged").length;
1259
+ const totalConflicts = branches.filter(b => b.status === "merge-failed").length;
808
1260
  console.log(chalk.dim(`\n ${"─".repeat(36)}`));
809
- console.log(` ${chalk.green("✓")} ${chalk.bold("Complete")} ${wavePart}${summaryText}${costText}`);
810
- if (accFailed > 0 && waves === 1) {
811
- const failedAgents = currentSwarm?.agents.filter((a) => a.status === "error") ?? [];
812
- if (failedAgents.length > 0) {
813
- console.log(chalk.red(`\n Failed agents:`));
814
- for (const a of failedAgents) {
815
- console.log(chalk.red(` \u2717 Agent ${a.id + 1}: ${a.task.prompt.slice(0, 60)}${a.task.prompt.length > 60 ? "\u2026" : ""}`));
816
- console.log(chalk.dim(` ${a.error || "unknown error"}`));
817
- }
818
- }
1261
+ console.log(` ${accFailed === 0 ? chalk.green("✓") : chalk.yellow("⚠")} ${chalk.bold("Complete")}\n`);
1262
+ const boxLines = [];
1263
+ const statusLine = accFailed > 0 ? `${accCompleted} done · ${accFailed} failed` : `${accCompleted} done`;
1264
+ boxLines.push(`${waves} wave${waves > 1 ? "s" : ""} · ${statusLine} · $${accCost.toFixed(2)}`);
1265
+ boxLines.push(`${elapsedStr} · ${fmtTokens(accIn)} in / ${fmtTokens(accOut)} out · ${accTools} tools`);
1266
+ if (totalMerged > 0 || totalConflicts > 0)
1267
+ boxLines.push(`${totalMerged} merged${totalConflicts > 0 ? ` · ${totalConflicts} conflicts` : ""}`);
1268
+ if (reflectionBudgetUsed > 0)
1269
+ boxLines.push(`${reflectionBudgetUsed} reflection agents`);
1270
+ if (lastCapped)
1271
+ boxLines.push(chalk.yellow(`Capped at ${usageCap != null ? Math.round(usageCap * 100) : 100}%`));
1272
+ const boxW = Math.max(...boxLines.map(l => l.replace(/\x1B\[[0-9;]*m/g, "").length)) + 4;
1273
+ console.log(chalk.dim(` ╭${"─".repeat(boxW)}╮`));
1274
+ for (const line of boxLines) {
1275
+ const plainLen = line.replace(/\x1B\[[0-9;]*m/g, "").length;
1276
+ console.log(chalk.dim(" │") + ` ${line}${" ".repeat(Math.max(0, boxW - 2 - plainLen))}` + chalk.dim("│"));
1277
+ }
1278
+ console.log(chalk.dim(` ╰${"─".repeat(boxW)}╯`));
1279
+ if (totalConflicts > 0) {
1280
+ const conflictBranches = branches.filter(b => b.status === "merge-failed");
1281
+ console.log(chalk.red(`\n Unresolved conflicts:`));
1282
+ for (const c of conflictBranches)
1283
+ console.log(chalk.red(` ${c.branch}`));
1284
+ console.log(chalk.dim(" git merge <branch> to resolve"));
819
1285
  }
820
- const elapsed = Math.round((Date.now() - runStartedAt) / 1000);
821
- const elapsedStr = elapsed < 60 ? `${elapsed}s` : `${Math.floor(elapsed / 60)}m ${elapsed % 60}s`;
822
- console.log(chalk.dim(` ${elapsedStr} ${fmtTokens(accIn)} in / ${fmtTokens(accOut)} out ${accTools} tool calls`));
823
1286
  if (runBranch) {
824
- console.log(chalk.dim(` Branch: ${runBranch} \u2014 create a PR or: git merge ${runBranch}`));
825
- }
826
- else if (currentSwarm?.mergeResults && currentSwarm.mergeResults.length > 0) {
827
- const merged = currentSwarm.mergeResults.filter((r) => r.ok);
828
- const autoResolved = merged.filter((r) => r.autoResolved).length;
829
- const conflicts = currentSwarm.mergeResults.filter((r) => !r.ok);
830
- const target = currentSwarm.mergeBranch || "HEAD";
831
- if (merged.length > 0) {
832
- const extra = autoResolved > 0 ? chalk.yellow(` (${autoResolved} auto-resolved)`) : "";
833
- console.log(chalk.green(` Merged ${merged.length} branch(es) into ${target}`) + extra);
834
- }
835
- if (currentSwarm.mergeBranch)
836
- console.log(chalk.dim(` Branch: ${currentSwarm.mergeBranch} \u2014 create a PR or: git merge ${currentSwarm.mergeBranch}`));
837
- if (conflicts.length > 0) {
838
- console.log(chalk.red(` ${conflicts.length} unresolved conflict(s):`));
839
- for (const c of conflicts)
840
- console.log(chalk.red(` ${c.branch}`));
841
- console.log(chalk.dim(" Merge manually: git merge <branch>"));
842
- }
1287
+ console.log(chalk.dim(`\n Branch: ${runBranch} git merge ${runBranch}`));
843
1288
  }
1289
+ console.log(chalk.dim(` Run: ${runDir}`));
844
1290
  if (currentSwarm?.logFile)
845
1291
  console.log(chalk.dim(` Log: ${currentSwarm.logFile}`));
846
1292
  console.log("");