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,7 @@
1
+ export declare function parseCliFlags(argv: string[]): {
2
+ flags: Record<string, string>;
3
+ positional: string[];
4
+ };
5
+ export declare function validateConcurrency(value: unknown): asserts value is number;
6
+ export declare function isGitRepo(cwd: string): boolean;
7
+ export declare function validateGitRepo(cwd: string): void;
@@ -0,0 +1,47 @@
1
+ // Argv parsing + bootstrap-time validation. Pure: no stdio, no SDK.
2
+ import { execSync } from "child_process";
3
+ const KNOWN_VALUE_FLAGS = new Set(["concurrency", "model", "timeout", "budget", "usage-cap", "extra-usage-budget", "merge"]);
4
+ const BOOLEAN_FLAGS = new Set(["--dry-run", "-h", "--help", "-v", "--version", "--flex", "--no-flex", "--allow-extra-usage", "--worktrees", "--no-worktrees", "--yolo"]);
5
+ export function parseCliFlags(argv) {
6
+ const flags = {};
7
+ const positional = [];
8
+ for (let i = 0; i < argv.length; i++) {
9
+ const arg = argv[i];
10
+ if (BOOLEAN_FLAGS.has(arg))
11
+ continue;
12
+ const eq = arg.match(/^--(\w[\w-]*)=(.+)$/);
13
+ if (eq && KNOWN_VALUE_FLAGS.has(eq[1])) {
14
+ flags[eq[1]] = eq[2];
15
+ continue;
16
+ }
17
+ const bare = arg.match(/^--(\w[\w-]*)$/);
18
+ if (bare && KNOWN_VALUE_FLAGS.has(bare[1]) && i + 1 < argv.length && !argv[i + 1].startsWith("-")) {
19
+ flags[bare[1]] = argv[++i];
20
+ continue;
21
+ }
22
+ if (!arg.startsWith("--"))
23
+ positional.push(arg);
24
+ }
25
+ return { flags, positional };
26
+ }
27
+ export function validateConcurrency(value) {
28
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
29
+ throw new Error(`Concurrency must be a positive integer (got ${JSON.stringify(value)})`);
30
+ }
31
+ }
32
+ export function isGitRepo(cwd) {
33
+ try {
34
+ execSync("git rev-parse --git-dir", { cwd, encoding: "utf-8", stdio: "pipe" });
35
+ return true;
36
+ }
37
+ catch {
38
+ return false;
39
+ }
40
+ }
41
+ export function validateGitRepo(cwd) {
42
+ if (!isGitRepo(cwd)) {
43
+ throw new Error(`Worktrees require a git repository, but ${cwd} is not inside one.\n` +
44
+ ` Run: cd ${cwd} && git init\n` +
45
+ ` Or set "worktrees": false in your task file.`);
46
+ }
47
+ }
package/dist/cli/cli.d.ts CHANGED
@@ -1,65 +1,10 @@
1
1
  import type { ModelInfo } from "@anthropic-ai/claude-agent-sdk";
2
- import type { Task, MergeStrategy } from "../core/types.js";
3
- export declare function parseCliFlags(argv: string[]): {
4
- flags: Record<string, string>;
5
- positional: string[];
6
- };
7
2
  import { isJWTAuthError } from "../core/auth.js";
8
- /** @deprecated Use isJWTAuthError from auth.ts instead. */
9
- export declare const isAuthError: typeof isJWTAuthError;
10
3
  export { isJWTAuthError };
4
+ export { parseCliFlags, validateConcurrency, isGitRepo, validateGitRepo } from "./argv.js";
5
+ export { ask, select, selectKey, PASTE_PLACEHOLDER_MAX } from "./prompts.js";
6
+ export { loadTaskFile, loadPlanFile, type FileArgs } from "./files.js";
7
+ export { BRAILLE, showPlan, makeProgressLog, numberedLine } from "./display.js";
8
+ /** Fetch the SDK's reported model list. Silent on timeout (callers degrade
9
+ * to a free-form text prompt); hard-fails on auth errors. */
11
10
  export declare function fetchModels(timeoutMs?: number): Promise<ModelInfo[]>;
