claude-overnight 1.11.4 → 1.11.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,6 +14,15 @@ npm install -g claude-overnight
14
14
 
15
15
  Requires Node.js >= 20 and Claude authentication (`claude auth login`, or set `ANTHROPIC_API_KEY`).
16
16
 
17
+ ### Claude Code plugin
18
+
19
+ This repo also ships a Claude Code plugin so any Claude instance (inside this repo or any other) knows how to use, inspect, and resume `claude-overnight` runs:
20
+
21
+ ```
22
+ /plugin marketplace add Fornace/claude-overnight
23
+ /plugin install claude-overnight
24
+ ```
25
+
17
26
  ## Quick start
18
27
 
19
28
  ```bash
@@ -131,7 +140,9 @@ If the thinking phase succeeds but orchestration crashes, the next run detects t
131
140
 
132
141
  **Knowledge carries forward** — new runs inherit knowledge from completed previous runs. Thinking agents and steering see what past runs built. Run 2 knows run 1 already built the auth system.
133
142
 
134
- Add `.claude-overnight` to your `.gitignore`.
143
+ Add `.claude-overnight/` to your `.gitignore` (with the trailing slash — see below).
144
+
145
+ A separate, tiny `claude-overnight.log.md` is also written at the repo root on every run. It's human-readable, append-only, one block per run (objective, start/finish, cost, outcome, branch), and is designed to be **committed** — so even after `.claude-overnight/` is cleaned up you can still recover which prompt produced which commits. Use `.claude-overnight/` (with trailing slash) in your gitignore so this file isn't matched by accident.
135
146
 
136
147
  ## Other usage modes
137
148
 
package/dist/render.js CHANGED
@@ -80,10 +80,9 @@ function renderUsageBars(out, w, swarm) {
80
80
  }
81
81
  let label = `${Math.round(pct * 100)}% used`;
82
82
  if (swarm.cappedOut) {
83
- if (swarm.isUsingOverage && !swarm.allowExtraUsage)
84
- label = chalk.red("Extra usage blocked \u2014 stopping");
85
- else
86
- label = chalk.yellow(`Capped at ${capFrac != null ? Math.round(capFrac * 100) : 100}% \u2014 finishing active`);
83
+ label = swarm.extraUsageBudget != null
84
+ ? chalk.red(`Budget $${swarm.extraUsageBudget} exhausted \u2014 finishing active`)
85
+ : chalk.yellow(`Capped at ${capFrac != null ? Math.round(capFrac * 100) : 100}% \u2014 finishing active`);
87
86
  }
88
87
  else if (swarm.rateLimitPaused > 0) {
89
88
  label = chalk.yellow(`Cooling down \u2014 ${swarm.rateLimitPaused} worker(s) waiting`);
@@ -94,7 +93,7 @@ function renderUsageBars(out, w, swarm) {
94
93
  label = chalk.red(`Waiting for reset ${mm > 0 ? `${mm}m ${ss}s` : `${ss}s`}`);
95
94
  }
96
95
  if (swarm.isUsingOverage && !swarm.cappedOut)
97
- label += chalk.red(" [EXTRA USAGE]");
96
+ label += chalk.red(" [OVERAGE]");
98
97
  const prefix = windowLabel ? chalk.dim(windowLabel.padEnd(6)) : chalk.dim("Usage ");
99
98
  out.push(` ${prefix}${barStr} ${label}`);
100
99
  };
package/dist/run.js CHANGED
@@ -9,7 +9,7 @@ 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, writeSteerInbox, consumeSteerInbox, countSteerInbox, } from "./state.js";
12
+ import { readRunMemory, writeStatus, writeGoalUpdate, saveRunState, saveWaveSession, loadWaveHistory, recordBranches, archiveMilestone, writeSteerInbox, consumeSteerInbox, countSteerInbox, appendOvernightLogStart, updateOvernightLogEnd, } from "./state.js";
13
13
  export async function executeRun(cfg) {
14
14
  const restore = () => { try {
15
15
  process.stdout.write("\x1B[?25h\n");
@@ -160,6 +160,20 @@ export async function executeRun(cfg) {
160
160
  catch { }
161
161
  }
162
162
  const waveMerge = (flex && runBranch) ? "yolo" : mergeStrategy;
163
+ const runId = runDir.split(/[/\\]/).pop() ?? "";
164
+ if (!cfg.resuming) {
165
+ try {
166
+ appendOvernightLogStart(cwd, runId, {
167
+ objective: objective || "",
168
+ model: workerModel,
169
+ budget: cfg.budget,
170
+ flex,
171
+ usageCap,
172
+ branch: runBranch,
173
+ });
174
+ }
175
+ catch { }
176
+ }
163
177
  let stopping = false;
164
178
  const gracefulStop = () => {
165
179
  if (stopping) {
@@ -369,6 +383,17 @@ export async function executeRun(cfg) {
369
383
  }
370
384
  catch { }
371
385
  }
386
+ try {
387
+ updateOvernightLogEnd(cwd, runId, {
388
+ cost: accCost,
389
+ completed: accCompleted,
390
+ failed: accFailed,
391
+ waves: waveNum + 1,
392
+ phase: finalPhase,
393
+ elapsedSec: Math.round((Date.now() - cfg.runStartedAt) / 1000),
394
+ });
395
+ }
396
+ catch { }
372
397
  if (runBranch && originalRef) {
373
398
  try {
374
399
  execSync(`git checkout "${originalRef}"`, { cwd, encoding: "utf-8", stdio: "pipe" });
@@ -390,7 +415,7 @@ export async function executeRun(cfg) {
390
415
  else if (remaining <= 0)
391
416
  console.log(chalk.bold.yellow(` CLAUDE OVERNIGHT — BUDGET EXHAUSTED`));
392
417
  else if (lastCapped)
393
- console.log(chalk.bold.yellow(` CLAUDE OVERNIGHT — RATE LIMITED`));
418
+ console.log(chalk.bold.yellow(` CLAUDE OVERNIGHT — BUDGET EXHAUSTED`));
394
419
  else if (stopping || lastAborted)
395
420
  console.log(chalk.bold.yellow(` CLAUDE OVERNIGHT — INTERRUPTED`));
396
421
  else
@@ -406,7 +431,7 @@ export async function executeRun(cfg) {
406
431
  for (const [k1, v1, k2, v2] of statRows)
407
432
  console.log(` ${k1} ${v1.padEnd(20)} ${k2} ${v2}`);
408
433
  if (lastCapped)
409
- console.log(` ${chalk.yellow(`Capped at ${usageCap != null ? Math.round(usageCap * 100) : 100}%`)}`);
434
+ console.log(` ${chalk.yellow(`Overage budget exhausted`)}`);
410
435
  console.log("");
411
436
  const statusFile = join(runDir, "status.md");
412
437
  if (existsSync(statusFile)) {
package/dist/state.d.ts CHANGED
@@ -11,6 +11,24 @@ export declare function writeSteerInbox(runDir: string, text: string): string;
11
11
  export declare function consumeSteerInbox(runDir: string, waveNum: number): number;
12
12
  export declare function writeStatus(baseDir: string, status: string): void;
13
13
  export declare function writeGoalUpdate(baseDir: string, update: string): void;
14
+ export interface OvernightLogStart {
15
+ objective: string;
16
+ model: string;
17
+ budget: number;
18
+ flex: boolean;
19
+ usageCap?: number;
20
+ branch?: string;
21
+ }
22
+ export interface OvernightLogEnd {
23
+ cost: number;
24
+ completed: number;
25
+ failed: number;
26
+ waves: number;
27
+ phase: string;
28
+ elapsedSec: number;
29
+ }
30
+ export declare function appendOvernightLogStart(cwd: string, runId: string, meta: OvernightLogStart): void;
31
+ export declare function updateOvernightLogEnd(cwd: string, runId: string, meta: OvernightLogEnd): void;
14
32
  export declare function saveRunState(runDir: string, state: RunState): void;
15
33
  export declare function loadRunState(runDir: string): RunState | null;
16
34
  export declare function findIncompleteRuns(rootDir: string, filterCwd: string): {
package/dist/state.js CHANGED
@@ -109,6 +109,65 @@ export function writeGoalUpdate(baseDir, update) {
109
109
  const trimmed = full.length > 4000 ? full.slice(0, 1000) + "\n\n...\n\n" + full.slice(-3000) : full;
110
110
  writeFileSync(goalPath, trimmed, "utf-8");
111
111
  }
112
+ // ── Durable run log (claude-overnight.log.md, committed) ──
113
+ // Tiny human-readable record per run so the objective survives even after
114
+ // .claude-overnight/ is cleaned up. Append-only friendly: each run's block
115
+ // is keyed by runId (the run dir basename) so concurrent runs on different
116
+ // machines don't collide.
117
+ function escapeRegExp(s) {
118
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
119
+ }
120
+ export function appendOvernightLogStart(cwd, runId, meta) {
121
+ const path = join(cwd, "claude-overnight.log.md");
122
+ const startedAt = new Date().toISOString().replace("T", " ").slice(0, 19) + " UTC";
123
+ const capStr = meta.usageCap != null ? ` · **Cap:** ${meta.usageCap}%` : "";
124
+ const branchLine = meta.branch ? `\n- **Branch:** ${meta.branch}` : "";
125
+ const block = [
126
+ `## ${runId}`,
127
+ `- **Objective:** ${meta.objective || "(none)"}`,
128
+ `- **Started:** ${startedAt}`,
129
+ `- **Model:** ${meta.model} · **Budget:** ${meta.budget} · **Flex:** ${meta.flex ? "yes" : "no"}${capStr}${branchLine}`,
130
+ `- **Status:** running`,
131
+ "",
132
+ "",
133
+ ].join("\n");
134
+ let existing = "";
135
+ try {
136
+ existing = readFileSync(path, "utf-8");
137
+ }
138
+ catch { }
139
+ const header = existing ? "" : "# claude-overnight — run history\n\n";
140
+ writeFileSync(path, header + existing + block, "utf-8");
141
+ }
142
+ export function updateOvernightLogEnd(cwd, runId, meta) {
143
+ const path = join(cwd, "claude-overnight.log.md");
144
+ let existing = "";
145
+ try {
146
+ existing = readFileSync(path, "utf-8");
147
+ }
148
+ catch {
149
+ return;
150
+ }
151
+ const finishedAt = new Date().toISOString().replace("T", " ").slice(0, 19) + " UTC";
152
+ const sec = meta.elapsedSec;
153
+ const elapsed = sec < 60 ? `${sec}s` : sec < 3600 ? `${Math.floor(sec / 60)}m ${sec % 60}s` : `${Math.floor(sec / 3600)}h ${Math.floor((sec % 3600) / 60)}m`;
154
+ const outcome = meta.phase === "done" ? "✓ done" : meta.phase === "capped" ? "⊘ capped" : "⊘ stopped";
155
+ const endLines = [
156
+ `- **Finished:** ${finishedAt} (${elapsed})`,
157
+ `- **Cost:** $${meta.cost.toFixed(2)}`,
158
+ `- **Tasks:** ${meta.completed} done${meta.failed > 0 ? ` / ${meta.failed} failed` : ""} · **Waves:** ${meta.waves}`,
159
+ `- **Status:** ${outcome}`,
160
+ ].join("\n");
161
+ const re = new RegExp(`(## ${escapeRegExp(runId)}\\n(?:(?!\\n## )[\\s\\S])*?)- \\*\\*Status:\\*\\* running`);
162
+ if (re.test(existing)) {
163
+ writeFileSync(path, existing.replace(re, `$1${endLines}`), "utf-8");
164
+ }
165
+ else {
166
+ const header = existing ? "" : "# claude-overnight — run history\n\n";
167
+ const block = `## ${runId}\n${endLines}\n\n`;
168
+ writeFileSync(path, header + existing + block, "utf-8");
169
+ }
170
+ }
112
171
  // ── Run state persistence ──
