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.
- package/dist/cli/argv.d.ts +7 -0
- package/dist/cli/argv.js +47 -0
- package/dist/cli/cli.d.ts +6 -61
- package/dist/cli/cli.js +12 -415
- package/dist/cli/display.d.ts +17 -0
- package/dist/cli/display.js +44 -0
- package/dist/cli/files.d.ts +22 -0
- package/dist/cli/files.js +94 -0
- package/dist/cli/help.js +4 -4
- package/dist/cli/plan-phase.js +16 -17
- package/dist/cli/prompts.d.ts +16 -0
- package/dist/cli/prompts.js +226 -0
- package/dist/cli/resume.js +25 -56
- package/dist/cli/run-paths.d.ts +4 -0
- package/dist/cli/run-paths.js +8 -0
- package/dist/cli/settings.d.ts +7 -0
- package/dist/cli/settings.js +22 -6
- package/dist/core/_version.d.ts +1 -1
- package/dist/core/_version.js +1 -1
- package/dist/core/errors.d.ts +6 -0
- package/dist/core/errors.js +23 -0
- package/dist/core/fs-helpers.d.ts +19 -0
- package/dist/core/fs-helpers.js +52 -0
- package/dist/core/key-vault.js +8 -12
- package/dist/core/proxy-port.js +6 -14
- package/dist/index.js +3 -6
- package/dist/planner/coach/coach.js +4 -6
- package/dist/planner/coach/context.js +13 -19
- package/dist/planner/coach/settings.js +5 -11
- package/dist/planner/planner.js +40 -72
- package/dist/planner/query-direct.d.ts +4 -0
- package/dist/planner/query-direct.js +52 -0
- package/dist/planner/query-stream.d.ts +4 -0
- package/dist/planner/query-stream.js +385 -0
- package/dist/planner/query.js +19 -435
- package/dist/planner/sdk-events.d.ts +55 -0
- package/dist/planner/sdk-events.js +10 -0
- package/dist/prompt-evolution/fixtures/generate.js +9 -17
- package/dist/prompt-evolution/fixtures/harvest.js +15 -21
- package/dist/prompt-evolution/llm-judge.js +5 -22
- package/dist/prompt-evolution/mutator.js +0 -1
- package/dist/prompt-evolution/persistence.js +38 -48
- package/dist/prompts/load.js +18 -22
- package/dist/providers/cursor/env.d.ts +53 -0
- package/dist/providers/cursor/env.js +267 -0
- package/dist/providers/cursor/index.d.ts +4 -0
- package/dist/providers/cursor/index.js +5 -0
- package/dist/providers/cursor/picker.d.ts +7 -0
- package/dist/providers/cursor/picker.js +298 -0
- package/dist/providers/cursor/proxy.d.ts +26 -0
- package/dist/providers/cursor/proxy.js +392 -0
- package/dist/providers/index.d.ts +5 -27
- package/dist/providers/index.js +31 -79
- package/dist/providers/store.d.ts +24 -0
- package/dist/providers/store.js +33 -0
- package/dist/run/health.js +10 -12
- package/dist/run/run.js +170 -223
- package/dist/run/summary.js +14 -16
- package/dist/run/wave-loop.d.ts +19 -37
- package/dist/run/wave-loop.js +207 -222
- package/dist/skills/injection.js +26 -41
- package/dist/skills/librarian.js +36 -55
- package/dist/state/state.d.ts +1 -0
- package/dist/state/state.js +154 -253
- package/dist/swarm/agent-run.js +117 -103
- package/dist/swarm/errors.js +9 -13
- package/dist/swarm/merge-autocommit.js +16 -22
- package/dist/swarm/merge-helpers.d.ts +10 -0
- package/dist/swarm/merge-helpers.js +44 -69
- package/dist/swarm/merge.d.ts +2 -2
- package/dist/swarm/merge.js +25 -58
- package/dist/swarm/message-handler.js +10 -13
- package/dist/swarm/swarm.d.ts +27 -26
- package/dist/swarm/swarm.js +48 -40
- package/dist/ui/footer.js +1 -2
- package/dist/ui/header.js +1 -2
- package/dist/ui/input.js +67 -68
- package/dist/ui/overlay.js +1 -2
- package/dist/ui/primitives.d.ts +3 -0
- package/dist/ui/primitives.js +5 -0
- package/dist/ui/run-body.js +32 -32
- package/dist/ui/settings.d.ts +1 -1
- package/dist/ui/steering-body.js +1 -2
- package/dist/ui/summary.js +2 -2
- package/dist/ui/ui.d.ts +1 -0
- package/dist/ui/ui.js +18 -28
- package/package.json +1 -1
- 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
|
|
8
|
-
const pkg =
|
|
9
|
-
console.log(`claude-overnight v${pkg
|
|
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(`
|
package/dist/cli/plan-phase.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
132
|
-
if (
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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 =
|
|
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
|
+
}
|
package/dist/cli/resume.js
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
import { readFileSync } from "fs";
|
|
2
|
-
import { join } from "path";
|
|
3
1
|
import chalk from "chalk";
|
|
4
|
-
import {
|
|
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 {
|
|
10
|
-
import {
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
155
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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,
|
|
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,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");
|
package/dist/cli/settings.d.ts
CHANGED
|
@@ -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 {};
|
package/dist/cli/settings.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { modelDisplayName, formatContextWindow } from "../core/models.js";
|
|
3
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
89
|
-
|
|
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
|
-
|
|
94
|
-
|
|
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) {
|