12
- export declare const PASTE_PLACEHOLDER_MAX = 80;
13
- /**
14
- * Read a line from the user with bracketed-paste awareness. Pasted multi-line
15
- * text stays in the buffer as a single block -- only a typed Enter submits.
16
- * Falls back to cooked readline when stdin isn't a TTY.
17
- */
18
- export declare function ask(question: string): Promise<string>;
19
- export declare function select<T>(label: string, items: {
20
- name: string;
21
- value: T;
22
- hint?: string;
23
- }[], defaultIdx?: number): Promise<T>;
24
- export declare function selectKey(label: string, options: {
25
- key: string;
26
- desc: string;
27
- }[]): Promise<string>;
28
- export interface FileArgs {
29
- tasks: Task[];
30
- objective?: string;
31
- concurrency?: number;
32
- model?: string;
33
- cwd?: string;
34
- allowedTools?: string[];
35
- beforeWave?: string | string[];
36
- afterWave?: string | string[];
37
- afterRun?: string | string[];
38
- useWorktrees?: boolean;
39
- mergeStrategy?: MergeStrategy;
40
- usageCap?: number;
41
- flexiblePlan?: boolean;
42
- }
43
- /** Load a markdown plan file. Extracts the first H1 as objective and returns the full body as planContent. */
44
- export declare function loadPlanFile(file: string): {
45
- objective: string;
46
- planContent: string;
47
- };
48
- export declare function loadTaskFile(file: string): FileArgs;
49
- export declare function validateConcurrency(value: unknown): asserts value is number;
50
- export declare function isGitRepo(cwd: string): boolean;
51
- export declare function validateGitRepo(cwd: string): void;
52
- export declare function showPlan(tasks: Task[]): void;
53
- export declare const BRAILLE: readonly ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
54
- /** Dual-mode progress renderer.
55
- *
56
- * - `status` (default): transient single-line ticker — clears itself each frame.
57
- * Use for elapsed-time / cost / rolling tail of model text.
58
- * - `event`: permanent log line — scrolls up, ticker redraws underneath.
59
- * Use for tool calls and notable state changes.
60
- *
61
- * The two modes cooperate: an event clears the current ticker, writes the
62
- * event on its own line, and the next status tick redraws the ticker below.
63
- * That gives the user a visible history of what the planner did, with a live
64
- * "now" indicator that always stays pinned at the bottom. */
65
- export declare function makeProgressLog(): (text: string, kind?: "status" | "event") => void;
package/dist/cli/cli.js CHANGED
@@ -1,41 +1,16 @@
1
- import { execSync } from "child_process";
2
- import { readFileSync } from "fs";
3
- import { resolve } from "path";
4
- import { createInterface } from "readline";
1
+ // Barrel + SDK-bound bits that don't fit the focused split modules.
5
2
  import chalk from "chalk";
6
3
  import { query } from "@anthropic-ai/claude-agent-sdk";
