claude-overnight 0.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.
@@ -0,0 +1,246 @@
1
+ import { query } from "@anthropic-ai/claude-agent-sdk";
2
+ /**
3
+ * Coordinator: analyzes the codebase, breaks objective into parallel tasks.
4
+ */
5
+ export async function planTasks(objective, cwd, model, permissionMode, onLog) {
6
+ onLog("Analyzing codebase...");
7
+ const INACTIVITY_MS = 5 * 60 * 1000;
8
+ let resultText = "";
9
+ const plannerQuery = query({
10
+ prompt: `You are a task coordinator for a parallel agent swarm. Analyze this codebase and break the following objective into independent tasks.
11
+
12
+ Objective: ${objective}
13
+
14
+ Requirements:
15
+ - Each task MUST be independent — no task depends on another
16
+ - Each task should target specific files/areas to avoid merge conflicts
17
+ - Be specific: mention exact file paths, function names, what to change
18
+ - Keep tasks focused: one logical change per task
19
+ - Aim for 3-15 tasks depending on scope
20
+
21
+ Respond with ONLY a JSON object (no markdown fences):
22
+ {
23
+ "tasks": [
24
+ { "prompt": "In src/foo.ts, refactor the bar() function to..." },
25
+ { "prompt": "Add unit tests for the baz module in test/baz.test.ts..." }
26
+ ]
27
+ }`,
28
+ options: {
29
+ cwd,
30
+ model,
31
+ tools: ["Read", "Glob", "Grep"],
32
+ allowedTools: ["Read", "Glob", "Grep"],
33
+ permissionMode: permissionMode,
34
+ ...(permissionMode === "bypassPermissions" && { allowDangerouslySkipPermissions: true }),
35
+ persistSession: false,
36
+ includePartialMessages: true,
37
+ },
38
+ });
39
+ // Inactivity watchdog — only kills planner if it goes completely silent
40
+ let lastActivity = Date.now();
41
+ let timer;
42
+ const watchdog = new Promise((_, reject) => {
43
+ const check = () => {
44
+ const silent = Date.now() - lastActivity;
45
+ if (silent >= INACTIVITY_MS) {
46
+ plannerQuery.close();
47
+ reject(new Error(`Planner silent for ${Math.round(silent / 1000)}s — assumed hung`));
48
+ }
49
+ else {
50
+ timer = setTimeout(check, Math.min(30_000, INACTIVITY_MS - silent + 1000));
51
+ }
52
+ };
53
+ timer = setTimeout(check, INACTIVITY_MS);
54
+ });
55
+ const consume = async () => {
56
+ for await (const msg of plannerQuery) {
57
+ lastActivity = Date.now();
58
+ if (msg.type === "stream_event") {
59
+ const ev = msg.event;
60
+ if (ev?.type === "content_block_start" &&
61
+ ev.content_block?.type === "tool_use") {
62
+ onLog(ev.content_block.name);
63
+ }
64
+ }
65
+ if (msg.type === "result") {
66
+ if (msg.subtype === "success") {
67
+ resultText = msg.result || "";
68
+ }
69
+ else {
70
+ throw new Error(`Planner failed: ${msg.subtype}`);
71
+ }
72
+ }
73
+ }
74
+ };
75
+ try {
76
+ await Promise.race([consume(), watchdog]);
77
+ }
78
+ finally {
79
+ clearTimeout(timer);
80
+ }
81
+ const parsed = await extractTaskJson(resultText, async () => {
82
+ onLog("Retrying for valid JSON...");
83
+ let retryText = "";
84
+ for await (const msg of query({
85
+ prompt: `Your previous response did not contain valid JSON. Output ONLY a JSON object with this shape, nothing else:\n{"tasks":[{"prompt":"..."}]}`,
86
+ options: {
87
+ cwd,
88
+ model,
89
+ permissionMode,
90
+ ...(permissionMode === "bypassPermissions" && { allowDangerouslySkipPermissions: true }),
91
+ persistSession: false,
92
+ },
93
+ })) {
94
+ if (msg.type === "result" && msg.subtype === "success") {
95
+ retryText = msg.result || "";
96
+ }
97
+ }
98
+ return retryText;
99
+ });
100
+ let tasks = (parsed.tasks || []).map((t, i) => ({
101
+ id: String(i),
102
+ prompt: typeof t === "string" ? t : t.prompt,
103
+ }));
104
+ // Filter garbage tasks (require at least 3 space-separated words)
105
+ const before = tasks.length;
106
+ tasks = tasks.filter((t) => t.prompt && t.prompt.trim().split(/\s+/).length >= 3);
107
+ if (tasks.length < before) {
108
+ onLog(`Filtered ${before - tasks.length} task(s) with fewer than 3 words`);
109
+ }
110
+ // Deduplicate tasks with very similar prompts (>80% word overlap)
111
+ {
112
+ const dominated = new Set();
113
+ for (let i = 0; i < tasks.length; i++) {
114
+ if (dominated.has(i))
115
+ continue;
116
+ const setA = new Set(tasks[i].prompt.toLowerCase().split(/\s+/));
117
+ for (let j = i + 1; j < tasks.length; j++) {
118
+ if (dominated.has(j))
119
+ continue;
120
+ const setB = new Set(tasks[j].prompt.toLowerCase().split(/\s+/));
121
+ const shared = [...setA].filter((w) => setB.has(w)).length;
122
+ const overlap = shared / Math.min(setA.size, setB.size);
123
+ if (overlap > 0.8) {
124
+ // Keep the more specific (longer) prompt
125
+ const drop = setA.size >= setB.size ? j : i;
126
+ const keep = drop === j ? i : j;
127
+ onLog(`Dedup: task ${tasks[drop].id} >${Math.round(overlap * 100)}% overlap with ${tasks[keep].id}, dropping`);
128
+ dominated.add(drop);
129
+ if (drop === i)
130
+ break;
131
+ }
132
+ }
133
+ }
134
+ if (dominated.size) {
135
+ tasks = tasks.filter((_, i) => !dominated.has(i));
136
+ onLog(`Deduplicated to ${tasks.length} tasks`);
137
+ }
138
+ }
139
+ // Warn on compound tasks joining unrelated changes with 'and'
140
+ for (const t of tasks) {
141
+ const parts = t.prompt.split(/\s+and\s+/i);
142
+ if (parts.length >= 2 &&
143
+ parts.every((p) => p.trim().split(/\s+/).length >= 3)) {
144
+ onLog(`⚠ Task ${t.id} looks compound ("…and…") — consider splitting into separate tasks`);
145
+ }
146
+ }
147
+ // Warn on file overlap between tasks
148
+ const fileRe = /(?:^|\s)((?:[\w.-]+\/)+[\w.-]+\.\w+)/g;
149
+ const pathToTasks = new Map();
150
+ for (const t of tasks) {
151
+ for (const m of t.prompt.matchAll(fileRe)) {
152
+ const ids = pathToTasks.get(m[1]);
153
+ if (ids)
154
+ ids.push(t.id);
155
+ else
156
+ pathToTasks.set(m[1], [t.id]);
157
+ }
158
+ }
159
+ for (const [path, ids] of pathToTasks) {
160
+ if (ids.length > 1)
161
+ onLog(`Overlap risk: ${path} in tasks ${ids.join(", ")}`);
162
+ }
163
+ // Warn if every task targets the same file — high merge conflict risk
164
+ if (tasks.length > 1 && pathToTasks.size === 1) {
165
+ const [singlePath] = pathToTasks.keys();
166
+ onLog(`⚠ All ${tasks.length} tasks target ${singlePath} — high merge conflict risk`);
167
+ }
168
+ // Cap at 20 tasks
169
+ if (tasks.length > 20) {
170
+ onLog(`Too many tasks (${tasks.length}), truncating to 20`);
171
+ tasks = tasks.slice(0, 20);
172
+ }
173
+ if (tasks.length === 0)
174
+ throw new Error("Planner generated 0 tasks");
175
+ // Sort test-related tasks last — they benefit from other changes landing first
176
+ tasks.sort((a, b) => Number(/\btest/i.test(a.prompt)) - Number(/\btest/i.test(b.prompt)));
177
+ onLog(`Generated ${tasks.length} tasks`);
178
+ return tasks;
179
+ }
180
+ /** Find the outermost balanced { } substring. */
181
+ function extractOutermostBraces(text) {
182
+ const start = text.indexOf("{");
183
+ if (start === -1)
184
+ return null;
185
+ let depth = 0;
186
+ for (let i = start; i < text.length; i++) {
187
+ if (text[i] === "{")
188
+ depth++;
189
+ else if (text[i] === "}")
190
+ depth--;
191
+ if (depth === 0)
192
+ return text.slice(start, i + 1);
193
+ }
194
+ return null;
195
+ }
196
+ /** Try multiple strategies to parse task JSON, with one retry callback. */
197
+ async function extractTaskJson(raw, retry) {
198
+ const attempt = (text) => {
199
+ // 1) Direct parse
200
+ try {
201
+ const obj = JSON.parse(text);
202
+ if (obj?.tasks)
203
+ return obj;
204
+ }
205
+ catch { }
206
+ // 2) Outermost braces
207
+ const braces = extractOutermostBraces(text);
208
+ if (braces) {
209
+ try {
210
+ const obj = JSON.parse(braces);
211
+ if (obj?.tasks)
212
+ return obj;
213
+ }
214
+ catch { }
215
+ }
216
+ // 3) Strip markdown fences and retry
217
+ const stripped = text.replace(/```json?\s*/g, "").replace(/```/g, "").trim();
218
+ if (stripped !== text) {
219
+ try {
220
+ const obj = JSON.parse(stripped);
221
+ if (obj?.tasks)
222
+ return obj;
223
+ }
224
+ catch { }
225
+ const braces2 = extractOutermostBraces(stripped);
226
+ if (braces2) {
227
+ try {
228
+ const obj = JSON.parse(braces2);
229
+ if (obj?.tasks)
230
+ return obj;
231
+ }
232
+ catch { }
233
+ }
234
+ }
235
+ return null;
236
+ };
237
+ const first = attempt(raw);
238
+ if (first)
239
+ return first;
240
+ // One retry with a shorter prompt
241
+ const retryText = await retry();
242
+ const second = attempt(retryText);
243
+ if (second)
244
+ return second;
245
+ throw new Error("Planner did not return valid task JSON after retry");
246
+ }
@@ -0,0 +1,68 @@
1
+ import type { Task, AgentState, SwarmPhase, PermMode, MergeStrategy } from "./types.js";
2
+ export interface SwarmConfig {
3
+ tasks: Task[];
4
+ concurrency: number;
5
+ cwd: string;
6
+ model?: string;
7
+ allowedTools?: string[];
8
+ useWorktrees?: boolean;
9
+ permissionMode?: PermMode;
10
+ agentTimeoutMs?: number;
11
+ maxRetries?: number;
12
+ mergeStrategy?: MergeStrategy;
13
+ }
14
+ export interface MergeResult {
15
+ branch: string;
16
+ ok: boolean;
17
+ autoResolved?: boolean;
18
+ error?: string;
19
+ filesChanged: number;
20
+ }
21
+ export declare class Swarm {
22
+ readonly agents: AgentState[];
23
+ readonly logs: {
24
+ time: number;
25
+ agentId: number;
26
+ text: string;
27
+ }[];
28
+ private readonly allLogs;
29
+ readonly startedAt: number;
30
+ readonly total: number;
31
+ completed: number;
32
+ failed: number;
33
+ totalCostUsd: number;
34
+ totalInputTokens: number;
35
+ totalOutputTokens: number;
36
+ phase: SwarmPhase;
37
+ aborted: boolean;
38
+ mergeResults: MergeResult[];
39
+ rateLimitUtilization: number;
40
+ rateLimitStatus: string;
41
+ private rateLimitResetsAt?;
42
+ private queue;
43
+ private config;
44
+ private nextId;
45
+ private worktreeBase?;
46
+ private activeQueries;
47
+ private cleanedUp;
48
+ logFile?: string;
49
+ readonly model: string | undefined;
50
+ constructor(config: SwarmConfig);
51
+ get active(): number;
52
+ get pending(): number;
53
+ run(): Promise<void>;
54
+ abort(): void;
55
+ log(agentId: number, text: string): void;
56
+ private worker;
57
+ private throttle;
58
+ private runAgent;
59
+ private autoCommit;
60
+ mergeBranch?: string;
61
+ private mergeAll;
62
+ cleanup(): void;
63
+ private warnDirtyTree;
64
+ private cleanStaleWorktrees;
65
+ private writeLogFile;
66
+ private agentSummary;
67
+ private handleMsg;
68
+ }