claude-overnight 1.59.0 → 1.60.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,3 @@
1
+ export declare function runDiff(runIdA: string | undefined, runIdB: string | undefined): Promise<void>;
2
+ export declare function runDownload(runIdArg?: string, ...rest: string[]): Promise<void>;
3
+ export declare function runPromote(runIdArg?: string, ...rest: string[]): Promise<void>;
@@ -0,0 +1,234 @@
1
+ export async function runDiff(runIdA, runIdB) {
2
+ if (!runIdA || !runIdB) {
3
+ console.error("usage: claude-overnight-evolve diff <runIdA> <runIdB>");
4
+ process.exit(2);
5
+ }
6
+ const { loadRun } = await import("../prompt-evolution/persistence.js");
7
+ const a = loadRun(runIdA);
8
+ const b = loadRun(runIdB);
9
+ const collect = (run) => {
10
+ const out = new Map();
11
+ for (const rec of run.matrix) {
12
+ // Keep the latest-generation row per variantId so diff compares final state.
13
+ const existing = out.get(rec.variantId);
14
+ if (!existing || rec.generation > existing.generation) {
15
+ out.set(rec.variantId, { generation: rec.generation, variantId: rec.variantId, gmean: rec.gmean });
16
+ }
17
+ }
18
+ return out;
19
+ };
20
+ const rowsA = collect(a);
21
+ const rowsB = collect(b);
22
+ const ids = new Set([...rowsA.keys(), ...rowsB.keys()]);
23
+ console.log(`# Diff: ${runIdA} → ${runIdB}`);
24
+ console.log("");
25
+ console.log(`| Variant | A gmean | B gmean | Δ | note |`);
26
+ console.log(`|-----------|-----------|-----------|-------|--------|`);
27
+ const sorted = [...ids].sort();
28
+ for (const id of sorted) {
29
+ const ra = rowsA.get(id);
30
+ const rb = rowsB.get(id);
31
+ const ga = ra ? (ra.gmean * 100).toFixed(1) : "—";
32
+ const gb = rb ? (rb.gmean * 100).toFixed(1) : "—";
33
+ const delta = ra && rb ? ((rb.gmean - ra.gmean) * 100).toFixed(1) : "—";
34
+ const note = !ra ? "new in B" : !rb ? "missing in B" : ra.gmean < rb.gmean ? "↑" : ra.gmean > rb.gmean ? "↓" : "=";
35
+ console.log(`| ${id.padEnd(10)}| ${ga.padStart(9)} | ${gb.padStart(9)} | ${delta.padStart(5)} | ${note} |`);
36
+ }
37
+ }
38
+ export async function runDownload(runIdArg, ...rest) {
39
+ if (!runIdArg) {
40
+ console.error("usage: claude-overnight-evolve download <runId> --base-url <url> [--token <token>] [--project <id>]");
41
+ process.exit(2);
42
+ }
43
+ const runId = runIdArg;
44
+ let baseUrl;
45
+ let token;
46
+ let projectId;
47
+ for (let i = 0; i < rest.length; i++) {
48
+ if (rest[i] === "--base-url" && rest[i + 1]) {
49
+ baseUrl = rest[i + 1];
50
+ i++;
51
+ }
52
+ else if (rest[i] === "--token" && rest[i + 1]) {
53
+ token = rest[i + 1];
54
+ i++;
55
+ }
56
+ else if (rest[i] === "--project" && rest[i + 1]) {
57
+ projectId = rest[i + 1];
58
+ i++;
59
+ }
60
+ }
61
+ if (!baseUrl) {
62
+ console.error("--base-url is required (e.g. https://fornace.net or http://localhost:8787)");
63
+ process.exit(2);
64
+ }
65
+ const authHeaders = {};
66
+ if (token)
67
+ authHeaders.Authorization = `Bearer ${token}`;
68
+ const prefix = projectId
69
+ ? `${baseUrl.replace(/\/$/, "")}/api/projects/${projectId}/prompt-evolution/${runId}`
70
+ : `${baseUrl.replace(/\/$/, "")}/runs/${runId}`;
71
+ const metaRes = await fetch(prefix, { headers: authHeaders });
72
+ if (!metaRes.ok) {
73
+ console.error(`Failed to fetch run metadata: HTTP ${metaRes.status}`);
74
+ process.exit(1);
75
+ }
76
+ const metaBody = (await metaRes.json());
77
+ const remoteMeta = typeof metaBody.meta === "object" && metaBody.meta
78
+ ? metaBody.meta
79
+ : metaBody;
80
+ const { runDir } = await import("../prompt-evolution/persistence.js");
81
+ const { mkdirSync, writeFileSync } = await import("node:fs");
82
+ const { dirname, join } = await import("node:path");
83
+ const localDir = runDir(runId);
84
+ mkdirSync(localDir, { recursive: true });
85
+ mkdirSync(join(localDir, "prompts"), { recursive: true });
86
+ const meta = {
87
+ runId,
88
+ promptPath: (remoteMeta.promptPath ?? remoteMeta.prompt ?? ""),
89
+ target: (remoteMeta.target ?? "claude-overnight"),
90
+ evalModel: (remoteMeta.evalModel ?? ""),
91
+ mutateModel: (remoteMeta.mutateModel ?? remoteMeta.evalModel ?? ""),
92
+ generations: (remoteMeta.generations ?? 10),
93
+ populationCap: (remoteMeta.populationCap ?? remoteMeta.population ?? 8),
94
+ startedAt: (remoteMeta.startedAt ?? remoteMeta.queuedAt ?? new Date().toISOString()),
95
+ status: (remoteMeta.status ?? "done"),
96
+ caseNames: [],
97
+ };
98
+ writeFileSync(join(localDir, "meta.json"), JSON.stringify(meta, null, 2) + "\n");
99
+ const inlineReport = typeof metaBody.report === "string" ? metaBody.report : metaBody.report_md;
100
+ if (typeof inlineReport === "string") {
101
+ writeFileSync(join(localDir, "report.md"), inlineReport);
102
+ console.log(" ✓ report.md (inline)");
103
+ }
104
+ const listRes = await fetch(`${prefix}/files`, { headers: authHeaders });
105
+ let files = [];
106
+ if (listRes.ok) {
107
+ const listBody = (await listRes.json());
108
+ files = listBody.files ?? [];
109
+ }
110
+ else {
111
+ console.log(` ⚠ File listing not available (HTTP ${listRes.status}); trying known files...`);
112
+ files = ["report.md", "best.md", "matrix.jsonl", "learning.jsonl"];
113
+ }
114
+ for (const file of files) {
115
+ const fileRes = await fetch(`${prefix}/files/${encodeURIComponent(file)}`, { headers: authHeaders });
116
+ if (!fileRes.ok) {
117
+ console.error(` ⚠ ${file}: HTTP ${fileRes.status}`);
118
+ continue;
119
+ }
120
+ const data = Buffer.from(await fileRes.arrayBuffer());
121
+ const localPath = join(localDir, file);
122
+ mkdirSync(dirname(localPath), { recursive: true });
123
+ writeFileSync(localPath, data);
124
+ console.log(` ✓ ${file}`);
125
+ }
126
+ const matrixPath = join(localDir, "matrix.jsonl");
127
+ const { existsSync, readFileSync } = await import("node:fs");
128
+ if (existsSync(matrixPath)) {
129
+ const variantIds = new Set();
130
+ for (const line of readFileSync(matrixPath, "utf-8").trim().split("\n")) {
131
+ if (!line)
132
+ continue;
133
+ try {
134
+ const row = JSON.parse(line);
135
+ if (row.variantId)
136
+ variantIds.add(row.variantId);
137
+ }
138
+ catch { /* ignore */ }
139
+ }
140
+ for (const vid of variantIds) {
141
+ const safeId = vid.replace(/[^a-zA-Z0-9_-]/g, "_");
142
+ const promptFile = `prompts/${safeId}.md`;
143
+ if (existsSync(join(localDir, promptFile)))
144
+ continue;
145
+ const fileRes = await fetch(`${prefix}/files/${encodeURIComponent(promptFile)}`, { headers: authHeaders });
146
+ if (!fileRes.ok)
147
+ continue;
148
+ const data = Buffer.from(await fileRes.arrayBuffer());
149
+ mkdirSync(dirname(join(localDir, promptFile)), { recursive: true });
150
+ writeFileSync(join(localDir, promptFile), data);
151
+ console.log(` ✓ ${promptFile} (from matrix)`);
152
+ }
153
+ }
154
+ console.log(`\nDownloaded to ${localDir}`);
155
+ }
156
+ export async function runPromote(runIdArg, ...rest) {
157
+ if (!runIdArg) {
158
+ console.error("usage: claude-overnight-evolve promote <runId> [--variant <id>] [--into <block>]");
159
+ process.exit(2);
160
+ }
161
+ const runId = runIdArg;
162
+ let variantId;
163
+ let intoBlock;
164
+ for (let i = 0; i < rest.length; i++) {
165
+ if (rest[i] === "--variant" && rest[i + 1]) {
166
+ variantId = rest[i + 1];
167
+ i++;
168
+ }
169
+ else if (rest[i] === "--into" && rest[i + 1]) {
170
+ intoBlock = rest[i + 1];
171
+ i++;
172
+ }
173
+ }
174
+ const { loadRun, runDir } = await import("../prompt-evolution/persistence.js");
175
+ const { PROMPTS_ROOT } = await import("../prompts/load.js");
176
+ const { readFileSync, writeFileSync, existsSync } = await import("node:fs");
177
+ const { join } = await import("node:path");
178
+ const run = loadRun(runId);
179
+ const promptPath = run.meta.promptPath;
180
+ let sourceVariant = variantId;
181
+ if (!sourceVariant) {
182
+ const bestMatch = run.bestMd.match(/variantId\s*\|\s*`([^`]+)`/);
183
+ sourceVariant = bestMatch ? bestMatch[1] : undefined;
184
+ if (!sourceVariant) {
185
+ const rows = run.matrix;
186
+ if (rows.length)
187
+ sourceVariant = [...rows].sort((a, b) => b.gmean - a.gmean)[0].variantId;
188
+ }
189
+ }
190
+ if (!sourceVariant) {
191
+ console.error("Could not determine best variant for run. Use --variant <id>.");
192
+ process.exit(2);
193
+ }
194
+ const safeId = sourceVariant.replace(/[^a-zA-Z0-9_-]/g, "_");
195
+ const variantFile = join(runDir(runId), "prompts", `${safeId}.md`);
196
+ if (!existsSync(variantFile)) {
197
+ console.error(`Variant file not found: ${variantFile}`);
198
+ process.exit(2);
199
+ }
200
+ const variantText = readFileSync(variantFile, "utf-8").replace(/^<!--\s*generation=[\s\S]*?-->\n\n?/, "");
201
+ const namedVariants = ["tight", "standard", "large", "wrap", "amend", "wave", "run", "file", "all", "postfailed", "nofiles"];
202
+ const targetBlock = intoBlock ?? (namedVariants.includes(sourceVariant.toLowerCase()) ? sourceVariant : undefined);
203
+ if (!targetBlock) {
204
+ console.error(`Variant "${sourceVariant}" is not a named seed variant. Use --into <block> to specify which marker block to overwrite.`);
205
+ process.exit(2);
206
+ }
207
+ const promptFile = join(PROMPTS_ROOT, promptPath + ".md");
208
+ if (!existsSync(promptFile)) {
209
+ console.error(`Prompt file not found: ${promptFile}`);
210
+ process.exit(2);
211
+ }
212
+ const newText = replaceVariantBlock(readFileSync(promptFile, "utf-8"), targetBlock, variantText);
213
+ writeFileSync(promptFile, newText);
214
+ console.log(`Promoted ${sourceVariant} → ${promptPath} (<!-- ${targetBlock.toUpperCase()} -->)`);
215
+ console.log(` file: ${promptFile}`);
216
+ }
217
+ function replaceVariantBlock(fileText, blockName, newText) {
218
+ const separator = "\n<!-- @@@ -->\n";
219
+ const sections = fileText.split(separator);
220
+ const markerRegex = new RegExp(`<!--\\s*(?:[─\\-]+\\s*)?${blockName.toUpperCase()}\\s*-->`, "i");
221
+ let found = false;
222
+ const newSections = sections.map((section) => {
223
+ const lines = section.split("\n");
224
+ const markerIndex = lines.findIndex((line) => markerRegex.test(line));
225
+ if (markerIndex === -1)
226
+ return section;
227
+ found = true;
228
+ const before = lines.slice(0, markerIndex + 1);
229
+ return [...before, "", newText.trim(), ""].join("\n").trimEnd() + "\n";
230
+ });
231
+ if (!found)
232
+ throw new Error(`Variant block "${blockName.toUpperCase()}" not found in prompt file`);
233
+ return newSections.join(separator);
234
+ }
@@ -19,6 +19,7 @@ import { evolvePrompt } from "../prompt-evolution/index.js";
19
19
  import { PLAN_CASES } from "../prompt-evolution/fixtures/plan-cases.js";
20
20
  import { harvestRealCases } from "../prompt-evolution/fixtures/harvest.js";
21
21
  import { generateCases } from "../prompt-evolution/fixtures/generate.js";
22
+ import { runDiff, runDownload, runPromote } from "./evolve-subcommands.js";
22
23
  import { scenariosToCases, PLANNING_SCENARIOS, REVIEW_SCENARIOS, SUPERVISION_SCENARIOS, STUCK_SCENARIOS, hydrateCases, extractPrompt, } from "../prompt-evolution/adapters/mcp-browser.js";
23
24
  function help() {
24
25
  process.stdout.write(`Usage: claude-overnight-evolve [options]
@@ -57,6 +58,18 @@ Options:
57
58
  --gen-model <model> Model used by the case generator (default: eval-model)
58
59
 
59
60
  Subcommands:
61
+ claude-overnight-evolve download <runId> --base-url <url> [--token <token>]
62
+ [--project <id>]
63
+ Pull a remote run (fornace or self-host) into the local
64
+ ~/.claude-overnight/prompt-evolution/<runId>/ directory
65
+ so you can audit, diff, or promote it offline. Use
66
+ --project for fornace; omit for self-host.
67
+ claude-overnight-evolve promote <runId> [--variant <id>] [--into <block>]
68
+ Write a run's winning variant back into the source
69
+ prompt file's <!-- BLOCK --> marker. If --variant is
70
+ omitted, uses the run's best variant. If the variant is
71
+ a seed (tight/standard/large) --into defaults to its
72
+ name; evo-* or default variants require --into.
60
73
  claude-overnight-evolve diff <runIdA> <runIdB>
61
74
  Print a per-variant diff of two persisted runs
62
75
  --base-url <url> API base URL override
@@ -208,6 +221,16 @@ function parseArgs() {
208
221
  return opts;
209
222
  }
210
223
  async function main() {
224
+ // Subcommand: download a remote run for local audit/promote.
225
+ if (process.argv[2] === "download") {
226
+ await runDownload(process.argv[3], ...process.argv.slice(4));
227
+ return;
228
+ }
229
+ // Subcommand: promote a run variant back into the source prompt file.
230
+ if (process.argv[2] === "promote") {
231
+ await runPromote(process.argv[3], ...process.argv.slice(4));
232
+ return;
233
+ }
211
234
  // Subcommand: diff two persisted runs.
212
235
  if (process.argv[2] === "diff") {
213
236
  await runDiff(process.argv[3], process.argv[4]);
@@ -357,43 +380,6 @@ async function evolveOne(opts) {
357
380
  console.log(result.bestVariant.text);
358
381
  return result;
359
382
  }
360
- async function runDiff(runIdA, runIdB) {
361
- if (!runIdA || !runIdB) {
362
- console.error("usage: claude-overnight-evolve diff <runIdA> <runIdB>");
363
- process.exit(2);
364
- }
365
- const { loadRun } = await import("../prompt-evolution/persistence.js");
366
- const a = loadRun(runIdA);
367
- const b = loadRun(runIdB);
368
- const collect = (run) => {
369
- const out = new Map();
370
- for (const rec of run.matrix) {
371
- // Keep the latest-generation row per variantId so diff compares final state.
372
- const existing = out.get(rec.variantId);
373
- if (!existing || rec.generation > existing.generation) {
374
- out.set(rec.variantId, { generation: rec.generation, variantId: rec.variantId, gmean: rec.gmean });
375
- }
376
- }
377
- return out;
378
- };
379
- const rowsA = collect(a);
380
- const rowsB = collect(b);
381
- const ids = new Set([...rowsA.keys(), ...rowsB.keys()]);
382
- console.log(`# Diff: ${runIdA} → ${runIdB}`);
383
- console.log("");
384
- console.log(`| Variant | A gmean | B gmean | Δ | note |`);
385
- console.log(`|-----------|-----------|-----------|-------|--------|`);
386
- const sorted = [...ids].sort();
387
- for (const id of sorted) {
388
- const ra = rowsA.get(id);
389
- const rb = rowsB.get(id);
390
- const ga = ra ? (ra.gmean * 100).toFixed(1) : "—";
391
- const gb = rb ? (rb.gmean * 100).toFixed(1) : "—";
392
- const delta = ra && rb ? ((rb.gmean - ra.gmean) * 100).toFixed(1) : "—";
393
- const note = !ra ? "new in B" : !rb ? "missing in B" : ra.gmean < rb.gmean ? "↑" : ra.gmean > rb.gmean ? "↓" : "=";
394
- console.log(`| ${id.padEnd(10)}| ${ga.padStart(9)} | ${gb.padStart(9)} | ${delta.padStart(5)} | ${note} |`);
395
- }
396
- }
397
383
  main().catch((err) => {
398
384
  console.error(err);
399
385
  process.exit(1);
@@ -1 +1 @@
1
- export declare const VERSION = "1.59.0";
1
+ export declare const VERSION = "1.60.0";
@@ -1,2 +1,2 @@
1
1
  // Auto-generated by build — do not edit manually.
2
- export const VERSION = "1.59.0";
2
+ export const VERSION = "1.60.0";
@@ -1,3 +1,4 @@
1
+ export declare const PROMPTS_ROOT: string;
1
2
  export type PromptVars = Record<string, string | number | boolean | undefined | null>;
2
3
  export interface RenderOpts {
3
4
  variant?: string;
@@ -2,7 +2,7 @@ import { readFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  // Resolve <pkg>/prompts whether running from dist/ (installed) or src/ (dev).
5
- const PROMPTS_ROOT = (() => {
5
+ export const PROMPTS_ROOT = (() => {
6
6
  const here = dirname(fileURLToPath(import.meta.url));
7
7
  for (const depth of [2, 3, 4]) {
8
8
  const candidate = join(here, ...Array(depth).fill(".."), "prompts");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-overnight",
3
- "version": "1.59.0",
3
+ "version": "1.60.0",
4
4
  "description": "Overnight parallel coding agents in git worktrees, with a self-curating skill memory that improves while the run is going. Mix Claude Opus as planner, Kimi 2.6 or Cursor composer-2 as cheap fast worker, Gemini or Qwen for bulk implementation. Multi-wave autonomous loop that plans, executes, reviews, and steers itself until the objective is met. Crash-safe resume, rate-limit aware, usage cap preserves headroom for your interactive Claude Code.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-overnight",
3
- "version": "1.59.0",
3
+ "version": "1.60.0",
4
4
  "description": "Claude Code skill for understanding, installing, and inspecting claude-overnight runs: overnight parallel coding agents in git worktrees with a self-curating skill memory, multi-wave steering, three-layer review, and crash-safe resume. Mix Opus planner with Kimi 2.6, Cursor composer-2, Gemini, Qwen, or any Anthropic-compatible worker.",
5
5
  "author": {
6
6
  "name": "Francesco Fornace"