7
- import { parseChunk, setBracketedPaste, deleteWordBackward } from "../ui/raw-input.js";
8
- // ── CLI flag parsing ──
9
- export function parseCliFlags(argv) {
10
- const known = new Set(["concurrency", "model", "timeout", "budget", "usage-cap", "extra-usage-budget", "merge"]);
11
- const booleans = new Set(["--dry-run", "-h", "--help", "-v", "--version", "--flex", "--no-flex", "--allow-extra-usage", "--worktrees", "--no-worktrees", "--yolo"]);
12
- const flags = {};
13
- const positional = [];
14
- for (let i = 0; i < argv.length; i++) {
15
- const arg = argv[i];
16
- if (booleans.has(arg))
17
- continue;
18
- const eq = arg.match(/^--(\w[\w-]*)=(.+)$/);
19
- if (eq && known.has(eq[1])) {
20
- flags[eq[1]] = eq[2];
21
- continue;
22
- }
23
- const bare = arg.match(/^--(\w[\w-]*)$/);
24
- if (bare && known.has(bare[1]) && i + 1 < argv.length && !argv[i + 1].startsWith("-")) {
25
- flags[bare[1]] = argv[++i];
26
- continue;
27
- }
28
- if (!arg.startsWith("--"))
29
- positional.push(arg);
30
- }
31
- return { flags, positional };
32
- }
33
- // ── Auth error detection (re-exported from auth module for backward compatibility) ──
34
4
  import { isJWTAuthError } from "../core/auth.js";
35
- /** @deprecated Use isJWTAuthError from auth.ts instead. */
36
- export const isAuthError = isJWTAuthError;
37
5
  export { isJWTAuthError };
