claude-overnight 1.25.20 → 1.25.22

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.22";
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.22";
package/dist/index.js CHANGED
@@ -822,14 +822,44 @@ async function main() {
822
822
  // proxy now uses an account pool (`CURSOR_CONFIG_DIRS`) — each parallel
823
823
  // cursor-agent subprocess gets its own config dir, eliminating the race.
824
824
  // See ensureCursorAccountPool() in providers.ts.
825
- const progress = (msg) => process.stdout.write(chalk.dim(` ${msg}\n`));
825
+ //
826
+ // Single in-place status line collapses N parallel progress streams (one
827
+ // per provider) into one tty line updated via `\r` + ANSI clear. Keeps the
828
+ // "window head" calm instead of appending 3 lines per 3s tick.
829
+ const statuses = new Map();
830
+ const isTTY = process.stdout.isTTY;
831
+ let statusLineActive = false;
832
+ const renderStatus = () => {
833
+ if (!isTTY)
834
+ return;
835
+ const parts = [...statuses.entries()].map(([r, s]) => `${r} ${chalk.dim(s)}`);
836
+ process.stdout.write(`\x1B[2K\r`);
837
+ if (parts.length === 0) {
838
+ statusLineActive = false;
839
+ return;
840
+ }
841
+ process.stdout.write(chalk.dim(" " + parts.join(" · ")));
842
+ statusLineActive = true;
843
+ };
844
+ const clearStatusLine = () => {
845
+ if (isTTY && statusLineActive) {
846
+ process.stdout.write(`\x1B[2K\r`);
847
+ statusLineActive = false;
848
+ }
849
+ };
826
850
  /** Cursor agent cold start + thinking-variant model latency can exceed 20s; API providers stay tight. */
827
851
  const preflightMs = (p) => isCursorProxyProvider(p) ? 60_000 : 20_000;
828
- const results = await Promise.all(pending.map(async ([role, p]) => ({
829
- role,
830
- provider: p,
831
- result: await preflightProvider(p, cwd, preflightMs(p), { onProgress: progress }),
832
- })));
852
+ const results = await Promise.all(pending.map(async ([role, p]) => {
853
+ statuses.set(role, "connecting…");
854
+ renderStatus();
855
+ const result = await preflightProvider(p, cwd, preflightMs(p), {
856
+ onProgress: (msg) => { statuses.set(role, msg); renderStatus(); },
857
+ });
858
+ statuses.delete(role);
859
+ renderStatus();
860
+ return { role, provider: p, result };
861
+ }));
862
+ clearStatusLine();
833
863
  for (const { role, provider, result } of results) {
834
864
  if (!result.ok) {
835
865
  console.error(chalk.red(` ✗ ${role} preflight failed: ${chalk.dim(result.error)}`));
@@ -25,6 +25,16 @@ 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. Defaults to 20.
30
+ */
31
+ maxTurns?: number;
32
+ /**
33
+ * Tools the planner agent may use. Defaults to read-only + Write (for outFile
34
+ * resilience). Deliberately excludes Bash/Agent/TodoWrite/WebFetch to prevent
35
+ * the multi-turn tool loops that cause error_max_turns with thinking models.
36
+ */
37
+ tools?: string[];
28
38
  }
29
39
  export declare function setPlannerEnvResolver(fn: ((model?: string) => Record<string, string> | undefined) | undefined): void;
30
40
  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
@@ -143,20 +144,32 @@ async function runPlannerQueryOnce(prompt, opts, onLog) {
143
144
  options: {
144
145
  cwd: opts.cwd,
145
146
  model: opts.model,
146
- tools: ["Read", "Glob", "Grep", "Write", "Bash", "WebFetch", "WebSearch", "TodoWrite", "Agent"],
147
- allowedTools: ["Read", "Glob", "Grep", "Write", "Bash", "WebFetch", "WebSearch", "TodoWrite", "Agent"],
147
+ tools: opts.tools ?? ["Read", "Glob", "Grep", "Write"],
148
+ allowedTools: opts.tools ?? ["Read", "Glob", "Grep", "Write"],
148
149
  permissionMode: opts.permissionMode,
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 }),
155
157
  },
156
158
  });
157
- let lastLogText = "";
159
+ // Default to "thinking…" so the ticker conveys meaning during the pre-output
160
+ // reasoning phase. Thinking-variant models (e.g. claude-opus-4-7-thinking-*)
161
+ // can sit silent for 60-90s before emitting any tokens, and cursor-agent
162
+ // doesn't forward thinking deltas — without this, the ticker reads "4m 5s"
163
+ // with nothing else for a minute plus.
164
+ let lastLogText = "thinking…";
158
165
  let toolCount = 0;
