claude-overnight 1.11.5 → 1.11.7
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 +12 -1
- package/dist/index.js +58 -3
- package/dist/planner.d.ts +1 -0
- package/dist/planner.js +45 -2
- package/dist/render.js +3 -3
- package/dist/run.js +38 -5
- package/dist/state.d.ts +18 -0
- package/dist/state.js +68 -4
- package/dist/swarm.js +6 -8
- package/dist/types.d.ts +1 -1
- package/package.json +3 -2
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
|
|
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/index.js
CHANGED
|
@@ -5,13 +5,22 @@ import { fileURLToPath } from "url";
|
|
|
5
5
|
import chalk from "chalk";
|
|
6
6
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
7
7
|
import { Swarm } from "./swarm.js";
|
|
8
|
-
import { planTasks, refinePlan, identifyThemes, buildThinkingTasks, orchestrate } from "./planner.js";
|
|
8
|
+
import { planTasks, refinePlan, identifyThemes, buildThinkingTasks, orchestrate, salvageFromFile } from "./planner.js";
|
|
9
9
|
import { detectModelTier } from "./planner-query.js";
|
|
10
10
|
import { RunDisplay } from "./ui.js";
|
|
11
11
|
import { renderSummary } from "./render.js";
|
|
12
12
|
import { executeRun } from "./run.js";
|
|
13
13
|
import { parseCliFlags, isAuthError, fetchModels, ask, select, selectKey, loadTaskFile, validateConcurrency, isGitRepo, validateGitRepo, showPlan, BRAILLE, makeProgressLog, } from "./cli.js";
|
|
14
14
|
import { loadRunState, findIncompleteRuns, findOrphanedDesigns, formatTimeAgo, showRunHistory, readPreviousRunKnowledge, createRunDir, updateLatestSymlink, readMdDir, saveRunState, autoMergeBranches, } from "./state.js";
|
|
15
|
+
function countTasksInFile(path) {
|
|
16
|
+
try {
|
|
17
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
18
|
+
return Array.isArray(parsed?.tasks) ? parsed.tasks.length : 0;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return 0;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
15
24
|
async function main() {
|
|
16
25
|
const argv = process.argv.slice(2);
|
|
17
26
|
if (argv.includes("-v") || argv.includes("--version")) {
|
|
@@ -162,8 +171,13 @@ async function main() {
|
|
|
162
171
|
lastStatus = readFileSync(join(run.dir, "status.md"), "utf-8").trim().slice(0, 120);
|
|
163
172
|
}
|
|
164
173
|
catch { }
|
|
174
|
+
const planTaskCount = prev.phase === "planning" ? countTasksInFile(join(run.dir, "tasks.json")) : 0;
|
|
165
175
|
console.log(chalk.yellow(`\n ⚠ Unfinished run`) + chalk.dim(` · ${ago}`));
|
|
166
|
-
const boxLines = [
|
|
176
|
+
const boxLines = prev.phase === "planning" ? [
|
|
177
|
+
`${obj}${obj.length >= 50 ? "…" : ""}`,
|
|
178
|
+
`Plan ready · ${planTaskCount} tasks · budget ${prev.budget} · ${prev.concurrency}× concurrent`,
|
|
179
|
+
`Plan phase · not yet executing`,
|
|
180
|
+
] : [
|
|
167
181
|
`${obj}${obj.length >= 50 ? "…" : ""}`,
|
|
168
182
|
`${prev.accCompleted}/${prev.budget} sessions · ${Math.max(1, (prev.budget ?? 0) - prev.accCompleted)} remaining · $${prev.accCost.toFixed(2)}`,
|
|
169
183
|
`Wave ${prev.waveNum + 1} · ${prev.phase}`,
|
|
@@ -207,7 +221,13 @@ async function main() {
|
|
|
207
221
|
}
|
|
208
222
|
catch { }
|
|
209
223
|
console.log(chalk.cyan(` ${i + 1}`) + ` ${obj}${obj.length >= 50 ? "…" : ""}`);
|
|
210
|
-
|
|
224
|
+
if (s.phase === "planning") {
|
|
225
|
+
const n = countTasksInFile(join(shown[i].dir, "tasks.json"));
|
|
226
|
+
console.log(chalk.dim(` plan ready · ${n} tasks · budget ${s.budget} · ${ago} · not yet executing`));
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
console.log(chalk.dim(` ${s.accCompleted}/${s.budget} · $${s.accCost.toFixed(2)} · ${ago} · ${s.phase} at wave ${s.waveNum + 1}${merged ? ` · ${merged} merged` : ""}`));
|
|
230
|
+
}
|
|
211
231
|
if (lastStatus)
|
|
212
232
|
console.log(chalk.dim(` ${lastStatus}`));
|
|
213
233
|
console.log("");
|
|
@@ -236,6 +256,18 @@ async function main() {
|
|
|
236
256
|
}
|
|
237
257
|
}
|
|
238
258
|
if (resuming && resumeState && resumeRunDir) {
|
|
259
|
+
// Planning-phase resume: the prior run died before executeRun ran any
|
|
260
|
+
// wave, but the orchestrate agent wrote tasks.json to disk. Load those
|
|
261
|
+
// tasks into currentTasks so executeRun can pick them up as wave 0.
|
|
262
|
+
if (resumeState.phase === "planning") {
|
|
263
|
+
const loaded = salvageFromFile(join(resumeRunDir, "tasks.json"), resumeState.budget, () => { }, "resume");
|
|
264
|
+
if (!loaded) {
|
|
265
|
+
console.error(chalk.red(`\n Planning-phase run has no usable tasks.json — start Fresh instead.\n`));
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
resumeState.currentTasks = loaded;
|
|
269
|
+
console.log(chalk.green(`\n ✓ Resuming plan · ${loaded.length} tasks loaded from tasks.json`));
|
|
270
|
+
}
|
|
239
271
|
const unmerged = resumeState.branches.filter(b => b.status === "unmerged").length;
|
|
240
272
|
if (unmerged > 0) {
|
|
241
273
|
console.log("");
|
|
@@ -479,6 +511,29 @@ async function main() {
|
|
|
479
511
|
const previousKnowledge = readPreviousRunKnowledge(rootDir);
|
|
480
512
|
const needsPlan = tasks.length === 0 && !resuming;
|
|
481
513
|
const designDir = join(runDir, "designs");
|
|
514
|
+
// Persist an early planning-phase state so the run is visible to the resume
|
|
515
|
+
// picker even if orchestrate dies before executeRun gets a chance to run.
|
|
516
|
+
// Without this, a crashed plan phase leaves no run.json and the run vanishes
|
|
517
|
+
// from findIncompleteRuns — you pay for orchestration and can't see it.
|
|
518
|
+
if (needsPlan && objective) {
|
|
519
|
+
try {
|
|
520
|
+
saveRunState(runDir, {
|
|
521
|
+
id: runDir.split(/[/\\]/).pop() ?? "",
|
|
522
|
+
objective, budget: budget ?? 10, remaining: budget ?? 10,
|
|
523
|
+
workerModel, plannerModel, concurrency, permissionMode,
|
|
524
|
+
usageCap, allowExtraUsage, extraUsageBudget,
|
|
525
|
+
flex, useWorktrees, mergeStrategy,
|
|
526
|
+
waveNum: 0, currentTasks: [],
|
|
527
|
+
accCost: 0, accCompleted: 0, accFailed: 0,
|
|
528
|
+
accIn: 0, accOut: 0, accTools: 0,
|
|
529
|
+
branches: [],
|
|
530
|
+
phase: "planning",
|
|
531
|
+
startedAt: new Date().toISOString(),
|
|
532
|
+
cwd,
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
catch { }
|
|
536
|
+
}
|
|
482
537
|
if (needsPlan) {
|
|
483
538
|
if (noTTY) {
|
|
484
539
|
console.error(chalk.red(" No tasks provided and stdin is not a TTY."));
|
package/dist/planner.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Task, PermMode } from "./types.js";
|
|
2
|
+
export declare function salvageFromFile(outFile: string | undefined, budget: number | undefined, onLog: (text: string, kind?: "status" | "event") => void, why: string): Task[] | null;
|
|
2
3
|
export declare const DESIGN_THINKING = "\nHOW TO THINK ABOUT EVERY TASK:\n\nStart from the user's job. What is someone hiring this product to do? \"I need to send money abroad cheaply\" \u2014 not \"I need a currency conversion API.\" Every decision \u2014 what to build, how fast it responds, what happens on error \u2014 flows from the job.\n\nThe experience IS the product. A 200ms server response is not a \"performance metric\" \u2014 it's the difference between an app that feels alive and one that feels broken. A loading state is not \"polish\" \u2014 it's the user knowing the app heard them. An error message is not \"error handling\" \u2014 it's the app being honest. There is no line between backend and UX. The server, the API, the database query, the render \u2014 they're all one experience the user either trusts or doesn't.\n\nBuild the core, verify it works, learn, iterate. Don't plan 20 features and build them all. Build the ONE thing that matters most, run it, see if it actually works from a user's chair. What you learn from seeing it run will change what you build next. Each wave should make what exists better before adding what doesn't exist yet.\n\nConsistency is what makes complex things feel simple. One design system, rigid rules, no exceptions. This is how Revolut ships a super-app with 30+ features that doesn't feel like chaos.\n";
|
|
3
4
|
export declare function planTasks(objective: string, cwd: string, plannerModel: string, workerModel: string, permissionMode: PermMode, budget: number | undefined, concurrency: number, onLog: (text: string) => void, flexNote?: string, outFile?: string): Promise<Task[]>;
|
|
4
5
|
export declare function identifyThemes(objective: string, count: number, cwd: string, model: string, permissionMode: PermMode, onLog?: (text: string) => void): Promise<string[]>;
|
package/dist/planner.js
CHANGED
|
@@ -1,4 +1,29 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
1
2
|
import { runPlannerQuery, extractTaskJson, attemptJsonParse, postProcess, detectModelTier, modelCapabilityBlock } from "./planner-query.js";
|
|
3
|
+
// Resilience: if the planner query throws but the agent already wrote valid
|
|
4
|
+
// tasks to `outFile` (via its Write tool), salvage them instead of discarding
|
|
5
|
+
// expensive work. Returns salvaged tasks on success, null if nothing usable on
|
|
6
|
+
// disk — caller should then re-throw the original error.
|
|
7
|
+
export function salvageFromFile(outFile, budget, onLog, why) {
|
|
8
|
+
if (!outFile)
|
|
9
|
+
return null;
|
|
10
|
+
try {
|
|
11
|
+
const parsed = attemptJsonParse(readFileSync(outFile, "utf-8"));
|
|
12
|
+
if (!parsed?.tasks?.length)
|
|
13
|
+
return null;
|
|
14
|
+
let tasks = parsed.tasks.map((t, i) => ({
|
|
15
|
+
id: String(i), prompt: typeof t === "string" ? t : t.prompt,
|
|
16
|
+
}));
|
|
17
|
+
tasks = postProcess(tasks, budget, onLog);
|
|
18
|
+
if (tasks.length === 0)
|
|
19
|
+
return null;
|
|
20
|
+
onLog(`Planner errored (${why}) — salvaged ${tasks.length} tasks from ${outFile}`, "event");
|
|
21
|
+
return tasks;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
2
27
|
// The core framing for all planning. Not a checklist — a way of thinking.
|
|
3
28
|
export const DESIGN_THINKING = `
|
|
4
29
|
HOW TO THINK ABOUT EVERY TASK:
|
|
@@ -152,7 +177,16 @@ export async function planTasks(objective, cwd, plannerModel, workerModel, permi
|
|
|
152
177
|
onLog("Analyzing codebase...");
|
|
153
178
|
const prompt = plannerPrompt(objective, workerModel, budget, concurrency, flexNote);
|
|
154
179
|
const fileInstruction = outFile ? `\n\nAFTER generating the JSON, also write it to ${outFile} using the Write tool.` : "";
|
|
155
|
-
|
|
180
|
+
let resultText;
|
|
181
|
+
try {
|
|
182
|
+
resultText = await runPlannerQuery(prompt + fileInstruction, { cwd, model: plannerModel, permissionMode, outputFormat: TASKS_SCHEMA }, onLog);
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
const salvaged = salvageFromFile(outFile, budget, onLog, err?.message ?? String(err));
|
|
186
|
+
if (salvaged)
|
|
187
|
+
return salvaged;
|
|
188
|
+
throw err;
|
|
189
|
+
}
|
|
156
190
|
const parsed = await extractTaskJson(resultText, async () => {
|
|
157
191
|
onLog("Retrying...");
|
|
158
192
|
return runPlannerQuery(`Your previous response was not valid JSON. Respond with ONLY a JSON object {"tasks":[{"prompt":"..."}]}.\n\n${prompt}`, { cwd, model: plannerModel, permissionMode, outputFormat: TASKS_SCHEMA }, onLog);
|
|
@@ -234,7 +268,16 @@ Requirements:
|
|
|
234
268
|
Respond with ONLY a JSON object (no markdown fences):
|
|
235
269
|
{"tasks": [{"prompt": "..."}]}${fileInstruction}`;
|
|
236
270
|
onLog("Synthesizing...");
|
|
237
|
-
|
|
271
|
+
let resultText;
|
|
272
|
+
try {
|
|
273
|
+
resultText = await runPlannerQuery(prompt, { cwd, model: plannerModel, permissionMode, outputFormat: TASKS_SCHEMA }, onLog);
|
|
274
|
+
}
|
|
275
|
+
catch (err) {
|
|
276
|
+
const salvaged = salvageFromFile(outFile, budget, onLog, err?.message ?? String(err));
|
|
277
|
+
if (salvaged)
|
|
278
|
+
return salvaged;
|
|
279
|
+
throw err;
|
|
280
|
+
}
|
|
238
281
|
const parsed = await extractTaskJson(resultText, async () => {
|
|
239
282
|
onLog("Retrying...");
|
|
240
283
|
return runPlannerQuery(`Your previous response was not valid JSON. Respond with ONLY a JSON object {"tasks":[{"prompt":"..."}]}.\n\n${prompt}`, { cwd, model: plannerModel, permissionMode, outputFormat: TASKS_SCHEMA }, onLog);
|
package/dist/render.js
CHANGED
|
@@ -80,8 +80,8 @@ function renderUsageBars(out, w, swarm) {
|
|
|
80
80
|
}
|
|
81
81
|
let label = `${Math.round(pct * 100)}% used`;
|
|
82
82
|
if (swarm.cappedOut) {
|
|
83
|
-
label = swarm.
|
|
84
|
-
? chalk.red(`
|
|
83
|
+
label = swarm.extraUsageBudget != null
|
|
84
|
+
? chalk.red(`Budget $${swarm.extraUsageBudget} exhausted \u2014 finishing active`)
|
|
85
85
|
: chalk.yellow(`Capped at ${capFrac != null ? Math.round(capFrac * 100) : 100}% \u2014 finishing active`);
|
|
86
86
|
}
|
|
87
87
|
else if (swarm.rateLimitPaused > 0) {
|
|
@@ -93,7 +93,7 @@ function renderUsageBars(out, w, swarm) {
|
|
|
93
93
|
label = chalk.red(`Waiting for reset ${mm > 0 ? `${mm}m ${ss}s` : `${ss}s`}`);
|
|
94
94
|
}
|
|
95
95
|
if (swarm.isUsingOverage && !swarm.cappedOut)
|
|
96
|
-
label += chalk.red(" [
|
|
96
|
+
label += chalk.red(" [OVERAGE]");
|
|
97
97
|
const prefix = windowLabel ? chalk.dim(windowLabel.padEnd(6)) : chalk.dim("Usage ");
|
|
98
98
|
out.push(` ${prefix}${barStr} ${label}`);
|
|
99
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");
|
|
@@ -46,8 +46,16 @@ export async function executeRun(cfg) {
|
|
|
46
46
|
branches.push(...rs.branches);
|
|
47
47
|
flex = rs.flex;
|
|
48
48
|
waveHistory.push(...loadWaveHistory(runDir));
|
|
49
|
-
|
|
50
|
-
waveNum
|
|
49
|
+
// Planning-phase resume starts at wave 0 (nothing ran before); all other
|
|
50
|
+
// resumes bump to the next wave since rs.waveNum is the last completed one.
|
|
51
|
+
const fromPlanning = rs.phase === "planning";
|
|
52
|
+
if (fromPlanning && !existsSync(join(runDir, "goal.md")) && objective) {
|
|
53
|
+
writeFileSync(join(runDir, "goal.md"), `## Original Objective\n${objective}`, "utf-8");
|
|
54
|
+
}
|
|
55
|
+
const detail = fromPlanning ? `${currentTasks.length} tasks from plan` : `${waveHistory.length} prior waves`;
|
|
56
|
+
console.log(chalk.green(`\n ✓ Resumed`) + chalk.dim(` · wave ${waveNum + 1} · ${remaining} remaining · $${accCost.toFixed(2)} spent · ${detail}\n`));
|
|
57
|
+
if (!fromPlanning)
|
|
58
|
+
waveNum++;
|
|
51
59
|
}
|
|
52
60
|
else {
|
|
53
61
|
if (objective && !existsSync(join(runDir, "goal.md"))) {
|
|
@@ -160,6 +168,20 @@ export async function executeRun(cfg) {
|
|
|
160
168
|
catch { }
|
|
161
169
|
}
|
|
162
170
|
const waveMerge = (flex && runBranch) ? "yolo" : mergeStrategy;
|
|
171
|
+
const runId = runDir.split(/[/\\]/).pop() ?? "";
|
|
172
|
+
if (!cfg.resuming) {
|
|
173
|
+
try {
|
|
174
|
+
appendOvernightLogStart(cwd, runId, {
|
|
175
|
+
objective: objective || "",
|
|
176
|
+
model: workerModel,
|
|
177
|
+
budget: cfg.budget,
|
|
178
|
+
flex,
|
|
179
|
+
usageCap,
|
|
180
|
+
branch: runBranch,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
catch { }
|
|
184
|
+
}
|
|
163
185
|
let stopping = false;
|
|
164
186
|
const gracefulStop = () => {
|
|
165
187
|
if (stopping) {
|
|
@@ -369,6 +391,17 @@ export async function executeRun(cfg) {
|
|
|
369
391
|
}
|
|
370
392
|
catch { }
|
|
371
393
|
}
|
|
394
|
+
try {
|
|
395
|
+
updateOvernightLogEnd(cwd, runId, {
|
|
396
|
+
cost: accCost,
|
|
397
|
+
completed: accCompleted,
|
|
398
|
+
failed: accFailed,
|
|
399
|
+
waves: waveNum + 1,
|
|
400
|
+
phase: finalPhase,
|
|
401
|
+
elapsedSec: Math.round((Date.now() - cfg.runStartedAt) / 1000),
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
catch { }
|
|
372
405
|
if (runBranch && originalRef) {
|
|
373
406
|
try {
|
|
374
407
|
execSync(`git checkout "${originalRef}"`, { cwd, encoding: "utf-8", stdio: "pipe" });
|
|
@@ -390,7 +423,7 @@ export async function executeRun(cfg) {
|
|
|
390
423
|
else if (remaining <= 0)
|
|
391
424
|
console.log(chalk.bold.yellow(` CLAUDE OVERNIGHT — BUDGET EXHAUSTED`));
|
|
392
425
|
else if (lastCapped)
|
|
393
|
-
console.log(chalk.bold.yellow(` CLAUDE OVERNIGHT —
|
|
426
|
+
console.log(chalk.bold.yellow(` CLAUDE OVERNIGHT — BUDGET EXHAUSTED`));
|
|
394
427
|
else if (stopping || lastAborted)
|
|
395
428
|
console.log(chalk.bold.yellow(` CLAUDE OVERNIGHT — INTERRUPTED`));
|
|
396
429
|
else
|
|
@@ -406,7 +439,7 @@ export async function executeRun(cfg) {
|
|
|
406
439
|
for (const [k1, v1, k2, v2] of statRows)
|
|
407
440
|
console.log(` ${k1} ${v1.padEnd(20)} ${k2} ${v2}`);
|
|
408
441
|
if (lastCapped)
|
|
409
|
-
console.log(` ${chalk.yellow(`
|
|
442
|
+
console.log(` ${chalk.yellow(`Overage budget exhausted`)}`);
|
|
410
443
|
console.log("");
|
|
411
444
|
const statusFile = join(runDir, "status.md");
|
|
412
445
|
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 });
|
|
@@ -128,10 +187,15 @@ export function findIncompleteRuns(rootDir, filterCwd) {
|
|
|
128
187
|
const dirs = readdirSync(runsDir).sort().reverse();
|
|
129
188
|
const results = [];
|
|
130
189
|
for (const d of dirs) {
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
190
|
+
const runDir = join(runsDir, d);
|
|
191
|
+
const state = loadRunState(runDir);
|
|
192
|
+
if (!state || state.phase === "done" || state.cwd !== filterCwd)
|
|
193
|
+
continue;
|
|
194
|
+
// Planning-phase runs are only resumable if tasks.json was actually
|
|
195
|
+
// written — resuming without tasks is nothing to resume.
|
|
196
|
+
if (state.phase === "planning" && !existsSync(join(runDir, "tasks.json")))
|
|
197
|
+
continue;
|
|
198
|
+
results.push({ dir: runDir, state });
|
|
135
199
|
}
|
|
136
200
|
return results;
|
|
137
201
|
}
|
package/dist/swarm.js
CHANGED
|
@@ -185,30 +185,28 @@ export class Swarm {
|
|
|
185
185
|
return;
|
|
186
186
|
}
|
|
187
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.
|
|
188
190
|
let consecutiveWaits = 0;
|
|
189
191
|
for (;;) {
|
|
190
192
|
if (this.aborted || this.cappedOut)
|
|
191
193
|
return;
|
|
192
194
|
const cap = this.usageCap;
|
|
193
|
-
const overageBlocked = this.isUsingOverage && !this.allowExtraUsage;
|
|
194
195
|
const capExceeded = cap != null && cap < 1 && this.rateLimitUtilization >= cap;
|
|
195
196
|
const rejected = this.rateLimitResetsAt && this.rateLimitResetsAt > Date.now();
|
|
196
|
-
if (!
|
|
197
|
+
if (!capExceeded && !rejected)
|
|
197
198
|
break;
|
|
198
|
-
// Use SDK reset time if available, otherwise escalate: 1m → 3m → 5m (max)
|
|
199
199
|
const fallbackMs = Math.min(300_000, 60_000 * (1 + consecutiveWaits * 2));
|
|
200
200
|
const waitMs = this.rateLimitResetsAt && this.rateLimitResetsAt > Date.now()
|
|
201
201
|
? Math.max(5000, this.rateLimitResetsAt - Date.now())
|
|
202
202
|
: fallbackMs;
|
|
203
|
-
const reason =
|
|
204
|
-
|
|
205
|
-
|
|
203
|
+
const reason = capExceeded
|
|
204
|
+
? `Usage at ${Math.round(this.rateLimitUtilization * 100)}% (cap ${Math.round(cap * 100)}%)`
|
|
205
|
+
: "Rate limited";
|
|
206
206
|
this.log(-1, `${reason} — waiting ${Math.ceil(waitMs / 1000)}s then retrying`);
|
|
207
207
|
this.rateLimitPaused++;
|
|
208
208
|
await sleep(waitMs);
|
|
209
209
|
this.rateLimitPaused--;
|
|
210
|
-
// Reset stale flags — fresh state will come from the next SDK event
|
|
211
|
-
this.isUsingOverage = false;
|
|
212
210
|
this.rateLimitUtilization = 0;
|
|
213
211
|
this.rateLimitResetsAt = undefined;
|
|
214
212
|
consecutiveWaits++;
|
package/dist/types.d.ts
CHANGED
|
@@ -172,7 +172,7 @@ export interface RunState {
|
|
|
172
172
|
accOut?: number;
|
|
173
173
|
accTools?: number;
|
|
174
174
|
branches: BranchRecord[];
|
|
175
|
-
phase: "steering" | "capped" | "done" | "stopped";
|
|
175
|
+
phase: "planning" | "steering" | "capped" | "done" | "stopped";
|
|
176
176
|
startedAt: string;
|
|
177
177
|
cwd: string;
|
|
178
178
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-overnight",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.7",
|
|
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",
|