claude-overnight 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -8
- package/dist/index.js +112 -52
- package/dist/planner.d.ts +2 -2
- package/dist/planner.js +58 -47
- package/dist/types.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -52,7 +52,7 @@ claude-overnight
|
|
|
52
52
|
◆ Assessing... ✓ Vision met
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
You interact once (objective, budget, model, review themes), then everything runs autonomously — thinking, planning, executing, reflecting, steering. Rate-limited? It waits and retries. Crash? Resume where you left off.
|
|
55
|
+
You interact once (objective, budget, model, review themes), then everything runs autonomously — thinking, planning, executing, reflecting, steering. Rate-limited? It waits and retries. Crash? Resume where you left off. Capped at usage limit? Pick up next time with full context preserved.
|
|
56
56
|
|
|
57
57
|
## How it works
|
|
58
58
|
|
|
@@ -62,7 +62,7 @@ For budgets > 15, the tool launches **architect agents** that explore your codeb
|
|
|
62
62
|
|
|
63
63
|
### 2. Orchestration
|
|
64
64
|
|
|
65
|
-
An orchestrator agent reads all design documents and synthesizes concrete execution tasks — grounded in real files and patterns the architects found. No guesswork.
|
|
65
|
+
An orchestrator agent reads all design documents and synthesizes concrete execution tasks — grounded in real files and patterns the architects found. No guesswork. The task plan is also written to a file for resilience — if orchestration is interrupted, partial results survive.
|
|
66
66
|
|
|
67
67
|
### 3. Iterative execution
|
|
68
68
|
|
|
@@ -97,20 +97,30 @@ Every run gets its own folder in `.claude-overnight/runs/`. Nothing is ever over
|
|
|
97
97
|
run.json, sessions/
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
Any run that stops before the steering system declares the objective complete — capped at usage limit, Ctrl+C, crash, rate limit timeout, steering failure — is automatically resumable:
|
|
101
101
|
|
|
102
102
|
```
|
|
103
|
-
⚠
|
|
103
|
+
⚠ Unfinished run
|
|
104
104
|
╭──────────────────────────────────────────────────╮
|
|
105
105
|
│ refactor auth, add tests, update docs │
|
|
106
|
-
│ 50/200 sessions ·
|
|
106
|
+
│ 50/200 sessions · 150 remaining · $69.16 │
|
|
107
107
|
│ 34 merged · 16 unmerged · 0 failed branches │
|
|
108
108
|
╰──────────────────────────────────────────────────╯
|
|
109
109
|
|
|
110
110
|
Resume │ Fresh │ Quit
|
|
111
111
|
```
|
|
112
112
|
|
|
113
|
-
On resume: unmerged branches auto-merge, the wave loop continues, all context is preserved.
|
|
113
|
+
On resume: unmerged branches auto-merge, the wave loop continues, all context is preserved. Designs and reflections stay on disk until the objective is truly complete.
|
|
114
|
+
|
|
115
|
+
If the thinking phase succeeds but orchestration crashes, the next run detects the orphaned design docs and reuses them — no re-running $9 worth of architect agents:
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
✓ Reusing 5 design docs (from prior attempt)
|
|
119
|
+
Focus 0: Project Wizard UI vs VISION.md Flow
|
|
120
|
+
Focus 1: Team Load and Rebalancer Surface
|
|
121
|
+
Focus 2: Code Health After Swarm Wave
|
|
122
|
+
...
|
|
123
|
+
```
|
|
114
124
|
|
|
115
125
|
**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.
|
|
116
126
|
|
|
@@ -186,9 +196,10 @@ Built for unattended runs lasting hours or days.
|
|
|
186
196
|
|
|
187
197
|
- **Hard block**: pauses until the rate limit window resets, then resumes
|
|
188
198
|
- **Soft throttle**: slows dispatch at >75% utilization
|
|
199
|
+
- **Cooldown between phases**: waits for rate limit reset after thinking before starting orchestration
|
|
189
200
|
- **Retry with backoff**: transient errors (429, overloaded) retry automatically
|
|
190
|
-
- **Usage cap**: set a ceiling, active agents finish, no new ones start
|
|
191
|
-
- **Planner retries**: steering and orchestration
|
|
201
|
+
- **Usage cap**: set a ceiling, active agents finish, no new ones start — run is resumable
|
|
202
|
+
- **Planner retries**: steering and orchestration retry on rate limits (30s/60s/120s backoff) with full context
|
|
192
203
|
|
|
193
204
|
## Worktrees and merging
|
|
194
205
|
|
package/dist/index.js
CHANGED
|
@@ -330,6 +330,24 @@ function findIncompleteRun(rootDir) {
|
|
|
330
330
|
catch { }
|
|
331
331
|
return null;
|
|
332
332
|
}
|
|
333
|
+
/** Find orphaned designs: a run where thinking succeeded but orchestration crashed (has designs, no run.json). */
|
|
334
|
+
function findOrphanedDesigns(rootDir) {
|
|
335
|
+
const runsDir = join(rootDir, "runs");
|
|
336
|
+
try {
|
|
337
|
+
const dirs = readdirSync(runsDir).sort().reverse();
|
|
338
|
+
for (const d of dirs) {
|
|
339
|
+
const runDir = join(runsDir, d);
|
|
340
|
+
const hasState = existsSync(join(runDir, "run.json"));
|
|
341
|
+
if (hasState)
|
|
342
|
+
continue; // has state — either complete or properly resumable
|
|
343
|
+
const designs = readMdDir(join(runDir, "designs"));
|
|
344
|
+
if (designs)
|
|
345
|
+
return runDir;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
catch { }
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
333
351
|
/** Read final status + goal from all completed previous runs (newest first, max 5). */
|
|
334
352
|
function readPreviousRunKnowledge(rootDir) {
|
|
335
353
|
const runsDir = join(rootDir, "runs");
|
|
@@ -588,9 +606,18 @@ async function main() {
|
|
|
588
606
|
console.log(chalk.dim(`\n ${completedRuns.length} previous run${completedRuns.length > 1 ? "s" : ""}`));
|
|
589
607
|
for (const r of completedRuns.slice(0, 3)) {
|
|
590
608
|
const date = r.state.startedAt?.slice(0, 10) || "unknown";
|
|
591
|
-
const obj = r.state.objective?.slice(0,
|
|
609
|
+
const obj = r.state.objective?.slice(0, 50) || "";
|
|
592
610
|
const cost = r.state.accCost > 0 ? ` · $${r.state.accCost.toFixed(0)}` : "";
|
|
593
|
-
|
|
611
|
+
const merged = r.state.branches.filter(b => b.status === "merged").length;
|
|
612
|
+
console.log(chalk.dim(` ${date} · ${r.state.accCompleted} done · ${merged} merged${cost}${obj ? ` · ${obj}` : ""}${obj.length >= 50 ? "…" : ""}`));
|
|
613
|
+
// Show status if available
|
|
614
|
+
let status = "";
|
|
615
|
+
try {
|
|
616
|
+
status = readFileSync(join(r.dir, "status.md"), "utf-8").trim().split("\n")[0].slice(0, 80);
|
|
617
|
+
}
|
|
618
|
+
catch { }
|
|
619
|
+
if (status)
|
|
620
|
+
console.log(chalk.dim(` ${status}`));
|
|
594
621
|
}
|
|
595
622
|
}
|
|
596
623
|
// ── Resume detection ──
|
|
@@ -610,10 +637,11 @@ async function main() {
|
|
|
610
637
|
lastStatus = readFileSync(join(incomplete.dir, "status.md"), "utf-8").trim().slice(0, 120);
|
|
611
638
|
}
|
|
612
639
|
catch { }
|
|
613
|
-
|
|
640
|
+
const label = "Unfinished run";
|
|
641
|
+
console.log(chalk.yellow(`\n ⚠ ${label}`));
|
|
614
642
|
const boxLines = [
|
|
615
643
|
`${obj}${obj.length >= 50 ? "…" : ""}`,
|
|
616
|
-
`${prev.accCompleted}/${prev.budget} sessions · ${prev.
|
|
644
|
+
`${prev.accCompleted}/${prev.budget} sessions · ${prev.remaining} remaining · $${prev.accCost.toFixed(2)}`,
|
|
617
645
|
];
|
|
618
646
|
if (lastStatus)
|
|
619
647
|
boxLines.push(lastStatus);
|
|
@@ -769,8 +797,9 @@ async function main() {
|
|
|
769
797
|
let thinkingUsed = 0;
|
|
770
798
|
let thinkingCost = 0, thinkingIn = 0, thinkingOut = 0, thinkingTools = 0;
|
|
771
799
|
let thinkingHistory;
|
|
772
|
-
// Create run directory
|
|
773
|
-
const
|
|
800
|
+
// Create run directory — reuse orphaned run (thinking succeeded, orchestration crashed) if available
|
|
801
|
+
const orphanedDir = !resuming ? findOrphanedDesigns(rootDir) : null;
|
|
802
|
+
const runDir = resuming && resumeRunDir ? resumeRunDir : (orphanedDir ?? createRunDir(rootDir));
|
|
774
803
|
const previousKnowledge = readPreviousRunKnowledge(rootDir);
|
|
775
804
|
// ── Plan phase (interactive: review loop, non-interactive: auto-plan or skip) ──
|
|
776
805
|
const needsPlan = tasks.length === 0;
|
|
@@ -839,56 +868,81 @@ async function main() {
|
|
|
839
868
|
}
|
|
840
869
|
// ── From here, fully autonomous — no more user interaction ──
|
|
841
870
|
process.stdout.write("\x1B[?25l");
|
|
842
|
-
// Phase 2: Thinking wave
|
|
871
|
+
// Phase 2: Thinking wave — skip if design docs already exist (e.g. previous orchestration failed)
|
|
843
872
|
mkdirSync(designDir, { recursive: true });
|
|
844
|
-
const
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
await thinkingSwarm.run();
|
|
873
|
+
const existingDesigns = readMdDir(designDir);
|
|
874
|
+
if (existingDesigns) {
|
|
875
|
+
const designFiles = readdirSync(designDir).filter(f => f.endsWith(".md")).sort();
|
|
876
|
+
console.log(chalk.green(`\n ✓ Reusing ${designFiles.length} design docs`) + chalk.dim(` (from prior attempt)`));
|
|
877
|
+
for (const f of designFiles) {
|
|
878
|
+
try {
|
|
879
|
+
const firstLine = readFileSync(join(designDir, f), "utf-8").split("\n")[0].replace(/^#+\s*/, "").trim();
|
|
880
|
+
if (firstLine)
|
|
881
|
+
console.log(chalk.dim(` ${firstLine.slice(0, 80)}`));
|
|
882
|
+
}
|
|
883
|
+
catch { }
|
|
884
|
+
}
|
|
885
|
+
console.log("");
|
|
858
886
|
}
|
|
859
|
-
|
|
860
|
-
|
|
887
|
+
else {
|
|
888
|
+
const thinkingTasks = buildThinkingTasks(objective, themes, designDir, plannerModel, previousKnowledge || undefined);
|
|
889
|
+
console.log(chalk.cyan(`\n ◆ Thinking: ${thinkingTasks.length} agents exploring...\n`));
|
|
890
|
+
const thinkingSwarm = new Swarm({
|
|
891
|
+
tasks: thinkingTasks, concurrency, cwd,
|
|
892
|
+
model: plannerModel,
|
|
893
|
+
permissionMode,
|
|
894
|
+
useWorktrees: false,
|
|
895
|
+
mergeStrategy: "yolo",
|
|
896
|
+
agentTimeoutMs,
|
|
897
|
+
usageCap,
|
|
898
|
+
});
|
|
899
|
+
const stopThinkRender = startRenderLoop(thinkingSwarm);
|
|
900
|
+
try {
|
|
901
|
+
await thinkingSwarm.run();
|
|
902
|
+
}
|
|
903
|
+
finally {
|
|
904
|
+
stopThinkRender();
|
|
905
|
+
}
|
|
906
|
+
console.log(renderSummary(thinkingSwarm));
|
|
907
|
+
thinkingUsed = thinkingSwarm.completed + thinkingSwarm.failed;
|
|
908
|
+
thinkingCost = thinkingSwarm.totalCostUsd;
|
|
909
|
+
thinkingIn = thinkingSwarm.totalInputTokens;
|
|
910
|
+
thinkingOut = thinkingSwarm.totalOutputTokens;
|
|
911
|
+
thinkingTools = thinkingSwarm.agents.reduce((sum, a) => sum + a.toolCalls, 0);
|
|
912
|
+
// Record thinking wave so steering knows what happened
|
|
913
|
+
thinkingHistory = {
|
|
914
|
+
wave: -1,
|
|
915
|
+
kind: "think",
|
|
916
|
+
tasks: thinkingSwarm.agents.map(a => ({
|
|
917
|
+
prompt: a.task.prompt.slice(0, 200),
|
|
918
|
+
status: a.status,
|
|
919
|
+
filesChanged: a.filesChanged,
|
|
920
|
+
error: a.error,
|
|
921
|
+
})),
|
|
922
|
+
};
|
|
923
|
+
// Wait for rate limit reset before orchestration
|
|
924
|
+
if (thinkingSwarm.rateLimitResetsAt) {
|
|
925
|
+
const waitMs = thinkingSwarm.rateLimitResetsAt - Date.now();
|
|
926
|
+
if (waitMs > 0) {
|
|
927
|
+
console.log(chalk.dim(` Waiting ${Math.ceil(waitMs / 1000)}s for rate limit reset...`));
|
|
928
|
+
await new Promise(r => setTimeout(r, waitMs + 2000));
|
|
929
|
+
}
|
|
930
|
+
}
|
|
861
931
|
}
|
|
862
|
-
console.log(renderSummary(thinkingSwarm));
|
|
863
|
-
thinkingUsed = thinkingSwarm.completed + thinkingSwarm.failed;
|
|
864
|
-
thinkingCost = thinkingSwarm.totalCostUsd;
|
|
865
|
-
thinkingIn = thinkingSwarm.totalInputTokens;
|
|
866
|
-
thinkingOut = thinkingSwarm.totalOutputTokens;
|
|
867
|
-
thinkingTools = thinkingSwarm.agents.reduce((sum, a) => sum + a.toolCalls, 0);
|
|
868
|
-
// Record thinking wave so steering knows what happened
|
|
869
|
-
thinkingHistory = {
|
|
870
|
-
wave: -1,
|
|
871
|
-
kind: "think",
|
|
872
|
-
tasks: thinkingSwarm.agents.map(a => ({
|
|
873
|
-
prompt: a.task.prompt.slice(0, 200),
|
|
874
|
-
status: a.status,
|
|
875
|
-
filesChanged: a.filesChanged,
|
|
876
|
-
error: a.error,
|
|
877
|
-
})),
|
|
878
|
-
};
|
|
879
932
|
// Phase 3: Orchestrate from design docs
|
|
880
933
|
const designs = readMdDir(designDir);
|
|
934
|
+
const taskFile = join(runDir, "tasks.json");
|
|
881
935
|
if (designs) {
|
|
882
936
|
const orchBudget = Math.min(50, Math.max(concurrency, Math.ceil(((budget ?? 10) - thinkingUsed) * 0.5)));
|
|
883
937
|
const flexNote = `This is wave 1 of an adaptive multi-wave run (total budget: ${(budget ?? 10) - thinkingUsed}). Plan the highest-impact foundational work first. Future waves will iterate based on what's learned.`;
|
|
884
938
|
console.log(chalk.cyan(`\n ◆ Orchestrating plan...\n`));
|
|
885
|
-
tasks = await orchestrate(objective, designs, cwd, plannerModel, workerModel, permissionMode, orchBudget, concurrency, makeProgressLog(), flexNote);
|
|
939
|
+
tasks = await orchestrate(objective, designs, cwd, plannerModel, workerModel, permissionMode, orchBudget, concurrency, makeProgressLog(), flexNote, taskFile);
|
|
886
940
|
process.stdout.write(`\x1B[2K\r ${chalk.green(`\u2713 ${tasks.length} tasks`)}\n\n`);
|
|
887
941
|
}
|
|
888
942
|
else {
|
|
889
943
|
console.log(chalk.yellow(`\n No design docs — falling back to direct planning\n`));
|
|
890
944
|
const waveBudget = Math.min(50, Math.max(concurrency, Math.ceil(((budget ?? 10) - thinkingUsed) * 0.5)));
|
|
891
|
-
tasks = await planTasks(objective, cwd, plannerModel, workerModel, permissionMode, waveBudget, concurrency, makeProgressLog());
|
|
945
|
+
tasks = await planTasks(objective, cwd, plannerModel, workerModel, permissionMode, waveBudget, concurrency, makeProgressLog(), undefined, taskFile);
|
|
892
946
|
process.stdout.write(`\x1B[2K\r ${chalk.green(`\u2713 ${tasks.length} tasks`)}\n\n`);
|
|
893
947
|
}
|
|
894
948
|
}
|
|
@@ -996,7 +1050,7 @@ async function main() {
|
|
|
996
1050
|
const waveHistory = [];
|
|
997
1051
|
let accCost, accCompleted, accFailed, accTools;
|
|
998
1052
|
let accIn = 0, accOut = 0;
|
|
999
|
-
let lastCapped = false, lastAborted = false;
|
|
1053
|
+
let lastCapped = false, lastAborted = false, objectiveComplete = false;
|
|
1000
1054
|
let lastWaveKind;
|
|
1001
1055
|
let reflectionBudgetUsed;
|
|
1002
1056
|
const branches = [];
|
|
@@ -1159,6 +1213,7 @@ async function main() {
|
|
|
1159
1213
|
if (steer.done || steer.action === "done") {
|
|
1160
1214
|
console.log(chalk.green(` \u2713 ${steer.reasoning}\n`));
|
|
1161
1215
|
steerDone = true;
|
|
1216
|
+
objectiveComplete = true;
|
|
1162
1217
|
remaining = 0; // exit outer loop too
|
|
1163
1218
|
break;
|
|
1164
1219
|
}
|
|
@@ -1211,6 +1266,7 @@ async function main() {
|
|
|
1211
1266
|
// action === "execute"
|
|
1212
1267
|
if (steer.tasks.length === 0) {
|
|
1213
1268
|
console.log(chalk.green(` \u2713 ${steer.reasoning}\n`));
|
|
1269
|
+
objectiveComplete = true;
|
|
1214
1270
|
remaining = 0;
|
|
1215
1271
|
break;
|
|
1216
1272
|
}
|
|
@@ -1228,22 +1284,26 @@ async function main() {
|
|
|
1228
1284
|
}
|
|
1229
1285
|
waveNum++;
|
|
1230
1286
|
}
|
|
1231
|
-
//
|
|
1287
|
+
// Only truly "done" if steering explicitly completed the objective (or non-flex single wave with budget exhausted)
|
|
1288
|
+
const trulyDone = objectiveComplete || (!flex && remaining <= 0);
|
|
1289
|
+
const finalPhase = trulyDone ? "done" : "capped";
|
|
1232
1290
|
saveRunState(runDir, {
|
|
1233
1291
|
id: `run-${new Date().toISOString().slice(0, 19)}`, objective: objective ?? "", budget: budget ?? tasks.length,
|
|
1234
1292
|
remaining, workerModel, plannerModel, concurrency, permissionMode,
|
|
1235
1293
|
usageCap, flex, useWorktrees, mergeStrategy, waveNum, currentTasks: [],
|
|
1236
1294
|
lastWaveKind, reflectionBudgetUsed, accCost, accCompleted, accFailed,
|
|
1237
|
-
branches, phase:
|
|
1295
|
+
branches, phase: finalPhase, startedAt: new Date(runStartedAt).toISOString(), cwd,
|
|
1238
1296
|
});
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1297
|
+
if (trulyDone) {
|
|
1298
|
+
try {
|
|
1299
|
+
rmSync(join(runDir, "designs"), { recursive: true, force: true });
|
|
1300
|
+
}
|
|
1301
|
+
catch { }
|
|
1302
|
+
try {
|
|
1303
|
+
rmSync(join(runDir, "reflections"), { recursive: true, force: true });
|
|
1304
|
+
}
|
|
1305
|
+
catch { }
|
|
1245
1306
|
}
|
|
1246
|
-
catch { }
|
|
1247
1307
|
// Switch back if we created a run branch
|
|
1248
1308
|
if (runBranch && originalRef) {
|
|
1249
1309
|
try {
|
package/dist/planner.d.ts
CHANGED
|
@@ -27,10 +27,10 @@ export interface RunMemory {
|
|
|
27
27
|
}
|
|
28
28
|
export type ModelTier = "opus" | "sonnet" | "haiku" | "unknown";
|
|
29
29
|
export declare function detectModelTier(model: string): ModelTier;
|
|
30
|
-
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): Promise<Task[]>;
|
|
30
|
+
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[]>;
|
|
31
31
|
export declare function identifyThemes(objective: string, count: number, model: string, permissionMode: PermMode): Promise<string[]>;
|
|
32
32
|
export declare function buildThinkingTasks(objective: string, themes: string[], designDir: string, plannerModel: string, previousKnowledge?: string): Task[];
|
|
33
33
|
export declare function buildReflectionTasks(objective: string, goal: string, reflectionDir: string, waveNum: number, plannerModel: string): Task[];
|
|
34
|
-
export declare function orchestrate(objective: string, designDocs: string, cwd: string, plannerModel: string, workerModel: string, permissionMode: PermMode, budget: number, concurrency: number, onLog: (text: string) => void, flexNote?: string): Promise<Task[]>;
|
|
34
|
+
export declare function orchestrate(objective: string, designDocs: string, cwd: string, plannerModel: string, workerModel: string, permissionMode: PermMode, budget: number, concurrency: number, onLog: (text: string) => void, flexNote?: string, outFile?: string): Promise<Task[]>;
|
|
35
35
|
export declare function refinePlan(objective: string, previousTasks: Task[], feedback: string, cwd: string, plannerModel: string, workerModel: string, permissionMode: PermMode, budget: number | undefined, concurrency: number, onLog: (text: string) => void): Promise<Task[]>;
|
|
36
36
|
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>;
|
package/dist/planner.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
+
import { readFileSync } from "fs";
|
|
2
3
|
const INACTIVITY_MS = 5 * 60 * 1000;
|
|
3
4
|
export function detectModelTier(model) {
|
|
4
5
|
const m = model.toLowerCase();
|
|
@@ -179,8 +180,8 @@ async function runPlannerQueryOnce(prompt, opts, onLog) {
|
|
|
179
180
|
options: {
|
|
180
181
|
cwd: opts.cwd,
|
|
181
182
|
model: opts.model,
|
|
182
|
-
tools: ["Read", "Glob", "Grep"],
|
|
183
|
-
allowedTools: ["Read", "Glob", "Grep"],
|
|
183
|
+
tools: ["Read", "Glob", "Grep", "Write"],
|
|
184
|
+
allowedTools: ["Read", "Glob", "Grep", "Write"],
|
|
184
185
|
permissionMode: opts.permissionMode,
|
|
185
186
|
...(opts.permissionMode === "bypassPermissions" && { allowDangerouslySkipPermissions: true }),
|
|
186
187
|
persistSession: false,
|
|
@@ -311,21 +312,15 @@ function postProcess(raw, budget, onLog) {
|
|
|
311
312
|
tasks = tasks.map((t, i) => ({ ...t, id: String(i) }));
|
|
312
313
|
return tasks;
|
|
313
314
|
}
|
|
314
|
-
export async function planTasks(objective, cwd, plannerModel, workerModel, permissionMode, budget, concurrency, onLog, flexNote) {
|
|
315
|
+
export async function planTasks(objective, cwd, plannerModel, workerModel, permissionMode, budget, concurrency, onLog, flexNote, outFile) {
|
|
315
316
|
onLog("Analyzing codebase...");
|
|
316
|
-
const
|
|
317
|
+
const prompt = plannerPrompt(objective, workerModel, budget, concurrency, flexNote);
|
|
318
|
+
const fileInstruction = outFile ? `\n\nAFTER generating the JSON, also write it to ${outFile} using the Write tool.` : "";
|
|
319
|
+
const resultText = await runPlannerQuery(prompt + fileInstruction, { cwd, model: plannerModel, permissionMode }, onLog);
|
|
317
320
|
const parsed = await extractTaskJson(resultText, async () => {
|
|
318
|
-
onLog("Retrying
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
prompt: `Your previous response did not contain valid JSON. Output ONLY a JSON object:\n{"tasks":[{"prompt":"..."}]}`,
|
|
322
|
-
options: { cwd, model: plannerModel, permissionMode, ...(permissionMode === "bypassPermissions" && { allowDangerouslySkipPermissions: true }), persistSession: false },
|
|
323
|
-
})) {
|
|
324
|
-
if (msg.type === "result" && msg.subtype === "success")
|
|
325
|
-
retryText = msg.result || "";
|
|
326
|
-
}
|
|
327
|
-
return retryText;
|
|
328
|
-
});
|
|
321
|
+
onLog("Retrying...");
|
|
322
|
+
return runPlannerQuery(`Your previous response was not valid JSON. Respond with ONLY a JSON object {"tasks":[{"prompt":"..."}]}.\n\n${prompt}`, { cwd, model: plannerModel, permissionMode }, onLog);
|
|
323
|
+
}, onLog, outFile);
|
|
329
324
|
let tasks = (parsed.tasks || []).map((t, i) => ({
|
|
330
325
|
id: String(i),
|
|
331
326
|
prompt: typeof t === "string" ? t : t.prompt,
|
|
@@ -428,9 +423,10 @@ End with ## Priorities: rank the top 3 things that would most improve the result
|
|
|
428
423
|
},
|
|
429
424
|
];
|
|
430
425
|
}
|
|
431
|
-
export async function orchestrate(objective, designDocs, cwd, plannerModel, workerModel, permissionMode, budget, concurrency, onLog, flexNote) {
|
|
426
|
+
export async function orchestrate(objective, designDocs, cwd, plannerModel, workerModel, permissionMode, budget, concurrency, onLog, flexNote, outFile) {
|
|
432
427
|
const capability = modelCapabilityBlock(workerModel);
|
|
433
428
|
const flexLine = flexNote ? `\n\n${flexNote}` : "";
|
|
429
|
+
const fileInstruction = outFile ? `\n\nAFTER generating the JSON, also write it to ${outFile} using the Write tool.` : "";
|
|
434
430
|
const prompt = `You are a tech lead planning a sprint based on your team's codebase research.
|
|
435
431
|
|
|
436
432
|
Objective: ${objective}
|
|
@@ -452,21 +448,13 @@ Requirements:
|
|
|
452
448
|
- Priority order: foundational first, polish last${flexLine}
|
|
453
449
|
|
|
454
450
|
Respond with ONLY a JSON object (no markdown fences):
|
|
455
|
-
{"tasks": [{"prompt": "..."}]}`;
|
|
451
|
+
{"tasks": [{"prompt": "..."}]}${fileInstruction}`;
|
|
456
452
|
onLog("Synthesizing...");
|
|
457
453
|
const resultText = await runPlannerQuery(prompt, { cwd, model: plannerModel, permissionMode }, onLog);
|
|
458
454
|
const parsed = await extractTaskJson(resultText, async () => {
|
|
459
455
|
onLog("Retrying...");
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
prompt: `Output ONLY a JSON object:\n{"tasks":[{"prompt":"..."}]}`,
|
|
463
|
-
options: { cwd, model: plannerModel, permissionMode, ...(permissionMode === "bypassPermissions" && { allowDangerouslySkipPermissions: true }), persistSession: false },
|
|
464
|
-
})) {
|
|
465
|
-
if (msg.type === "result" && msg.subtype === "success")
|
|
466
|
-
retryText = msg.result || "";
|
|
467
|
-
}
|
|
468
|
-
return retryText;
|
|
469
|
-
});
|
|
456
|
+
return runPlannerQuery(`Your previous response was not valid JSON. Respond with ONLY a JSON object {"tasks":[{"prompt":"..."}]}.\n\n${prompt}`, { cwd, model: plannerModel, permissionMode }, onLog);
|
|
457
|
+
}, onLog, outFile);
|
|
470
458
|
let tasks = (parsed.tasks || []).map((t, i) => ({
|
|
471
459
|
id: String(i),
|
|
472
460
|
prompt: typeof t === "string" ? t : t.prompt,
|
|
@@ -505,16 +493,8 @@ Respond with ONLY a JSON object (no markdown):
|
|
|
505
493
|
const resultText = await runPlannerQuery(prompt, { cwd, model: plannerModel, permissionMode }, onLog);
|
|
506
494
|
const parsed = await extractTaskJson(resultText, async () => {
|
|
507
495
|
onLog("Retrying...");
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
prompt: `Output ONLY a JSON object:\n{"tasks":[{"prompt":"..."}]}`,
|
|
511
|
-
options: { cwd, model: plannerModel, permissionMode, ...(permissionMode === "bypassPermissions" && { allowDangerouslySkipPermissions: true }), persistSession: false },
|
|
512
|
-
})) {
|
|
513
|
-
if (msg.type === "result" && msg.subtype === "success")
|
|
514
|
-
retryText = msg.result || "";
|
|
515
|
-
}
|
|
516
|
-
return retryText;
|
|
517
|
-
});
|
|
496
|
+
return runPlannerQuery(`Your previous response was not valid JSON. Respond with ONLY a JSON object {"tasks":[{"prompt":"..."}]}.\n\n${prompt}`, { cwd, model: plannerModel, permissionMode }, onLog);
|
|
497
|
+
}, onLog);
|
|
518
498
|
let tasks = (parsed.tasks || []).map((t, i) => ({
|
|
519
499
|
id: String(i),
|
|
520
500
|
prompt: typeof t === "string" ? t : t.prompt,
|
|
@@ -574,17 +554,55 @@ function attemptJsonParse(text) {
|
|
|
574
554
|
catch { }
|
|
575
555
|
}
|
|
576
556
|
}
|
|
557
|
+
// Salvage truncated task JSON — find last complete task object and close
|
|
558
|
+
const tasksMatch = text.match(/\{\s*"tasks"\s*:\s*\[/);
|
|
559
|
+
if (tasksMatch) {
|
|
560
|
+
const lastBrace = text.lastIndexOf("}");
|
|
561
|
+
if (lastBrace > tasksMatch.index) {
|
|
562
|
+
const salvaged = text.slice(tasksMatch.index, lastBrace + 1) + "]}";
|
|
563
|
+
try {
|
|
564
|
+
const obj = JSON.parse(salvaged);
|
|
565
|
+
if (obj?.tasks?.length > 0)
|
|
566
|
+
return obj;
|
|
567
|
+
}
|
|
568
|
+
catch { }
|
|
569
|
+
}
|
|
570
|
+
}
|
|
577
571
|
return null;
|
|
578
572
|
}
|
|
579
|
-
/** Extract task JSON
|
|
580
|
-
async function extractTaskJson(raw, retry) {
|
|
573
|
+
/** Extract task JSON: try file first, then in-memory parse, then retry with context. */
|
|
574
|
+
async function extractTaskJson(raw, retry, onLog, outFile) {
|
|
575
|
+
// 1. Try reading from file (most resilient — survives truncated output)
|
|
576
|
+
if (outFile) {
|
|
577
|
+
try {
|
|
578
|
+
const fileContent = readFileSync(outFile, "utf-8");
|
|
579
|
+
const fromFile = attemptJsonParse(fileContent);
|
|
580
|
+
if (fromFile?.tasks)
|
|
581
|
+
return fromFile;
|
|
582
|
+
}
|
|
583
|
+
catch { }
|
|
584
|
+
}
|
|
585
|
+
// 2. Try parsing result text
|
|
581
586
|
const first = attemptJsonParse(raw);
|
|
582
587
|
if (first?.tasks)
|
|
583
588
|
return first;
|
|
589
|
+
onLog?.(`Parse failed (${raw.length} chars): ${raw.slice(0, 300)}`);
|
|
590
|
+
// 3. Retry with full context
|
|
584
591
|
const retryText = await retry();
|
|
592
|
+
// Re-check file in case retry wrote it
|
|
593
|
+
if (outFile) {
|
|
594
|
+
try {
|
|
595
|
+
const fileContent = readFileSync(outFile, "utf-8");
|
|
596
|
+
const fromFile = attemptJsonParse(fileContent);
|
|
597
|
+
if (fromFile?.tasks)
|
|
598
|
+
return fromFile;
|
|
599
|
+
}
|
|
600
|
+
catch { }
|
|
601
|
+
}
|
|
585
602
|
const second = attemptJsonParse(retryText);
|
|
586
603
|
if (second?.tasks)
|
|
587
604
|
return second;
|
|
605
|
+
onLog?.(`Retry failed (${retryText.length} chars): ${retryText.slice(0, 300)}`);
|
|
588
606
|
throw new Error("Planner did not return valid task JSON after retry");
|
|
589
607
|
}
|
|
590
608
|
// ── Wave steering ──
|
|
@@ -655,14 +673,7 @@ Respond with ONLY a JSON object (no markdown fences):
|
|
|
655
673
|
if (first)
|
|
656
674
|
return first;
|
|
657
675
|
onLog("Retrying...");
|
|
658
|
-
|
|
659
|
-
for await (const msg of query({
|
|
660
|
-
prompt: `Output ONLY a JSON object: {"action":"execute"|"reflect"|"done","done":true/false,"reasoning":"...","tasks":[{"prompt":"..."}]}`,
|
|
661
|
-
options: { cwd, model: plannerModel, permissionMode, ...(permissionMode === "bypassPermissions" && { allowDangerouslySkipPermissions: true }), persistSession: false },
|
|
662
|
-
})) {
|
|
663
|
-
if (msg.type === "result" && msg.subtype === "success")
|
|
664
|
-
retryText = msg.result || "";
|
|
665
|
-
}
|
|
676
|
+
const retryText = await runPlannerQuery(`Your previous response was not valid JSON. Respond with ONLY a JSON object {"action":"execute"|"reflect"|"done","done":true/false,"reasoning":"...","tasks":[{"prompt":"..."}]}.\n\n${prompt}`, { cwd, model: plannerModel, permissionMode }, onLog);
|
|
666
677
|
return attemptJsonParse(retryText) ?? { action: "done", done: true, reasoning: "Could not parse steering response" };
|
|
667
678
|
})();
|
|
668
679
|
const action = parsed.action || (parsed.done ? "done" : "execute");
|
package/dist/types.d.ts
CHANGED
|
@@ -125,7 +125,7 @@ export interface RunState {
|
|
|
125
125
|
accCompleted: number;
|
|
126
126
|
accFailed: number;
|
|
127
127
|
branches: BranchRecord[];
|
|
128
|
-
phase: "executing" | "steering" | "reflecting" | "done";
|
|
128
|
+
phase: "executing" | "steering" | "reflecting" | "capped" | "done";
|
|
129
129
|
startedAt: string;
|
|
130
130
|
cwd: string;
|
|
131
131
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-overnight",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.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": {
|