claude-overnight 1.10.0 → 1.11.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/cli.js CHANGED
@@ -6,8 +6,8 @@ import chalk from "chalk";
6
6
  import { query } from "@anthropic-ai/claude-agent-sdk";
7
7
  // ── CLI flag parsing ──
8
8
  export function parseCliFlags(argv) {
9
- const known = new Set(["concurrency", "model", "timeout", "budget", "usage-cap", "extra-usage-budget"]);
10
- const booleans = new Set(["--dry-run", "-h", "--help", "-v", "--version", "--no-flex", "--allow-extra-usage"]);
9
+ const known = new Set(["concurrency", "model", "timeout", "budget", "usage-cap", "extra-usage-budget", "merge", "perm"]);
10
+ const booleans = new Set(["--dry-run", "-h", "--help", "-v", "--version", "--no-flex", "--allow-extra-usage", "--worktrees", "--no-worktrees", "--yolo"]);
11
11
  const flags = {};
12
12
  const positional = [];
13
13
  for (let i = 0; i < argv.length; i++) {
package/dist/index.js CHANGED
@@ -42,6 +42,11 @@ async function main() {
42
42
  --extra-usage-budget=N Max $ for extra usage ${chalk.dim("(implies --allow-extra-usage)")}
43
43
  --timeout=SECONDS Agent inactivity timeout ${chalk.dim("(default: 900s, nudges at timeout, kills at 2×)")}
44
44
  --no-flex Disable adaptive multi-wave planning ${chalk.dim("(run all tasks in one shot)")}
45
+ --worktrees Force worktree isolation on ${chalk.dim("(default: auto-detect git repo)")}
46
+ --no-worktrees Disable worktree isolation ${chalk.dim("(all agents work in real cwd)")}
47
+ --merge=MODE Merge strategy: yolo or branch ${chalk.dim("(default: yolo)")}
48
+ --perm=MODE Permission mode: auto, bypassPermissions, default ${chalk.dim("(default: auto)")}
49
+ --yolo Shorthand for --perm=bypassPermissions --no-worktrees
45
50
 
46
51
  ${chalk.cyan("Defaults")} ${chalk.dim("(non-interactive)")}
47
52
  model: first available concurrency: 5 worktrees: auto merge: yolo perms: auto
@@ -251,6 +256,9 @@ async function main() {
251
256
  let usageCap;
252
257
  let allowExtraUsage = false;
253
258
  let extraUsageBudget;
259
+ let permissionMode = "auto";
260
+ let useWorktrees = false;
261
+ let mergeStrategy = "yolo";
254
262
  if (resuming) {
255
263
  workerModel = resumeState.workerModel;
256
264
  plannerModel = resumeState.plannerModel;
@@ -260,6 +268,9 @@ async function main() {
260
268
  usageCap = resumeState.usageCap;
261
269
  allowExtraUsage = resumeState.allowExtraUsage ?? false;
262
270
  extraUsageBudget = resumeState.extraUsageBudget;
271
+ permissionMode = resumeState.permissionMode;
272
+ useWorktrees = resumeState.useWorktrees;
273
+ mergeStrategy = resumeState.mergeStrategy;
263
274
  }
264
275
  else if (!nonInteractive) {
265
276
  while (true) {
@@ -279,6 +290,17 @@ async function main() {
279
290
  console.error(chalk.red(` Budget must be a positive number`));
280
291
  process.exit(1);
281
292
  }
293
+ // ③ Max concurrency (skip if --concurrency set)
294
+ if (cliFlags.concurrency) {
295
+ concurrency = parseInt(cliFlags.concurrency);
296
+ }
297
+ else {
298
+ const defaultC = Math.min(5, budget);
299
+ const concAns = await ask(`\n ${chalk.cyan("③")} ${chalk.dim("Max concurrency")} ${chalk.dim("[")}${chalk.white(String(defaultC))}${chalk.dim("]:")} `);
300
+ concurrency = parseInt(concAns) || defaultC;
301
+ if (concurrency < 1)
302
+ concurrency = 1;
303
+ }
282
304
  let modelFrame = 0;
283
305
  const modelSpinner = setInterval(() => {
284
306
  process.stdout.write(`\x1B[2K\r ${chalk.cyan(BRAILLE[modelFrame++ % BRAILLE.length])} ${chalk.dim("loading models...")}`);
@@ -293,19 +315,19 @@ async function main() {
293
315
  }
294
316
  plannerModel = models[0]?.value || "claude-sonnet-4-6";
295
317
  if (models.length > 0) {
296
- workerModel = await select(`${chalk.cyan("")} Worker model:`, models.map(m => ({ name: m.displayName, value: m.value, hint: m.description })));
318
+ workerModel = await select(`${chalk.cyan("")} Worker model:`, models.map(m => ({ name: m.displayName, value: m.value, hint: m.description })));
297
319
  }
298
320
  else {
299
- const ans = await ask(` ${chalk.cyan("")} ${chalk.dim("Worker model [claude-sonnet-4-6]:")} `);
321
+ const ans = await ask(` ${chalk.cyan("")} ${chalk.dim("Worker model [claude-sonnet-4-6]:")} `);
300
322
  workerModel = ans || "claude-sonnet-4-6";
301
323
  }
302
- usageCap = await select(`${chalk.cyan("")} Usage cap:`, [
324
+ usageCap = await select(`${chalk.cyan("")} Usage cap:`, [
303
325
  { name: "Unlimited", value: undefined, hint: "full capacity, wait through rate limits" },
304
326
  { name: "90%", value: 0.9, hint: "leave 10% for other work" },
305
327
  { name: "75%", value: 0.75, hint: "conservative, plenty of headroom" },
306
328
  { name: "50%", value: 0.5, hint: "use half, keep the rest" },
307
329
  ]);
308
- const extraChoice = await select(`${chalk.cyan("")} Allow extra usage ${chalk.dim("(billed separately)")}:`, [
330
+ const extraChoice = await select(`${chalk.cyan("")} Allow extra usage ${chalk.dim("(billed separately)")}:`, [
309
331
  { name: "No", value: "no", hint: "stop when plan limits are reached" },
310
332
  { name: "Yes, with $ limit", value: "budget", hint: "set a spending cap" },
311
333
  { name: "Yes, unlimited", value: "unlimited", hint: "keep going no matter what" },
@@ -319,7 +341,44 @@ async function main() {
319
341
  }
320
342
  else if (extraChoice === "unlimited")
321
343
  allowExtraUsage = true;
322
- concurrency = Math.min(5, budget);
344
+ // Permission mode (skip if --yolo or --perm set)
345
+ const cliYolo = argv.includes("--yolo");
346
+ if (cliFlags.perm) {
347
+ permissionMode = cliFlags.perm;
348
+ }
349
+ else if (cliYolo) {
350
+ permissionMode = "bypassPermissions";
351
+ }
352
+ else {
353
+ permissionMode = await select(`${chalk.cyan("⑦")} Permissions:`, [
354
+ { name: "Auto", value: "auto", hint: "accept low-risk, reject high-risk" },
355
+ { name: "Bypass all", value: "bypassPermissions", hint: "agents can run anything (yolo)" },
356
+ { name: "Prompt each", value: "default", hint: "ask for every dangerous op" },
357
+ ]);
358
+ }
359
+ // ⑧ Worktrees + merge (skip if --yolo, --worktrees, --no-worktrees, or --merge set)
360
+ const gitRepo = isGitRepo(cwd);
361
+ if (cliYolo || argv.includes("--no-worktrees")) {
362
+ useWorktrees = false;
363
+ mergeStrategy = cliFlags.merge || "yolo";
364
+ }
365
+ else if (argv.includes("--worktrees")) {
366
+ useWorktrees = true;
367
+ mergeStrategy = cliFlags.merge || "yolo";
368
+ }
369
+ else if (gitRepo) {
370
+ const wtChoice = await select(`${chalk.cyan("⑧")} Git isolation:`, [
371
+ { name: "Worktrees + yolo merge", value: "wt-yolo", hint: "isolate agents, merge into current branch" },
372
+ { name: "Worktrees + new branch", value: "wt-branch", hint: "isolate agents, merge into a new branch" },
373
+ { name: "No worktrees", value: "no-wt", hint: "all agents share the working directory" },
374
+ ]);
375
+ useWorktrees = wtChoice !== "no-wt";
376
+ mergeStrategy = wtChoice === "wt-branch" ? "branch" : "yolo";
377
+ }
378
+ else {
379
+ useWorktrees = false;
380
+ mergeStrategy = "yolo";
381
+ }
323
382
  const parts = [];
324
383
  if (workerModel !== plannerModel)
325
384
  parts.push(`${detectModelTier(workerModel)} → ${detectModelTier(plannerModel)}`);
@@ -331,6 +390,12 @@ async function main() {
331
390
  if (usageCap != null)
332
391
  parts.push(`cap ${Math.round(usageCap * 100)}%`);
333
392
  parts.push(allowExtraUsage ? (extraUsageBudget ? `extra $${extraUsageBudget}` : "extra ∞") : "no extra");
393
+ if (permissionMode !== "auto")
394
+ parts.push(permissionMode === "bypassPermissions" ? "yolo" : "prompt");
395
+ if (useWorktrees)
396
+ parts.push(mergeStrategy === "branch" ? "wt→branch" : "wt→yolo");
397
+ else
398
+ parts.push("no wt");
334
399
  if (completedRuns.length > 0)
335
400
  parts.push(`${completedRuns.length} prior`);
336
401
  const inner = parts.join(chalk.dim(" · "));
@@ -375,11 +440,28 @@ async function main() {
375
440
  }
376
441
  }
377
442
  validateConcurrency(concurrency);
378
- const permissionMode = resuming ? resumeState.permissionMode : (fileCfg?.permissionMode ?? "auto");
379
- const useWorktrees = resuming ? resumeState.useWorktrees : (fileCfg?.useWorktrees ?? isGitRepo(cwd));
443
+ // Resolve permissionMode, useWorktrees, mergeStrategy for non-interactive + non-resume
444
+ if (!resuming && nonInteractive) {
445
+ const yolo = argv.includes("--yolo");
446
+ permissionMode = cliFlags.perm ? cliFlags.perm
447
+ : yolo ? "bypassPermissions"
448
+ : (fileCfg?.permissionMode ?? "auto");
449
+ if (!["auto", "bypassPermissions", "default"].includes(permissionMode)) {
450
+ console.error(chalk.red(` --perm must be auto, bypassPermissions, or default (got ${permissionMode})`));
451
+ process.exit(1);
452
+ }
453
+ useWorktrees = argv.includes("--no-worktrees") || yolo ? false
454
+ : argv.includes("--worktrees") ? true
455
+ : (fileCfg?.useWorktrees ?? isGitRepo(cwd));
456
+ mergeStrategy = cliFlags.merge ? cliFlags.merge
457
+ : (fileCfg?.mergeStrategy ?? "yolo");
458
+ if (!["yolo", "branch"].includes(mergeStrategy)) {
459
+ console.error(chalk.red(` --merge must be yolo or branch (got ${mergeStrategy})`));
460
+ process.exit(1);
461
+ }
462
+ }
380
463
  if (useWorktrees)
381
464
  validateGitRepo(cwd);
382
- const mergeStrategy = resuming ? resumeState.mergeStrategy : (fileCfg?.mergeStrategy ?? "yolo");
383
465
  if (nonInteractive) {
384
466
  const capStr = usageCap != null ? ` cap=${Math.round(usageCap * 100)}%` : "";
385
467
  const extraStr = allowExtraUsage ? (extraUsageBudget ? ` extra=$${extraUsageBudget}` : " extra=∞") : " extra=off";
@@ -416,7 +498,7 @@ async function main() {
416
498
  for (let i = 0; i < themes.length; i++)
417
499
  console.log(chalk.dim(` ${String(i + 1).padStart(3)}.`) + ` ${themes[i]}`);
418
500
  console.log(chalk.dim(`\n ${thinkingCount} thinking agents → orchestrate → ${(budget ?? 10) - thinkingCount} execution sessions\n`));
419
- const action = await selectKey(`${chalk.white(`${themes.length} themes`)} ${chalk.dim(`· ${thinkingCount} thinking · ${concurrency} concurrent`)}`, [{ key: "r", desc: "un" }, { key: "e", desc: "dit" }, { key: "q", desc: "uit" }]);
501
+ const action = await selectKey(`${chalk.white(`${themes.length} themes`)} ${chalk.dim(`· ${thinkingCount} thinking · ${concurrency} concurrent`)}`, [{ key: "r", desc: "un" }, { key: "e", desc: "dit" }, { key: "c", desc: "hat" }, { key: "q", desc: "uit" }]);
420
502
  if (action === "r") {
421
503
  reviewing = false;
422
504
  break;
@@ -435,6 +517,28 @@ async function main() {
435
517
  }
436
518
  planRestore();
437
519
  }
520
+ else if (action === "c") {
521
+ const question = await ask(`\n ${chalk.bold("Ask about the themes:")}\n ${chalk.cyan(">")} `);
522
+ if (!question)
523
+ continue;
524
+ process.stdout.write("\x1B[?25l");
525
+ try {
526
+ let answer = "";
527
+ for await (const msg of query({
528
+ prompt: `You're planning work for: "${objective}"\n\nThemes identified:\n${themes.map((t, i) => `${i + 1}. ${t}`).join("\n")}\n\nUser question: ${question}`,
529
+ options: { cwd, model: plannerModel, permissionMode, persistSession: false },
530
+ })) {
531
+ if (msg.type === "result" && msg.subtype === "success")
532
+ answer = msg.result || "";
533
+ }
534
+ planRestore();
535
+ if (answer)
536
+ console.log(chalk.dim(`\n ${answer.slice(0, 500)}\n`));
537
+ }
538
+ catch {
539
+ planRestore();
540
+ }
541
+ }
438
542
  else {
439
543
  console.log(chalk.dim("\n Aborted.\n"));
440
544
  process.exit(0);
package/dist/render.js CHANGED
@@ -183,7 +183,8 @@ export function renderFrame(swarm, showHotkeys, runInfo) {
183
183
  if (showHotkeys) {
184
184
  const pending = runInfo?.pendingSteer ?? 0;
185
185
  const chip = pending > 0 ? chalk.cyan(` \u270E ${pending} steer queued`) : "";
186
- out.push(chalk.dim(" [b] budget [t] threshold [s] steer [?] ask [q] stop") + chip);
186
+ const fixChip = swarm.failed > 0 && swarm.active > 0 ? chalk.yellow(" [f] fix") : "";
187
+ out.push(chalk.dim(" [b] budget [t] threshold [s] steer [?] ask [q] stop") + fixChip + chip);
187
188
  }
188
189
  out.push("");
189
190
  return out.join("\n");
package/dist/run.js CHANGED
@@ -30,7 +30,7 @@ export async function executeRun(cfg) {
30
30
  const waveHistory = [];
31
31
  let accCost, accCompleted, accFailed, accTools;
32
32
  let accIn = 0, accOut = 0;
33
- let lastCapped = false, lastAborted = false, objectiveComplete = false;
33
+ let lastCapped = false, lastAborted = false, objectiveComplete = false, lastHealed = false;
34
34
  const branches = [];
35
35
  if (cfg.resuming && cfg.resumeState) {
36
36
  const rs = cfg.resumeState;
@@ -161,7 +161,6 @@ export async function executeRun(cfg) {
161
161
  }
162
162
  const waveMerge = (flex && runBranch) ? "yolo" : mergeStrategy;
163
163
  let stopping = false;
164
- let steeringFailed = false;
165
164
  const gracefulStop = () => {
166
165
  if (stopping) {
167
166
  currentSwarm?.cleanup();
@@ -236,9 +235,17 @@ export async function executeRun(cfg) {
236
235
  display.appendSteeringEvent(`Steering failed (attempt ${steerAttempts}/3) — retrying...`);
237
236
  continue;
238
237
  }
239
- display.stop();
240
- console.log(chalk.yellow(` Steering failed after ${steerAttempts} attempts: ${err.message?.slice(0, 80)} — stopping\n`));
241
- steeringFailed = true;
238
+ display.appendSteeringEvent(`Steering failed ${steerAttempts}× — falling back`);
239
+ let fallbackStatus = "";
240
+ try {
241
+ fallbackStatus = readFileSync(join(runDir, "status.md"), "utf-8");
242
+ }
243
+ catch { }
244
+ currentTasks = [{
245
+ id: "fallback-0",
246
+ prompt: `Steering couldn't decide the next step. Read the project, assess what's done vs. remaining, and do the most impactful work.\n\nObjective: ${objective}${fallbackStatus ? `\n\nStatus:\n${fallbackStatus}` : ""}`,
247
+ }];
248
+ steered = true;
242
249
  break;
243
250
  }
244
251
  }
@@ -256,6 +263,16 @@ export async function executeRun(cfg) {
256
263
  display.start();
257
264
  // ── Main wave loop ──
258
265
  while (remaining > 0 && currentTasks.length > 0 && !stopping) {
266
+ if (!lastHealed) {
267
+ const healTask = checkProjectHealth(cwd);
268
+ if (healTask && remaining > 0) {
269
+ lastHealed = true;
270
+ currentTasks = [healTask];
271
+ }
272
+ }
273
+ else {
274
+ lastHealed = false;
275
+ }
259
276
  if (currentTasks.length > remaining)
260
277
  currentTasks = currentTasks.slice(0, remaining);
261
278
  syncRunInfo();
@@ -329,7 +346,7 @@ export async function executeRun(cfg) {
329
346
  // ── Finalize ──
330
347
  const trulyDone = objectiveComplete || (!flex && remaining <= 0);
331
348
  const wasCapped = lastCapped || lastAborted;
332
- const finalPhase = trulyDone ? "done" : steeringFailed ? "steering" : wasCapped ? "capped" : remaining <= 0 ? "capped" : "stopped";
349
+ const finalPhase = trulyDone ? "done" : wasCapped ? "capped" : remaining <= 0 ? "capped" : "stopped";
333
350
  saveRunState(runDir, {
334
351
  id: `run-${new Date().toISOString().slice(0, 19)}`, objective: objective ?? "", budget: cfg.budget,
335
352
  remaining, workerModel, plannerModel, concurrency, permissionMode,
@@ -370,8 +387,6 @@ export async function executeRun(cfg) {
370
387
  console.log(chalk.green(` ${bannerChar.repeat(Math.min(termW - 4, 60))}`));
371
388
  if (trulyDone)
372
389
  console.log(chalk.bold.green(` CLAUDE OVERNIGHT — COMPLETE`));
373
- else if (steeringFailed)
374
- console.log(chalk.bold.yellow(` CLAUDE OVERNIGHT — STEERING FAILED`));
375
390
  else if (remaining <= 0)
376
391
  console.log(chalk.bold.yellow(` CLAUDE OVERNIGHT — BUDGET EXHAUSTED`));
377
392
  else if (lastCapped)
@@ -426,3 +441,40 @@ export async function executeRun(cfg) {
426
441
  if (lastAborted || accCompleted === 0)
427
442
  process.exit(2);
428
443
  }
444
+ function checkProjectHealth(cwd) {
445
+ let pkg;
446
+ try {
447
+ pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf-8"));
448
+ }
449
+ catch {
450
+ return undefined;
451
+ }
452
+ const scripts = pkg.scripts || {};
453
+ let scriptName;
454
+ for (const name of ["typecheck", "check:types", "type-check", "build"]) {
455
+ if (scripts[name]) {
456
+ scriptName = name;
457
+ break;
458
+ }
459
+ }
460
+ if (!scriptName)
461
+ return undefined;
462
+ const pm = existsSync(join(cwd, "pnpm-lock.yaml")) ? "pnpm"
463
+ : existsSync(join(cwd, "bun.lockb")) || existsSync(join(cwd, "bun.lock")) ? "bun"
464
+ : existsSync(join(cwd, "yarn.lock")) ? "yarn" : "npm";
465
+ const cmd = `${pm} run ${scriptName}`;
466
+ try {
467
+ execSync(cmd, { cwd, encoding: "utf-8", stdio: "pipe", timeout: 60_000 });
468
+ return undefined;
469
+ }
470
+ catch (err) {
471
+ if (err.killed)
472
+ return undefined;
473
+ const output = ((err.stdout || "") + "\n" + (err.stderr || "")).trim();
474
+ const trimmed = output.length > 4000 ? output.slice(0, 2000) + "\n…\n" + output.slice(-2000) : output;
475
+ return {
476
+ id: "heal-0",
477
+ prompt: `Fix the broken build. \`${cmd}\` fails after merging parallel work:\n\`\`\`\n${trimmed}\n\`\`\`\nFix every error. Run \`${cmd}\` when done to verify.`,
478
+ };
479
+ }
480
+ }
package/dist/steering.js CHANGED
@@ -26,11 +26,13 @@ export async function steerWave(objective, history, remainingBudget, cwd, planne
26
26
  const recentWaves = history.slice(-3);
27
27
  const recentText = recentWaves.length > 0 ? recentWaves.map(w => {
28
28
  const lines = w.tasks.map(t => {
29
- const files = t.filesChanged ? ` (${t.filesChanged} files)` : "";
29
+ const files = t.filesChanged ? ` (${t.filesChanged} files)` : " (0 files)";
30
30
  const err = t.error ? ` — ${t.error}` : "";
31
31
  return ` - [${t.status}] ${t.prompt.slice(0, 120)}${files}${err}`;
32
32
  }).join("\n");
33
- return `Wave ${w.wave + 1}:\n${lines}`;
33
+ const zeroChange = w.tasks.filter(t => t.status === "done" && !t.filesChanged).length;
34
+ const warn = zeroChange > w.tasks.length / 2 ? `\n ⚠ ${zeroChange}/${w.tasks.length} agents changed 0 files — tasks may be mis-scoped or blocked` : "";
35
+ return `Wave ${w.wave + 1}:\n${lines}${warn}`;
34
36
  }).join("\n\n") : "(first wave)";
35
37
  const cap = (s, max) => s.length > max ? s.slice(0, max) + "\n...(truncated)" : s;
36
38
  const statusBlock = runMemory?.status ? `\nCurrent project status:\n${runMemory.status}\n` : "";
package/dist/swarm.d.ts CHANGED
@@ -59,6 +59,8 @@ export declare class Swarm {
59
59
  get pending(): number;
60
60
  run(): Promise<void>;
61
61
  abort(): void;
62
+ /** Re-queue all errored agents' tasks for retry within this wave. */
63
+ requeueFailed(): number;
62
64
  logSequence: number;
63
65
  log(agentId: number, text: string): void;
64
66
  cleanup(): void;
package/dist/swarm.js CHANGED
@@ -110,6 +110,21 @@ export class Swarm {
110
110
  this.activeQueries.forEach(q => q.close());
111
111
  this.activeQueries.clear();
112
112
  }
113
+ /** Re-queue all errored agents' tasks for retry within this wave. */
114
+ requeueFailed() {
115
+ const errored = this.agents.filter(a => a.status === "error");
116
+ if (errored.length === 0)
117
+ return 0;
118
+ for (const a of errored) {
119
+ this.queue.push(a.task);
120
+ a.status = "pending";
121
+ a.error = undefined;
122
+ a.finishedAt = undefined;
123
+ }
124
+ this.failed -= errored.length;
125
+ this.log(-1, `Re-queued ${errored.length} failed task(s)`);
126
+ return errored.length;
127
+ }
113
128
  logSequence = 0;
114
129
  log(agentId, text) {
115
130
  const entry = { time: Date.now(), agentId, text };
@@ -201,21 +216,39 @@ export class Swarm {
201
216
  this.agents.push(agent);
202
217
  let agentCwd = task.cwd || this.config.cwd;
203
218
  if (this.config.useWorktrees && this.worktreeBase && !task.noWorktree) {
204
- try {
205
- const branch = `swarm/task-${id}`;
206
- const dir = join(this.worktreeBase, `agent-${id}`);
207
- gitExec(`git worktree add -b "${branch}" "${dir}" HEAD`, this.config.cwd);
219
+ const branch = `swarm/task-${id}`;
220
+ const dir = join(this.worktreeBase, `agent-${id}`);
221
+ let worktreeOk = false;
222
+ for (let wt = 0; wt < 2 && !worktreeOk; wt++) {
223
+ try {
224
+ gitExec(`git worktree add -b "${branch}" "${dir}" HEAD`, this.config.cwd);
225
+ worktreeOk = true;
226
+ }
227
+ catch (e) {
228
+ if (wt === 0) {
229
+ this.log(id, `Worktree failed, cleaning up: ${e.message?.slice(0, 50)}`);
230
+ try {
231
+ gitExec(`git branch -D "${branch}"`, this.config.cwd);
232
+ }
233
+ catch { }
234
+ try {
235
+ rmSync(dir, { recursive: true, force: true });
236
+ }
237
+ catch { }
238
+ try {
239
+ gitExec("git worktree prune", this.config.cwd);
240
+ }
241
+ catch { }
242
+ }
243
+ }
244
+ }
245
+ if (worktreeOk) {
208
246
  agentCwd = dir;
209
247
  agent.branch = branch;
210
248
  this.log(id, `Worktree: ${branch}`);
211
249
  }
212
- catch (e) {
213
- this.log(id, `Worktree failed: ${e.message?.slice(0, 60)}`);
214
- agent.status = "error";
215
- agent.error = "worktree creation failed";
216
- agent.finishedAt = Date.now();
217
- this.failed++;
218
- return;
250
+ else {
251
+ this.log(id, `Worktree failed after retry — running without isolation`);
219
252
  }
220
253
  }
221
254
  this.log(id, `Starting: ${task.prompt.slice(0, 60)}`);
package/dist/ui.d.ts CHANGED
@@ -87,6 +87,8 @@ export declare class RunDisplay {
87
87
  resume(): void;
88
88
  stop(): void;
89
89
  private resumeInterval;
90
+ /** Write the full frame to stdout, clamped to terminal height. */
91
+ private flush;
90
92
  private render;
91
93
  private renderInputPrompt;
92
94
  private renderAskPanel;
package/dist/ui.js CHANGED
@@ -112,15 +112,20 @@ export class RunDisplay {
112
112
  catch {
113
113
  return;
114
114
  }
115
- this.interval = setInterval(() => {
116
- try {
117
- process.stdout.write("\x1B[H\x1B[J");
118
- process.stdout.write(this.render());
119
- }
120
- catch {
121
- this.pause();
122
- }
123
- }, 250);
115
+ this.interval = setInterval(() => this.flush(), 250);
116
+ }
117
+ /** Write the full frame to stdout, clamped to terminal height. */
118
+ flush() {
119
+ try {
120
+ const maxRows = (process.stdout.rows || 40) - 1;
121
+ const frame = this.render();
122
+ const lines = frame.split("\n");
123
+ process.stdout.write("\x1B[H\x1B[J");
124
+ process.stdout.write(lines.length > maxRows ? lines.slice(0, maxRows).join("\n") : frame);
125
+ }
126
+ catch {
127
+ this.pause();
128
+ }
124
129
  }
125
130
  render() {
126
131
  let frame = "";
@@ -223,6 +228,7 @@ export class RunDisplay {
223
228
  else if (/^[0-9.]$/.test(s)) {
224
229
  this.inputBuf += s;
225
230
  }
231
+ this.flush();
226
232
  return;
227
233
  }
228
234
  if (this.inputMode === "steer" || this.inputMode === "ask") {
@@ -238,11 +244,20 @@ export class RunDisplay {
238
244
  else
239
245
  this.onSteer?.(text);
240
246
  }
247
+ this.flush();
241
248
  return;
242
249
  }
243
- if (ch === "\x1B" || ch === "\x03") {
250
+ if (ch === "\x03") {
244
251
  this.inputMode = "none";
245
252
  this.inputBuf = "";
253
+ this.flush();
254
+ return;
255
+ }
256
+ // Ignore raw ESC only — let ANSI sequences (arrows etc.) fall through
257
+ if (ch === "\x1B" && s.length === 1) {
258
+ this.inputMode = "none";
259
+ this.inputBuf = "";
260
+ this.flush();
246
261
  return;
247
262
  }
248
263
  if (ch === "\x7F" || ch === "\b") {
@@ -254,6 +269,12 @@ export class RunDisplay {
254
269
  this.inputBuf += ch;
255
270
  }
256
271
  }
272
+ this.flush();
273
+ return;
274
+ }
275
+ // Dismiss completed ask panel on Escape
276
+ if (s === "\x1B" && this.askState && !this.askState.streaming) {
277
+ this.askState = undefined;
257
278
  return;
258
279
  }
259
280
  if (s === "b" || s === "B") {
@@ -266,11 +287,19 @@ export class RunDisplay {
266
287
  this.inputBuf = "";
267
288
  }
268
289
  }
290
+ else if ((s === "f" || s === "F") && this.swarm && this.swarm.failed > 0 && this.swarm.active > 0) {
291
+ this.swarm.requeueFailed();
292
+ }
269
293
  else if ((s === "s" || s === "S") && this.onSteer) {
270
294
  this.inputMode = "steer";
271
295
  this.inputBuf = "";
272
296
  }
273
297
  else if (s === "?" && this.onAsk && this.swarm && !this.askBusy) {
298
+ // If ask panel is showing a completed answer, dismiss it instead of opening new
299
+ if (this.askState && !this.askState.streaming) {
300
+ this.askState = undefined;
301
+ return;
302
+ }
274
303
  this.inputMode = "ask";
275
304
  this.inputBuf = "";
276
305
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-overnight",
3
- "version": "1.10.0",
3
+ "version": "1.11.2",
4
4
  "description": "Run 10, 100, or 1000 Claude agents overnight. Parallel autonomous AI coding with thinking waves, iterative quality steering, crash recovery, and rate limit handling. Built on the Claude Agent SDK.",
5
5
  "type": "module",
6
6
  "bin": {