38
- // ── Fetch models via SDK ──
6
+ // Focused modules (split out of this file). Re-exported so existing callers
7
+ // — including src/index.ts — can keep importing from "./cli/cli.js".
8
+ export { parseCliFlags, validateConcurrency, isGitRepo, validateGitRepo } from "./argv.js";
9
+ export { ask, select, selectKey, PASTE_PLACEHOLDER_MAX } from "./prompts.js";
10
+ export { loadTaskFile, loadPlanFile } from "./files.js";
11
+ export { BRAILLE, showPlan, makeProgressLog, numberedLine } from "./display.js";
12
+ /** Fetch the SDK's reported model list. Silent on timeout (callers degrade
13
+ * to a free-form text prompt); hard-fails on auth errors. */
39
14
  export async function fetchModels(timeoutMs = 10_000) {
40
15
  let q;
41
16
  let timer;
@@ -57,391 +32,13 @@ export async function fetchModels(timeoutMs = 10_000) {
57
32
  if (err.message === "model_fetch_timeout") {
58
33
  // Silent: callers fall back to a text prompt with the current value as default.
59
34
  }
60
- else if (isAuthError(err)) {
61
- console.error(chalk.red("\n Authentication failed -- check your API key or run: claude auth\n"));
35
+ else if (isJWTAuthError(err)) {
36
+ console.error(chalk.red("\n Authentication failed check your API key or run: claude auth\n"));
62
37
  process.exit(1);
63
38
  }
64
39
  else {
65
- console.warn(chalk.yellow(`\n Could not fetch models: ${String(err.message || err).slice(0, 80)} -- continuing with defaults`));
40
+ console.warn(chalk.yellow(`\n Could not fetch models: ${String(err.message || err).slice(0, 80)} continuing with defaults`));
66
41
  }
67
42
  return [];
68
43
  }
69
44
  }
70
- // ── Interactive primitives ──
71
- //
72
- // Text entry goes through the shared raw-input parser in `../ui/raw-input.ts`,
73
- // which enforces the single invariant that used to be duplicated (and buggy)
74
- // here and in the Ink overlay:
75
- // - Typed Enter = a stdin chunk that is exactly "\r", "\n", or "\r\n".
76
- // - Anything else with embedded newlines is a paste, not a submit.
77
- // Multi-line pastes render as a compact `[Pasted +N lines]` placeholder while
78
- // editing — the full content is substituted on submit.
79
- export const PASTE_PLACEHOLDER_MAX = 80;
80
- function appendTypedChar(segs, ch) {
81
- const last = segs[segs.length - 1];
82
- if (last && last.type === "text")
83
- last.content += ch;
84
- else
85
- segs.push({ type: "text", content: ch });
86
- }
87
- function appendPaste(segs, text) {
88
- if (!text)
89
- return;
90
- const norm = text.replace(/\r\n?/g, "\n");
91
- if (!norm.includes("\n") && norm.length <= PASTE_PLACEHOLDER_MAX) {
92
- appendTypedChar(segs, norm);
93
- return;
94
- }
95
- segs.push({ type: "paste", content: norm });
96
- }
97
- function backspaceSegs(segs) {
98
- while (segs.length > 0) {
99
- const last = segs[segs.length - 1];
100
- if (last.type === "paste") {
101
- segs.pop();
102
- return;
103
- }
104
- if (last.content.length > 1) {
105
- last.content = last.content.slice(0, -1);
106
- return;
107
- }
108
- segs.pop();
109
- return;
110
- }
111
- }
112
- function segsToString(segs) { return segs.map((s) => s.content).join(""); }
113
- function renderSegs(segs) {
114
- return segs.map((s) => {
115
- if (s.type === "text")
116
- return s.content;
117
- const lines = s.content.split("\n").length;
118
- return chalk.dim(`[Pasted +${lines} line${lines === 1 ? "" : "s"}]`);
119
- }).join("");
120
- }
121
- function stripAnsi(s) {
122
- return s.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "");
123
- }
124
- /**
125
- * Read a line from the user with bracketed-paste awareness. Pasted multi-line
126
- * text stays in the buffer as a single block -- only a typed Enter submits.
127
- * Falls back to cooked readline when stdin isn't a TTY.
128
- */
129
- export function ask(question) {
130
- const { stdin, stdout } = process;
131
- if (!stdin.isTTY) {
132
- const rl = createInterface({ input: stdin, output: stdout });
133
- return new Promise((res) => rl.question(question, (a) => { rl.close(); res(a.trim()); }));
134
- }
135
- return new Promise((resolve) => {
136
- const segs = [];
137
- const tail = question.split("\n").pop() ?? "";
138
- const tailVisibleLen = stripAnsi(tail).length;
139
- let prevWrapRows = 0;
140
- const redraw = () => {
141
- const cols = stdout.columns || 80;
142
- if (prevWrapRows > 0)
143
- stdout.write(`\x1B[${prevWrapRows}A`);
144
- stdout.write("\r\x1B[J");
145
- const rendered = renderSegs(segs);
146
- stdout.write(tail + rendered);
147
- const visible = tailVisibleLen + stripAnsi(rendered).length;
148
- prevWrapRows = visible > 0 ? Math.floor((visible - 1) / cols) : 0;
149
- };
150
- stdout.write(question);
151
- setBracketedPaste(stdout, true);
152
- try {
153
- stdin.setRawMode(true);
154
- }
155
- catch { }
156
- stdin.resume();
157
- const cleanup = () => {
158
- setBracketedPaste(stdout, false);
159
- try {
160
- stdin.setRawMode(false);
161
- }
162
- catch { }
163
- stdin.removeListener("data", onData);
164
- stdin.pause();
165
- };
166
- const submit = () => { stdout.write("\n"); cleanup(); resolve(segsToString(segs).trim()); };
167
- const onData = (buf) => {
168
- for (const ev of parseChunk(buf.toString())) {
169
- switch (ev.type) {
170
- case "char":
171
- appendTypedChar(segs, ev.text);
172
- break;
173
- case "paste":
174
- appendPaste(segs, ev.text);
175
- break;
176
- case "backspace":
177
- backspaceSegs(segs);
178
- break;
179
- case "word-delete": {
180
- const s = segsToString(segs);
181
- const next = deleteWordBackward(s);
182
- segs.length = 0;
183
- if (next)
184
- segs.push({ type: "text", content: next });
185
- break;
186
- }
187
- case "clear-line":
188
- segs.length = 0;
189
- break;
190
- case "submit":
191
- submit();
192
- return;
193
- case "cancel":
194
- submit();
195
- return; // lone ESC = submit, preserves old behavior
196
- case "interrupt":
197
- cleanup();
198
- stdout.write("\n");
199
- process.exit(130);
200
- // tab + nav: ignore during single-line prompts
201
- }
202
- }
203
- redraw();
204
- };
205
- stdin.on("data", onData);
206
- });
207
- }
208
- export async function select(label, items, defaultIdx = 0) {
209
- const { stdin, stdout } = process;
210
- let idx = defaultIdx;
211
- const draw = (first = false) => {
212
- if (!first)
213
- stdout.write(`\x1B[${items.length}A`);
214
- for (let i = 0; i < items.length; i++) {
215
- const sel = i === idx;
216
- const radio = sel ? chalk.cyan(" ● ") : chalk.dim(" ○ ");
217
- const name = sel ? chalk.white(items[i].name) : chalk.dim(items[i].name);
218
- const hint = items[i].hint ? chalk.dim(` · ${items[i].hint}`) : "";
219
- stdout.write(`\x1B[2K${radio}${name}${hint}\n`);
220
- }
221
- };
222
- stdout.write(`\n ${chalk.bold(label)}\n`);
223
- draw(true);
224
- return new Promise((resolve) => {
225
- stdin.setRawMode(true);
226
- stdin.resume();
227
- const done = (val) => {
228
- stdin.setRawMode(false);
229
- stdin.removeListener("data", handler);
230
- stdin.pause();
231
- resolve(val);
232
- };
233
- const handler = (buf) => {
234
- const s = buf.toString();
235
- // Arrow keys: \x1B[A = up, \x1B[B = down
236
- if (s === "\x1B[A") {
237
- idx = (idx - 1 + items.length) % items.length;
238
- draw();
239
- return;
240
- }
241
- if (s === "\x1B[B") {
242
- idx = (idx + 1) % items.length;
243
- draw();
244
- return;
245
- }
246
- // Ignore any other ANSI escape sequences
247
- if (s[0] === "\x1B")
248
- return;
249
- if (s === "\r")
250
- done(items[idx].value);
251
- else if (s === "\x03") {
252
- stdin.setRawMode(false);
253
- process.exit(0);
254
- }
255
- else if (/^[1-9]$/.test(s)) {
256
- const n = parseInt(s) - 1;
257
- if (n < items.length) {
258
- idx = n;
259
- draw();
260
- done(items[idx].value);
261
- }
262
- }
263
- };
264
- stdin.on("data", handler);
265
- });
266
- }
267
- export async function selectKey(label, options) {
268
- const { stdin, stdout } = process;
269
- const keys = options.map((o) => o.key.toLowerCase());
270
- const optStr = options.map((o) => `${chalk.cyan.bold(o.key.toUpperCase())}${chalk.dim(o.desc)}`).join(chalk.dim(" │ "));
271
- stdout.write(`\n ${label}\n ${optStr}\n `);
272
- return new Promise((resolve) => {
273
- stdin.setRawMode(true);
274
- stdin.resume();
275
- const handler = (buf) => {
276
- const s = buf.toString().toLowerCase();
277
- // Ignore ANSI escape sequences
278
- if (s[0] === "\x1B")
279
- return;
280
- if (s === "\x03") {
281
- stdin.setRawMode(false);
282
- process.exit(0);
283
- }
284
- if (s === "\r") {
285
- stdin.setRawMode(false);
286
- stdin.removeListener("data", handler);
287
- stdin.pause();
288
- resolve(keys[0]);
289
- return;
290
- }
291
- if (s.length === 1 && keys.includes(s)) {
292
- stdin.setRawMode(false);
293
- stdin.removeListener("data", handler);
294
- stdin.pause();
295
- resolve(s);
296
- }
297
- };
298
- stdin.on("data", handler);
299
- });
300
- }
301
- const KNOWN_TASK_FILE_KEYS = new Set([
302
- "tasks", "objective", "concurrency", "cwd", "model", "allowedTools", "beforeWave", "afterWave", "afterRun", "worktrees", "mergeStrategy", "usageCap", "flexiblePlan",
303
- ]);
304
- /** Load a markdown plan file. Extracts the first H1 as objective and returns the full body as planContent. */
305
- export function loadPlanFile(file) {
306
- const path = resolve(file);
307
- let raw;
308
- try {
309
- raw = readFileSync(path, "utf-8");
310
- }
311
- catch {
312
- throw new Error(`Cannot read plan file: ${path}`);
313
- }
314
- const body = raw.trim();
315
- if (!body)
316
- throw new Error(`Plan file is empty: ${path}`);
317
- const h1 = body.match(/^#\s+(.+)$/m);
318
- const objective = (h1?.[1] ?? body.split("\n").find(l => l.trim())).trim();
319
- return { objective, planContent: body };
320
- }
321
- export function loadTaskFile(file) {
322
- const path = resolve(file);
323
- let raw;
324
- try {
325
- raw = readFileSync(path, "utf-8");
326
- }
327
- catch {
328
- throw new Error(`Cannot read task file: ${path}`);
329
- }
330
- let json;
331
- try {
332
- json = JSON.parse(raw);
333
- }
334
- catch {
335
- throw new Error(`Task file is not valid JSON: ${path}`);
336
- }
337
- const parsed = Array.isArray(json) ? { tasks: json } : json;
338
- if (!Array.isArray(json) && typeof json === "object" && json !== null) {
339
- const unknown = Object.keys(json).filter((k) => !KNOWN_TASK_FILE_KEYS.has(k));
340
- if (unknown.length > 0) {
341
- throw new Error(`Unknown key${unknown.length > 1 ? "s" : ""} in task file: ${unknown.join(", ")}. Allowed: ${[...KNOWN_TASK_FILE_KEYS].join(", ")}`);
342
- }
343
- }
344
- if (!Array.isArray(parsed.tasks))
345
- throw new Error(`Task file must contain a "tasks" array (got ${typeof parsed.tasks})`);
346
- const tasks = [];
347
- for (let i = 0; i < parsed.tasks.length; i++) {
348
- const t = parsed.tasks[i];
349
- const id = String(tasks.length);
350
- if (typeof t === "string") {
351
- if (!t.trim())
352
- throw new Error(`Task ${i} is an empty string`);
353
- tasks.push({ id, prompt: t });
354
- }
355
- else if (typeof t === "object" && t !== null) {
356
- if (typeof t.prompt !== "string" || !t.prompt.trim())
357
- throw new Error(`Task ${i} is missing a "prompt" string`);
358
- tasks.push({ id, prompt: t.prompt, cwd: t.cwd ? resolve(t.cwd) : undefined, model: t.model });
359
- }
360
- else {
361
- throw new Error(`Task ${i} must be a string or object with a "prompt" field (got ${typeof t})`);
362
- }
363
- }
364
- if (parsed.concurrency !== undefined)
365
- validateConcurrency(parsed.concurrency);
366
- const usageCap = parsed.usageCap;
367
- if (usageCap != null && (typeof usageCap !== "number" || usageCap < 0 || usageCap > 100)) {
368
- throw new Error(`usageCap must be a number between 0 and 100 (got ${JSON.stringify(usageCap)})`);
369
- }
370
- if (parsed.flexiblePlan && typeof parsed.objective !== "string") {
371
- throw new Error(`flexiblePlan requires an "objective" string in the task file`);
372
- }
373
- return {
374
- tasks,
375
- objective: typeof parsed.objective === "string" ? parsed.objective : undefined,
376
- concurrency: parsed.concurrency,
377
- model: parsed.model,
378
- cwd: parsed.cwd ? resolve(parsed.cwd) : undefined,
379
- allowedTools: parsed.allowedTools,
380
- beforeWave: parsed.beforeWave,
381
- afterWave: parsed.afterWave,
382
- afterRun: parsed.afterRun,
383
- useWorktrees: parsed.worktrees,
384
- mergeStrategy: parsed.mergeStrategy,
385
- usageCap,
386
- flexiblePlan: parsed.flexiblePlan,
387
- };
388
- }
389
- // ── Validation helpers ──
390
- export function validateConcurrency(value) {
391
- if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
392
- throw new Error(`Concurrency must be a positive integer (got ${JSON.stringify(value)})`);
393
- }
394
- }
395
- export function isGitRepo(cwd) {
396
- try {
397
- execSync("git rev-parse --git-dir", { cwd, encoding: "utf-8", stdio: "pipe" });
398
- return true;
399
- }
400
- catch {
401
- return false;
402
- }
403
- }
404
- export function validateGitRepo(cwd) {
405
- if (!isGitRepo(cwd)) {
406
- throw new Error(`Worktrees require a git repository, but ${cwd} is not inside one.\n` +
407
- ` Run: cd ${cwd} && git init\n` +
408
- ` Or set "worktrees": false in your task file.`);
409
- }
410
- }
411
- // ── Display helpers ──
412
- export function showPlan(tasks) {
413
- const w = Math.max((process.stdout.columns ?? 80) - 6, 40);
414
- const ruleLen = Math.min(w, 70);
415
- console.log(chalk.dim(` ─── ${tasks.length} tasks ${"─".repeat(Math.max(0, ruleLen - String(tasks.length).length - 10))}`));
416
- for (const t of tasks) {
417
- const num = chalk.dim(String(Number(t.id) + 1).padStart(4) + ".");
418
- console.log(`${num} ${t.prompt.slice(0, w)}`);
419
- }
420
- console.log(chalk.dim(` ${"─".repeat(ruleLen)}\n`));
421
- }
422
- export const BRAILLE = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
423
- /** Dual-mode progress renderer.
424
- *
425
- * - `status` (default): transient single-line ticker — clears itself each frame.
426
- * Use for elapsed-time / cost / rolling tail of model text.
427
- * - `event`: permanent log line — scrolls up, ticker redraws underneath.
428
- * Use for tool calls and notable state changes.
429
- *
430
- * The two modes cooperate: an event clears the current ticker, writes the
431
- * event on its own line, and the next status tick redraws the ticker below.
432
- * That gives the user a visible history of what the planner did, with a live
433
- * "now" indicator that always stays pinned at the bottom. */
434
- export function makeProgressLog() {
435
- let frame = 0;
436
- return (text, kind = "status") => {
437
- const maxW = (process.stdout.columns ?? 80) - 6;
438
- const clean = text.replace(/\n/g, " ");
439
- const line = clean.length > maxW ? clean.slice(0, maxW - 1) + "\u2026" : clean;
440
- if (kind === "event") {
441
- process.stdout.write(`\x1B[2K\r ${chalk.cyan("›")} ${chalk.dim(line)}\n`);
442
- return;
443
- }
444
- const spin = chalk.cyan(BRAILLE[frame++ % BRAILLE.length]);
445
- process.stdout.write(`\x1B[2K\r ${spin} ${chalk.dim(line)}`);
446
- };
447
- }
@@ -0,0 +1,17 @@
1
+ import type { Task } from "../core/types.js";
2
+ export declare const BRAILLE: readonly ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
3
+ export declare function showPlan(tasks: Task[]): void;
4
+ /** Numbered list line: ` N. text` with a dim N. Use for themes, plan reviews, etc. */
5
+ export declare function numberedLine(i: number, text: string, padStart?: number): string;
6
+ /** Dual-mode progress renderer.
7
+ *
8
+ * - `status` (default): transient single-line ticker — clears itself each frame.
9
+ * Use for elapsed-time / cost / rolling tail of model text.
10
+ * - `event`: permanent log line — scrolls up, ticker redraws underneath.
11
+ * Use for tool calls and notable state changes.
12
+ *
13
+ * The two modes cooperate: an event clears the current ticker, writes the
14
+ * event on its own line, and the next status tick redraws the ticker below.
15
+ * That gives the user a visible history of what the planner did, with a live
16
+ * "now" indicator that always stays pinned at the bottom. */
17
+ export declare function makeProgressLog(): (text: string, kind?: "status" | "event") => void;
@@ -0,0 +1,44 @@
1
+ // Terminal display helpers: spinner frames, plan listing, dual-mode progress log.
2
+ import chalk from "chalk";
3
+ import { terminalWidth } from "../ui/primitives.js";
4
+ export const BRAILLE = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
5
+ const termWidth = (margin = 6) => Math.max(terminalWidth() - margin, 40);
6
+ export function showPlan(tasks) {
7
+ const w = termWidth();
8
+ const ruleLen = Math.min(w, 70);
9
+ console.log(chalk.dim(` ─── ${tasks.length} tasks ${"─".repeat(Math.max(0, ruleLen - String(tasks.length).length - 10))}`));
10
+ for (const t of tasks) {
11
+ const num = chalk.dim(String(Number(t.id) + 1).padStart(4) + ".");
12
+ console.log(`${num} ${t.prompt.slice(0, w)}`);
13
+ }
14
+ console.log(chalk.dim(` ${"─".repeat(ruleLen)}\n`));
15
+ }
16
+ /** Numbered list line: ` N. text` with a dim N. Use for themes, plan reviews, etc. */
17
+ export function numberedLine(i, text, padStart = 3) {
18
+ return `${chalk.dim(` ${String(i + 1).padStart(padStart)}.`)} ${text}`;
19
+ }
20
+ /** Dual-mode progress renderer.
21
+ *
22
+ * - `status` (default): transient single-line ticker — clears itself each frame.
23
+ * Use for elapsed-time / cost / rolling tail of model text.
24
+ * - `event`: permanent log line — scrolls up, ticker redraws underneath.
25
+ * Use for tool calls and notable state changes.
26
+ *
27
+ * The two modes cooperate: an event clears the current ticker, writes the
28
+ * event on its own line, and the next status tick redraws the ticker below.
29
+ * That gives the user a visible history of what the planner did, with a live
30
+ * "now" indicator that always stays pinned at the bottom. */
31
+ export function makeProgressLog() {
32
+ let frame = 0;
33
+ return (text, kind = "status") => {
34
+ const maxW = termWidth();
35
+ const clean = text.replace(/\n/g, " ");
36
+ const line = clean.length > maxW ? clean.slice(0, maxW - 1) + "…" : clean;
37
+ if (kind === "event") {
38
+ process.stdout.write(`\x1B[2K\r ${chalk.cyan("›")} ${chalk.dim(line)}\n`);
39
+ return;
40
+ }
41
+ const spin = chalk.cyan(BRAILLE[frame++ % BRAILLE.length]);
42
+ process.stdout.write(`\x1B[2K\r ${spin} ${chalk.dim(line)}`);
43
+ };
44
+ }
@@ -0,0 +1,22 @@
1
+ import type { Task, MergeStrategy } from "../core/types.js";
2
+ export interface FileArgs {
3
+ tasks: Task[];
4
+ objective?: string;
5
+ concurrency?: number;
6
+ model?: string;
7
+ cwd?: string;
8
+ allowedTools?: string[];
9
+ beforeWave?: string | string[];
10
+ afterWave?: string | string[];
11
+ afterRun?: string | string[];
12
+ useWorktrees?: boolean;
13
+ mergeStrategy?: MergeStrategy;
14
+ usageCap?: number;
15
+ flexiblePlan?: boolean;
16
+ }
17
+ /** Load a markdown plan file. Extracts the first H1 as objective and returns the full body as planContent. */
18
+ export declare function loadPlanFile(file: string): {
19
+ objective: string;
20
+ planContent: string;
21
+ };
22
+ export declare function loadTaskFile(file: string): FileArgs;