claude-overnight 1.50.1 → 1.50.5

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.1";
1
+ export declare const VERSION = "1.50.5";
@@ -1,2 +1,2 @@
1
1
  // Auto-generated by build — do not edit manually.
2
- export const VERSION = "1.50.1";
2
+ export const VERSION = "1.50.5";
@@ -310,6 +310,10 @@ export interface RunState extends RunConfigBase {
310
310
  coachedObjective?: string;
311
311
  /** Unix timestamp (ms) when the coach produced the accepted rewrite. */
312
312
  coachedAt?: number;
313
+ /** Run-branch created when mergeStrategy="branch" — must survive resume so subsequent waves accumulate into it instead of orphan branches. */
314
+ runBranch?: string;
315
+ /** Branch (or detached HEAD sha) the user was on before the run-branch was created — restored on run completion. */
316
+ originalRef?: string;
313
317
  }
314
318
  /** Function that returns a rate-limit snapshot with optional context token info. */
315
319
  export type RLGetter = () => {
package/dist/run/run.js CHANGED
@@ -10,6 +10,7 @@ import { buildEnvResolver, isCursorProxyProvider } from "../providers/index.js";
10
10
  import { RunDisplay } from "../ui/ui.js";
11
11
  import { renderSummary } from "../ui/summary.js";
12
12
  import { readRunMemory, writeStatus, writeGoalUpdate, saveRunState, saveWaveSession, loadWaveHistory, recordBranches, archiveMilestone, writeSteerInbox, consumeSteerInbox, countSteerInbox, appendOvernightLogStart, updateOvernightLogEnd, } from "../state/state.js";
13
+ import { composeRunState } from "../state/run-state.js";
13
14
  import { runPostRunReview } from "./review.js";
14
15
  import { printFinalSummary } from "./summary.js";
15
16
  import { runWaveLoop } from "./wave-loop.js";
@@ -206,10 +207,25 @@ export async function executeRun(cfg) {
206
207
  .then(text => { display.setDebrief(text.trim().slice(0, 210), label); })
207
208
  .catch(() => { display.setDebrief(undefined); });
208
209
  };
209
- // For flex + branch strategy: create one target branch
210
+ // For flex + branch strategy: create one target branch (or restore on resume).
211
+ // The run-branch + originalRef are persisted in run.json so resumes accumulate
212
+ // into the original branch instead of spawning orphan swarm/run-* branches.
210
213
  let runBranch;
211
214
  let originalRef;
212
- if (flex && mergeStrategy === "branch" && useWorktrees && !cfg.resuming) {
215
+ if (cfg.resuming && cfg.resumeState) {
216
+ runBranch = cfg.resumeState.runBranch;
217
+ originalRef = cfg.resumeState.originalRef;
218
+ if (runBranch) {
219
+ try {
220
+ execSync(`git checkout "${runBranch}"`, { cwd, encoding: "utf-8", stdio: "pipe" });
221
+ console.log(chalk.dim(` Resumed on branch: ${runBranch}\n`));
222
+ }
223
+ catch {
224
+ console.log(chalk.yellow(` ⚠ Could not check out run branch ${runBranch} — wave merges may diverge\n`));
225
+ }
226
+ }
227
+ }
228
+ if (flex && mergeStrategy === "branch" && useWorktrees && !runBranch) {
213
229
  try {
214
230
  originalRef = execSync("git rev-parse --abbrev-ref HEAD", { cwd, encoding: "utf-8", stdio: "pipe" }).trim();
215
231
  if (originalRef === "HEAD")
@@ -237,21 +253,25 @@ export async function executeRun(cfg) {
237
253
  }
238
254
  catch { }
239
255
  }
240
- const buildRunState = (varying) => ({
241
- id: `run-${new Date().toISOString().slice(0, 19)}`, objective: objective ?? "", budget: cfg.budget,
242
- remaining, workerModel, plannerModel, fastModel,
243
- workerProviderId: cfg.workerProvider?.id, plannerProviderId: cfg.plannerProvider?.id,
256
+ const runStateBase = {
257
+ cwd,
258
+ id: `run-${new Date(cfg.runStartedAt).toISOString().slice(0, 19)}`,
259
+ startedAt: new Date(cfg.runStartedAt).toISOString(),
260
+ objective: objective ?? "",
261
+ budget: cfg.budget,
262
+ workerProviderId: cfg.workerProvider?.id,
263
+ plannerProviderId: cfg.plannerProvider?.id,
244
264
  fastProviderId: cfg.fastProvider?.id,
245
- concurrency,
246
- usageCap, allowExtraUsage: cfg.allowExtraUsage, extraUsageBudget: cfg.extraUsageBudget,
247
- flex, useWorktrees, mergeStrategy, waveNum,
248
- currentTasks: varying.currentTasks,
249
- accCost, accCompleted, accFailed, accIn, accOut, accTools,
250
- branches, phase: varying.phase, startedAt: new Date(cfg.runStartedAt).toISOString(), cwd,
265
+ allowExtraUsage: cfg.allowExtraUsage ?? false,
266
+ extraUsageBudget: cfg.extraUsageBudget,
267
+ flex, useWorktrees, mergeStrategy,
251
268
  repoFingerprint,
252
269
  coachedObjective: cfg.coachedObjective,
253
270
  coachedAt: cfg.coachedAt,
254
- });
271
+ runBranch,
272
+ originalRef,
273
+ };
274
+ const buildRunState = (varying) => composeRunState({ ...runStateBase, workerModel, plannerModel, fastModel, concurrency, usageCap }, { remaining: varying.remaining, waveNum, accCost, accCompleted, accFailed, accIn, accOut, accTools, branches }, { phase: varying.phase, currentTasks: varying.currentTasks });
255
275
  const gracefulStop = () => {
256
276
  if (stopping) {
257
277
  currentSwarm?.cleanup();
@@ -509,6 +529,7 @@ export async function executeRun(cfg) {
509
529
  rlGetter,
510
530
  isStopping: () => stopping,
511
531
  syncRunInfo,
532
+ buildRunState,
512
533
  renderSummary,
513
534
  runDebrief,
514
535
  recordBranches: (agents, mergeResults, currentWave) => {
@@ -1,4 +1,4 @@
1
- import type { Task, MergeStrategy, BranchRecord, WaveSummary, RLGetter } from "../core/types.js";
1
+ import type { Task, MergeStrategy, BranchRecord, WaveSummary, RLGetter, RunState } from "../core/types.js";
2
2
  import { Swarm } from "../swarm/swarm.js";
3
3
  import { RunDisplay } from "../ui/ui.js";
4
4
  import type { LiveConfig, SteeringContext } from "../ui/ui.js";
@@ -70,6 +70,15 @@ export interface WaveLoopCtx {
70
70
  ok: boolean;
71
71
  }[], currentWave?: number) => void;
72
72
  onLibrarianResult?: (promoted: number, patched: number, quarantined: number, rejected: number) => void;
73
+ /** Builds a full RunState snapshot. Provided by run.ts so cwd, budget, branches,
74
+ * provider ids, etc. are preserved — the wave loop used to rebuild a truncated
75
+ * state that omitted cwd, which made saved runs invisible to `findIncompleteRuns`
76
+ * (the cwd-equality filter dropped them). */
77
+ buildRunState: (varying: {
78
+ remaining: number;
79
+ phase: RunState["phase"];
80
+ currentTasks: Task[];
81
+ }) => RunState;
73
82
  }
74
83
  export interface WaveLoopResult {
75
84
  runAnotherRound: boolean;
@@ -59,7 +59,7 @@ export async function runWaveLoop(host, ctx) {
59
59
  if (host.currentTasks.length > host.remaining)
60
60
  host.currentTasks = host.currentTasks.slice(0, host.remaining);
61
61
  ctx.syncRunInfo();
62
- saveRunState(ctx.runDir, buildRunState(host, "steering", host.currentTasks));
62
+ saveRunState(ctx.runDir, ctx.buildRunState({ remaining: host.remaining, phase: "steering", currentTasks: host.currentTasks }));
63
63
  // ── Pre-wave rate limit gate ──
64
64
  await throttleBeforeWave(ctx.rlGetter, (text) => ctx.display.appendSteeringEvent(text), ctx.isStopping);
65
65
  if (ctx.isStopping())
@@ -181,7 +181,7 @@ export async function runWaveLoop(host, ctx) {
181
181
  // On user-initiated quit mid-wave, "never started" tasks are real leftover
182
182
  // work the user expects to see on resume — save them under "stopped".
183
183
  const midWavePhase = (ctx.isStopping() || swarm.aborted) ? "stopped" : "steering";
184
- saveRunState(ctx.runDir, buildRunState(host, midWavePhase, neverStarted));
184
+ saveRunState(ctx.runDir, ctx.buildRunState({ remaining: host.remaining, phase: midWavePhase, currentTasks: neverStarted }));
185
185
  // Preserve the leftover tasks on the host so resume / verifier see the
186
186
  // real pending queue (not the full original batch) after each wave.
187
187
  host.currentTasks = neverStarted;
@@ -235,7 +235,7 @@ export async function runWaveLoop(host, ctx) {
235
235
  if (circuitHalt) {
236
236
  ctx.display.appendSteeringEvent(`Circuit breaker: 2 consecutive waves produced no merged changes — halting to prevent budget drain`);
237
237
  ctx.display.stop();
238
- saveRunState(ctx.runDir, buildRunState(host, "stopped", []));
238
+ saveRunState(ctx.runDir, ctx.buildRunState({ remaining: host.remaining, phase: "stopped", currentTasks: [] }));
239
239
  ctx.display.stop();
240
240
  console.log(chalk.red(`\n Circuit breaker: 2 consecutive waves produced no merged changes.`));
241
241
  console.log(chalk.red(` Halting to prevent budget drain. Run preserved at ${ctx.runDir}.`));
@@ -521,16 +521,6 @@ function handleZeroWorkRetry(swarm, host, ctx) {
521
521
  swarm.totalOutputTokens += retrySwarm.totalOutputTokens;
522
522
  host.liveConfig.remaining = host.remaining;
523
523
  }
524
- function buildRunState(host, phase, currentTasks) {
525
- return {
526
- remaining: host.remaining, phase, currentTasks,
527
- workerModel: host.workerModel, plannerModel: host.plannerModel, fastModel: host.fastModel,
528
- concurrency: host.concurrency,
529
- usageCap: host.usageCap, flex: true, waveNum: host.waveNum,
530
- accCost: host.accCost, accCompleted: host.accCompleted, accFailed: host.accFailed,
531
- accIn: host.accIn, accOut: host.accOut, accTools: host.accTools,
532
- };
533
- }
534
524
  function captureAbOutcome(swarm, assignment, host, ctx) {
535
525
  const treatmentAgents = swarm.agents.filter(a => assignment.treatmentTaskIds.includes(a.task.id));
536
526
  const controlAgents = swarm.agents.filter(a => assignment.controlTaskIds.includes(a.task.id));
@@ -0,0 +1,45 @@
1
+ import type { RunState, Task, BranchRecord } from "../core/types.js";
2
+ /** Static inputs that don't change between RunState snapshots within a single run. */
3
+ export interface RunStateBase {
4
+ cwd: string;
5
+ id: string;
6
+ startedAt: string;
7
+ objective: string;
8
+ budget: number;
9
+ workerModel: string;
10
+ plannerModel: string;
11
+ fastModel: string | undefined;
12
+ workerProviderId?: string;
13
+ plannerProviderId?: string;
14
+ fastProviderId?: string;
15
+ concurrency: number;
16
+ usageCap: number | undefined;
17
+ allowExtraUsage: boolean;
18
+ extraUsageBudget?: number;
19
+ flex: boolean;
20
+ useWorktrees: boolean;
21
+ mergeStrategy: RunState["mergeStrategy"];
22
+ repoFingerprint: string;
23
+ coachedObjective?: string;
24
+ coachedAt?: number;
25
+ runBranch?: string;
26
+ originalRef?: string;
27
+ }
28
+ /** Live counters captured at snapshot time. */
29
+ export interface RunStateLive {
30
+ remaining: number;
31
+ waveNum: number;
32
+ accCost: number;
33
+ accCompleted: number;
34
+ accFailed: number;
35
+ accIn: number;
36
+ accOut: number;
37
+ accTools: number;
38
+ branches: BranchRecord[];
39
+ }
40
+ /** Variable-per-snapshot inputs: phase and the task slice for resume. */
41
+ export interface RunStateVarying {
42
+ phase: RunState["phase"];
43
+ currentTasks: Task[];
44
+ }
45
+ export declare function composeRunState(base: RunStateBase, live: RunStateLive, varying: RunStateVarying): RunState;
@@ -0,0 +1,32 @@
1
+ // Single source of truth for constructing a RunState snapshot for persistence.
2
+ //
3
+ // Two writers used to exist (run.ts and wave-loop.ts) and one drifted —
4
+ // silently omitting cwd, which made saved runs invisible to findIncompleteRuns.
5
+ // Now both call this. Adding a field to RunState forces an edit here.
6
+ //
7
+ // `saveRunState` enforces required fields at the write boundary; this module
8
+ // enforces them at the call boundary.
9
+ export function composeRunState(base, live, varying) {
10
+ return {
11
+ id: base.id, objective: base.objective, budget: base.budget,
12
+ remaining: live.remaining,
13
+ workerModel: base.workerModel, plannerModel: base.plannerModel, fastModel: base.fastModel,
14
+ workerProviderId: base.workerProviderId, plannerProviderId: base.plannerProviderId,
15
+ fastProviderId: base.fastProviderId,
16
+ concurrency: base.concurrency,
17
+ usageCap: base.usageCap, allowExtraUsage: base.allowExtraUsage, extraUsageBudget: base.extraUsageBudget,
18
+ flex: base.flex, useWorktrees: base.useWorktrees, mergeStrategy: base.mergeStrategy,
19
+ waveNum: live.waveNum,
20
+ currentTasks: varying.currentTasks,
21
+ accCost: live.accCost, accCompleted: live.accCompleted, accFailed: live.accFailed,
22
+ accIn: live.accIn, accOut: live.accOut, accTools: live.accTools,
23
+ branches: live.branches,
24
+ phase: varying.phase,
25
+ startedAt: base.startedAt, cwd: base.cwd,
26
+ repoFingerprint: base.repoFingerprint,
27
+ coachedObjective: base.coachedObjective,
28
+ coachedAt: base.coachedAt,
29
+ runBranch: base.runBranch,
30
+ originalRef: base.originalRef,
31
+ };
32
+ }
@@ -180,13 +180,31 @@ export function updateOvernightLogEnd(cwd, runId, meta) {
180
180
  }
181
181
  }
182
182
  // ── Run state persistence ──
183
+ /**
184
+ * Required fields on every persisted RunState. The type already marks these as
185
+ * non-optional, but callers that build state dynamically (or upcast through
186
+ * `any`) can still slip a truncated snapshot past the compiler. A truncated
187
+ * snapshot is silently excluded by `findIncompleteRuns` (cwd-equality filter),
188
+ * so the run becomes unresumable without any visible error. Guard at the write
189
+ * boundary so the bug surfaces where it's introduced, not weeks later.
190
+ */
191
+ const REQUIRED_RUN_STATE_FIELDS = ["cwd", "id", "phase", "startedAt"];
183
192
  export function saveRunState(runDir, state) {
193
+ const missing = REQUIRED_RUN_STATE_FIELDS.filter(k => !state[k]);
194
+ if (missing.length) {
195
+ throw new Error(`saveRunState: refusing to persist truncated state, missing fields: ${missing.join(", ")}`);
196
+ }
184
197
  mkdirSync(runDir, { recursive: true });
185
198
  writeFileSync(join(runDir, "run.json"), JSON.stringify(state, null, 2), "utf-8");
186
199
  }
187
200
  export function loadRunState(runDir) {
188
201
  try {
189
- return JSON.parse(readFileSync(join(runDir, "run.json"), "utf-8"));
202
+ const state = JSON.parse(readFileSync(join(runDir, "run.json"), "utf-8"));
203
+ if (state && !Array.isArray(state.branches))
204
+ state.branches = [];
205
+ if (state && !Array.isArray(state.currentTasks))
206
+ state.currentTasks = [];
207
+ return state;
190
208
  }
191
209
  catch {
192
210
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-overnight",
3
- "version": "1.50.1",
3
+ "version": "1.50.5",
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.1",
3
+ "version": "1.50.5",
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"