113
172
  export function saveRunState(runDir, state) {
114
173
  mkdirSync(runDir, { recursive: true });
package/dist/swarm.d.ts CHANGED
@@ -40,7 +40,6 @@ export declare class Swarm {
40
40
  rateLimitWindows: Map<string, RateLimitWindow>;
41
41
  rateLimitPaused: number;
42
42
  isUsingOverage: boolean;
43
- overageDisabledReason?: string;
44
43
  overageCostUsd: number;
45
44
  private queue;
46
45
  private config;
package/dist/swarm.js CHANGED
@@ -33,7 +33,6 @@ export class Swarm {
33
33
  rateLimitWindows = new Map();
34
34
  rateLimitPaused = 0;
35
35
  isUsingOverage = false;
36
- overageDisabledReason;
37
36
  overageCostUsd = 0;
38
37
  queue;
39
38
  config;
@@ -180,43 +179,43 @@ export class Swarm {
180
179
  async throttle() {
181
180
  if (this.cappedOut)
182
181
  return;
183
- if (this.isUsingOverage && !this.allowExtraUsage) {
184
- this.capForOverage(`Extra usage detected but not allowed — stopping dispatch`);
185
- return;
186
- }
182
+ // Hard stop: overage budget exhausted (only legitimate cap)
187
183
  if (this.isUsingOverage && this.extraUsageBudget != null && this.overageCostUsd >= this.extraUsageBudget) {
188
184
  this.capForOverage(`Extra usage budget $${this.extraUsageBudget} reached ($${this.overageCostUsd.toFixed(2)} spent) — stopping dispatch`);
189
185
  return;
190
186
  }
191
- const cap = this.usageCap;
192
- if (cap != null && cap < 1 && this.rateLimitUtilization >= cap) {
193
- const waitMs = this.rateLimitResetsAt
187
+ // Wait loop: keep waiting until the blocking condition clears
188
+ // isUsingOverage is purely informational the API enforces overage via 429s
189
+ // which the retry loop handles. Throttle only gates on actual rejections and user cap.
190
+ let consecutiveWaits = 0;
191
+ for (;;) {
192
+ if (this.aborted || this.cappedOut)
193
+ return;
194
+ const cap = this.usageCap;
195
+ const capExceeded = cap != null && cap < 1 && this.rateLimitUtilization >= cap;
196
+ const rejected = this.rateLimitResetsAt && this.rateLimitResetsAt > Date.now();
197
+ if (!capExceeded && !rejected)
198
+ break;
199
+ const fallbackMs = Math.min(300_000, 60_000 * (1 + consecutiveWaits * 2));
200
+ const waitMs = this.rateLimitResetsAt && this.rateLimitResetsAt > Date.now()
194
201
  ? Math.max(5000, this.rateLimitResetsAt - Date.now())
195
- : 60_000;
196
- this.log(-1, `Usage at ${Math.round(this.rateLimitUtilization * 100)}% (cap ${Math.round(cap * 100)}%), waiting ${Math.ceil(waitMs / 1000)}s for cooldown`);
202
+ : fallbackMs;
203
+ const reason = capExceeded
204
+ ? `Usage at ${Math.round(this.rateLimitUtilization * 100)}% (cap ${Math.round(cap * 100)}%)`
205
+ : "Rate limited";
206
+ this.log(-1, `${reason} — waiting ${Math.ceil(waitMs / 1000)}s then retrying`);
197
207
  this.rateLimitPaused++;
198
208
  await sleep(waitMs);
199
209
  this.rateLimitPaused--;
200
210
  this.rateLimitUtilization = 0;
201
211
  this.rateLimitResetsAt = undefined;
202
- return;
203
- }
204
- if (this.rateLimitResetsAt) {
205
- const resetTarget = this.rateLimitResetsAt;
206
- const waitMs = resetTarget - Date.now();
207
- if (waitMs > 0) {
208
- this.log(-1, `Rate limited, pausing ${Math.ceil(waitMs / 1000)}s`);
209
- this.rateLimitPaused++;
210
- await sleep(waitMs);
211
- this.rateLimitPaused--;
212
- }
213
- if (this.rateLimitResetsAt === resetTarget)
214
- this.rateLimitResetsAt = undefined;
212
+ consecutiveWaits++;
215
213
  }
216
- else if (this.rateLimitUtilization > 0.75) {
214
+ // Soft delay: high utilization, pace requests
215
+ if (this.rateLimitUtilization > 0.75) {
217
216
  const delay = Math.floor((this.rateLimitUtilization - 0.75) * 60000);
218
- this.log(-1, `Soft throttle: ${Math.round(this.rateLimitUtilization * 100)}% utilization, pausing ${(delay / 1000).toFixed(1)}s`);
219
- await sleep(delay);
217
+ if (delay > 0)
218
+ await sleep(delay);
220
219
  }
221
220
  }
222
221
  // ── Agent execution ──
@@ -370,6 +369,21 @@ export class Swarm {
370
369
  catch (err) {
371
370
  if (agent.status !== "running")
372
371
  break;
372
+ // Rate-limit errors: wait and retry WITHOUT burning the retry budget
373
+ if (!this.aborted && isRateLimitError(err)) {
374
+ const waitMs = this.rateLimitResetsAt && this.rateLimitResetsAt > Date.now()
375
+ ? Math.max(5000, this.rateLimitResetsAt - Date.now())
376
+ : 120_000;
377
+ this.log(id, `Rate limited — waiting ${Math.ceil(waitMs / 1000)}s (attempt not counted)`);
378
+ this.rateLimitPaused++;
379
+ await sleep(waitMs);
380
+ this.rateLimitPaused--;
381
+ this.isUsingOverage = false;
382
+ this.rateLimitUtilization = 0;
383
+ this.rateLimitResetsAt = undefined;
384
+ attempt--; // don't count this against retries
385
+ continue;
386
+ }
373
387
  const canRetry = attempt < maxRetries && !this.aborted && isTransientError(err);
374
388
  if (canRetry) {
375
389
  this.log(id, `Transient error: ${String(err.message || err).slice(0, 80)}`);
@@ -460,26 +474,21 @@ export class Swarm {
460
474
  const rl = msg;
461
475
  const info = rl.rate_limit_info;
462
476
  this.rateLimitUtilization = info.utilization ?? 0;
463
- if (info.status === "rejected" && info.resetsAt && !this.allowExtraUsage)
477
+ if (info.resetsAt)
464
478
  this.rateLimitResetsAt = info.resetsAt;
465
479
  else if (info.status !== "rejected")
466
480
  this.rateLimitResetsAt = undefined;
481
+ if (info.isUsingOverage)
482
+ this.isUsingOverage = true;
467
483
  const windowType = info.rateLimitType;
468
484
  if (windowType) {
469
485
  this.rateLimitWindows.set(windowType, {
470
486
  type: windowType, utilization: info.utilization ?? 0, status: info.status, resetsAt: info.resetsAt,
471
487
  });
472
488
  }
473
- if (info.isUsingOverage)
474
- this.isUsingOverage = true;
475
- if (info.overageDisabledReason)
476
- this.overageDisabledReason = info.overageDisabledReason;
477
- if (this.isUsingOverage && !this.allowExtraUsage)
478
- this.capForOverage(`Extra usage detected but not allowed — stopping dispatch`);
479
489
  const pct = info.utilization != null ? `${Math.round(info.utilization * 100)}%` : "";
480
490
  const overageTag = this.isUsingOverage ? " [EXTRA]" : "";
481
- const statusLabel = info.status === "rejected" && this.allowExtraUsage ? "switching to extra usage" : info.status;
482
- this.log(agent.id, `Rate: ${statusLabel} ${pct}${overageTag}${windowType ? ` (${windowType})` : ""}`);
491
+ this.log(agent.id, `Rate: ${info.status} ${pct}${overageTag}${windowType ? ` (${windowType})` : ""}`);
483
492
  break;
484
493
  }
485
494
  }
@@ -491,6 +500,18 @@ class AgentTimeoutError extends Error {
491
500
  this.name = "AgentTimeoutError";
492
501
  }
493
502
  }
503
+ function isRateLimitError(err) {
504
+ const status = err?.status ?? err?.statusCode;
505
+ if (status === 429)
506
+ return true;
507
+ const msg = String(err?.message || err).toLowerCase();
508
+ if (msg.includes("rate limit") || msg.includes("rate_limit") || msg.includes("too many requests"))
509
+ return true;
510
+ const cause = err?.cause;
511
+ if (cause && cause !== err)
512
+ return isRateLimitError(cause);
513
+ return false;
514
+ }
494
515
  function isTransientError(err) {
495
516
  if (err instanceof AgentTimeoutError)
496
517
  return false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-overnight",
3
- "version": "1.11.4",
3
+ "version": "1.11.6",
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": {
@@ -9,7 +9,8 @@
9
9
  "scripts": {
10
10
  "build": "tsc",
11
11
  "dev": "tsc --watch",
12
- "start": "node dist/index.js"
12
+ "start": "node dist/index.js",
13
+ "prepublishOnly": "node scripts/sync-plugin-version.js"
13
14
  },
14
15
  "dependencies": {
15
16
  "@anthropic-ai/claude-agent-sdk": "^0.2.92",