claude-overnight 1.25.45 → 1.25.46
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/cli.d.ts +5 -0
- package/dist/cli/cli.js +18 -1
- package/dist/core/_version.d.ts +1 -1
- package/dist/core/_version.js +1 -1
- package/dist/core/jwt-signer.js +12 -10
- package/dist/core/rate-limiter.d.ts +10 -0
- package/dist/core/rate-limiter.js +18 -1
- package/dist/core/token-cache.js +2 -7
- package/dist/core/token-manager.js +8 -5
- package/dist/core/types.d.ts +7 -0
- package/dist/planner/coach/coach.d.ts +4 -0
- package/dist/planner/coach/coach.js +22 -14
- package/dist/planner/query.js +2 -2
- package/dist/providers/index.js +2 -2
- package/dist/run/circuit-breaker-state.d.ts +16 -0
- package/dist/run/circuit-breaker-state.js +18 -0
- package/dist/run/wave-loop.js +39 -24
- package/dist/swarm/agent-run.js +19 -9
- package/dist/swarm/config.d.ts +7 -0
- package/dist/swarm/config.js +15 -0
- package/dist/swarm/errors.d.ts +7 -0
- package/dist/swarm/errors.js +15 -1
- package/dist/swarm/message-handler.d.ts +4 -0
- package/dist/swarm/message-handler.js +20 -0
- package/dist/swarm/swarm.js +3 -0
- package/dist/ui/footer.js +3 -1
- package/dist/ui/header.js +38 -12
- package/dist/ui/input.d.ts +7 -0
- package/dist/ui/input.js +131 -31
- package/dist/ui/overlay.js +22 -10
- package/package.json +1 -1
- package/plugins/claude-overnight/.claude-plugin/plugin.json +1 -1
package/dist/cli/cli.d.ts
CHANGED
|
@@ -61,6 +61,11 @@ export interface FileArgs {
|
|
|
61
61
|
usageCap?: number;
|
|
62
62
|
flexiblePlan?: boolean;
|
|
63
63
|
}
|
|
64
|
+
/** Load a markdown plan file. Extracts the first H1 as objective and returns the full body as planContent. */
|
|
65
|
+
export declare function loadPlanFile(file: string): {
|
|
66
|
+
objective: string;
|
|
67
|
+
planContent: string;
|
|
68
|
+
};
|
|
64
69
|
export declare function loadTaskFile(file: string): FileArgs;
|
|
65
70
|
export declare function validateConcurrency(value: unknown): asserts value is number;
|
|
66
71
|
export declare function isGitRepo(cwd: string): boolean;
|
package/dist/cli/cli.js
CHANGED
|
@@ -7,7 +7,7 @@ import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
|
7
7
|
// ── CLI flag parsing ──
|
|
8
8
|
export function parseCliFlags(argv) {
|
|
9
9
|
const known = new Set(["concurrency", "model", "timeout", "budget", "usage-cap", "extra-usage-budget", "merge"]);
|
|
10
|
-
const booleans = new Set(["--dry-run", "-h", "--help", "-v", "--version", "--no-flex", "--allow-extra-usage", "--worktrees", "--no-worktrees", "--yolo"]);
|
|
10
|
+
const booleans = new Set(["--dry-run", "-h", "--help", "-v", "--version", "--flex", "--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++) {
|
|
@@ -334,6 +334,23 @@ export async function selectKey(label, options) {
|
|
|
334
334
|
const KNOWN_TASK_FILE_KEYS = new Set([
|
|
335
335
|
"tasks", "objective", "concurrency", "cwd", "model", "allowedTools", "beforeWave", "afterWave", "afterRun", "worktrees", "mergeStrategy", "usageCap", "flexiblePlan",
|
|
336
336
|
]);
|
|
337
|
+
/** Load a markdown plan file. Extracts the first H1 as objective and returns the full body as planContent. */
|
|
338
|
+
export function loadPlanFile(file) {
|
|
339
|
+
const path = resolve(file);
|
|
340
|
+
let raw;
|
|
341
|
+
try {
|
|
342
|
+
raw = readFileSync(path, "utf-8");
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
throw new Error(`Cannot read plan file: ${path}`);
|
|
346
|
+
}
|
|
347
|
+
const body = raw.trim();
|
|
348
|
+
if (!body)
|
|
349
|
+
throw new Error(`Plan file is empty: ${path}`);
|
|
350
|
+
const h1 = body.match(/^#\s+(.+)$/m);
|
|
351
|
+
const objective = (h1?.[1] ?? body.split("\n").find(l => l.trim())).trim();
|
|
352
|
+
return { objective, planContent: body };
|
|
353
|
+
}
|
|
337
354
|
export function loadTaskFile(file) {
|
|
338
355
|
const path = resolve(file);
|
|
339
356
|
let raw;
|
package/dist/core/_version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "1.25.
|
|
1
|
+
export declare const VERSION = "1.25.46";
|
package/dist/core/_version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Auto-generated by build — do not edit manually.
|
|
2
|
-
export const VERSION = "1.25.
|
|
2
|
+
export const VERSION = "1.25.46";
|
package/dist/core/jwt-signer.js
CHANGED
|
@@ -59,27 +59,29 @@ export function verifyToken(token, providerId) {
|
|
|
59
59
|
*/
|
|
60
60
|
export function verifyTokenWithResult(token, options = {}) {
|
|
61
61
|
const { providerId, model, baseURL } = options;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const raw = jwt.decode(token);
|
|
65
|
-
if (!raw || typeof raw !== "object") {
|
|
62
|
+
const header = jwt.decode(token, { complete: true });
|
|
63
|
+
if (!header || typeof header === "string") {
|
|
66
64
|
return { valid: false, reason: "invalid_signature" };
|
|
67
65
|
}
|
|
68
|
-
const
|
|
69
|
-
|
|
66
|
+
const loose = header.payload;
|
|
67
|
+
const subForKey = loose.sub;
|
|
68
|
+
if (!subForKey || typeof subForKey !== "string") {
|
|
69
|
+
return { valid: false, reason: "invalid_signature" };
|
|
70
|
+
}
|
|
71
|
+
let key;
|
|
72
|
+
try {
|
|
73
|
+
key = deriveKey(subForKey);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
70
76
|
return { valid: false, reason: "invalid_signature" };
|
|
71
77
|
}
|
|
72
|
-
const key = deriveKey(sub);
|
|
73
78
|
try {
|
|
74
79
|
const decoded = jwt.verify(token, key, {
|
|
75
80
|
algorithms: ["HS256"],
|
|
76
|
-
// Let jwt.verify check expiration for us
|
|
77
81
|
});
|
|
78
|
-
// Reject tokens from older versions
|
|
79
82
|
if (decoded.ver !== TOKEN_VERSION) {
|
|
80
83
|
return { valid: false, reason: "wrong_version" };
|
|
81
84
|
}
|
|
82
|
-
// Validate claims if expected values are provided
|
|
83
85
|
if (providerId && decoded.sub !== providerId) {
|
|
84
86
|
return { valid: false, reason: "claim_mismatch" };
|
|
85
87
|
}
|
|
@@ -8,6 +8,12 @@ export interface RateLimiterConfig {
|
|
|
8
8
|
windowMs: number;
|
|
9
9
|
minIntervalMs?: number;
|
|
10
10
|
}
|
|
11
|
+
export interface AcquireOptions {
|
|
12
|
+
/** When true, skip sliding-window / min-interval waits (caller still records after the request). */
|
|
13
|
+
skipWhen?: () => boolean;
|
|
14
|
+
/** Invoked once when `skipWhen()` returned true and the throttle was bypassed. */
|
|
15
|
+
onBypass?: () => void;
|
|
16
|
+
}
|
|
11
17
|
export declare class RateLimiter {
|
|
12
18
|
private readonly maxRequests;
|
|
13
19
|
private readonly windowMs;
|
|
@@ -18,6 +24,8 @@ export declare class RateLimiter {
|
|
|
18
24
|
record(): void;
|
|
19
25
|
get currentCount(): number;
|
|
20
26
|
canRequest(): boolean;
|
|
27
|
+
/** Wait until a request slot is available. Optional `skipWhen` bypasses the throttle entirely. */
|
|
28
|
+
acquire(options?: AcquireOptions): Promise<number>;
|
|
21
29
|
waitIfNeeded(): Promise<number>;
|
|
22
30
|
waitMs(): number;
|
|
23
31
|
reset(): void;
|
|
@@ -29,6 +37,8 @@ export declare class RateLimiter {
|
|
|
29
37
|
}
|
|
30
38
|
/** Shared rate limiter for SDK query calls — enforced globally across all workers. */
|
|
31
39
|
export declare const sdkQueryRateLimiter: RateLimiter;
|
|
40
|
+
/** Acquire SDK query slot. Skips the SDK sliding-window limiter when `CURSOR_PROXY_URL` is set (proxy has its own limiters). */
|
|
41
|
+
export declare function acquireSdkQueryRateLimit(): Promise<number>;
|
|
32
42
|
/** Shared rate limiter for Cursor proxy direct fetches — enforced globally. */
|
|
33
43
|
export declare const cursorProxyRateLimiter: RateLimiter;
|
|
34
44
|
/** Shared rate limiter for direct API endpoint calls — guards against rapid
|
|
@@ -38,12 +38,20 @@ export class RateLimiter {
|
|
|
38
38
|
return this.timestamps.length < this.maxRequests
|
|
39
39
|
&& (Date.now() - this.lastRequestAt) >= this.minIntervalMs;
|
|
40
40
|
}
|
|
41
|
-
|
|
41
|
+
/** Wait until a request slot is available. Optional `skipWhen` bypasses the throttle entirely. */
|
|
42
|
+
async acquire(options) {
|
|
43
|
+
if (options?.skipWhen?.()) {
|
|
44
|
+
options.onBypass?.();
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
42
47
|
const waited = this.waitMs();
|
|
43
48
|
if (waited > 0)
|
|
44
49
|
await new Promise(r => setTimeout(r, waited));
|
|
45
50
|
return waited;
|
|
46
51
|
}
|
|
52
|
+
async waitIfNeeded() {
|
|
53
|
+
return this.acquire();
|
|
54
|
+
}
|
|
47
55
|
waitMs() {
|
|
48
56
|
this.evict();
|
|
49
57
|
const volumeWait = this.timestamps.length >= this.maxRequests
|
|
@@ -86,6 +94,15 @@ const _cursorProxyLimiter = new RateLimiter({ maxRequests: 4, windowMs: 10_000 }
|
|
|
86
94
|
const _apiEndpointLimiter = new RateLimiter({ maxRequests: 6, windowMs: 15_000, minIntervalMs: 1_000 });
|
|
87
95
|
/** Shared rate limiter for SDK query calls — enforced globally across all workers. */
|
|
88
96
|
export const sdkQueryRateLimiter = _sdkQueryLimiter;
|
|
97
|
+
/** Acquire SDK query slot. Skips the SDK sliding-window limiter when `CURSOR_PROXY_URL` is set (proxy has its own limiters). */
|
|
98
|
+
export async function acquireSdkQueryRateLimit() {
|
|
99
|
+
return _sdkQueryLimiter.acquire({
|
|
100
|
+
skipWhen: () => !!process.env.CURSOR_PROXY_URL,
|
|
101
|
+
onBypass: () => {
|
|
102
|
+
console.log("[rate-limiter] Skipping SDK rate limit (Cursor proxy has its own limiter)");
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
}
|
|
89
106
|
/** Shared rate limiter for Cursor proxy direct fetches — enforced globally. */
|
|
90
107
|
export const cursorProxyRateLimiter = _cursorProxyLimiter;
|
|
91
108
|
/** Shared rate limiter for direct API endpoint calls — guards against rapid
|
package/dist/core/token-cache.js
CHANGED
|
@@ -21,7 +21,7 @@ export function getCachedToken(providerId) {
|
|
|
21
21
|
const entry = tokenCache.get(providerId);
|
|
22
22
|
if (!entry)
|
|
23
23
|
return null;
|
|
24
|
-
if (
|
|
24
|
+
if (isSessionRevoked(entry.sessionId)) {
|
|
25
25
|
tokenCache.delete(providerId);
|
|
26
26
|
return null;
|
|
27
27
|
}
|
|
@@ -51,7 +51,7 @@ export function tryRefreshCachedToken(providerId, refresher) {
|
|
|
51
51
|
const entry = tokenCache.get(providerId);
|
|
52
52
|
if (!entry)
|
|
53
53
|
return null;
|
|
54
|
-
if (
|
|
54
|
+
if (isSessionRevoked(entry.sessionId)) {
|
|
55
55
|
tokenCache.delete(providerId);
|
|
56
56
|
return null;
|
|
57
57
|
}
|
|
@@ -99,11 +99,6 @@ export function clearRevocations() {
|
|
|
99
99
|
export function getRevocationCount() {
|
|
100
100
|
return revokedSessions.size;
|
|
101
101
|
}
|
|
102
|
-
/** Check if a session ID has been revoked, pruning expired entries first. */
|
|
103
|
-
function isRevoked(sessionId) {
|
|
104
|
-
pruneRevocations();
|
|
105
|
-
return revokedSessions.has(sessionId);
|
|
106
|
-
}
|
|
107
102
|
/** Remove expired revocation entries and enforce max size. */
|
|
108
103
|
function pruneRevocations() {
|
|
109
104
|
if (revokedSessions.size === 0)
|
|
@@ -65,10 +65,9 @@ export function refreshToken(oldToken, providerId) {
|
|
|
65
65
|
*/
|
|
66
66
|
export function verifyBearerToken(token, providerId) {
|
|
67
67
|
const result = verifyTokenWithResult(token, { providerId });
|
|
68
|
-
if (!result.valid)
|
|
68
|
+
if (!result.valid || !result.payload)
|
|
69
69
|
return result;
|
|
70
|
-
|
|
71
|
-
if (result.payload && isSessionRevoked(result.payload.jti)) {
|
|
70
|
+
if (isSessionRevoked(result.payload.jti)) {
|
|
72
71
|
return { valid: false, reason: "revoked" };
|
|
73
72
|
}
|
|
74
73
|
return result;
|
|
@@ -100,11 +99,15 @@ function tryPeekAndRevoke(providerId) {
|
|
|
100
99
|
* reducing false positives from unrelated 401/403 responses.
|
|
101
100
|
*/
|
|
102
101
|
export function isJWTAuthError(err) {
|
|
103
|
-
const msg = err instanceof Error
|
|
104
|
-
|
|
102
|
+
const msg = err instanceof Error
|
|
103
|
+
? err.message
|
|
104
|
+
: err && typeof err === "object" && "message" in err && typeof err.message === "string"
|
|
105
105
|
? err.message
|
|
106
106
|
: String(err);
|
|
107
107
|
const lower = msg.toLowerCase();
|
|
108
|
+
if (lower.includes("bearer") && lower.includes("token") && lower.includes("invalid")) {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
108
111
|
// JWT-specific indicators (high confidence)
|
|
109
112
|
const jwtIndicators = [
|
|
110
113
|
"token expired", "invalid_token", "jwt", "signature",
|
package/dist/core/types.d.ts
CHANGED
|
@@ -88,6 +88,8 @@ export interface AgentState {
|
|
|
88
88
|
peakContextTokens?: number;
|
|
89
89
|
/** Resolved model this agent is running (task override or swarm default). */
|
|
90
90
|
model?: string;
|
|
91
|
+
/** Unix timestamp (ms) of the last assistant stream content (text, tool deltas, etc.). Used to detect SDK streams that yield no content. */
|
|
92
|
+
lastContentTimestamp?: number;
|
|
91
93
|
}
|
|
92
94
|
/** A timestamped log line from an agent's execution. */
|
|
93
95
|
export interface LogEntry {
|
|
@@ -173,8 +175,13 @@ export interface WaveSummary {
|
|
|
173
175
|
status: string;
|
|
174
176
|
type?: string;
|
|
175
177
|
filesChanged?: number;
|
|
178
|
+
toolCalls?: number;
|
|
176
179
|
error?: string;
|
|
177
180
|
}[];
|
|
181
|
+
/** Sum of `toolCalls` across all agents in this wave (diagnostics). */
|
|
182
|
+
totalToolCalls?: number;
|
|
183
|
+
/** Non-heal tasks landed 0 files but agents invoked tools — possible worktree/merge bug. */
|
|
184
|
+
suspectedInfraFailure?: boolean;
|
|
178
185
|
}
|
|
179
186
|
/** Result from the steering function. */
|
|
180
187
|
export interface SteerResult {
|
|
@@ -11,5 +11,9 @@ export interface CoachContext {
|
|
|
11
11
|
log?: PlannerLog;
|
|
12
12
|
coachModel?: string;
|
|
13
13
|
coachProvider?: ProviderConfig;
|
|
14
|
+
/** Full markdown plan content (e.g. from a .md plan file). Overrides URL fetching. */
|
|
15
|
+
planContent?: string;
|
|
16
|
+
/** When true, show only accept/skip and do not persist user settings. */
|
|
17
|
+
confirmOnly?: boolean;
|
|
14
18
|
}
|
|
15
19
|
export declare function runSetupCoach(rawObjective: string, cwd: string, ctx: CoachContext): Promise<CoachResult | null>;
|
|
@@ -47,13 +47,15 @@ export async function runSetupCoach(rawObjective, cwd, ctx) {
|
|
|
47
47
|
const facts = collectRepoFacts(cwd);
|
|
48
48
|
if (facts.srcFileCount > 1_000_000)
|
|
49
49
|
return null;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
50
|
+
let planContent = ctx.planContent ?? null;
|
|
51
|
+
if (!planContent) {
|
|
52
|
+
const urls = rawObjective.match(URL_REGEX) ?? [];
|
|
53
|
+
if (urls.length > 0) {
|
|
54
|
+
const results = await Promise.all(urls.map(u => fetchUrlContent(u, 4_000)));
|
|
55
|
+
const fetched = results.filter(Boolean);
|
|
56
|
+
if (fetched.length > 0) {
|
|
57
|
+
planContent = fetched.map((c, i) => `[URL ${i + 1}: ${urls[i]}]\n${c}`).join("\n\n---\n\n");
|
|
58
|
+
}
|
|
57
59
|
}
|
|
58
60
|
}
|
|
59
61
|
const userMessage = renderRepoFacts(facts, rawObjective, ctx.providers, ctx.cliFlags, planContent);
|
|
@@ -120,14 +122,20 @@ export async function runSetupCoach(rawObjective, cwd, ctx) {
|
|
|
120
122
|
return null;
|
|
121
123
|
}
|
|
122
124
|
renderCoachBlock(result, elapsedMs, model);
|
|
123
|
-
const choice =
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
125
|
+
const choice = ctx.confirmOnly
|
|
126
|
+
? await selectKey("", [
|
|
127
|
+
{ key: "y", desc: " accept" },
|
|
128
|
+
{ key: "s", desc: "kip" },
|
|
129
|
+
])
|
|
130
|
+
: await selectKey("", [
|
|
131
|
+
{ key: "y", desc: " accept" },
|
|
132
|
+
{ key: "e", desc: "dit objective" },
|
|
133
|
+
{ key: "s", desc: "kip coach" },
|
|
134
|
+
{ key: "x", desc: " skip coach forever" },
|
|
135
|
+
]);
|
|
129
136
|
if (choice === "y") {
|
|
130
|
-
|
|
137
|
+
if (!ctx.confirmOnly)
|
|
138
|
+
saveUserSettings({ ...loadUserSettings(), lastCoachedAt: Date.now() });
|
|
131
139
|
return result;
|
|
132
140
|
}
|
|
133
141
|
if (choice === "e") {
|
package/dist/planner/query.js
CHANGED
|
@@ -3,7 +3,7 @@ import { NudgeError, extractToolTarget, sumUsageTokens } from "../core/types.js"
|
|
|
3
3
|
import { writeTranscriptEvent } from "../core/transcripts.js";
|
|
4
4
|
import { getTurn, updateTurn } from "../core/turns.js";
|
|
5
5
|
import { isRateLimitError, throttlePlanner, addPlannerCost, recordPeakContext, resetPlannerRateLimit, setContextTokens, applyRateLimitEvent, getPlannerRateLimitInfo, } from "./throttle.js";
|
|
6
|
-
import { cursorProxyRateLimiter, sdkQueryRateLimiter, apiEndpointLimiter } from "../core/rate-limiter.js";
|
|
6
|
+
import { cursorProxyRateLimiter, sdkQueryRateLimiter, apiEndpointLimiter, acquireSdkQueryRateLimit } from "../core/rate-limiter.js";
|
|
7
7
|
export { getTotalPlannerCost, getPeakPlannerContext, getPlannerRateLimitInfo, } from "./throttle.js";
|
|
8
8
|
export { attemptJsonParse, extractTaskJson } from "./json.js";
|
|
9
9
|
export { postProcess } from "./postprocess.js";
|
|
@@ -126,7 +126,7 @@ async function runPlannerQueryOnce(prompt, opts, onLog) {
|
|
|
126
126
|
promptBytes: prompt.length,
|
|
127
127
|
});
|
|
128
128
|
}
|
|
129
|
-
await
|
|
129
|
+
await acquireSdkQueryRateLimit();
|
|
130
130
|
const pq = query({
|
|
131
131
|
prompt,
|
|
132
132
|
options: {
|
package/dist/providers/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import { DEFAULT_MODEL } from "../core/models.js";
|
|
|
11
11
|
import { isCursorProxyProvider, resolveCursorAgentToken, cachedAgentPaths, } from "./cursor-env.js";
|
|
12
12
|
import { preflightCursorProxyViaHttp } from "./cursor-proxy.js";
|
|
13
13
|
import { pickCursorModel } from "./cursor-picker.js";
|
|
14
|
-
import { sdkQueryRateLimiter } from "../core/rate-limiter.js";
|
|
14
|
+
import { sdkQueryRateLimiter, acquireSdkQueryRateLimit } from "../core/rate-limiter.js";
|
|
15
15
|
// Re-export Cursor utilities so callers can keep a single import point.
|
|
16
16
|
export { PROXY_DEFAULT_URL, isCursorProxyProvider, bundledComposerProxyShellCommand, readCursorProxyLogTail, warnMacCursorAgentShellPatchIfNeeded, hasCursorAgentToken, getCursorAgentToken, } from "./cursor-env.js";
|
|
17
17
|
export { healthCheckCursorProxy, ensureCursorProxyRunning } from "./cursor-proxy.js";
|
|
@@ -243,7 +243,7 @@ export async function preflightProvider(p, cwd, timeoutMs = 20_000, opts) {
|
|
|
243
243
|
let pq;
|
|
244
244
|
const rl = sdkQueryRateLimiter;
|
|
245
245
|
try {
|
|
246
|
-
await
|
|
246
|
+
await acquireSdkQueryRateLimit();
|
|
247
247
|
pq = query({
|
|
248
248
|
prompt: "Reply with exactly the word ok and nothing else.",
|
|
249
249
|
options: {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Autonomous circuit breaker: halt after consecutive waves where no non-heal
|
|
3
|
+
* task landed merged file changes *and* no agent used tools (true idle).
|
|
4
|
+
*
|
|
5
|
+
* If agents used tools but files stayed at 0, treat as possible worktree/merge
|
|
6
|
+
* infrastructure issue — do not advance the halt streak.
|
|
7
|
+
*/
|
|
8
|
+
export declare function updateCircuitBreakerStreak(args: {
|
|
9
|
+
waveNum: number;
|
|
10
|
+
prevStreak: number;
|
|
11
|
+
nonHealFiles: number;
|
|
12
|
+
totalToolCallsAllAgents: number;
|
|
13
|
+
}): {
|
|
14
|
+
streak: number;
|
|
15
|
+
shouldHalt: boolean;
|
|
16
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Autonomous circuit breaker: halt after consecutive waves where no non-heal
|
|
3
|
+
* task landed merged file changes *and* no agent used tools (true idle).
|
|
4
|
+
*
|
|
5
|
+
* If agents used tools but files stayed at 0, treat as possible worktree/merge
|
|
6
|
+
* infrastructure issue — do not advance the halt streak.
|
|
7
|
+
*/
|
|
8
|
+
export function updateCircuitBreakerStreak(args) {
|
|
9
|
+
const { waveNum, prevStreak, nonHealFiles, totalToolCallsAllAgents } = args;
|
|
10
|
+
if (waveNum <= 0 || nonHealFiles > 0) {
|
|
11
|
+
return { streak: 0, shouldHalt: false };
|
|
12
|
+
}
|
|
13
|
+
if (totalToolCallsAllAgents > 0) {
|
|
14
|
+
return { streak: 0, shouldHalt: false };
|
|
15
|
+
}
|
|
16
|
+
const streak = prevStreak + 1;
|
|
17
|
+
return { streak, shouldHalt: streak >= 2 };
|
|
18
|
+
}
|
package/dist/run/wave-loop.js
CHANGED
|
@@ -15,6 +15,7 @@ import { getModelCapability } from "../core/models.js";
|
|
|
15
15
|
import { isJWTAuthError } from "../core/auth.js";
|
|
16
16
|
import { saveRunState, saveWaveSession, } from "../state/state.js";
|
|
17
17
|
import { throttleBeforeWave } from "./throttle.js";
|
|
18
|
+
import { updateCircuitBreakerStreak } from "./circuit-breaker-state.js";
|
|
18
19
|
import { checkProjectHealth } from "./health.js";
|
|
19
20
|
import { runPostWaveReview } from "./review.js";
|
|
20
21
|
import { promptBudgetExtension } from "./budget.js";
|
|
@@ -159,18 +160,27 @@ export async function runWaveLoop(host, ctx) {
|
|
|
159
160
|
host.currentTasks = neverStarted;
|
|
160
161
|
// ── Overlay merge outcomes into wave history ──
|
|
161
162
|
const failedMergeBranches = new Set(swarm.mergeResults.filter(r => !r.ok).map(r => r.branch));
|
|
163
|
+
const tasks = swarm.agents.map(a => {
|
|
164
|
+
const mergeFailed = a.branch && failedMergeBranches.has(a.branch);
|
|
165
|
+
return {
|
|
166
|
+
prompt: a.task.prompt,
|
|
167
|
+
status: a.status,
|
|
168
|
+
type: a.task.type,
|
|
169
|
+
filesChanged: mergeFailed ? 0 : a.filesChanged,
|
|
170
|
+
toolCalls: a.toolCalls,
|
|
171
|
+
error: mergeFailed ? `merge-failed: branch ${a.branch} did not land` : a.error,
|
|
172
|
+
};
|
|
173
|
+
});
|
|
174
|
+
const nonHealTasks = tasks.filter(t => t.type !== "heal");
|
|
175
|
+
const nonHealFiles = nonHealTasks.reduce((sum, t) => sum + (t.filesChanged ?? 0), 0);
|
|
176
|
+
const nonHealToolCalls = nonHealTasks.reduce((sum, t) => sum + (t.toolCalls ?? 0), 0);
|
|
177
|
+
const totalToolCalls = swarm.agents.reduce((sum, a) => sum + a.toolCalls, 0);
|
|
178
|
+
const suspectedInfraFailure = host.waveNum > 0 && nonHealFiles === 0 && nonHealToolCalls > 0;
|
|
162
179
|
host.waveHistory.push({
|
|
163
180
|
wave: host.waveNum,
|
|
164
|
-
tasks
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
prompt: a.task.prompt,
|
|
168
|
-
status: a.status,
|
|
169
|
-
type: a.task.type,
|
|
170
|
-
filesChanged: mergeFailed ? 0 : a.filesChanged,
|
|
171
|
-
error: mergeFailed ? `merge-failed: branch ${a.branch} did not land` : a.error,
|
|
172
|
-
};
|
|
173
|
-
}),
|
|
181
|
+
tasks,
|
|
182
|
+
totalToolCalls,
|
|
183
|
+
suspectedInfraFailure,
|
|
174
184
|
});
|
|
175
185
|
// ── Heal fail streak ──
|
|
176
186
|
const lastWave = host.waveHistory[host.waveHistory.length - 1];
|
|
@@ -182,21 +192,26 @@ export async function runWaveLoop(host, ctx) {
|
|
|
182
192
|
healFailStreak = 0;
|
|
183
193
|
}
|
|
184
194
|
// ── Circuit breaker ──
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
zeroFileWaves
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
process.exit(3);
|
|
196
|
-
}
|
|
195
|
+
const { streak: nextZeroFileWaves, shouldHalt: circuitHalt } = updateCircuitBreakerStreak({
|
|
196
|
+
waveNum: host.waveNum,
|
|
197
|
+
prevStreak: zeroFileWaves,
|
|
198
|
+
nonHealFiles,
|
|
199
|
+
totalToolCallsAllAgents: totalToolCalls,
|
|
200
|
+
});
|
|
201
|
+
if (host.waveNum > 0 && nonHealFiles === 0 && nonHealToolCalls > 0) {
|
|
202
|
+
const msg = "[circuit-breaker] Agents completed with tool calls but 0 files landed — possible worktree/merge bug, NOT a stuck agent. Continuing run.";
|
|
203
|
+
console.warn(chalk.yellow(`\n ${msg}`));
|
|
204
|
+
ctx.display.appendSteeringEvent(msg);
|
|
197
205
|
}
|
|
198
|
-
|
|
199
|
-
|
|
206
|
+
zeroFileWaves = nextZeroFileWaves;
|
|
207
|
+
if (circuitHalt) {
|
|
208
|
+
ctx.display.appendSteeringEvent(`Circuit breaker: 2 consecutive waves produced no merged changes — halting to prevent budget drain`);
|
|
209
|
+
ctx.display.stop();
|
|
210
|
+
saveRunState(ctx.runDir, buildRunState(host, "stopped", []));
|
|
211
|
+
ctx.display.stop();
|
|
212
|
+
console.log(chalk.red(`\n Circuit breaker: 2 consecutive waves produced no merged changes.`));
|
|
213
|
+
console.log(chalk.red(` Halting to prevent budget drain. Run preserved at ${ctx.runDir}.`));
|
|
214
|
+
process.exit(3);
|
|
200
215
|
}
|
|
201
216
|
// ── Hook-blocked work ──
|
|
202
217
|
const hookBlocked = swarm.agents.filter(a => swarm.logs.some(l => l.agentId === a.id && l.text.includes("did NOT land")));
|
package/dist/swarm/agent-run.js
CHANGED
|
@@ -13,10 +13,10 @@ import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
|
13
13
|
import { NudgeError } from "../core/types.js";
|
|
14
14
|
import { gitExec, autoCommit } from "./merge.js";
|
|
15
15
|
import { createTurn, beginTurn, endTurn, updateTurn } from "../core/turns.js";
|
|
16
|
-
import { SIMPLIFY_PROMPT, withCursorWorkspaceHeader } from "./config.js";
|
|
17
|
-
import { AgentTimeoutError, isRateLimitError, isTransientError, sleep } from "./errors.js";
|
|
18
|
-
import { handleMsg } from "./message-handler.js";
|
|
19
|
-
import { sdkQueryRateLimiter } from "../core/rate-limiter.js";
|
|
16
|
+
import { SIMPLIFY_PROMPT, withCursorWorkspaceHeader, getAgentTimeout } from "./config.js";
|
|
17
|
+
import { AgentTimeoutError, StreamStalledError, isRateLimitError, isTransientError, sleep } from "./errors.js";
|
|
18
|
+
import { handleMsg, checkStreamHealth, NO_CONTENT_TIMEOUT_MS } from "./message-handler.js";
|
|
19
|
+
import { sdkQueryRateLimiter, acquireSdkQueryRateLimit } from "../core/rate-limiter.js";
|
|
20
20
|
export async function runAgent(host, task) {
|
|
21
21
|
// Guard: if pause was triggered between dispatch and here, re-queue immediately.
|
|
22
22
|
// The worker already shifted this task, so unshift puts it back for resume.
|
|
@@ -87,7 +87,7 @@ export async function runAgent(host, task) {
|
|
|
87
87
|
const isResumed = !!task.resumeSessionId;
|
|
88
88
|
host.log(id, isResumed ? `Resuming: ${task.prompt.slice(0, 60)}` : `Starting: ${task.prompt.slice(0, 60)}`);
|
|
89
89
|
const maxRetries = host.config.maxRetries ?? 2;
|
|
90
|
-
const inactivityMs = host.config.agentTimeoutMs ??
|
|
90
|
+
const inactivityMs = host.config.agentTimeoutMs ?? getAgentTimeout();
|
|
91
91
|
const rl = sdkQueryRateLimiter;
|
|
92
92
|
// Hoisted so the catch block can read the session captured during the turn
|
|
93
93
|
// when routing a pause-interrupt through the requeue path.
|
|
@@ -115,7 +115,7 @@ export async function runAgent(host, task) {
|
|
|
115
115
|
// Read host.model (live — setModel updates it between waves), not host.config.model (frozen at construction).
|
|
116
116
|
const effectiveModel = task.model || host.model;
|
|
117
117
|
const envOverride = withCursorWorkspaceHeader(host.config.envForModel?.(effectiveModel), agentCwd);
|
|
118
|
-
await
|
|
118
|
+
await acquireSdkQueryRateLimit();
|
|
119
119
|
const agentQuery = query({
|
|
120
120
|
prompt: agentPrompt,
|
|
121
121
|
options: {
|
|
@@ -129,19 +129,29 @@ export async function runAgent(host, task) {
|
|
|
129
129
|
const timeoutMs = isResume ? inactivityMs * 2 : inactivityMs;
|
|
130
130
|
let sessionId;
|
|
131
131
|
let lastActivity = Date.now();
|
|
132
|
+
agent.lastContentTimestamp = Date.now();
|
|
132
133
|
let timer;
|
|
133
134
|
const watchdog = new Promise((_, reject) => {
|
|
134
135
|
const check = () => {
|
|
136
|
+
const lastContent = agent.lastContentTimestamp;
|
|
137
|
+
if (lastContent != null && !checkStreamHealth(lastContent, NO_CONTENT_TIMEOUT_MS)) {
|
|
138
|
+
const elapsed = Date.now() - lastContent;
|
|
139
|
+
agentQuery.interrupt().catch(() => agentQuery.close());
|
|
140
|
+
reject(new StreamStalledError(elapsed, NO_CONTENT_TIMEOUT_MS));
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
135
143
|
const silent = Date.now() - lastActivity;
|
|
136
144
|
if (silent >= timeoutMs) {
|
|
137
145
|
agentQuery.interrupt().catch(() => agentQuery.close());
|
|
138
146
|
reject(isResume ? new AgentTimeoutError(silent) : new NudgeError(sessionId, silent));
|
|
139
147
|
}
|
|
140
148
|
else {
|
|
141
|
-
|
|
149
|
+
const gap = lastContent != null ? Date.now() - lastContent : 0;
|
|
150
|
+
const untilNoContent = lastContent != null ? Math.max(250, NO_CONTENT_TIMEOUT_MS - gap + 50) : Number.POSITIVE_INFINITY;
|
|
151
|
+
timer = setTimeout(check, Math.min(30_000, timeoutMs - silent + 1000, 5_000, untilNoContent));
|
|
142
152
|
}
|
|
143
153
|
};
|
|
144
|
-
timer = setTimeout(check, timeoutMs);
|
|
154
|
+
timer = setTimeout(check, Math.min(5_000, timeoutMs));
|
|
145
155
|
});
|
|
146
156
|
host.activeQueries.add(agentQuery);
|
|
147
157
|
// Guard: if pause was triggered between runAgent check and here, close the query
|
|
@@ -403,7 +413,7 @@ Respond with JSON: {"keep": true/false, "reason": "brief explanation"}`;
|
|
|
403
413
|
const rl = sdkQueryRateLimiter;
|
|
404
414
|
let eq;
|
|
405
415
|
try {
|
|
406
|
-
await
|
|
416
|
+
await acquireSdkQueryRateLimit();
|
|
407
417
|
eq = query({
|
|
408
418
|
prompt,
|
|
409
419
|
options: {
|
package/dist/swarm/config.d.ts
CHANGED
|
@@ -29,3 +29,10 @@ export declare const SIMPLIFY_PROMPT = "You just finished your task. Review and
|
|
|
29
29
|
* all worktree paths) so the header value passes the safety check.
|
|
30
30
|
*/
|
|
31
31
|
export declare function withCursorWorkspaceHeader(env: Record<string, string> | undefined, cwd: string): Record<string, string> | undefined;
|
|
32
|
+
/** Default per-agent inactivity watchdog (see `agent-run` race with SDK `query`). */
|
|
33
|
+
export declare const DEFAULT_AGENT_TIMEOUT_MS: number;
|
|
34
|
+
/**
|
|
35
|
+
* Per-agent watchdog timeout in ms. Override with `AGENT_TIMEOUT_MS` (integer).
|
|
36
|
+
* Explicit `SwarmConfig.agentTimeoutMs` still wins at call sites that pass it.
|
|
37
|
+
*/
|
|
38
|
+
export declare function getAgentTimeout(): number;
|
package/dist/swarm/config.js
CHANGED
|
@@ -33,3 +33,18 @@ export function withCursorWorkspaceHeader(env, cwd) {
|
|
|
33
33
|
: hdr,
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
|
+
/** Default per-agent inactivity watchdog (see `agent-run` race with SDK `query`). */
|
|
37
|
+
export const DEFAULT_AGENT_TIMEOUT_MS = 15 * 60 * 1000;
|
|
38
|
+
/**
|
|
39
|
+
* Per-agent watchdog timeout in ms. Override with `AGENT_TIMEOUT_MS` (integer).
|
|
40
|
+
* Explicit `SwarmConfig.agentTimeoutMs` still wins at call sites that pass it.
|
|
41
|
+
*/
|
|
42
|
+
export function getAgentTimeout() {
|
|
43
|
+
const raw = process.env.AGENT_TIMEOUT_MS;
|
|
44
|
+
if (raw == null || raw.trim() === "")
|
|
45
|
+
return DEFAULT_AGENT_TIMEOUT_MS;
|
|
46
|
+
const n = Number.parseInt(raw, 10);
|
|
47
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
48
|
+
return DEFAULT_AGENT_TIMEOUT_MS;
|
|
49
|
+
return n;
|
|
50
|
+
}
|
package/dist/swarm/errors.d.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
export declare class AgentTimeoutError extends Error {
|
|
2
2
|
constructor(silentMs: number);
|
|
3
3
|
}
|
|
4
|
+
/** Thrown when the SDK query stream stops emitting assistant content for too long while still open. */
|
|
5
|
+
export declare class StreamStalledError extends Error {
|
|
6
|
+
readonly elapsed: number;
|
|
7
|
+
readonly timeoutMs: number;
|
|
8
|
+
constructor(elapsed: number, timeoutMs: number);
|
|
9
|
+
}
|
|
10
|
+
export declare function isStreamStalledError(err: unknown): err is StreamStalledError;
|
|
4
11
|
export declare function isRateLimitError(err: unknown): boolean;
|
|
5
12
|
export declare function isTransientError(err: unknown): boolean;
|
|
6
13
|
export declare function sleep(ms: number): Promise<void>;
|
package/dist/swarm/errors.js
CHANGED
|
@@ -7,6 +7,20 @@ export class AgentTimeoutError extends Error {
|
|
|
7
7
|
this.name = "AgentTimeoutError";
|
|
8
8
|
}
|
|
9
9
|
}
|
|
10
|
+
/** Thrown when the SDK query stream stops emitting assistant content for too long while still open. */
|
|
11
|
+
export class StreamStalledError extends Error {
|
|
12
|
+
elapsed;
|
|
13
|
+
timeoutMs;
|
|
14
|
+
constructor(elapsed, timeoutMs) {
|
|
15
|
+
super(`Stream stalled: no content for ${timeoutMs}ms (last gap ${Math.round(elapsed)}ms)`);
|
|
16
|
+
this.elapsed = elapsed;
|
|
17
|
+
this.timeoutMs = timeoutMs;
|
|
18
|
+
this.name = "StreamStalledError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function isStreamStalledError(err) {
|
|
22
|
+
return err instanceof StreamStalledError;
|
|
23
|
+
}
|
|
10
24
|
export function isRateLimitError(err) {
|
|
11
25
|
const status = err?.status ?? err?.statusCode;
|
|
12
26
|
if (status === 429)
|
|
@@ -20,7 +34,7 @@ export function isRateLimitError(err) {
|
|
|
20
34
|
return false;
|
|
21
35
|
}
|
|
22
36
|
export function isTransientError(err) {
|
|
23
|
-
if (err instanceof AgentTimeoutError)
|
|
37
|
+
if (err instanceof AgentTimeoutError || err instanceof StreamStalledError)
|
|
24
38
|
return false;
|
|
25
39
|
const msg = String(err?.message || err).toLowerCase();
|
|
26
40
|
const status = err?.status ?? err?.statusCode;
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { SDKMessage } from "@anthropic-ai/claude-agent-sdk";
|
|
2
2
|
import type { AgentState, AITurn, RateLimitWindow } from "../core/types.js";
|
|
3
|
+
/** Default: no assistant content for this long means the SDK stream is stuck. */
|
|
4
|
+
export declare const NO_CONTENT_TIMEOUT_MS = 15000;
|
|
5
|
+
/** @returns false if no stream content has arrived within {@link timeoutMs} of {@link lastContentTimestamp}. */
|
|
6
|
+
export declare function checkStreamHealth(lastContentTimestamp: number, timeoutMs: number): boolean;
|
|
3
7
|
/** Per-agent pending tool_use block while we wait for the delta stream to
|
|
4
8
|
* finish materializing the real `input`. */
|
|
5
9
|
export interface PendingTool {
|
|
@@ -11,6 +11,15 @@
|
|
|
11
11
|
import { RATE_LIMIT_WINDOW_SHORT, extractToolTarget, sumUsageTokens } from "../core/types.js";
|
|
12
12
|
import { getModelCapability } from "../core/models.js";
|
|
13
13
|
import { updateTurn } from "../core/turns.js";
|
|
14
|
+
/** Default: no assistant content for this long means the SDK stream is stuck. */
|
|
15
|
+
export const NO_CONTENT_TIMEOUT_MS = 15_000;
|
|
16
|
+
/** @returns false if no stream content has arrived within {@link timeoutMs} of {@link lastContentTimestamp}. */
|
|
17
|
+
export function checkStreamHealth(lastContentTimestamp, timeoutMs) {
|
|
18
|
+
return Date.now() - lastContentTimestamp <= timeoutMs;
|
|
19
|
+
}
|
|
20
|
+
function markStreamContent(agent) {
|
|
21
|
+
agent.lastContentTimestamp = Date.now();
|
|
22
|
+
}
|
|
14
23
|
/** Log a tool invocation with a short target extracted from its input. */
|
|
15
24
|
export function logToolUse(host, agent, name, input) {
|
|
16
25
|
const target = extractToolTarget(input);
|
|
@@ -49,6 +58,10 @@ export function handleMsg(host, agent, msg) {
|
|
|
49
58
|
if (!m.message?.content)
|
|
50
59
|
break;
|
|
51
60
|
for (const block of m.message.content) {
|
|
61
|
+
const bt = block.type;
|
|
62
|
+
if (bt === "text" || bt === "tool_use" || bt === "thinking" || bt === "redacted_thinking") {
|
|
63
|
+
markStreamContent(agent);
|
|
64
|
+
}
|
|
52
65
|
if (block.type === "text" && block.text) {
|
|
53
66
|
const line = block.text.trim().split("\n")[0]?.slice(0, 80);
|
|
54
67
|
if (line)
|
|
@@ -63,6 +76,7 @@ export function handleMsg(host, agent, msg) {
|
|
|
63
76
|
if (ev.type === "content_block_start") {
|
|
64
77
|
const cb = ev.content_block;
|
|
65
78
|
if (cb?.type === "tool_use") {
|
|
79
|
+
markStreamContent(agent);
|
|
66
80
|
agent.currentTool = cb.name;
|
|
67
81
|
agent.toolCalls++;
|
|
68
82
|
const input = (cb.input ?? {});
|
|
@@ -72,13 +86,18 @@ export function handleMsg(host, agent, msg) {
|
|
|
72
86
|
logToolUse(host, agent, cb.name, input);
|
|
73
87
|
}
|
|
74
88
|
else if (cb?.type === "thinking" || cb?.type === "redacted_thinking") {
|
|
89
|
+
markStreamContent(agent);
|
|
75
90
|
agent.lastText = "thinking…";
|
|
76
91
|
}
|
|
92
|
+
else if (cb?.type === "text") {
|
|
93
|
+
markStreamContent(agent);
|
|
94
|
+
}
|
|
77
95
|
}
|
|
78
96
|
else if (ev.type === "content_block_delta") {
|
|
79
97
|
const delta = ev.delta;
|
|
80
98
|
const pending = host.pendingTools.get(agent);
|
|
81
99
|
if (delta?.type === "input_json_delta" && pending && typeof delta.partial_json === "string") {
|
|
100
|
+
markStreamContent(agent);
|
|
82
101
|
pending.buf += delta.partial_json;
|
|
83
102
|
break;
|
|
84
103
|
}
|
|
@@ -87,6 +106,7 @@ export function handleMsg(host, agent, msg) {
|
|
|
87
106
|
: delta?.type === "thinking_delta" ? delta.thinking
|
|
88
107
|
: undefined;
|
|
89
108
|
if (typeof raw === "string") {
|
|
109
|
+
markStreamContent(agent);
|
|
90
110
|
const t = raw.trim();
|
|
91
111
|
if (t)
|
|
92
112
|
agent.lastText = t.slice(-80);
|
package/dist/swarm/swarm.js
CHANGED
|
@@ -5,6 +5,7 @@ import chalk from "chalk";
|
|
|
5
5
|
import { RATE_LIMIT_WINDOW_SHORT } from "../core/types.js";
|
|
6
6
|
import { gitExec, mergeAllBranches, warnDirtyTree, cleanStaleWorktrees, writeSwarmLog } from "./merge.js";
|
|
7
7
|
import { ensureCursorProxyRunning, PROXY_DEFAULT_URL } from "../providers/index.js";
|
|
8
|
+
import { getAgentTimeout } from "./config.js";
|
|
8
9
|
import { sleep } from "./errors.js";
|
|
9
10
|
import { runAgent as runAgentImpl, buildErroredBranchEvaluator } from "./agent-run.js";
|
|
10
11
|
export class Swarm {
|
|
@@ -315,6 +316,8 @@ export class Swarm {
|
|
|
315
316
|
const task = this.queue.shift();
|
|
316
317
|
if (!task)
|
|
317
318
|
break;
|
|
319
|
+
const watchdogMs = this.config.agentTimeoutMs ?? getAgentTimeout();
|
|
320
|
+
this.log(-1, `[swarm] Agent watchdog timeout: ${Math.round(watchdogMs / 1000)}s`);
|
|
318
321
|
try {
|
|
319
322
|
await this.runAgent(task);
|
|
320
323
|
}
|
package/dist/ui/footer.js
CHANGED
|
@@ -27,8 +27,10 @@ function layoutActions(rendered, pendingChip, termW) {
|
|
|
27
27
|
}
|
|
28
28
|
export function Footer({ state, toast }) {
|
|
29
29
|
const inOverlay = state.input.mode;
|
|
30
|
+
// When an input overlay is active the InputPrompt box renders its own hint
|
|
31
|
+
// line, so the footer stays quiet — just a spacer to keep the layout stable.
|
|
30
32
|
if (inOverlay !== "none") {
|
|
31
|
-
return
|
|
33
|
+
return _jsx(Text, { children: " " });
|
|
32
34
|
}
|
|
33
35
|
const actions = deriveFooter(state);
|
|
34
36
|
const pending = state.runInfo.pendingSteer ?? 0;
|
package/dist/ui/header.js
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Text, Box } from "ink";
|
|
3
3
|
import chalk from "chalk";
|
|
4
|
-
import { fmtDur, fmtTokens } from "./primitives.js";
|
|
4
|
+
import { fmtDur, fmtTokens, visibleLen } from "./primitives.js";
|
|
5
5
|
import { UsageBars, SteeringBars } from "./bars.js";
|
|
6
|
-
|
|
6
|
+
function terminalWidth() { return Math.max((process.stdout.columns ?? 80) || 80, 60); }
|
|
7
|
+
// Scales with terminal width so narrow panes get a short bar and wide ones
|
|
8
|
+
// don't waste the right half of the screen.
|
|
9
|
+
function headerBarWidth(termW) {
|
|
10
|
+
if (termW < 90)
|
|
11
|
+
return 16;
|
|
12
|
+
if (termW < 120)
|
|
13
|
+
return 24;
|
|
14
|
+
return 30;
|
|
15
|
+
}
|
|
7
16
|
function runPhaseLabel(swarm) {
|
|
8
17
|
const allDone = swarm.agents.length > 0 && swarm.agents.every(a => a.status !== "running");
|
|
9
18
|
const doneTag = allDone && !swarm.aborted ? chalk.green("COMPLETE") : "";
|
|
@@ -16,6 +25,9 @@ function runPhaseLabel(swarm) {
|
|
|
16
25
|
return [phaseLabel, doneTag, pausedTag, stallTag, stoppingTag].filter(Boolean).join(" ");
|
|
17
26
|
}
|
|
18
27
|
export function Header({ phase, runInfo, swarm, rlGetter, selectedAgentId }) {
|
|
28
|
+
const termW = terminalWidth();
|
|
29
|
+
const barW = headerBarWidth(termW);
|
|
30
|
+
const narrow = termW < 90;
|
|
19
31
|
const model = runInfo.model ?? swarm?.model;
|
|
20
32
|
const modelTag = model ? chalk.dim(` [${model}]`) : "";
|
|
21
33
|
let phaseTag = "";
|
|
@@ -42,24 +54,38 @@ export function Header({ phase, runInfo, swarm, rlGetter, selectedAgentId }) {
|
|
|
42
54
|
barPct = runInfo.sessionsBudget > 0 ? sessionsUsed / runInfo.sessionsBudget : 0;
|
|
43
55
|
barLabel = `${sessionsUsed}/${runInfo.sessionsBudget}`;
|
|
44
56
|
}
|
|
45
|
-
const filled = Math.round(barPct *
|
|
46
|
-
const bar = chalk.green("\u2588".repeat(filled)) + chalk.gray("\u2591".repeat(
|
|
57
|
+
const filled = Math.round(barPct * barW);
|
|
58
|
+
const bar = chalk.green("\u2588".repeat(filled)) + chalk.gray("\u2591".repeat(barW - filled));
|
|
47
59
|
const working = Math.max(0, active - blocked);
|
|
48
60
|
const stuck = blocked > 0 && working === 0;
|
|
49
61
|
const activeChip = active > 0
|
|
50
62
|
? (stuck ? chalk.yellow(`${active} blocked`) : chalk.cyan(`${working} active`) + (blocked > 0 ? chalk.yellow(` (${blocked} blocked)`) : ""))
|
|
51
63
|
: "";
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
64
|
+
const elapsed = chalk.gray(`\u23F1 ${fmtDur(Date.now() - runInfo.startedAt)}`);
|
|
65
|
+
const queuedChip = queued > 0 ? chalk.gray(`${queued} queued`) : "";
|
|
66
|
+
// Top line: brand · phase · bar · counters · elapsed. Narrow terminals drop
|
|
67
|
+
// the brand and model so the live indicators stay on one row.
|
|
68
|
+
const brand = narrow ? "" : chalk.bold.white("CLAUDE OVERNIGHT") + modelTag;
|
|
69
|
+
const progress = barLabel ? bar + " " + barLabel : bar;
|
|
70
|
+
const topParts = [
|
|
71
|
+
brand,
|
|
72
|
+
phaseTag,
|
|
73
|
+
progress,
|
|
74
|
+
activeChip,
|
|
75
|
+
queuedChip,
|
|
76
|
+
elapsed,
|
|
77
|
+
].filter(Boolean);
|
|
78
|
+
const topLine = " " + topParts.join(" ");
|
|
56
79
|
const tokIn = fmtTokens(totalIn);
|
|
57
80
|
const tokOut = fmtTokens(totalOut);
|
|
58
81
|
const costStr = totalCost > 0 ? chalk.yellow(`$${totalCost.toFixed(2)}`) : "";
|
|
59
|
-
const waveLabel = runInfo.waveNum >= 0 ? `wave ${runInfo.waveNum + 1}
|
|
60
|
-
const
|
|
61
|
-
|
|
82
|
+
const waveLabel = runInfo.waveNum >= 0 ? `wave ${runInfo.waveNum + 1}` : "";
|
|
83
|
+
const tokens = chalk.gray(`\u2191 ${tokIn} in \u2193 ${tokOut} out`);
|
|
84
|
+
const sessions = chalk.white(`${sessionsUsed}/${runInfo.sessionsBudget}`) +
|
|
62
85
|
chalk.dim(` sessions \u00b7 ${runInfo.remaining} left`);
|
|
63
|
-
const
|
|
86
|
+
const bottomLeftParts = [tokens, costStr].filter(Boolean).join(" ");
|
|
87
|
+
const bottomRightParts = [waveLabel ? chalk.dim(waveLabel) : "", sessions].filter(Boolean).join(chalk.dim(" \u00b7 "));
|
|
88
|
+
const gap = Math.max(2, termW - visibleLen(bottomLeftParts) - visibleLen(bottomRightParts) - 4);
|
|
89
|
+
const bottomLine = " " + bottomLeftParts + " ".repeat(gap) + bottomRightParts;
|
|
64
90
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: " " }), _jsx(Text, { children: topLine }), _jsx(Text, { children: bottomLine }), phase === "run" && swarm ? _jsx(UsageBars, { swarm: swarm, selectedAgentId: selectedAgentId }) : null, phase === "steering" && rlGetter ? _jsx(SteeringBars, { rl: rlGetter() }) : null, _jsx(Text, { children: " " })] }));
|
|
65
91
|
}
|
package/dist/ui/input.d.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import type { UiStore, HostCallbacks } from "./store.js";
|
|
3
3
|
export declare const MAX_INPUT_LEN = 600;
|
|
4
|
+
export declare const CONTROL_CHAR_RE: RegExp;
|
|
5
|
+
/** Strip control characters from typed raw input so escape flushes, newlines,
|
|
6
|
+
* and C1 bytes never end up in the user's buffer. Exported for tests. */
|
|
7
|
+
export declare function sanitizeTyped(raw: string): string;
|
|
8
|
+
/** Delete the previous word including any trailing whitespace, readline-style.
|
|
9
|
+
* Bound to Ctrl+W and Opt/Cmd+Backspace. Exported for tests. */
|
|
10
|
+
export declare function deleteWordBackward(s: string): string;
|
|
4
11
|
interface Props {
|
|
5
12
|
store: UiStore;
|
|
6
13
|
callbacks: HostCallbacks;
|
package/dist/ui/input.js
CHANGED
|
@@ -9,11 +9,34 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
9
9
|
// Text-entry overlays (steer, ask, settings) are *not* separate phases — they
|
|
10
10
|
// capture typed chars, show a minimal hint in the footer (handled by
|
|
11
11
|
// footer.tsx), and dispatch on Enter/Esc.
|
|
12
|
+
//
|
|
13
|
+
// Text-entry input hygiene: terminals send a zoo of escape/control sequences
|
|
14
|
+
// for navigation keys (arrows, cmd+arrow → ctrl+a/e on macOS, option+letter,
|
|
15
|
+
// pageup/pgdn, home/end, lone ESC flushes). We explicitly swallow all of them
|
|
16
|
+
// instead of letting them fall through to the "typed char" branch, which used
|
|
17
|
+
// to corrupt the buffer or dismiss the overlay and discard unfinished text.
|
|
12
18
|
import { useState, useSyncExternalStore } from "react";
|
|
13
19
|
import { Text, Box, useInput } from "ink";
|
|
14
20
|
import chalk from "chalk";
|
|
21
|
+
import { visibleLen, wrap } from "./primitives.js";
|
|
15
22
|
import { SETTINGS_FIELDS, SETTINGS_LABELS, NUMERIC_SETTINGS_FIELDS, applySettingEdit, readSettingValue, } from "./settings.js";
|
|
16
23
|
export const MAX_INPUT_LEN = 600;
|
|
24
|
+
// Any printable char is kept verbatim; everything else is filtered out before
|
|
25
|
+
// touching the buffer. Matches: ASCII C0 controls (0x00-0x1F), DEL (0x7F), C1
|
|
26
|
+
// controls (0x80-0x9F), and lone ESC (already handled by `key.escape`).
|
|
27
|
+
export const CONTROL_CHAR_RE = /[\x00-\x1f\x7f-\x9f]/g;
|
|
28
|
+
/** Strip control characters from typed raw input so escape flushes, newlines,
|
|
29
|
+
* and C1 bytes never end up in the user's buffer. Exported for tests. */
|
|
30
|
+
export function sanitizeTyped(raw) {
|
|
31
|
+
return raw.replace(CONTROL_CHAR_RE, "");
|
|
32
|
+
}
|
|
33
|
+
/** Delete the previous word including any trailing whitespace, readline-style.
|
|
34
|
+
* Bound to Ctrl+W and Opt/Cmd+Backspace. Exported for tests. */
|
|
35
|
+
export function deleteWordBackward(s) {
|
|
36
|
+
const trimmed = s.replace(/\s+$/, "");
|
|
37
|
+
const idx = trimmed.search(/\S+$/);
|
|
38
|
+
return idx < 0 ? "" : trimmed.slice(0, idx);
|
|
39
|
+
}
|
|
17
40
|
export function InputLayer({ store, callbacks, onToast }) {
|
|
18
41
|
const [buffer, setBuffer] = useState("");
|
|
19
42
|
const [settingsField, setSettingsField] = useState(0);
|
|
@@ -24,7 +47,14 @@ export function InputLayer({ store, callbacks, onToast }) {
|
|
|
24
47
|
const lc = state.liveConfig;
|
|
25
48
|
// ── Text-entry modes ──
|
|
26
49
|
if (mode !== "none") {
|
|
27
|
-
//
|
|
50
|
+
// Navigation keys must NEVER touch the buffer or dismiss the overlay.
|
|
51
|
+
// (Bug: cmd+arrow on macOS Terminal sends ctrl+a/ctrl+e which used to
|
|
52
|
+
// leak through and append "a"/"e"; arrows on some terminals flushed
|
|
53
|
+
// partial ESC sequences that dropped the user's unfinished text.)
|
|
54
|
+
if (key.upArrow || key.downArrow || key.leftArrow || key.rightArrow ||
|
|
55
|
+
key.pageUp || key.pageDown || key.home || key.end)
|
|
56
|
+
return;
|
|
57
|
+
// Esc bails (loses the buffer — always intentional).
|
|
28
58
|
if (key.escape) {
|
|
29
59
|
setBuffer("");
|
|
30
60
|
setSettingsField(0);
|
|
@@ -59,25 +89,53 @@ export function InputLayer({ store, callbacks, onToast }) {
|
|
|
59
89
|
store.patch({ input: { mode: "none", buffer: "", settingsField: 0 } });
|
|
60
90
|
return;
|
|
61
91
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
92
|
+
// Word-delete: option/alt + backspace — expected on macOS.
|
|
93
|
+
if ((key.meta || key.ctrl) && (key.backspace || key.delete)) {
|
|
94
|
+
const next = deleteWordBackward(buffer);
|
|
95
|
+
setBuffer(next);
|
|
96
|
+
store.patch({ input: { ...state.input, buffer: next } });
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
// Swallow modifier combos so they can't leak as stray letters.
|
|
100
|
+
// (cmd+→ on macOS Terminal = \x05 = ctrl+e → input handler sees raw='e';
|
|
101
|
+
// without this guard we used to append 'e'.)
|
|
102
|
+
if (key.ctrl || key.meta) {
|
|
103
|
+
if (mode !== "settings" && key.ctrl && raw === "u") {
|
|
104
|
+
// ctrl+U: clear the whole line — standard readline behavior.
|
|
105
|
+
setBuffer("");
|
|
106
|
+
store.patch({ input: { ...state.input, buffer: "" } });
|
|
107
|
+
return;
|
|
70
108
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
109
|
+
if (key.ctrl && raw === "w") {
|
|
110
|
+
const next = deleteWordBackward(buffer);
|
|
111
|
+
setBuffer(next);
|
|
112
|
+
store.patch({ input: { ...state.input, buffer: next } });
|
|
113
|
+
return;
|
|
76
114
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (key.tab) {
|
|
118
|
+
if (mode === "settings") {
|
|
119
|
+
const field = SETTINGS_FIELDS[settingsField % SETTINGS_FIELDS.length];
|
|
120
|
+
if (field === "pause" && swarm && lc) {
|
|
121
|
+
const next = !swarm.paused;
|
|
122
|
+
swarm.setPaused(next);
|
|
123
|
+
lc.paused = next;
|
|
124
|
+
lc.dirty = true;
|
|
125
|
+
callbacks.settingsTick();
|
|
126
|
+
}
|
|
127
|
+
const next = settingsField + 1;
|
|
128
|
+
setBuffer("");
|
|
129
|
+
if (next >= SETTINGS_FIELDS.length) {
|
|
130
|
+
setSettingsField(0);
|
|
131
|
+
store.patch({ input: { mode: "none", buffer: "", settingsField: 0 } });
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
setSettingsField(next);
|
|
135
|
+
store.patch({ input: { mode: "settings", buffer: "", settingsField: next } });
|
|
136
|
+
}
|
|
80
137
|
}
|
|
138
|
+
// Tab in steer/ask modes is a no-op, not a submit or a "tab character".
|
|
81
139
|
return;
|
|
82
140
|
}
|
|
83
141
|
if (key.backspace || key.delete) {
|
|
@@ -86,9 +144,11 @@ export function InputLayer({ store, callbacks, onToast }) {
|
|
|
86
144
|
store.patch({ input: { ...state.input, buffer: next } });
|
|
87
145
|
return;
|
|
88
146
|
}
|
|
89
|
-
// Typed char(s) — raw is the string for this event
|
|
147
|
+
// Typed printable char(s) — raw is the string for this event. Strip any
|
|
148
|
+
// control chars (lone ESC flushes, \n linefeeds parseKeypress labels as
|
|
149
|
+
// 'enter', partial ESC [ sequences) before touching the buffer.
|
|
90
150
|
if (raw && raw.length > 0) {
|
|
91
|
-
let text = raw;
|
|
151
|
+
let text = sanitizeTyped(raw);
|
|
92
152
|
if (mode === "settings") {
|
|
93
153
|
const field = SETTINGS_FIELDS[settingsField % SETTINGS_FIELDS.length];
|
|
94
154
|
if (NUMERIC_SETTINGS_FIELDS.has(field))
|
|
@@ -149,6 +209,10 @@ export function InputLayer({ store, callbacks, onToast }) {
|
|
|
149
209
|
const code = raw.charCodeAt(0);
|
|
150
210
|
if (code < 0x20 || code > 0x7E)
|
|
151
211
|
return;
|
|
212
|
+
// Any ctrl/meta combo (that isn't one of the specific hotkeys above) is
|
|
213
|
+
// nav-adjacent on most terminals; ignore instead of matching "c"/"i" etc.
|
|
214
|
+
if (key.ctrl || key.meta)
|
|
215
|
+
return;
|
|
152
216
|
const toast = (msg) => onToast(msg);
|
|
153
217
|
switch (raw.toLowerCase()) {
|
|
154
218
|
case "?":
|
|
@@ -225,22 +289,58 @@ export function InputLayer({ store, callbacks, onToast }) {
|
|
|
225
289
|
const state = useSyncExternalStore(store.subscribe, store.get, store.get);
|
|
226
290
|
if (state.input.mode === "none")
|
|
227
291
|
return null;
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
return _jsx(InputPrompt, { mode: state.input.mode, buffer: buffer, settingsField: settingsField, state: state, caret: caret });
|
|
292
|
+
const caretOn = state.tick % 2 === 0;
|
|
293
|
+
return (_jsx(InputPrompt, { mode: state.input.mode, buffer: buffer, settingsField: settingsField, state: state, caretOn: caretOn }));
|
|
231
294
|
}
|
|
232
|
-
function
|
|
295
|
+
function terminalWidth() { return Math.max((process.stdout.columns ?? 80) || 80, 60); }
|
|
296
|
+
function InputPrompt({ mode, buffer, settingsField, state, caretOn }) {
|
|
297
|
+
const termW = terminalWidth();
|
|
298
|
+
const boxW = Math.min(Math.max(44, termW - 6), 120);
|
|
299
|
+
const innerW = boxW - 6;
|
|
300
|
+
const accent = mode === "settings" ? chalk.yellow : mode === "steer" ? chalk.cyan : chalk.magenta;
|
|
301
|
+
const borderColor = mode === "settings" ? "yellow" : mode === "steer" ? "cyan" : "magenta";
|
|
302
|
+
const title = mode === "steer" ? "Steer next wave"
|
|
303
|
+
: mode === "ask" ? "Ask the planner"
|
|
304
|
+
: "Settings";
|
|
305
|
+
let subtitle;
|
|
306
|
+
let hint;
|
|
307
|
+
let currentLine = null;
|
|
308
|
+
let filteredBuffer = buffer;
|
|
233
309
|
if (mode === "settings") {
|
|
234
310
|
const total = SETTINGS_FIELDS.length;
|
|
235
311
|
const field = SETTINGS_FIELDS[settingsField % total];
|
|
236
|
-
|
|
312
|
+
subtitle = SETTINGS_LABELS[field];
|
|
237
313
|
const current = readSettingValue(field, state.liveConfig, state.swarm);
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
314
|
+
currentLine = chalk.dim(`current: ${chalk.white(current)}`);
|
|
315
|
+
hint = field === "pause"
|
|
316
|
+
? chalk.dim(`Enter toggle \u00b7 Tab skip \u00b7 Esc exit`)
|
|
317
|
+
: chalk.dim(`[${settingsField + 1}/${total}] Enter save \u00b7 Tab skip \u00b7 Esc exit`);
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
subtitle = mode === "steer"
|
|
321
|
+
? "queued as the next wave's seed"
|
|
322
|
+
: "planner answers inline — you keep working";
|
|
323
|
+
const action = mode === "steer" ? "queue" : "send";
|
|
324
|
+
hint = chalk.dim(`Enter ${action} \u00b7 Esc cancel \u00b7 Ctrl+U clear \u00b7 Ctrl+W del word`);
|
|
242
325
|
}
|
|
243
|
-
|
|
244
|
-
const
|
|
245
|
-
|
|
326
|
+
// Word-wrap the buffer so long entries don't blow past the box edge.
|
|
327
|
+
const bufferLines = filteredBuffer.length === 0
|
|
328
|
+
? [""]
|
|
329
|
+
: wrap(filteredBuffer, Math.max(20, innerW));
|
|
330
|
+
const caret = caretOn ? accent("\u2588") : " ";
|
|
331
|
+
const lastIdx = bufferLines.length - 1;
|
|
332
|
+
// Char counter — dims normally, warns at 80%, red at 95%.
|
|
333
|
+
const pct = buffer.length / MAX_INPUT_LEN;
|
|
334
|
+
const counter = buffer.length === 0 ? "" :
|
|
335
|
+
pct >= 0.95 ? chalk.red(`${buffer.length}/${MAX_INPUT_LEN}`)
|
|
336
|
+
: pct >= 0.8 ? chalk.yellow(`${buffer.length}/${MAX_INPUT_LEN}`)
|
|
337
|
+
: chalk.dim(`${buffer.length}/${MAX_INPUT_LEN}`);
|
|
338
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, marginLeft: 2, borderStyle: "round", borderColor: borderColor, width: boxW, children: [_jsxs(Text, { children: [" ", accent("\u25C6"), " ", chalk.bold.white(title), " ", chalk.dim(subtitle)] }), currentLine ? _jsxs(Text, { children: [" ", currentLine] }) : null, bufferLines.map((ln, i) => {
|
|
339
|
+
const showCaret = i === lastIdx;
|
|
340
|
+
const pad = Math.max(0, innerW - visibleLen(ln));
|
|
341
|
+
const counterSuffix = showCaret && counter
|
|
342
|
+
? " " + counter
|
|
343
|
+
: "";
|
|
344
|
+
return (_jsxs(Text, { children: [" ", accent("\u203A "), ln, showCaret ? caret : " ", " ".repeat(pad), counterSuffix] }, i));
|
|
345
|
+
}), _jsxs(Text, { children: [" ", hint] })] }));
|
|
246
346
|
}
|
package/dist/ui/overlay.js
CHANGED
|
@@ -3,40 +3,52 @@ import { Text, Box } from "ink";
|
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { wrap } from "./primitives.js";
|
|
5
5
|
function terminalWidth() { return Math.max((process.stdout.columns ?? 80) || 80, 60); }
|
|
6
|
+
const ASK_BODY_LINES = 8;
|
|
7
|
+
const DEBRIEF_BODY_LINES = 6;
|
|
6
8
|
export function Overlay({ ask, debrief }) {
|
|
7
9
|
if (!ask && !debrief)
|
|
8
10
|
return null;
|
|
9
11
|
const w = terminalWidth();
|
|
10
|
-
const
|
|
12
|
+
const boxW = Math.min(Math.max(44, w - 6), 120);
|
|
13
|
+
const innerW = boxW - 4;
|
|
11
14
|
if (ask) {
|
|
12
|
-
const title = ask.streaming ? chalk.cyan("Ask
|
|
13
|
-
|
|
15
|
+
const title = ask.streaming ? chalk.cyan("Ask") + chalk.dim(" (streaming\u2026)")
|
|
16
|
+
: ask.error ? chalk.red("Ask") + chalk.dim(" (error)")
|
|
17
|
+
: chalk.bold.white("Ask");
|
|
18
|
+
const qLines = wrap(ask.question, innerW - 4);
|
|
14
19
|
const bodyLines = [];
|
|
20
|
+
let hiddenExtra = 0;
|
|
15
21
|
if (ask.error) {
|
|
16
|
-
for (const wl of wrap(`Error: ${ask.error}`,
|
|
22
|
+
for (const wl of wrap(`Error: ${ask.error}`, innerW - 2))
|
|
17
23
|
bodyLines.push(chalk.red(wl));
|
|
18
24
|
}
|
|
19
25
|
else if (ask.answer) {
|
|
20
|
-
|
|
21
|
-
|
|
26
|
+
const rawLines = ask.answer.split("\n");
|
|
27
|
+
const visible = rawLines.slice(0, ASK_BODY_LINES);
|
|
28
|
+
hiddenExtra = Math.max(0, rawLines.length - visible.length);
|
|
29
|
+
for (const ln of visible) {
|
|
30
|
+
for (const wl of wrap(ln, innerW - 2))
|
|
22
31
|
bodyLines.push(wl);
|
|
23
32
|
}
|
|
24
33
|
}
|
|
25
34
|
else if (ask.streaming) {
|
|
26
35
|
bodyLines.push(chalk.dim("\u2026"));
|
|
27
36
|
}
|
|
28
|
-
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, marginLeft: 2, borderStyle: "round", borderColor: "cyan", width:
|
|
37
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, marginLeft: 2, borderStyle: "round", borderColor: "cyan", width: boxW, children: [_jsxs(Text, { children: [" ", title] }), qLines.map((ql, i) => (_jsxs(Text, { children: [" ", i === 0 ? chalk.dim("Q:") + " " : " ", ql] }, `q${i}`))), bodyLines.map((l, i) => _jsxs(Text, { children: [" ", l] }, i)), hiddenExtra > 0 ? _jsxs(Text, { children: [" ", chalk.dim(`\u2026 + ${hiddenExtra} more line${hiddenExtra === 1 ? "" : "s"} \u00b7 press Enter to open`)] }) : null] }));
|
|
29
38
|
}
|
|
30
39
|
if (debrief) {
|
|
31
40
|
const title = debrief.label
|
|
32
41
|
? `${chalk.bold.white("Debrief")} ${chalk.dim("\u00b7")} ${chalk.white(debrief.label)}`
|
|
33
42
|
: chalk.bold.white("Debrief");
|
|
34
43
|
const lines = [];
|
|
35
|
-
|
|
36
|
-
|
|
44
|
+
const rawLines = debrief.text.split("\n");
|
|
45
|
+
const visible = rawLines.slice(0, DEBRIEF_BODY_LINES);
|
|
46
|
+
const hiddenExtra = Math.max(0, rawLines.length - visible.length);
|
|
47
|
+
for (const ln of visible) {
|
|
48
|
+
for (const wl of wrap(ln, innerW - 2))
|
|
37
49
|
lines.push(wl);
|
|
38
50
|
}
|
|
39
|
-
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, marginLeft: 2, borderStyle: "round", borderColor: "green", width:
|
|
51
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, marginLeft: 2, borderStyle: "round", borderColor: "green", width: boxW, children: [_jsxs(Text, { children: [" ", title] }), lines.map((l, i) => _jsxs(Text, { children: [" ", l] }, i)), hiddenExtra > 0 ? _jsxs(Text, { children: [" ", chalk.dim(`\u2026 + ${hiddenExtra} more line${hiddenExtra === 1 ? "" : "s"}`)] }) : null] }));
|
|
40
52
|
}
|
|
41
53
|
return null;
|
|
42
54
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-overnight",
|
|
3
|
-
"version": "1.25.
|
|
3
|
+
"version": "1.25.46",
|
|
4
4
|
"description": "Parallel Claude agents in git worktrees with a usage cap that reserves headroom for your interactive Claude Code. Crash-safe resume. Provider-agnostic model catalog (Anthropic, Cursor, OpenAI, Gemini, DeepSeek, Llama, Qwen) with capability-based task scoping.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-overnight",
|
|
3
|
-
"version": "1.25.
|
|
3
|
+
"version": "1.25.46",
|
|
4
4
|
"description": "Claude Code skill for understanding, installing, and inspecting claude-overnight runs -- parallel Claude agents in git worktrees with thinking waves, multi-wave steering, and crash-safe resume. Supports Cursor API Proxy, Qwen, OpenRouter.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Francesco Fornace"
|