claude-overnight 1.9.1 → 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/planner-query.d.ts +8 -1
- package/dist/planner-query.js +5 -5
- package/dist/render.d.ts +10 -2
- package/dist/render.js +110 -38
- package/dist/run.js +82 -8
- package/dist/state.d.ts +8 -0
- package/dist/state.js +61 -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/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/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");
|
|
@@ -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
|
}
|
|
@@ -244,7 +318,7 @@ export async function executeRun(cfg) {
|
|
|
244
318
|
if (!flex || remaining <= 0 || swarm.aborted || swarm.cappedOut)
|
|
245
319
|
break;
|
|
246
320
|
syncRunInfo();
|
|
247
|
-
display.setSteering(rlGetter);
|
|
321
|
+
display.setSteering(rlGetter, buildSteeringContext());
|
|
248
322
|
display.resume();
|
|
249
323
|
const steered = await runSteering();
|
|
250
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
|
}
|
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": {
|