claude-overnight 1.9.1 → 1.11.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 +2 -2
- package/dist/index.js +113 -9
- 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 +111 -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/swarm.d.ts +2 -0
- package/dist/swarm.js +44 -11
- package/dist/types.d.ts +2 -0
- package/dist/ui.d.ts +45 -4
- package/dist/ui.js +126 -19
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -6,8 +6,8 @@ import chalk from "chalk";
|
|
|
6
6
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
7
7
|
// ── CLI flag parsing ──
|
|
8
8
|
export function parseCliFlags(argv) {
|
|
9
|
-
const known = new Set(["concurrency", "model", "timeout", "budget", "usage-cap", "extra-usage-budget"]);
|
|
10
|
-
const booleans = new Set(["--dry-run", "-h", "--help", "-v", "--version", "--no-flex", "--allow-extra-usage"]);
|
|
9
|
+
const known = new Set(["concurrency", "model", "timeout", "budget", "usage-cap", "extra-usage-budget", "merge", "perm"]);
|
|
10
|
+
const booleans = new Set(["--dry-run", "-h", "--help", "-v", "--version", "--no-flex", "--allow-extra-usage", "--worktrees", "--no-worktrees", "--yolo"]);
|
|
11
11
|
const flags = {};
|
|
12
12
|
const positional = [];
|
|
13
13
|
for (let i = 0; i < argv.length; i++) {
|
package/dist/index.js
CHANGED
|
@@ -42,6 +42,11 @@ async function main() {
|
|
|
42
42
|
--extra-usage-budget=N Max $ for extra usage ${chalk.dim("(implies --allow-extra-usage)")}
|
|
43
43
|
--timeout=SECONDS Agent inactivity timeout ${chalk.dim("(default: 900s, nudges at timeout, kills at 2×)")}
|
|
44
44
|
--no-flex Disable adaptive multi-wave planning ${chalk.dim("(run all tasks in one shot)")}
|
|
45
|
+
--worktrees Force worktree isolation on ${chalk.dim("(default: auto-detect git repo)")}
|
|
46
|
+
--no-worktrees Disable worktree isolation ${chalk.dim("(all agents work in real cwd)")}
|
|
47
|
+
--merge=MODE Merge strategy: yolo or branch ${chalk.dim("(default: yolo)")}
|
|
48
|
+
--perm=MODE Permission mode: auto, bypassPermissions, default ${chalk.dim("(default: auto)")}
|
|
49
|
+
--yolo Shorthand for --perm=bypassPermissions --no-worktrees
|
|
45
50
|
|
|
46
51
|
${chalk.cyan("Defaults")} ${chalk.dim("(non-interactive)")}
|
|
47
52
|
model: first available concurrency: 5 worktrees: auto merge: yolo perms: auto
|
|
@@ -251,6 +256,9 @@ async function main() {
|
|
|
251
256
|
let usageCap;
|
|
252
257
|
let allowExtraUsage = false;
|
|
253
258
|
let extraUsageBudget;
|
|
259
|
+
let permissionMode = "auto";
|
|
260
|
+
let useWorktrees = false;
|
|
261
|
+
let mergeStrategy = "yolo";
|
|
254
262
|
if (resuming) {
|
|
255
263
|
workerModel = resumeState.workerModel;
|
|
256
264
|
plannerModel = resumeState.plannerModel;
|
|
@@ -260,6 +268,9 @@ async function main() {
|
|
|
260
268
|
usageCap = resumeState.usageCap;
|
|
261
269
|
allowExtraUsage = resumeState.allowExtraUsage ?? false;
|
|
262
270
|
extraUsageBudget = resumeState.extraUsageBudget;
|
|
271
|
+
permissionMode = resumeState.permissionMode;
|
|
272
|
+
useWorktrees = resumeState.useWorktrees;
|
|
273
|
+
mergeStrategy = resumeState.mergeStrategy;
|
|
263
274
|
}
|
|
264
275
|
else if (!nonInteractive) {
|
|
265
276
|
while (true) {
|
|
@@ -279,6 +290,17 @@ async function main() {
|
|
|
279
290
|
console.error(chalk.red(` Budget must be a positive number`));
|
|
280
291
|
process.exit(1);
|
|
281
292
|
}
|
|
293
|
+
// ③ Max concurrency (skip if --concurrency set)
|
|
294
|
+
if (cliFlags.concurrency) {
|
|
295
|
+
concurrency = parseInt(cliFlags.concurrency);
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
const defaultC = Math.min(5, budget);
|
|
299
|
+
const concAns = await ask(`\n ${chalk.cyan("③")} ${chalk.dim("Max concurrency")} ${chalk.dim("[")}${chalk.white(String(defaultC))}${chalk.dim("]:")} `);
|
|
300
|
+
concurrency = parseInt(concAns) || defaultC;
|
|
301
|
+
if (concurrency < 1)
|
|
302
|
+
concurrency = 1;
|
|
303
|
+
}
|
|
282
304
|
let modelFrame = 0;
|
|
283
305
|
const modelSpinner = setInterval(() => {
|
|
284
306
|
process.stdout.write(`\x1B[2K\r ${chalk.cyan(BRAILLE[modelFrame++ % BRAILLE.length])} ${chalk.dim("loading models...")}`);
|
|
@@ -293,19 +315,19 @@ async function main() {
|
|
|
293
315
|
}
|
|
294
316
|
plannerModel = models[0]?.value || "claude-sonnet-4-6";
|
|
295
317
|
if (models.length > 0) {
|
|
296
|
-
workerModel = await select(`${chalk.cyan("
|
|
318
|
+
workerModel = await select(`${chalk.cyan("④")} Worker model:`, models.map(m => ({ name: m.displayName, value: m.value, hint: m.description })));
|
|
297
319
|
}
|
|
298
320
|
else {
|
|
299
|
-
const ans = await ask(` ${chalk.cyan("
|
|
321
|
+
const ans = await ask(` ${chalk.cyan("④")} ${chalk.dim("Worker model [claude-sonnet-4-6]:")} `);
|
|
300
322
|
workerModel = ans || "claude-sonnet-4-6";
|
|
301
323
|
}
|
|
302
|
-
usageCap = await select(`${chalk.cyan("
|
|
324
|
+
usageCap = await select(`${chalk.cyan("⑤")} Usage cap:`, [
|
|
303
325
|
{ name: "Unlimited", value: undefined, hint: "full capacity, wait through rate limits" },
|
|
304
326
|
{ name: "90%", value: 0.9, hint: "leave 10% for other work" },
|
|
305
327
|
{ name: "75%", value: 0.75, hint: "conservative, plenty of headroom" },
|
|
306
328
|
{ name: "50%", value: 0.5, hint: "use half, keep the rest" },
|
|
307
329
|
]);
|
|
308
|
-
const extraChoice = await select(`${chalk.cyan("
|
|
330
|
+
const extraChoice = await select(`${chalk.cyan("⑥")} Allow extra usage ${chalk.dim("(billed separately)")}:`, [
|
|
309
331
|
{ name: "No", value: "no", hint: "stop when plan limits are reached" },
|
|
310
332
|
{ name: "Yes, with $ limit", value: "budget", hint: "set a spending cap" },
|
|
311
333
|
{ name: "Yes, unlimited", value: "unlimited", hint: "keep going no matter what" },
|
|
@@ -319,7 +341,44 @@ async function main() {
|
|
|
319
341
|
}
|
|
320
342
|
else if (extraChoice === "unlimited")
|
|
321
343
|
allowExtraUsage = true;
|
|
322
|
-
|
|
344
|
+
// ⑦ Permission mode (skip if --yolo or --perm set)
|
|
345
|
+
const cliYolo = argv.includes("--yolo");
|
|
346
|
+
if (cliFlags.perm) {
|
|
347
|
+
permissionMode = cliFlags.perm;
|
|
348
|
+
}
|
|
349
|
+
else if (cliYolo) {
|
|
350
|
+
permissionMode = "bypassPermissions";
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
permissionMode = await select(`${chalk.cyan("⑦")} Permissions:`, [
|
|
354
|
+
{ name: "Auto", value: "auto", hint: "accept low-risk, reject high-risk" },
|
|
355
|
+
{ name: "Bypass all", value: "bypassPermissions", hint: "agents can run anything (yolo)" },
|
|
356
|
+
{ name: "Prompt each", value: "default", hint: "ask for every dangerous op" },
|
|
357
|
+
]);
|
|
358
|
+
}
|
|
359
|
+
// ⑧ Worktrees + merge (skip if --yolo, --worktrees, --no-worktrees, or --merge set)
|
|
360
|
+
const gitRepo = isGitRepo(cwd);
|
|
361
|
+
if (cliYolo || argv.includes("--no-worktrees")) {
|
|
362
|
+
useWorktrees = false;
|
|
363
|
+
mergeStrategy = cliFlags.merge || "yolo";
|
|
364
|
+
}
|
|
365
|
+
else if (argv.includes("--worktrees")) {
|
|
366
|
+
useWorktrees = true;
|
|
367
|
+
mergeStrategy = cliFlags.merge || "yolo";
|
|
368
|
+
}
|
|
369
|
+
else if (gitRepo) {
|
|
370
|
+
const wtChoice = await select(`${chalk.cyan("⑧")} Git isolation:`, [
|
|
371
|
+
{ name: "Worktrees + yolo merge", value: "wt-yolo", hint: "isolate agents, merge into current branch" },
|
|
372
|
+
{ name: "Worktrees + new branch", value: "wt-branch", hint: "isolate agents, merge into a new branch" },
|
|
373
|
+
{ name: "No worktrees", value: "no-wt", hint: "all agents share the working directory" },
|
|
374
|
+
]);
|
|
375
|
+
useWorktrees = wtChoice !== "no-wt";
|
|
376
|
+
mergeStrategy = wtChoice === "wt-branch" ? "branch" : "yolo";
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
useWorktrees = false;
|
|
380
|
+
mergeStrategy = "yolo";
|
|
381
|
+
}
|
|
323
382
|
const parts = [];
|
|
324
383
|
if (workerModel !== plannerModel)
|
|
325
384
|
parts.push(`${detectModelTier(workerModel)} → ${detectModelTier(plannerModel)}`);
|
|
@@ -331,6 +390,12 @@ async function main() {
|
|
|
331
390
|
if (usageCap != null)
|
|
332
391
|
parts.push(`cap ${Math.round(usageCap * 100)}%`);
|
|
333
392
|
parts.push(allowExtraUsage ? (extraUsageBudget ? `extra $${extraUsageBudget}` : "extra ∞") : "no extra");
|
|
393
|
+
if (permissionMode !== "auto")
|
|
394
|
+
parts.push(permissionMode === "bypassPermissions" ? "yolo" : "prompt");
|
|
395
|
+
if (useWorktrees)
|
|
396
|
+
parts.push(mergeStrategy === "branch" ? "wt→branch" : "wt→yolo");
|
|
397
|
+
else
|
|
398
|
+
parts.push("no wt");
|
|
334
399
|
if (completedRuns.length > 0)
|
|
335
400
|
parts.push(`${completedRuns.length} prior`);
|
|
336
401
|
const inner = parts.join(chalk.dim(" · "));
|
|
@@ -375,11 +440,28 @@ async function main() {
|
|
|
375
440
|
}
|
|
376
441
|
}
|
|
377
442
|
validateConcurrency(concurrency);
|
|
378
|
-
|
|
379
|
-
|
|
443
|
+
// Resolve permissionMode, useWorktrees, mergeStrategy for non-interactive + non-resume
|
|
444
|
+
if (!resuming && nonInteractive) {
|
|
445
|
+
const yolo = argv.includes("--yolo");
|
|
446
|
+
permissionMode = cliFlags.perm ? cliFlags.perm
|
|
447
|
+
: yolo ? "bypassPermissions"
|
|
448
|
+
: (fileCfg?.permissionMode ?? "auto");
|
|
449
|
+
if (!["auto", "bypassPermissions", "default"].includes(permissionMode)) {
|
|
450
|
+
console.error(chalk.red(` --perm must be auto, bypassPermissions, or default (got ${permissionMode})`));
|
|
451
|
+
process.exit(1);
|
|
452
|
+
}
|
|
453
|
+
useWorktrees = argv.includes("--no-worktrees") || yolo ? false
|
|
454
|
+
: argv.includes("--worktrees") ? true
|
|
455
|
+
: (fileCfg?.useWorktrees ?? isGitRepo(cwd));
|
|
456
|
+
mergeStrategy = cliFlags.merge ? cliFlags.merge
|
|
457
|
+
: (fileCfg?.mergeStrategy ?? "yolo");
|
|
458
|
+
if (!["yolo", "branch"].includes(mergeStrategy)) {
|
|
459
|
+
console.error(chalk.red(` --merge must be yolo or branch (got ${mergeStrategy})`));
|
|
460
|
+
process.exit(1);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
380
463
|
if (useWorktrees)
|
|
381
464
|
validateGitRepo(cwd);
|
|
382
|
-
const mergeStrategy = resuming ? resumeState.mergeStrategy : (fileCfg?.mergeStrategy ?? "yolo");
|
|
383
465
|
if (nonInteractive) {
|
|
384
466
|
const capStr = usageCap != null ? ` cap=${Math.round(usageCap * 100)}%` : "";
|
|
385
467
|
const extraStr = allowExtraUsage ? (extraUsageBudget ? ` extra=$${extraUsageBudget}` : " extra=∞") : " extra=off";
|
|
@@ -416,7 +498,7 @@ async function main() {
|
|
|
416
498
|
for (let i = 0; i < themes.length; i++)
|
|
417
499
|
console.log(chalk.dim(` ${String(i + 1).padStart(3)}.`) + ` ${themes[i]}`);
|
|
418
500
|
console.log(chalk.dim(`\n ${thinkingCount} thinking agents → orchestrate → ${(budget ?? 10) - thinkingCount} execution sessions\n`));
|
|
419
|
-
const action = await selectKey(`${chalk.white(`${themes.length} themes`)} ${chalk.dim(`· ${thinkingCount} thinking · ${concurrency} concurrent`)}`, [{ key: "r", desc: "un" }, { key: "e", desc: "dit" }, { key: "q", desc: "uit" }]);
|
|
501
|
+
const action = await selectKey(`${chalk.white(`${themes.length} themes`)} ${chalk.dim(`· ${thinkingCount} thinking · ${concurrency} concurrent`)}`, [{ key: "r", desc: "un" }, { key: "e", desc: "dit" }, { key: "c", desc: "hat" }, { key: "q", desc: "uit" }]);
|
|
420
502
|
if (action === "r") {
|
|
421
503
|
reviewing = false;
|
|
422
504
|
break;
|
|
@@ -435,6 +517,28 @@ async function main() {
|
|
|
435
517
|
}
|
|
436
518
|
planRestore();
|
|
437
519
|
}
|
|
520
|
+
else if (action === "c") {
|
|
521
|
+
const question = await ask(`\n ${chalk.bold("Ask about the themes:")}\n ${chalk.cyan(">")} `);
|
|
522
|
+
if (!question)
|
|
523
|
+
continue;
|
|
524
|
+
process.stdout.write("\x1B[?25l");
|
|
525
|
+
try {
|
|
526
|
+
let answer = "";
|
|
527
|
+
for await (const msg of query({
|
|
528
|
+
prompt: `You're planning work for: "${objective}"\n\nThemes identified:\n${themes.map((t, i) => `${i + 1}. ${t}`).join("\n")}\n\nUser question: ${question}`,
|
|
529
|
+
options: { cwd, model: plannerModel, permissionMode, persistSession: false },
|
|
530
|
+
})) {
|
|
531
|
+
if (msg.type === "result" && msg.subtype === "success")
|
|
532
|
+
answer = msg.result || "";
|
|
533
|
+
}
|
|
534
|
+
planRestore();
|
|
535
|
+
if (answer)
|
|
536
|
+
console.log(chalk.dim(`\n ${answer.slice(0, 500)}\n`));
|
|
537
|
+
}
|
|
538
|
+
catch {
|
|
539
|
+
planRestore();
|
|
540
|
+
}
|
|
541
|
+
}
|
|
438
542
|
else {
|
|
439
543
|
console.log(chalk.dim("\n Aborted.\n"));
|
|
440
544
|
process.exit(0);
|
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,87 @@ 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
|
+
const fixChip = swarm.failed > 0 && swarm.active > 0 ? chalk.yellow(" [f] fix") : "";
|
|
187
|
+
out.push(chalk.dim(" [b] budget [t] threshold [s] steer [?] ask [q] stop") + fixChip + chip);
|
|
188
|
+
}
|
|
185
189
|
out.push("");
|
|
186
190
|
return out.join("\n");
|
|
187
191
|
}
|
|
188
|
-
|
|
192
|
+
function section(out, w, title) {
|
|
193
|
+
const inner = ` ${title} `;
|
|
194
|
+
const dashW = Math.max(3, Math.min(w - 6, 96) - inner.length);
|
|
195
|
+
out.push(chalk.gray(" \u2500\u2500\u2500" + inner + "\u2500".repeat(dashW)));
|
|
196
|
+
}
|
|
197
|
+
function renderSteeringUsageBar(out, w, rl) {
|
|
198
|
+
const rlBarW = Math.min(30, w - 40);
|
|
199
|
+
const draw = (pct, label) => {
|
|
200
|
+
let barStr = "";
|
|
201
|
+
const f = Math.round(pct * rlBarW);
|
|
202
|
+
for (let i = 0; i < rlBarW; i++) {
|
|
203
|
+
if (i < f)
|
|
204
|
+
barStr += pct > 0.9 ? chalk.red("\u2588") : pct > 0.75 ? chalk.yellow("\u2588") : chalk.blue("\u2588");
|
|
205
|
+
else
|
|
206
|
+
barStr += chalk.gray("\u2591");
|
|
207
|
+
}
|
|
208
|
+
let lbl = `${Math.round(pct * 100)}% used`;
|
|
209
|
+
if (rl.isUsingOverage)
|
|
210
|
+
lbl += chalk.red(" [EXTRA USAGE]");
|
|
211
|
+
if (rl.resetsAt && rl.resetsAt > Date.now()) {
|
|
212
|
+
const waitSec = Math.ceil((rl.resetsAt - Date.now()) / 1000);
|
|
213
|
+
const mm = Math.floor(waitSec / 60), ss = waitSec % 60;
|
|
214
|
+
lbl = chalk.red(`Waiting for reset ${mm > 0 ? `${mm}m ${ss}s` : `${ss}s`}`);
|
|
215
|
+
}
|
|
216
|
+
const prefix = label ? chalk.dim(label.padEnd(6)) : chalk.dim("Usage ");
|
|
217
|
+
out.push(` ${prefix}${barStr} ${lbl}`);
|
|
218
|
+
};
|
|
219
|
+
if (rl.windows.size > 1) {
|
|
220
|
+
const wins = Array.from(rl.windows.values());
|
|
221
|
+
const idx = Math.floor(Date.now() / 3000) % wins.length;
|
|
222
|
+
draw(wins[idx].utilization, wins[idx].type.replace(/_/g, " ").slice(0, 5));
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
draw(rl.utilization);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function renderLastWave(out, w, lw) {
|
|
229
|
+
section(out, w, `Wave ${lw.wave + 1} summary`);
|
|
230
|
+
const done = lw.tasks.filter(t => t.status === "done").length;
|
|
231
|
+
const failed = lw.tasks.filter(t => t.status === "error").length;
|
|
232
|
+
const running = lw.tasks.filter(t => t.status === "running").length;
|
|
233
|
+
const parts = [];
|
|
234
|
+
if (done > 0)
|
|
235
|
+
parts.push(chalk.green(`\u2713 ${done} done`));
|
|
236
|
+
if (failed > 0)
|
|
237
|
+
parts.push(chalk.red(`\u2717 ${failed} failed`));
|
|
238
|
+
if (running > 0)
|
|
239
|
+
parts.push(chalk.blue(`~ ${running} running`));
|
|
240
|
+
if (parts.length === 0)
|
|
241
|
+
parts.push(chalk.dim("(no tasks)"));
|
|
242
|
+
out.push(" " + parts.join(" "));
|
|
243
|
+
const show = lw.tasks.slice(0, 5);
|
|
244
|
+
for (const t of show) {
|
|
245
|
+
const icon = t.status === "done" ? chalk.green("\u2713")
|
|
246
|
+
: t.status === "error" ? chalk.red("\u2717")
|
|
247
|
+
: t.status === "running" ? chalk.blue("~")
|
|
248
|
+
: chalk.gray("\u00b7");
|
|
249
|
+
const line = t.prompt.replace(/\n/g, " ");
|
|
250
|
+
out.push(` ${icon} ${chalk.dim(truncate(line, w - 8))}`);
|
|
251
|
+
}
|
|
252
|
+
if (lw.tasks.length > 5)
|
|
253
|
+
out.push(chalk.dim(` \u2026 + ${lw.tasks.length - 5} more`));
|
|
254
|
+
}
|
|
255
|
+
function renderStatusBlock(out, w, status) {
|
|
256
|
+
const lines = status.trim().split("\n").filter(l => l.trim()).slice(0, 6);
|
|
257
|
+
if (lines.length === 0)
|
|
258
|
+
return;
|
|
259
|
+
section(out, w, "Status");
|
|
260
|
+
for (const ln of lines)
|
|
261
|
+
out.push(` ${chalk.dim(truncate(ln.trim(), w - 4))}`);
|
|
262
|
+
}
|
|
263
|
+
export function renderSteeringFrame(runInfo, data, showHotkeys, rlGetter) {
|
|
189
264
|
const w = Math.max((process.stdout.columns ?? 80) || 80, 60);
|
|
190
265
|
const out = [];
|
|
191
266
|
const totalUsed = runInfo.accCompleted + runInfo.accFailed;
|
|
@@ -201,45 +276,43 @@ export function renderSteeringFrame(runInfo, steeringText, showHotkeys, rlGetter
|
|
|
201
276
|
sessionsUsed: totalUsed, sessionsBudget: runInfo.sessionsBudget, remaining: runInfo.remaining,
|
|
202
277
|
});
|
|
203
278
|
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);
|
|
279
|
+
if (rl && (rl.utilization > 0 || rl.windows.size > 0))
|
|
280
|
+
renderSteeringUsageBar(out, w, rl);
|
|
281
|
+
out.push("");
|
|
282
|
+
const ctx = data.context;
|
|
283
|
+
if (ctx?.objective) {
|
|
284
|
+
const obj = ctx.objective.replace(/\s+/g, " ").trim();
|
|
285
|
+
out.push(` ${chalk.bold.white("Objective")} ${chalk.dim(truncate(obj, w - 15))}`);
|
|
286
|
+
out.push("");
|
|
287
|
+
}
|
|
288
|
+
if (ctx?.lastWave && ctx.lastWave.tasks.length > 0) {
|
|
289
|
+
renderLastWave(out, w, ctx.lastWave);
|
|
290
|
+
out.push("");
|
|
291
|
+
}
|
|
292
|
+
if (ctx?.status) {
|
|
293
|
+
renderStatusBlock(out, w, ctx.status);
|
|
294
|
+
out.push("");
|
|
295
|
+
}
|
|
296
|
+
section(out, w, "Planner activity");
|
|
297
|
+
const events = data.events.slice(-10);
|
|
298
|
+
if (events.length === 0) {
|
|
299
|
+
out.push(chalk.dim(" (waiting for planner\u2026)"));
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
for (const e of events) {
|
|
303
|
+
const t = new Date(e.time).toLocaleTimeString("en", { hour12: false });
|
|
304
|
+
out.push(chalk.gray(` ${t} `) + chalk.magenta("[plan] ") + colorEvent(truncate(e.text, w - 22)));
|
|
233
305
|
}
|
|
234
306
|
}
|
|
235
307
|
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}`);
|
|
308
|
+
const liveClean = data.statusLine.replace(/\n/g, " ");
|
|
309
|
+
out.push(` ${chalk.cyan("\u25B6")} ${chalk.dim(truncate(liveClean, w - 6))}`);
|
|
240
310
|
out.push("");
|
|
241
|
-
if (showHotkeys)
|
|
242
|
-
|
|
311
|
+
if (showHotkeys) {
|
|
312
|
+
const pending = runInfo?.pendingSteer ?? 0;
|
|
313
|
+
const chip = pending > 0 ? chalk.cyan(` \u270E ${pending} steer queued`) : "";
|
|
314
|
+
out.push(chalk.dim(" [b] budget [s] steer [q] stop") + chip);
|
|
315
|
+
}
|
|
243
316
|
out.push("");
|
|
244
317
|
return out.join("\n");
|
|
245
318
|
}
|
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/swarm.d.ts
CHANGED
|
@@ -59,6 +59,8 @@ export declare class Swarm {
|
|
|
59
59
|
get pending(): number;
|
|
60
60
|
run(): Promise<void>;
|
|
61
61
|
abort(): void;
|
|
62
|
+
/** Re-queue all errored agents' tasks for retry within this wave. */
|
|
63
|
+
requeueFailed(): number;
|
|
62
64
|
logSequence: number;
|
|
63
65
|
log(agentId: number, text: string): void;
|
|
64
66
|
cleanup(): void;
|
package/dist/swarm.js
CHANGED
|
@@ -110,6 +110,21 @@ export class Swarm {
|
|
|
110
110
|
this.activeQueries.forEach(q => q.close());
|
|
111
111
|
this.activeQueries.clear();
|
|
112
112
|
}
|
|
113
|
+
/** Re-queue all errored agents' tasks for retry within this wave. */
|
|
114
|
+
requeueFailed() {
|
|
115
|
+
const errored = this.agents.filter(a => a.status === "error");
|
|
116
|
+
if (errored.length === 0)
|
|
117
|
+
return 0;
|
|
118
|
+
for (const a of errored) {
|
|
119
|
+
this.queue.push(a.task);
|
|
120
|
+
a.status = "pending";
|
|
121
|
+
a.error = undefined;
|
|
122
|
+
a.finishedAt = undefined;
|
|
123
|
+
}
|
|
124
|
+
this.failed -= errored.length;
|
|
125
|
+
this.log(-1, `Re-queued ${errored.length} failed task(s)`);
|
|
126
|
+
return errored.length;
|
|
127
|
+
}
|
|
113
128
|
logSequence = 0;
|
|
114
129
|
log(agentId, text) {
|
|
115
130
|
const entry = { time: Date.now(), agentId, text };
|
|
@@ -201,21 +216,39 @@ export class Swarm {
|
|
|
201
216
|
this.agents.push(agent);
|
|
202
217
|
let agentCwd = task.cwd || this.config.cwd;
|
|
203
218
|
if (this.config.useWorktrees && this.worktreeBase && !task.noWorktree) {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
219
|
+
const branch = `swarm/task-${id}`;
|
|
220
|
+
const dir = join(this.worktreeBase, `agent-${id}`);
|
|
221
|
+
let worktreeOk = false;
|
|
222
|
+
for (let wt = 0; wt < 2 && !worktreeOk; wt++) {
|
|
223
|
+
try {
|
|
224
|
+
gitExec(`git worktree add -b "${branch}" "${dir}" HEAD`, this.config.cwd);
|
|
225
|
+
worktreeOk = true;
|
|
226
|
+
}
|
|
227
|
+
catch (e) {
|
|
228
|
+
if (wt === 0) {
|
|
229
|
+
this.log(id, `Worktree failed, cleaning up: ${e.message?.slice(0, 50)}`);
|
|
230
|
+
try {
|
|
231
|
+
gitExec(`git branch -D "${branch}"`, this.config.cwd);
|
|
232
|
+
}
|
|
233
|
+
catch { }
|
|
234
|
+
try {
|
|
235
|
+
rmSync(dir, { recursive: true, force: true });
|
|
236
|
+
}
|
|
237
|
+
catch { }
|
|
238
|
+
try {
|
|
239
|
+
gitExec("git worktree prune", this.config.cwd);
|
|
240
|
+
}
|
|
241
|
+
catch { }
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (worktreeOk) {
|
|
208
246
|
agentCwd = dir;
|
|
209
247
|
agent.branch = branch;
|
|
210
248
|
this.log(id, `Worktree: ${branch}`);
|
|
211
249
|
}
|
|
212
|
-
|
|
213
|
-
this.log(id, `Worktree failed
|
|
214
|
-
agent.status = "error";
|
|
215
|
-
agent.error = "worktree creation failed";
|
|
216
|
-
agent.finishedAt = Date.now();
|
|
217
|
-
this.failed++;
|
|
218
|
-
return;
|
|
250
|
+
else {
|
|
251
|
+
this.log(id, `Worktree failed after retry — running without isolation`);
|
|
219
252
|
}
|
|
220
253
|
}
|
|
221
254
|
this.log(id, `Starting: ${task.prompt.slice(0, 60)}`);
|
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,17 @@ export class RunDisplay {
|
|
|
170
266
|
this.inputBuf = "";
|
|
171
267
|
}
|
|
172
268
|
}
|
|
269
|
+
else if ((s === "f" || s === "F") && this.swarm && this.swarm.failed > 0 && this.swarm.active > 0) {
|
|
270
|
+
this.swarm.requeueFailed();
|
|
271
|
+
}
|
|
272
|
+
else if ((s === "s" || s === "S") && this.onSteer) {
|
|
273
|
+
this.inputMode = "steer";
|
|
274
|
+
this.inputBuf = "";
|
|
275
|
+
}
|
|
276
|
+
else if (s === "?" && this.onAsk && this.swarm && !this.askBusy) {
|
|
277
|
+
this.inputMode = "ask";
|
|
278
|
+
this.inputBuf = "";
|
|
279
|
+
}
|
|
173
280
|
else if (s === "q" || s === "Q" || s === "\x03") {
|
|
174
281
|
if (this.swarm) {
|
|
175
282
|
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.11.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": {
|