claude-overnight 1.60.2 → 1.60.3

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.
Files changed (88) hide show
  1. package/dist/cli/argv.d.ts +7 -0
  2. package/dist/cli/argv.js +47 -0
  3. package/dist/cli/cli.d.ts +6 -61
  4. package/dist/cli/cli.js +12 -415
  5. package/dist/cli/display.d.ts +17 -0
  6. package/dist/cli/display.js +44 -0
  7. package/dist/cli/files.d.ts +22 -0
  8. package/dist/cli/files.js +94 -0
  9. package/dist/cli/help.js +4 -4
  10. package/dist/cli/plan-phase.js +16 -17
  11. package/dist/cli/prompts.d.ts +16 -0
  12. package/dist/cli/prompts.js +226 -0
  13. package/dist/cli/resume.js +25 -56
  14. package/dist/cli/run-paths.d.ts +4 -0
  15. package/dist/cli/run-paths.js +8 -0
  16. package/dist/cli/settings.d.ts +7 -0
  17. package/dist/cli/settings.js +22 -6
  18. package/dist/core/_version.d.ts +1 -1
  19. package/dist/core/_version.js +1 -1
  20. package/dist/core/errors.d.ts +6 -0
  21. package/dist/core/errors.js +23 -0
  22. package/dist/core/fs-helpers.d.ts +19 -0
  23. package/dist/core/fs-helpers.js +52 -0
  24. package/dist/core/key-vault.js +8 -12
  25. package/dist/core/proxy-port.js +6 -14
  26. package/dist/index.js +3 -6
  27. package/dist/planner/coach/coach.js +4 -6
  28. package/dist/planner/coach/context.js +13 -19
  29. package/dist/planner/coach/settings.js +5 -11
  30. package/dist/planner/planner.js +40 -72
  31. package/dist/planner/query-direct.d.ts +4 -0
  32. package/dist/planner/query-direct.js +52 -0
  33. package/dist/planner/query-stream.d.ts +4 -0
  34. package/dist/planner/query-stream.js +385 -0
  35. package/dist/planner/query.js +19 -435
  36. package/dist/planner/sdk-events.d.ts +55 -0
  37. package/dist/planner/sdk-events.js +10 -0
  38. package/dist/prompt-evolution/fixtures/generate.js +9 -17
  39. package/dist/prompt-evolution/fixtures/harvest.js +15 -21
  40. package/dist/prompt-evolution/llm-judge.js +5 -22
  41. package/dist/prompt-evolution/mutator.js +0 -1
  42. package/dist/prompt-evolution/persistence.js +38 -48
  43. package/dist/prompts/load.js +18 -22
  44. package/dist/providers/cursor/env.d.ts +53 -0
  45. package/dist/providers/cursor/env.js +267 -0
  46. package/dist/providers/cursor/index.d.ts +4 -0
  47. package/dist/providers/cursor/index.js +5 -0
  48. package/dist/providers/cursor/picker.d.ts +7 -0
  49. package/dist/providers/cursor/picker.js +298 -0
  50. package/dist/providers/cursor/proxy.d.ts +26 -0
  51. package/dist/providers/cursor/proxy.js +392 -0
  52. package/dist/providers/index.d.ts +5 -27
  53. package/dist/providers/index.js +31 -79
  54. package/dist/providers/store.d.ts +24 -0
  55. package/dist/providers/store.js +33 -0
  56. package/dist/run/health.js +10 -12
  57. package/dist/run/run.js +170 -223
  58. package/dist/run/summary.js +14 -16
  59. package/dist/run/wave-loop.d.ts +19 -37
  60. package/dist/run/wave-loop.js +207 -222
  61. package/dist/skills/injection.js +26 -41
  62. package/dist/skills/librarian.js +36 -55
  63. package/dist/state/state.d.ts +1 -0
  64. package/dist/state/state.js +154 -253
  65. package/dist/swarm/agent-run.js +117 -103
  66. package/dist/swarm/errors.js +9 -13
  67. package/dist/swarm/merge-autocommit.js +16 -22
  68. package/dist/swarm/merge-helpers.d.ts +10 -0
  69. package/dist/swarm/merge-helpers.js +44 -69
  70. package/dist/swarm/merge.d.ts +2 -2
  71. package/dist/swarm/merge.js +25 -58
  72. package/dist/swarm/message-handler.js +10 -13
  73. package/dist/swarm/swarm.d.ts +27 -26
  74. package/dist/swarm/swarm.js +48 -40
  75. package/dist/ui/footer.js +1 -2
  76. package/dist/ui/header.js +1 -2
  77. package/dist/ui/input.js +67 -68
  78. package/dist/ui/overlay.js +1 -2
  79. package/dist/ui/primitives.d.ts +3 -0
  80. package/dist/ui/primitives.js +5 -0
  81. package/dist/ui/run-body.js +32 -32
  82. package/dist/ui/settings.d.ts +1 -1
  83. package/dist/ui/steering-body.js +1 -2
  84. package/dist/ui/summary.js +2 -2
  85. package/dist/ui/ui.d.ts +1 -0
  86. package/dist/ui/ui.js +18 -28
  87. package/package.json +1 -1
  88. package/plugins/claude-overnight/.claude-plugin/plugin.json +1 -1
