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/README.md +107 -102
- package/dist/index.js +587 -141
- package/dist/planner.d.ts +15 -2
- package/dist/planner.js +126 -24
- package/dist/swarm.js +16 -7
- package/dist/types.d.ts +34 -0
- package/package.json +14 -8
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
//
|
|
802
|
+
process.stdout.write(`\x1B[2K\r ${chalk.green(`\u2713 ${themes.length} themes`)}\n\n`);
|
|
803
|
+
// Show themes for review — this 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
|
-
|
|
569
|
-
if (
|
|
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,
|
|
574
|
-
|
|
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
|
-
|
|
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
|
|
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(`
|
|
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
|
|
681
|
-
let currentTasks
|
|
682
|
-
let waveNum
|
|
993
|
+
let remaining;
|
|
994
|
+
let currentTasks;
|
|
995
|
+
let waveNum;
|
|
683
996
|
const waveHistory = [];
|
|
684
|
-
let accCost
|
|
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
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
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
|
-
//
|
|
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(
|
|
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
|
|
803
|
-
const
|
|
804
|
-
|
|
805
|
-
|
|
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")}
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
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(
|
|
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("");
|