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/README.md +97 -29
- package/dist/index.js +254 -85
- package/dist/planner.d.ts +19 -2
- package/dist/planner.js +284 -75
- package/dist/swarm.d.ts +7 -1
- package/dist/swarm.js +62 -21
- package/dist/types.d.ts +6 -0
- package/dist/ui.js +38 -15
- package/package.json +1 -1
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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("
|
|
358
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
390
|
-
console.log(chalk.magenta(
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
531
|
+
planRestore();
|
|
453
532
|
if (answer)
|
|
454
533
|
console.log(chalk.dim(`\n ${answer.slice(0, 500)}\n`));
|
|
455
534
|
}
|
|
456
535
|
catch {
|
|
457
|
-
|
|
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
|
|
481
|
-
|
|
482
|
-
|
|
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
|
-
|
|
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...
|
|
494
|
-
|
|
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) => {
|
|
499
|
-
process.on("unhandledRejection", (reason) => {
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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
|
-
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
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
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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 (
|
|
556
|
-
|
|
557
|
-
|
|
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
|
|
3
|
-
|
|
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>;
|