claude-overnight 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Francesco Fornace
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,87 @@
1
+ # claude-overnight
2
+
3
+ Run parallel Claude Code agents with a real-time terminal UI.
4
+
5
+ Give it an objective and it plans, executes, and merges the results — or feed it explicit tasks. Each agent gets full Claude Code tooling (Read, Edit, Bash, etc.) and optionally runs in an isolated git worktree. A live TUI shows progress, cost, rate limits, and per-agent status.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install -g claude-overnight
11
+ ```
12
+
13
+ Requires Node.js >= 20 and a valid Claude authentication (OAuth via `claude` CLI login, or `ANTHROPIC_API_KEY` env var).
14
+
15
+ ## Quick start
16
+
17
+ ### Interactive (planner mode)
18
+
19
+ ```bash
20
+ claude-overnight
21
+ ```
22
+
23
+ Prompts for model, concurrency, and permission mode, then asks for an objective. A planner agent analyzes your codebase and breaks it into parallel tasks.
24
+
25
+ ### Task file
26
+
27
+ ```bash
28
+ claude-overnight tasks.json
29
+ ```
30
+
31
+ ### Inline tasks
32
+
33
+ ```bash
34
+ claude-overnight "fix auth bug in src/auth.ts" "add tests for user model"
35
+ ```
36
+
37
+ Each quoted argument becomes one parallel task.
38
+
39
+ ## Task file format
40
+
41
+ A JSON file with a `tasks` array and optional configuration:
42
+
43
+ ```json
44
+ {
45
+ "tasks": [
46
+ "Add input validation to all API routes",
47
+ { "prompt": "Refactor database queries", "cwd": "./packages/api" }
48
+ ],
49
+ "model": "claude-sonnet-4-6",
50
+ "concurrency": 4,
51
+ "worktrees": true
52
+ }
53
+ ```
54
+
55
+ A plain JSON array of strings also works: `["task one", "task two"]`.
56
+
57
+ | Field | Type | Default | Description |
58
+ |---|---|---|---|
59
+ | `tasks` | `(string \| {prompt, cwd?, model?})[]` | required | Tasks to run in parallel |
60
+ | `model` | `string` | prompted | Model for all agents (per-task overridable) |
61
+ | `concurrency` | `number` | `5` | Max agents running simultaneously |
62
+ | `worktrees` | `boolean` | prompted | Isolate each agent in a git worktree |
63
+ | `permissionMode` | `"auto" \| "bypassPermissions" \| "default"` | `"auto"` | How agents handle dangerous operations |
64
+ | `cwd` | `string` | `process.cwd()` | Working directory for all agents |
65
+ | `allowedTools` | `string[]` | all | Restrict which tools agents can use |
66
+ | `mergeStrategy` | `"yolo" \| "branch"` | `"yolo"` | Merge into HEAD or into a new branch |
67
+
68
+ ## CLI flags
69
+
70
+ | Flag | Default | Description |
71
+ |---|---|---|
72
+ | `--concurrency=N` | `5` | Max parallel agents (overrides task file) |
73
+ | `--model=NAME` | — | Model override for all agents |
74
+ | `--timeout=SECONDS` | `300` | Agent inactivity timeout (kills only silent agents) |
75
+ | `--dry-run` | — | Show planned tasks without executing them |
76
+
77
+ ## Worktrees and merging
78
+
79
+ When worktrees are enabled, each agent runs in an isolated git worktree on a `swarm/task-N` branch. Changes are auto-committed when the agent finishes. After all agents complete, branches are merged back sequentially. The default `"yolo"` strategy merges directly into your current branch; `"branch"` creates a new `swarm/run-{timestamp}` branch instead. If a merge conflicts, the swarm retries with `-X theirs`; if that still fails, the branch is preserved for manual resolution. Stale worktrees and orphaned `swarm/*` branches from previous runs are cleaned up automatically on startup.
80
+
81
+ ## Exit codes
82
+
83
+ | Code | Meaning |
84
+ |---|---|
85
+ | `0` | All tasks succeeded |
86
+ | `1` | Some tasks failed |
87
+ | `2` | All tasks failed or no tasks completed |
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,555 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, existsSync } from "fs";
3
+ import { resolve, dirname, join } from "path";
4
+ import { fileURLToPath } from "url";
5
+ import { execSync } from "child_process";
6
+ import { createInterface } from "readline";
7
+ import chalk from "chalk";
8
+ import { query } from "@anthropic-ai/claude-agent-sdk";
9
+ import { Swarm } from "./swarm.js";
10
+ import { planTasks } from "./planner.js";
11
+ import { startRenderLoop, renderSummary } from "./ui.js";
12
+ // ── CLI flag parsing ──
13
+ function parseCliFlags(argv) {
14
+ const known = new Set(["concurrency", "model", "timeout"]);
15
+ const booleans = new Set(["--dry-run", "-h", "--help", "-v", "--version"]);
16
+ const flags = {};
17
+ const positional = [];
18
+ for (let i = 0; i < argv.length; i++) {
19
+ const arg = argv[i];
20
+ if (booleans.has(arg))
21
+ continue;
22
+ // --key=value
23
+ const eq = arg.match(/^--(\w[\w-]*)=(.+)$/);
24
+ if (eq && known.has(eq[1])) {
25
+ flags[eq[1]] = eq[2];
26
+ continue;
27
+ }
28
+ // --key value
29
+ const bare = arg.match(/^--(\w[\w-]*)$/);
30
+ if (bare && known.has(bare[1]) && i + 1 < argv.length && !argv[i + 1].startsWith("-")) {
31
+ flags[bare[1]] = argv[++i];
32
+ continue;
33
+ }
34
+ if (!arg.startsWith("--"))
35
+ positional.push(arg);
36
+ }
37
+ return { flags, positional };
38
+ }
39
+ // ── Auth error detection ──
40
+ const AUTH_PATTERNS = ["unauthorized", "forbidden", "invalid_api_key", "authentication"];
41
+ function isAuthError(err) {
42
+ const msg = err instanceof Error ? err.message : String(err);
43
+ const lower = msg.toLowerCase();
44
+ return AUTH_PATTERNS.some((p) => lower.includes(p));
45
+ }
46
+ function authErrorMessage() {
47
+ return "Authentication failed — check your API key or run: claude auth";
48
+ }
49
+ // ── Fetch models via SDK (works with OAuth / Max / API key) ──
50
+ async function fetchModels(timeoutMs = 10_000) {
51
+ let q;
52
+ try {
53
+ q = query({ prompt: "", options: { persistSession: false } });
54
+ const models = await Promise.race([
55
+ q.supportedModels(),
56
+ sleep(timeoutMs).then(() => { throw new Error("model_fetch_timeout"); }),
57
+ ]);
58
+ q.close();
59
+ return models;
60
+ }
61
+ catch (err) {
62
+ q?.close();
63
+ if (err.message === "model_fetch_timeout") {
64
+ console.warn(chalk.yellow("\n ⚠ Model fetch timed out — continuing with defaults"));
65
+ }
66
+ return [];
67
+ }
68
+ }
69
+ // ── Interactive prompts ──
70
+ function ask(question) {
71
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
72
+ return new Promise((res) => {
73
+ rl.question(question, (answer) => {
74
+ rl.close();
75
+ res(answer.trim());
76
+ });
77
+ });
78
+ }
79
+ async function pickModel(models) {
80
+ if (models.length === 0) {
81
+ console.log(chalk.yellow(" Could not fetch models. Enter model ID manually."));
82
+ const ans = await ask(chalk.dim(" Model: "));
83
+ return ans || "claude-sonnet-4-6";
84
+ }
85
+ console.log(chalk.bold("\n Model:"));
86
+ for (let i = 0; i < models.length; i++) {
87
+ const marker = i === 0 ? chalk.green("→") : " ";
88
+ const name = models[i].displayName;
89
+ const desc = models[i].description ? chalk.dim(` — ${models[i].description}`) : "";
90
+ const label = i === 0 ? chalk.green(name) + desc : chalk.dim(name) + desc;
91
+ console.log(` ${marker} ${i + 1}. ${label}`);
92
+ }
93
+ const ans = await ask(chalk.dim(` Choose [1]: `));
94
+ const idx = ans ? parseInt(ans) - 1 : 0;
95
+ const pick = models[idx] ?? models[0];
96
+ console.log(chalk.dim(` Using ${pick.displayName}`));
97
+ return pick.value;
98
+ }
99
+ async function pickConcurrency() {
100
+ const ans = await ask(chalk.dim(" Concurrency [5]: "));
101
+ return parseInt(ans) || 5;
102
+ }
103
+ async function pickWorktrees() {
104
+ const ans = await ask(chalk.dim(" Use git worktrees? [Y/n]: "));
105
+ return ans.toLowerCase() !== "n";
106
+ }
107
+ async function pickMergeStrategy() {
108
+ console.log(chalk.bold("\n Merge strategy:"));
109
+ console.log(` ${chalk.green("→")} 1. ${chalk.green("YOLO")}${chalk.dim(" — merge into current branch")}`);
110
+ console.log(` 2. ${chalk.dim("New branch")}${chalk.dim(" — merge into a new branch (safe for PRs)")}`);
111
+ const ans = await ask(chalk.dim(" Choose [1]: "));
112
+ const pick = ans === "2" ? "branch" : "yolo";
113
+ console.log(chalk.dim(` Using ${pick === "yolo" ? "YOLO (merge into current)" : "new branch"}`));
114
+ return pick;
115
+ }
116
+ const PERM_MODES = [
117
+ { label: "Auto", value: "auto", desc: "AI decides what's safe" },
118
+ { label: "Bypass permissions", value: "bypassPermissions", desc: "skip all prompts (dangerous)" },
119
+ { label: "Default", value: "default", desc: "prompt for dangerous ops" },
120
+ ];
121
+ async function pickPermissionMode() {
122
+ console.log(chalk.bold("\n Permission mode:"));
123
+ for (let i = 0; i < PERM_MODES.length; i++) {
124
+ const marker = i === 0 ? chalk.green("→") : " ";
125
+ const name = i === 0 ? chalk.green(PERM_MODES[i].label) : chalk.dim(PERM_MODES[i].label);
126
+ const desc = chalk.dim(` — ${PERM_MODES[i].desc}`);
127
+ console.log(` ${marker} ${i + 1}. ${name}${desc}`);
128
+ }
129
+ const ans = await ask(chalk.dim(" Choose [1]: "));
130
+ const idx = ans ? parseInt(ans) - 1 : 0;
131
+ const pick = PERM_MODES[idx] ?? PERM_MODES[0];
132
+ console.log(chalk.dim(` Using ${pick.label}`));
133
+ return pick.value;
134
+ }
135
+ async function pickObjective() {
136
+ console.log("");
137
+ while (true) {
138
+ const ans = await ask(chalk.bold(" What should the swarm do?\n > "));
139
+ if (!ans)
140
+ return ans;
141
+ if (ans.split(/\s+/).length < 5) {
142
+ console.log(chalk.yellow(' Tip: be specific about what you want, e.g. "refactor auth module and add tests"'));
143
+ continue;
144
+ }
145
+ return ans;
146
+ }
147
+ }
148
+ const KNOWN_TASK_FILE_KEYS = new Set([
149
+ "tasks", "concurrency", "cwd", "model", "permissionMode", "allowedTools", "worktrees", "mergeStrategy",
150
+ ]);
151
+ function loadTaskFile(file) {
152
+ const path = resolve(file);
153
+ let raw;
154
+ try {
155
+ raw = readFileSync(path, "utf-8");
156
+ }
157
+ catch {
158
+ throw new Error(`Cannot read task file: ${path}`);
159
+ }
160
+ let json;
161
+ try {
162
+ json = JSON.parse(raw);
163
+ }
164
+ catch {
165
+ throw new Error(`Task file is not valid JSON: ${path}`);
166
+ }
167
+ const parsed = Array.isArray(json)
168
+ ? { tasks: json }
169
+ : json;
170
+ // Reject unknown top-level keys
171
+ if (!Array.isArray(json) && typeof json === "object" && json !== null) {
172
+ const unknown = Object.keys(json).filter((k) => !KNOWN_TASK_FILE_KEYS.has(k));
173
+ if (unknown.length > 0) {
174
+ throw new Error(`Unknown key${unknown.length > 1 ? "s" : ""} in task file: ${unknown.join(", ")}. ` +
175
+ `Allowed keys: ${[...KNOWN_TASK_FILE_KEYS].join(", ")}`);
176
+ }
177
+ }
178
+ // Validate tasks array
179
+ if (!Array.isArray(parsed.tasks)) {
180
+ throw new Error(`Task file must contain a "tasks" array (got ${typeof parsed.tasks})`);
181
+ }
182
+ const tasks = [];
183
+ for (let i = 0; i < parsed.tasks.length; i++) {
184
+ const t = parsed.tasks[i];
185
+ const id = String(tasks.length);
186
+ if (typeof t === "string") {
187
+ if (!t.trim())
188
+ throw new Error(`Task ${i} is an empty string`);
189
+ tasks.push({ id, prompt: t });
190
+ }
191
+ else if (typeof t === "object" && t !== null) {
192
+ if (typeof t.prompt !== "string" || !t.prompt.trim()) {
193
+ throw new Error(`Task ${i} is missing a "prompt" string`);
194
+ }
195
+ tasks.push({ id, prompt: t.prompt, cwd: t.cwd ? resolve(t.cwd) : undefined, model: t.model });
196
+ }
197
+ else {
198
+ throw new Error(`Task ${i} must be a string or object with a "prompt" field (got ${typeof t})`);
199
+ }
200
+ }
201
+ // Validate concurrency if present
202
+ if (parsed.concurrency !== undefined) {
203
+ validateConcurrency(parsed.concurrency);
204
+ }
205
+ return {
206
+ tasks,
207
+ concurrency: parsed.concurrency,
208
+ model: parsed.model,
209
+ cwd: parsed.cwd ? resolve(parsed.cwd) : undefined,
210
+ permissionMode: parsed.permissionMode,
211
+ allowedTools: parsed.allowedTools,
212
+ useWorktrees: parsed.worktrees,
213
+ mergeStrategy: parsed.mergeStrategy,
214
+ };
215
+ }
216
+ // ── Validation helpers ──
217
+ function validateConcurrency(value) {
218
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
219
+ throw new Error(`Concurrency must be a positive integer (got ${JSON.stringify(value)})`);
220
+ }
221
+ }
222
+ function validateCwd(cwd) {
223
+ if (!existsSync(cwd)) {
224
+ throw new Error(`Working directory does not exist: ${cwd}`);
225
+ }
226
+ }
227
+ function isGitRepo(cwd) {
228
+ try {
229
+ execSync("git rev-parse --git-dir", { cwd, encoding: "utf-8", stdio: "pipe" });
230
+ return true;
231
+ }
232
+ catch {
233
+ return false;
234
+ }
235
+ }
236
+ function validateGitRepo(cwd) {
237
+ if (!isGitRepo(cwd)) {
238
+ throw new Error(`Worktrees require a git repository, but ${cwd} is not inside one.\n` +
239
+ ` Run this to initialize one:\n\n` +
240
+ ` cd ${cwd} && git init\n\n` +
241
+ ` Or disable worktrees (set "worktrees": false in your task file).`);
242
+ }
243
+ }
244
+ // ── Main ──
245
+ async function main() {
246
+ const argv = process.argv.slice(2);
247
+ if (argv.includes("-v") || argv.includes("--version")) {
248
+ const __dirname = dirname(fileURLToPath(import.meta.url));
249
+ const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
250
+ console.log(`claude-overnight v${pkg.version}`);
251
+ process.exit(0);
252
+ }
253
+ if (argv.includes("-h") || argv.includes("--help")) {
254
+ console.log(`
255
+ ${chalk.bold("claude-overnight")} — fire off Claude agents, come back to shipped work
256
+
257
+ ${chalk.dim("Usage:")}
258
+ claude-overnight ${chalk.dim("interactive — pick model, concurrency, objective")}
259
+ claude-overnight tasks.json ${chalk.dim("run tasks defined in a JSON file")}
260
+ claude-overnight "fix auth" "add tests" ${chalk.dim("run inline tasks in parallel")}
261
+
262
+ ${chalk.dim("Flags:")}
263
+ -h, --help Show this help
264
+ -v, --version Print version
265
+ --dry-run Show planned tasks without running them
266
+ --concurrency=N Max parallel agents ${chalk.dim("(overrides task file & defaults)")}
267
+ --model=NAME Model to use ${chalk.dim("(overrides task file & defaults)")}
268
+ --timeout=SECONDS Agent inactivity timeout ${chalk.dim("(default: 300s, kills only silent agents)")}
269
+
270
+ ${chalk.dim("Permission modes")} ${chalk.dim("(task file: \"permissionMode\", interactive: prompted)")}
271
+ auto AI decides which ops are safe ${chalk.dim("(default)")}
272
+ bypassPermissions Skip all permission prompts ${chalk.yellow("(dangerous)")}
273
+ default Prompt before destructive operations
274
+
275
+ ${chalk.dim("Non-interactive defaults (task file / inline / piped):")}
276
+ model: first available concurrency: 5 worktrees: auto (git repo) perms: auto
277
+ `);
278
+ process.exit(0);
279
+ }
280
+ const dryRun = argv.includes("--dry-run");
281
+ const { flags: cliFlags, positional: args } = parseCliFlags(argv);
282
+ // Validate CLI flag values early
283
+ if (cliFlags.concurrency !== undefined) {
284
+ const n = parseInt(cliFlags.concurrency);
285
+ if (!Number.isInteger(n) || n < 1) {
286
+ console.error(chalk.red(` --concurrency must be a positive integer (got "${cliFlags.concurrency}")`));
287
+ process.exit(1);
288
+ }
289
+ }
290
+ if (cliFlags.timeout !== undefined) {
291
+ const n = parseFloat(cliFlags.timeout);
292
+ if (isNaN(n) || n <= 0) {
293
+ console.error(chalk.red(` --timeout must be a positive number (got "${cliFlags.timeout}")`));
294
+ process.exit(1);
295
+ }
296
+ }
297
+ // ── Load tasks from file or inline args ──
298
+ let tasks = [];
299
+ let fileCfg;
300
+ const jsonFiles = args.filter((a) => a.endsWith(".json"));
301
+ if (jsonFiles.length > 1) {
302
+ console.error(chalk.red(` Multiple task files provided: ${jsonFiles.join(", ")}`));
303
+ console.error(chalk.red(` Only one .json task file is supported at a time.`));
304
+ process.exit(1);
305
+ }
306
+ for (const arg of args) {
307
+ if (arg.endsWith(".json")) {
308
+ fileCfg = loadTaskFile(arg);
309
+ tasks = fileCfg.tasks;
310
+ }
311
+ else if (!arg.startsWith("-") && existsSync(resolve(arg))) {
312
+ console.error(chalk.red(` "${arg}" looks like a file path but doesn't end in .json.`));
313
+ console.error(chalk.red(` To use it as a task file, rename it: mv ${arg} ${arg.replace(/(\.\w+)?$/, ".json")}`));
314
+ console.error(chalk.red(` To run it as an inline task prompt, ignore this message and quote the string.`));
315
+ process.exit(1);
316
+ }
317
+ else {
318
+ tasks.push({ id: String(tasks.length), prompt: arg });
319
+ }
320
+ }
321
+ // ── Config: defaults for non-interactive, prompts for interactive ──
322
+ console.log(chalk.bold("\n 🌙 claude-overnight\n"));
323
+ const noTTY = !process.stdin.isTTY;
324
+ const nonInteractive = noTTY || fileCfg !== undefined || tasks.length > 0;
325
+ const cwd = fileCfg?.cwd ?? process.cwd();
326
+ const allowedTools = fileCfg?.allowedTools;
327
+ validateCwd(cwd);
328
+ if (!nonInteractive) {
329
+ console.log(chalk.dim(" Run parallel Claude Code agents with real-time UI.\n"));
330
+ console.log(chalk.dim(" • Interactive — describe an objective, auto-plan into tasks"));
331
+ console.log(chalk.dim(" • Task file — claude-overnight tasks.json"));
332
+ console.log(chalk.dim(" • Inline args — claude-overnight \"fix auth\" \"add tests\""));
333
+ }
334
+ if (noTTY) {
335
+ console.log(chalk.dim(" Non-interactive mode — using defaults"));
336
+ }
337
+ let models = [];
338
+ // Fetch models unless already overridden by CLI/file — non-interactive uses a shorter timeout
339
+ const modelAlreadySet = cliFlags.model || fileCfg?.model;
340
+ if (!modelAlreadySet) {
341
+ if (nonInteractive) {
342
+ models = await fetchModels(5_000);
343
+ }
344
+ else {
345
+ process.stdout.write(chalk.dim(" Fetching available models..."));
346
+ models = await fetchModels();
347
+ process.stdout.write(`\x1B[2K\r`);
348
+ }
349
+ }
350
+ // CLI flags override task file values, which override interactive defaults
351
+ const model = cliFlags.model ?? fileCfg?.model ?? (nonInteractive ? (models[0]?.value || "claude-sonnet-4-6") : await pickModel(models));
352
+ const permissionMode = fileCfg?.permissionMode ?? (nonInteractive ? "auto" : await pickPermissionMode());
353
+ const concurrency = cliFlags.concurrency ? parseInt(cliFlags.concurrency) : (fileCfg?.concurrency ?? (nonInteractive ? 5 : await pickConcurrency()));
354
+ validateConcurrency(concurrency);
355
+ const useWorktrees = fileCfg?.useWorktrees ?? (nonInteractive ? (noTTY ? false : isGitRepo(cwd)) : await pickWorktrees());
356
+ if (useWorktrees)
357
+ validateGitRepo(cwd);
358
+ const mergeStrategy = fileCfg?.mergeStrategy ?? (useWorktrees && !nonInteractive ? await pickMergeStrategy() : "yolo");
359
+ if (nonInteractive) {
360
+ console.log(chalk.dim(` ${model} concurrency=${concurrency} worktrees=${useWorktrees} merge=${mergeStrategy} perms=${permissionMode}`));
361
+ }
362
+ // If no tasks yet, ask for an objective and plan
363
+ let planMode = tasks.length === 0;
364
+ let objective;
365
+ if (planMode) {
366
+ if (noTTY) {
367
+ console.error(chalk.red("\n No tasks provided and stdin is not a TTY. Provide tasks via args or a JSON file."));
368
+ process.exit(1);
369
+ }
370
+ objective = await pickObjective();
371
+ if (!objective) {
372
+ console.error(chalk.red("\n No objective provided."));
373
+ process.exit(1);
374
+ }
375
+ }
376
+ // Hide cursor + graceful shutdown (swarm-aware handler installed after swarm is created)
377
+ process.stdout.write("\x1B[?25l");
378
+ const restore = () => process.stdout.write("\x1B[?25h\n");
379
+ process.on("SIGINT", () => { restore(); process.exit(0); });
380
+ process.on("SIGTERM", () => { restore(); process.exit(0); });
381
+ // ── Plan phase ──
382
+ if (planMode && objective) {
383
+ console.log(chalk.magenta("\n Planning...\n"));
384
+ try {
385
+ tasks = await planTasks(objective, cwd, model, permissionMode, (text) => {
386
+ process.stdout.write(`\x1B[2K\r ${chalk.dim(text)}`);
387
+ });
388
+ process.stdout.write(`\x1B[2K\r ${chalk.green(`Generated ${tasks.length} tasks`)}\n\n`);
389
+ for (const t of tasks) {
390
+ console.log(chalk.dim(` ${t.id}. ${t.prompt.slice(0, 70)}`));
391
+ }
392
+ console.log("");
393
+ process.stdout.write("\x1B[?25h"); // show cursor for prompt
394
+ const confirm = await ask(` Run ${tasks.length} task${tasks.length === 1 ? "" : "s"} with concurrency ${concurrency}? [Y/n] `);
395
+ if (confirm.toLowerCase() === "n") {
396
+ restore();
397
+ console.log(chalk.dim(" Aborted.\n"));
398
+ process.exit(0);
399
+ }
400
+ process.stdout.write("\x1B[?25l"); // re-hide cursor
401
+ }
402
+ catch (err) {
403
+ restore();
404
+ if (isAuthError(err)) {
405
+ console.error(chalk.red(`\n ${authErrorMessage()}\n`));
406
+ }
407
+ else {
408
+ console.error(chalk.red(`\n Planning failed: ${err.message}\n`));
409
+ }
410
+ process.exit(1);
411
+ }
412
+ }
413
+ if (tasks.length === 0) {
414
+ restore();
415
+ console.error("No tasks provided.");
416
+ process.exit(1);
417
+ }
418
+ if (dryRun) {
419
+ restore();
420
+ console.log(chalk.bold(" Tasks:"));
421
+ for (const t of tasks) {
422
+ console.log(` ${chalk.dim(`${Number(t.id) + 1}.`)} ${t.prompt}`);
423
+ }
424
+ console.log("");
425
+ process.exit(0);
426
+ }
427
+ const agentTimeoutMs = cliFlags.timeout ? parseFloat(cliFlags.timeout) * 1000 : undefined;
428
+ const swarm = new Swarm({
429
+ tasks,
430
+ concurrency,
431
+ cwd,
432
+ model,
433
+ permissionMode,
434
+ allowedTools,
435
+ useWorktrees,
436
+ mergeStrategy,
437
+ agentTimeoutMs,
438
+ });
439
+ // Replace simple handlers with graceful drain: first signal stops queue, second force-exits
440
+ process.removeAllListeners("SIGINT");
441
+ process.removeAllListeners("SIGTERM");
442
+ let stopping = false;
443
+ const gracefulStop = (signal) => {
444
+ if (stopping) {
445
+ swarm.cleanup();
446
+ restore();
447
+ process.exit(0);
448
+ }
449
+ stopping = true;
450
+ process.stdout.write(`\n ${chalk.yellow(`${signal}: stopping... waiting for ${swarm.active} active agent(s) to finish (send again to force)`)}\n`);
451
+ swarm.abort();
452
+ };
453
+ process.on("SIGINT", () => gracefulStop("SIGINT"));
454
+ process.on("SIGTERM", () => gracefulStop("SIGTERM"));
455
+ process.on("uncaughtException", (err) => {
456
+ swarm.abort();
457
+ swarm.cleanup();
458
+ restore();
459
+ console.error(chalk.red(`\n Uncaught exception: ${err.message}`));
460
+ process.exit(1);
461
+ });
462
+ process.on("unhandledRejection", (reason) => {
463
+ swarm.abort();
464
+ swarm.cleanup();
465
+ restore();
466
+ const msg = reason instanceof Error ? reason.message : String(reason);
467
+ console.error(chalk.red(`\n Unhandled rejection: ${msg}`));
468
+ process.exit(1);
469
+ });
470
+ const stopRender = startRenderLoop(swarm);
471
+ try {
472
+ await swarm.run();
473
+ }
474
+ catch (err) {
475
+ if (isAuthError(err)) {
476
+ stopRender();
477
+ restore();
478
+ console.error(chalk.red(`\n ${authErrorMessage()}\n`));
479
+ process.exit(1);
480
+ }
481
+ throw err;
482
+ }
483
+ finally {
484
+ stopRender();
485
+ console.log(renderSummary(swarm));
486
+ const failedAgents = swarm.agents.filter((a) => a.status === "error");
487
+ const summary = failedAgents.length > 0
488
+ ? chalk.yellow(`${swarm.completed} done, ${failedAgents.length} failed`)
489
+ : chalk.green(`${swarm.completed} done`);
490
+ const cost = swarm.totalCostUsd > 0
491
+ ? ` ($${swarm.totalCostUsd.toFixed(3)})`
492
+ : "";
493
+ console.log(`\n ${chalk.bold("Complete:")} ${summary}${chalk.dim(cost)}`);
494
+ if (failedAgents.length > 0) {
495
+ console.log(chalk.red(`\n Failed agents:`));
496
+ for (const a of failedAgents) {
497
+ const prompt = a.task.prompt;
498
+ const label = prompt.slice(0, 60) + (prompt.length > 60 ? "…" : "");
499
+ const reason = a.error || "unknown error";
500
+ console.log(chalk.red(` ✗ Agent ${a.id + 1}: ${label}`));
501
+ console.log(chalk.dim(` ${reason}`));
502
+ }
503
+ }
504
+ const elapsed = Math.round((Date.now() - swarm.startedAt) / 1000);
505
+ const elapsedStr = elapsed < 60 ? `${elapsed}s` : `${Math.floor(elapsed / 60)}m ${elapsed % 60}s`;
506
+ const tools = swarm.agents.reduce((sum, a) => sum + a.toolCalls, 0);
507
+ console.log(chalk.dim(` ${elapsedStr} ${fmtTokens(swarm.totalInputTokens)} in / ${fmtTokens(swarm.totalOutputTokens)} out ${tools} tool calls`));
508
+ if (swarm.mergeResults.length > 0) {
509
+ const merged = swarm.mergeResults.filter((r) => r.ok);
510
+ const autoResolved = merged.filter((r) => r.autoResolved).length;
511
+ const conflicts = swarm.mergeResults.filter((r) => !r.ok);
512
+ const target = swarm.mergeBranch || "HEAD";
513
+ if (merged.length > 0) {
514
+ const extra = autoResolved > 0 ? chalk.yellow(` (${autoResolved} auto-resolved)`) : "";
515
+ console.log(chalk.green(` Merged ${merged.length} branch(es) into ${target}`) + extra);
516
+ }
517
+ if (swarm.mergeBranch) {
518
+ console.log(chalk.dim(` Branch: ${swarm.mergeBranch} — create a PR or: git merge ${swarm.mergeBranch}`));
519
+ }
520
+ if (conflicts.length > 0) {
521
+ console.log(chalk.red(` ${conflicts.length} branch(es) had unresolved conflicts:`));
522
+ for (const c of conflicts) {
523
+ console.log(chalk.red(` ${c.branch}`));
524
+ }
525
+ console.log(chalk.dim(" Branches preserved — merge manually with: git merge <branch>"));
526
+ }
527
+ }
528
+ if (swarm.logFile) {
529
+ console.log(chalk.dim(` Log: ${swarm.logFile}`));
530
+ }
531
+ console.log("");
532
+ }
533
+ // Exit codes: 0 = all succeeded, 1 = some failed, 2 = all failed or aborted
534
+ if (swarm.aborted || swarm.completed === 0) {
535
+ process.exit(2);
536
+ }
537
+ if (swarm.failed > 0) {
538
+ process.exit(1);
539
+ }
540
+ }
541
+ function sleep(ms) {
542
+ return new Promise((r) => setTimeout(r, ms));
543
+ }
544
+ function fmtTokens(n) {
545
+ if (n >= 1_000_000)
546
+ return `${(n / 1_000_000).toFixed(1)}M`;
547
+ if (n >= 1_000)
548
+ return `${(n / 1_000).toFixed(1)}K`;
549
+ return String(n);
550
+ }
551
+ main().catch((err) => {
552
+ process.stdout.write("\x1B[?25h");
553
+ console.error(chalk.red(err.message || err));
554
+ process.exit(1);
555
+ });
@@ -0,0 +1,5 @@
1
+ import type { Task, PermMode } from "./types.js";
2
+ /**
3
+ * Coordinator: analyzes the codebase, breaks objective into parallel tasks.
4
+ */
5
+ export declare function planTasks(objective: string, cwd: string, model: string, permissionMode: PermMode, onLog: (text: string) => void): Promise<Task[]>;