claude-overnight 1.50.5 → 1.51.1

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.50.5";
1
+ export declare const VERSION = "1.51.1";
@@ -1,2 +1,2 @@
1
1
  // Auto-generated by build — do not edit manually.
2
- export const VERSION = "1.50.5";
2
+ export const VERSION = "1.51.1";
@@ -21,10 +21,16 @@ let _envResolver;
21
21
  export function setPlannerEnvResolver(fn) {
22
22
  _envResolver = fn;
23
23
  }
24
- // ── Cursor proxy: direct HTTP bypass ──
25
- // SDK spawns 4+ subprocesses (~15s each) for the proxy; one direct POST is 4-10x faster.
26
- function isCursorProxyEnv(env) {
27
- return !!env?.CURSOR_API_KEY && !env?.ANTHROPIC_API_KEY;
24
+ // ── Direct HTTP bypass for non-Anthropic endpoints ──
25
+ // The Claude Code CLI subprocess spawned by @anthropic-ai/claude-agent-sdk validates
26
+ // model names against its built-in Anthropic list and rejects custom ids (qwen3.6-plus,
27
+ // composer-2, etc.) pre-flight, even when ANTHROPIC_BASE_URL points at an Anthropic-
28
+ // compatible proxy. Bypass the SDK with a direct POST for any non-anthropic.com base.
29
+ function shouldUseDirectFetch(env) {
30
+ const base = env?.ANTHROPIC_BASE_URL;
31
+ if (!base)
32
+ return false;
33
+ return !/^https?:\/\/(api\.)?anthropic\.com/i.test(base);
28
34
  }
29
35
  async function runViaDirectFetch(prompt, opts, onLog) {
30
36
  const env = opts.env ?? _envResolver?.(opts.model);
@@ -39,30 +45,34 @@ async function runViaDirectFetch(prompt, opts, onLog) {
39
45
  await apiEndpointLimiter.waitIfNeeded();
40
46
  const waited = await rl.waitIfNeeded();
41
47
  if (waited > 0)
42
- onLog(`Cursor proxy rate gate — waited ${Math.round(waited / 1000)}s`, "event");
48
+ onLog(`Planner proxy rate gate — waited ${Math.round(waited / 1000)}s`, "event");
43
49
  const res = await fetch(`${baseUrl}/v1/messages`, {
44
50
  method: "POST",
45
- headers: { "Content-Type": "application/json", "Authorization": `Bearer ${authToken}` },
51
+ headers: {
52
+ "Content-Type": "application/json",
53
+ "Authorization": `Bearer ${authToken}`,
54
+ "anthropic-version": "2023-06-01",
55
+ },
46
56
  body: JSON.stringify({ model: opts.model, max_tokens: 8192, messages: [{ role: "user", content: prompt }] }),
47
57
  });
48
58
  if (res.status === 429 && attempt < MAX_RETRIES) {
49
59
  const waitMs = BACKOFF[attempt];
50
- onLog(`Cursor proxy rate limited — waiting ${Math.round(waitMs / 1000)}s`, "event");
60
+ onLog(`Planner proxy rate limited — waiting ${Math.round(waitMs / 1000)}s`, "event");
51
61
  await sleep(waitMs);
52
62
  continue;
53
63
  }
54
64
  if (!res.ok)
55
- throw new Error(`Cursor proxy ${res.status}: ${(await res.text().catch(() => ""))}`);
65
+ throw new Error(`Planner proxy ${res.status}: ${(await res.text().catch(() => ""))}`);
56
66
  rl.record();
57
67
  apiEndpointLimiter.record();
58
68
  const data = await res.json();
59
69
  return data.content?.[0]?.text ?? "";
60
70
  }
61
- throw new Error("Cursor proxy direct fetch failed after retries");
71
+ throw new Error("Planner proxy direct fetch failed after retries");
62
72
  }
63
73
  export async function runPlannerQuery(prompt, opts, onLog) {
64
74
  const env = opts.env ?? _envResolver?.(opts.model);
65
- if (isCursorProxyEnv(env))
75
+ if (shouldUseDirectFetch(env))
66
76
  return runViaDirectFetch(prompt, opts, onLog);
67
77
  const MAX_RETRIES = 3;
68
78
  const BACKOFF = [30_000, 60_000, 120_000];
@@ -27,7 +27,11 @@ export const STEER_SCHEMA = {
27
27
  required: ["done", "tasks", "reasoning", "statusUpdate", "estimatedSessionsRemaining"],
28
28
  },
29
29
  };
30
- const PROMPT_BUDGET = 6000;
30
+ // The base 30-1_steer template alone is ~7 KB, so any budget below that is
31
+ // unreachable no matter how aggressively we trim variables. 20 KB leaves room
32
+ // for the template + moderate run memory while still being a tiny fraction of
33
+ // any planner's context window.
34
+ const PROMPT_BUDGET = 20_000;
31
35
  const DEFAULT_CAPS = {
32
36
  milestones: 2000, designs: 1500, reflections: 1000,
33
37
  verifications: 1000, previousRuns: 800, userGuidance: 4000,
@@ -109,9 +109,6 @@ export function envFor(p) {
109
109
  base.ANTHROPIC_AUTH_TOKEN = key;
110
110
  }
111
111
  delete base.ANTHROPIC_API_KEY;
112
- // Prevent CURSOR_API_KEY from leaking into non-proxy envs — would trip
113
- // isCursorProxyEnv false-positive, silently rerouting through direct fetch
114
- // which ignores outputFormat (no JSON schema enforcement).
115
112
  delete base.CURSOR_API_KEY;
116
113
  delete base.CURSOR_AUTH_TOKEN;
117
114
  return base;
package/dist/run/run.js CHANGED
@@ -545,10 +545,18 @@ export async function executeRun(cfg) {
545
545
  display.stop();
546
546
  // ── Finalize ──
547
547
  const trulyDone = objectiveComplete || (!flex && remaining <= 0);
548
- // User-initiated quit (or abort via 'q' / SIGINT / stall-watchdog) ⇒ save as
549
- // "stopped" so resume.ts offers the run and the incomplete work comes back.
550
548
  const userQuit = stopping || lastAborted;
551
549
  const wasCapped = lastCapped && !userQuit;
550
+ // Determine specific exit reason for the end brief
551
+ let exitReason;
552
+ if (trulyDone)
553
+ exitReason = "done";
554
+ else if (userQuit)
555
+ exitReason = "user-interrupted";
556
+ else if (wasCapped || remaining <= 0)
557
+ exitReason = "budget-exhausted";
558
+ else
559
+ exitReason = "planner-gave-up"; // steering returned false, planner couldn't produce tasks
552
560
  const finalPhase = trulyDone ? "done"
553
561
  : userQuit ? "stopped"
554
562
  : wasCapped ? "capped"
@@ -632,7 +640,7 @@ export async function executeRun(cfg) {
632
640
  runDir, runBranch, objective, waveNum, runStartedAt: cfg.runStartedAt,
633
641
  branches, waveHistory,
634
642
  accCost, accCompleted, accFailed, accTools, accIn, accOut,
635
- remaining, lastCapped, lastAborted, stopping, trulyDone,
643
+ remaining, lastCapped, lastAborted, stopping, trulyDone, exitReason,
636
644
  peakWorkerCtxTokens, peakWorkerCtxPct,
637
645
  currentSwarmLogFile: currentSwarm?.logFile,
638
646
  narrativeDeps: {
@@ -11,6 +11,7 @@ export interface FinalNarrativeDeps {
11
11
  /** Generate a longer narrative summary at run end. Awaited (not fire-and-forget)
12
12
  * because the caller wants the text inline in the final status block. */
13
13
  export declare function generateFinalNarrative(deps: FinalNarrativeDeps, phase: string): Promise<string>;
14
+ export type ExitReason = "done" | "budget-exhausted" | "user-interrupted" | "planner-gave-up" | "circuit-breaker" | "stalled";
14
15
  export interface SummaryArgs {
15
16
  runDir: string;
16
17
  runBranch?: string;
@@ -30,6 +31,7 @@ export interface SummaryArgs {
30
31
  lastAborted: boolean;
31
32
  stopping: boolean;
32
33
  trulyDone: boolean;
34
+ exitReason: ExitReason;
33
35
  peakWorkerCtxTokens: number;
34
36
  peakWorkerCtxPct: number;
35
37
  currentSwarmLogFile?: string;
@@ -32,7 +32,7 @@ export async function generateFinalNarrative(deps, phase) {
32
32
  }
33
33
  }
34
34
  export async function printFinalSummary(args) {
35
- const { runDir, runBranch, objective, waveNum, runStartedAt, branches, waveHistory, accCost, accCompleted, accFailed, accTools, accIn, accOut, remaining, lastCapped, lastAborted, stopping, trulyDone, peakWorkerCtxTokens, peakWorkerCtxPct, currentSwarmLogFile, narrativeDeps, } = args;
35
+ const { runDir, runBranch, objective, waveNum, runStartedAt, branches, waveHistory, accCost, accCompleted, accFailed, accTools, accIn, accOut, remaining, lastCapped, exitReason, peakWorkerCtxTokens, peakWorkerCtxPct, currentSwarmLogFile, narrativeDeps, } = args;
36
36
  const waves = waveNum + 1;
37
37
  const elapsed = Math.round((Date.now() - runStartedAt) / 1000);
38
38
  const elapsedStr = elapsed < 60 ? `${elapsed}s` : elapsed < 3600 ? `${Math.floor(elapsed / 60)}m ${elapsed % 60}s` : `${Math.floor(elapsed / 3600)}h ${Math.floor((elapsed % 3600) / 60)}m`;
@@ -40,26 +40,31 @@ export async function printFinalSummary(args) {
40
40
  const totalConflicts = branches.filter(b => b.status === "merge-failed").length;
41
41
  const termW = Math.max((process.stdout.columns ?? 80) || 80, 50);
42
42
  const rule = (c = "─") => chalk.dim(` ${c.repeat(Math.min(termW - 4, 60))}`);
43
- const phaseWord = trulyDone ? "complete"
44
- : remaining <= 0 || lastCapped ? "budget exhausted"
45
- : stopping || lastAborted ? "interrupted"
46
- : "stopped";
43
+ const bannerChar = accFailed === 0 ? "" : "─";
44
+ // Banner: title + subtitle explaining why the run ended
45
+ const banner = {
46
+ done: { icon: "", title: "CLAUDE OVERNIGHT -- COMPLETE", color: chalk.green, explain: "The planner determined the objective was achieved." },
47
+ "budget-exhausted": { icon: "⚠", title: "CLAUDE OVERNIGHT -- BUDGET EXHAUSTED", color: chalk.yellow, explain: "All allocated sessions were consumed." },
48
+ "user-interrupted": { icon: "⚠", title: "CLAUDE OVERNIGHT -- INTERRUPTED", color: chalk.yellow, explain: "You quit mid-run with [q] or a signal." },
49
+ "planner-gave-up": { icon: "⚠", title: "CLAUDE OVERNIGHT -- PLANNER GAVE UP", color: chalk.magenta, explain: "The planner could not decompose the remaining work into actionable tasks." },
50
+ "circuit-breaker": { icon: "⚠", title: "CLAUDE OVERNIGHT -- HALTED", color: chalk.red, explain: "2+ consecutive waves produced no merged changes." },
51
+ stalled: { icon: "⚠", title: "CLAUDE OVERNIGHT -- STALLED", color: chalk.magenta, explain: "No progress detected; the run was halted to preserve budget." },
52
+ }[exitReason] ?? { icon: "⚠", title: "CLAUDE OVERNIGHT -- STOPPED", color: chalk.magenta, explain: "The run ended without a clear reason." };
53
+ const narrativePhase = exitReason === "done" ? "complete"
54
+ : exitReason === "budget-exhausted" ? "budget exhausted"
55
+ : exitReason === "user-interrupted" ? "interrupted"
56
+ : exitReason === "planner-gave-up" ? "planner gave up"
57
+ : exitReason === "circuit-breaker" ? "circuit breaker"
58
+ : exitReason === "stalled" ? "stalled"
59
+ : "stopped";
47
60
  process.stdout.write(chalk.dim(`\n Writing final summary…`));
48
- const narrative = await generateFinalNarrative(narrativeDeps, phaseWord);
61
+ const narrative = await generateFinalNarrative(narrativeDeps, narrativePhase);
49
62
  process.stdout.write("\r" + " ".repeat(40) + "\r");
50
63
  console.log("");
51
- const bannerChar = accFailed === 0 ? "━" : "─";
52
- const bannerColor = trulyDone ? chalk.green : (stopping || lastAborted) ? chalk.yellow : chalk.magenta;
53
- console.log(bannerColor(` ${bannerChar.repeat(Math.min(termW - 4, 60))}`));
54
- if (trulyDone)
55
- console.log(chalk.bold.green(` ✓ CLAUDE OVERNIGHT -- COMPLETE`));
56
- else if (remaining <= 0 || lastCapped)
57
- console.log(chalk.bold.yellow(` ⚠ CLAUDE OVERNIGHT -- BUDGET EXHAUSTED`));
58
- else if (stopping || lastAborted)
59
- console.log(chalk.bold.yellow(` ⚠ CLAUDE OVERNIGHT -- INTERRUPTED`));
60
- else
61
- console.log(chalk.bold.yellow(` ⚠ CLAUDE OVERNIGHT -- STOPPED`));
62
- console.log(bannerColor(` ${bannerChar.repeat(Math.min(termW - 4, 60))}`));
64
+ console.log(banner.color(` ${bannerChar.repeat(Math.min(termW - 4, 60))}`));
65
+ console.log(chalk.bold(banner.color(` ${banner.icon} ${banner.title}`)));
66
+ console.log(chalk.dim(` ${banner.explain}`));
67
+ console.log(banner.color(` ${bannerChar.repeat(Math.min(termW - 4, 60))}`));
63
68
  console.log("");
64
69
  if (objective) {
65
70
  console.log(chalk.bold(" Objective"));
@@ -162,15 +167,34 @@ export async function printFinalSummary(args) {
162
167
  if (currentSwarmLogFile)
163
168
  console.log(chalk.dim(` Log: ${currentSwarmLogFile}`));
164
169
  console.log("");
165
- console.log(bannerColor(` ${bannerChar.repeat(Math.min(termW - 4, 60))}`));
166
- if (trulyDone)
167
- console.log(chalk.bold.green(` Done. Review the diff, then ship it.`));
168
- else if (remaining <= 0 || lastCapped)
169
- console.log(chalk.bold.yellow(` Paused on budget. Re-run with --resume to continue.`));
170
- else if (stopping || lastAborted)
171
- console.log(chalk.bold.yellow(` Interrupted. --resume to pick up where this left off.`));
172
- else
173
- console.log(chalk.bold.yellow(` Stopped. --resume to continue.`));
174
- console.log(bannerColor(` ${bannerChar.repeat(Math.min(termW - 4, 60))}`));
170
+ console.log(banner.color(` ${bannerChar.repeat(Math.min(termW - 4, 60))}`));
171
+ // Actionable next-steps based on exit reason
172
+ const endMsg = (() => {
173
+ switch (exitReason) {
174
+ case "done":
175
+ return "Review the diff, then ship it.";
176
+ case "budget-exhausted":
177
+ return remaining > 0
178
+ ? "Budget sessions remaining but usage cap hit. Raise the cap or re-run with --resume."
179
+ : "All sessions spent. Re-run with --resume to continue, or raise the budget.";
180
+ case "user-interrupted":
181
+ return "Run preserved. Use --resume to pick up where this left off.";
182
+ case "planner-gave-up": {
183
+ const lines = ["Planner could not decompose remaining work."];
184
+ if (remaining > 0)
185
+ lines.push(`${remaining} sessions unused — the work may be too vague or out of scope.`);
186
+ lines.push("Refine the objective or break it down manually, then re-run.");
187
+ return lines.join(" ");
188
+ }
189
+ case "circuit-breaker":
190
+ return "No changes landed in 2+ waves. Check for merge conflicts or agent errors in the log.";
191
+ case "stalled":
192
+ return "Run halted to preserve budget. Inspect status.md for blockers, then --resume.";
193
+ default:
194
+ return "Run preserved. --resume to continue.";
195
+ }
196
+ })();
197
+ console.log(chalk.bold(banner.color(` ${endMsg}`)));
198
+ console.log(banner.color(` ${bannerChar.repeat(Math.min(termW - 4, 60))}`));
175
199
  console.log("");
176
200
  }
@@ -362,12 +362,13 @@ export async function runWaveLoop(host, ctx) {
362
362
  const librarianStart = Date.now();
363
363
  let librarianPromoted = 0, librarianPatched = 0, librarianQuarantined = 0, librarianRejected = 0;
364
364
  try {
365
+ const librarianModel = host.fastModel ?? host.workerModel;
365
366
  const lr = await runLibrarian({
366
367
  fingerprint: host.repoFingerprint,
367
368
  runId: host.runId,
368
369
  wave: host.waveNum,
369
370
  cwd: ctx.cwd,
370
- model: host.plannerModel,
371
+ model: librarianModel,
371
372
  envForModel: ctx.envForModel,
372
373
  });
373
374
  librarianPromoted = lr.promoted;
@@ -1,4 +1,3 @@
1
- import { query } from "@anthropic-ai/claude-agent-sdk";
2
1
  import { readFileSync, writeFileSync, mkdirSync, renameSync, existsSync, readdirSync, appendFileSync, } from "node:fs";
3
2
  import { join } from "node:path";
4
3
  import { openSkillsDb } from "./index-db.js";
@@ -84,44 +83,44 @@ function buildSubagentInput(canon, candidates, abOutcomes) {
84
83
  return JSON.stringify({ canon, candidates, ab_outcomes: abOutcomes });
85
84
  }
86
85
  // ── Subagent call ──
86
+ // Direct POST /v1/messages — no tools needed, so the Agent SDK's CLI subprocess
87
+ // (with its multi-KB built-in system prompt and turn loop) is pure overhead and
88
+ // also pre-flight-rejects non-Anthropic model ids (qwen, composer-2, ...) even
89
+ // when routed through an Anthropic-compatible proxy.
87
90
  async function callLibrarianSubagent(input, data) {
88
91
  const env = input.envForModel?.(input.model);
89
92
  const prompt = renderPrompt("40_skills/40-3_librarian-wrap", { vars: { data } });
90
- let timedOut = false;
91
- const timer = setTimeout(() => { timedOut = true; }, LIBRARIAN_TIMEOUT_MS);
93
+ const baseUrl = (env?.ANTHROPIC_BASE_URL ?? process.env.ANTHROPIC_BASE_URL ?? "https://api.anthropic.com").replace(/\/$/, "");
94
+ const headers = {
95
+ "Content-Type": "application/json",
96
+ "anthropic-version": "2023-06-01",
97
+ };
98
+ const bearer = env?.ANTHROPIC_AUTH_TOKEN ?? process.env.ANTHROPIC_AUTH_TOKEN;
99
+ const apiKey = env?.ANTHROPIC_API_KEY ?? process.env.ANTHROPIC_API_KEY;
100
+ if (bearer)
101
+ headers["Authorization"] = `Bearer ${bearer}`;
102
+ else if (apiKey)
103
+ headers["x-api-key"] = apiKey;
92
104
  try {
93
- const pq = query({
94
- prompt,
95
- options: {
96
- cwd: input.cwd,
97
- model: input.model,
98
- permissionMode: "bypassPermissions",
99
- allowDangerouslySkipPermissions: true,
100
- maxTurns: 8,
101
- ...(env && { env }),
102
- },
105
+ const res = await fetch(`${baseUrl}/v1/messages`, {
106
+ method: "POST",
107
+ headers,
108
+ body: JSON.stringify({ model: input.model, max_tokens: 8192, messages: [{ role: "user", content: prompt }] }),
109
+ signal: AbortSignal.timeout(LIBRARIAN_TIMEOUT_MS),
103
110
  });
104
- let resultText = "";
105
- for await (const msg of pq) {
106
- if (timedOut) {
107
- pq.interrupt().catch(() => { });
108
- break;
109
- }
110
- if (msg.type === "result" && msg.subtype === "success") {
111
- resultText = msg.result || "";
112
- }
113
- }
114
- pq.close();
115
- if (timedOut) {
116
- process.stderr.write("[librarian] subagent timed out\n");
111
+ if (!res.ok) {
112
+ process.stderr.write(`[librarian] HTTP ${res.status}: ${(await res.text().catch(() => "")).slice(0, 200)}\n`);
117
113
  return null;
118
114
  }
119
- // Parse JSON try direct parse first, then strip markdown fences
115
+ const body = await res.json();
116
+ const resultText = body.content?.map(c => c.text ?? "").join("") ?? "";
120
117
  const cleaned = resultText.replace(/^```(?:json)?\s*\n([\s\S]*?)\n```\s*$/, "$1").trim();
121
118
  return JSON.parse(cleaned);
122
119
  }
123
- finally {
124
- clearTimeout(timer);
120
+ catch (err) {
121
+ const msg = err instanceof Error ? err.message : String(err);
122
+ process.stderr.write(`[librarian] ${msg.includes("aborted") ? "timed out" : `error: ${msg}`}\n`);
123
+ return null;
125
124
  }
126
125
  }
127
126
  // ── Action application ──
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-overnight",
3
- "version": "1.50.5",
3
+ "version": "1.51.1",
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.50.5",
3
+ "version": "1.51.1",
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"