@@ -0,0 +1,94 @@
1
+ // Task-file / plan-file loading. Pure: throws on bad input, no stdio.
2
+ import { readFileSync } from "fs";
3
+ import { resolve } from "path";
4
+ import { validateConcurrency } from "./argv.js";
5
+ const KNOWN_TASK_FILE_KEYS = new Set([
6
+ "tasks", "objective", "concurrency", "cwd", "model", "allowedTools",
7
+ "beforeWave", "afterWave", "afterRun", "worktrees", "mergeStrategy",
8
+ "usageCap", "flexiblePlan",
9
+ ]);
10
+ /** Load a markdown plan file. Extracts the first H1 as objective and returns the full body as planContent. */
11
+ export function loadPlanFile(file) {
12
+ const path = resolve(file);
13
+ let raw;
14
+ try {
15
+ raw = readFileSync(path, "utf-8");
16
+ }
17
+ catch {
18
+ throw new Error(`Cannot read plan file: ${path}`);
19
+ }
20
+ const body = raw.trim();
21
+ if (!body)
22
+ throw new Error(`Plan file is empty: ${path}`);
23
+ const h1 = body.match(/^#\s+(.+)$/m);
24
+ const objective = (h1?.[1] ?? body.split("\n").find(l => l.trim())).trim();
25
+ return { objective, planContent: body };
26
+ }
27
+ export function loadTaskFile(file) {
28
+ const path = resolve(file);
29
+ let raw;
30
+ try {
31
+ raw = readFileSync(path, "utf-8");
32
+ }
33
+ catch {
34
+ throw new Error(`Cannot read task file: ${path}`);
35
+ }
36
+ let json;
37
+ try {
38
+ json = JSON.parse(raw);
39
+ }
40
+ catch {
41
+ throw new Error(`Task file is not valid JSON: ${path}`);
42
+ }
43
+ const parsed = Array.isArray(json) ? { tasks: json } : json;
44
+ if (!Array.isArray(json) && typeof json === "object" && json !== null) {
45
+ const unknown = Object.keys(json).filter((k) => !KNOWN_TASK_FILE_KEYS.has(k));
46
+ if (unknown.length > 0) {
47
+ throw new Error(`Unknown key${unknown.length > 1 ? "s" : ""} in task file: ${unknown.join(", ")}. Allowed: ${[...KNOWN_TASK_FILE_KEYS].join(", ")}`);
48
+ }
49
+ }
50
+ if (!Array.isArray(parsed.tasks))
51
+ throw new Error(`Task file must contain a "tasks" array (got ${typeof parsed.tasks})`);
52
+ const tasks = [];
53
+ for (let i = 0; i < parsed.tasks.length; i++) {
54
+ const t = parsed.tasks[i];
55
+ const id = String(tasks.length);
56
+ if (typeof t === "string") {
57
+ if (!t.trim())
58
+ throw new Error(`Task ${i} is an empty string`);
59
+ tasks.push({ id, prompt: t });
60
+ }
61
+ else if (typeof t === "object" && t !== null) {
62
+ if (typeof t.prompt !== "string" || !t.prompt.trim())
63
+ throw new Error(`Task ${i} is missing a "prompt" string`);
64
+ tasks.push({ id, prompt: t.prompt, cwd: t.cwd ? resolve(t.cwd) : undefined, model: t.model });
65
+ }
66
+ else {
67
+ throw new Error(`Task ${i} must be a string or object with a "prompt" field (got ${typeof t})`);
68
+ }
69
+ }
70
+ if (parsed.concurrency !== undefined)
71
+ validateConcurrency(parsed.concurrency);
72
+ const usageCap = parsed.usageCap;
73
+ if (usageCap != null && (typeof usageCap !== "number" || usageCap < 0 || usageCap > 100)) {
74
+ throw new Error(`usageCap must be a number between 0 and 100 (got ${JSON.stringify(usageCap)})`);
75
+ }
76
+ if (parsed.flexiblePlan && typeof parsed.objective !== "string") {
77
+ throw new Error(`flexiblePlan requires an "objective" string in the task file`);
78
+ }
79
+ return {
80
+ tasks,
81
+ objective: typeof parsed.objective === "string" ? parsed.objective : undefined,
82
+ concurrency: parsed.concurrency,
83
+ model: parsed.model,
84
+ cwd: parsed.cwd ? resolve(parsed.cwd) : undefined,
85
+ allowedTools: parsed.allowedTools,
86
+ beforeWave: parsed.beforeWave,
87
+ afterWave: parsed.afterWave,
88
+ afterRun: parsed.afterRun,
89
+ useWorktrees: parsed.worktrees,
90
+ mergeStrategy: parsed.mergeStrategy,
91
+ usageCap,
92
+ flexiblePlan: parsed.flexiblePlan,
93
+ };
94
+ }
package/dist/cli/help.js CHANGED
@@ -1,12 +1,12 @@
1
- import { readFileSync } from "fs";
2
1
  import { dirname, join } from "path";
3
2
  import { fileURLToPath } from "url";
4
3
  import chalk from "chalk";
5
4
  import { VERSION } from "../core/_version.js";
5
+ import { readJsonOrNull } from "../core/fs-helpers.js";
6
6
  export function printVersion() {
7
- const __dirname = dirname(fileURLToPath(import.meta.url));
8
- const pkg = JSON.parse(readFileSync(join(__dirname, "..", "..", "package.json"), "utf-8"));
9
- console.log(`claude-overnight v${pkg.version}`);
7
+ const here = dirname(fileURLToPath(import.meta.url));
8
+ const pkg = readJsonOrNull(join(here, "..", "..", "package.json"));
9
+ console.log(`claude-overnight v${pkg?.version ?? VERSION}`);
10
10
  }
11
11
  export function printHelp() {
12
12
  console.log(`
@@ -1,5 +1,5 @@
1
- import { readFileSync, readdirSync, mkdirSync, writeFileSync } from "fs";
2
- import { join } from "path";
1
+ import { mkdirSync, writeFileSync } from "fs";
2
+ import { readMdEntries } from "../core/fs-helpers.js";
3
3
  import chalk from "chalk";
4
4
  import { query } from "@anthropic-ai/claude-agent-sdk";
5
5
  import { Swarm } from "../swarm/swarm.js";
@@ -9,7 +9,10 @@ import { renderSummary } from "../ui/summary.js";
9
9
  import { isCursorProxyProvider } from "../providers/index.js";
10
10
  import { readMdDir, saveRunState } from "../state/state.js";
11
11
  import { computeRepoFingerprint } from "../skills/scribe.js";
12
- import { selectKey, ask, showPlan, makeProgressLog, isJWTAuthError } from "./cli.js";
12
+ import { selectKey, ask } from "./prompts.js";
13
+ import { showPlan, makeProgressLog, numberedLine } from "./display.js";
14
+ import { isJWTAuthError } from "./cli.js";
15
+ import { tasksJsonPath, themesMdPath } from "./run-paths.js";
13
16
  import { renderPrompt } from "../prompts/load.js";
14
17
  export async function runPlanPhase(input) {
15
18
  const { objective, noTTY, flex, budget, concurrency, cwd, plannerModel, workerModel, fastModel, plannerProvider, workerProvider, fastProvider, usageCap, allowExtraUsage, extraUsageBudget, useWorktrees, mergeStrategy, agentTimeoutMs, runDir, designDir, previousKnowledge, envForModel, coachedOriginal, coachedAt, } = input;
@@ -58,7 +61,7 @@ export async function runPlanPhase(input) {
58
61
  // readable record (and a future resume can skip identifyThemes).
59
62
  const saveThemesMd = (list) => {
60
63
  try {
61
- writeFileSync(join(runDir, "themes.md"), `# Themes\n\n**Objective:** ${objective}\n\n${list.map((t, i) => `${i + 1}. ${t}`).join("\n")}\n`, "utf-8");
64
+ writeFileSync(themesMdPath(runDir), `# Themes\n\n**Objective:** ${objective}\n\n${list.map((t, i) => `${i + 1}. ${t}`).join("\n")}\n`, "utf-8");
62
65
  }
63
66
  catch { }
64
67
  };
@@ -69,7 +72,7 @@ export async function runPlanPhase(input) {
69
72
  let reviewing = true;
70
73
  while (reviewing) {
71
74
  for (let i = 0; i < themes.length; i++)
72
- console.log(chalk.dim(` ${String(i + 1).padStart(3)}.`) + ` ${themes[i]}`);
75
+ console.log(numberedLine(i, themes[i]));
73
76
  console.log(chalk.dim(`\n ${thinkingCount} thinking agents → orchestrate → ${(budget ?? 10) - thinkingCount} execution sessions\n`));
74
77
  const action = await selectKey(`${chalk.white(`${themes.length} themes`)} ${chalk.dim(`· ${thinkingCount} thinking · ${concurrency} concurrent`)}`, [{ key: "r", desc: "un" }, { key: "e", desc: "dit" }, { key: "c", desc: "hat" }, { key: "q", desc: "uit" }]);
75
78
  if (action === "r") {
@@ -128,17 +131,13 @@ export async function runPlanPhase(input) {
128
131
  }
129
132
  process.stdout.write("\x1B[?25l");
130
133
  mkdirSync(designDir, { recursive: true });
131
- const existingDesigns = readMdDir(designDir);
132
- if (existingDesigns) {
133
- const designFiles = readdirSync(designDir).filter(f => f.endsWith(".md")).sort();
134
- console.log(chalk.green(`\n ✓ Reusing ${designFiles.length} design docs`) + chalk.dim(` (from prior attempt)`));
135
- for (const f of designFiles) {
136
- try {
137
- const firstLine = readFileSync(join(designDir, f), "utf-8").split("\n")[0].replace(/^#+\s*/, "").trim();
138
- if (firstLine)
139
- console.log(chalk.dim(` ${firstLine.slice(0, 80)}`));
140
- }
141
- catch { }
134
+ const priorDesigns = readMdEntries(designDir);
135
+ if (priorDesigns.length > 0) {
136
+ console.log(chalk.green(`\n ✓ Reusing ${priorDesigns.length} design docs`) + chalk.dim(` (from prior attempt)`));
137
+ for (const { body } of priorDesigns) {
138
+ const firstLine = body.split("\n")[0].replace(/^#+\s*/, "").trim();
139
+ if (firstLine)
140
+ console.log(chalk.dim(` ${firstLine.slice(0, 80)}`));
142
141
  }
143
142
  console.log("");
144
143
  }
@@ -213,7 +212,7 @@ export async function runPlanPhase(input) {
213
212
  }
214
213
  }
215
214
  const designs = readMdDir(designDir);
216
- const taskFile = join(runDir, "tasks.json");
215
+ const taskFile = tasksJsonPath(runDir);
217
216
  if (designs) {
218
217
  const orchBudget = Math.min(50, Math.max(concurrency, Math.ceil(((budget ?? 10) - thinkingUsed) * 0.5)));
219
218
  const flexNote = renderPrompt("_shared/flex-note", { vars: { remainingBudget: (budget ?? 10) - thinkingUsed } });
@@ -0,0 +1,16 @@
1
+ export declare const PASTE_PLACEHOLDER_MAX = 80;
2
+ /**
3
+ * Read a line from the user with bracketed-paste awareness. Pasted multi-line
4
+ * text stays in the buffer as a single block — only a typed Enter submits.
5
+ * Falls back to cooked readline when stdin isn't a TTY.
6
+ */
7
+ export declare function ask(question: string): Promise<string>;
8
+ export declare function select<T>(label: string, items: {
9
+ name: string;
10
+ value: T;
11
+ hint?: string;
12
+ }[], defaultIdx?: number): Promise<T>;
13
+ export declare function selectKey(label: string, options: {
14
+ key: string;
15
+ desc: string;
16
+ }[]): Promise<string>;
@@ -0,0 +1,226 @@
1
+ // Interactive terminal primitives: ask, select, selectKey.
2
+ //
3
+ // Text entry goes through the shared raw-input parser in `../ui/raw-input.ts`,
4
+ // which enforces the single invariant that used to be duplicated (and buggy)
5
+ // here and in the Ink overlay:
6
+ // - Typed Enter = a stdin chunk that is exactly "\r", "\n", or "\r\n".
7
+ // - Anything else with embedded newlines is a paste, not a submit.
8
+ // Multi-line pastes render as a compact `[Pasted +N lines]` placeholder while
9
+ // editing — the full content is substituted on submit.
10
+ import { createInterface } from "readline";
11
+ import chalk from "chalk";
12
+ import { parseChunk, setBracketedPaste, deleteWordBackward } from "../ui/raw-input.js";
13
+ export const PASTE_PLACEHOLDER_MAX = 80;
14
+ function appendTypedChar(segs, ch) {
15
+ const last = segs[segs.length - 1];
16
+ if (last && last.type === "text")
17
+ last.content += ch;
18
+ else
19
+ segs.push({ type: "text", content: ch });
20
+ }
21
+ function appendPaste(segs, text) {
22
+ if (!text)
23
+ return;
24
+ const norm = text.replace(/\r\n?/g, "\n");
25
+ if (!norm.includes("\n") && norm.length <= PASTE_PLACEHOLDER_MAX) {
26
+ appendTypedChar(segs, norm);
27
+ return;
28
+ }
29
+ segs.push({ type: "paste", content: norm });
30
+ }
31
+ function backspaceSegs(segs) {
32
+ while (segs.length > 0) {
33
+ const last = segs[segs.length - 1];
34
+ if (last.type === "paste") {
35
+ segs.pop();
36
+ return;
37
+ }
38
+ if (last.content.length > 1) {
39
+ last.content = last.content.slice(0, -1);
40
+ return;
41
+ }
42
+ segs.pop();
43
+ return;
44
+ }
45
+ }
46
+ const segsToString = (segs) => segs.map((s) => s.content).join("");
47
+ function renderSegs(segs) {
48
+ return segs.map((s) => {
49
+ if (s.type === "text")
50
+ return s.content;
51
+ const lines = s.content.split("\n").length;
52
+ return chalk.dim(`[Pasted +${lines} line${lines === 1 ? "" : "s"}]`);
53
+ }).join("");
54
+ }
55
+ const stripAnsi = (s) => s.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "");
56
+ /**
57
+ * Read a line from the user with bracketed-paste awareness. Pasted multi-line
58
+ * text stays in the buffer as a single block — only a typed Enter submits.
59
+ * Falls back to cooked readline when stdin isn't a TTY.
60
+ */
61
+ export function ask(question) {
62
+ const { stdin, stdout } = process;
63
+ if (!stdin.isTTY) {
64
+ const rl = createInterface({ input: stdin, output: stdout });
65
+ return new Promise((res) => rl.question(question, (a) => { rl.close(); res(a.trim()); }));
66
+ }
67
+ return new Promise((resolve) => {
68
+ const segs = [];
69
+ const tail = question.split("\n").pop() ?? "";
70
+ const tailVisibleLen = stripAnsi(tail).length;
71
+ let prevWrapRows = 0;
72
+ const redraw = () => {
73
+ const cols = stdout.columns || 80;
74
+ if (prevWrapRows > 0)
75
+ stdout.write(`\x1B[${prevWrapRows}A`);
76
+ stdout.write("\r\x1B[J");
77
+ const rendered = renderSegs(segs);
78
+ stdout.write(tail + rendered);
79
+ const visible = tailVisibleLen + stripAnsi(rendered).length;
80
+ prevWrapRows = visible > 0 ? Math.floor((visible - 1) / cols) : 0;
81
+ };
82
+ stdout.write(question);
83
+ setBracketedPaste(stdout, true);
84
+ try {
85
+ stdin.setRawMode(true);
86
+ }
87
+ catch { }
88
+ stdin.resume();
89
+ const cleanup = () => {
90
+ setBracketedPaste(stdout, false);
91
+ try {
92
+ stdin.setRawMode(false);
93
+ }
94
+ catch { }
95
+ stdin.removeListener("data", onData);
96
+ stdin.pause();
97
+ };
98
+ const submit = () => { stdout.write("\n"); cleanup(); resolve(segsToString(segs).trim()); };
99
+ const onData = (buf) => {
100
+ for (const ev of parseChunk(buf.toString())) {
101
+ switch (ev.type) {
102
+ case "char":
103
+ appendTypedChar(segs, ev.text);
104
+ break;
105
+ case "paste":
106
+ appendPaste(segs, ev.text);
107
+ break;
108
+ case "backspace":
109
+ backspaceSegs(segs);
110
+ break;
111
+ case "word-delete": {
112
+ const next = deleteWordBackward(segsToString(segs));
113
+ segs.length = 0;
114
+ if (next)
115
+ segs.push({ type: "text", content: next });
116
+ break;
117
+ }
118
+ case "clear-line":
119
+ segs.length = 0;
120
+ break;
121
+ case "submit":
122
+ submit();
123
+ return;
124
+ case "cancel":
125
+ submit();
126
+ return; // lone ESC = submit, preserves old behavior
127
+ case "interrupt":
128
+ cleanup();
129
+ stdout.write("\n");
130
+ process.exit(130);
131
+ // tab + nav: ignore during single-line prompts
132
+ }
133
+ }
134
+ redraw();
135
+ };
136
+ stdin.on("data", onData);
137
+ });
138
+ }
139
+ export async function select(label, items, defaultIdx = 0) {
140
+ const { stdin, stdout } = process;
141
+ let idx = defaultIdx;
142
+ const draw = (first = false) => {
143
+ if (!first)
144
+ stdout.write(`\x1B[${items.length}A`);
145
+ for (let i = 0; i < items.length; i++) {
146
+ const sel = i === idx;
147
+ const radio = sel ? chalk.cyan(" ● ") : chalk.dim(" ○ ");
148
+ const name = sel ? chalk.white(items[i].name) : chalk.dim(items[i].name);
149
+ const hint = items[i].hint ? chalk.dim(` · ${items[i].hint}`) : "";
150
+ stdout.write(`\x1B[2K${radio}${name}${hint}\n`);
151
+ }
152
+ };
153
+ stdout.write(`\n ${chalk.bold(label)}\n`);
154
+ draw(true);
155
+ return new Promise((resolve) => {
156
+ stdin.setRawMode(true);
157
+ stdin.resume();
158
+ const done = (val) => {
159
+ stdin.setRawMode(false);
160
+ stdin.removeListener("data", handler);
161
+ stdin.pause();
162
+ resolve(val);
163
+ };
164
+ const handler = (buf) => {
165
+ const s = buf.toString();
166
+ // Arrow keys: \x1B[A = up, \x1B[B = down. Ignore other escape sequences.
167
+ if (s === "\x1B[A") {
168
+ idx = (idx - 1 + items.length) % items.length;
169
+ draw();
170
+ return;
171
+ }
172
+ if (s === "\x1B[B") {
173
+ idx = (idx + 1) % items.length;
174
+ draw();
175
+ return;
176
+ }
177
+ if (s[0] === "\x1B")
178
+ return;
179
+ if (s === "\r")
180
+ done(items[idx].value);
181
+ else if (s === "\x03") {
182
+ stdin.setRawMode(false);
183
+ process.exit(0);
184
+ }
185
+ else if (/^[1-9]$/.test(s)) {
186
+ const n = parseInt(s) - 1;
187
+ if (n < items.length) {
188
+ idx = n;
189
+ draw();
190
+ done(items[idx].value);
191
+ }
192
+ }
193
+ };
194
+ stdin.on("data", handler);
195
+ });
196
+ }
197
+ export async function selectKey(label, options) {
198
+ const { stdin, stdout } = process;
199
+ const keys = options.map((o) => o.key.toLowerCase());
200
+ const optStr = options.map((o) => `${chalk.cyan.bold(o.key.toUpperCase())}${chalk.dim(o.desc)}`).join(chalk.dim(" │ "));
201
+ stdout.write(`\n ${label}\n ${optStr}\n `);
202
+ return new Promise((resolve) => {
203
+ stdin.setRawMode(true);
204
+ stdin.resume();
205
+ const finish = (val) => {
206
+ stdin.setRawMode(false);
207
+ stdin.removeListener("data", handler);
208
+ stdin.pause();
209
+ resolve(val);
210
+ };
211
+ const handler = (buf) => {
212
+ const s = buf.toString().toLowerCase();
213
+ if (s[0] === "\x1B")
214
+ return;
215
+ if (s === "\x03") {
216
+ stdin.setRawMode(false);
217
+ process.exit(0);
218
+ }
219
+ if (s === "\r")
220
+ return finish(keys[0]);
221
+ if (s.length === 1 && keys.includes(s))
222
+ finish(s);
223
+ };
224
+ stdin.on("data", handler);
225
+ });
226
+ }
@@ -1,22 +1,22 @@
1
- import { readFileSync } from "fs";
2
- import { join } from "path";
3
1
  import chalk from "chalk";
4
- import { formatContextWindow } from "../core/models.js";
2
+ import { readFileOrEmpty, readJsonOrNull } from "../core/fs-helpers.js";
5
3
  import { saveRunState, findIncompleteRuns, showRunHistory, formatTimeAgo, autoMergeBranches, readMdDir, } from "../state/state.js";
6
4
  import { orchestrate, salvageFromFile } from "../planner/planner.js";
7
5
  import { setTranscriptRunDir } from "../core/transcripts.js";
8
- import { wrap } from "../ui/primitives.js";
9
- import { makeProgressLog, selectKey } from "./cli.js";
10
- import { editRunSettings } from "./settings.js";
6
+ import { wrap, terminalWidth } from "../ui/primitives.js";
7
+ import { selectKey } from "./prompts.js";
8
+ import { makeProgressLog } from "./display.js";
9
+ import { editRunSettings, printRunSettings } from "./settings.js";
10
+ import { tasksJsonPath, designsDir, statusMdPath } from "./run-paths.js";
11
11
  import { renderPrompt } from "../prompts/load.js";
12
12
  export function countTasksInFile(path) {
13
- try {
14
- const parsed = JSON.parse(readFileSync(path, "utf-8"));
15
- return Array.isArray(parsed?.tasks) ? parsed.tasks.length : 0;
16
- }
17
- catch {
18
- return 0;
19
- }
13
+ const parsed = readJsonOrNull(path);
14
+ return parsed && Array.isArray(parsed.tasks) ? parsed.tasks.length : 0;
15
+ }
16
+ /** Read the first line / preview of a run's status.md. Returns "" if missing. */
17
+ function readStatusPreview(runDir, maxLen, firstLineOnly = false) {
18
+ const raw = readFileOrEmpty(statusMdPath(runDir)).trim();
19
+ return (firstLineOnly ? raw.split("\n")[0] : raw).slice(0, maxLen);
20
20
  }
21
21
  export async function promptResumeOverrides(state, cliFlags, argv, noTTY, runDir) {
22
22
  // ── Apply CLI flag overrides first ──
@@ -56,29 +56,8 @@ export async function promptResumeOverrides(state, cliFlags, argv, noTTY, runDir
56
56
  return;
57
57
  }
58
58
  // ── Interactive review ──
59
- const fmtSummary = () => {
60
- const remaining = Math.max(1, state.remaining);
61
- const capStr = state.usageCap != null ? `${Math.round(state.usageCap * 100)}%` : "unlimited";
62
- const extraStr = state.allowExtraUsage
63
- ? (state.extraUsageBudget ? `$${state.extraUsageBudget}` : "unlimited")
64
- : "off";
65
- const modelLine = (label, m) => m ? ` ${chalk.dim(label.padEnd(11))}${chalk.white(m)} ${chalk.dim(`(${formatContextWindow(m)} context)`)}` : null;
66
- console.log();
67
- console.log(` ${chalk.dim("Resume settings")}`);
68
- console.log(` ${chalk.dim("─".repeat(40))}`);
69
- const lines = [
70
- modelLine("planner", state.plannerModel),
71
- modelLine("worker", state.workerModel),
72
- modelLine("fast", state.fastModel),
73
- ].filter(Boolean);
74
- for (const l of lines)
75
- console.log(l);
76
- console.log(` ${chalk.dim("remaining ")}${chalk.white(String(remaining))} ${chalk.dim("sessions")}`);
77
- console.log(` ${chalk.dim("concur ")}${chalk.white(String(state.concurrency))}`);
78
- console.log(` ${chalk.dim("usage cap ")}${chalk.white(capStr)}`);
79
- console.log(` ${chalk.dim("extra ")}${chalk.white(extraStr)}`);
80
- };
81
- fmtSummary();
59
+ const showSummary = () => printRunSettings(state, { header: "Resume settings", remaining: state.remaining });
60
+ showSummary();
82
61
  const action = await selectKey("", [
83
62
  { key: "r", desc: "esume" },
84
63
  { key: "e", desc: "dit" },
@@ -110,7 +89,7 @@ export async function promptResumeOverrides(state, cliFlags, argv, noTTY, runDir
110
89
  }
111
90
  catch { }
112
91
  console.log(chalk.green("\n ✓ Settings updated"));
113
- fmtSummary();
92
+ showSummary();
114
93
  console.log();
115
94
  }
116
95
  export async function detectResume(input) {
@@ -151,15 +130,10 @@ export async function detectResume(input) {
151
130
  const failed = prev.branches.filter(b => b.status === "failed" || b.status === "merge-failed").length;
152
131
  const obj = prev.objective?.slice(0, 50) || "";
153
132
  const ago = formatTimeAgo(prev.startedAt);
154
- let lastStatus = "";
155
- try {
156
- lastStatus = readFileSync(join(run.dir, "status.md"), "utf-8").trim().slice(0, 200);
157
- }
158
- catch { }
159
- const planTaskCount = prev.phase === "planning" ? countTasksInFile(join(run.dir, "tasks.json")) : 0;
133
+ const lastStatus = readStatusPreview(run.dir, 200);
134
+ const planTaskCount = prev.phase === "planning" ? countTasksInFile(tasksJsonPath(run.dir)) : 0;
160
135
  console.log(chalk.yellow(`\n ⚠ Unfinished run`) + chalk.dim(` · ${ago}`));
161
- const termW = Math.max(process.stdout.columns ?? 80, 60);
162
- const statusMaxW = Math.min(termW - 8, 80);
136
+ const statusMaxW = Math.min(terminalWidth() - 8, 80);
163
137
  const leftover = prev.currentTasks?.length ?? 0;
164
138
  const leftoverNote = prev.phase === "stopped" && leftover > 0
165
139
  ? ` · ${leftover} leftover task${leftover === 1 ? "" : "s"} preserved`
@@ -209,22 +183,17 @@ export async function detectResume(input) {
209
183
  const ago = formatTimeAgo(s.startedAt);
210
184
  const obj = s.objective?.slice(0, 50) || "";
211
185
  const merged = s.branches.filter(b => b.status === "merged").length;
212
- let lastStatus = "";
213
- try {
214
- lastStatus = readFileSync(join(shown[i].dir, "status.md"), "utf-8").trim().split("\n")[0].slice(0, 120);
215
- }
216
- catch { }
186
+ const lastStatus = readStatusPreview(shown[i].dir, 120, true);
217
187
  console.log(chalk.cyan(` ${i + 1}`) + ` ${obj}${obj.length >= 50 ? "…" : ""}`);
218
188
  if (s.phase === "planning") {
219
- const n = countTasksInFile(join(shown[i].dir, "tasks.json"));
189
+ const n = countTasksInFile(tasksJsonPath(shown[i].dir));
220
190
  console.log(chalk.dim(` plan ready · ${n} tasks · budget ${s.budget} · ${ago} · not yet executing`));
221
191
  }
222
192
  else {
223
193
  console.log(chalk.dim(` ${s.accCompleted}/${s.budget} · $${s.accCost.toFixed(2)} · ${ago} · ${s.phase} at wave ${s.waveNum + 1}${merged ? ` · ${merged} merged` : ""}`));
224
194
  }
225
195
  if (lastStatus) {
226
- const termW = Math.max(process.stdout.columns ?? 80, 60);
227
- for (const wl of wrap(lastStatus, termW - 6))
196
+ for (const wl of wrap(lastStatus, terminalWidth() - 6))
228
197
  console.log(chalk.dim(` ${wl}`));
229
198
  }
230
199
  console.log("");
@@ -257,7 +226,7 @@ export async function detectResume(input) {
257
226
  // already persisted the leftover work — resume executes those directly.
258
227
  // Otherwise fall back to tasks.json (planning-phase + legacy stopped runs).
259
228
  if (resumeState.currentTasks.length === 0) {
260
- const loaded = salvageFromFile(join(resumeRunDir, "tasks.json"), resumeState.budget, () => { }, "resume");
229
+ const loaded = salvageFromFile(tasksJsonPath(resumeRunDir), resumeState.budget, () => { }, "resume");
261
230
  if (loaded) {
262
231
  resumeState.currentTasks = loaded;
263
232
  const label = resumeState.phase === "planning" ? "Resuming plan" : `Resuming ${resumeState.phase} run`;
@@ -267,7 +236,7 @@ export async function detectResume(input) {
267
236
  // No tasks.json -- the thinking wave got killed before orchestrate ran.
268
237
  // If design docs survived, re-orchestrate from them (salvages the
269
238
  // thinking spend instead of throwing it away).
270
- const designs = readMdDir(join(resumeRunDir, "designs"));
239
+ const designs = readMdDir(designsDir(resumeRunDir));
271
240
  if (!designs || !resumeState.objective) {
272
241
  // Planning died before producing anything — re-run planning from
273
242
  // scratch while keeping all saved settings (model, budget, etc.).
@@ -284,7 +253,7 @@ export async function detectResume(input) {
284
253
  // land alongside the prior run's planning trail.
285
254
  setTranscriptRunDir(resumeRunDir);
286
255
  try {
287
- const orchTasks = await orchestrate(resumeState.objective, designs, cwd, resumeState.plannerModel, resumeState.workerModel, orchBudget, resumeState.concurrency, makeProgressLog(), flexNote, join(resumeRunDir, "tasks.json"), "orchestrate-resume");
256
+ const orchTasks = await orchestrate(resumeState.objective, designs, cwd, resumeState.plannerModel, resumeState.workerModel, orchBudget, resumeState.concurrency, makeProgressLog(), flexNote, tasksJsonPath(resumeRunDir), "orchestrate-resume");
288
257
  resumeState.currentTasks = orchTasks;
289
258
  process.stdout.write(`\x1B[2K\r ${chalk.green(`✓ ${orchTasks.length} tasks`)}\n`);
290
259
  }
@@ -0,0 +1,4 @@
1
+ export declare const tasksJsonPath: (runDir: string) => string;
2
+ export declare const designsDir: (runDir: string) => string;
3
+ export declare const themesMdPath: (runDir: string) => string;
4
+ export declare const statusMdPath: (runDir: string) => string;
@@ -0,0 +1,8 @@
1
+ // Filesystem layout of a single run dir. Keeps the on-disk schema in one
2
+ // place so resume.ts and plan-phase.ts don't bake "tasks.json" / "designs"
3
+ // strings independently.
4
+ import { join } from "path";
5
+ export const tasksJsonPath = (runDir) => join(runDir, "tasks.json");
6
+ export const designsDir = (runDir) => join(runDir, "designs");
7
+ export const themesMdPath = (runDir) => join(runDir, "themes.md");
8
+ export const statusMdPath = (runDir) => join(runDir, "status.md");
@@ -15,6 +15,13 @@ interface EditSettingsOptions {
15
15
  }
16
16
  /** Interactively edit all mutable run settings. Mutates `options.current` in place. */
17
17
  export declare function editRunSettings(options: EditSettingsOptions): Promise<MutableRunSettings>;
18
+ /** Print the planner/worker/fast/concur/usage/extra block.
19
+ * When `header` is set, the block gets a "Resume settings" / ─── header.
20
+ * When `remaining` is set, a `remaining N sessions` line is inserted before concur. */
21
+ export declare function printRunSettings(s: MutableRunSettings, opts?: {
22
+ header?: string;
23
+ remaining?: number;
24
+ }): void;
18
25
  /** Format a MutableRunSettings as a compact summary line for the terminal. */
19
26
  export declare function formatSettingsSummary(s: MutableRunSettings): string;
20
27
  export {};
@@ -1,6 +1,8 @@
1
1
  import chalk from "chalk";
2
2
  import { modelDisplayName, formatContextWindow } from "../core/models.js";
3
- import { fetchModels, ask, select, BRAILLE } from "./cli.js";
3
+ import { ask, select } from "./prompts.js";
4
+ import { BRAILLE } from "./display.js";
5
+ import { fetchModels } from "./cli.js";
4
6
  import { pickModel } from "../providers/index.js";
5
7
  /** Interactively edit all mutable run settings. Mutates `options.current` in place. */
6
8
  export async function editRunSettings(options) {
@@ -76,22 +78,36 @@ export async function editRunSettings(options) {
76
78
  s.allowExtraUsage = false;
77
79
  s.extraUsageBudget = undefined;
78
80
  }
79
- const modelLine = (label, m) => m ? `${chalk.dim(label.padEnd(11))}${chalk.white(m)} ${chalk.dim(`(${formatContextWindow(m)} context)`)}` : null;
81
+ printRunSettings(s);
82
+ return s;
83
+ }
84
+ /** Print the planner/worker/fast/concur/usage/extra block.
85
+ * When `header` is set, the block gets a "Resume settings" / ─── header.
86
+ * When `remaining` is set, a `remaining N sessions` line is inserted before concur. */
87
+ export function printRunSettings(s, opts = {}) {
88
+ const modelLine = (label, m) => m ? ` ${chalk.dim(label.padEnd(11))}${chalk.white(m)} ${chalk.dim(`(${formatContextWindow(m)} context)`)}` : null;
80
89
  const lines = [
81
90
  modelLine("planner", s.plannerModel),
82
91
  modelLine("worker", s.workerModel),
83
92
  modelLine("fast", s.fastModel),
84
93
  ].filter(Boolean);
94
+ const capStr = s.usageCap != null ? `${Math.round(s.usageCap * 100)}%` : "unlimited";
95
+ const extraStr = s.allowExtraUsage ? (s.extraUsageBudget ? `$${s.extraUsageBudget}` : "unlimited") : "off";
85
96
  console.log();
97
+ if (opts.header) {
98
+ console.log(` ${chalk.dim(opts.header)}`);
99
+ console.log(` ${chalk.dim("─".repeat(40))}`);
100
+ }
86
101
  for (const l of lines)
87
102
  console.log(l);
88
- const capStr = s.usageCap != null ? `${Math.round(s.usageCap * 100)}%` : "unlimited";
89
- const extraStr = s.allowExtraUsage ? (s.extraUsageBudget ? `$${s.extraUsageBudget}` : "unlimited") : "off";
103
+ if (opts.remaining != null) {
104
+ console.log(` ${chalk.dim("remaining ")}${chalk.white(String(Math.max(1, opts.remaining)))} ${chalk.dim("sessions")}`);
105
+ }
90
106
  console.log(` ${chalk.dim("concur ")}${chalk.white(String(s.concurrency))}`);
91
107
  console.log(` ${chalk.dim("usage cap ")}${chalk.white(capStr)}`);
92
108
  console.log(` ${chalk.dim("extra ")}${chalk.white(extraStr)}`);
93
- console.log();
94
- return s;
109
+ if (!opts.header)
110
+ console.log();
95
111
  }
96
112
  /** Format a MutableRunSettings as a compact summary line for the terminal. */
97
113
  export function formatSettingsSummary(s) {