claude-overnight 1.25.20 → 1.25.21

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.
@@ -1 +1 @@
1
- export declare const VERSION = "1.25.20";
1
+ export declare const VERSION = "1.25.21";
package/dist/_version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // Auto-generated by build — do not edit manually.
2
- export const VERSION = "1.25.20";
2
+ export const VERSION = "1.25.21";
@@ -25,6 +25,13 @@ export interface PlannerOpts {
25
25
  };
26
26
  /** When set, stream events are appended to <runDir>/transcripts/<name>.ndjson */
27
27
  transcriptName?: string;
28
+ /**
29
+ * Hard cap on conversation turns. Protects against runaway exploration when
30
+ * the underlying endpoint ignores `outputFormat` (e.g. the Cursor proxy
31
+ * strips json_schema, leaving thinking models with no signal to stop).
32
+ * Defaults to 20 — generous for recon, bounded against infinite loops.
33
+ */
34
+ maxTurns?: number;
28
35
  }
29
36
  export declare function setPlannerEnvResolver(fn: ((model?: string) => Record<string, string> | undefined) | undefined): void;
30
37
  export declare function getTotalPlannerCost(): number;
@@ -2,6 +2,7 @@ import { query } from "@anthropic-ai/claude-agent-sdk";
2
2
  import { readFileSync } from "fs";
3
3
  import { NudgeError } from "./types.js";
4
4
  import { writeTranscriptEvent } from "./transcripts.js";
5
+ const DEFAULT_MAX_TURNS = 20;
5
6
  // ── Shared env resolver (set once at run start, used by every planner query) ──
6
7
  //
7
8
  // Swarm and planner calls share a model→env map so a custom provider configured
