claude-overnight 1.16.12 → 1.16.16
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/QUICKSHEET_PLAYWRIGHT.md +65 -0
- package/README.md +30 -0
- package/dist/cli.js +5 -3
- package/dist/index.js +1 -1
- package/dist/providers.js +3 -3
- package/dist/render.d.ts +31 -0
- package/dist/render.js +232 -139
- package/dist/run.js +57 -7
- package/dist/ui.d.ts +23 -1
- package/dist/ui.js +233 -43
- package/package.json +6 -1
- package/plugins/claude-overnight/.claude-plugin/plugin.json +10 -0
- package/plugins/claude-overnight/skills/claude-overnight/SKILL.md +92 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Playwright Parallel Usage
|
|
2
|
+
|
|
3
|
+
When running claude-overnight with parallel agents that use the Playwright MCP server, avoid lock conflicts and session cross-contamination.
|
|
4
|
+
|
|
5
|
+
## Isolation Levels
|
|
6
|
+
|
|
7
|
+
| Goal | Approach |
|
|
8
|
+
|---|---|
|
|
9
|
+
| Non-disruptive, no focus steal | Headless mode (default) |
|
|
10
|
+
| Several agents in parallel, no shared cookies | Headless + each MCP server: `--isolated` (or `isolated: true`) |
|
|
11
|
+
| Several agents, each with saved login | Headless + each MCP server: unique `userDataDir` or its own `--storage-state` file |
|
|
12
|
+
| Anti-bot interception (CAPTCHA, Cloudflare) | Fall back to headed mode only when necessary |
|
|
13
|
+
|
|
14
|
+
**Headless preferred by default.** Every headed browser launch becomes the foreground app on macOS, which is disruptive during long runs. Only fall back to headed when anti-bot detection (CAPTCHA, Cloudflare challenge, etc.) requires visible browser interaction.
|
|
15
|
+
|
|
16
|
+
## MCP Server Configuration
|
|
17
|
+
|
|
18
|
+
Add to your `settings.json` or `.claude/settings.local.json`:
|
|
19
|
+
|
|
20
|
+
```json
|
|
21
|
+
{
|
|
22
|
+
"mcpServers": {
|
|
23
|
+
"playwright-1": {
|
|
24
|
+
"command": "npx",
|
|
25
|
+
"args": ["@anthropic-ai/mcp-playwright@latest", "--isolated", "--headless"],
|
|
26
|
+
"env": {}
|
|
27
|
+
},
|
|
28
|
+
"playwright-2": {
|
|
29
|
+
"command": "npx",
|
|
30
|
+
"args": ["@anthropic-ai/mcp-playwright@latest", "--isolated", "--headless"],
|
|
31
|
+
"env": {}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
For saved logins, give each server its own `userDataDir`:
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"mcpServers": {
|
|
42
|
+
"playwright-agent-a": {
|
|
43
|
+
"command": "npx",
|
|
44
|
+
"args": ["@anthropic-ai/mcp-playwright@latest", "--headless", "--userDataDir", "/tmp/pw-agent-a"],
|
|
45
|
+
"env": {}
|
|
46
|
+
},
|
|
47
|
+
"playwright-agent-b": {
|
|
48
|
+
"command": "npx",
|
|
49
|
+
"args": ["@anthropic-ai/mcp-playwright@latest", "--headless", "--userDataDir", "/tmp/pw-agent-b"],
|
|
50
|
+
"env": {}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Context7 Documentation
|
|
57
|
+
|
|
58
|
+
For the latest Playwright API docs and patterns:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npx ctx7@latest library playwright "parallel browser instances isolation"
|
|
62
|
+
npx ctx7@latest docs <libraryId> "parallel browser instances"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Note:** ctx7 requires authentication (`npx ctx7@latest login` or `CONTEXT7_API_KEY` env var). If unauthenticated, lookups will fail — agents should fall back to training data.
|
package/README.md
CHANGED
|
@@ -280,6 +280,36 @@ Saved providers live user-level at `~/.claude/claude-overnight/providers.json` (
|
|
|
280
280
|
|
|
281
281
|
**Non-interactive / CI.** `claude-overnight --model=qwen3.6-plus` auto-resolves the model id to a saved provider — no separate `--provider` flag.
|
|
282
282
|
|
|
283
|
+
## Parallel Playwright Testing
|
|
284
|
+
|
|
285
|
+
When agents use the Playwright MCP server for testing, parallel instances conflict on browser locks and cookie state. Add multiple MCP entries to `settings.json`:
|
|
286
|
+
|
|
287
|
+
```json
|
|
288
|
+
{
|
|
289
|
+
"mcpServers": {
|
|
290
|
+
"playwright-1": {
|
|
291
|
+
"command": "npx",
|
|
292
|
+
"args": ["@anthropic-ai/mcp-playwright@latest", "--isolated", "--headless"]
|
|
293
|
+
},
|
|
294
|
+
"playwright-2": {
|
|
295
|
+
"command": "npx",
|
|
296
|
+
"args": ["@anthropic-ai/mcp-playwright@latest", "--isolated", "--headless"]
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
**Isolation levels:**
|
|
303
|
+
|
|
304
|
+
| Goal | Approach |
|
|
305
|
+
|---|---|
|
|
306
|
+
| Non-disruptive, no focus steal | Headless mode (default) |
|
|
307
|
+
| Parallel agents, no shared cookies | Headless + `--isolated` per MCP server |
|
|
308
|
+
| Parallel agents, each with saved login | Headless + unique `userDataDir` or `--storage-state` per server |
|
|
309
|
+
| Anti-bot interception (CAPTCHA, Cloudflare) | Drop `--headless` only when necessary |
|
|
310
|
+
|
|
311
|
+
See `QUICKSHEET_PLAYWRIGHT.md` for full config examples.
|
|
312
|
+
|
|
283
313
|
## Spend caps and usage controls
|
|
284
314
|
|
|
285
315
|
### Extra usage protection
|
package/dist/cli.js
CHANGED
|
@@ -219,10 +219,12 @@ export function ask(question) {
|
|
|
219
219
|
redraw();
|
|
220
220
|
continue;
|
|
221
221
|
}
|
|
222
|
-
//
|
|
223
|
-
// (arrow keys, function keys, etc. arrive as \x1B [ ... letter)
|
|
222
|
+
// ESC submits the current input (same as Enter)
|
|
224
223
|
if (ch === "\x1B") {
|
|
225
|
-
|
|
224
|
+
stdout.write("\n");
|
|
225
|
+
cleanup();
|
|
226
|
+
resolve(segmentsToString(segs).trim());
|
|
227
|
+
return;
|
|
226
228
|
}
|
|
227
229
|
const code = ch.charCodeAt(0);
|
|
228
230
|
if (code < 0x20)
|
package/dist/index.js
CHANGED
|
@@ -541,7 +541,7 @@ async function main() {
|
|
|
541
541
|
const plannerPick = await pickModel(`${chalk.cyan("④")} Planner model ${chalk.dim("(thinking, steering — use your strongest)")}:`, models);
|
|
542
542
|
plannerModel = plannerPick.model;
|
|
543
543
|
plannerProvider = plannerPick.provider;
|
|
544
|
-
const workerPick = await pickModel(`${chalk.cyan("⑤")} Executor model ${chalk.dim("(what runs the tasks — Qwen/OpenRouter/etc via Other…)")}:`, models);
|
|
544
|
+
const workerPick = await pickModel(`${chalk.cyan("⑤")} Executor model ${chalk.dim("(what runs the tasks — Qwen 3.6 Plus / OpenRouter / etc via Other…)")}:`, models);
|
|
545
545
|
workerModel = workerPick.model;
|
|
546
546
|
workerProvider = workerPick.provider;
|
|
547
547
|
usageCap = await select(`${chalk.cyan("⑥")} Usage cap:`, [
|
package/dist/providers.js
CHANGED
|
@@ -92,7 +92,7 @@ export async function pickModel(label, anthropicModels, currentModelId) {
|
|
|
92
92
|
const keySrc = p.keyEnv ? `env ${p.keyEnv}` : "stored key";
|
|
93
93
|
items.push({ name: `${p.displayName}`, value: { kind: "provider", provider: p }, hint: `${p.model} · ${keySrc}` });
|
|
94
94
|
}
|
|
95
|
-
items.push({ name: chalk.cyan("Other…"), value: { kind: "other" }, hint: "
|
|
95
|
+
items.push({ name: chalk.cyan("Other…"), value: { kind: "other" }, hint: "Qwen 3.6 Plus, OpenRouter, or any Anthropic-compatible endpoint" });
|
|
96
96
|
let defaultIdx = 0;
|
|
97
97
|
if (currentModelId) {
|
|
98
98
|
const i = items.findIndex(it => {
|
|
@@ -126,11 +126,11 @@ async function promptNewProvider() {
|
|
|
126
126
|
if (!displayName)
|
|
127
127
|
return null;
|
|
128
128
|
const id = slugify(displayName);
|
|
129
|
-
const baseURLRaw = await ask(`\n ${chalk.cyan("Base URL")} ${chalk.dim("(e.g. https://dashscope-intl.aliyuncs.com/
|
|
129
|
+
const baseURLRaw = await ask(`\n ${chalk.cyan("Base URL")} ${chalk.dim("(e.g. https://dashscope-intl.aliyuncs.com/apps/anthropic for Qwen 3.6 Plus):")} `);
|
|
130
130
|
if (!baseURLRaw)
|
|
131
131
|
return null;
|
|
132
132
|
const baseURL = normalizeBaseURL(baseURLRaw);
|
|
133
|
-
const model = await ask(`\n ${chalk.cyan("Model id")} ${chalk.dim("(e.g. qwen3-
|
|
133
|
+
const model = await ask(`\n ${chalk.cyan("Model id")} ${chalk.dim("(e.g. qwen3.6-plus):")} `);
|
|
134
134
|
if (!model)
|
|
135
135
|
return null;
|
|
136
136
|
const keyMode = await select(` ${chalk.cyan("API key source")}:`, [
|
package/dist/render.d.ts
CHANGED
|
@@ -1,9 +1,40 @@
|
|
|
1
1
|
import type { Swarm } from "./swarm.js";
|
|
2
2
|
import type { RateLimitWindow } from "./types.js";
|
|
3
3
|
import type { RunInfo, SteeringContext, SteeringEvent } from "./ui.js";
|
|
4
|
+
export interface Section {
|
|
5
|
+
title: string;
|
|
6
|
+
rows: string[];
|
|
7
|
+
scrollable?: boolean;
|
|
8
|
+
highlightKey?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface ContentRenderer {
|
|
11
|
+
/** Returns an array of sections to render in the content area */
|
|
12
|
+
sections(): Section[];
|
|
13
|
+
}
|
|
4
14
|
export declare function truncate(s: string, max: number): string;
|
|
5
15
|
export declare function fmtTokens(n: number): string;
|
|
6
16
|
export declare function fmtDur(ms: number): string;
|
|
17
|
+
export declare function renderUnifiedFrame(params: {
|
|
18
|
+
model?: string;
|
|
19
|
+
phase: string;
|
|
20
|
+
barPct: number;
|
|
21
|
+
barLabel: string;
|
|
22
|
+
active?: number;
|
|
23
|
+
blocked?: number;
|
|
24
|
+
queued?: number;
|
|
25
|
+
startedAt: number;
|
|
26
|
+
totalIn: number;
|
|
27
|
+
totalOut: number;
|
|
28
|
+
totalCost: number;
|
|
29
|
+
waveNum: number;
|
|
30
|
+
sessionsUsed: number;
|
|
31
|
+
sessionsBudget: number;
|
|
32
|
+
remaining: number;
|
|
33
|
+
usageBarRender?: (out: string[], w: number) => void;
|
|
34
|
+
content: ContentRenderer;
|
|
35
|
+
hotkeyRow?: string;
|
|
36
|
+
extraFooterRows?: string[];
|
|
37
|
+
}): string;
|
|
7
38
|
type RLGetter = () => {
|
|
8
39
|
utilization: number;
|
|
9
40
|
isUsingOverage: boolean;
|
package/dist/render.js
CHANGED
|
@@ -138,9 +138,57 @@ function renderUsageBars(out, w, swarm) {
|
|
|
138
138
|
out.push(` ${chalk.dim("Extra ")}${barStr} ${label}`);
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
|
-
|
|
141
|
+
// ── Unified frame renderer ──
|
|
142
|
+
export function renderUnifiedFrame(params) {
|
|
142
143
|
const w = Math.max((process.stdout.columns ?? 80) || 80, 60);
|
|
143
144
|
const out = [];
|
|
145
|
+
// Header
|
|
146
|
+
renderHeader(out, w, {
|
|
147
|
+
model: params.model,
|
|
148
|
+
phase: params.phase,
|
|
149
|
+
barPct: params.barPct,
|
|
150
|
+
barLabel: params.barLabel,
|
|
151
|
+
active: params.active ?? 0,
|
|
152
|
+
blocked: params.blocked,
|
|
153
|
+
queued: params.queued ?? 0,
|
|
154
|
+
startedAt: params.startedAt,
|
|
155
|
+
totalIn: params.totalIn,
|
|
156
|
+
totalOut: params.totalOut,
|
|
157
|
+
totalCost: params.totalCost,
|
|
158
|
+
waveNum: params.waveNum,
|
|
159
|
+
sessionsUsed: params.sessionsUsed,
|
|
160
|
+
sessionsBudget: params.sessionsBudget,
|
|
161
|
+
remaining: params.remaining,
|
|
162
|
+
});
|
|
163
|
+
// Usage bar
|
|
164
|
+
if (params.usageBarRender) {
|
|
165
|
+
params.usageBarRender(out, w);
|
|
166
|
+
}
|
|
167
|
+
out.push("");
|
|
168
|
+
// Content sections
|
|
169
|
+
const sections = params.content.sections();
|
|
170
|
+
for (const sec of sections) {
|
|
171
|
+
if (sec.title) {
|
|
172
|
+
section(out, w, sec.title);
|
|
173
|
+
}
|
|
174
|
+
for (const row of sec.rows) {
|
|
175
|
+
out.push(row);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// Footer
|
|
179
|
+
out.push("");
|
|
180
|
+
if (params.hotkeyRow) {
|
|
181
|
+
out.push(params.hotkeyRow);
|
|
182
|
+
}
|
|
183
|
+
if (params.extraFooterRows) {
|
|
184
|
+
for (const row of params.extraFooterRows) {
|
|
185
|
+
out.push(row);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
out.push("");
|
|
189
|
+
return out.join("\n");
|
|
190
|
+
}
|
|
191
|
+
export function renderFrame(swarm, showHotkeys, runInfo, selectedAgentId) {
|
|
144
192
|
const stoppingTag = swarm.aborted ? chalk.yellow("STOPPING") : "";
|
|
145
193
|
const pausedTag = swarm.paused ? chalk.yellow("PAUSED") : "";
|
|
146
194
|
const stallTag = swarm.stallLevel >= 3 ? chalk.red("STALL") : swarm.stallLevel > 0 ? chalk.yellow(`STALL L${swarm.stallLevel}`) : "";
|
|
@@ -149,93 +197,90 @@ export function renderFrame(swarm, showHotkeys, runInfo, selectedAgentId) {
|
|
|
149
197
|
: swarm.rateLimitPaused > 0 ? chalk.yellow("COOLING") : "";
|
|
150
198
|
const phase = [phaseLabel, pausedTag, stallTag, stoppingTag].filter(Boolean).join(" ");
|
|
151
199
|
const waveUsed = swarm.completed + swarm.failed;
|
|
152
|
-
renderHeader(out, w, {
|
|
153
|
-
model: runInfo?.model ?? swarm.model,
|
|
154
|
-
phase,
|
|
155
|
-
barPct: swarm.total > 0 ? swarm.completed / swarm.total : 0,
|
|
156
|
-
barLabel: `${swarm.completed}/${swarm.total}`,
|
|
157
|
-
active: swarm.active, blocked: swarm.blocked, queued: swarm.pending,
|
|
158
|
-
startedAt: runInfo?.startedAt ?? swarm.startedAt,
|
|
159
|
-
totalIn: (runInfo?.accIn ?? 0) + swarm.totalInputTokens,
|
|
160
|
-
totalOut: (runInfo?.accOut ?? 0) + swarm.totalOutputTokens,
|
|
161
|
-
totalCost: (runInfo?.accCost ?? swarm.baseCostUsd) + swarm.totalCostUsd,
|
|
162
|
-
waveNum: runInfo?.waveNum ?? -1,
|
|
163
|
-
sessionsUsed: (runInfo ? runInfo.accCompleted + runInfo.accFailed : 0) + waveUsed,
|
|
164
|
-
sessionsBudget: runInfo?.sessionsBudget ?? swarm.total,
|
|
165
|
-
remaining: Math.max(0, (runInfo?.remaining ?? swarm.total) - waveUsed),
|
|
166
|
-
});
|
|
167
|
-
renderUsageBars(out, w, swarm);
|
|
168
|
-
out.push("");
|
|
169
|
-
// Agent table
|
|
170
200
|
const running = swarm.agents.filter(a => a.status === "running");
|
|
171
201
|
const finished = swarm.agents.filter(a => a.status !== "running");
|
|
172
202
|
const showFinished = finished.slice(-Math.max(2, 12 - running.length));
|
|
173
203
|
const show = [...running, ...showFinished];
|
|
174
|
-
if (show.length > 0) {
|
|
175
|
-
out.push(chalk.gray(" # Status Task" + " ".repeat(Math.max(1, w - 56)) + "Action"));
|
|
176
|
-
out.push(chalk.gray(" " + "\u2500".repeat(Math.min(w - 4, 100))));
|
|
177
|
-
for (const a of show)
|
|
178
|
-
out.push(fmtRow(a, w, a.id === (selectedAgentId ?? -1)));
|
|
179
|
-
if (swarm.pending > 0)
|
|
180
|
-
out.push(chalk.gray(` ... + ${swarm.pending} queued`));
|
|
181
|
-
}
|
|
182
|
-
// ── Agent detail (progressive discovery) ──
|
|
183
204
|
const detailAgent = selectedAgentId != null
|
|
184
205
|
? swarm.agents.find(a => a.id === selectedAgentId)
|
|
185
206
|
: undefined;
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
207
|
+
const content = {
|
|
208
|
+
sections() {
|
|
209
|
+
const secs = [];
|
|
210
|
+
// Agent table (undecorated — raw header + rows)
|
|
211
|
+
if (show.length > 0) {
|
|
212
|
+
const rows = [
|
|
213
|
+
chalk.gray(" # Status Task" + " ".repeat(Math.max(1, (process.stdout.columns ?? 80) || 80, 60) - 56)) + "Action",
|
|
214
|
+
chalk.gray(" " + "\u2500".repeat(Math.min(Math.max((process.stdout.columns ?? 80) || 80, 60) - 4, 100))),
|
|
215
|
+
];
|
|
216
|
+
for (const a of show)
|
|
217
|
+
rows.push(fmtRow(a, (process.stdout.columns ?? 80) || 80, a.id === (selectedAgentId ?? -1)));
|
|
218
|
+
if (swarm.pending > 0)
|
|
219
|
+
rows.push(chalk.gray(` ... + ${swarm.pending} queued`));
|
|
220
|
+
secs.push({ title: "", rows });
|
|
221
|
+
}
|
|
222
|
+
// Agent detail (decorated)
|
|
223
|
+
if (detailAgent) {
|
|
224
|
+
const rows = [];
|
|
225
|
+
const taskLines = detailAgent.task.prompt.split("\n");
|
|
226
|
+
const maxTaskLines = Math.min(6, taskLines.length);
|
|
227
|
+
const ww = Math.max((process.stdout.columns ?? 80) || 80, 60);
|
|
228
|
+
for (let i = 0; i < maxTaskLines; i++) {
|
|
229
|
+
rows.push(` ${chalk.dim(truncate(taskLines[i].trim(), ww - 6))}`);
|
|
230
|
+
}
|
|
231
|
+
if (taskLines.length > maxTaskLines)
|
|
232
|
+
rows.push(chalk.dim(` \u2026 + ${taskLines.length - maxTaskLines} more lines`));
|
|
233
|
+
const meta = [];
|
|
234
|
+
if (detailAgent.currentTool)
|
|
235
|
+
meta.push(chalk.yellow(`tool: ${detailAgent.currentTool}`));
|
|
236
|
+
if (detailAgent.lastText)
|
|
237
|
+
meta.push(chalk.dim(truncate(detailAgent.lastText, 60)));
|
|
238
|
+
if (detailAgent.filesChanged != null)
|
|
239
|
+
meta.push(chalk.dim(`${detailAgent.filesChanged} files`));
|
|
240
|
+
if (detailAgent.costUsd != null)
|
|
241
|
+
meta.push(chalk.yellow(`$${detailAgent.costUsd.toFixed(3)}`));
|
|
242
|
+
if (detailAgent.toolCalls > 0)
|
|
243
|
+
meta.push(chalk.dim(`${detailAgent.toolCalls} tools`));
|
|
244
|
+
if (meta.length > 0)
|
|
245
|
+
rows.push(` ${meta.join(chalk.dim(" \u00b7 "))}`);
|
|
246
|
+
secs.push({ title: `Agent ${detailAgent.id} detail \u00b7 [d] next \u00b7 [Esc] close`, rows });
|
|
247
|
+
}
|
|
248
|
+
// Merge results (undecorated)
|
|
249
|
+
if (swarm.mergeResults.length > 0) {
|
|
250
|
+
const ww = Math.max((process.stdout.columns ?? 80) || 80, 60);
|
|
251
|
+
const rows = [chalk.gray(" \u2500\u2500\u2500 Merges " + "\u2500".repeat(Math.min(ww - 16, 90)))];
|
|
252
|
+
for (const mr of swarm.mergeResults) {
|
|
253
|
+
const icon = mr.ok ? chalk.green("\u2713") : chalk.red("\u2717");
|
|
254
|
+
const info = mr.ok ? chalk.dim(`${mr.filesChanged} file(s)`) : chalk.red(truncate(mr.error || "conflict", 40));
|
|
255
|
+
rows.push(` ${icon} ${mr.branch} ${info}`);
|
|
256
|
+
}
|
|
257
|
+
secs.push({ title: "", rows });
|
|
258
|
+
}
|
|
259
|
+
// Event log (undecorated)
|
|
260
|
+
const ww = Math.max((process.stdout.columns ?? 80) || 80, 60);
|
|
261
|
+
const eventRows = [chalk.gray(" \u2500\u2500\u2500 Events " + "\u2500".repeat(Math.min(ww - 16, 90)))];
|
|
262
|
+
const logN = Math.min(12, swarm.logs.length);
|
|
263
|
+
for (const entry of swarm.logs.slice(-logN)) {
|
|
264
|
+
const t = new Date(entry.time).toLocaleTimeString("en", { hour12: false });
|
|
265
|
+
const tag = entry.agentId < 0 ? chalk.magenta("[sys]") : chalk.cyan(`[${entry.agentId}]`);
|
|
266
|
+
const arrowIdx = entry.text.indexOf(" \u2192 ");
|
|
267
|
+
if (arrowIdx > 0 && arrowIdx < 20) {
|
|
268
|
+
const toolName = entry.text.slice(0, arrowIdx);
|
|
269
|
+
const target = entry.text.slice(arrowIdx + 3);
|
|
270
|
+
eventRows.push(chalk.gray(` ${t} `) + tag + ` ${chalk.yellow(toolName)}`);
|
|
271
|
+
eventRows.push(chalk.dim(` ${truncate(target, ww - 10)}`));
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
eventRows.push(chalk.gray(` ${t} `) + tag + ` ${colorEvent(truncate(entry.text, ww - 22))}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
secs.push({ title: "", rows: eventRows });
|
|
278
|
+
return secs;
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
// Build footer
|
|
282
|
+
let hotkeyRow;
|
|
283
|
+
const extraFooterRows = [];
|
|
239
284
|
if (showHotkeys) {
|
|
240
285
|
const pending = runInfo?.pendingSteer ?? 0;
|
|
241
286
|
const chip = pending > 0 ? chalk.cyan(` \u270E ${pending} steer queued`) : "";
|
|
@@ -244,13 +289,32 @@ export function renderFrame(swarm, showHotkeys, runInfo, selectedAgentId) {
|
|
|
244
289
|
const pauseLabel = swarm.paused ? "[p] resume" : "[p] pause";
|
|
245
290
|
const detailChip = swarm.active > 0 ? chalk.dim(" [d] detail") : "";
|
|
246
291
|
const selectChip = swarm.active > 0 && running.length <= 10 ? chalk.dim(" [0-9] select") : "";
|
|
247
|
-
|
|
292
|
+
hotkeyRow = chalk.dim(` [b] budget [t] cap [c] conc [e] extra ${pauseLabel} [s] steer [?] ask [q] stop`) + fixChip + retryChip + chip + detailChip + selectChip;
|
|
248
293
|
if (swarm.blocked > 0 && swarm.blocked === swarm.active) {
|
|
249
|
-
|
|
294
|
+
extraFooterRows.push(chalk.yellow(` all workers rate-limited — [r] retry-now, [c] reduce concurrency, [p] pause, [q] quit`));
|
|
250
295
|
}
|
|
251
296
|
}
|
|
252
|
-
|
|
253
|
-
|
|
297
|
+
return renderUnifiedFrame({
|
|
298
|
+
model: runInfo?.model ?? swarm.model,
|
|
299
|
+
phase,
|
|
300
|
+
barPct: swarm.total > 0 ? swarm.completed / swarm.total : 0,
|
|
301
|
+
barLabel: `${swarm.completed}/${swarm.total}`,
|
|
302
|
+
active: swarm.active,
|
|
303
|
+
blocked: swarm.blocked,
|
|
304
|
+
queued: swarm.pending,
|
|
305
|
+
startedAt: runInfo?.startedAt ?? swarm.startedAt,
|
|
306
|
+
totalIn: (runInfo?.accIn ?? 0) + swarm.totalInputTokens,
|
|
307
|
+
totalOut: (runInfo?.accOut ?? 0) + swarm.totalOutputTokens,
|
|
308
|
+
totalCost: (runInfo?.accCost ?? swarm.baseCostUsd) + swarm.totalCostUsd,
|
|
309
|
+
waveNum: runInfo?.waveNum ?? -1,
|
|
310
|
+
sessionsUsed: (runInfo ? runInfo.accCompleted + runInfo.accFailed : 0) + waveUsed,
|
|
311
|
+
sessionsBudget: runInfo?.sessionsBudget ?? swarm.total,
|
|
312
|
+
remaining: Math.max(0, (runInfo?.remaining ?? swarm.total) - waveUsed),
|
|
313
|
+
usageBarRender: (out, w) => renderUsageBars(out, w, swarm),
|
|
314
|
+
content,
|
|
315
|
+
hotkeyRow,
|
|
316
|
+
extraFooterRows,
|
|
317
|
+
});
|
|
254
318
|
}
|
|
255
319
|
function section(out, w, title) {
|
|
256
320
|
const inner = ` ${title} `;
|
|
@@ -324,70 +388,99 @@ function renderStatusBlock(out, w, status) {
|
|
|
324
388
|
out.push(` ${chalk.dim(truncate(ln.trim(), w - 4))}`);
|
|
325
389
|
}
|
|
326
390
|
export function renderSteeringFrame(runInfo, data, showHotkeys, rlGetter) {
|
|
327
|
-
const w = Math.max((process.stdout.columns ?? 80) || 80, 60);
|
|
328
|
-
const out = [];
|
|
329
391
|
const totalUsed = runInfo.accCompleted + runInfo.accFailed;
|
|
330
|
-
renderHeader(out, w, {
|
|
331
|
-
model: runInfo.model,
|
|
332
|
-
phase: chalk.magenta("STEERING"),
|
|
333
|
-
barPct: runInfo.sessionsBudget > 0 ? totalUsed / runInfo.sessionsBudget : 0,
|
|
334
|
-
barLabel: `${totalUsed}/${runInfo.sessionsBudget}`,
|
|
335
|
-
active: 0, queued: 0,
|
|
336
|
-
startedAt: runInfo.startedAt,
|
|
337
|
-
totalIn: runInfo.accIn, totalOut: runInfo.accOut, totalCost: runInfo.accCost,
|
|
338
|
-
waveNum: runInfo.waveNum,
|
|
339
|
-
sessionsUsed: totalUsed, sessionsBudget: runInfo.sessionsBudget, remaining: runInfo.remaining,
|
|
340
|
-
});
|
|
341
|
-
const rl = rlGetter?.();
|
|
342
|
-
if (rl && (rl.utilization > 0 || rl.windows.size > 0))
|
|
343
|
-
renderSteeringUsageBar(out, w, rl);
|
|
344
|
-
out.push("");
|
|
345
392
|
const ctx = data.context;
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
//
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
393
|
+
const content = {
|
|
394
|
+
sections() {
|
|
395
|
+
const secs = [];
|
|
396
|
+
const ww = Math.max((process.stdout.columns ?? 80) || 80, 60);
|
|
397
|
+
// Objective (undecorated — raw line)
|
|
398
|
+
if (ctx?.objective) {
|
|
399
|
+
const obj = ctx.objective.replace(/\s+/g, " ").trim();
|
|
400
|
+
secs.push({ title: "", rows: [
|
|
401
|
+
` ${chalk.bold.white("Objective")} ${chalk.dim(truncate(obj, ww - 15))}`,
|
|
402
|
+
"",
|
|
403
|
+
] });
|
|
404
|
+
}
|
|
405
|
+
// Status (decorated via renderStatusBlock)
|
|
406
|
+
if (ctx?.status) {
|
|
407
|
+
const statusRows = [];
|
|
408
|
+
renderStatusBlock(statusRows, ww, ctx.status);
|
|
409
|
+
if (statusRows.length > 0) {
|
|
410
|
+
statusRows.push("");
|
|
411
|
+
secs.push({ title: "", rows: statusRows });
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
// Last wave (decorated via renderLastWave)
|
|
415
|
+
if (ctx?.lastWave && ctx.lastWave.tasks.length > 0) {
|
|
416
|
+
const lwRows = [];
|
|
417
|
+
renderLastWave(lwRows, ww, ctx.lastWave);
|
|
418
|
+
lwRows.push("");
|
|
419
|
+
secs.push({ title: "", rows: lwRows });
|
|
420
|
+
}
|
|
421
|
+
// Planner activity (decorated)
|
|
422
|
+
const plannerRows = [];
|
|
423
|
+
const events = data.events.slice(-15);
|
|
424
|
+
if (events.length === 0) {
|
|
425
|
+
plannerRows.push(chalk.dim(" (waiting for planner\u2026)"));
|
|
374
426
|
}
|
|
375
427
|
else {
|
|
376
|
-
|
|
428
|
+
for (const e of events) {
|
|
429
|
+
const t = new Date(e.time).toLocaleTimeString("en", { hour12: false });
|
|
430
|
+
const arrowIdx = e.text.indexOf(" \u2192 ");
|
|
431
|
+
if (arrowIdx > 0 && arrowIdx < 30) {
|
|
432
|
+
const toolName = e.text.slice(0, arrowIdx);
|
|
433
|
+
const target = e.text.slice(arrowIdx + 3);
|
|
434
|
+
plannerRows.push(chalk.gray(` ${t} `) + chalk.magenta("[plan] ") + chalk.yellow(toolName));
|
|
435
|
+
plannerRows.push(chalk.dim(` ${truncate(target, ww - 10)}`));
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
plannerRows.push(chalk.gray(` ${t} `) + chalk.magenta("[plan] ") + colorEvent(truncate(e.text, ww - 22)));
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
secs.push({ title: "Planner activity", rows: plannerRows });
|
|
443
|
+
// Status line (undecorated)
|
|
444
|
+
const liveClean = data.statusLine.replace(/\n/g, " ");
|
|
445
|
+
secs.push({ title: "", rows: [` ${chalk.cyan("\u25B6")} ${chalk.dim(truncate(liveClean, ww - 6))}`] });
|
|
446
|
+
return secs;
|
|
447
|
+
},
|
|
448
|
+
};
|
|
449
|
+
// Usage bar
|
|
450
|
+
const usageBarRender = rlGetter
|
|
451
|
+
? (out, w) => {
|
|
452
|
+
const rl = rlGetter();
|
|
453
|
+
if (rl && (rl.utilization > 0 || rl.windows.size > 0)) {
|
|
454
|
+
renderSteeringUsageBar(out, w, rl);
|
|
377
455
|
}
|
|
378
456
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
out.push(` ${chalk.cyan("\u25B6")} ${chalk.dim(truncate(liveClean, w - 6))}`);
|
|
383
|
-
out.push("");
|
|
457
|
+
: undefined;
|
|
458
|
+
// Footer
|
|
459
|
+
let hotkeyRow;
|
|
384
460
|
if (showHotkeys) {
|
|
385
461
|
const pending = runInfo?.pendingSteer ?? 0;
|
|
386
462
|
const chip = pending > 0 ? chalk.cyan(` \u270E ${pending} steer queued`) : "";
|
|
387
|
-
|
|
463
|
+
hotkeyRow = chalk.dim(" [b] budget [s] steer [q] stop") + chip;
|
|
388
464
|
}
|
|
389
|
-
|
|
390
|
-
|
|
465
|
+
return renderUnifiedFrame({
|
|
466
|
+
model: runInfo.model,
|
|
467
|
+
phase: chalk.magenta(`STEERING \u2192 wave ${runInfo.waveNum + 2}`),
|
|
468
|
+
barPct: runInfo.sessionsBudget > 0 ? totalUsed / runInfo.sessionsBudget : 0,
|
|
469
|
+
barLabel: `${totalUsed}/${runInfo.sessionsBudget}`,
|
|
470
|
+
active: 0,
|
|
471
|
+
queued: 0,
|
|
472
|
+
startedAt: runInfo.startedAt,
|
|
473
|
+
totalIn: runInfo.accIn,
|
|
474
|
+
totalOut: runInfo.accOut,
|
|
475
|
+
totalCost: runInfo.accCost,
|
|
476
|
+
waveNum: runInfo.waveNum,
|
|
477
|
+
sessionsUsed: totalUsed,
|
|
478
|
+
sessionsBudget: runInfo.sessionsBudget,
|
|
479
|
+
remaining: runInfo.remaining,
|
|
480
|
+
usageBarRender,
|
|
481
|
+
content,
|
|
482
|
+
hotkeyRow,
|
|
483
|
+
});
|
|
391
484
|
}
|
|
392
485
|
export function renderSummary(swarm) {
|
|
393
486
|
const w = Math.max((process.stdout.columns ?? 80) || 80, 60);
|
package/dist/run.js
CHANGED
|
@@ -205,8 +205,35 @@ export async function executeRun(cfg) {
|
|
|
205
205
|
};
|
|
206
206
|
process.on("SIGINT", gracefulStop);
|
|
207
207
|
process.on("SIGTERM", gracefulStop);
|
|
208
|
-
|
|
209
|
-
|
|
208
|
+
const crashHandler = (label, detail) => {
|
|
209
|
+
// Save run state with currentTasks so resume can pick up where we left off.
|
|
210
|
+
try {
|
|
211
|
+
saveRunState(runDir, {
|
|
212
|
+
id: `run-${new Date().toISOString().slice(0, 19)}`, objective: objective ?? "", budget: cfg.budget,
|
|
213
|
+
remaining, workerModel, plannerModel,
|
|
214
|
+
workerProviderId: cfg.workerProvider?.id, plannerProviderId: cfg.plannerProvider?.id,
|
|
215
|
+
concurrency, permissionMode,
|
|
216
|
+
usageCap, allowExtraUsage: cfg.allowExtraUsage, extraUsageBudget: cfg.extraUsageBudget,
|
|
217
|
+
flex, useWorktrees, mergeStrategy, waveNum, currentTasks,
|
|
218
|
+
accCost, accCompleted, accFailed, accIn, accOut, accTools,
|
|
219
|
+
branches, phase: "stopped", startedAt: new Date(cfg.runStartedAt).toISOString(), cwd,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
catch { }
|
|
223
|
+
// Save partial wave session if swarm was running.
|
|
224
|
+
if (currentSwarm?.agents.length) {
|
|
225
|
+
try {
|
|
226
|
+
saveWaveSession(runDir, waveNum, currentSwarm.agents, currentSwarm.totalCostUsd);
|
|
227
|
+
}
|
|
228
|
+
catch { }
|
|
229
|
+
}
|
|
230
|
+
display.stop();
|
|
231
|
+
restore();
|
|
232
|
+
console.error(chalk.red(`\n ${label}: ${detail}`));
|
|
233
|
+
process.exit(1);
|
|
234
|
+
};
|
|
235
|
+
process.on("uncaughtException", (err) => { currentSwarm?.abort(); currentSwarm?.cleanup(); crashHandler("Uncaught", err instanceof Error ? err.message : String(err)); });
|
|
236
|
+
process.on("unhandledRejection", (reason) => { currentSwarm?.abort(); currentSwarm?.cleanup(); crashHandler("Unhandled", reason instanceof Error ? reason.message : String(reason)); });
|
|
210
237
|
// Shared steering logic used by both resume-steering and in-loop steering
|
|
211
238
|
const runSteering = async () => {
|
|
212
239
|
let steered = false;
|
|
@@ -300,19 +327,33 @@ export async function executeRun(cfg) {
|
|
|
300
327
|
while (runAnotherRound) {
|
|
301
328
|
runAnotherRound = false;
|
|
302
329
|
while (remaining > 0 && currentTasks.length > 0 && !stopping) {
|
|
330
|
+
// Health check: runs once per process start to fix a broken build before
|
|
331
|
+
// any real work begins. Only triggers when there's nothing else queued —
|
|
332
|
+
// it must NEVER override tasks that steering has already planned.
|
|
303
333
|
if (!lastHealed) {
|
|
334
|
+
lastHealed = true;
|
|
335
|
+
// Only replace tasks with health fix if the queue was essentially empty.
|
|
336
|
+
// Steering-planned tasks always take priority.
|
|
304
337
|
const healTask = checkProjectHealth(cwd);
|
|
305
|
-
if (healTask && remaining > 0) {
|
|
306
|
-
lastHealed = true;
|
|
338
|
+
if (healTask && remaining > 0 && currentTasks.length <= 1) {
|
|
307
339
|
currentTasks = [healTask];
|
|
308
340
|
}
|
|
309
341
|
}
|
|
310
|
-
else {
|
|
311
|
-
lastHealed = false;
|
|
312
|
-
}
|
|
313
342
|
if (currentTasks.length > remaining)
|
|
314
343
|
currentTasks = currentTasks.slice(0, remaining);
|
|
315
344
|
syncRunInfo();
|
|
345
|
+
// Save run state BEFORE the swarm — if the process crashes mid-swarm,
|
|
346
|
+
// resume picks up currentTasks. This is the critical resilience checkpoint.
|
|
347
|
+
saveRunState(runDir, {
|
|
348
|
+
id: `run-${new Date().toISOString().slice(0, 19)}`, objective: objective ?? "", budget: cfg.budget,
|
|
349
|
+
remaining, workerModel, plannerModel,
|
|
350
|
+
workerProviderId: cfg.workerProvider?.id, plannerProviderId: cfg.plannerProvider?.id,
|
|
351
|
+
concurrency, permissionMode,
|
|
352
|
+
usageCap, allowExtraUsage: cfg.allowExtraUsage, extraUsageBudget: cfg.extraUsageBudget,
|
|
353
|
+
flex, useWorktrees, mergeStrategy, waveNum, currentTasks,
|
|
354
|
+
accCost, accCompleted, accFailed, accIn, accOut, accTools,
|
|
355
|
+
branches, phase: "steering", startedAt: new Date(cfg.runStartedAt).toISOString(), cwd,
|
|
356
|
+
});
|
|
316
357
|
const swarm = new Swarm({
|
|
317
358
|
tasks: currentTasks, concurrency, cwd, model: workerModel, permissionMode, allowedTools,
|
|
318
359
|
useWorktrees, mergeStrategy: waveMerge, agentTimeoutMs: cfg.agentTimeoutMs,
|
|
@@ -332,6 +373,15 @@ export async function executeRun(cfg) {
|
|
|
332
373
|
console.error(chalk.red(`\n Authentication failed — check your API key or run: claude auth\n`));
|
|
333
374
|
process.exit(1);
|
|
334
375
|
}
|
|
376
|
+
// Swarm crashed mid-execution — save partial results before propagating.
|
|
377
|
+
// The pre-swarm saveRunState already preserved currentTasks for resume.
|
|
378
|
+
// Also save the wave session with whatever agents completed.
|
|
379
|
+
if (swarm.agents.length > 0) {
|
|
380
|
+
try {
|
|
381
|
+
saveWaveSession(runDir, waveNum, swarm.agents, swarm.totalCostUsd);
|
|
382
|
+
}
|
|
383
|
+
catch { }
|
|
384
|
+
}
|
|
335
385
|
throw err;
|
|
336
386
|
}
|
|
337
387
|
display.pause();
|
package/dist/ui.d.ts
CHANGED
|
@@ -71,6 +71,7 @@ export declare class RunDisplay {
|
|
|
71
71
|
private askTempFile?;
|
|
72
72
|
/** ID of the agent whose detail panel is open; undefined = no detail shown. */
|
|
73
73
|
private selectedAgentId?;
|
|
74
|
+
private navState;
|
|
74
75
|
private onSteer?;
|
|
75
76
|
private onAsk?;
|
|
76
77
|
constructor(runInfo: RunInfo, liveConfig?: LiveConfig, callbacks?: {
|
|
@@ -87,6 +88,15 @@ export declare class RunDisplay {
|
|
|
87
88
|
selectAgent(id: number): void;
|
|
88
89
|
/** Clear the agent detail panel. */
|
|
89
90
|
clearSelectedAgent(): void;
|
|
91
|
+
/** Arrow-key navigation dispatched by the demux in handleTyped(). */
|
|
92
|
+
navigate(direction: "up" | "down" | "left" | "right" | "enter"): boolean;
|
|
93
|
+
/** Get the agents visible in the table (running + last N finished). */
|
|
94
|
+
private getVisibleAgents;
|
|
95
|
+
/** Discover sections from the current render state for navigation boundaries. */
|
|
96
|
+
private getSections;
|
|
97
|
+
private clampNavState;
|
|
98
|
+
/** Returns the unique highlight key for the currently focused row, used by renderer. */
|
|
99
|
+
getHighlightKey(): string | undefined;
|
|
90
100
|
private clearAskTempFile;
|
|
91
101
|
/** Get the currently selected agent's ID for rendering. */
|
|
92
102
|
getSelectedAgentId(): number | undefined;
|
|
@@ -112,7 +122,19 @@ export declare class RunDisplay {
|
|
|
112
122
|
private setupHotkeys;
|
|
113
123
|
/** Handle a pasted block. Returns true if the frame needs a redraw. */
|
|
114
124
|
private handlePaste;
|
|
115
|
-
/** Handle a typed (non-pasted) chunk. Returns true if the frame needs a redraw.
|
|
125
|
+
/** Handle a typed (non-pasted) chunk. Returns true if the frame needs a redraw.
|
|
126
|
+
*
|
|
127
|
+
* Demux pipeline — routes arrow keys and ESC BEFORE hotkey matching:
|
|
128
|
+
* Raw stdin chunk → splitPaste
|
|
129
|
+
* ├─ paste → handlePaste (existing, fine)
|
|
130
|
+
* └─ typed → demux
|
|
131
|
+
* ├─ ESC + [A/B/C/D → this.navigate("up"/"down"/"right"/"left")
|
|
132
|
+
* ├─ ESC → cancel input / close detail / dismiss panel
|
|
133
|
+
* ├─ Enter → submit / reveal / select
|
|
134
|
+
* ├─ Ctrl+C → abort
|
|
135
|
+
* ├─ Backspace → delete
|
|
136
|
+
* └─ printable → hotkey matching (b, t, c, e, p, s, q, ?, d, 0-9)
|
|
137
|
+
*/
|
|
116
138
|
private handleTyped;
|
|
117
139
|
private plainTick;
|
|
118
140
|
}
|
package/dist/ui.js
CHANGED
|
@@ -31,6 +31,7 @@ export class RunDisplay {
|
|
|
31
31
|
askTempFile;
|
|
32
32
|
/** ID of the agent whose detail panel is open; undefined = no detail shown. */
|
|
33
33
|
selectedAgentId;
|
|
34
|
+
navState = { focusSection: 0, focusRow: 0, scrollOffset: 0 };
|
|
34
35
|
onSteer;
|
|
35
36
|
onAsk;
|
|
36
37
|
constructor(runInfo, liveConfig, callbacks) {
|
|
@@ -86,6 +87,160 @@ export class RunDisplay {
|
|
|
86
87
|
}
|
|
87
88
|
/** Clear the agent detail panel. */
|
|
88
89
|
clearSelectedAgent() { this.selectedAgentId = undefined; }
|
|
90
|
+
/** Arrow-key navigation dispatched by the demux in handleTyped(). */
|
|
91
|
+
navigate(direction) {
|
|
92
|
+
const sections = this.getSections();
|
|
93
|
+
const nav = this.navState;
|
|
94
|
+
const section = sections[Math.min(nav.focusSection, sections.length - 1)];
|
|
95
|
+
let changed = false;
|
|
96
|
+
switch (direction) {
|
|
97
|
+
case "up":
|
|
98
|
+
if (nav.focusRow > 0) {
|
|
99
|
+
nav.focusRow--;
|
|
100
|
+
nav.scrollOffset = Math.max(0, nav.scrollOffset - 1);
|
|
101
|
+
changed = true;
|
|
102
|
+
}
|
|
103
|
+
else if (nav.focusSection > 0) {
|
|
104
|
+
nav.focusSection--;
|
|
105
|
+
const prevSection = sections[nav.focusSection];
|
|
106
|
+
nav.focusRow = Math.max(0, prevSection.rowCount - 1);
|
|
107
|
+
changed = true;
|
|
108
|
+
}
|
|
109
|
+
break;
|
|
110
|
+
case "down":
|
|
111
|
+
if (nav.focusRow < section.rowCount - 1) {
|
|
112
|
+
nav.focusRow++;
|
|
113
|
+
changed = true;
|
|
114
|
+
}
|
|
115
|
+
else if (nav.focusSection < sections.length - 1) {
|
|
116
|
+
nav.focusSection++;
|
|
117
|
+
nav.focusRow = 0;
|
|
118
|
+
changed = true;
|
|
119
|
+
}
|
|
120
|
+
break;
|
|
121
|
+
case "left":
|
|
122
|
+
if (this.selectedAgentId != null) {
|
|
123
|
+
this.clearSelectedAgent();
|
|
124
|
+
changed = true;
|
|
125
|
+
}
|
|
126
|
+
else if (nav.focusSection > 0) {
|
|
127
|
+
nav.focusSection--;
|
|
128
|
+
nav.focusRow = 0;
|
|
129
|
+
changed = true;
|
|
130
|
+
}
|
|
131
|
+
break;
|
|
132
|
+
case "right":
|
|
133
|
+
if (this.swarm && this.selectedAgentId == null) {
|
|
134
|
+
const agents = this.getVisibleAgents();
|
|
135
|
+
const agent = agents[nav.focusRow];
|
|
136
|
+
if (agent && agent.status === "running") {
|
|
137
|
+
this.selectAgent(agent.id);
|
|
138
|
+
changed = true;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
else if (nav.focusSection < sections.length - 1) {
|
|
142
|
+
nav.focusSection++;
|
|
143
|
+
nav.focusRow = 0;
|
|
144
|
+
changed = true;
|
|
145
|
+
}
|
|
146
|
+
break;
|
|
147
|
+
case "enter":
|
|
148
|
+
if (this.swarm) {
|
|
149
|
+
const agents = this.getVisibleAgents();
|
|
150
|
+
const agent = agents[nav.focusRow];
|
|
151
|
+
if (agent) {
|
|
152
|
+
if (this.selectedAgentId === agent.id) {
|
|
153
|
+
this.clearSelectedAgent();
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
this.selectAgent(agent.id);
|
|
157
|
+
}
|
|
158
|
+
changed = true;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
this.clampNavState(sections);
|
|
164
|
+
return changed;
|
|
165
|
+
}
|
|
166
|
+
/** Get the agents visible in the table (running + last N finished). */
|
|
167
|
+
getVisibleAgents() {
|
|
168
|
+
if (!this.swarm)
|
|
169
|
+
return [];
|
|
170
|
+
const running = this.swarm.agents.filter(a => a.status === "running");
|
|
171
|
+
const finished = this.swarm.agents.filter(a => a.status !== "running");
|
|
172
|
+
const showFinished = finished.slice(-Math.max(2, 12 - running.length));
|
|
173
|
+
return [...running, ...showFinished];
|
|
174
|
+
}
|
|
175
|
+
/** Discover sections from the current render state for navigation boundaries. */
|
|
176
|
+
getSections() {
|
|
177
|
+
const sections = [];
|
|
178
|
+
if (this.swarm) {
|
|
179
|
+
// Agent table section
|
|
180
|
+
const show = this.getVisibleAgents();
|
|
181
|
+
sections.push({
|
|
182
|
+
title: "Agents",
|
|
183
|
+
rowCount: show.length,
|
|
184
|
+
highlightKeyForRow: (row) => show[row]?.id != null ? `agent-${show[row].id}` : undefined,
|
|
185
|
+
});
|
|
186
|
+
// Agent detail section
|
|
187
|
+
if (this.selectedAgentId != null) {
|
|
188
|
+
sections.push({
|
|
189
|
+
title: "Detail",
|
|
190
|
+
rowCount: 1,
|
|
191
|
+
highlightKeyForRow: () => "detail",
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
// Merge results section
|
|
195
|
+
if (this.swarm.mergeResults.length > 0) {
|
|
196
|
+
sections.push({
|
|
197
|
+
title: "Merges",
|
|
198
|
+
rowCount: this.swarm.mergeResults.length,
|
|
199
|
+
highlightKeyForRow: (row) => `merge-${row}`,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
// Event log section
|
|
203
|
+
sections.push({
|
|
204
|
+
title: "Events",
|
|
205
|
+
rowCount: Math.min(12, this.swarm.logs.length),
|
|
206
|
+
highlightKeyForRow: (row) => `event-${row}`,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
else if (this.steeringActive) {
|
|
210
|
+
// Steering mode sections
|
|
211
|
+
if (this.steeringContext?.objective) {
|
|
212
|
+
sections.push({ title: "Objective", rowCount: 1, highlightKeyForRow: () => "objective" });
|
|
213
|
+
}
|
|
214
|
+
if (this.steeringContext?.status) {
|
|
215
|
+
sections.push({ title: "Status", rowCount: 1, highlightKeyForRow: () => "status" });
|
|
216
|
+
}
|
|
217
|
+
if (this.steeringContext?.lastWave) {
|
|
218
|
+
sections.push({ title: "LastWave", rowCount: Math.min(6, this.steeringContext.lastWave.tasks.length + 1), highlightKeyForRow: (row) => `wave-task-${row}` });
|
|
219
|
+
}
|
|
220
|
+
sections.push({ title: "PlannerActivity", rowCount: Math.min(15, this.steeringEvents.length), highlightKeyForRow: (row) => `steer-event-${row}` });
|
|
221
|
+
sections.push({ title: "StatusLine", rowCount: 1, highlightKeyForRow: () => "status-line" });
|
|
222
|
+
}
|
|
223
|
+
// Ensure at least one section
|
|
224
|
+
if (sections.length === 0) {
|
|
225
|
+
sections.push({ title: "Content", rowCount: 1, highlightKeyForRow: () => "content" });
|
|
226
|
+
}
|
|
227
|
+
return sections;
|
|
228
|
+
}
|
|
229
|
+
clampNavState(sections) {
|
|
230
|
+
const nav = this.navState;
|
|
231
|
+
nav.focusSection = Math.min(Math.max(0, nav.focusSection), sections.length - 1);
|
|
232
|
+
const s = sections[nav.focusSection];
|
|
233
|
+
if (s) {
|
|
234
|
+
nav.focusRow = Math.min(Math.max(0, nav.focusRow), Math.max(0, s.rowCount - 1));
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/** Returns the unique highlight key for the currently focused row, used by renderer. */
|
|
238
|
+
getHighlightKey() {
|
|
239
|
+
const sections = this.getSections();
|
|
240
|
+
const nav = this.navState;
|
|
241
|
+
const section = sections[Math.min(nav.focusSection, sections.length - 1)];
|
|
242
|
+
return section?.highlightKeyForRow?.(nav.focusRow);
|
|
243
|
+
}
|
|
89
244
|
clearAskTempFile() {
|
|
90
245
|
if (this.askTempFile) {
|
|
91
246
|
try {
|
|
@@ -325,9 +480,62 @@ export class RunDisplay {
|
|
|
325
480
|
}
|
|
326
481
|
return false;
|
|
327
482
|
}
|
|
328
|
-
/** Handle a typed (non-pasted) chunk. Returns true if the frame needs a redraw.
|
|
483
|
+
/** Handle a typed (non-pasted) chunk. Returns true if the frame needs a redraw.
|
|
484
|
+
*
|
|
485
|
+
* Demux pipeline — routes arrow keys and ESC BEFORE hotkey matching:
|
|
486
|
+
* Raw stdin chunk → splitPaste
|
|
487
|
+
* ├─ paste → handlePaste (existing, fine)
|
|
488
|
+
* └─ typed → demux
|
|
489
|
+
* ├─ ESC + [A/B/C/D → this.navigate("up"/"down"/"right"/"left")
|
|
490
|
+
* ├─ ESC → cancel input / close detail / dismiss panel
|
|
491
|
+
* ├─ Enter → submit / reveal / select
|
|
492
|
+
* ├─ Ctrl+C → abort
|
|
493
|
+
* ├─ Backspace → delete
|
|
494
|
+
* └─ printable → hotkey matching (b, t, c, e, p, s, q, ?, d, 0-9)
|
|
495
|
+
*/
|
|
329
496
|
handleTyped(s) {
|
|
330
497
|
const lc = this.liveConfig;
|
|
498
|
+
// ── 1. Arrow keys: \x1B[A = up, \x1B[B = down, \x1B[C = right, \x1B[D = left ──
|
|
499
|
+
if (s.startsWith("\x1B[")) {
|
|
500
|
+
const dir = s[2];
|
|
501
|
+
if (dir === "A") {
|
|
502
|
+
this.navigate("up");
|
|
503
|
+
return true;
|
|
504
|
+
}
|
|
505
|
+
if (dir === "B") {
|
|
506
|
+
this.navigate("down");
|
|
507
|
+
return true;
|
|
508
|
+
}
|
|
509
|
+
if (dir === "C") {
|
|
510
|
+
this.navigate("right");
|
|
511
|
+
return true;
|
|
512
|
+
}
|
|
513
|
+
if (dir === "D") {
|
|
514
|
+
this.navigate("left");
|
|
515
|
+
return true;
|
|
516
|
+
}
|
|
517
|
+
// Other ANSI sequences — swallow silently
|
|
518
|
+
return true;
|
|
519
|
+
}
|
|
520
|
+
// ── 2. Standalone ESC ──
|
|
521
|
+
if (s === "\x1B") {
|
|
522
|
+
if (this.inputMode !== "none") {
|
|
523
|
+
this.inputMode = "none";
|
|
524
|
+
this.inputSegs = [];
|
|
525
|
+
return true;
|
|
526
|
+
}
|
|
527
|
+
if (this.selectedAgentId != null) {
|
|
528
|
+
this.clearSelectedAgent();
|
|
529
|
+
return true;
|
|
530
|
+
}
|
|
531
|
+
if (this.askState && !this.askState.streaming) {
|
|
532
|
+
this.askState = undefined;
|
|
533
|
+
this.clearAskTempFile();
|
|
534
|
+
return true;
|
|
535
|
+
}
|
|
536
|
+
return false;
|
|
537
|
+
}
|
|
538
|
+
// ── 3. Input mode: budget / threshold / concurrency / extra ──
|
|
331
539
|
if (this.inputMode === "budget" || this.inputMode === "threshold" || this.inputMode === "concurrency" || this.inputMode === "extra") {
|
|
332
540
|
let dirty = false;
|
|
333
541
|
for (const ch of s) {
|
|
@@ -366,12 +574,6 @@ export class RunDisplay {
|
|
|
366
574
|
this.inputSegs = [];
|
|
367
575
|
return true;
|
|
368
576
|
}
|
|
369
|
-
// ESC cancels input mode
|
|
370
|
-
if (ch === "\x1B") {
|
|
371
|
-
this.inputMode = "none";
|
|
372
|
-
this.inputSegs = [];
|
|
373
|
-
return true;
|
|
374
|
-
}
|
|
375
577
|
if (ch === "\x7F") {
|
|
376
578
|
backspaceSegments(this.inputSegs);
|
|
377
579
|
dirty = true;
|
|
@@ -384,6 +586,7 @@ export class RunDisplay {
|
|
|
384
586
|
}
|
|
385
587
|
return dirty;
|
|
386
588
|
}
|
|
589
|
+
// ── 4. Input mode: steer / ask ──
|
|
387
590
|
if (this.inputMode === "steer" || this.inputMode === "ask") {
|
|
388
591
|
let dirty = false;
|
|
389
592
|
for (let ci = 0; ci < s.length; ci++) {
|
|
@@ -406,18 +609,11 @@ export class RunDisplay {
|
|
|
406
609
|
this.inputSegs = [];
|
|
407
610
|
return true;
|
|
408
611
|
}
|
|
409
|
-
// ESC cancels
|
|
612
|
+
// ESC cancels input mode (no ANSI-byte consumption loop — arrows arrive
|
|
613
|
+
// as "\x1B[A" in a single call and are caught by step 1 above)
|
|
410
614
|
if (ch === "\x1B") {
|
|
411
615
|
this.inputMode = "none";
|
|
412
616
|
this.inputSegs = [];
|
|
413
|
-
// Skip any remaining ANSI sequence bytes (e.g. [A for arrow keys)
|
|
414
|
-
while (ci + 1 < s.length) {
|
|
415
|
-
const next = s[ci + 1];
|
|
416
|
-
const nc = next.charCodeAt(0);
|
|
417
|
-
ci++;
|
|
418
|
-
if ((nc >= 0x40 && nc <= 0x7E) || nc === 0x7F)
|
|
419
|
-
break; // final byte
|
|
420
|
-
}
|
|
421
617
|
return true;
|
|
422
618
|
}
|
|
423
619
|
if (ch === "\x7F" || ch === "\b") {
|
|
@@ -427,9 +623,9 @@ export class RunDisplay {
|
|
|
427
623
|
}
|
|
428
624
|
const code = ch.charCodeAt(0);
|
|
429
625
|
if (code < 0x20)
|
|
430
|
-
continue;
|
|
626
|
+
continue;
|
|
431
627
|
if (code >= 0x7F && code < 0xA0)
|
|
432
|
-
continue;
|
|
628
|
+
continue;
|
|
433
629
|
if (code >= 0x20 && code <= 0x7E && segmentsToString(this.inputSegs).length < MAX_INPUT_LEN) {
|
|
434
630
|
appendCharToSegments(this.inputSegs, ch);
|
|
435
631
|
dirty = true;
|
|
@@ -437,16 +633,9 @@ export class RunDisplay {
|
|
|
437
633
|
}
|
|
438
634
|
return dirty;
|
|
439
635
|
}
|
|
440
|
-
// Hotkey mode
|
|
441
|
-
//
|
|
442
|
-
if (s
|
|
443
|
-
return false;
|
|
444
|
-
if (s.length !== 1)
|
|
445
|
-
return false;
|
|
446
|
-
const key = s[0];
|
|
447
|
-
const code = key.charCodeAt(0);
|
|
448
|
-
// Allow \r / \n through for Enter-to-reveal
|
|
449
|
-
if (code === 0x0D || code === 0x0A) {
|
|
636
|
+
// ── 5. Hotkey mode ──
|
|
637
|
+
// Enter
|
|
638
|
+
if (s === "\r" || s === "\n") {
|
|
450
639
|
if (this.askTempFile) {
|
|
451
640
|
try {
|
|
452
641
|
execSync(`open -R ${JSON.stringify(this.askTempFile)}`);
|
|
@@ -455,12 +644,21 @@ export class RunDisplay {
|
|
|
455
644
|
}
|
|
456
645
|
return true;
|
|
457
646
|
}
|
|
458
|
-
//
|
|
459
|
-
if (
|
|
460
|
-
this.
|
|
461
|
-
|
|
462
|
-
|
|
647
|
+
// Ctrl+C
|
|
648
|
+
if (s === "\x03") {
|
|
649
|
+
if (this.swarm && !this.swarm.aborted) {
|
|
650
|
+
this.swarm.abort();
|
|
651
|
+
}
|
|
652
|
+
else {
|
|
653
|
+
process.exit(0);
|
|
654
|
+
}
|
|
655
|
+
return true;
|
|
463
656
|
}
|
|
657
|
+
// Only single printable ASCII characters reach hotkey matching
|
|
658
|
+
if (s.length !== 1)
|
|
659
|
+
return false;
|
|
660
|
+
const key = s[0];
|
|
661
|
+
const code = key.charCodeAt(0);
|
|
464
662
|
if (code < 0x20 || code > 0x7E)
|
|
465
663
|
return false;
|
|
466
664
|
if (key === "b" || key === "B") {
|
|
@@ -527,15 +725,7 @@ export class RunDisplay {
|
|
|
527
725
|
}
|
|
528
726
|
// [d] cycle agent detail panel
|
|
529
727
|
if ((key === "d" || key === "D") && this.swarm && this.swarm.active > 0) {
|
|
530
|
-
|
|
531
|
-
this.cycleSelectedAgent();
|
|
532
|
-
else
|
|
533
|
-
this.cycleSelectedAgent();
|
|
534
|
-
return true;
|
|
535
|
-
}
|
|
536
|
-
// ESC closes detail panel
|
|
537
|
-
if (key === "\x1B" && this.selectedAgentId != null) {
|
|
538
|
-
this.clearSelectedAgent();
|
|
728
|
+
this.cycleSelectedAgent();
|
|
539
729
|
return true;
|
|
540
730
|
}
|
|
541
731
|
// Number keys 0-9 select a specific agent by row index in the visible table
|
|
@@ -547,7 +737,7 @@ export class RunDisplay {
|
|
|
547
737
|
return true;
|
|
548
738
|
}
|
|
549
739
|
}
|
|
550
|
-
if (key === "q" || key === "Q"
|
|
740
|
+
if (key === "q" || key === "Q") {
|
|
551
741
|
if (this.swarm) {
|
|
552
742
|
if (this.swarm.aborted)
|
|
553
743
|
process.exit(0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-overnight",
|
|
3
|
-
"version": "1.16.
|
|
3
|
+
"version": "1.16.16",
|
|
4
4
|
"description": "Background lane for your Claude Max plan. Parallel Claude Agent SDK sessions in git worktrees with a usage cap that reserves headroom for your interactive Claude Code. Crash-safe resume. Opus/Sonnet/Haiku + Qwen/OpenRouter.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"build": "tsc",
|
|
11
11
|
"dev": "tsc --watch",
|
|
12
12
|
"start": "node dist/bin.js",
|
|
13
|
+
"test": "node --test dist/__tests__/*.test.js",
|
|
13
14
|
"prepublishOnly": "node scripts/sync-plugin-version.js"
|
|
14
15
|
},
|
|
15
16
|
"dependencies": {
|
|
@@ -18,6 +19,8 @@
|
|
|
18
19
|
},
|
|
19
20
|
"devDependencies": {
|
|
20
21
|
"@types/node": "^22.0.0",
|
|
22
|
+
"node-pty": "^1.1.0",
|
|
23
|
+
"strip-ansi": "^7.1.0",
|
|
21
24
|
"typescript": "^5.7.0"
|
|
22
25
|
},
|
|
23
26
|
"license": "MIT",
|
|
@@ -65,6 +68,8 @@
|
|
|
65
68
|
"files": [
|
|
66
69
|
"dist",
|
|
67
70
|
"!dist/__tests__",
|
|
71
|
+
"plugins",
|
|
72
|
+
"QUICKSHEET_PLAYWRIGHT.md",
|
|
68
73
|
"README.md",
|
|
69
74
|
"LICENSE"
|
|
70
75
|
]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "claude-overnight",
|
|
3
|
+
"version": "1.16.16",
|
|
4
|
+
"description": "Claude Code skill for understanding, installing, and inspecting claude-overnight runs — parallel Claude agents in git worktrees with thinking waves, multi-wave steering, and crash-safe resume.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Francesco Fornace"
|
|
7
|
+
},
|
|
8
|
+
"homepage": "https://github.com/Fornace/claude-overnight",
|
|
9
|
+
"license": "MIT"
|
|
10
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: claude-overnight
|
|
3
|
+
description: >
|
|
4
|
+
Understand, install, and inspect claude-overnight runs — a CLI that
|
|
5
|
+
launches parallel Claude agents in git worktrees with thinking waves,
|
|
6
|
+
multi-wave steering, and crash-safe resume. Use when the user mentions
|
|
7
|
+
claude-overnight, a `.claude-overnight/` folder, an "overnight" or
|
|
8
|
+
"swarm" run, or asks to check status / resume / continue a
|
|
9
|
+
multi-phase plan. Not for Vercel Workflow DevKit.
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# What it is
|
|
13
|
+
|
|
14
|
+
`claude-overnight` is a CLI (npm: `claude-overnight`, bin: `claude-overnight`) that takes an objective + budget and launches many Claude agent sessions in parallel, each in an isolated git worktree. It's a local multi-session orchestrator built on top of the Claude Agent SDK — not itself an agent harness, but a layer that plans, dispatches, and steers many sessions that run on the SDK's harness. A "thinking wave" of architect sessions explores the codebase, an orchestrator synthesizes concrete tasks, executor waves run them in parallel, and steering decides between more execution, reflection, or declaring done. Rate limits, crashes, and usage caps are all resumable — nothing is lost.
|
|
15
|
+
|
|
16
|
+
Repo: https://github.com/Fornace/claude-overnight
|
|
17
|
+
|
|
18
|
+
# Install / run
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install -g claude-overnight # Node >= 20; needs Claude auth or ANTHROPIC_API_KEY (or Qwen 3.6 Plus — see repo README)
|
|
22
|
+
claude-overnight # interactive in cwd
|
|
23
|
+
claude-overnight tasks.json # task file mode
|
|
24
|
+
claude-overnight "task a" "task b" # inline
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Common flags: `--budget=N`, `--concurrency=N`, `--model=<name>`, `--usage-cap=N`, `--allow-extra-usage`, `--extra-usage-budget=N`, `--timeout=SECONDS`, `--no-flex`, `--dry-run`.
|
|
28
|
+
|
|
29
|
+
Live keys while running: `b` change budget · `t` change usage cap · `q` graceful stop (twice = force).
|
|
30
|
+
|
|
31
|
+
Exit codes: `0` all ok · `1` some failed · `2` all/none.
|
|
32
|
+
|
|
33
|
+
# On-disk layout (this is how you inspect status)
|
|
34
|
+
|
|
35
|
+
Every run lives at `<repo>/.claude-overnight/runs/<ISO-timestamp>/`:
|
|
36
|
+
|
|
37
|
+
| File / dir | What it tells you |
|
|
38
|
+
|----------------------|-----------------------------------------------------------------------------------|
|
|
39
|
+
| `run.json` | Machine state: objective, model, budget, cost, waves done, branches, done flag. |
|
|
40
|
+
| `status.md` | **Living project snapshot**, rewritten by steering every wave. First line = short status. |
|
|
41
|
+
| `goal.md` | Evolving "north star" — what the run currently thinks "amazing" means. |
|
|
42
|
+
| `milestones/*.md` | Strategic snapshots archived ~every 5 waves. Long-term memory of the run. |
|
|
43
|
+
| `designs/*.md` | Architect outputs from the thinking wave. Deleted once the objective is complete. |
|
|
44
|
+
| `sessions/wave-N.json` | Per-wave agent records: prompt, status, cost, files changed, branch, error. |
|
|
45
|
+
|
|
46
|
+
The newest subfolder under `runs/` is the current/last run. A run that never reached "done" is **resumable** — `run.json` will not be marked complete and `designs/` may still be present.
|
|
47
|
+
|
|
48
|
+
To assess status of a run from scratch, read in this order: `goal.md` → `status.md` → newest file in `milestones/` → newest `sessions/wave-*.json` → `run.json`. Five reads and you know exactly where it stands.
|
|
49
|
+
|
|
50
|
+
**Durable run history (committed, survives cleanup):** `claude-overnight.log.md` at the repo root is updated on every run with a block per run ID — original objective, start/finish times, cost, outcome, branch. If the user asks "what was my prompt" or "what did last night's run do" and `.claude-overnight/runs/` is empty, this file is the canonical recovery path.
|
|
51
|
+
|
|
52
|
+
# Resume / continue
|
|
53
|
+
|
|
54
|
+
Just run `claude-overnight` again in the same repo. It auto-detects the unfinished run and shows a **Resume / Fresh / Quit** prompt. On resume: unmerged `swarm/task-*` branches auto-merge, the wave loop continues, status/goal/milestones/designs are preserved. If orchestration crashed after the thinking wave, surviving `designs/*.md` are reused — no re-paying for architects.
|
|
55
|
+
|
|
56
|
+
For **multi-phase plans** (task file with `objective` + `flexiblePlan: true`), resuming picks up at the next wave with full steering context. Don't hand-edit `run.json` to "fix" a stuck run unless something is demonstrably corrupt — prefer re-running and letting steering re-assess.
|
|
57
|
+
|
|
58
|
+
Merged branches from prior runs are not re-run. Knowledge carries forward across runs: new runs see what completed runs built.
|
|
59
|
+
|
|
60
|
+
# Diagnosing a stuck / failed run
|
|
61
|
+
|
|
62
|
+
1. Read `status.md` (living snapshot) and the newest `sessions/wave-*.json`.
|
|
63
|
+
2. Check for `swarm/task-*` branches left behind (`git branch --list 'swarm/*'`) — these are unmerged worktree outputs, usually from a conflict or crash.
|
|
64
|
+
3. Look at `run.json` for `done`, `lastError`, usage/cost fields.
|
|
65
|
+
4. If the thinking wave succeeded but orchestration crashed, `designs/` will still contain the architect docs — a resume will reuse them.
|
|
66
|
+
5. If rate-limited, the tool waits and retries on its own — do not kill it unless the user asks.
|
|
67
|
+
|
|
68
|
+
# What NOT to do
|
|
69
|
+
|
|
70
|
+
- Don't micromanage the tool. It has its own planner, steering, and goal refinement — trust them.
|
|
71
|
+
- Don't invent a resume procedure. The CLI handles resume itself; the correct action is almost always "re-run `claude-overnight` in the repo".
|
|
72
|
+
- Don't delete `.claude-overnight/` to "clean up" — it holds the only record of what the run learned. It should be in `.gitignore`.
|
|
73
|
+
- Don't truncate or summarize agent output files when reading them back — never discard expensive agent output.
|
|
74
|
+
- Don't confuse this with Vercel Workflow DevKit — unrelated despite the word "workflow".
|
|
75
|
+
|
|
76
|
+
# Playwright Parallel Usage
|
|
77
|
+
|
|
78
|
+
When agents use the Playwright MCP server for testing, parallel instances conflict on browser locks and cookie state. See `QUICKSHEET_PLAYWRIGHT.md` at the repo root for the full reference.
|
|
79
|
+
|
|
80
|
+
**Quick rules:**
|
|
81
|
+
- **Headless by default** — prevents focus stealing on macOS. Only use headed when anti-bot detection (CAPTCHA, Cloudflare) forces it
|
|
82
|
+
- **Isolated agents (no login):** Each MCP server needs `--isolated --headless`
|
|
83
|
+
- **Isolated agents (with saved login):** Each needs its own `userDataDir` or `--storage-state` file, plus `--headless`
|
|
84
|
+
- Multiple MCP entries in `settings.json` — one per concurrency slot, or use a single `--isolated` entry if cookies don't need to persist
|
|
85
|
+
|
|
86
|
+
**Context7 (ctx7) docs:** Requires authentication (`npx ctx7@latest login` or `CONTEXT7_API_KEY`). Pre-flight check:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
npx ctx7@latest library playwright "parallel browser instances"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
If this fails with a quota/auth error, fall back to training data — don't block the run.
|