claude-overnight 1.25.43 → 1.25.45
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/bin.js +1 -1
- package/dist/{cli.d.ts → cli/cli.d.ts} +14 -4
- package/dist/{cli.js → cli/cli.js} +20 -6
- package/dist/cli/help.d.ts +2 -0
- package/dist/cli/help.js +44 -0
- package/dist/cli/plan-phase.d.ts +38 -0
- package/dist/cli/plan-phase.js +290 -0
- package/dist/cli/preflight.d.ts +15 -0
- package/dist/cli/preflight.js +139 -0
- package/dist/cli/resume.d.ts +27 -0
- package/dist/cli/resume.js +312 -0
- package/dist/{settings.d.ts → cli/settings.d.ts} +1 -2
- package/dist/{settings.js → cli/settings.js} +2 -13
- package/dist/core/_version.d.ts +1 -0
- package/dist/{_version.js → core/_version.js} +1 -1
- package/dist/core/auth.d.ts +20 -0
- package/dist/core/auth.js +20 -0
- package/dist/core/jwt-signer.d.ts +59 -0
- package/dist/core/jwt-signer.js +122 -0
- package/dist/core/key-vault.d.ts +11 -0
- package/dist/core/key-vault.js +63 -0
- package/dist/core/rate-limiter.d.ts +38 -0
- package/dist/core/rate-limiter.js +99 -0
- package/dist/core/secret-manager.d.ts +8 -0
- package/dist/core/secret-manager.js +51 -0
- package/dist/core/token-cache.d.ts +56 -0
- package/dist/core/token-cache.js +125 -0
- package/dist/core/token-manager.d.ts +32 -0
- package/dist/core/token-manager.js +127 -0
- package/dist/{types.d.ts → core/types.d.ts} +0 -12
- package/dist/index.js +51 -776
- package/dist/middleware/index.d.ts +1 -0
- package/dist/middleware/index.js +1 -0
- package/dist/middleware/rate-limit.d.ts +49 -0
- package/dist/middleware/rate-limit.js +65 -0
- package/dist/planner/coach/coach.d.ts +15 -0
- package/dist/planner/coach/coach.js +207 -0
- package/dist/planner/coach/context.d.ts +23 -0
- package/dist/planner/coach/context.js +154 -0
- package/dist/planner/coach/schema.d.ts +111 -0
- package/dist/planner/coach/schema.js +101 -0
- package/dist/planner/coach/settings.d.ts +8 -0
- package/dist/planner/coach/settings.js +24 -0
- package/dist/planner/json.d.ts +4 -0
- package/dist/planner/json.js +106 -0
- package/dist/{planner.d.ts → planner/planner.d.ts} +5 -5
- package/dist/{planner.js → planner/planner.js} +35 -19
- package/dist/planner/postprocess.d.ts +2 -0
- package/dist/planner/postprocess.js +106 -0
- package/dist/planner/query.d.ts +28 -0
- package/dist/{planner-query.js → planner/query.js} +47 -332
- package/dist/{steering.d.ts → planner/steering.d.ts} +3 -3
- package/dist/{steering.js → planner/steering.js} +7 -7
- package/dist/planner/throttle.d.ts +48 -0
- package/dist/planner/throttle.js +87 -0
- package/dist/providers/cursor-env.d.ts +53 -0
- package/dist/providers/cursor-env.js +283 -0
- package/dist/providers/cursor-picker.d.ts +7 -0
- package/dist/providers/cursor-picker.js +306 -0
- package/dist/providers/cursor-proxy.d.ts +26 -0
- package/dist/providers/cursor-proxy.js +459 -0
- package/dist/providers/index.d.ts +70 -0
- package/dist/providers/index.js +324 -0
- package/dist/run/budget.d.ts +6 -0
- package/dist/run/budget.js +33 -0
- package/dist/run/health.d.ts +5 -0
- package/dist/run/health.js +71 -0
- package/dist/run/review.d.ts +24 -0
- package/dist/run/review.js +32 -0
- package/dist/{run.d.ts → run/run.d.ts} +2 -2
- package/dist/run/run.js +576 -0
- package/dist/run/summary.d.ts +38 -0
- package/dist/run/summary.js +174 -0
- package/dist/run/throttle.d.ts +21 -0
- package/dist/run/throttle.js +48 -0
- package/dist/run/wave-loop.d.ts +71 -0
- package/dist/run/wave-loop.js +443 -0
- package/dist/{state.d.ts → state/state.d.ts} +1 -1
- package/dist/{state.js → state/state.js} +4 -4
- package/dist/swarm/agent-run.d.ts +34 -0
- package/dist/swarm/agent-run.js +465 -0
- package/dist/swarm/config.d.ts +31 -0
- package/dist/swarm/config.js +35 -0
- package/dist/swarm/errors.d.ts +6 -0
- package/dist/swarm/errors.js +40 -0
- package/dist/swarm/merge-autocommit.d.ts +1 -0
- package/dist/swarm/merge-autocommit.js +93 -0
- package/dist/swarm/merge-helpers.d.ts +15 -0
- package/dist/swarm/merge-helpers.js +175 -0
- package/dist/{merge.d.ts → swarm/merge.d.ts} +4 -17
- package/dist/swarm/merge.js +180 -0
- package/dist/swarm/message-handler.d.ts +35 -0
- package/dist/swarm/message-handler.js +195 -0
- package/dist/{swarm.d.ts → swarm/swarm.d.ts} +35 -50
- package/dist/swarm/swarm.js +470 -0
- package/dist/ui/bars.d.ts +12 -0
- package/dist/ui/bars.js +183 -0
- package/dist/ui/footer-state.d.ts +12 -0
- package/dist/ui/footer-state.js +93 -0
- package/dist/ui/footer.d.ts +11 -0
- package/dist/ui/footer.js +40 -0
- package/dist/ui/header.d.ts +14 -0
- package/dist/ui/header.js +65 -0
- package/dist/ui/input.d.ts +10 -0
- package/dist/ui/input.js +246 -0
- package/dist/ui/overlay.d.ts +11 -0
- package/dist/ui/overlay.js +42 -0
- package/dist/ui/primitives.d.ts +33 -0
- package/dist/ui/primitives.js +111 -0
- package/dist/ui/run-body.d.ts +6 -0
- package/dist/ui/run-body.js +169 -0
- package/dist/ui/settings.d.ts +13 -0
- package/dist/ui/settings.js +112 -0
- package/dist/ui/shell.d.ts +8 -0
- package/dist/ui/shell.js +34 -0
- package/dist/ui/steering-body.d.ts +13 -0
- package/dist/ui/steering-body.js +101 -0
- package/dist/ui/store.d.ts +66 -0
- package/dist/ui/store.js +44 -0
- package/dist/ui/summary.d.ts +2 -0
- package/dist/ui/summary.js +71 -0
- package/dist/ui/types.d.ts +50 -0
- package/dist/ui/types.js +4 -0
- package/dist/ui/ui.d.ts +46 -0
- package/dist/ui/ui.js +260 -0
- package/docs/CURSOR_PROXY.md +63 -0
- package/package.json +7 -4
- package/plugins/claude-overnight/.claude-plugin/plugin.json +1 -1
- package/plugins/lsp-first/.claude-plugin/plugin.json +18 -0
- package/plugins/lsp-first/README.md +38 -0
- package/plugins/lsp-first/hooks/lsp-first-guard.js +112 -0
- package/dist/_version.d.ts +0 -1
- package/dist/auth.d.ts +0 -19
- package/dist/auth.js +0 -82
- package/dist/coach.d.ts +0 -49
- package/dist/coach.js +0 -498
- package/dist/interactive-panel.d.ts +0 -42
- package/dist/interactive-panel.js +0 -169
- package/dist/merge.js +0 -442
- package/dist/planner-query.d.ts +0 -60
- package/dist/providers.d.ts +0 -159
- package/dist/providers.js +0 -1442
- package/dist/render.d.ts +0 -79
- package/dist/render.js +0 -768
- package/dist/run.js +0 -1257
- package/dist/swarm.js +0 -1059
- package/dist/test-coach.d.ts +0 -1
- package/dist/ui.d.ts +0 -162
- package/dist/ui.js +0 -1011
- /package/dist/{cursor-models.d.ts → core/cursor-models.d.ts} +0 -0
- /package/dist/{cursor-models.js → core/cursor-models.js} +0 -0
- /package/dist/{models.d.ts → core/models.d.ts} +0 -0
- /package/dist/{models.js → core/models.js} +0 -0
- /package/dist/{proxy-port.d.ts → core/proxy-port.d.ts} +0 -0
- /package/dist/{proxy-port.js → core/proxy-port.js} +0 -0
- /package/dist/{transcripts.d.ts → core/transcripts.d.ts} +0 -0
- /package/dist/{transcripts.js → core/transcripts.js} +0 -0
- /package/dist/{turns.d.ts → core/turns.d.ts} +0 -0
- /package/dist/{turns.js → core/turns.js} +0 -0
- /package/dist/{types.js → core/types.js} +0 -0
package/dist/bin.js
CHANGED
|
@@ -30,7 +30,7 @@ if (process.stdout.isTTY && !quiet && !process.env.CLAUDE_OVERNIGHT_UPDATED) {
|
|
|
30
30
|
writeFileSync(tsFile, String(Date.now())); // stamp first so failures don't re-trigger
|
|
31
31
|
const { execFileSync, spawnSync } = await import("node:child_process");
|
|
32
32
|
const latest = execFileSync("npm", ["show", "claude-overnight", "version"], { encoding: "utf-8", timeout: 6000 }).trim();
|
|
33
|
-
const { VERSION } = await import("./_version.js");
|
|
33
|
+
const { VERSION } = await import("./core/_version.js");
|
|
34
34
|
if (latest !== VERSION) {
|
|
35
35
|
process.stdout.write(`\r\x1b[2K 🌙 claude-overnight \x1b[33m${VERSION} → ${latest}\x1b[0m updating…\n`);
|
|
36
36
|
execFileSync("npm", ["i", "-g", `claude-overnight@${latest}`], { stdio: "inherit", timeout: 60000 });
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { ModelInfo } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
-
import type { Task,
|
|
2
|
+
import type { Task, MergeStrategy } from "../core/types.js";
|
|
3
3
|
export declare function parseCliFlags(argv: string[]): {
|
|
4
4
|
flags: Record<string, string>;
|
|
5
5
|
positional: string[];
|
|
6
6
|
};
|
|
7
|
-
import { isJWTAuthError } from "
|
|
7
|
+
import { isJWTAuthError } from "../core/auth.js";
|
|
8
8
|
/** @deprecated Use isJWTAuthError from auth.ts instead. */
|
|
9
9
|
export declare const isAuthError: typeof isJWTAuthError;
|
|
10
10
|
export { isJWTAuthError };
|
|
@@ -51,7 +51,6 @@ export interface FileArgs {
|
|
|
51
51
|
objective?: string;
|
|
52
52
|
concurrency?: number;
|
|
53
53
|
model?: string;
|
|
54
|
-
permissionMode?: PermMode;
|
|
55
54
|
cwd?: string;
|
|
56
55
|
allowedTools?: string[];
|
|
57
56
|
beforeWave?: string | string[];
|
|
@@ -68,4 +67,15 @@ export declare function isGitRepo(cwd: string): boolean;
|
|
|
68
67
|
export declare function validateGitRepo(cwd: string): void;
|
|
69
68
|
export declare function showPlan(tasks: Task[]): void;
|
|
70
69
|
export declare const BRAILLE: readonly ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
71
|
-
|
|
70
|
+
/** Dual-mode progress renderer.
|
|
71
|
+
*
|
|
72
|
+
* - `status` (default): transient single-line ticker — clears itself each frame.
|
|
73
|
+
* Use for elapsed-time / cost / rolling tail of model text.
|
|
74
|
+
* - `event`: permanent log line — scrolls up, ticker redraws underneath.
|
|
75
|
+
* Use for tool calls and notable state changes.
|
|
76
|
+
*
|
|
77
|
+
* The two modes cooperate: an event clears the current ticker, writes the
|
|
78
|
+
* event on its own line, and the next status tick redraws the ticker below.
|
|
79
|
+
* That gives the user a visible history of what the planner did, with a live
|
|
80
|
+
* "now" indicator that always stays pinned at the bottom. */
|
|
81
|
+
export declare function makeProgressLog(): (text: string, kind?: "status" | "event") => void;
|
|
@@ -6,7 +6,7 @@ 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", "merge"
|
|
9
|
+
const known = new Set(["concurrency", "model", "timeout", "budget", "usage-cap", "extra-usage-budget", "merge"]);
|
|
10
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 = [];
|
|
@@ -30,7 +30,7 @@ export function parseCliFlags(argv) {
|
|
|
30
30
|
return { flags, positional };
|
|
31
31
|
}
|
|
32
32
|
// ── Auth error detection (re-exported from auth module for backward compatibility) ──
|
|
33
|
-
import { isJWTAuthError } from "
|
|
33
|
+
import { isJWTAuthError } from "../core/auth.js";
|
|
34
34
|
/** @deprecated Use isJWTAuthError from auth.ts instead. */
|
|
35
35
|
export const isAuthError = isJWTAuthError;
|
|
36
36
|
export { isJWTAuthError };
|
|
@@ -332,7 +332,7 @@ export async function selectKey(label, options) {
|
|
|
332
332
|
});
|
|
333
333
|
}
|
|
334
334
|
const KNOWN_TASK_FILE_KEYS = new Set([
|
|
335
|
-
"tasks", "objective", "concurrency", "cwd", "model", "
|
|
335
|
+
"tasks", "objective", "concurrency", "cwd", "model", "allowedTools", "beforeWave", "afterWave", "afterRun", "worktrees", "mergeStrategy", "usageCap", "flexiblePlan",
|
|
336
336
|
]);
|
|
337
337
|
export function loadTaskFile(file) {
|
|
338
338
|
const path = resolve(file);
|
|
@@ -392,7 +392,6 @@ export function loadTaskFile(file) {
|
|
|
392
392
|
concurrency: parsed.concurrency,
|
|
393
393
|
model: parsed.model,
|
|
394
394
|
cwd: parsed.cwd ? resolve(parsed.cwd) : undefined,
|
|
395
|
-
permissionMode: parsed.permissionMode,
|
|
396
395
|
allowedTools: parsed.allowedTools,
|
|
397
396
|
beforeWave: parsed.beforeWave,
|
|
398
397
|
afterWave: parsed.afterWave,
|
|
@@ -437,13 +436,28 @@ export function showPlan(tasks) {
|
|
|
437
436
|
console.log(chalk.dim(` ${"─".repeat(ruleLen)}\n`));
|
|
438
437
|
}
|
|
439
438
|
export const BRAILLE = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
439
|
+
/** Dual-mode progress renderer.
|
|
440
|
+
*
|
|
441
|
+
* - `status` (default): transient single-line ticker — clears itself each frame.
|
|
442
|
+
* Use for elapsed-time / cost / rolling tail of model text.
|
|
443
|
+
* - `event`: permanent log line — scrolls up, ticker redraws underneath.
|
|
444
|
+
* Use for tool calls and notable state changes.
|
|
445
|
+
*
|
|
446
|
+
* The two modes cooperate: an event clears the current ticker, writes the
|
|
447
|
+
* event on its own line, and the next status tick redraws the ticker below.
|
|
448
|
+
* That gives the user a visible history of what the planner did, with a live
|
|
449
|
+
* "now" indicator that always stays pinned at the bottom. */
|
|
440
450
|
export function makeProgressLog() {
|
|
441
451
|
let frame = 0;
|
|
442
|
-
return (text) => {
|
|
443
|
-
const spin = chalk.cyan(BRAILLE[frame++ % BRAILLE.length]);
|
|
452
|
+
return (text, kind = "status") => {
|
|
444
453
|
const maxW = (process.stdout.columns ?? 80) - 6;
|
|
445
454
|
const clean = text.replace(/\n/g, " ");
|
|
446
455
|
const line = clean.length > maxW ? clean.slice(0, maxW - 1) + "\u2026" : clean;
|
|
456
|
+
if (kind === "event") {
|
|
457
|
+
process.stdout.write(`\x1B[2K\r ${chalk.cyan("›")} ${chalk.dim(line)}\n`);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
const spin = chalk.cyan(BRAILLE[frame++ % BRAILLE.length]);
|
|
447
461
|
process.stdout.write(`\x1B[2K\r ${spin} ${chalk.dim(line)}`);
|
|
448
462
|
};
|
|
449
463
|
}
|
package/dist/cli/help.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
import { dirname, join } from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { VERSION } from "../core/_version.js";
|
|
6
|
+
export function printVersion() {
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, "..", "..", "package.json"), "utf-8"));
|
|
9
|
+
console.log(`claude-overnight v${pkg.version}`);
|
|
10
|
+
}
|
|
11
|
+
export function printHelp() {
|
|
12
|
+
console.log(`
|
|
13
|
+
${chalk.bold("🌙 claude-overnight")} ${chalk.dim("v" + VERSION + " -- background lane for your Claude Max plan")}
|
|
14
|
+
${chalk.dim("─".repeat(60))}
|
|
15
|
+
|
|
16
|
+
${chalk.cyan("Usage")}
|
|
17
|
+
claude-overnight ${chalk.dim("interactive mode")}
|
|
18
|
+
claude-overnight tasks.json ${chalk.dim("task file mode")}
|
|
19
|
+
claude-overnight "fix auth" "add tests" ${chalk.dim("inline tasks")}
|
|
20
|
+
|
|
21
|
+
${chalk.cyan("Flags")}
|
|
22
|
+
-h, --help Show this help
|
|
23
|
+
-v, --version Print version
|
|
24
|
+
--dry-run Show planned tasks without running them
|
|
25
|
+
--budget=N Target number of agent runs ${chalk.dim("(default: 10)")}
|
|
26
|
+
--concurrency=N Max parallel agents ${chalk.dim("(default: 5)")}
|
|
27
|
+
--model=NAME Worker model override ${chalk.dim("(interactive mode picks planner + worker separately -- supports 'Other…' for Qwen / OpenRouter / etc.)")}
|
|
28
|
+
--fast-model=NAME Fast worker model for quick tasks ${chalk.dim("(optional -- checked by next wave's workers)")}
|
|
29
|
+
--usage-cap=N Stop at N% utilization ${chalk.dim("(e.g. 90 to save 10% for other work)")}
|
|
30
|
+
--allow-extra-usage Allow extra/overage usage ${chalk.dim("(default: stop when plan limits hit)")}
|
|
31
|
+
--extra-usage-budget=N Max $ for extra usage ${chalk.dim("(implies --allow-extra-usage)")}
|
|
32
|
+
--timeout=SECONDS Agent inactivity timeout ${chalk.dim("(default: 900s, nudges at timeout, kills at 2×)")}
|
|
33
|
+
--no-flex Disable adaptive multi-wave planning ${chalk.dim("(run all tasks in one shot)")}
|
|
34
|
+
--worktrees Force worktree isolation on ${chalk.dim("(default: auto-detect git repo)")}
|
|
35
|
+
--no-worktrees Disable worktree isolation ${chalk.dim("(all agents work in real cwd)")}
|
|
36
|
+
--merge=MODE Merge strategy: yolo or branch ${chalk.dim("(default: yolo)")}
|
|
37
|
+
--yolo Shorthand for --no-worktrees
|
|
38
|
+
--no-coach Skip the setup coach ${chalk.dim("(raw objective, no preflight rewrite)")}
|
|
39
|
+
--coach-model Re-pick coach model ${chalk.dim("(overrides saved choice)")}
|
|
40
|
+
|
|
41
|
+
${chalk.cyan("Defaults")} ${chalk.dim("(non-interactive)")}
|
|
42
|
+
model: first available concurrency: 5 worktrees: auto merge: yolo
|
|
43
|
+
`);
|
|
44
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ProviderConfig, EnvResolver } from "../providers/index.js";
|
|
2
|
+
import type { Task, MergeStrategy, WaveSummary } from "../core/types.js";
|
|
3
|
+
export interface PlanPhaseInput {
|
|
4
|
+
objective: string | undefined;
|
|
5
|
+
noTTY: boolean;
|
|
6
|
+
flex: boolean;
|
|
7
|
+
budget: number | undefined;
|
|
8
|
+
concurrency: number;
|
|
9
|
+
cwd: string;
|
|
10
|
+
plannerModel: string;
|
|
11
|
+
workerModel: string;
|
|
12
|
+
fastModel: string | undefined;
|
|
13
|
+
plannerProvider: ProviderConfig | undefined;
|
|
14
|
+
workerProvider: ProviderConfig | undefined;
|
|
15
|
+
fastProvider: ProviderConfig | undefined;
|
|
16
|
+
usageCap: number | undefined;
|
|
17
|
+
allowExtraUsage: boolean;
|
|
18
|
+
extraUsageBudget: number | undefined;
|
|
19
|
+
useWorktrees: boolean;
|
|
20
|
+
mergeStrategy: MergeStrategy;
|
|
21
|
+
agentTimeoutMs: number | undefined;
|
|
22
|
+
runDir: string;
|
|
23
|
+
designDir: string;
|
|
24
|
+
previousKnowledge: string;
|
|
25
|
+
envForModel: EnvResolver;
|
|
26
|
+
coachedOriginal: string | undefined;
|
|
27
|
+
coachedAt: number | undefined;
|
|
28
|
+
}
|
|
29
|
+
export interface PlanPhaseResult {
|
|
30
|
+
tasks: Task[];
|
|
31
|
+
thinkingHistory?: WaveSummary;
|
|
32
|
+
thinkingUsed: number;
|
|
33
|
+
thinkingCost: number;
|
|
34
|
+
thinkingIn: number;
|
|
35
|
+
thinkingOut: number;
|
|
36
|
+
thinkingTools: number;
|
|
37
|
+
}
|
|
38
|
+
export declare function runPlanPhase(input: PlanPhaseInput): Promise<PlanPhaseResult>;
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, mkdirSync, writeFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
5
|
+
import { Swarm } from "../swarm/swarm.js";
|
|
6
|
+
import { identifyThemes, buildThinkingTasks, orchestrate, planTasks, refinePlan } from "../planner/planner.js";
|
|
7
|
+
import { RunDisplay } from "../ui/ui.js";
|
|
8
|
+
import { renderSummary } from "../ui/summary.js";
|
|
9
|
+
import { isCursorProxyProvider } from "../providers/index.js";
|
|
10
|
+
import { readMdDir, saveRunState } from "../state/state.js";
|
|
11
|
+
import { selectKey, ask, showPlan, makeProgressLog, isJWTAuthError } from "./cli.js";
|
|
12
|
+
export async function runPlanPhase(input) {
|
|
13
|
+
const { objective, noTTY, flex, budget, concurrency, cwd, plannerModel, workerModel, fastModel, plannerProvider, workerProvider, fastProvider, usageCap, allowExtraUsage, extraUsageBudget, useWorktrees, mergeStrategy, agentTimeoutMs, runDir, designDir, previousKnowledge, envForModel, coachedOriginal, coachedAt, } = input;
|
|
14
|
+
let tasks = [];
|
|
15
|
+
let thinkingHistory;
|
|
16
|
+
let thinkingUsed = 0, thinkingCost = 0, thinkingIn = 0, thinkingOut = 0, thinkingTools = 0;
|
|
17
|
+
// Persist an early planning-phase state so the run is visible to the resume
|
|
18
|
+
// picker even if orchestrate dies before executeRun gets a chance to run.
|
|
19
|
+
// Without this, a crashed plan phase leaves no run.json and the run vanishes
|
|
20
|
+
// from findIncompleteRuns -- you pay for orchestration and can't see it.
|
|
21
|
+
if (objective) {
|
|
22
|
+
try {
|
|
23
|
+
saveRunState(runDir, {
|
|
24
|
+
id: runDir.split(/[/\\]/).pop() ?? "",
|
|
25
|
+
objective, budget: budget ?? 10, remaining: budget ?? 10,
|
|
26
|
+
workerModel, plannerModel, fastModel,
|
|
27
|
+
workerProviderId: workerProvider?.id, plannerProviderId: plannerProvider?.id,
|
|
28
|
+
fastProviderId: fastProvider?.id,
|
|
29
|
+
concurrency,
|
|
30
|
+
usageCap, allowExtraUsage, extraUsageBudget,
|
|
31
|
+
flex, useWorktrees, mergeStrategy,
|
|
32
|
+
waveNum: 0, currentTasks: [],
|
|
33
|
+
accCost: 0, accCompleted: 0, accFailed: 0,
|
|
34
|
+
accIn: 0, accOut: 0, accTools: 0,
|
|
35
|
+
branches: [],
|
|
36
|
+
phase: "planning",
|
|
37
|
+
startedAt: new Date().toISOString(),
|
|
38
|
+
cwd,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
catch { }
|
|
42
|
+
}
|
|
43
|
+
if (noTTY) {
|
|
44
|
+
console.error(chalk.red(" No tasks provided and stdin is not a TTY."));
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
process.stdout.write("\x1B[?25l");
|
|
48
|
+
const planRestore = () => process.stdout.write("\x1B[?25h");
|
|
49
|
+
const useThinking = flex && (budget ?? 10) > concurrency * 3;
|
|
50
|
+
const thinkingCount = useThinking ? Math.min(Math.max(concurrency, Math.ceil((budget ?? 10) * 0.005)), 10) : 0;
|
|
51
|
+
try {
|
|
52
|
+
if (useThinking) {
|
|
53
|
+
// Persist themes as a Markdown doc so a planning-phase crash leaves a
|
|
54
|
+
// readable record (and a future resume can skip identifyThemes).
|
|
55
|
+
const saveThemesMd = (list) => {
|
|
56
|
+
try {
|
|
57
|
+
writeFileSync(join(runDir, "themes.md"), `# Themes\n\n**Objective:** ${objective}\n\n${list.map((t, i) => `${i + 1}. ${t}`).join("\n")}\n`, "utf-8");
|
|
58
|
+
}
|
|
59
|
+
catch { }
|
|
60
|
+
};
|
|
61
|
+
let themes = await identifyThemes(objective, thinkingCount, cwd, plannerModel, makeProgressLog(), "themes");
|
|
62
|
+
saveThemesMd(themes);
|
|
63
|
+
process.stdout.write(`\x1B[2K\r ${chalk.green(`✓ ${themes.length} themes`)}\n\n`);
|
|
64
|
+
planRestore();
|
|
65
|
+
let reviewing = true;
|
|
66
|
+
while (reviewing) {
|
|
67
|
+
for (let i = 0; i < themes.length; i++)
|
|
68
|
+
console.log(chalk.dim(` ${String(i + 1).padStart(3)}.`) + ` ${themes[i]}`);
|
|
69
|
+
console.log(chalk.dim(`\n ${thinkingCount} thinking agents → orchestrate → ${(budget ?? 10) - thinkingCount} execution sessions\n`));
|
|
70
|
+
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" }]);
|
|
71
|
+
if (action === "r") {
|
|
72
|
+
reviewing = false;
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
if (action === "e") {
|
|
76
|
+
const feedback = await ask(`\n ${chalk.bold("What should change?")}\n ${chalk.cyan(">")} `);
|
|
77
|
+
if (!feedback)
|
|
78
|
+
continue;
|
|
79
|
+
process.stdout.write("\x1B[?25l");
|
|
80
|
+
try {
|
|
81
|
+
themes = await identifyThemes(`${objective}\n\nUser feedback: ${feedback}`, thinkingCount, cwd, plannerModel, makeProgressLog(), "themes-refine");
|
|
82
|
+
saveThemesMd(themes);
|
|
83
|
+
process.stdout.write(`\x1B[2K\r ${chalk.green(`✓ ${themes.length} themes`)}\n\n`);
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
console.error(chalk.red(`\n Re-planning failed: ${err.message}\n`));
|
|
87
|
+
}
|
|
88
|
+
planRestore();
|
|
89
|
+
}
|
|
90
|
+
else if (action === "c") {
|
|
91
|
+
const question = await ask(`\n ${chalk.bold("Ask about the themes:")}\n ${chalk.cyan(">")} `);
|
|
92
|
+
if (!question)
|
|
93
|
+
continue;
|
|
94
|
+
process.stdout.write("\x1B[?25l");
|
|
95
|
+
try {
|
|
96
|
+
let answer = "";
|
|
97
|
+
const plannerEnv = envForModel(plannerModel);
|
|
98
|
+
for await (const msg of query({
|
|
99
|
+
prompt: `You're planning work for: "${objective}"\n\nThemes identified:\n${themes.map((t, i) => `${i + 1}. ${t}`).join("\n")}\n\nUser question: ${question}`,
|
|
100
|
+
options: { cwd, model: plannerModel, permissionMode: "bypassPermissions", allowDangerouslySkipPermissions: true, persistSession: false, ...(plannerEnv && { env: plannerEnv }) },
|
|
101
|
+
})) {
|
|
102
|
+
if (msg.type === "result" && msg.subtype === "success")
|
|
103
|
+
answer = msg.result || "";
|
|
104
|
+
}
|
|
105
|
+
planRestore();
|
|
106
|
+
if (answer)
|
|
107
|
+
console.log(chalk.dim(`\n ${answer.slice(0, 500)}\n`));
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
planRestore();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
console.log(chalk.dim("\n Aborted.\n"));
|
|
115
|
+
process.exit(0);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
process.stdout.write("\x1B[?25l");
|
|
119
|
+
mkdirSync(designDir, { recursive: true });
|
|
120
|
+
const existingDesigns = readMdDir(designDir);
|
|
121
|
+
if (existingDesigns) {
|
|
122
|
+
const designFiles = readdirSync(designDir).filter(f => f.endsWith(".md")).sort();
|
|
123
|
+
console.log(chalk.green(`\n ✓ Reusing ${designFiles.length} design docs`) + chalk.dim(` (from prior attempt)`));
|
|
124
|
+
for (const f of designFiles) {
|
|
125
|
+
try {
|
|
126
|
+
const firstLine = readFileSync(join(designDir, f), "utf-8").split("\n")[0].replace(/^#+\s*/, "").trim();
|
|
127
|
+
if (firstLine)
|
|
128
|
+
console.log(chalk.dim(` ${firstLine.slice(0, 80)}`));
|
|
129
|
+
}
|
|
130
|
+
catch { }
|
|
131
|
+
}
|
|
132
|
+
console.log("");
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
const researchModel = fastModel ? workerModel : plannerModel;
|
|
136
|
+
const thinkingTasks = buildThinkingTasks(objective, themes, designDir, researchModel, previousKnowledge || undefined);
|
|
137
|
+
console.log(chalk.cyan(`\n ◆ Thinking: ${thinkingTasks.length} agents exploring...\n`));
|
|
138
|
+
const thinkingSwarm = new Swarm({
|
|
139
|
+
tasks: thinkingTasks, concurrency, cwd, model: researchModel,
|
|
140
|
+
useWorktrees: false, mergeStrategy: "yolo", agentTimeoutMs, usageCap, allowExtraUsage, extraUsageBudget,
|
|
141
|
+
envForModel,
|
|
142
|
+
cursorProxy: [plannerProvider, workerProvider, fastProvider].some(p => p && isCursorProxyProvider(p)),
|
|
143
|
+
});
|
|
144
|
+
const thinkRunInfo = { accIn: 0, accOut: 0, accCost: 0, accCompleted: 0, accFailed: 0, sessionsBudget: budget ?? 10, waveNum: -1, remaining: budget ?? 10, model: researchModel, startedAt: Date.now() };
|
|
145
|
+
const thinkDisplay = new RunDisplay(thinkRunInfo, { remaining: 0, usageCap, concurrency, paused: false, dirty: false });
|
|
146
|
+
thinkDisplay.setWave(thinkingSwarm);
|
|
147
|
+
thinkDisplay.start();
|
|
148
|
+
// Save thinking-wave state on every exit path (normal, abort, double-q).
|
|
149
|
+
const saveThinkingState = () => {
|
|
150
|
+
thinkingUsed = thinkingSwarm.completed + thinkingSwarm.failed;
|
|
151
|
+
thinkingCost = thinkingSwarm.totalCostUsd;
|
|
152
|
+
thinkingIn = thinkingSwarm.totalInputTokens;
|
|
153
|
+
thinkingOut = thinkingSwarm.totalOutputTokens;
|
|
154
|
+
thinkingTools = thinkingSwarm.agents.reduce((sum, a) => sum + a.toolCalls, 0);
|
|
155
|
+
try {
|
|
156
|
+
saveRunState(runDir, {
|
|
157
|
+
id: runDir.split(/[/\\]/).pop() ?? "",
|
|
158
|
+
objective: objective, budget: budget ?? 10, remaining: (budget ?? 10) - thinkingUsed,
|
|
159
|
+
workerModel, plannerModel, fastModel,
|
|
160
|
+
workerProviderId: workerProvider?.id, plannerProviderId: plannerProvider?.id,
|
|
161
|
+
fastProviderId: fastProvider?.id,
|
|
162
|
+
concurrency,
|
|
163
|
+
usageCap, allowExtraUsage, extraUsageBudget,
|
|
164
|
+
flex, useWorktrees, mergeStrategy,
|
|
165
|
+
waveNum: 0, currentTasks: [],
|
|
166
|
+
accCost: thinkingCost, accCompleted: thinkingUsed, accFailed: 0,
|
|
167
|
+
accIn: thinkingIn, accOut: thinkingOut, accTools: thinkingTools,
|
|
168
|
+
branches: [],
|
|
169
|
+
phase: "planning",
|
|
170
|
+
startedAt: new Date().toISOString(),
|
|
171
|
+
cwd,
|
|
172
|
+
coachedObjective: coachedOriginal,
|
|
173
|
+
coachedAt,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
catch { }
|
|
177
|
+
};
|
|
178
|
+
// Catch double-q / hard exit during thinking wave
|
|
179
|
+
const exitHandler = () => { try {
|
|
180
|
+
saveThinkingState();
|
|
181
|
+
}
|
|
182
|
+
catch { } };
|
|
183
|
+
process.on("exit", exitHandler);
|
|
184
|
+
try {
|
|
185
|
+
await thinkingSwarm.run();
|
|
186
|
+
}
|
|
187
|
+
finally {
|
|
188
|
+
thinkDisplay.pause();
|
|
189
|
+
console.log(renderSummary(thinkingSwarm));
|
|
190
|
+
thinkDisplay.stop();
|
|
191
|
+
saveThinkingState();
|
|
192
|
+
process.removeListener("exit", exitHandler);
|
|
193
|
+
}
|
|
194
|
+
thinkingHistory = { wave: -1, tasks: thinkingSwarm.agents.map(a => ({ prompt: a.task.prompt.slice(0, 200), status: a.status, filesChanged: a.filesChanged, error: a.error })) };
|
|
195
|
+
if (thinkingSwarm.rateLimitResetsAt) {
|
|
196
|
+
const waitMs = thinkingSwarm.rateLimitResetsAt - Date.now();
|
|
197
|
+
if (waitMs > 0) {
|
|
198
|
+
console.log(chalk.dim(` Waiting ${Math.ceil(waitMs / 1000)}s for rate limit reset...`));
|
|
199
|
+
await new Promise(r => setTimeout(r, waitMs + 2000));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const designs = readMdDir(designDir);
|
|
204
|
+
const taskFile = join(runDir, "tasks.json");
|
|
205
|
+
if (designs) {
|
|
206
|
+
const orchBudget = Math.min(50, Math.max(concurrency, Math.ceil(((budget ?? 10) - thinkingUsed) * 0.5)));
|
|
207
|
+
const flexNote = `This is wave 1 of an adaptive multi-wave run (total budget: ${(budget ?? 10) - thinkingUsed}). Plan the highest-impact foundational work first. Future waves will iterate based on what's learned.`;
|
|
208
|
+
console.log(chalk.cyan(`\n ◆ Orchestrating plan...\n`));
|
|
209
|
+
tasks = await orchestrate(objective, designs, cwd, plannerModel, workerModel, orchBudget, concurrency, makeProgressLog(), flexNote, taskFile);
|
|
210
|
+
process.stdout.write(`\x1B[2K\r ${chalk.green(`✓ ${tasks.length} tasks`)}\n\n`);
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
console.log(chalk.yellow(`\n No design docs -- falling back to direct planning\n`));
|
|
214
|
+
const waveBudget = Math.min(50, Math.max(concurrency, Math.ceil(((budget ?? 10) - thinkingUsed) * 0.5)));
|
|
215
|
+
tasks = await planTasks(objective, cwd, plannerModel, workerModel, waveBudget, concurrency, makeProgressLog(), undefined, taskFile);
|
|
216
|
+
process.stdout.write(`\x1B[2K\r ${chalk.green(`✓ ${tasks.length} tasks`)}\n\n`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
const waveBudget = flex ? Math.min(50, Math.max(concurrency, Math.ceil((budget ?? 10) * 0.5))) : budget;
|
|
221
|
+
const flexNote = flex ? `This is wave 1 of an adaptive multi-wave run (total budget: ${budget}). Plan the highest-impact foundational work first. Future waves will iterate, polish, and expand based on what's learned.` : undefined;
|
|
222
|
+
console.log(chalk.cyan(`\n ◆ Planning${flex ? " wave 1" : ""}...\n`));
|
|
223
|
+
tasks = await planTasks(objective, cwd, plannerModel, workerModel, waveBudget, concurrency, makeProgressLog(), flexNote);
|
|
224
|
+
process.stdout.write(`\x1B[2K\r ${chalk.green(`✓ ${tasks.length} tasks`)}${flex ? chalk.dim(` · wave 1`) : ""}\n\n`);
|
|
225
|
+
planRestore();
|
|
226
|
+
let reviewing = true;
|
|
227
|
+
while (reviewing) {
|
|
228
|
+
showPlan(tasks);
|
|
229
|
+
const action = await selectKey(`${chalk.white(`${tasks.length} tasks`)} ${chalk.dim(`· ${concurrency} concurrent`)}`, [{ key: "r", desc: "un" }, { key: "e", desc: "dit" }, { key: "c", desc: "hat" }, { key: "q", desc: "uit" }]);
|
|
230
|
+
switch (action) {
|
|
231
|
+
case "r":
|
|
232
|
+
reviewing = false;
|
|
233
|
+
break;
|
|
234
|
+
case "e": {
|
|
235
|
+
const feedback = await ask(`\n ${chalk.bold("What should change?")}\n ${chalk.cyan(">")} `);
|
|
236
|
+
if (!feedback)
|
|
237
|
+
break;
|
|
238
|
+
console.log(chalk.cyan("\n ◆ Re-planning...\n"));
|
|
239
|
+
process.stdout.write("\x1B[?25l");
|
|
240
|
+
try {
|
|
241
|
+
tasks = await refinePlan(objective, tasks, feedback, cwd, plannerModel, workerModel, budget, concurrency, makeProgressLog());
|
|
242
|
+
process.stdout.write(`\x1B[2K\r ${chalk.green(`✓ ${tasks.length} tasks`)}\n\n`);
|
|
243
|
+
}
|
|
244
|
+
catch (err) {
|
|
245
|
+
console.error(chalk.red(`\n Re-planning failed: ${err.message}\n`));
|
|
246
|
+
}
|
|
247
|
+
planRestore();
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
case "c": {
|
|
251
|
+
const question = await ask(`\n ${chalk.bold("Ask about the plan:")}\n ${chalk.cyan(">")} `);
|
|
252
|
+
if (!question)
|
|
253
|
+
break;
|
|
254
|
+
process.stdout.write("\x1B[?25l");
|
|
255
|
+
try {
|
|
256
|
+
let answer = "";
|
|
257
|
+
const plannerEnv = envForModel(plannerModel);
|
|
258
|
+
for await (const msg of query({
|
|
259
|
+
prompt: `You planned these tasks for the objective "${objective}":\n${tasks.map((t, i) => `${i + 1}. ${t.prompt}`).join("\n")}\n\nUser question: ${question}`,
|
|
260
|
+
options: { cwd, model: plannerModel, permissionMode: "bypassPermissions", allowDangerouslySkipPermissions: true, persistSession: false, ...(plannerEnv && { env: plannerEnv }) },
|
|
261
|
+
})) {
|
|
262
|
+
if (msg.type === "result" && msg.subtype === "success")
|
|
263
|
+
answer = msg.result || "";
|
|
264
|
+
}
|
|
265
|
+
planRestore();
|
|
266
|
+
if (answer)
|
|
267
|
+
console.log(chalk.dim(`\n ${answer.slice(0, 500)}\n`));
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
planRestore();
|
|
271
|
+
}
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
case "q":
|
|
275
|
+
console.log(chalk.dim("\n Aborted.\n"));
|
|
276
|
+
process.exit(0);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
catch (err) {
|
|
282
|
+
planRestore();
|
|
283
|
+
if (isJWTAuthError(err))
|
|
284
|
+
console.error(chalk.red(`\n Authentication failed -- check your API key or run: claude auth\n`));
|
|
285
|
+
else
|
|
286
|
+
console.error(chalk.red(`\n Planning failed: ${err.message}\n`));
|
|
287
|
+
process.exit(1);
|
|
288
|
+
}
|
|
289
|
+
return { tasks, thinkingHistory, thinkingUsed, thinkingCost, thinkingIn, thinkingOut, thinkingTools };
|
|
290
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ProviderConfig } from "../providers/index.js";
|
|
2
|
+
export interface PreflightInput {
|
|
3
|
+
plannerModel: string;
|
|
4
|
+
plannerProvider?: ProviderConfig | undefined;
|
|
5
|
+
workerModel: string;
|
|
6
|
+
workerProvider?: ProviderConfig | undefined;
|
|
7
|
+
fastModel?: string | undefined;
|
|
8
|
+
fastProvider?: ProviderConfig | undefined;
|
|
9
|
+
cwd: string;
|
|
10
|
+
}
|
|
11
|
+
export interface PreflightResult {
|
|
12
|
+
/** true when the fast provider failed preflight and the caller should drop it */
|
|
13
|
+
fastDegraded: boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare function runProviderPreflight(input: PreflightInput): Promise<PreflightResult>;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { getProxyPort, buildProxyUrl } from "../core/proxy-port.js";
|
|
3
|
+
import { setPlannerEnvResolver } from "../planner/query.js";
|
|
4
|
+
import { preflightProvider, isCursorProxyProvider, readCursorProxyLogTail, ensureCursorProxyRunning, bundledComposerProxyShellCommand, hasCursorAgentToken, PROXY_DEFAULT_URL, buildEnvResolver, } from "../providers/index.js";
|
|
5
|
+
export async function runProviderPreflight(input) {
|
|
6
|
+
const { plannerModel, plannerProvider, workerModel, workerProvider, fastProvider, cwd } = input;
|
|
7
|
+
const seen = new Set();
|
|
8
|
+
const all = [
|
|
9
|
+
["planner", plannerProvider],
|
|
10
|
+
["worker", workerProvider],
|
|
11
|
+
["fast", fastProvider],
|
|
12
|
+
];
|
|
13
|
+
const pending = [];
|
|
14
|
+
const cursorProxies = [];
|
|
15
|
+
for (const [role, p] of all) {
|
|
16
|
+
if (p && !seen.has(p.id)) {
|
|
17
|
+
seen.add(p.id);
|
|
18
|
+
pending.push([role, p]);
|
|
19
|
+
if (isCursorProxyProvider(p))
|
|
20
|
+
cursorProxies.push(p);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// Auto-start cursor proxy before pinging (restarts when a token exists so stale listeners get CURSOR_API_KEY).
|
|
24
|
+
if (cursorProxies.length > 0) {
|
|
25
|
+
const resolvedPort = getProxyPort(cwd);
|
|
26
|
+
const resolvedUrl = buildProxyUrl(resolvedPort);
|
|
27
|
+
await ensureCursorProxyRunning(resolvedUrl);
|
|
28
|
+
// Sync providers to the resolved port (may differ from default if per-project port was picked)
|
|
29
|
+
for (const p of cursorProxies) {
|
|
30
|
+
if (!p.baseURL || p.baseURL === PROXY_DEFAULT_URL) {
|
|
31
|
+
p.baseURL = resolvedUrl;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (resolvedUrl !== PROXY_DEFAULT_URL) {
|
|
35
|
+
console.log(chalk.dim(` Proxy port: ${resolvedPort}`));
|
|
36
|
+
}
|
|
37
|
+
if (!hasCursorAgentToken()) {
|
|
38
|
+
console.error(chalk.red(` ✗ Cursor models require a User API key — add it via ${chalk.bold("Cursor…")} setup, or set ` +
|
|
39
|
+
`${chalk.bold("CURSOR_API_KEY")} / ${chalk.bold("CURSOR_BRIDGE_API_KEY")}, or ${chalk.bold("cursorApiKey")} in providers.json.`));
|
|
40
|
+
console.error(chalk.dim(` Without it the Cursor CLI falls back to macOS Keychain (\`cursor-user\`).`));
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
process.stdout.write(` ${chalk.dim(`◆ Pinging ${pending.map(([r, p]) => `${r} (${p.displayName})`).join(", ")}…`)}\n`);
|
|
45
|
+
// Preflight strategy: all providers run fully in parallel. Cursor proxy
|
|
46
|
+
// providers used to race on the shared `~/.cursor/cli-config.json`, but the
|
|
47
|
+
// proxy now uses an account pool (`CURSOR_CONFIG_DIRS`) — each parallel
|
|
48
|
+
// cursor-agent subprocess gets its own config dir, eliminating the race.
|
|
49
|
+
// See ensureCursorAccountPool() in providers.ts.
|
|
50
|
+
//
|
|
51
|
+
// Single in-place status line collapses N parallel progress streams (one
|
|
52
|
+
// per provider) into one tty line updated via `\r` + ANSI clear. Keeps the
|
|
53
|
+
// "window head" calm instead of appending 3 lines per 3s tick.
|
|
54
|
+
const statuses = new Map();
|
|
55
|
+
const isTTY = process.stdout.isTTY;
|
|
56
|
+
let statusLineActive = false;
|
|
57
|
+
const renderStatus = () => {
|
|
58
|
+
if (!isTTY)
|
|
59
|
+
return;
|
|
60
|
+
const parts = [...statuses.entries()].map(([r, s]) => `${r} ${chalk.dim(s)}`);
|
|
61
|
+
process.stdout.write(`\x1B[2K\r`);
|
|
62
|
+
if (parts.length === 0) {
|
|
63
|
+
statusLineActive = false;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
process.stdout.write(chalk.dim(" " + parts.join(" · ")));
|
|
67
|
+
statusLineActive = true;
|
|
68
|
+
};
|
|
69
|
+
const clearStatusLine = () => {
|
|
70
|
+
if (isTTY && statusLineActive) {
|
|
71
|
+
process.stdout.write(`\x1B[2K\r`);
|
|
72
|
+
statusLineActive = false;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
/** Cursor agent cold start + thinking-variant model latency can exceed 20s, and the cursor
|
|
76
|
+
* preflight now also runs a write-capability probe (see probeCursorWriteCapability) that
|
|
77
|
+
* asks cursor to Bash a marker file — so the total budget must cover auth ping + write turn. */
|
|
78
|
+
const preflightMs = (p) => isCursorProxyProvider(p) ? 90_000 : 20_000;
|
|
79
|
+
// Cursor's composer-2 pipeline intermittently stalls for 100s+ on a write-tool turn
|
|
80
|
+
// even though the tool succeeded (proxy logs it as "SLOW response"). A single retry
|
|
81
|
+
// almost always clears it — so we retry once on timeout-style failures for cursor
|
|
82
|
+
// proxy providers before giving up.
|
|
83
|
+
const isTimeoutError = (err) => /^timeout after /.test(err) || /: timeout after /.test(err);
|
|
84
|
+
const runPreflight = async (role, p) => {
|
|
85
|
+
statuses.set(role, "connecting…");
|
|
86
|
+
renderStatus();
|
|
87
|
+
let result = await preflightProvider(p, cwd, preflightMs(p), {
|
|
88
|
+
onProgress: (msg) => { statuses.set(role, msg); renderStatus(); },
|
|
89
|
+
});
|
|
90
|
+
if (!result.ok && isCursorProxyProvider(p) && isTimeoutError(result.error)) {
|
|
91
|
+
statuses.set(role, "retrying after timeout…");
|
|
92
|
+
renderStatus();
|
|
93
|
+
result = await preflightProvider(p, cwd, preflightMs(p), {
|
|
94
|
+
onProgress: (msg) => { statuses.set(role, `retry: ${msg}`); renderStatus(); },
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
statuses.delete(role);
|
|
98
|
+
renderStatus();
|
|
99
|
+
return { role, provider: p, result };
|
|
100
|
+
};
|
|
101
|
+
const results = await Promise.all(pending.map(([role, p]) => runPreflight(role, p)));
|
|
102
|
+
clearStatusLine();
|
|
103
|
+
let fastDegraded = false;
|
|
104
|
+
for (const { role, provider, result } of results) {
|
|
105
|
+
if (!result.ok) {
|
|
106
|
+
const degradable = role === "fast";
|
|
107
|
+
const prefix = degradable ? chalk.yellow(` ⚠ ${role} preflight failed`) : chalk.red(` ✗ ${role} preflight failed`);
|
|
108
|
+
console.error(`${prefix}: ${chalk.dim(result.error)}`);
|
|
109
|
+
if (isCursorProxyProvider(provider)) {
|
|
110
|
+
const tail = readCursorProxyLogTail(25);
|
|
111
|
+
if (tail) {
|
|
112
|
+
console.error(chalk.yellow(` ── proxy log tail (agent stderr + sessions) ──`));
|
|
113
|
+
for (const line of tail.split("\n"))
|
|
114
|
+
console.error(chalk.dim(` ${line}`));
|
|
115
|
+
}
|
|
116
|
+
const cmd = bundledComposerProxyShellCommand();
|
|
117
|
+
const proxyUrl = provider.baseURL || PROXY_DEFAULT_URL;
|
|
118
|
+
console.error(chalk.yellow(` The proxy at ${proxyUrl} may have crashed or timed out (e.g. keychain/UI). Retry, or start the bundled proxy: ${cmd ?? "npm install in the claude-overnight package, then re-run"}`));
|
|
119
|
+
}
|
|
120
|
+
else if (!degradable) {
|
|
121
|
+
console.error(chalk.red(` Fix the provider at ~/.claude/claude-overnight/providers.json and retry.`));
|
|
122
|
+
}
|
|
123
|
+
if (degradable) {
|
|
124
|
+
console.error(chalk.yellow(` Continuing without the fast worker — fast-eligible tasks will run on the main worker model instead.`));
|
|
125
|
+
console.error("");
|
|
126
|
+
fastDegraded = true;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
console.error("");
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
console.log(` ${chalk.green(`✓ ${role} ready`)} ${chalk.dim(`· ${provider.displayName} · ${provider.model}`)}`);
|
|
133
|
+
}
|
|
134
|
+
if (fastDegraded) {
|
|
135
|
+
const rebuilt = buildEnvResolver({ plannerModel, plannerProvider, workerModel, workerProvider, fastModel: undefined, fastProvider: undefined });
|
|
136
|
+
setPlannerEnvResolver(rebuilt);
|
|
137
|
+
}
|
|
138
|
+
return { fastDegraded };
|
|
139
|
+
}
|