claude-overnight 1.9.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1 -1
- package/dist/index.js +2 -2
- package/dist/planner-query.d.ts +8 -1
- package/dist/planner-query.js +5 -5
- package/dist/planner.d.ts +1 -1
- package/dist/planner.js +4 -4
- package/dist/render.d.ts +10 -2
- package/dist/render.js +110 -38
- package/dist/run.js +85 -10
- package/dist/state.d.ts +8 -0
- package/dist/state.js +63 -1
- package/dist/steering.d.ts +2 -1
- package/dist/steering.js +5 -3
- package/dist/types.d.ts +2 -0
- package/dist/ui.d.ts +45 -4
- package/dist/ui.js +123 -19
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
1
2
|
import { readFileSync } from "fs";
|
|
2
3
|
import { resolve } from "path";
|
|
3
4
|
import { createInterface } from "readline";
|
|
@@ -233,7 +234,6 @@ export function validateConcurrency(value) {
|
|
|
233
234
|
}
|
|
234
235
|
}
|
|
235
236
|
export function isGitRepo(cwd) {
|
|
236
|
-
const { execSync } = require("child_process");
|
|
237
237
|
try {
|
|
238
238
|
execSync("git rev-parse --git-dir", { cwd, encoding: "utf-8", stdio: "pipe" });
|
|
239
239
|
return true;
|
package/dist/index.js
CHANGED
|
@@ -408,7 +408,7 @@ async function main() {
|
|
|
408
408
|
const thinkingCount = useThinking ? Math.min(Math.max(concurrency, Math.ceil((budget ?? 10) * 0.005)), 10) : 0;
|
|
409
409
|
try {
|
|
410
410
|
if (useThinking) {
|
|
411
|
-
let themes = await identifyThemes(objective, thinkingCount, plannerModel, permissionMode, makeProgressLog());
|
|
411
|
+
let themes = await identifyThemes(objective, thinkingCount, cwd, plannerModel, permissionMode, makeProgressLog());
|
|
412
412
|
process.stdout.write(`\x1B[2K\r ${chalk.green(`✓ ${themes.length} themes`)}\n\n`);
|
|
413
413
|
planRestore();
|
|
414
414
|
let reviewing = true;
|
|
@@ -427,7 +427,7 @@ async function main() {
|
|
|
427
427
|
continue;
|
|
428
428
|
process.stdout.write("\x1B[?25l");
|
|
429
429
|
try {
|
|
430
|
-
themes = await identifyThemes(`${objective}\n\nUser feedback: ${feedback}`, thinkingCount, plannerModel, permissionMode, makeProgressLog());
|
|
430
|
+
themes = await identifyThemes(`${objective}\n\nUser feedback: ${feedback}`, thinkingCount, cwd, plannerModel, permissionMode, makeProgressLog());
|
|
431
431
|
process.stdout.write(`\x1B[2K\r ${chalk.green(`✓ ${themes.length} themes`)}\n\n`);
|
|
432
432
|
}
|
|
433
433
|
catch (err) {
|
package/dist/planner-query.d.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import type { Task, PermMode, RateLimitWindow } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Logging callback used by planner/steering queries.
|
|
4
|
+
* `kind` distinguishes ephemeral status updates (heartbeat ticker) from
|
|
5
|
+
* discrete events worth persisting in a scrollback log (tool uses, retries).
|
|
6
|
+
* Plain (text) callers still work — extra arg is ignored.
|
|
7
|
+
*/
|
|
8
|
+
export type PlannerLog = (text: string, kind?: "status" | "event") => void;
|
|
2
9
|
export interface PlannerRateLimitInfo {
|
|
3
10
|
utilization: number;
|
|
4
11
|
status: string;
|
|
@@ -22,7 +29,7 @@ export declare function detectModelTier(model: string): ModelTier;
|
|
|
22
29
|
export declare function modelCapabilityBlock(model: string): string;
|
|
23
30
|
export declare function getTotalPlannerCost(): number;
|
|
24
31
|
export declare function getPlannerRateLimitInfo(): PlannerRateLimitInfo;
|
|
25
|
-
export declare function runPlannerQuery(prompt: string, opts: PlannerOpts, onLog:
|
|
32
|
+
export declare function runPlannerQuery(prompt: string, opts: PlannerOpts, onLog: PlannerLog): Promise<string>;
|
|
26
33
|
export declare function postProcess(raw: Task[], budget: number | undefined, onLog: (text: string) => void): Task[];
|
|
27
34
|
export declare function attemptJsonParse(text: string): any | null;
|
|
28
35
|
export declare function extractTaskJson(raw: string, retry: () => Promise<string>, onLog?: (text: string) => void, outFile?: string): Promise<{
|
package/dist/planner-query.js
CHANGED
|
@@ -51,18 +51,18 @@ export async function runPlannerQuery(prompt, opts, onLog) {
|
|
|
51
51
|
catch (err) {
|
|
52
52
|
if (err instanceof NudgeError) {
|
|
53
53
|
if (err.sessionId) {
|
|
54
|
-
onLog("Silent 15m — resuming session with continue");
|
|
54
|
+
onLog("Silent 15m — resuming session with continue", "event");
|
|
55
55
|
currentPrompt = "Continue. Complete the task.";
|
|
56
56
|
currentOpts = { ...opts, resumeSessionId: err.sessionId };
|
|
57
57
|
}
|
|
58
58
|
else {
|
|
59
|
-
onLog("Silent 15m — restarting planner (no session to resume)");
|
|
59
|
+
onLog("Silent 15m — restarting planner (no session to resume)", "event");
|
|
60
60
|
}
|
|
61
61
|
continue;
|
|
62
62
|
}
|
|
63
63
|
if (attempt < MAX_RETRIES && isRateLimitError(err)) {
|
|
64
64
|
const waitMs = BACKOFF[attempt];
|
|
65
|
-
onLog(`Rate limited — waiting ${Math.round(waitMs / 1000)}s before retry ${attempt + 1}/${MAX_RETRIES}
|
|
65
|
+
onLog(`Rate limited — waiting ${Math.round(waitMs / 1000)}s before retry ${attempt + 1}/${MAX_RETRIES}`, "event");
|
|
66
66
|
await new Promise((r) => setTimeout(r, waitMs));
|
|
67
67
|
continue;
|
|
68
68
|
}
|
|
@@ -105,7 +105,7 @@ async function runPlannerQueryOnce(prompt, opts, onLog) {
|
|
|
105
105
|
const rlPct = _plannerRateLimitInfo.utilization;
|
|
106
106
|
const rlStr = rlPct > 0 ? ` · ${Math.round(rlPct * 100)}%` : "";
|
|
107
107
|
const extra = lastLogText ? ` · ${lastLogText}` : "";
|
|
108
|
-
onLog(`${timeStr}${toolStr}${costStr}${rlStr}${extra}
|
|
108
|
+
onLog(`${timeStr}${toolStr}${costStr}${rlStr}${extra}`, "status");
|
|
109
109
|
}, 500);
|
|
110
110
|
const timeoutMs = isResume ? HARD_TIMEOUT_MS : NUDGE_MS;
|
|
111
111
|
let sessionId;
|
|
@@ -143,7 +143,7 @@ async function runPlannerQueryOnce(prompt, opts, onLog) {
|
|
|
143
143
|
if (ev?.type === "content_block_start" && ev.content_block?.type === "tool_use") {
|
|
144
144
|
toolCount++;
|
|
145
145
|
lastLogText = ev.content_block.name;
|
|
146
|
-
onLog(ev.content_block.name);
|
|
146
|
+
onLog(ev.content_block.name, "event");
|
|
147
147
|
}
|
|
148
148
|
if (ev?.type === "content_block_delta") {
|
|
149
149
|
const delta = ev.delta;
|
package/dist/planner.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Task, PermMode } from "./types.js";
|
|
2
2
|
export declare const DESIGN_THINKING = "\nHOW TO THINK ABOUT EVERY TASK:\n\nStart from the user's job. What is someone hiring this product to do? \"I need to send money abroad cheaply\" \u2014 not \"I need a currency conversion API.\" Every decision \u2014 what to build, how fast it responds, what happens on error \u2014 flows from the job.\n\nThe experience IS the product. A 200ms server response is not a \"performance metric\" \u2014 it's the difference between an app that feels alive and one that feels broken. A loading state is not \"polish\" \u2014 it's the user knowing the app heard them. An error message is not \"error handling\" \u2014 it's the app being honest. There is no line between backend and UX. The server, the API, the database query, the render \u2014 they're all one experience the user either trusts or doesn't.\n\nBuild the core, verify it works, learn, iterate. Don't plan 20 features and build them all. Build the ONE thing that matters most, run it, see if it actually works from a user's chair. What you learn from seeing it run will change what you build next. Each wave should make what exists better before adding what doesn't exist yet.\n\nConsistency is what makes complex things feel simple. One design system, rigid rules, no exceptions. This is how Revolut ships a super-app with 30+ features that doesn't feel like chaos.\n";
|
|
3
3
|
export declare function planTasks(objective: string, cwd: string, plannerModel: string, workerModel: string, permissionMode: PermMode, budget: number | undefined, concurrency: number, onLog: (text: string) => void, flexNote?: string, outFile?: string): Promise<Task[]>;
|
|
4
|
-
export declare function identifyThemes(objective: string, count: number, model: string, permissionMode: PermMode, onLog?: (text: string) => void): Promise<string[]>;
|
|
4
|
+
export declare function identifyThemes(objective: string, count: number, cwd: string, model: string, permissionMode: PermMode, onLog?: (text: string) => void): Promise<string[]>;
|
|
5
5
|
export declare function buildThinkingTasks(objective: string, themes: string[], designDir: string, plannerModel: string, previousKnowledge?: string): Task[];
|
|
6
6
|
export declare function orchestrate(objective: string, designDocs: string, cwd: string, plannerModel: string, workerModel: string, permissionMode: PermMode, budget: number, concurrency: number, onLog: (text: string) => void, flexNote?: string, outFile?: string): Promise<Task[]>;
|
|
7
7
|
export declare function refinePlan(objective: string, previousTasks: Task[], feedback: string, cwd: string, plannerModel: string, workerModel: string, permissionMode: PermMode, budget: number | undefined, concurrency: number, onLog: (text: string) => void): Promise<Task[]>;
|
package/dist/planner.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { runPlannerQuery, extractTaskJson, postProcess, detectModelTier, modelCapabilityBlock } from "./planner-query.js";
|
|
1
|
+
import { runPlannerQuery, extractTaskJson, attemptJsonParse, postProcess, detectModelTier, modelCapabilityBlock } from "./planner-query.js";
|
|
2
2
|
// The core framing for all planning. Not a checklist — a way of thinking.
|
|
3
3
|
export const DESIGN_THINKING = `
|
|
4
4
|
HOW TO THINK ABOUT EVERY TASK:
|
|
@@ -166,9 +166,9 @@ export async function planTasks(objective, cwd, plannerModel, workerModel, permi
|
|
|
166
166
|
onLog(`${tasks.length} tasks`);
|
|
167
167
|
return tasks;
|
|
168
168
|
}
|
|
169
|
-
export async function identifyThemes(objective, count, model, permissionMode, onLog = () => { }) {
|
|
170
|
-
const resultText = await runPlannerQuery(`Split this objective into exactly ${count} independent research angles for architects exploring a codebase. Each angle should cover a distinct aspect.\n\nObjective: ${objective}\n\nReturn ONLY a JSON object: {"themes": ["angle description", ...]}`, { cwd
|
|
171
|
-
const parsed =
|
|
169
|
+
export async function identifyThemes(objective, count, cwd, model, permissionMode, onLog = () => { }) {
|
|
170
|
+
const resultText = await runPlannerQuery(`Split this objective into exactly ${count} independent research angles for architects exploring a codebase. Each angle should cover a distinct aspect.\n\nObjective: ${objective}\n\nReturn ONLY a JSON object: {"themes": ["angle description", ...]}`, { cwd, model, permissionMode, outputFormat: THEMES_SCHEMA }, onLog);
|
|
171
|
+
const parsed = attemptJsonParse(resultText);
|
|
172
172
|
if (parsed?.themes && Array.isArray(parsed.themes))
|
|
173
173
|
return parsed.themes.slice(0, count);
|
|
174
174
|
const fallback = ["architecture, patterns, and conventions", "data models, state, and persistence", "user-facing flows, components, and UX", "APIs, integrations, and services", "testing, quality, and error handling", "security, performance, and infrastructure", "build, deployment, and configuration", "documentation and developer experience"];
|
package/dist/render.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Swarm } from "./swarm.js";
|
|
2
2
|
import type { RateLimitWindow } from "./types.js";
|
|
3
|
-
import type { RunInfo } from "./ui.js";
|
|
3
|
+
import type { RunInfo, SteeringContext, SteeringEvent } from "./ui.js";
|
|
4
4
|
export declare function truncate(s: string, max: number): string;
|
|
5
5
|
export declare function fmtTokens(n: number): string;
|
|
6
6
|
export declare function fmtDur(ms: number): string;
|
|
@@ -11,6 +11,14 @@ type RLGetter = () => {
|
|
|
11
11
|
resetsAt?: number;
|
|
12
12
|
};
|
|
13
13
|
export declare function renderFrame(swarm: Swarm, showHotkeys: boolean, runInfo?: RunInfo): string;
|
|
14
|
-
export
|
|
14
|
+
export interface SteeringViewData {
|
|
15
|
+
/** The ephemeral ticker heartbeat — elapsed, tool count, cost, current reasoning snippet. */
|
|
16
|
+
statusLine: string;
|
|
17
|
+
/** Persistent scrollback of discrete events (tool uses, retries, nudges). */
|
|
18
|
+
events: SteeringEvent[];
|
|
19
|
+
/** Optional context read from disk at setSteering() time. */
|
|
20
|
+
context?: SteeringContext;
|
|
21
|
+
}
|
|
22
|
+
export declare function renderSteeringFrame(runInfo: RunInfo, data: SteeringViewData, showHotkeys: boolean, rlGetter?: RLGetter): string;
|
|
15
23
|
export declare function renderSummary(swarm: Swarm): string;
|
|
16
24
|
export {};
|
package/dist/render.js
CHANGED
|
@@ -180,12 +180,86 @@ export function renderFrame(swarm, showHotkeys, runInfo) {
|
|
|
180
180
|
const tag = entry.agentId < 0 ? chalk.magenta("[sys]") : chalk.cyan(`[${entry.agentId}]`);
|
|
181
181
|
out.push(chalk.gray(` ${t} `) + tag + ` ${colorEvent(truncate(entry.text, w - 22))}`);
|
|
182
182
|
}
|
|
183
|
-
if (showHotkeys)
|
|
184
|
-
|
|
183
|
+
if (showHotkeys) {
|
|
184
|
+
const pending = runInfo?.pendingSteer ?? 0;
|
|
185
|
+
const chip = pending > 0 ? chalk.cyan(` \u270E ${pending} steer queued`) : "";
|
|
186
|
+
out.push(chalk.dim(" [b] budget [t] threshold [s] steer [?] ask [q] stop") + chip);
|
|
187
|
+
}
|
|
185
188
|
out.push("");
|
|
186
189
|
return out.join("\n");
|
|
187
190
|
}
|
|
188
|
-
|
|
191
|
+
function section(out, w, title) {
|
|
192
|
+
const inner = ` ${title} `;
|
|
193
|
+
const dashW = Math.max(3, Math.min(w - 6, 96) - inner.length);
|
|
194
|
+
out.push(chalk.gray(" \u2500\u2500\u2500" + inner + "\u2500".repeat(dashW)));
|
|
195
|
+
}
|
|
196
|
+
function renderSteeringUsageBar(out, w, rl) {
|
|
197
|
+
const rlBarW = Math.min(30, w - 40);
|
|
198
|
+
const draw = (pct, label) => {
|
|
199
|
+
let barStr = "";
|
|
200
|
+
const f = Math.round(pct * rlBarW);
|
|
201
|
+
for (let i = 0; i < rlBarW; i++) {
|
|
202
|
+
if (i < f)
|
|
203
|
+
barStr += pct > 0.9 ? chalk.red("\u2588") : pct > 0.75 ? chalk.yellow("\u2588") : chalk.blue("\u2588");
|
|
204
|
+
else
|
|
205
|
+
barStr += chalk.gray("\u2591");
|
|
206
|
+
}
|
|
207
|
+
let lbl = `${Math.round(pct * 100)}% used`;
|
|
208
|
+
if (rl.isUsingOverage)
|
|
209
|
+
lbl += chalk.red(" [EXTRA USAGE]");
|
|
210
|
+
if (rl.resetsAt && rl.resetsAt > Date.now()) {
|
|
211
|
+
const waitSec = Math.ceil((rl.resetsAt - Date.now()) / 1000);
|
|
212
|
+
const mm = Math.floor(waitSec / 60), ss = waitSec % 60;
|
|
213
|
+
lbl = chalk.red(`Waiting for reset ${mm > 0 ? `${mm}m ${ss}s` : `${ss}s`}`);
|
|
214
|
+
}
|
|
215
|
+
const prefix = label ? chalk.dim(label.padEnd(6)) : chalk.dim("Usage ");
|
|
216
|
+
out.push(` ${prefix}${barStr} ${lbl}`);
|
|
217
|
+
};
|
|
218
|
+
if (rl.windows.size > 1) {
|
|
219
|
+
const wins = Array.from(rl.windows.values());
|
|
220
|
+
const idx = Math.floor(Date.now() / 3000) % wins.length;
|
|
221
|
+
draw(wins[idx].utilization, wins[idx].type.replace(/_/g, " ").slice(0, 5));
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
draw(rl.utilization);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
function renderLastWave(out, w, lw) {
|
|
228
|
+
section(out, w, `Wave ${lw.wave + 1} summary`);
|
|
229
|
+
const done = lw.tasks.filter(t => t.status === "done").length;
|
|
230
|
+
const failed = lw.tasks.filter(t => t.status === "error").length;
|
|
231
|
+
const running = lw.tasks.filter(t => t.status === "running").length;
|
|
232
|
+
const parts = [];
|
|
233
|
+
if (done > 0)
|
|
234
|
+
parts.push(chalk.green(`\u2713 ${done} done`));
|
|
235
|
+
if (failed > 0)
|
|
236
|
+
parts.push(chalk.red(`\u2717 ${failed} failed`));
|
|
237
|
+
if (running > 0)
|
|
238
|
+
parts.push(chalk.blue(`~ ${running} running`));
|
|
239
|
+
if (parts.length === 0)
|
|
240
|
+
parts.push(chalk.dim("(no tasks)"));
|
|
241
|
+
out.push(" " + parts.join(" "));
|
|
242
|
+
const show = lw.tasks.slice(0, 5);
|
|
243
|
+
for (const t of show) {
|
|
244
|
+
const icon = t.status === "done" ? chalk.green("\u2713")
|
|
245
|
+
: t.status === "error" ? chalk.red("\u2717")
|
|
246
|
+
: t.status === "running" ? chalk.blue("~")
|
|
247
|
+
: chalk.gray("\u00b7");
|
|
248
|
+
const line = t.prompt.replace(/\n/g, " ");
|
|
249
|
+
out.push(` ${icon} ${chalk.dim(truncate(line, w - 8))}`);
|
|
250
|
+
}
|
|
251
|
+
if (lw.tasks.length > 5)
|
|
252
|
+
out.push(chalk.dim(` \u2026 + ${lw.tasks.length - 5} more`));
|
|
253
|
+
}
|
|
254
|
+
function renderStatusBlock(out, w, status) {
|
|
255
|
+
const lines = status.trim().split("\n").filter(l => l.trim()).slice(0, 6);
|
|
256
|
+
if (lines.length === 0)
|
|
257
|
+
return;
|
|
258
|
+
section(out, w, "Status");
|
|
259
|
+
for (const ln of lines)
|
|
260
|
+
out.push(` ${chalk.dim(truncate(ln.trim(), w - 4))}`);
|
|
261
|
+
}
|
|
262
|
+
export function renderSteeringFrame(runInfo, data, showHotkeys, rlGetter) {
|
|
189
263
|
const w = Math.max((process.stdout.columns ?? 80) || 80, 60);
|
|
190
264
|
const out = [];
|
|
191
265
|
const totalUsed = runInfo.accCompleted + runInfo.accFailed;
|
|
@@ -201,45 +275,43 @@ export function renderSteeringFrame(runInfo, steeringText, showHotkeys, rlGetter
|
|
|
201
275
|
sessionsUsed: totalUsed, sessionsBudget: runInfo.sessionsBudget, remaining: runInfo.remaining,
|
|
202
276
|
});
|
|
203
277
|
const rl = rlGetter?.();
|
|
204
|
-
if (rl && (rl.utilization > 0 || rl.windows.size > 0))
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
}
|
|
231
|
-
else {
|
|
232
|
-
renderBar(rl.utilization);
|
|
278
|
+
if (rl && (rl.utilization > 0 || rl.windows.size > 0))
|
|
279
|
+
renderSteeringUsageBar(out, w, rl);
|
|
280
|
+
out.push("");
|
|
281
|
+
const ctx = data.context;
|
|
282
|
+
if (ctx?.objective) {
|
|
283
|
+
const obj = ctx.objective.replace(/\s+/g, " ").trim();
|
|
284
|
+
out.push(` ${chalk.bold.white("Objective")} ${chalk.dim(truncate(obj, w - 15))}`);
|
|
285
|
+
out.push("");
|
|
286
|
+
}
|
|
287
|
+
if (ctx?.lastWave && ctx.lastWave.tasks.length > 0) {
|
|
288
|
+
renderLastWave(out, w, ctx.lastWave);
|
|
289
|
+
out.push("");
|
|
290
|
+
}
|
|
291
|
+
if (ctx?.status) {
|
|
292
|
+
renderStatusBlock(out, w, ctx.status);
|
|
293
|
+
out.push("");
|
|
294
|
+
}
|
|
295
|
+
section(out, w, "Planner activity");
|
|
296
|
+
const events = data.events.slice(-10);
|
|
297
|
+
if (events.length === 0) {
|
|
298
|
+
out.push(chalk.dim(" (waiting for planner\u2026)"));
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
for (const e of events) {
|
|
302
|
+
const t = new Date(e.time).toLocaleTimeString("en", { hour12: false });
|
|
303
|
+
out.push(chalk.gray(` ${t} `) + chalk.magenta("[plan] ") + colorEvent(truncate(e.text, w - 22)));
|
|
233
304
|
}
|
|
234
305
|
}
|
|
235
306
|
out.push("");
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const maxTextW = w - 8;
|
|
239
|
-
out.push(` ${chalk.cyan("\u25C6")} ${clean.length > maxTextW ? clean.slice(0, maxTextW - 1) + "\u2026" : clean}`);
|
|
307
|
+
const liveClean = data.statusLine.replace(/\n/g, " ");
|
|
308
|
+
out.push(` ${chalk.cyan("\u25B6")} ${chalk.dim(truncate(liveClean, w - 6))}`);
|
|
240
309
|
out.push("");
|
|
241
|
-
if (showHotkeys)
|
|
242
|
-
|
|
310
|
+
if (showHotkeys) {
|
|
311
|
+
const pending = runInfo?.pendingSteer ?? 0;
|
|
312
|
+
const chip = pending > 0 ? chalk.cyan(` \u270E ${pending} steer queued`) : "";
|
|
313
|
+
out.push(chalk.dim(" [b] budget [s] steer [q] stop") + chip);
|
|
314
|
+
}
|
|
243
315
|
out.push("");
|
|
244
316
|
return out.join("\n");
|
|
245
317
|
}
|
package/dist/run.js
CHANGED
|
@@ -4,12 +4,12 @@ import { execSync } from "child_process";
|
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import { Swarm } from "./swarm.js";
|
|
6
6
|
import { steerWave } from "./steering.js";
|
|
7
|
-
import { getTotalPlannerCost, getPlannerRateLimitInfo } from "./planner-query.js";
|
|
7
|
+
import { getTotalPlannerCost, getPlannerRateLimitInfo, runPlannerQuery } from "./planner-query.js";
|
|
8
8
|
import { RunDisplay } from "./ui.js";
|
|
9
9
|
import { renderSummary } from "./render.js";
|
|
10
10
|
import { fmtTokens } from "./render.js";
|
|
11
11
|
import { isAuthError } from "./cli.js";
|
|
12
|
-
import { readRunMemory, writeStatus, writeGoalUpdate, saveRunState, saveWaveSession, loadWaveHistory, recordBranches, archiveMilestone, } from "./state.js";
|
|
12
|
+
import { readRunMemory, writeStatus, writeGoalUpdate, saveRunState, saveWaveSession, loadWaveHistory, recordBranches, archiveMilestone, writeSteerInbox, consumeSteerInbox, countSteerInbox, } from "./state.js";
|
|
13
13
|
export async function executeRun(cfg) {
|
|
14
14
|
const restore = () => { try {
|
|
15
15
|
process.stdout.write("\x1B[?25h\n");
|
|
@@ -34,7 +34,7 @@ export async function executeRun(cfg) {
|
|
|
34
34
|
const branches = [];
|
|
35
35
|
if (cfg.resuming && cfg.resumeState) {
|
|
36
36
|
const rs = cfg.resumeState;
|
|
37
|
-
remaining = Math.max(1,
|
|
37
|
+
remaining = Math.max(1, rs.remaining);
|
|
38
38
|
currentTasks = rs.currentTasks;
|
|
39
39
|
waveNum = rs.waveNum;
|
|
40
40
|
accCost = rs.accCost;
|
|
@@ -71,13 +71,79 @@ export async function executeRun(cfg) {
|
|
|
71
71
|
accIn, accOut, accCost, accCompleted, accFailed,
|
|
72
72
|
sessionsBudget: cfg.budget, waveNum, remaining,
|
|
73
73
|
model: workerModel, startedAt: cfg.runStartedAt,
|
|
74
|
+
pendingSteer: countSteerInbox(runDir),
|
|
74
75
|
};
|
|
75
|
-
|
|
76
|
+
let display;
|
|
77
|
+
const onSteer = (text) => {
|
|
78
|
+
try {
|
|
79
|
+
writeSteerInbox(runDir, text);
|
|
80
|
+
runInfoRef.pendingSteer = countSteerInbox(runDir);
|
|
81
|
+
if (currentSwarm)
|
|
82
|
+
currentSwarm.log(-1, `Steer queued: ${text.slice(0, 80)}`);
|
|
83
|
+
}
|
|
84
|
+
catch { }
|
|
85
|
+
};
|
|
86
|
+
let askInFlight = false;
|
|
87
|
+
const onAsk = (question) => {
|
|
88
|
+
if (askInFlight)
|
|
89
|
+
return;
|
|
90
|
+
askInFlight = true;
|
|
91
|
+
display.setAskBusy(true);
|
|
92
|
+
display.setAsk({ question, answer: "", streaming: true });
|
|
93
|
+
void (async () => {
|
|
94
|
+
const plannerCostBefore = getTotalPlannerCost();
|
|
95
|
+
try {
|
|
96
|
+
const memory = readRunMemory(runDir, previousKnowledge || undefined);
|
|
97
|
+
const cap = (s, max) => s && s.length > max ? s.slice(0, max) + "\n...(truncated)" : (s || "");
|
|
98
|
+
const memBlob = [
|
|
99
|
+
objective ? `Objective: ${objective}` : "",
|
|
100
|
+
memory.goal ? `Goal:\n${cap(memory.goal, 1500)}` : "",
|
|
101
|
+
memory.status ? `Current status:\n${cap(memory.status, 2000)}` : "",
|
|
102
|
+
memory.verifications ? `Latest verification:\n${cap(memory.verifications, 1500)}` : "",
|
|
103
|
+
memory.reflections ? `Latest reflections:\n${cap(memory.reflections, 1500)}` : "",
|
|
104
|
+
waveHistory.length ? `Waves completed: ${waveHistory.length}` : "",
|
|
105
|
+
].filter(Boolean).join("\n\n");
|
|
106
|
+
const prompt = `You are answering a user question about an in-progress autonomous agent run. Use the context below; read files in the repo if needed. Answer concisely (a few sentences) and cite files or waves when relevant.\n\n${memBlob}\n\n---\nUser question: ${question}`;
|
|
107
|
+
const answer = await runPlannerQuery(prompt, { cwd, model: plannerModel, permissionMode }, () => { });
|
|
108
|
+
accCost += getTotalPlannerCost() - plannerCostBefore;
|
|
109
|
+
syncRunInfo();
|
|
110
|
+
display.setAsk({ question, answer: answer.trim() || "(no answer)", streaming: false });
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
accCost += getTotalPlannerCost() - plannerCostBefore;
|
|
114
|
+
syncRunInfo();
|
|
115
|
+
display.setAsk({ question, answer: "", streaming: false, error: err?.message?.slice(0, 200) || "ask failed" });
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
askInFlight = false;
|
|
119
|
+
display.setAskBusy(false);
|
|
120
|
+
}
|
|
121
|
+
})();
|
|
122
|
+
};
|
|
123
|
+
display = new RunDisplay(runInfoRef, liveConfig, { onSteer, onAsk });
|
|
76
124
|
const rlGetter = () => {
|
|
77
125
|
const rl = getPlannerRateLimitInfo();
|
|
78
126
|
return { utilization: rl.utilization, isUsingOverage: rl.isUsingOverage, windows: rl.windows, resetsAt: rl.resetsAt };
|
|
79
127
|
};
|
|
80
128
|
const syncRunInfo = () => Object.assign(runInfoRef, { accIn, accOut, accCost, accCompleted, accFailed, waveNum, remaining });
|
|
129
|
+
const buildSteeringContext = () => {
|
|
130
|
+
let status;
|
|
131
|
+
try {
|
|
132
|
+
status = readFileSync(join(runDir, "status.md"), "utf-8");
|
|
133
|
+
}
|
|
134
|
+
catch { }
|
|
135
|
+
return {
|
|
136
|
+
objective: objective || undefined,
|
|
137
|
+
status,
|
|
138
|
+
lastWave: waveHistory[waveHistory.length - 1],
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
const steeringLog = (text, kind) => {
|
|
142
|
+
if (kind === "event")
|
|
143
|
+
display.appendSteeringEvent(text);
|
|
144
|
+
else
|
|
145
|
+
display.updateSteeringStatus(text);
|
|
146
|
+
};
|
|
81
147
|
// For flex + branch strategy: create one target branch
|
|
82
148
|
let runBranch;
|
|
83
149
|
let originalRef;
|
|
@@ -119,7 +185,10 @@ export async function executeRun(cfg) {
|
|
|
119
185
|
const plannerCostBefore = getTotalPlannerCost();
|
|
120
186
|
try {
|
|
121
187
|
const memory = readRunMemory(runDir, previousKnowledge || undefined);
|
|
122
|
-
const
|
|
188
|
+
const appliedGuidance = memory.userGuidance;
|
|
189
|
+
if (appliedGuidance)
|
|
190
|
+
display.appendSteeringEvent(`User directives applied: ${appliedGuidance.slice(0, 80)}`);
|
|
191
|
+
const steer = await steerWave(objective, waveHistory, remaining, cwd, plannerModel, workerModel, permissionMode, concurrency, steeringLog, memory);
|
|
123
192
|
accCost += getTotalPlannerCost() - plannerCostBefore;
|
|
124
193
|
syncRunInfo();
|
|
125
194
|
if (steer.statusUpdate)
|
|
@@ -131,13 +200,18 @@ export async function executeRun(cfg) {
|
|
|
131
200
|
writeFileSync(join(steerDir, `wave-${waveNum}-attempt-${steerAttempts}.json`), JSON.stringify({
|
|
132
201
|
done: steer.done, reasoning: steer.reasoning,
|
|
133
202
|
taskCount: steer.tasks.length, statusUpdate: steer.statusUpdate, goalUpdate: steer.goalUpdate,
|
|
203
|
+
appliedGuidance: appliedGuidance || undefined,
|
|
134
204
|
}, null, 2), "utf-8");
|
|
205
|
+
if (appliedGuidance) {
|
|
206
|
+
consumeSteerInbox(runDir, waveNum);
|
|
207
|
+
runInfoRef.pendingSteer = countSteerInbox(runDir);
|
|
208
|
+
}
|
|
135
209
|
if (waveHistory.length > 0 && waveHistory.length % 5 === 0)
|
|
136
210
|
archiveMilestone(runDir, waveNum);
|
|
137
211
|
if (steer.done || steer.tasks.length === 0) {
|
|
138
212
|
const hasVerification = waveHistory.some(w => w.tasks.some(t => t.prompt.toLowerCase().includes("verif")));
|
|
139
213
|
if (!hasVerification && remaining >= 1) {
|
|
140
|
-
display.
|
|
214
|
+
display.appendSteeringEvent("Done blocked — auto-composing verification wave");
|
|
141
215
|
currentTasks = [{
|
|
142
216
|
id: "verify-0",
|
|
143
217
|
prompt: `## Verification: Build, run, and test the application end-to-end\n\nYou are the final gatekeeper before this run is marked complete. The steerer believes the objective is done. Your job: prove it or disprove it.\n\n1. Run the build (npm run build, or whatever this project uses). Report ALL errors.\n2. Start the dev server. If a port is taken, try another. If a dependency is missing, install it.\n3. Navigate key flows as a real user would. Check that the main features work.\n4. Write your findings to .claude-overnight/latest/verifications/final-verify.md\n\nBe relentless. Do not give up if the first approach fails. Search the codebase for dev login routes, test tokens, seed users, env vars, CLI auth commands, or any bypass.`,
|
|
@@ -159,7 +233,7 @@ export async function executeRun(cfg) {
|
|
|
159
233
|
catch (err) {
|
|
160
234
|
accCost += getTotalPlannerCost() - plannerCostBefore;
|
|
161
235
|
if (steerAttempts < 3) {
|
|
162
|
-
display.
|
|
236
|
+
display.appendSteeringEvent(`Steering failed (attempt ${steerAttempts}/3) — retrying...`);
|
|
163
237
|
continue;
|
|
164
238
|
}
|
|
165
239
|
display.stop();
|
|
@@ -172,7 +246,7 @@ export async function executeRun(cfg) {
|
|
|
172
246
|
};
|
|
173
247
|
// Resume: steer immediately if no queued tasks
|
|
174
248
|
if (cfg.resuming && flex && currentTasks.length === 0 && remaining > 0) {
|
|
175
|
-
display.setSteering(rlGetter);
|
|
249
|
+
display.setSteering(rlGetter, buildSteeringContext());
|
|
176
250
|
display.start();
|
|
177
251
|
await runSteering();
|
|
178
252
|
}
|
|
@@ -215,7 +289,8 @@ export async function executeRun(cfg) {
|
|
|
215
289
|
accFailed += swarm.failed;
|
|
216
290
|
accTools += swarm.agents.reduce((sum, a) => sum + a.toolCalls, 0);
|
|
217
291
|
remaining = Math.max(0, remaining - swarm.completed - swarm.failed);
|
|
218
|
-
const
|
|
292
|
+
const totalConsumed = accCompleted + accFailed + cfg.thinkingUsed;
|
|
293
|
+
const expectedFloor = Math.max(0, cfg.budget - totalConsumed);
|
|
219
294
|
if (remaining < expectedFloor)
|
|
220
295
|
remaining = expectedFloor;
|
|
221
296
|
if (liveConfig.dirty) {
|
|
@@ -243,7 +318,7 @@ export async function executeRun(cfg) {
|
|
|
243
318
|
if (!flex || remaining <= 0 || swarm.aborted || swarm.cappedOut)
|
|
244
319
|
break;
|
|
245
320
|
syncRunInfo();
|
|
246
|
-
display.setSteering(rlGetter);
|
|
321
|
+
display.setSteering(rlGetter, buildSteeringContext());
|
|
247
322
|
display.resume();
|
|
248
323
|
const steered = await runSteering();
|
|
249
324
|
if (!steered)
|
package/dist/state.d.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import type { RunState, BranchRecord, AgentState, RunMemory, WaveSummary } from "./types.js";
|
|
2
2
|
export declare function readMdDir(dir: string): string;
|
|
3
3
|
export declare function readRunMemory(runDir: string, previousRuns?: string): RunMemory;
|
|
4
|
+
/** Read pending .md files in steer-inbox/ (top-level only, not processed/). */
|
|
5
|
+
export declare function readSteerInbox(runDir: string): string;
|
|
6
|
+
/** Count pending steer files without reading them. */
|
|
7
|
+
export declare function countSteerInbox(runDir: string): number;
|
|
8
|
+
/** Append a user directive to the inbox as its own timestamped file. Returns the file path. */
|
|
9
|
+
export declare function writeSteerInbox(runDir: string, text: string): string;
|
|
10
|
+
/** Move all pending .md files from steer-inbox/ into steer-inbox/processed/wave-N/. Returns moved count. */
|
|
11
|
+
export declare function consumeSteerInbox(runDir: string, waveNum: number): number;
|
|
4
12
|
export declare function writeStatus(baseDir: string, status: string): void;
|
|
5
13
|
export declare function writeGoalUpdate(baseDir: string, update: string): void;
|
|
6
14
|
export declare function saveRunState(runDir: string, state: RunState): void;
|
package/dist/state.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync, existsSync, mkdirSync, readdirSync, writeFileSync, symlinkSync, unlinkSync } from "fs";
|
|
1
|
+
import { readFileSync, existsSync, mkdirSync, readdirSync, writeFileSync, symlinkSync, unlinkSync, renameSync } from "fs";
|
|
2
2
|
import { execSync } from "child_process";
|
|
3
3
|
import { join } from "path";
|
|
4
4
|
import chalk from "chalk";
|
|
@@ -31,8 +31,68 @@ export function readRunMemory(runDir, previousRuns) {
|
|
|
31
31
|
verifications: readMdDir(join(runDir, "verifications")),
|
|
32
32
|
milestones: readMdDir(join(runDir, "milestones")),
|
|
33
33
|
status, goal, previousRuns,
|
|
34
|
+
userGuidance: readSteerInbox(runDir),
|
|
34
35
|
};
|
|
35
36
|
}
|
|
37
|
+
// ── Steer inbox (user directives queued for the next steering call) ──
|
|
38
|
+
/** Read pending .md files in steer-inbox/ (top-level only, not processed/). */
|
|
39
|
+
export function readSteerInbox(runDir) {
|
|
40
|
+
const dir = join(runDir, "steer-inbox");
|
|
41
|
+
try {
|
|
42
|
+
const files = readdirSync(dir).filter(f => f.endsWith(".md")).sort();
|
|
43
|
+
return files.map(f => {
|
|
44
|
+
try {
|
|
45
|
+
return readFileSync(join(dir, f), "utf-8").trim();
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return "";
|
|
49
|
+
}
|
|
50
|
+
}).filter(Boolean).join("\n\n---\n\n");
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return "";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Count pending steer files without reading them. */
|
|
57
|
+
export function countSteerInbox(runDir) {
|
|
58
|
+
try {
|
|
59
|
+
return readdirSync(join(runDir, "steer-inbox")).filter(f => f.endsWith(".md")).length;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Append a user directive to the inbox as its own timestamped file. Returns the file path. */
|
|
66
|
+
export function writeSteerInbox(runDir, text) {
|
|
67
|
+
const dir = join(runDir, "steer-inbox");
|
|
68
|
+
mkdirSync(dir, { recursive: true });
|
|
69
|
+
const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
70
|
+
const rand = Math.random().toString(36).slice(2, 6);
|
|
71
|
+
const path = join(dir, `${ts}-${rand}.md`);
|
|
72
|
+
writeFileSync(path, text.trim() + "\n", "utf-8");
|
|
73
|
+
return path;
|
|
74
|
+
}
|
|
75
|
+
/** Move all pending .md files from steer-inbox/ into steer-inbox/processed/wave-N/. Returns moved count. */
|
|
76
|
+
export function consumeSteerInbox(runDir, waveNum) {
|
|
77
|
+
const dir = join(runDir, "steer-inbox");
|
|
78
|
+
let moved = 0;
|
|
79
|
+
try {
|
|
80
|
+
const files = readdirSync(dir).filter(f => f.endsWith(".md"));
|
|
81
|
+
if (files.length === 0)
|
|
82
|
+
return 0;
|
|
83
|
+
const processedDir = join(dir, "processed", `wave-${waveNum}`);
|
|
84
|
+
mkdirSync(processedDir, { recursive: true });
|
|
85
|
+
for (const f of files) {
|
|
86
|
+
try {
|
|
87
|
+
renameSync(join(dir, f), join(processedDir, f));
|
|
88
|
+
moved++;
|
|
89
|
+
}
|
|
90
|
+
catch { }
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch { }
|
|
94
|
+
return moved;
|
|
95
|
+
}
|
|
36
96
|
export function writeStatus(baseDir, status) {
|
|
37
97
|
writeFileSync(join(baseDir, "status.md"), status, "utf-8");
|
|
38
98
|
}
|
|
@@ -98,6 +158,8 @@ export function findOrphanedDesigns(rootDir) {
|
|
|
98
158
|
// ── History display ──
|
|
99
159
|
export function formatTimeAgo(isoStr) {
|
|
100
160
|
const ms = Date.now() - new Date(isoStr).getTime();
|
|
161
|
+
if (!isFinite(ms))
|
|
162
|
+
return "unknown";
|
|
101
163
|
const mins = Math.floor(ms / 60000);
|
|
102
164
|
if (mins < 1)
|
|
103
165
|
return "just now";
|
package/dist/steering.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
import type { PermMode, SteerResult, RunMemory, WaveSummary } from "./types.js";
|
|
2
|
-
|
|
2
|
+
import { type PlannerLog } from "./planner-query.js";
|
|
3
|
+
export declare function steerWave(objective: string, history: WaveSummary[], remainingBudget: number, cwd: string, plannerModel: string, workerModel: string, permissionMode: PermMode, concurrency: number, onLog: PlannerLog, runMemory?: RunMemory): Promise<SteerResult>;
|
package/dist/steering.js
CHANGED
|
@@ -40,8 +40,9 @@ export async function steerWave(objective, history, remainingBudget, cwd, planne
|
|
|
40
40
|
const verificationBlock = runMemory?.verifications ? `\nVerification results (from actually running the app):\n${cap(runMemory.verifications, 3000)}\n` : "";
|
|
41
41
|
const goalBlock = runMemory?.goal ? `\nNorth star — what "amazing" means:\n${runMemory.goal}\n` : "";
|
|
42
42
|
const prevRunBlock = runMemory?.previousRuns ? `\nKnowledge from previous runs:\n${cap(runMemory.previousRuns, 3000)}\n` : "";
|
|
43
|
+
const guidanceBlock = runMemory?.userGuidance ? `\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nUSER DIRECTIVES — highest priority\nThese come directly from the user running this session. They override prior assumptions about status, goal, and next steps. Incorporate them into the wave you compose below. If they conflict with earlier decisions, the user wins. Reflect the new direction in statusUpdate so future waves remember.\n\n${cap(runMemory.userGuidance, 4000)}\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n` : "";
|
|
43
44
|
const prompt = `You are the quality director for an autonomous multi-wave agent system. Your job is to push the work toward "amazing," not just "done."
|
|
44
|
-
|
|
45
|
+
${guidanceBlock}
|
|
45
46
|
Objective: ${objective}
|
|
46
47
|
${goalBlock}${statusBlock}${milestoneBlock}${prevRunBlock}
|
|
47
48
|
Recent waves:
|
|
@@ -104,13 +105,14 @@ The "model" field on each task: use "worker" (${workerModel}) for implementation
|
|
|
104
105
|
Set "noWorktree": true for verify/user-test tasks — they need the real project directory with env files, dependencies, and local config.
|
|
105
106
|
|
|
106
107
|
If done: {"done": true, "reasoning": "...", "statusUpdate": "...", "tasks": []}`;
|
|
107
|
-
onLog("Assessing...");
|
|
108
|
+
onLog("Assessing...", "status");
|
|
109
|
+
onLog(`Reading codebase — wave ${history.length + 1}`, "event");
|
|
108
110
|
const resultText = await runPlannerQuery(prompt, { cwd, model: plannerModel, permissionMode, outputFormat: STEER_SCHEMA }, onLog);
|
|
109
111
|
const parsed = await (async () => {
|
|
110
112
|
const first = attemptJsonParse(resultText);
|
|
111
113
|
if (first)
|
|
112
114
|
return first;
|
|
113
|
-
onLog(`Steering parse failed (${resultText.length} chars). Asking model to fix
|
|
115
|
+
onLog(`Steering parse failed (${resultText.length} chars). Asking model to fix...`, "event");
|
|
114
116
|
const snippet = resultText.length > 2000 ? resultText.slice(0, 1000) + "\n...\n" + resultText.slice(-800) : resultText;
|
|
115
117
|
const retryText = await runPlannerQuery(`Your previous steering response could not be parsed as JSON. Here is what you returned:\n\n---\n${snippet}\n---\n\nExtract or rewrite the above as ONLY a valid JSON object with this schema: {"done":boolean,"reasoning":"...","statusUpdate":"...","tasks":[{"prompt":"..."}]}\n\nRespond with ONLY the JSON, no markdown fences, no explanation.`, { cwd, model: plannerModel, permissionMode, outputFormat: STEER_SCHEMA }, onLog);
|
|
116
118
|
const retryParsed = attemptJsonParse(retryText);
|
package/dist/types.d.ts
CHANGED
|
@@ -144,6 +144,8 @@ export interface RunMemory {
|
|
|
144
144
|
status: string;
|
|
145
145
|
goal: string;
|
|
146
146
|
previousRuns?: string;
|
|
147
|
+
/** Pending user directives from the steer inbox, consumed by the next successful steering call. */
|
|
148
|
+
userGuidance?: string;
|
|
147
149
|
}
|
|
148
150
|
/** Persisted run state for crash recovery and resume. */
|
|
149
151
|
export interface RunState {
|
package/dist/ui.d.ts
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import type { Swarm } from "./swarm.js";
|
|
2
|
-
import type { RateLimitWindow } from "./types.js";
|
|
2
|
+
import type { RateLimitWindow, WaveSummary } from "./types.js";
|
|
3
|
+
/** Short-lived context the steering view renders around its live log. */
|
|
4
|
+
export interface SteeringContext {
|
|
5
|
+
objective?: string;
|
|
6
|
+
status?: string;
|
|
7
|
+
lastWave?: WaveSummary;
|
|
8
|
+
}
|
|
9
|
+
/** One scrollback line in the steering event log. */
|
|
10
|
+
export interface SteeringEvent {
|
|
11
|
+
time: number;
|
|
12
|
+
text: string;
|
|
13
|
+
}
|
|
3
14
|
/** Cumulative run-level stats — mutable, updated between phases. */
|
|
4
15
|
export interface RunInfo {
|
|
5
16
|
accIn: number;
|
|
@@ -12,6 +23,8 @@ export interface RunInfo {
|
|
|
12
23
|
remaining: number;
|
|
13
24
|
model?: string;
|
|
14
25
|
startedAt: number;
|
|
26
|
+
/** Number of pending directives in the steer inbox; displayed as a chip in the hotkey row. */
|
|
27
|
+
pendingSteer?: number;
|
|
15
28
|
}
|
|
16
29
|
/** Mutable config that can be changed live during execution. */
|
|
17
30
|
export interface LiveConfig {
|
|
@@ -19,6 +32,13 @@ export interface LiveConfig {
|
|
|
19
32
|
usageCap: number | undefined;
|
|
20
33
|
dirty: boolean;
|
|
21
34
|
}
|
|
35
|
+
/** State of an in-flight or recently-completed ask side query. */
|
|
36
|
+
export interface AskState {
|
|
37
|
+
question: string;
|
|
38
|
+
answer: string;
|
|
39
|
+
streaming: boolean;
|
|
40
|
+
error?: string;
|
|
41
|
+
}
|
|
22
42
|
type RLGetter = () => {
|
|
23
43
|
utilization: number;
|
|
24
44
|
isUsingOverage: boolean;
|
|
@@ -29,7 +49,10 @@ export declare class RunDisplay {
|
|
|
29
49
|
readonly runInfo: RunInfo;
|
|
30
50
|
private liveConfig?;
|
|
31
51
|
private swarm?;
|
|
32
|
-
private
|
|
52
|
+
private steeringActive;
|
|
53
|
+
private steeringStatusLine;
|
|
54
|
+
private steeringEvents;
|
|
55
|
+
private steeringContext?;
|
|
33
56
|
private rlGetter?;
|
|
34
57
|
private interval?;
|
|
35
58
|
private keyHandler?;
|
|
@@ -39,16 +62,34 @@ export declare class RunDisplay {
|
|
|
39
62
|
private readonly isTTY;
|
|
40
63
|
private lastSeq;
|
|
41
64
|
private lastCompleted;
|
|
42
|
-
|
|
65
|
+
private askState?;
|
|
66
|
+
private askBusy;
|
|
67
|
+
private onSteer?;
|
|
68
|
+
private onAsk?;
|
|
69
|
+
constructor(runInfo: RunInfo, liveConfig?: LiveConfig, callbacks?: {
|
|
70
|
+
onSteer?: (text: string) => void;
|
|
71
|
+
onAsk?: (text: string) => void;
|
|
72
|
+
});
|
|
73
|
+
/** Replace the ask state. Called by run.ts as the side query streams and completes. */
|
|
74
|
+
setAsk(state: AskState | undefined): void;
|
|
75
|
+
/** Signal to the UI whether an ask is in progress (prevents duplicate firings). */
|
|
76
|
+
setAskBusy(busy: boolean): void;
|
|
43
77
|
start(): void;
|
|
44
78
|
setWave(swarm: Swarm): void;
|
|
45
|
-
setSteering(rlGetter?: RLGetter): void;
|
|
79
|
+
setSteering(rlGetter?: RLGetter, ctx?: SteeringContext): void;
|
|
80
|
+
/** Replace the single live status line (ticker heartbeat). */
|
|
81
|
+
updateSteeringStatus(text: string): void;
|
|
82
|
+
/** Append a discrete, persistent line to the steering scrollback. */
|
|
83
|
+
appendSteeringEvent(text: string): void;
|
|
84
|
+
/** Backwards-compat alias — treats input as the current status line. */
|
|
46
85
|
updateText(text: string): void;
|
|
47
86
|
pause(): void;
|
|
48
87
|
resume(): void;
|
|
49
88
|
stop(): void;
|
|
50
89
|
private resumeInterval;
|
|
51
90
|
private render;
|
|
91
|
+
private renderInputPrompt;
|
|
92
|
+
private renderAskPanel;
|
|
52
93
|
private hasHotkeys;
|
|
53
94
|
private setupHotkeys;
|
|
54
95
|
private plainTick;
|
package/dist/ui.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { renderFrame, renderSteeringFrame } from "./render.js";
|
|
3
|
+
const MAX_STEERING_EVENTS = 60;
|
|
4
|
+
const MAX_INPUT_LEN = 600;
|
|
3
5
|
export class RunDisplay {
|
|
4
6
|
runInfo;
|
|
5
7
|
liveConfig;
|
|
6
8
|
swarm;
|
|
7
|
-
|
|
9
|
+
steeringActive = false;
|
|
10
|
+
steeringStatusLine = "Assessing...";
|
|
11
|
+
steeringEvents = [];
|
|
12
|
+
steeringContext;
|
|
8
13
|
rlGetter;
|
|
9
14
|
interval;
|
|
10
15
|
keyHandler;
|
|
@@ -14,11 +19,21 @@ export class RunDisplay {
|
|
|
14
19
|
isTTY;
|
|
15
20
|
lastSeq = 0;
|
|
16
21
|
lastCompleted = -1;
|
|
17
|
-
|
|
22
|
+
askState;
|
|
23
|
+
askBusy = false;
|
|
24
|
+
onSteer;
|
|
25
|
+
onAsk;
|
|
26
|
+
constructor(runInfo, liveConfig, callbacks) {
|
|
18
27
|
this.runInfo = runInfo;
|
|
19
28
|
this.liveConfig = liveConfig;
|
|
29
|
+
this.onSteer = callbacks?.onSteer;
|
|
30
|
+
this.onAsk = callbacks?.onAsk;
|
|
20
31
|
this.isTTY = !!process.stdout.isTTY;
|
|
21
32
|
}
|
|
33
|
+
/** Replace the ask state. Called by run.ts as the side query streams and completes. */
|
|
34
|
+
setAsk(state) { this.askState = state; }
|
|
35
|
+
/** Signal to the UI whether an ask is in progress (prevents duplicate firings). */
|
|
36
|
+
setAskBusy(busy) { this.askBusy = busy; }
|
|
22
37
|
start() {
|
|
23
38
|
if (this.started)
|
|
24
39
|
return;
|
|
@@ -28,17 +43,29 @@ export class RunDisplay {
|
|
|
28
43
|
}
|
|
29
44
|
setWave(swarm) {
|
|
30
45
|
this.swarm = swarm;
|
|
31
|
-
this.
|
|
46
|
+
this.steeringActive = false;
|
|
32
47
|
this.rlGetter = undefined;
|
|
33
48
|
this.lastSeq = 0;
|
|
34
49
|
this.lastCompleted = -1;
|
|
35
50
|
}
|
|
36
|
-
setSteering(rlGetter) {
|
|
51
|
+
setSteering(rlGetter, ctx) {
|
|
37
52
|
this.swarm = undefined;
|
|
38
|
-
this.
|
|
53
|
+
this.steeringActive = true;
|
|
54
|
+
this.steeringStatusLine = "Assessing...";
|
|
55
|
+
this.steeringEvents = [];
|
|
56
|
+
this.steeringContext = ctx;
|
|
39
57
|
this.rlGetter = rlGetter;
|
|
40
58
|
}
|
|
41
|
-
|
|
59
|
+
/** Replace the single live status line (ticker heartbeat). */
|
|
60
|
+
updateSteeringStatus(text) { this.steeringStatusLine = text; }
|
|
61
|
+
/** Append a discrete, persistent line to the steering scrollback. */
|
|
62
|
+
appendSteeringEvent(text) {
|
|
63
|
+
this.steeringEvents.push({ time: Date.now(), text });
|
|
64
|
+
if (this.steeringEvents.length > MAX_STEERING_EVENTS)
|
|
65
|
+
this.steeringEvents.shift();
|
|
66
|
+
}
|
|
67
|
+
/** Backwards-compat alias — treats input as the current status line. */
|
|
68
|
+
updateText(text) { this.updateSteeringStatus(text); }
|
|
42
69
|
pause() {
|
|
43
70
|
if (this.interval) {
|
|
44
71
|
clearInterval(this.interval);
|
|
@@ -96,23 +123,61 @@ export class RunDisplay {
|
|
|
96
123
|
}, 250);
|
|
97
124
|
}
|
|
98
125
|
render() {
|
|
126
|
+
let frame = "";
|
|
99
127
|
if (this.swarm) {
|
|
100
|
-
|
|
101
|
-
if (this.inputMode !== "none") {
|
|
102
|
-
const label = this.inputMode === "budget" ? "New budget (remaining sessions)" : "New usage cap (0-100%)";
|
|
103
|
-
frame += `\n ${chalk.cyan(">")} ${label}: ${this.inputBuf}\u2588`;
|
|
104
|
-
}
|
|
105
|
-
return frame;
|
|
128
|
+
frame = renderFrame(this.swarm, this.hasHotkeys(), this.runInfo);
|
|
106
129
|
}
|
|
107
|
-
if (this.
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
130
|
+
else if (this.steeringActive) {
|
|
131
|
+
frame = renderSteeringFrame(this.runInfo, {
|
|
132
|
+
statusLine: this.steeringStatusLine,
|
|
133
|
+
events: this.steeringEvents,
|
|
134
|
+
context: this.steeringContext,
|
|
135
|
+
}, this.hasHotkeys(), this.rlGetter);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
return "";
|
|
139
|
+
}
|
|
140
|
+
frame += this.renderInputPrompt();
|
|
141
|
+
frame += this.renderAskPanel();
|
|
142
|
+
return frame;
|
|
143
|
+
}
|
|
144
|
+
renderInputPrompt() {
|
|
145
|
+
if (this.inputMode === "none")
|
|
146
|
+
return "";
|
|
147
|
+
if (this.inputMode === "budget") {
|
|
148
|
+
return `\n ${chalk.cyan(">")} New budget (remaining sessions): ${this.inputBuf}\u2588`;
|
|
149
|
+
}
|
|
150
|
+
if (this.inputMode === "threshold") {
|
|
151
|
+
return `\n ${chalk.cyan(">")} New usage cap (0-100%): ${this.inputBuf}\u2588`;
|
|
152
|
+
}
|
|
153
|
+
if (this.inputMode === "steer") {
|
|
154
|
+
return `\n ${chalk.cyan(">")} ${chalk.bold("Steer next wave")} ${chalk.dim("(Enter to queue, Esc to cancel)")}\n ${this.inputBuf}\u2588`;
|
|
155
|
+
}
|
|
156
|
+
if (this.inputMode === "ask") {
|
|
157
|
+
return `\n ${chalk.cyan(">")} ${chalk.bold("Ask the planner")} ${chalk.dim("(Enter to send, Esc to cancel)")}\n ${this.inputBuf}\u2588`;
|
|
113
158
|
}
|
|
114
159
|
return "";
|
|
115
160
|
}
|
|
161
|
+
renderAskPanel() {
|
|
162
|
+
const a = this.askState;
|
|
163
|
+
if (!a)
|
|
164
|
+
return "";
|
|
165
|
+
const out = ["", chalk.gray(" \u2500\u2500\u2500 Ask " + "\u2500".repeat(40))];
|
|
166
|
+
out.push(` ${chalk.bold.cyan("Q:")} ${a.question}`);
|
|
167
|
+
if (a.error) {
|
|
168
|
+
out.push(` ${chalk.red("A:")} ${chalk.red(a.error)}`);
|
|
169
|
+
}
|
|
170
|
+
else if (a.streaming) {
|
|
171
|
+
out.push(` ${chalk.dim("A: " + (a.answer || "thinking..."))}`);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
const lines = a.answer.split("\n").slice(0, 20);
|
|
175
|
+
out.push(` ${chalk.bold.green("A:")} ${lines[0] || ""}`);
|
|
176
|
+
for (const ln of lines.slice(1))
|
|
177
|
+
out.push(` ${ln}`);
|
|
178
|
+
}
|
|
179
|
+
return "\n" + out.join("\n");
|
|
180
|
+
}
|
|
116
181
|
hasHotkeys() {
|
|
117
182
|
return !!this.liveConfig && !!process.stdin.isTTY;
|
|
118
183
|
}
|
|
@@ -129,7 +194,7 @@ export class RunDisplay {
|
|
|
129
194
|
const lc = this.liveConfig;
|
|
130
195
|
this.keyHandler = (buf) => {
|
|
131
196
|
const s = buf.toString();
|
|
132
|
-
if (this.inputMode
|
|
197
|
+
if (this.inputMode === "budget" || this.inputMode === "threshold") {
|
|
133
198
|
if (s === "\r" || s === "\n") {
|
|
134
199
|
const val = parseFloat(this.inputBuf);
|
|
135
200
|
if (this.inputMode === "budget" && !isNaN(val) && val > 0) {
|
|
@@ -160,6 +225,37 @@ export class RunDisplay {
|
|
|
160
225
|
}
|
|
161
226
|
return;
|
|
162
227
|
}
|
|
228
|
+
if (this.inputMode === "steer" || this.inputMode === "ask") {
|
|
229
|
+
for (const ch of s) {
|
|
230
|
+
if (ch === "\r" || ch === "\n") {
|
|
231
|
+
const text = this.inputBuf.trim();
|
|
232
|
+
const wasAsk = this.inputMode === "ask";
|
|
233
|
+
this.inputMode = "none";
|
|
234
|
+
this.inputBuf = "";
|
|
235
|
+
if (text) {
|
|
236
|
+
if (wasAsk)
|
|
237
|
+
this.onAsk?.(text);
|
|
238
|
+
else
|
|
239
|
+
this.onSteer?.(text);
|
|
240
|
+
}
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (ch === "\x1B" || ch === "\x03") {
|
|
244
|
+
this.inputMode = "none";
|
|
245
|
+
this.inputBuf = "";
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (ch === "\x7F" || ch === "\b") {
|
|
249
|
+
this.inputBuf = this.inputBuf.slice(0, -1);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
const code = ch.charCodeAt(0);
|
|
253
|
+
if (code >= 0x20 && code <= 0x7E && this.inputBuf.length < MAX_INPUT_LEN) {
|
|
254
|
+
this.inputBuf += ch;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
163
259
|
if (s === "b" || s === "B") {
|
|
164
260
|
this.inputMode = "budget";
|
|
165
261
|
this.inputBuf = "";
|
|
@@ -170,6 +266,14 @@ export class RunDisplay {
|
|
|
170
266
|
this.inputBuf = "";
|
|
171
267
|
}
|
|
172
268
|
}
|
|
269
|
+
else if ((s === "s" || s === "S") && this.onSteer) {
|
|
270
|
+
this.inputMode = "steer";
|
|
271
|
+
this.inputBuf = "";
|
|
272
|
+
}
|
|
273
|
+
else if (s === "?" && this.onAsk && this.swarm && !this.askBusy) {
|
|
274
|
+
this.inputMode = "ask";
|
|
275
|
+
this.inputBuf = "";
|
|
276
|
+
}
|
|
173
277
|
else if (s === "q" || s === "Q" || s === "\x03") {
|
|
174
278
|
if (this.swarm) {
|
|
175
279
|
if (this.swarm.aborted)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-overnight",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"description": "Run 10, 100, or 1000 Claude agents overnight. Parallel autonomous AI coding with thinking waves, iterative quality steering, crash recovery, and rate limit handling. Built on the Claude Agent SDK.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|