159
166
  let costUsd = 0;
167
+ const jsonOutput = opts.outputFormat?.type === "json_schema";
168
+ let jsonCharCount = 0;
169
+ // Dedup identical text snippets: cursor-agent with json_schema-ignoring
170
+ // proxies causes the SDK to loop multiple turns, each re-emitting the same
171
+ // final JSON. We don't want to spam the ticker or transcript with it.
172
+ let lastTextSeen = "";
160
173
  const ticker = setInterval(() => {
161
174
  const elapsed = Math.round((Date.now() - startedAt) / 1000);
162
175
  const m = Math.floor(elapsed / 60);
@@ -202,6 +215,11 @@ async function runPlannerQueryOnce(prompt, opts, onLog) {
202
215
  // Track the open tool block so we can re-log with the enriched target once
203
216
  // the input arrives, and write a complete transcript entry on block stop.
204
217
  let pendingTool = null;
218
+ // Dedup tool_use logging between stream_event path and full-assistant-message
219
+ // path. The Cursor proxy doesn't always relay content_block_* frames through
220
+ // the Claude CLI's stream-json, so we also mine complete `SDKAssistantMessage`
221
+ // content for progress -- without double-counting when both paths fire.
222
+ const seenToolIds = new Set();
205
223
  const logTool = (name, input) => {
206
224
  const target = extractToolTarget(input);
207
225
  lastLogText = target ? `${name} ${target}` : name;
@@ -220,6 +238,8 @@ async function runPlannerQueryOnce(prompt, opts, onLog) {
220
238
  toolCount++;
221
239
  const input = (cb.input ?? {});
222
240
  const hasInput = Object.keys(input).length > 0;
241
+ if (cb.id)
242
+ seenToolIds.add(cb.id);
223
243
  pendingTool = {
224
244
  index: ev.index ?? 0,
225
245
  name: cb.name,
@@ -252,9 +272,18 @@ async function runPlannerQueryOnce(prompt, opts, onLog) {
252
272
  : delta?.type === "thinking_delta" ? delta.thinking
253
273
  : undefined;
254
274
  if (typeof raw === "string" && raw) {
255
- const snippet = raw.trim().replace(/[{}"\\,[\]]+/g, " ").replace(/\s+/g, " ").trim();
256
- if (snippet.length > 5)
257
- lastLogText = snippet.slice(-60);
275
+ if (jsonOutput && delta.type === "text_delta") {
276
+ // Don't surface tail-of-JSON as "progress" — it reads as noise
277
+ // like `…ppression and optimistic-update rollback`. Show size
278
+ // growing instead, which is a genuine signal.
279
+ jsonCharCount += raw.length;
280
+ lastLogText = `writing JSON (${jsonCharCount} chars)…`;
281
+ }
282
+ else {
283
+ const snippet = raw.trim().replace(/[{}"\\,[\]]+/g, " ").replace(/\s+/g, " ").trim();
284
+ if (snippet.length > 5)
285
+ lastLogText = snippet.slice(-60);
286
+ }
258
287
  if (tname)
259
288
  writeTranscriptEvent(tname, { kind: delta.type, text: raw });
260
289
  }
@@ -274,6 +303,48 @@ async function runPlannerQueryOnce(prompt, opts, onLog) {
274
303
  pendingTool = null;
275
304
  }
276
305
  }
306
+ // Fallback progress surfacing: when stream events are sparse (e.g. the
307
+ // Cursor proxy's heartbeat thinking block doesn't always round-trip
308
+ // through the Claude CLI as partial messages), mine the full assistant
309
+ // turn message for tool_use / thinking / text so the ticker still moves
310
+ // every ~6-15s instead of sitting silent for minutes.
311
+ if (msg.type === "assistant") {
312
+ const content = msg.message?.content;
313
+ if (Array.isArray(content)) {
314
+ for (const part of content) {
315
+ if (part?.type === "tool_use" && part.id && !seenToolIds.has(part.id)) {
316
+ seenToolIds.add(part.id);
317
+ toolCount++;
318
+ const input = (part.input ?? {});
319
+ logTool(part.name, input);
320
+ if (tname)
321
+ writeTranscriptEvent(tname, { kind: "tool_use", tool: part.name, input });
322
+ }
323
+ else if (part?.type === "thinking" && typeof part.thinking === "string" && part.thinking) {
324
+ const snippet = part.thinking.trim().replace(/\s+/g, " ").slice(-60);
325
+ if (snippet)
326
+ lastLogText = snippet;
327
+ if (tname)
328
+ writeTranscriptEvent(tname, { kind: "thinking", text: part.thinking });
329
+ }
330
+ else if (part?.type === "text" && typeof part.text === "string" && part.text) {
331
+ if (part.text === lastTextSeen)
332
+ continue; // dedup repeated turns
333
+ lastTextSeen = part.text;
334
+ if (jsonOutput) {
335
+ lastLogText = `writing JSON (${part.text.length} chars)…`;
336
+ }
337
+ else {
338
+ const snippet = part.text.trim().replace(/[{}"\\,[\]]+/g, " ").replace(/\s+/g, " ").slice(-60);
339
+ if (snippet.length > 5)
340
+ lastLogText = snippet;
341
+ }
342
+ if (tname)
343
+ writeTranscriptEvent(tname, { kind: "text", text: part.text });
344
+ }
345
+ }
346
+ }
347
+ }
277
348
  if (msg.type === "rate_limit_event") {
278
349
  const info = msg.rate_limit_info;
279
350
  if (info) {
package/dist/planner.js CHANGED
@@ -158,7 +158,8 @@ 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,
162
+ tools: ["Read", "Glob", "Grep", "Write"] }, onLog);
162
163
  }
163
164
  catch (err) {
164
165
  const salvaged = salvageFromFile(outFile, budget, onLog, err?.message ?? String(err));
@@ -168,7 +169,7 @@ export async function planTasks(objective, cwd, plannerModel, workerModel, permi
168
169
  }
169
170
  const parsed = await extractTaskJson(resultText, async () => {
170
171
  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);
172
+ 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
173
  }, onLog, outFile);
173
174
  let tasks = (parsed.tasks || []).map((t, i) => ({
174
175
  id: String(i), prompt: typeof t === "string" ? t : t.prompt,
@@ -188,7 +189,7 @@ Then pick ${count} angles that carve up THIS specific codebase orthogonally. Pre
188
189
 
189
190
  Objective: ${objective}
190
191
 
191
- Return ONLY a JSON object: {"themes": ["angle description", ...]}`, { cwd, model, permissionMode, outputFormat: THEMES_SCHEMA, transcriptName }, onLog);
192
+ Return ONLY a JSON object: {"themes": ["angle description", ...]}`, { cwd, model, permissionMode, outputFormat: THEMES_SCHEMA, transcriptName, maxTurns: 12 }, onLog);
192
193
  const parsed = attemptJsonParse(resultText);
193
194
  if (parsed?.themes && Array.isArray(parsed.themes))
194
195
  return parsed.themes.slice(0, count);
@@ -259,7 +260,8 @@ Respond with ONLY a JSON object (no markdown fences):
259
260
  onLog("Synthesizing...");
260
261
  let resultText;
261
262
  try {
262
- resultText = await runPlannerQuery(prompt, { cwd, model: plannerModel, permissionMode, outputFormat: TASKS_SCHEMA, transcriptName }, onLog);
263
+ resultText = await runPlannerQuery(prompt, { cwd, model: plannerModel, permissionMode, outputFormat: TASKS_SCHEMA, transcriptName, maxTurns: 25,
264
+ tools: ["Write"] }, onLog);
263
265
  }
264
266
  catch (err) {
265
267
  const salvaged = salvageFromFile(outFile, budget, onLog, err?.message ?? String(err));
@@ -269,7 +271,7 @@ Respond with ONLY a JSON object (no markdown fences):
269
271
  }
270
272
  const parsed = await extractTaskJson(resultText, async () => {
271
273
  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);
274
+ 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
275
  }, onLog, outFile);
274
276
  let tasks = (parsed.tasks || []).map((t, i) => ({
275
277
  id: String(i), prompt: typeof t === "string" ? t : t.prompt,
@@ -303,10 +305,10 @@ ${scaleNote} ${concurrency} agents run in parallel. Update the plan accordingly.
303
305
 
304
306
  Respond with ONLY a JSON object (no markdown):
305
307
  {"tasks":[{"prompt":"..."}]}`;
306
- const resultText = await runPlannerQuery(prompt, { cwd, model: plannerModel, permissionMode, outputFormat: TASKS_SCHEMA, transcriptName }, onLog);
308
+ const resultText = await runPlannerQuery(prompt, { cwd, model: plannerModel, permissionMode, outputFormat: TASKS_SCHEMA, transcriptName, maxTurns: 15 }, onLog);
307
309
  const parsed = await extractTaskJson(resultText, async () => {
308
310
  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);
311
+ 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
312
  }, onLog);
311
313
  let tasks = (parsed.tasks || []).map((t, i) => ({
312
314
  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.22",
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.22",
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"