claude-overnight 1.9.1 → 1.11.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/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);
@@ -1,4 +1,11 @@
1
1
  import type { Task, PermMode, RateLimitWindow } from "./types.js";
2
+ /**
3
+ * Logging callback used by planner/steering queries.
4
+ * `kind` distinguishes ephemeral status updates (heartbeat ticker) from
5
+ * discrete events worth persisting in a scrollback log (tool uses, retries).
6
+ * Plain (text) callers still work — extra arg is ignored.
7
+ */
8
+ export type PlannerLog = (text: string, kind?: "status" | "event") => void;
2
9
  export interface PlannerRateLimitInfo {
3
10
  utilization: number;
4
11
  status: string;
@@ -22,7 +29,7 @@ export declare function detectModelTier(model: string): ModelTier;
22
29
  export declare function modelCapabilityBlock(model: string): string;
23
30
  export declare function getTotalPlannerCost(): number;
24
31
  export declare function getPlannerRateLimitInfo(): PlannerRateLimitInfo;
25
- export declare function runPlannerQuery(prompt: string, opts: PlannerOpts, onLog: (text: string) => void): Promise<string>;
32
+ export declare function runPlannerQuery(prompt: string, opts: PlannerOpts, onLog: PlannerLog): Promise<string>;
26
33
  export declare function postProcess(raw: Task[], budget: number | undefined, onLog: (text: string) => void): Task[];
27
34
  export declare function attemptJsonParse(text: string): any | null;
28
35
  export declare function extractTaskJson(raw: string, retry: () => Promise<string>, onLog?: (text: string) => void, outFile?: string): Promise<{
@@ -51,18 +51,18 @@ export async function runPlannerQuery(prompt, opts, onLog) {
51
51
  catch (err) {
52
52
  if (err instanceof NudgeError) {
53
53
  if (err.sessionId) {
54
- onLog("Silent 15m — resuming session with continue");
54
+ onLog("Silent 15m — resuming session with continue", "event");
55
55
  currentPrompt = "Continue. Complete the task.";
56
56
  currentOpts = { ...opts, resumeSessionId: err.sessionId };
57
57
  }
58
58
  else {
59
- onLog("Silent 15m — restarting planner (no session to resume)");
59
+ onLog("Silent 15m — restarting planner (no session to resume)", "event");
60
60
  }
61
61
  continue;
62
62
  }
63
63
  if (attempt < MAX_RETRIES && isRateLimitError(err)) {
64
64
  const waitMs = BACKOFF[attempt];
65
- onLog(`Rate limited — waiting ${Math.round(waitMs / 1000)}s before retry ${attempt + 1}/${MAX_RETRIES}`);
65
+ onLog(`Rate limited — waiting ${Math.round(waitMs / 1000)}s before retry ${attempt + 1}/${MAX_RETRIES}`, "event");
66
66
  await new Promise((r) => setTimeout(r, waitMs));
67
67
  continue;
68
68
  }
@@ -105,7 +105,7 @@ async function runPlannerQueryOnce(prompt, opts, onLog) {
105
105
  const rlPct = _plannerRateLimitInfo.utilization;
106
106
  const rlStr = rlPct > 0 ? ` · ${Math.round(rlPct * 100)}%` : "";
107
107
  const extra = lastLogText ? ` · ${lastLogText}` : "";
108
- onLog(`${timeStr}${toolStr}${costStr}${rlStr}${extra}`);
108
+ onLog(`${timeStr}${toolStr}${costStr}${rlStr}${extra}`, "status");
109
109
  }, 500);
110
110
  const timeoutMs = isResume ? HARD_TIMEOUT_MS : NUDGE_MS;
111
111
  let sessionId;
@@ -143,7 +143,7 @@ async function runPlannerQueryOnce(prompt, opts, onLog) {
143
143
  if (ev?.type === "content_block_start" && ev.content_block?.type === "tool_use") {
144
144
  toolCount++;
145
145
  lastLogText = ev.content_block.name;
146
- onLog(ev.content_block.name);
146
+ onLog(ev.content_block.name, "event");
147
147
  }
148
148
  if (ev?.type === "content_block_delta") {
149
149
  const delta = ev.delta;
package/dist/render.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { Swarm } from "./swarm.js";
2
2
  import type { RateLimitWindow } from "./types.js";
3
- import type { RunInfo } from "./ui.js";
3
+ import type { RunInfo, SteeringContext, SteeringEvent } from "./ui.js";
4
4
  export declare function truncate(s: string, max: number): string;
5
5
  export declare function fmtTokens(n: number): string;
6
6
  export declare function fmtDur(ms: number): string;
@@ -11,6 +11,14 @@ type RLGetter = () => {
11
11
  resetsAt?: number;
12
12
  };
13
13
  export declare function renderFrame(swarm: Swarm, showHotkeys: boolean, runInfo?: RunInfo): string;
14
- export declare function renderSteeringFrame(runInfo: RunInfo, steeringText: string, showHotkeys: boolean, rlGetter?: RLGetter): string;
14
+ export interface SteeringViewData {
15
+ /** The ephemeral ticker heartbeat — elapsed, tool count, cost, current reasoning snippet. */
16
+ statusLine: string;
17
+ /** Persistent scrollback of discrete events (tool uses, retries, nudges). */
18
+ events: SteeringEvent[];
19
+ /** Optional context read from disk at setSteering() time. */
20
+ context?: SteeringContext;
21
+ }
22
+ export declare function renderSteeringFrame(runInfo: RunInfo, data: SteeringViewData, showHotkeys: boolean, rlGetter?: RLGetter): string;
15
23
  export declare function renderSummary(swarm: Swarm): string;
16
24
  export {};
package/dist/render.js CHANGED
@@ -180,12 +180,87 @@ export function renderFrame(swarm, showHotkeys, runInfo) {
180
180
  const tag = entry.agentId < 0 ? chalk.magenta("[sys]") : chalk.cyan(`[${entry.agentId}]`);
181
181
  out.push(chalk.gray(` ${t} `) + tag + ` ${colorEvent(truncate(entry.text, w - 22))}`);
182
182
  }
183
- if (showHotkeys)
184
- out.push(chalk.dim(" [b] budget [t] threshold [q] stop"));
183
+ if (showHotkeys) {
184
+ const pending = runInfo?.pendingSteer ?? 0;
185
+ const chip = pending > 0 ? chalk.cyan(` \u270E ${pending} steer queued`) : "";
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);
188
+ }
185
189
  out.push("");
186
190
  return out.join("\n");
187
191
  }
188
- export function renderSteeringFrame(runInfo, steeringText, showHotkeys, rlGetter) {
192
+ function section(out, w, title) {
193
+ const inner = ` ${title} `;
194
+ const dashW = Math.max(3, Math.min(w - 6, 96) - inner.length);
195
+ out.push(chalk.gray(" \u2500\u2500\u2500" + inner + "\u2500".repeat(dashW)));
196
+ }
197
+ function renderSteeringUsageBar(out, w, rl) {
198
+ const rlBarW = Math.min(30, w - 40);
199
+ const draw = (pct, label) => {
200
+ let barStr = "";
201
+ const f = Math.round(pct * rlBarW);
202
+ for (let i = 0; i < rlBarW; i++) {
203
+ if (i < f)
204
+ barStr += pct > 0.9 ? chalk.red("\u2588") : pct > 0.75 ? chalk.yellow("\u2588") : chalk.blue("\u2588");
205
+ else
206
+ barStr += chalk.gray("\u2591");
207
+ }
208
+ let lbl = `${Math.round(pct * 100)}% used`;
209
+ if (rl.isUsingOverage)
210
+ lbl += chalk.red(" [EXTRA USAGE]");
211
+ if (rl.resetsAt && rl.resetsAt > Date.now()) {
212
+ const waitSec = Math.ceil((rl.resetsAt - Date.now()) / 1000);
213
+ const mm = Math.floor(waitSec / 60), ss = waitSec % 60;
214
+ lbl = chalk.red(`Waiting for reset ${mm > 0 ? `${mm}m ${ss}s` : `${ss}s`}`);
215
+ }
216
+ const prefix = label ? chalk.dim(label.padEnd(6)) : chalk.dim("Usage ");
217
+ out.push(` ${prefix}${barStr} ${lbl}`);
218
+ };
219
+ if (rl.windows.size > 1) {
220
+ const wins = Array.from(rl.windows.values());
221
+ const idx = Math.floor(Date.now() / 3000) % wins.length;
222
+ draw(wins[idx].utilization, wins[idx].type.replace(/_/g, " ").slice(0, 5));
223
+ }
224
+ else {
225
+ draw(rl.utilization);
226
+ }
227
+ }
228
+ function renderLastWave(out, w, lw) {
229
+ section(out, w, `Wave ${lw.wave + 1} summary`);
230
+ const done = lw.tasks.filter(t => t.status === "done").length;
231
+ const failed = lw.tasks.filter(t => t.status === "error").length;
232
+ const running = lw.tasks.filter(t => t.status === "running").length;
233
+ const parts = [];
234
+ if (done > 0)
235
+ parts.push(chalk.green(`\u2713 ${done} done`));
236
+ if (failed > 0)
237
+ parts.push(chalk.red(`\u2717 ${failed} failed`));
238
+ if (running > 0)
239
+ parts.push(chalk.blue(`~ ${running} running`));
240
+ if (parts.length === 0)
241
+ parts.push(chalk.dim("(no tasks)"));
242
+ out.push(" " + parts.join(" "));
243
+ const show = lw.tasks.slice(0, 5);
244
+ for (const t of show) {
245
+ const icon = t.status === "done" ? chalk.green("\u2713")
246
+ : t.status === "error" ? chalk.red("\u2717")
247
+ : t.status === "running" ? chalk.blue("~")
248
+ : chalk.gray("\u00b7");
249
+ const line = t.prompt.replace(/\n/g, " ");
250
+ out.push(` ${icon} ${chalk.dim(truncate(line, w - 8))}`);
251
+ }
252
+ if (lw.tasks.length > 5)
253
+ out.push(chalk.dim(` \u2026 + ${lw.tasks.length - 5} more`));
254
+ }
255
+ function renderStatusBlock(out, w, status) {
256
+ const lines = status.trim().split("\n").filter(l => l.trim()).slice(0, 6);
257
+ if (lines.length === 0)
258
+ return;
259
+ section(out, w, "Status");
260
+ for (const ln of lines)
261
+ out.push(` ${chalk.dim(truncate(ln.trim(), w - 4))}`);
262
+ }
263
+ export function renderSteeringFrame(runInfo, data, showHotkeys, rlGetter) {
189
264
  const w = Math.max((process.stdout.columns ?? 80) || 80, 60);
190
265
  const out = [];
191
266
  const totalUsed = runInfo.accCompleted + runInfo.accFailed;
@@ -201,45 +276,43 @@ export function renderSteeringFrame(runInfo, steeringText, showHotkeys, rlGetter
201
276
  sessionsUsed: totalUsed, sessionsBudget: runInfo.sessionsBudget, remaining: runInfo.remaining,
202
277
  });
203
278
  const rl = rlGetter?.();
204
- if (rl && (rl.utilization > 0 || rl.windows.size > 0)) {
205
- const rlBarW = Math.min(30, w - 40);
206
- const renderBar = (pct, label) => {
207
- const f = Math.round(pct * rlBarW);
208
- let barStr = "";
209
- for (let i = 0; i < rlBarW; i++) {
210
- if (i < f)
211
- barStr += pct > 0.9 ? chalk.red("\u2588") : pct > 0.75 ? chalk.yellow("\u2588") : chalk.blue("\u2588");
212
- else
213
- barStr += chalk.gray("\u2591");
214
- }
215
- let lbl = `${Math.round(pct * 100)}% used`;
216
- if (rl.isUsingOverage)
217
- lbl += chalk.red(" [EXTRA USAGE]");
218
- if (rl.resetsAt && rl.resetsAt > Date.now()) {
219
- const waitSec = Math.ceil((rl.resetsAt - Date.now()) / 1000);
220
- const mm = Math.floor(waitSec / 60), ss = waitSec % 60;
221
- lbl = chalk.red(`Waiting for reset ${mm > 0 ? `${mm}m ${ss}s` : `${ss}s`}`);
222
- }
223
- const prefix = label ? chalk.dim(label.padEnd(6)) : chalk.dim("Usage ");
224
- out.push(` ${prefix}${barStr} ${lbl}`);
225
- };
226
- if (rl.windows.size > 1) {
227
- const wins = Array.from(rl.windows.values());
228
- const idx = Math.floor(Date.now() / 3000) % wins.length;
229
- renderBar(wins[idx].utilization, wins[idx].type.replace(/_/g, " ").slice(0, 5));
230
- }
231
- else {
232
- renderBar(rl.utilization);
279
+ if (rl && (rl.utilization > 0 || rl.windows.size > 0))
280
+ renderSteeringUsageBar(out, w, rl);
281
+ out.push("");
282
+ const ctx = data.context;
283
+ if (ctx?.objective) {
284
+ const obj = ctx.objective.replace(/\s+/g, " ").trim();
285
+ out.push(` ${chalk.bold.white("Objective")} ${chalk.dim(truncate(obj, w - 15))}`);
286
+ out.push("");
287
+ }
288
+ if (ctx?.lastWave && ctx.lastWave.tasks.length > 0) {
289
+ renderLastWave(out, w, ctx.lastWave);
290
+ out.push("");
291
+ }
292
+ if (ctx?.status) {
293
+ renderStatusBlock(out, w, ctx.status);
294
+ out.push("");
295
+ }
296
+ section(out, w, "Planner activity");
297
+ const events = data.events.slice(-10);
298
+ if (events.length === 0) {
299
+ out.push(chalk.dim(" (waiting for planner\u2026)"));
300
+ }
301
+ else {
302
+ for (const e of events) {
303
+ const t = new Date(e.time).toLocaleTimeString("en", { hour12: false });
304
+ out.push(chalk.gray(` ${t} `) + chalk.magenta("[plan] ") + colorEvent(truncate(e.text, w - 22)));
233
305
  }
234
306
  }
235
307
  out.push("");
236
- out.push(chalk.gray(" " + "\u2500".repeat(Math.min(w - 4, 60))));
237
- const clean = steeringText.replace(/\n/g, " ");
238
- const maxTextW = w - 8;
239
- out.push(` ${chalk.cyan("\u25C6")} ${clean.length > maxTextW ? clean.slice(0, maxTextW - 1) + "\u2026" : clean}`);
308
+ const liveClean = data.statusLine.replace(/\n/g, " ");
309
+ out.push(` ${chalk.cyan("\u25B6")} ${chalk.dim(truncate(liveClean, w - 6))}`);
240
310
  out.push("");
241
- if (showHotkeys)
242
- out.push(chalk.dim(" [b] budget [q] stop"));
311
+ if (showHotkeys) {
312
+ const pending = runInfo?.pendingSteer ?? 0;
313
+ const chip = pending > 0 ? chalk.cyan(` \u270E ${pending} steer queued`) : "";
314
+ out.push(chalk.dim(" [b] budget [s] steer [q] stop") + chip);
315
+ }
243
316
  out.push("");
244
317
  return out.join("\n");
245
318
  }
package/dist/run.js CHANGED
@@ -4,12 +4,12 @@ import { execSync } from "child_process";
4
4
  import chalk from "chalk";
5
5
  import { Swarm } from "./swarm.js";
6
6
  import { steerWave } from "./steering.js";
7
- import { getTotalPlannerCost, getPlannerRateLimitInfo } from "./planner-query.js";
7
+ import { getTotalPlannerCost, getPlannerRateLimitInfo, runPlannerQuery } from "./planner-query.js";
8
8
  import { RunDisplay } from "./ui.js";
9
9
  import { renderSummary } from "./render.js";
10
10
  import { fmtTokens } from "./render.js";
11
11
  import { isAuthError } from "./cli.js";
12
- import { readRunMemory, writeStatus, writeGoalUpdate, saveRunState, saveWaveSession, loadWaveHistory, recordBranches, archiveMilestone, } from "./state.js";
12
+ import { readRunMemory, writeStatus, writeGoalUpdate, saveRunState, saveWaveSession, loadWaveHistory, recordBranches, archiveMilestone, writeSteerInbox, consumeSteerInbox, countSteerInbox, } from "./state.js";
13
13
  export async function executeRun(cfg) {
14
14
  const restore = () => { try {
15
15
  process.stdout.write("\x1B[?25h\n");
@@ -71,13 +71,79 @@ export async function executeRun(cfg) {
71
71
  accIn, accOut, accCost, accCompleted, accFailed,
72
72
  sessionsBudget: cfg.budget, waveNum, remaining,
73
73
  model: workerModel, startedAt: cfg.runStartedAt,
74
+ pendingSteer: countSteerInbox(runDir),
74
75
  };
75
- const display = new RunDisplay(runInfoRef, liveConfig);
76
+ let display;
77
+ const onSteer = (text) => {
78
+ try {
79
+ writeSteerInbox(runDir, text);
80
+ runInfoRef.pendingSteer = countSteerInbox(runDir);
81
+ if (currentSwarm)
82
+ currentSwarm.log(-1, `Steer queued: ${text.slice(0, 80)}`);
83
+ }
84
+ catch { }
85
+ };
86
+ let askInFlight = false;
87
+ const onAsk = (question) => {
88
+ if (askInFlight)
89
+ return;
90
+ askInFlight = true;
91
+ display.setAskBusy(true);
92
+ display.setAsk({ question, answer: "", streaming: true });
93
+ void (async () => {
94
+ const plannerCostBefore = getTotalPlannerCost();
95
+ try {
96
+ const memory = readRunMemory(runDir, previousKnowledge || undefined);
97
+ const cap = (s, max) => s && s.length > max ? s.slice(0, max) + "\n...(truncated)" : (s || "");
98
+ const memBlob = [
99
+ objective ? `Objective: ${objective}` : "",
100
+ memory.goal ? `Goal:\n${cap(memory.goal, 1500)}` : "",
101
+ memory.status ? `Current status:\n${cap(memory.status, 2000)}` : "",
102
+ memory.verifications ? `Latest verification:\n${cap(memory.verifications, 1500)}` : "",
103
+ memory.reflections ? `Latest reflections:\n${cap(memory.reflections, 1500)}` : "",
104
+ waveHistory.length ? `Waves completed: ${waveHistory.length}` : "",
105
+ ].filter(Boolean).join("\n\n");
106
+ const prompt = `You are answering a user question about an in-progress autonomous agent run. Use the context below; read files in the repo if needed. Answer concisely (a few sentences) and cite files or waves when relevant.\n\n${memBlob}\n\n---\nUser question: ${question}`;
107
+ const answer = await runPlannerQuery(prompt, { cwd, model: plannerModel, permissionMode }, () => { });
108
+ accCost += getTotalPlannerCost() - plannerCostBefore;
109
+ syncRunInfo();
110
+ display.setAsk({ question, answer: answer.trim() || "(no answer)", streaming: false });
111
+ }
112
+ catch (err) {
113
+ accCost += getTotalPlannerCost() - plannerCostBefore;
114
+ syncRunInfo();
115
+ display.setAsk({ question, answer: "", streaming: false, error: err?.message?.slice(0, 200) || "ask failed" });
116
+ }
117
+ finally {
118
+ askInFlight = false;
119
+ display.setAskBusy(false);
120
+ }
121
+ })();
122
+ };
123
+ display = new RunDisplay(runInfoRef, liveConfig, { onSteer, onAsk });
76
124
  const rlGetter = () => {
77
125
  const rl = getPlannerRateLimitInfo();
78
126
  return { utilization: rl.utilization, isUsingOverage: rl.isUsingOverage, windows: rl.windows, resetsAt: rl.resetsAt };
79
127
  };
80
128
  const syncRunInfo = () => Object.assign(runInfoRef, { accIn, accOut, accCost, accCompleted, accFailed, waveNum, remaining });
129
+ const buildSteeringContext = () => {
130
+ let status;
131
+ try {
132
+ status = readFileSync(join(runDir, "status.md"), "utf-8");
133
+ }
134
+ catch { }
135
+ return {
136
+ objective: objective || undefined,
137
+ status,
138
+ lastWave: waveHistory[waveHistory.length - 1],
139
+ };
140
+ };
141
+ const steeringLog = (text, kind) => {
142
+ if (kind === "event")
143
+ display.appendSteeringEvent(text);
144
+ else
145
+ display.updateSteeringStatus(text);
146
+ };
81
147
  // For flex + branch strategy: create one target branch
82
148
  let runBranch;
83
149
  let originalRef;
@@ -119,7 +185,10 @@ export async function executeRun(cfg) {
119
185
  const plannerCostBefore = getTotalPlannerCost();
120
186
  try {
121
187
  const memory = readRunMemory(runDir, previousKnowledge || undefined);
122
- const steer = await steerWave(objective, waveHistory, remaining, cwd, plannerModel, workerModel, permissionMode, concurrency, (text) => display.updateText(text), memory);
188
+ const appliedGuidance = memory.userGuidance;
189
+ if (appliedGuidance)
190
+ display.appendSteeringEvent(`User directives applied: ${appliedGuidance.slice(0, 80)}`);
191
+ const steer = await steerWave(objective, waveHistory, remaining, cwd, plannerModel, workerModel, permissionMode, concurrency, steeringLog, memory);
123
192
  accCost += getTotalPlannerCost() - plannerCostBefore;
124
193
  syncRunInfo();
125
194
  if (steer.statusUpdate)
@@ -131,13 +200,18 @@ export async function executeRun(cfg) {
131
200
  writeFileSync(join(steerDir, `wave-${waveNum}-attempt-${steerAttempts}.json`), JSON.stringify({
132
201
  done: steer.done, reasoning: steer.reasoning,
133
202
  taskCount: steer.tasks.length, statusUpdate: steer.statusUpdate, goalUpdate: steer.goalUpdate,
203
+ appliedGuidance: appliedGuidance || undefined,
134
204
  }, null, 2), "utf-8");
205
+ if (appliedGuidance) {
206
+ consumeSteerInbox(runDir, waveNum);
207
+ runInfoRef.pendingSteer = countSteerInbox(runDir);
208
+ }
135
209
  if (waveHistory.length > 0 && waveHistory.length % 5 === 0)
136
210
  archiveMilestone(runDir, waveNum);
137
211
  if (steer.done || steer.tasks.length === 0) {
138
212
  const hasVerification = waveHistory.some(w => w.tasks.some(t => t.prompt.toLowerCase().includes("verif")));
139
213
  if (!hasVerification && remaining >= 1) {
140
- display.updateText(`Done blocked — auto-composing verification wave`);
214
+ display.appendSteeringEvent("Done blocked — auto-composing verification wave");
141
215
  currentTasks = [{
142
216
  id: "verify-0",
143
217
  prompt: `## Verification: Build, run, and test the application end-to-end\n\nYou are the final gatekeeper before this run is marked complete. The steerer believes the objective is done. Your job: prove it or disprove it.\n\n1. Run the build (npm run build, or whatever this project uses). Report ALL errors.\n2. Start the dev server. If a port is taken, try another. If a dependency is missing, install it.\n3. Navigate key flows as a real user would. Check that the main features work.\n4. Write your findings to .claude-overnight/latest/verifications/final-verify.md\n\nBe relentless. Do not give up if the first approach fails. Search the codebase for dev login routes, test tokens, seed users, env vars, CLI auth commands, or any bypass.`,
@@ -159,7 +233,7 @@ export async function executeRun(cfg) {
159
233
  catch (err) {
160
234
  accCost += getTotalPlannerCost() - plannerCostBefore;
161
235
  if (steerAttempts < 3) {
162
- display.updateText(`Steering failed (attempt ${steerAttempts}/3) — retrying...`);
236
+ display.appendSteeringEvent(`Steering failed (attempt ${steerAttempts}/3) — retrying...`);
163
237
  continue;
164
238
  }
165
239
  display.stop();
@@ -172,7 +246,7 @@ export async function executeRun(cfg) {
172
246
  };
173
247
  // Resume: steer immediately if no queued tasks
174
248
  if (cfg.resuming && flex && currentTasks.length === 0 && remaining > 0) {
175
- display.setSteering(rlGetter);
249
+ display.setSteering(rlGetter, buildSteeringContext());
176
250
  display.start();
177
251
  await runSteering();
178
252
  }
@@ -244,7 +318,7 @@ export async function executeRun(cfg) {
244
318
  if (!flex || remaining <= 0 || swarm.aborted || swarm.cappedOut)
245
319
  break;
246
320
  syncRunInfo();
247
- display.setSteering(rlGetter);
321
+ display.setSteering(rlGetter, buildSteeringContext());
248
322
  display.resume();
249
323
  const steered = await runSteering();
250
324
  if (!steered)
package/dist/state.d.ts CHANGED
@@ -1,6 +1,14 @@
1
1
  import type { RunState, BranchRecord, AgentState, RunMemory, WaveSummary } from "./types.js";
2
2
  export declare function readMdDir(dir: string): string;
3
3
  export declare function readRunMemory(runDir: string, previousRuns?: string): RunMemory;
4
+ /** Read pending .md files in steer-inbox/ (top-level only, not processed/). */
5
+ export declare function readSteerInbox(runDir: string): string;
6
+ /** Count pending steer files without reading them. */
7
+ export declare function countSteerInbox(runDir: string): number;
8
+ /** Append a user directive to the inbox as its own timestamped file. Returns the file path. */
9
+ export declare function writeSteerInbox(runDir: string, text: string): string;
10
+ /** Move all pending .md files from steer-inbox/ into steer-inbox/processed/wave-N/. Returns moved count. */
11
+ export declare function consumeSteerInbox(runDir: string, waveNum: number): number;
4
12
  export declare function writeStatus(baseDir: string, status: string): void;
5
13
  export declare function writeGoalUpdate(baseDir: string, update: string): void;
6
14
  export declare function saveRunState(runDir: string, state: RunState): void;
package/dist/state.js CHANGED
@@ -1,4 +1,4 @@
1
- import { readFileSync, existsSync, mkdirSync, readdirSync, writeFileSync, symlinkSync, unlinkSync } from "fs";
1
+ import { readFileSync, existsSync, mkdirSync, readdirSync, writeFileSync, symlinkSync, unlinkSync, renameSync } from "fs";
2
2
  import { execSync } from "child_process";
3
3
  import { join } from "path";
4
4
  import chalk from "chalk";
@@ -31,8 +31,68 @@ export function readRunMemory(runDir, previousRuns) {
31
31
  verifications: readMdDir(join(runDir, "verifications")),
32
32
  milestones: readMdDir(join(runDir, "milestones")),
33
33
  status, goal, previousRuns,
34
+ userGuidance: readSteerInbox(runDir),
34
35
  };
35
36
  }
37
+ // ── Steer inbox (user directives queued for the next steering call) ──
38
+ /** Read pending .md files in steer-inbox/ (top-level only, not processed/). */
39
+ export function readSteerInbox(runDir) {
40
+ const dir = join(runDir, "steer-inbox");
41
+ try {
42
+ const files = readdirSync(dir).filter(f => f.endsWith(".md")).sort();
43
+ return files.map(f => {
44
+ try {
45
+ return readFileSync(join(dir, f), "utf-8").trim();
46
+ }
47
+ catch {
48
+ return "";
49
+ }
50
+ }).filter(Boolean).join("\n\n---\n\n");
51
+ }
52
+ catch {
53
+ return "";
54
+ }
55
+ }
56
+ /** Count pending steer files without reading them. */
57
+ export function countSteerInbox(runDir) {
58
+ try {
59
+ return readdirSync(join(runDir, "steer-inbox")).filter(f => f.endsWith(".md")).length;
60
+ }
61
+ catch {
62
+ return 0;
63
+ }
64
+ }
65
+ /** Append a user directive to the inbox as its own timestamped file. Returns the file path. */
66
+ export function writeSteerInbox(runDir, text) {
67
+ const dir = join(runDir, "steer-inbox");
68
+ mkdirSync(dir, { recursive: true });
69
+ const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
70
+ const rand = Math.random().toString(36).slice(2, 6);
71
+ const path = join(dir, `${ts}-${rand}.md`);
72
+ writeFileSync(path, text.trim() + "\n", "utf-8");
73
+ return path;
74
+ }
75
+ /** Move all pending .md files from steer-inbox/ into steer-inbox/processed/wave-N/. Returns moved count. */
76
+ export function consumeSteerInbox(runDir, waveNum) {
77
+ const dir = join(runDir, "steer-inbox");
78
+ let moved = 0;
79
+ try {
80
+ const files = readdirSync(dir).filter(f => f.endsWith(".md"));
81
+ if (files.length === 0)
82
+ return 0;
83
+ const processedDir = join(dir, "processed", `wave-${waveNum}`);
84
+ mkdirSync(processedDir, { recursive: true });
85
+ for (const f of files) {
86
+ try {
87
+ renameSync(join(dir, f), join(processedDir, f));
88
+ moved++;
89
+ }
90
+ catch { }
91
+ }
92
+ }
93
+ catch { }
94
+ return moved;
95
+ }
36
96
  export function writeStatus(baseDir, status) {
37
97
  writeFileSync(join(baseDir, "status.md"), status, "utf-8");
38
98
  }
@@ -1,2 +1,3 @@
1
1
  import type { PermMode, SteerResult, RunMemory, WaveSummary } from "./types.js";
2
- export declare function steerWave(objective: string, history: WaveSummary[], remainingBudget: number, cwd: string, plannerModel: string, workerModel: string, permissionMode: PermMode, concurrency: number, onLog: (text: string) => void, runMemory?: RunMemory): Promise<SteerResult>;
2
+ import { type PlannerLog } from "./planner-query.js";
3
+ export declare function steerWave(objective: string, history: WaveSummary[], remainingBudget: number, cwd: string, plannerModel: string, workerModel: string, permissionMode: PermMode, concurrency: number, onLog: PlannerLog, runMemory?: RunMemory): Promise<SteerResult>;
package/dist/steering.js CHANGED
@@ -40,8 +40,9 @@ export async function steerWave(objective, history, remainingBudget, cwd, planne
40
40
  const verificationBlock = runMemory?.verifications ? `\nVerification results (from actually running the app):\n${cap(runMemory.verifications, 3000)}\n` : "";
41
41
  const goalBlock = runMemory?.goal ? `\nNorth star — what "amazing" means:\n${runMemory.goal}\n` : "";
42
42
  const prevRunBlock = runMemory?.previousRuns ? `\nKnowledge from previous runs:\n${cap(runMemory.previousRuns, 3000)}\n` : "";
43
+ const guidanceBlock = runMemory?.userGuidance ? `\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nUSER DIRECTIVES — highest priority\nThese come directly from the user running this session. They override prior assumptions about status, goal, and next steps. Incorporate them into the wave you compose below. If they conflict with earlier decisions, the user wins. Reflect the new direction in statusUpdate so future waves remember.\n\n${cap(runMemory.userGuidance, 4000)}\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n` : "";
43
44
  const prompt = `You are the quality director for an autonomous multi-wave agent system. Your job is to push the work toward "amazing," not just "done."
44
-
45
+ ${guidanceBlock}
45
46
  Objective: ${objective}
46
47
  ${goalBlock}${statusBlock}${milestoneBlock}${prevRunBlock}
47
48
  Recent waves:
@@ -104,13 +105,14 @@ The "model" field on each task: use "worker" (${workerModel}) for implementation
104
105
  Set "noWorktree": true for verify/user-test tasks — they need the real project directory with env files, dependencies, and local config.
105
106
 
106
107
  If done: {"done": true, "reasoning": "...", "statusUpdate": "...", "tasks": []}`;
107
- onLog("Assessing...");
108
+ onLog("Assessing...", "status");
109
+ onLog(`Reading codebase — wave ${history.length + 1}`, "event");
108
110
  const resultText = await runPlannerQuery(prompt, { cwd, model: plannerModel, permissionMode, outputFormat: STEER_SCHEMA }, onLog);
109
111
  const parsed = await (async () => {
110
112
  const first = attemptJsonParse(resultText);
111
113
  if (first)
112
114
  return first;
113
- onLog(`Steering parse failed (${resultText.length} chars). Asking model to fix...`);
115
+ onLog(`Steering parse failed (${resultText.length} chars). Asking model to fix...`, "event");
114
116
  const snippet = resultText.length > 2000 ? resultText.slice(0, 1000) + "\n...\n" + resultText.slice(-800) : resultText;
115
117
  const retryText = await runPlannerQuery(`Your previous steering response could not be parsed as JSON. Here is what you returned:\n\n---\n${snippet}\n---\n\nExtract or rewrite the above as ONLY a valid JSON object with this schema: {"done":boolean,"reasoning":"...","statusUpdate":"...","tasks":[{"prompt":"..."}]}\n\nRespond with ONLY the JSON, no markdown fences, no explanation.`, { cwd, model: plannerModel, permissionMode, outputFormat: STEER_SCHEMA }, onLog);
116
118
  const retryParsed = attemptJsonParse(retryText);
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/types.d.ts CHANGED
@@ -144,6 +144,8 @@ export interface RunMemory {
144
144
  status: string;
145
145
  goal: string;
146
146
  previousRuns?: string;
147
+ /** Pending user directives from the steer inbox, consumed by the next successful steering call. */
148
+ userGuidance?: string;
147
149
  }
148
150
  /** Persisted run state for crash recovery and resume. */
149
151
  export interface RunState {
package/dist/ui.d.ts CHANGED
@@ -1,5 +1,16 @@
1
1
  import type { Swarm } from "./swarm.js";
2
- import type { RateLimitWindow } from "./types.js";
2
+ import type { RateLimitWindow, WaveSummary } from "./types.js";
3
+ /** Short-lived context the steering view renders around its live log. */
4
+ export interface SteeringContext {
5
+ objective?: string;
6
+ status?: string;
7
+ lastWave?: WaveSummary;
8
+ }
9
+ /** One scrollback line in the steering event log. */
10
+ export interface SteeringEvent {
11
+ time: number;
12
+ text: string;
13
+ }
3
14
  /** Cumulative run-level stats — mutable, updated between phases. */
4
15
  export interface RunInfo {
5
16
  accIn: number;
@@ -12,6 +23,8 @@ export interface RunInfo {
12
23
  remaining: number;
13
24
  model?: string;
14
25
  startedAt: number;
26
+ /** Number of pending directives in the steer inbox; displayed as a chip in the hotkey row. */
27
+ pendingSteer?: number;
15
28
  }
16
29
  /** Mutable config that can be changed live during execution. */
17
30
  export interface LiveConfig {
@@ -19,6 +32,13 @@ export interface LiveConfig {
19
32
  usageCap: number | undefined;
20
33
  dirty: boolean;
21
34
  }
35
+ /** State of an in-flight or recently-completed ask side query. */
36
+ export interface AskState {
37
+ question: string;
38
+ answer: string;
39
+ streaming: boolean;
40
+ error?: string;
41
+ }
22
42
  type RLGetter = () => {
23
43
  utilization: number;
24
44
  isUsingOverage: boolean;
@@ -29,7 +49,10 @@ export declare class RunDisplay {
29
49
  readonly runInfo: RunInfo;
30
50
  private liveConfig?;
31
51
  private swarm?;
32
- private steeringText?;
52
+ private steeringActive;
53
+ private steeringStatusLine;
54
+ private steeringEvents;
55
+ private steeringContext?;
33
56
  private rlGetter?;
34
57
  private interval?;
35
58
  private keyHandler?;
@@ -39,16 +62,34 @@ export declare class RunDisplay {
39
62
  private readonly isTTY;
40
63
  private lastSeq;
41
64
  private lastCompleted;
42
- constructor(runInfo: RunInfo, liveConfig?: LiveConfig);
65
+ private askState?;
66
+ private askBusy;
67
+ private onSteer?;
68
+ private onAsk?;
69
+ constructor(runInfo: RunInfo, liveConfig?: LiveConfig, callbacks?: {
70
+ onSteer?: (text: string) => void;
71
+ onAsk?: (text: string) => void;
72
+ });
73
+ /** Replace the ask state. Called by run.ts as the side query streams and completes. */
74
+ setAsk(state: AskState | undefined): void;
75
+ /** Signal to the UI whether an ask is in progress (prevents duplicate firings). */
76
+ setAskBusy(busy: boolean): void;
43
77
  start(): void;
44
78
  setWave(swarm: Swarm): void;
45
- setSteering(rlGetter?: RLGetter): void;
79
+ setSteering(rlGetter?: RLGetter, ctx?: SteeringContext): void;
80
+ /** Replace the single live status line (ticker heartbeat). */
81
+ updateSteeringStatus(text: string): void;
82
+ /** Append a discrete, persistent line to the steering scrollback. */
83
+ appendSteeringEvent(text: string): void;
84
+ /** Backwards-compat alias — treats input as the current status line. */
46
85
  updateText(text: string): void;
47
86
  pause(): void;
48
87
  resume(): void;
49
88
  stop(): void;
50
89
  private resumeInterval;
51
90
  private render;
91
+ private renderInputPrompt;
92
+ private renderAskPanel;
52
93
  private hasHotkeys;
53
94
  private setupHotkeys;
54
95
  private plainTick;
package/dist/ui.js CHANGED
@@ -1,10 +1,15 @@
1
1
  import chalk from "chalk";
2
2
  import { renderFrame, renderSteeringFrame } from "./render.js";
3
+ const MAX_STEERING_EVENTS = 60;
4
+ const MAX_INPUT_LEN = 600;
3
5
  export class RunDisplay {
4
6
  runInfo;
5
7
  liveConfig;
6
8
  swarm;
7
- steeringText;
9
+ steeringActive = false;
10
+ steeringStatusLine = "Assessing...";
11
+ steeringEvents = [];
12
+ steeringContext;
8
13
  rlGetter;
9
14
  interval;
10
15
  keyHandler;
@@ -14,11 +19,21 @@ export class RunDisplay {
14
19
  isTTY;
15
20
  lastSeq = 0;
16
21
  lastCompleted = -1;
17
- constructor(runInfo, liveConfig) {
22
+ askState;
23
+ askBusy = false;
24
+ onSteer;
25
+ onAsk;
26
+ constructor(runInfo, liveConfig, callbacks) {
18
27
  this.runInfo = runInfo;
19
28
  this.liveConfig = liveConfig;
29
+ this.onSteer = callbacks?.onSteer;
30
+ this.onAsk = callbacks?.onAsk;
20
31
  this.isTTY = !!process.stdout.isTTY;
21
32
  }
33
+ /** Replace the ask state. Called by run.ts as the side query streams and completes. */
34
+ setAsk(state) { this.askState = state; }
35
+ /** Signal to the UI whether an ask is in progress (prevents duplicate firings). */
36
+ setAskBusy(busy) { this.askBusy = busy; }
22
37
  start() {
23
38
  if (this.started)
24
39
  return;
@@ -28,17 +43,29 @@ export class RunDisplay {
28
43
  }
29
44
  setWave(swarm) {
30
45
  this.swarm = swarm;
31
- this.steeringText = undefined;
46
+ this.steeringActive = false;
32
47
  this.rlGetter = undefined;
33
48
  this.lastSeq = 0;
34
49
  this.lastCompleted = -1;
35
50
  }
36
- setSteering(rlGetter) {
51
+ setSteering(rlGetter, ctx) {
37
52
  this.swarm = undefined;
38
- this.steeringText = "Assessing...";
53
+ this.steeringActive = true;
54
+ this.steeringStatusLine = "Assessing...";
55
+ this.steeringEvents = [];
56
+ this.steeringContext = ctx;
39
57
  this.rlGetter = rlGetter;
40
58
  }
41
- updateText(text) { this.steeringText = text; }
59
+ /** Replace the single live status line (ticker heartbeat). */
60
+ updateSteeringStatus(text) { this.steeringStatusLine = text; }
61
+ /** Append a discrete, persistent line to the steering scrollback. */
62
+ appendSteeringEvent(text) {
63
+ this.steeringEvents.push({ time: Date.now(), text });
64
+ if (this.steeringEvents.length > MAX_STEERING_EVENTS)
65
+ this.steeringEvents.shift();
66
+ }
67
+ /** Backwards-compat alias — treats input as the current status line. */
68
+ updateText(text) { this.updateSteeringStatus(text); }
42
69
  pause() {
43
70
  if (this.interval) {
44
71
  clearInterval(this.interval);
@@ -96,23 +123,61 @@ export class RunDisplay {
96
123
  }, 250);
97
124
  }
98
125
  render() {
126
+ let frame = "";
99
127
  if (this.swarm) {
100
- let frame = renderFrame(this.swarm, this.hasHotkeys(), this.runInfo);
101
- if (this.inputMode !== "none") {
102
- const label = this.inputMode === "budget" ? "New budget (remaining sessions)" : "New usage cap (0-100%)";
103
- frame += `\n ${chalk.cyan(">")} ${label}: ${this.inputBuf}\u2588`;
104
- }
105
- return frame;
128
+ frame = renderFrame(this.swarm, this.hasHotkeys(), this.runInfo);
106
129
  }
107
- if (this.steeringText != null) {
108
- let frame = renderSteeringFrame(this.runInfo, this.steeringText, this.hasHotkeys(), this.rlGetter);
109
- if (this.inputMode === "budget") {
110
- frame += `\n ${chalk.cyan(">")} New budget (remaining sessions): ${this.inputBuf}\u2588`;
111
- }
112
- return frame;
130
+ else if (this.steeringActive) {
131
+ frame = renderSteeringFrame(this.runInfo, {
132
+ statusLine: this.steeringStatusLine,
133
+ events: this.steeringEvents,
134
+ context: this.steeringContext,
135
+ }, this.hasHotkeys(), this.rlGetter);
136
+ }
137
+ else {
138
+ return "";
139
+ }
140
+ frame += this.renderInputPrompt();
141
+ frame += this.renderAskPanel();
142
+ return frame;
143
+ }
144
+ renderInputPrompt() {
145
+ if (this.inputMode === "none")
146
+ return "";
147
+ if (this.inputMode === "budget") {
148
+ return `\n ${chalk.cyan(">")} New budget (remaining sessions): ${this.inputBuf}\u2588`;
149
+ }
150
+ if (this.inputMode === "threshold") {
151
+ return `\n ${chalk.cyan(">")} New usage cap (0-100%): ${this.inputBuf}\u2588`;
152
+ }
153
+ if (this.inputMode === "steer") {
154
+ return `\n ${chalk.cyan(">")} ${chalk.bold("Steer next wave")} ${chalk.dim("(Enter to queue, Esc to cancel)")}\n ${this.inputBuf}\u2588`;
155
+ }
156
+ if (this.inputMode === "ask") {
157
+ return `\n ${chalk.cyan(">")} ${chalk.bold("Ask the planner")} ${chalk.dim("(Enter to send, Esc to cancel)")}\n ${this.inputBuf}\u2588`;
113
158
  }
114
159
  return "";
115
160
  }
161
+ renderAskPanel() {
162
+ const a = this.askState;
163
+ if (!a)
164
+ return "";
165
+ const out = ["", chalk.gray(" \u2500\u2500\u2500 Ask " + "\u2500".repeat(40))];
166
+ out.push(` ${chalk.bold.cyan("Q:")} ${a.question}`);
167
+ if (a.error) {
168
+ out.push(` ${chalk.red("A:")} ${chalk.red(a.error)}`);
169
+ }
170
+ else if (a.streaming) {
171
+ out.push(` ${chalk.dim("A: " + (a.answer || "thinking..."))}`);
172
+ }
173
+ else {
174
+ const lines = a.answer.split("\n").slice(0, 20);
175
+ out.push(` ${chalk.bold.green("A:")} ${lines[0] || ""}`);
176
+ for (const ln of lines.slice(1))
177
+ out.push(` ${ln}`);
178
+ }
179
+ return "\n" + out.join("\n");
180
+ }
116
181
  hasHotkeys() {
117
182
  return !!this.liveConfig && !!process.stdin.isTTY;
118
183
  }
@@ -129,7 +194,7 @@ export class RunDisplay {
129
194
  const lc = this.liveConfig;
130
195
  this.keyHandler = (buf) => {
131
196
  const s = buf.toString();
132
- if (this.inputMode !== "none") {
197
+ if (this.inputMode === "budget" || this.inputMode === "threshold") {
133
198
  if (s === "\r" || s === "\n") {
134
199
  const val = parseFloat(this.inputBuf);
135
200
  if (this.inputMode === "budget" && !isNaN(val) && val > 0) {
@@ -160,6 +225,37 @@ export class RunDisplay {
160
225
  }
161
226
  return;
162
227
  }
228
+ if (this.inputMode === "steer" || this.inputMode === "ask") {
229
+ for (const ch of s) {
230
+ if (ch === "\r" || ch === "\n") {
231
+ const text = this.inputBuf.trim();
232
+ const wasAsk = this.inputMode === "ask";
233
+ this.inputMode = "none";
234
+ this.inputBuf = "";
235
+ if (text) {
236
+ if (wasAsk)
237
+ this.onAsk?.(text);
238
+ else
239
+ this.onSteer?.(text);
240
+ }
241
+ return;
242
+ }
243
+ if (ch === "\x1B" || ch === "\x03") {
244
+ this.inputMode = "none";
245
+ this.inputBuf = "";
246
+ return;
247
+ }
248
+ if (ch === "\x7F" || ch === "\b") {
249
+ this.inputBuf = this.inputBuf.slice(0, -1);
250
+ continue;
251
+ }
252
+ const code = ch.charCodeAt(0);
253
+ if (code >= 0x20 && code <= 0x7E && this.inputBuf.length < MAX_INPUT_LEN) {
254
+ this.inputBuf += ch;
255
+ }
256
+ }
257
+ return;
258
+ }
163
259
  if (s === "b" || s === "B") {
164
260
  this.inputMode = "budget";
165
261
  this.inputBuf = "";
@@ -170,6 +266,17 @@ export class RunDisplay {
170
266
  this.inputBuf = "";
171
267
  }
172
268
  }
269
+ else if ((s === "f" || s === "F") && this.swarm && this.swarm.failed > 0 && this.swarm.active > 0) {
270
+ this.swarm.requeueFailed();
271
+ }
272
+ else if ((s === "s" || s === "S") && this.onSteer) {
273
+ this.inputMode = "steer";
274
+ this.inputBuf = "";
275
+ }
276
+ else if (s === "?" && this.onAsk && this.swarm && !this.askBusy) {
277
+ this.inputMode = "ask";
278
+ this.inputBuf = "";
279
+ }
173
280
  else if (s === "q" || s === "Q" || s === "\x03") {
174
281
  if (this.swarm) {
175
282
  if (this.swarm.aborted)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-overnight",
3
- "version": "1.9.1",
3
+ "version": "1.11.0",
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": {