@@ -149,6 +150,7 @@ async function runPlannerQueryOnce(prompt, opts, onLog) {
149
150
  ...(opts.permissionMode === "bypassPermissions" && { allowDangerouslySkipPermissions: true }),
150
151
  persistSession: true,
151
152
  includePartialMessages: true,
153
+ maxTurns: opts.maxTurns ?? DEFAULT_MAX_TURNS,
152
154
  ...(isResume && { resume: opts.resumeSessionId }),
153
155
  ...(opts.outputFormat && { outputFormat: opts.outputFormat }),
154
156
  ...(envOverride && { env: envOverride }),
@@ -202,6 +204,11 @@ async function runPlannerQueryOnce(prompt, opts, onLog) {
202
204
  // Track the open tool block so we can re-log with the enriched target once
203
205
  // the input arrives, and write a complete transcript entry on block stop.
204
206
  let pendingTool = null;
207
+ // Dedup tool_use logging between stream_event path and full-assistant-message
208
+ // path. The Cursor proxy doesn't always relay content_block_* frames through
209
+ // the Claude CLI's stream-json, so we also mine complete `SDKAssistantMessage`
210
+ // content for progress -- without double-counting when both paths fire.
211
+ const seenToolIds = new Set();
205
212
  const logTool = (name, input) => {
206
213
  const target = extractToolTarget(input);
207
214
  lastLogText = target ? `${name} ${target}` : name;
@@ -220,6 +227,8 @@ async function runPlannerQueryOnce(prompt, opts, onLog) {
220
227
  toolCount++;
221
228
  const input = (cb.input ?? {});
222
229
  const hasInput = Object.keys(input).length > 0;
230
+ if (cb.id)
231
+ seenToolIds.add(cb.id);
223
232
  pendingTool = {
224
233
  index: ev.index ?? 0,
225
234
  name: cb.name,
@@ -274,6 +283,40 @@ async function runPlannerQueryOnce(prompt, opts, onLog) {
274
283
  pendingTool = null;
275
284
  }
276
285
  }
286
+ // Fallback progress surfacing: when stream events are sparse (e.g. the
287
+ // Cursor proxy's heartbeat thinking block doesn't always round-trip
288
+ // through the Claude CLI as partial messages), mine the full assistant
289
+ // turn message for tool_use / thinking / text so the ticker still moves
290
+ // every ~6-15s instead of sitting silent for minutes.
291
+ if (msg.type === "assistant") {
292
+ const content = msg.message?.content;
293
+ if (Array.isArray(content)) {
294
+ for (const part of content) {
295
+ if (part?.type === "tool_use" && part.id && !seenToolIds.has(part.id)) {
296
+ seenToolIds.add(part.id);
297
+ toolCount++;
298
+ const input = (part.input ?? {});
299
+ logTool(part.name, input);
300
+ if (tname)
301
+ writeTranscriptEvent(tname, { kind: "tool_use", tool: part.name, input });
302
+ }
303
+ else if (part?.type === "thinking" && typeof part.thinking === "string" && part.thinking) {
304
+ const snippet = part.thinking.trim().replace(/\s+/g, " ").slice(-60);
305
+ if (snippet)
306
+ lastLogText = snippet;
307
+ if (tname)
308
+ writeTranscriptEvent(tname, { kind: "thinking", text: part.thinking });
309
+ }
310
+ else if (part?.type === "text" && typeof part.text === "string" && part.text) {
311
+ const snippet = part.text.trim().replace(/[{}"\\,[\]]+/g, " ").replace(/\s+/g, " ").slice(-60);
312
+ if (snippet.length > 5)
313
+ lastLogText = snippet;
314
+ if (tname)
315
+ writeTranscriptEvent(tname, { kind: "text", text: part.text });
316
+ }
317
+ }
318
+ }
319
+ }
277
320
  if (msg.type === "rate_limit_event") {
278
321
  const info = msg.rate_limit_info;
279
322
  if (info) {
package/dist/planner.js CHANGED
@@ -158,7 +158,7 @@ export async function planTasks(objective, cwd, plannerModel, workerModel, permi
158
158
  const fileInstruction = outFile ? `\n\nAFTER generating the JSON, also write it to ${outFile} using the Write tool.` : "";
159
159
  let resultText;
160
160
  try {
161
- resultText = await runPlannerQuery(prompt + fileInstruction, { cwd, model: plannerModel, permissionMode, outputFormat: TASKS_SCHEMA, transcriptName }, onLog);
161
+ resultText = await runPlannerQuery(prompt + fileInstruction, { cwd, model: plannerModel, permissionMode, outputFormat: TASKS_SCHEMA, transcriptName, maxTurns: 40 }, onLog);
162
162
  }
163
163
  catch (err) {
164
164
  const salvaged = salvageFromFile(outFile, budget, onLog, err?.message ?? String(err));
@@ -168,7 +168,7 @@ export async function planTasks(objective, cwd, plannerModel, workerModel, permi
168
168
  }
169
169
  const parsed = await extractTaskJson(resultText, async () => {
170
170
  onLog("Retrying...");
171
- return runPlannerQuery(`Your previous response was not valid JSON. Respond with ONLY a JSON object {"tasks":[{"prompt":"..."}]}.\n\n${prompt}`, { cwd, model: plannerModel, permissionMode, outputFormat: TASKS_SCHEMA, transcriptName: `${transcriptName}-retry` }, onLog);
171
+ return runPlannerQuery(`Your previous response was not valid JSON. Respond with ONLY a JSON object {"tasks":[{"prompt":"..."}]}.\n\n${prompt}`, { cwd, model: plannerModel, permissionMode, outputFormat: TASKS_SCHEMA, transcriptName: `${transcriptName}-retry`, maxTurns: 15 }, onLog);
172
172
  }, onLog, outFile);
173
173
  let tasks = (parsed.tasks || []).map((t, i) => ({
174
174
  id: String(i), prompt: typeof t === "string" ? t : t.prompt,
@@ -188,7 +188,7 @@ Then pick ${count} angles that carve up THIS specific codebase orthogonally. Pre
188
188
 
189
189
  Objective: ${objective}
190
190
 
191
- Return ONLY a JSON object: {"themes": ["angle description", ...]}`, { cwd, model, permissionMode, outputFormat: THEMES_SCHEMA, transcriptName }, onLog);
191
+ Return ONLY a JSON object: {"themes": ["angle description", ...]}`, { cwd, model, permissionMode, outputFormat: THEMES_SCHEMA, transcriptName, maxTurns: 12 }, onLog);
192
192
  const parsed = attemptJsonParse(resultText);
193
193
  if (parsed?.themes && Array.isArray(parsed.themes))
194
194
  return parsed.themes.slice(0, count);
@@ -259,7 +259,7 @@ Respond with ONLY a JSON object (no markdown fences):
259
259
  onLog("Synthesizing...");
260
260
  let resultText;
261
261
  try {
262
- resultText = await runPlannerQuery(prompt, { cwd, model: plannerModel, permissionMode, outputFormat: TASKS_SCHEMA, transcriptName }, onLog);
262
+ resultText = await runPlannerQuery(prompt, { cwd, model: plannerModel, permissionMode, outputFormat: TASKS_SCHEMA, transcriptName, maxTurns: 25 }, onLog);
263
263
  }
264
264
  catch (err) {
265
265
  const salvaged = salvageFromFile(outFile, budget, onLog, err?.message ?? String(err));
@@ -269,7 +269,7 @@ Respond with ONLY a JSON object (no markdown fences):
269
269
  }
270
270
  const parsed = await extractTaskJson(resultText, async () => {
271
271
  onLog("Retrying...");
272
- return runPlannerQuery(`Your previous response was not valid JSON. Respond with ONLY a JSON object {"tasks":[{"prompt":"..."}]}.\n\n${prompt}`, { cwd, model: plannerModel, permissionMode, outputFormat: TASKS_SCHEMA, transcriptName: `${transcriptName}-retry` }, onLog);
272
+ return runPlannerQuery(`Your previous response was not valid JSON. Respond with ONLY a JSON object {"tasks":[{"prompt":"..."}]}.\n\n${prompt}`, { cwd, model: plannerModel, permissionMode, outputFormat: TASKS_SCHEMA, transcriptName: `${transcriptName}-retry`, maxTurns: 10 }, onLog);
273
273
  }, onLog, outFile);
274
274
  let tasks = (parsed.tasks || []).map((t, i) => ({
275
275
  id: String(i), prompt: typeof t === "string" ? t : t.prompt,
@@ -303,10 +303,10 @@ ${scaleNote} ${concurrency} agents run in parallel. Update the plan accordingly.
303
303
 
304
304
  Respond with ONLY a JSON object (no markdown):
305
305
  {"tasks":[{"prompt":"..."}]}`;
306
- const resultText = await runPlannerQuery(prompt, { cwd, model: plannerModel, permissionMode, outputFormat: TASKS_SCHEMA, transcriptName }, onLog);
306
+ const resultText = await runPlannerQuery(prompt, { cwd, model: plannerModel, permissionMode, outputFormat: TASKS_SCHEMA, transcriptName, maxTurns: 15 }, onLog);
307
307
  const parsed = await extractTaskJson(resultText, async () => {
308
308
  onLog("Retrying...");
309
- return runPlannerQuery(`Your previous response was not valid JSON. Respond with ONLY a JSON object {"tasks":[{"prompt":"..."}]}.\n\n${prompt}`, { cwd, model: plannerModel, permissionMode, outputFormat: TASKS_SCHEMA, transcriptName: `${transcriptName}-retry` }, onLog);
309
+ return runPlannerQuery(`Your previous response was not valid JSON. Respond with ONLY a JSON object {"tasks":[{"prompt":"..."}]}.\n\n${prompt}`, { cwd, model: plannerModel, permissionMode, outputFormat: TASKS_SCHEMA, transcriptName: `${transcriptName}-retry`, maxTurns: 8 }, onLog);
310
310
  }, onLog);
311
311
  let tasks = (parsed.tasks || []).map((t, i) => ({
312
312
  id: String(i), prompt: typeof t === "string" ? t : t.prompt,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-overnight",
3
- "version": "1.25.20",
3
+ "version": "1.25.21",
4
4
  "description": "Parallel Claude agents in git worktrees with a usage cap that reserves headroom for your interactive Claude Code. Crash-safe resume. Provider-agnostic model catalog (Anthropic, Cursor, OpenAI, Gemini, DeepSeek, Llama, Qwen) with capability-based task scoping.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-overnight",
3
- "version": "1.25.20",
3
+ "version": "1.25.21",
4
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. Supports Cursor API Proxy, Qwen, OpenRouter.",
5
5
  "author": {
6
6
  "name": "Francesco Fornace"