claude-overnight 0.1.2 → 0.3.2

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
@@ -7,12 +7,12 @@ 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 } from "./planner.js";
10
+ import { planTasks, refinePlan, detectModelTier, steerWave } from "./planner.js";
11
11
  import { startRenderLoop, renderSummary } from "./ui.js";
12
12
  // ── CLI flag parsing ──
13
13
  function parseCliFlags(argv) {
14
- const known = new Set(["concurrency", "model", "timeout", "budget"]);
15
- const booleans = new Set(["--dry-run", "-h", "--help", "-v", "--version"]);
14
+ const known = new Set(["concurrency", "model", "timeout", "budget", "usage-cap"]);
15
+ const booleans = new Set(["--dry-run", "-h", "--help", "-v", "--version", "--no-flex"]);
16
16
  const flags = {};
17
17
  const positional = [];
18
18
  for (let i = 0; i < argv.length; i++) {
@@ -43,20 +43,32 @@ function isAuthError(err) {
43
43
  // ── Fetch models via SDK ──
44
44
  async function fetchModels(timeoutMs = 10_000) {
45
45
  let q;
46
+ let timer;
46
47
  try {
47
48
  q = query({ prompt: "", options: { persistSession: false } });
48
49
  const models = await Promise.race([
49
50
  q.supportedModels(),
50
- sleep(timeoutMs).then(() => { throw new Error("model_fetch_timeout"); }),
51
+ new Promise((_, reject) => {
52
+ timer = setTimeout(() => reject(new Error("model_fetch_timeout")), timeoutMs);
53
+ }),
51
54
  ]);
55
+ clearTimeout(timer);
52
56
  q.close();
53
57
  return models;
54
58
  }
55
59
  catch (err) {
60
+ clearTimeout(timer);
56
61
  q?.close();
57
62
  if (err.message === "model_fetch_timeout") {
58
63
  console.warn(chalk.yellow("\n Model fetch timed out — continuing with defaults"));
59
64
  }
65
+ else if (isAuthError(err)) {
66
+ console.error(chalk.red("\n Authentication failed — check your API key or run: claude auth\n"));
67
+ process.exit(1);
68
+ }
69
+ else {
70
+ console.warn(chalk.yellow(`\n Could not fetch models: ${String(err.message || err).slice(0, 80)} — continuing with defaults`));
71
+ }
60
72
  return [];
61
73
  }
62
74
  }
@@ -150,7 +162,7 @@ async function selectKey(label, options) {
150
162
  });
151
163
  }
152
164
  const KNOWN_TASK_FILE_KEYS = new Set([
153
- "tasks", "concurrency", "cwd", "model", "permissionMode", "allowedTools", "worktrees", "mergeStrategy",
165
+ "tasks", "objective", "concurrency", "cwd", "model", "permissionMode", "allowedTools", "worktrees", "mergeStrategy", "usageCap", "flexiblePlan",
154
166
  ]);
155
167
  function loadTaskFile(file) {
156
168
  const path = resolve(file);
@@ -200,8 +212,18 @@ function loadTaskFile(file) {
200
212
  }
201
213
  if (parsed.concurrency !== undefined)
202
214
  validateConcurrency(parsed.concurrency);
215
+ const usageCap = parsed.usageCap;
216
+ if (usageCap != null && (typeof usageCap !== "number" || usageCap < 0 || usageCap > 100)) {
217
+ throw new Error(`usageCap must be a number between 0 and 100 (got ${JSON.stringify(usageCap)})`);
218
+ }
219
+ const flexiblePlan = parsed.flexiblePlan;
220
+ const objective = parsed.objective;
221
+ if (flexiblePlan && typeof objective !== "string") {
222
+ throw new Error(`flexiblePlan requires an "objective" string in the task file`);
223
+ }
203
224
  return {
204
225
  tasks,
226
+ objective: typeof objective === "string" ? objective : undefined,
205
227
  concurrency: parsed.concurrency,
206
228
  model: parsed.model,
207
229
  cwd: parsed.cwd ? resolve(parsed.cwd) : undefined,
@@ -209,6 +231,8 @@ function loadTaskFile(file) {
209
231
  allowedTools: parsed.allowedTools,
210
232
  useWorktrees: parsed.worktrees,
211
233
  mergeStrategy: parsed.mergeStrategy,
234
+ usageCap,
235
+ flexiblePlan,
212
236
  };
213
237
  }
214
238
  // ── Validation helpers ──
@@ -264,8 +288,10 @@ async function main() {
264
288
  --dry-run Show planned tasks without running them
265
289
  --budget=N Target number of agent runs ${chalk.dim("(planner aims for this many tasks)")}
266
290
  --concurrency=N Max parallel agents ${chalk.dim("(default: 5)")}
267
- --model=NAME Model override
291
+ --model=NAME Worker model override ${chalk.dim("(planner always uses best available)")}
292
+ --usage-cap=N Stop at N% utilization ${chalk.dim("(e.g. 90 to save 10% for other work)")}
268
293
  --timeout=SECONDS Agent inactivity timeout ${chalk.dim("(default: 300s, kills only silent agents)")}
294
+ --no-flex Disable adaptive multi-wave planning ${chalk.dim("(run all tasks in one shot)")}
269
295
 
270
296
  ${chalk.dim("Non-interactive defaults (task file / inline / piped):")}
271
297
  model: first available concurrency: 5 worktrees: auto perms: auto
@@ -298,6 +324,10 @@ async function main() {
298
324
  }
299
325
  for (const arg of args) {
300
326
  if (arg.endsWith(".json")) {
327
+ if (tasks.length > 0) {
328
+ console.error(chalk.red(` Cannot mix inline tasks with a task file. Use one or the other.`));
329
+ process.exit(1);
330
+ }
301
331
  fileCfg = loadTaskFile(arg);
302
332
  tasks = fileCfg.tasks;
303
333
  }
@@ -306,6 +336,10 @@ async function main() {
306
336
  process.exit(1);
307
337
  }
308
338
  else {
339
+ if (fileCfg) {
340
+ console.error(chalk.red(` Cannot mix inline tasks with a task file. Use one or the other.`));
341
+ process.exit(1);
342
+ }
309
343
  tasks.push({ id: String(tasks.length), prompt: arg });
310
344
  }
311
345
  }
@@ -321,11 +355,13 @@ async function main() {
321
355
  }
322
356
  if (noTTY)
323
357
  console.log(chalk.dim(" Non-interactive mode — using defaults\n"));
324
- // ── Interactive flow: Objective → Budget → Model → Plan → Review ──
325
- let model;
358
+ // ── Interactive flow: Objective → Budget → Model → Usage cap → Plan → Review ──
359
+ let workerModel;
360
+ let plannerModel;
326
361
  let budget;
327
362
  let concurrency;
328
- let objective;
363
+ let objective = fileCfg?.objective;
364
+ let usageCap;
329
365
  if (!nonInteractive) {
330
366
  console.log(chalk.dim(" Fire off Claude agents, come back to shipped work.\n"));
331
367
  // 1. Objective first — it's the whole point
@@ -342,21 +378,38 @@ async function main() {
342
378
  // 2. Budget — how many agent runs to spend
343
379
  const budgetAns = await ask(chalk.dim("\n Agent budget [10]: "));
344
380
  budget = parseInt(budgetAns) || 10;
345
- // 3. Model arrow keys
381
+ if (budget < 1) {
382
+ console.error(chalk.red(` Budget must be a positive number`));
383
+ process.exit(1);
384
+ }
385
+ // 3. Worker model — planner always uses best available
346
386
  process.stdout.write(chalk.dim(" Fetching models..."));
347
387
  const models = await fetchModels();
348
388
  process.stdout.write(`\x1B[2K\r`);
389
+ // Pick best model for planner (first = most capable)
390
+ plannerModel = models[0]?.value || "claude-sonnet-4-6";
349
391
  if (models.length > 0) {
350
- model = await select("Model:", models.map((m) => ({
392
+ workerModel = await select("Worker model (planner always uses best available):", models.map((m) => ({
351
393
  name: m.displayName,
352
394
  value: m.value,
353
395
  hint: m.description,
354
396
  })));
355
397
  }
356
398
  else {
357
- const ans = await ask(chalk.dim(" Model [claude-sonnet-4-6]: "));
358
- model = ans || "claude-sonnet-4-6";
399
+ const ans = await ask(chalk.dim(" Worker model [claude-sonnet-4-6]: "));
400
+ workerModel = ans || "claude-sonnet-4-6";
359
401
  }
402
+ if (workerModel !== plannerModel) {
403
+ const tier = detectModelTier(workerModel);
404
+ console.log(chalk.dim(`\n Planner: ${plannerModel} · Workers: ${workerModel} (${tier})`));
405
+ }
406
+ // 4. Usage cap — how much of your plan to use
407
+ usageCap = await select("Usage limit:", [
408
+ { name: "Unlimited", value: undefined, hint: "use full capacity, wait through rate limits" },
409
+ { name: "90%", value: 0.9, hint: "leave 10% for other work" },
410
+ { name: "75%", value: 0.75, hint: "conservative, plenty of headroom" },
411
+ { name: "50%", value: 0.5, hint: "use half, keep the rest" },
412
+ ]);
360
413
  // Concurrency defaults based on budget
361
414
  concurrency = Math.min(5, budget);
362
415
  }
@@ -365,9 +418,26 @@ async function main() {
365
418
  let models = [];
366
419
  if (!cliFlags.model && !fileCfg?.model)
367
420
  models = await fetchModels(5_000);
368
- model = cliFlags.model ?? fileCfg?.model ?? (models[0]?.value || "claude-sonnet-4-6");
421
+ workerModel = cliFlags.model ?? fileCfg?.model ?? (models[0]?.value || "claude-sonnet-4-6");
422
+ plannerModel = models[0]?.value || workerModel;
369
423
  concurrency = cliFlags.concurrency ? parseInt(cliFlags.concurrency) : (fileCfg?.concurrency ?? 5);
370
424
  budget = cliFlags.budget ? parseInt(cliFlags.budget) : undefined;
425
+ if (budget != null && (isNaN(budget) || budget < 1)) {
426
+ console.error(chalk.red(` --budget must be a positive integer`));
427
+ process.exit(1);
428
+ }
429
+ const capFlag = cliFlags["usage-cap"];
430
+ if (capFlag != null) {
431
+ const capVal = parseFloat(capFlag);
432
+ if (isNaN(capVal) || capVal < 0 || capVal > 100) {
433
+ console.error(chalk.red(` --usage-cap must be between 0 and 100 (got ${capFlag})`));
434
+ process.exit(1);
435
+ }
436
+ usageCap = capVal / 100;
437
+ }
438
+ else {
439
+ usageCap = fileCfg?.usageCap != null ? fileCfg.usageCap / 100 : undefined;
440
+ }
371
441
  }
372
442
  validateConcurrency(concurrency);
373
443
  const permissionMode = fileCfg?.permissionMode ?? "auto";
@@ -376,8 +446,11 @@ async function main() {
376
446
  validateGitRepo(cwd);
377
447
  const mergeStrategy = fileCfg?.mergeStrategy ?? "yolo";
378
448
  if (nonInteractive) {
379
- console.log(chalk.dim(` ${model} concurrency=${concurrency} worktrees=${useWorktrees} merge=${mergeStrategy} perms=${permissionMode}`));
449
+ const capStr = usageCap != null ? ` cap=${Math.round(usageCap * 100)}%` : "";
450
+ console.log(chalk.dim(` ${workerModel} concurrency=${concurrency} worktrees=${useWorktrees} merge=${mergeStrategy} perms=${permissionMode}${capStr}`));
380
451
  }
452
+ // ── Flex mode: adaptive multi-wave planning ──
453
+ const flex = !argv.includes("--no-flex") && (fileCfg?.flexiblePlan ?? objective != null) && objective != null && (budget ?? 10) > 2;
381
454
  // ── Plan phase (interactive: review loop, non-interactive: auto-plan or skip) ──
382
455
  const needsPlan = tasks.length === 0;
383
456
  if (needsPlan) {
@@ -385,17 +458,23 @@ async function main() {
385
458
  console.error(chalk.red(" No tasks provided and stdin is not a TTY. Provide tasks via args or a .json file."));
386
459
  process.exit(1);
387
460
  }
461
+ // In flex mode, plan ~50% of budget for wave 1, leaving room for steering
462
+ const waveBudget = flex ? Math.max(concurrency, Math.ceil((budget ?? 10) * 0.5)) : budget;
463
+ const flexNote = flex
464
+ ? `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.`
465
+ : undefined;
388
466
  process.stdout.write("\x1B[?25l");
389
- const restore = () => process.stdout.write("\x1B[?25h");
390
- console.log(chalk.magenta("\n Planning...\n"));
467
+ const planRestore = () => process.stdout.write("\x1B[?25h");
468
+ console.log(chalk.magenta(`\n Planning${flex ? " wave 1" : ""}...\n`));
391
469
  try {
392
- tasks = await planTasks(objective, cwd, model, permissionMode, budget, concurrency, (text) => {
470
+ tasks = await planTasks(objective, cwd, plannerModel, workerModel, permissionMode, waveBudget, concurrency, (text) => {
393
471
  process.stdout.write(`\x1B[2K\r ${chalk.dim(text)}`);
394
- });
395
- process.stdout.write(`\x1B[2K\r ${chalk.green(`${tasks.length} tasks`)}\n\n`);
472
+ }, flexNote);
473
+ const flexHint = flex ? chalk.dim(` (wave 1, ${(budget ?? 10) - tasks.length} remaining)`) : "";
474
+ process.stdout.write(`\x1B[2K\r ${chalk.green(`${tasks.length} tasks`)}${flexHint}\n\n`);
396
475
  }
397
476
  catch (err) {
398
- restore();
477
+ planRestore();
399
478
  if (isAuthError(err))
400
479
  console.error(chalk.red(`\n Authentication failed — check your API key or run: claude auth\n`));
401
480
  else
@@ -403,7 +482,7 @@ async function main() {
403
482
  process.exit(1);
404
483
  }
405
484
  // ── Review loop ──
406
- restore();
485
+ planRestore();
407
486
  let reviewing = true;
408
487
  while (reviewing) {
409
488
  showPlan(tasks);
@@ -424,7 +503,7 @@ async function main() {
424
503
  console.log(chalk.magenta("\n Re-planning...\n"));
425
504
  process.stdout.write("\x1B[?25l");
426
505
  try {
427
- tasks = await refinePlan(objective, tasks, feedback, cwd, model, permissionMode, budget, concurrency, (text) => {
506
+ tasks = await refinePlan(objective, tasks, feedback, cwd, plannerModel, workerModel, permissionMode, budget, concurrency, (text) => {
428
507
  process.stdout.write(`\x1B[2K\r ${chalk.dim(text)}`);
429
508
  });
430
509
  process.stdout.write(`\x1B[2K\r ${chalk.green(`${tasks.length} tasks`)}\n\n`);
@@ -432,7 +511,7 @@ async function main() {
432
511
  catch (err) {
433
512
  console.error(chalk.red(`\n Re-planning failed: ${err.message}\n`));
434
513
  }
435
- restore();
514
+ planRestore();
436
515
  break;
437
516
  }
438
517
  case "c": {
@@ -444,17 +523,17 @@ async function main() {
444
523
  let answer = "";
445
524
  for await (const msg of query({
446
525
  prompt: `You planned these tasks for the objective "${objective}":\n${tasks.map((t, i) => `${i + 1}. ${t.prompt}`).join("\n")}\n\nUser question: ${question}`,
447
- options: { cwd, model, permissionMode, persistSession: false },
526
+ options: { cwd, model: plannerModel, permissionMode, persistSession: false },
448
527
  })) {
449
528
  if (msg.type === "result" && msg.subtype === "success")
450
529
  answer = msg.result || "";
451
530
  }
452
- restore();
531
+ planRestore();
453
532
  if (answer)
454
533
  console.log(chalk.dim(`\n ${answer.slice(0, 500)}\n`));
455
534
  }
456
535
  catch {
457
- restore();
536
+ planRestore();
458
537
  }
459
538
  break;
460
539
  }
@@ -473,52 +552,141 @@ async function main() {
473
552
  showPlan(tasks);
474
553
  process.exit(0);
475
554
  }
476
- // ── Run ──
555
+ // ── Run (wave loop) ──
477
556
  process.stdout.write("\x1B[?25l");
478
557
  const restore = () => process.stdout.write("\x1B[?25h\n");
479
558
  const agentTimeoutMs = cliFlags.timeout ? parseFloat(cliFlags.timeout) * 1000 : undefined;
480
- const swarm = new Swarm({
481
- tasks, concurrency, cwd, model, permissionMode, allowedTools,
482
- useWorktrees, mergeStrategy, agentTimeoutMs,
483
- });
559
+ const runStartedAt = Date.now();
560
+ // Wave-loop state
561
+ let currentSwarm;
562
+ let remaining = budget ?? tasks.length;
563
+ let currentTasks = tasks;
564
+ let waveNum = 0;
565
+ const waveHistory = [];
566
+ let accCost = 0, accIn = 0, accOut = 0, accCompleted = 0, accFailed = 0, accTools = 0;
567
+ let lastCapped = false, lastAborted = false;
568
+ // For flex + branch strategy: create one target branch, waves merge via yolo into it
569
+ let runBranch;
570
+ let originalRef;
571
+ if (flex && mergeStrategy === "branch" && useWorktrees) {
572
+ try {
573
+ originalRef = execSync("git rev-parse --abbrev-ref HEAD", { cwd, encoding: "utf-8", stdio: "pipe" }).trim();
574
+ if (originalRef === "HEAD")
575
+ originalRef = execSync("git rev-parse HEAD", { cwd, encoding: "utf-8", stdio: "pipe" }).trim();
576
+ const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
577
+ runBranch = `swarm/run-${ts}`;
578
+ execSync(`git checkout -b "${runBranch}"`, { cwd, encoding: "utf-8", stdio: "pipe" });
579
+ console.log(chalk.dim(` Branch: ${runBranch}\n`));
580
+ }
581
+ catch { }
582
+ }
583
+ const waveMerge = (flex && runBranch) ? "yolo" : mergeStrategy;
484
584
  // Graceful drain
485
585
  let stopping = false;
486
586
  const gracefulStop = (signal) => {
487
587
  if (stopping) {
488
- swarm.cleanup();
588
+ currentSwarm?.cleanup();
489
589
  restore();
490
590
  process.exit(0);
491
591
  }
492
592
  stopping = true;
493
- process.stdout.write(`\n ${chalk.yellow(`${signal}: stopping... ${swarm.active} active (send again to force)`)}\n`);
494
- swarm.abort();
593
+ process.stdout.write(`\n ${chalk.yellow(`${signal}: stopping... (send again to force)`)}\n`);
594
+ currentSwarm?.abort();
495
595
  };
496
596
  process.on("SIGINT", () => gracefulStop("SIGINT"));
497
597
  process.on("SIGTERM", () => gracefulStop("SIGTERM"));
498
- process.on("uncaughtException", (err) => { swarm.abort(); swarm.cleanup(); restore(); console.error(chalk.red(`\n Uncaught: ${err.message}`)); process.exit(1); });
499
- process.on("unhandledRejection", (reason) => { swarm.abort(); swarm.cleanup(); restore(); console.error(chalk.red(`\n Unhandled: ${reason instanceof Error ? reason.message : reason}`)); process.exit(1); });
500
- const stopRender = startRenderLoop(swarm);
501
- try {
502
- await swarm.run();
503
- }
504
- catch (err) {
505
- if (isAuthError(err)) {
598
+ process.on("uncaughtException", (err) => { currentSwarm?.abort(); currentSwarm?.cleanup(); restore(); console.error(chalk.red(`\n Uncaught: ${err.message}`)); process.exit(1); });
599
+ process.on("unhandledRejection", (reason) => { currentSwarm?.abort(); currentSwarm?.cleanup(); restore(); console.error(chalk.red(`\n Unhandled: ${reason instanceof Error ? reason.message : reason}`)); process.exit(1); });
600
+ while (remaining > 0 && currentTasks.length > 0 && !stopping) {
601
+ if (currentTasks.length > remaining)
602
+ currentTasks = currentTasks.slice(0, remaining);
603
+ if (flex) {
604
+ console.log(chalk.magenta(`\n \u2500\u2500 Wave ${waveNum + 1} (${currentTasks.length} tasks, ${remaining} remaining) \u2500\u2500\n`));
605
+ }
606
+ const swarm = new Swarm({
607
+ tasks: currentTasks, concurrency, cwd, model: workerModel, permissionMode, allowedTools,
608
+ useWorktrees, mergeStrategy: waveMerge, agentTimeoutMs, usageCap,
609
+ });
610
+ currentSwarm = swarm;
611
+ const stopRender = startRenderLoop(swarm);
612
+ try {
613
+ await swarm.run();
614
+ }
615
+ catch (err) {
616
+ if (isAuthError(err)) {
617
+ stopRender();
618
+ restore();
619
+ console.error(chalk.red(`\n Authentication failed — check your API key or run: claude auth\n`));
620
+ process.exit(1);
621
+ }
622
+ throw err;
623
+ }
624
+ finally {
506
625
  stopRender();
507
- restore();
508
- console.error(chalk.red(`\n Authentication failed — check your API key or run: claude auth\n`));
509
- process.exit(1);
626
+ console.log(renderSummary(swarm));
627
+ }
628
+ // Accumulate
629
+ accCost += swarm.totalCostUsd;
630
+ accIn += swarm.totalInputTokens;
631
+ accOut += swarm.totalOutputTokens;
632
+ accCompleted += swarm.completed;
633
+ accFailed += swarm.failed;
634
+ accTools += swarm.agents.reduce((sum, a) => sum + a.toolCalls, 0);
635
+ remaining -= swarm.completed + swarm.failed;
636
+ lastCapped = swarm.cappedOut;
637
+ lastAborted = swarm.aborted;
638
+ waveHistory.push({
639
+ wave: waveNum,
640
+ tasks: swarm.agents.map(a => ({
641
+ prompt: a.task.prompt,
642
+ status: a.status,
643
+ filesChanged: a.filesChanged,
644
+ error: a.error,
645
+ })),
646
+ });
647
+ if (!flex || remaining <= 0 || swarm.aborted || swarm.cappedOut)
648
+ break;
649
+ // ── Steer next wave ──
650
+ console.log(chalk.magenta("\n Steering...\n"));
651
+ process.stdout.write("\x1B[?25l");
652
+ try {
653
+ const steer = await steerWave(objective, waveHistory, remaining, cwd, plannerModel, workerModel, permissionMode, concurrency, (text) => {
654
+ process.stdout.write(`\x1B[2K\r ${chalk.dim(text)}`);
655
+ });
656
+ process.stdout.write(`\x1B[2K\r`);
657
+ process.stdout.write("\x1B[?25h");
658
+ if (steer.done) {
659
+ console.log(chalk.green(` \u2713 ${steer.reasoning}\n`));
660
+ break;
661
+ }
662
+ console.log(chalk.dim(` ${steer.reasoning}\n`));
663
+ currentTasks = steer.tasks;
664
+ waveNum++;
665
+ }
666
+ catch (err) {
667
+ process.stdout.write("\x1B[?25h");
668
+ console.log(chalk.yellow(` Steering failed: ${err.message?.slice(0, 80)} \u2014 stopping\n`));
669
+ break;
670
+ }
671
+ }
672
+ // Switch back if we created a run branch
673
+ if (runBranch && originalRef) {
674
+ try {
675
+ execSync(`git checkout "${originalRef}"`, { cwd, encoding: "utf-8", stdio: "pipe" });
510
676
  }
511
- throw err;
512
- }
513
- finally {
514
- stopRender();
515
- console.log(renderSummary(swarm));
516
- const failedAgents = swarm.agents.filter((a) => a.status === "error");
517
- const summary = failedAgents.length > 0
518
- ? chalk.yellow(`${swarm.completed} done, ${failedAgents.length} failed`)
519
- : chalk.green(`${swarm.completed} done`);
520
- const cost = swarm.totalCostUsd > 0 ? ` ($${swarm.totalCostUsd.toFixed(3)})` : "";
521
- console.log(`\n ${chalk.bold("Complete:")} ${summary}${chalk.dim(cost)}`);
677
+ catch { }
678
+ }
679
+ // ── Final summary ──
680
+ const waves = waveNum + 1;
681
+ const cappedNote = lastCapped ? chalk.yellow(` (capped at ${usageCap != null ? Math.round(usageCap * 100) : 100}%)`) : "";
682
+ const summaryText = accFailed > 0
683
+ ? chalk.yellow(`${accCompleted} done, ${accFailed} failed`) + cappedNote
684
+ : chalk.green(`${accCompleted} done`) + cappedNote;
685
+ const costText = accCost > 0 ? ` ($${accCost.toFixed(3)})` : "";
686
+ const wavePart = waves > 1 ? `${waves} waves, ` : "";
687
+ console.log(`\n ${chalk.bold("Complete:")} ${wavePart}${summaryText}${chalk.dim(costText)}`);
688
+ if (accFailed > 0 && waves === 1) {
689
+ const failedAgents = currentSwarm?.agents.filter((a) => a.status === "error") ?? [];
522
690
  if (failedAgents.length > 0) {
523
691
  console.log(chalk.red(`\n Failed agents:`));
524
692
  for (const a of failedAgents) {
@@ -526,38 +694,39 @@ async function main() {
526
694
  console.log(chalk.dim(` ${a.error || "unknown error"}`));
527
695
  }
528
696
  }
529
- const elapsed = Math.round((Date.now() - swarm.startedAt) / 1000);
530
- const elapsedStr = elapsed < 60 ? `${elapsed}s` : `${Math.floor(elapsed / 60)}m ${elapsed % 60}s`;
531
- const tools = swarm.agents.reduce((sum, a) => sum + a.toolCalls, 0);
532
- console.log(chalk.dim(` ${elapsedStr} ${fmtTokens(swarm.totalInputTokens)} in / ${fmtTokens(swarm.totalOutputTokens)} out ${tools} tool calls`));
533
- if (swarm.mergeResults.length > 0) {
534
- const merged = swarm.mergeResults.filter((r) => r.ok);
535
- const autoResolved = merged.filter((r) => r.autoResolved).length;
536
- const conflicts = swarm.mergeResults.filter((r) => !r.ok);
537
- const target = swarm.mergeBranch || "HEAD";
538
- if (merged.length > 0) {
539
- const extra = autoResolved > 0 ? chalk.yellow(` (${autoResolved} auto-resolved)`) : "";
540
- console.log(chalk.green(` Merged ${merged.length} branch(es) into ${target}`) + extra);
541
- }
542
- if (swarm.mergeBranch)
543
- console.log(chalk.dim(` Branch: ${swarm.mergeBranch} create a PR or: git merge ${swarm.mergeBranch}`));
544
- if (conflicts.length > 0) {
545
- console.log(chalk.red(` ${conflicts.length} unresolved conflict(s):`));
546
- for (const c of conflicts)
547
- console.log(chalk.red(` ${c.branch}`));
548
- console.log(chalk.dim(" Merge manually: git merge <branch>"));
549
- }
697
+ }
698
+ const elapsed = Math.round((Date.now() - runStartedAt) / 1000);
699
+ const elapsedStr = elapsed < 60 ? `${elapsed}s` : `${Math.floor(elapsed / 60)}m ${elapsed % 60}s`;
700
+ console.log(chalk.dim(` ${elapsedStr} ${fmtTokens(accIn)} in / ${fmtTokens(accOut)} out ${accTools} tool calls`));
701
+ if (runBranch) {
702
+ console.log(chalk.dim(` Branch: ${runBranch} \u2014 create a PR or: git merge ${runBranch}`));
703
+ }
704
+ else if (currentSwarm?.mergeResults && currentSwarm.mergeResults.length > 0) {
705
+ const merged = currentSwarm.mergeResults.filter((r) => r.ok);
706
+ const autoResolved = merged.filter((r) => r.autoResolved).length;
707
+ const conflicts = currentSwarm.mergeResults.filter((r) => !r.ok);
708
+ const target = currentSwarm.mergeBranch || "HEAD";
709
+ if (merged.length > 0) {
710
+ const extra = autoResolved > 0 ? chalk.yellow(` (${autoResolved} auto-resolved)`) : "";
711
+ console.log(chalk.green(` Merged ${merged.length} branch(es) into ${target}`) + extra);
712
+ }
713
+ if (currentSwarm.mergeBranch)
714
+ console.log(chalk.dim(` Branch: ${currentSwarm.mergeBranch} \u2014 create a PR or: git merge ${currentSwarm.mergeBranch}`));
715
+ if (conflicts.length > 0) {
716
+ console.log(chalk.red(` ${conflicts.length} unresolved conflict(s):`));
717
+ for (const c of conflicts)
718
+ console.log(chalk.red(` ${c.branch}`));
719
+ console.log(chalk.dim(" Merge manually: git merge <branch>"));
550
720
  }
551
- if (swarm.logFile)
552
- console.log(chalk.dim(` Log: ${swarm.logFile}`));
553
- console.log("");
554
721
  }
555
- if (swarm.aborted || swarm.completed === 0)
556
- process.exit(2);
557
- if (swarm.failed > 0)
722
+ if (currentSwarm?.logFile)
723
+ console.log(chalk.dim(` Log: ${currentSwarm.logFile}`));
724
+ console.log("");
725
+ if (accFailed > 0)
558
726
  process.exit(1);
727
+ if (lastAborted || accCompleted === 0)
728
+ process.exit(2);
559
729
  }
560
- function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
561
730
  function fmtTokens(n) {
562
731
  if (n >= 1_000_000)
563
732
  return `${(n / 1_000_000).toFixed(1)}M`;
package/dist/planner.d.ts CHANGED
@@ -1,3 +1,20 @@
1
1
  import type { Task, PermMode } from "./types.js";
2
- export declare function planTasks(objective: string, cwd: string, model: string, permissionMode: PermMode, budget: number | undefined, concurrency: number, onLog: (text: string) => void): Promise<Task[]>;
3
- export declare function refinePlan(objective: string, previousTasks: Task[], feedback: string, cwd: string, model: string, permissionMode: PermMode, budget: number | undefined, concurrency: number, onLog: (text: string) => void): Promise<Task[]>;
2
+ export interface WaveSummary {
3
+ wave: number;
4
+ tasks: {
5
+ prompt: string;
6
+ status: string;
7
+ filesChanged?: number;
8
+ error?: string;
9
+ }[];
10
+ }
11
+ export interface SteerResult {
12
+ done: boolean;
13
+ tasks: Task[];
14
+ reasoning: string;
15
+ }
16
+ export type ModelTier = "opus" | "sonnet" | "haiku" | "unknown";
17
+ export declare function detectModelTier(model: string): ModelTier;
18
+ 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[]>;
19
+ 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[]>;
20
+ export declare function steerWave(objective: string, history: WaveSummary[], remainingBudget: number, cwd: string, plannerModel: string, workerModel: string, permissionMode: PermMode, concurrency: number, onLog: (text: string) => void): Promise<SteerResult>;