prompts-gpt 0.2.8

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/sweep.js ADDED
@@ -0,0 +1,765 @@
1
+ import { existsSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { mkdir, open as fsOpen, readFile, writeFile, readdir, unlink, stat, rm } from "node:fs/promises";
4
+ import { spawnSync } from "node:child_process";
5
+ import { loadRunConfig, detectProviders, normalizeOrchestrationAgent, resolveRunProvider, resolveTimeoutSeconds, resolveDefaultPromptFile, assertPromptFitsLaunch, executeProviderCommandWithRetries, captureWorktreeStatus, buildWorktreeDelta, appendFileSafe, aggregateTokenUsage, DEFAULT_MODELS, extractTokenUsageFromLog, } from "./runtime.js";
6
+ import { getAttribution } from "./index.js";
7
+ const LOCK_FILE_NAME = ".sweep.lock";
8
+ const LOCK_STALE_MS = 6 * 60 * 60 * 1000; // 6 hours
9
+ const DEFAULT_ITERATIONS = 1;
10
+ const DEFAULT_MAX_RETRIES = 2;
11
+ const DEFAULT_ITERATION_TIMEOUT = 5400;
12
+ const DEFAULT_SUMMARY_LINES = 40;
13
+ const DEFAULT_MAX_RUN_DIRS = 20;
14
+ const GIT_SAFETY_RULE = "\nNEVER run `git stash`, `git reset`, `git checkout`, `git commit`, `git push`, or any destructive git command.\n";
15
+ // ── Lock management ──────────────────────────────────────────────────────────
16
+ export async function acquireSweepLock(cwd) {
17
+ const lockPath = path.resolve(cwd, LOCK_FILE_NAME);
18
+ if (existsSync(lockPath)) {
19
+ try {
20
+ const content = JSON.parse(await readFile(lockPath, "utf8"));
21
+ const lockAge = Date.now() - new Date(content.startedAt ?? 0).getTime();
22
+ const pidAlive = typeof content.pid === "number" && content.pid > 0
23
+ ? isProcessAlive(content.pid)
24
+ : false;
25
+ if (pidAlive && lockAge < LOCK_STALE_MS) {
26
+ throw new Error(`Another sweep is running (PID ${content.pid}, started ${content.startedAt}).\n` +
27
+ `If the process is dead, remove the lock manually:\n` +
28
+ ` rm ${lockPath}`);
29
+ }
30
+ if (lockAge >= LOCK_STALE_MS) {
31
+ // Stale lock — process likely crashed without cleanup
32
+ }
33
+ try {
34
+ await unlink(lockPath);
35
+ }
36
+ catch { }
37
+ }
38
+ catch (error) {
39
+ if (error.message.includes("Another sweep"))
40
+ throw error;
41
+ }
42
+ }
43
+ const payload = {
44
+ pid: process.pid,
45
+ startedAt: new Date().toISOString(),
46
+ cwd,
47
+ };
48
+ const lockWriteController = new AbortController();
49
+ const lockTimeout = setTimeout(() => lockWriteController.abort(), 10_000);
50
+ lockTimeout.unref?.();
51
+ try {
52
+ await writeFile(lockPath, JSON.stringify(payload, null, 2), { mode: 0o644, signal: lockWriteController.signal });
53
+ if (!existsSync(lockPath)) {
54
+ throw new Error(`Failed to create sweep lock file: ${lockPath}. Filesystem may be full.`);
55
+ }
56
+ }
57
+ finally {
58
+ clearTimeout(lockTimeout);
59
+ }
60
+ return lockPath;
61
+ }
62
+ export async function releaseSweepLock(cwd) {
63
+ const lockPath = path.resolve(cwd, LOCK_FILE_NAME);
64
+ try {
65
+ await unlink(lockPath);
66
+ }
67
+ catch {
68
+ // Lock already removed
69
+ }
70
+ }
71
+ function isProcessAlive(pid) {
72
+ if (!Number.isInteger(pid) || pid <= 0)
73
+ return false;
74
+ try {
75
+ process.kill(pid, 0);
76
+ return true;
77
+ }
78
+ catch (err) {
79
+ const code = err.code;
80
+ if (code === "EPERM")
81
+ return true;
82
+ if (code === "ESRCH")
83
+ return false;
84
+ if (process.platform === "win32") {
85
+ try {
86
+ const result = spawnSync("tasklist", ["/FI", `PID eq ${pid}`, "/NH"], { encoding: "utf8", timeout: 3000, windowsHide: true });
87
+ return result.stdout?.includes(String(pid)) ?? false;
88
+ }
89
+ catch {
90
+ return false;
91
+ }
92
+ }
93
+ return false;
94
+ }
95
+ }
96
+ // ── Stream-JSON parsing ──────────────────────────────────────────────────────
97
+ export function parseStreamJsonToolCounts(logContent) {
98
+ const counts = { reads: 0, writes: 0, shells: 0, searches: 0, webSearches: 0, total: 0 };
99
+ if (!logContent || !logContent.includes('"tool_use"'))
100
+ return counts;
101
+ for (const line of logContent.split("\n")) {
102
+ const trimmed = line.trim();
103
+ if (!trimmed.startsWith("{") || !trimmed.includes("tool_use"))
104
+ continue;
105
+ let parsed = null;
106
+ try {
107
+ parsed = JSON.parse(trimmed);
108
+ }
109
+ catch {
110
+ continue;
111
+ }
112
+ if (!parsed || parsed.type !== "tool_use")
113
+ continue;
114
+ const tool = (parsed.tool ?? parsed.toolName ?? "").toLowerCase();
115
+ if (tool.includes("read") || tool === "file_read")
116
+ counts.reads++;
117
+ else if (tool.includes("write") || tool.includes("edit") || tool === "file_write" || tool === "str_replace")
118
+ counts.writes++;
119
+ else if (tool.includes("shell") || tool === "bash" || tool === "command")
120
+ counts.shells++;
121
+ else if (tool.includes("grep") || tool.includes("glob") || tool.includes("search") || tool === "file_search") {
122
+ if (tool.includes("web"))
123
+ counts.webSearches++;
124
+ else
125
+ counts.searches++;
126
+ }
127
+ else if (tool.includes("web"))
128
+ counts.webSearches++;
129
+ counts.total++;
130
+ }
131
+ return counts;
132
+ }
133
+ export function streamJsonHasResult(logContent) {
134
+ for (const line of logContent.split("\n")) {
135
+ const trimmed = line.trim();
136
+ if (!trimmed.startsWith("{"))
137
+ continue;
138
+ try {
139
+ const parsed = JSON.parse(trimmed);
140
+ if (parsed.type === "result")
141
+ return true;
142
+ }
143
+ catch {
144
+ continue;
145
+ }
146
+ }
147
+ return false;
148
+ }
149
+ // ── Summary extraction ───────────────────────────────────────────────────────
150
+ export function extractIterationSummary(logContent, maxLines) {
151
+ const lines = [];
152
+ for (const rawLine of logContent.split("\n")) {
153
+ const trimmed = rawLine.trim();
154
+ if (!trimmed.startsWith("{"))
155
+ continue;
156
+ try {
157
+ const parsed = JSON.parse(trimmed);
158
+ if (parsed.type === "assistant" || parsed.type === "text") {
159
+ const text = parsed.content ?? parsed.message ?? "";
160
+ if (text.trim()) {
161
+ lines.push(...text.split("\n"));
162
+ }
163
+ }
164
+ }
165
+ catch {
166
+ continue;
167
+ }
168
+ if (lines.length >= maxLines * 2)
169
+ break;
170
+ }
171
+ return lines.slice(0, maxLines);
172
+ }
173
+ // ── Prompt building ──────────────────────────────────────────────────────────
174
+ export function buildIterationPrompt(basePrompt, iteration, totalIterations, previousSummary, config) {
175
+ const parts = [];
176
+ if (totalIterations > 1) {
177
+ parts.push(`[Iteration ${iteration}/${totalIterations}]`);
178
+ }
179
+ if (config.phase) {
180
+ parts.push(`[Phase: ${config.phase}]`);
181
+ }
182
+ if (parts.length > 0)
183
+ parts.push("");
184
+ parts.push(basePrompt);
185
+ if (config.disallowDestructiveGit) {
186
+ parts.push(GIT_SAFETY_RULE);
187
+ }
188
+ if (previousSummary && previousSummary.length > 0) {
189
+ parts.push("");
190
+ parts.push("## Previous iteration summary");
191
+ parts.push("");
192
+ parts.push("The previous iteration completed the following work. Continue from where it left off — do NOT repeat already-completed items:");
193
+ parts.push("");
194
+ parts.push(previousSummary.join("\n"));
195
+ }
196
+ return parts.join("\n");
197
+ }
198
+ export async function runPreFlight(input) {
199
+ const errors = [];
200
+ const warnings = [];
201
+ if (!input.providerHealth.available) {
202
+ errors.push(`Provider ${input.provider} is not available. Install CLI or set PATH.`);
203
+ }
204
+ let promptSizeBytes = 0;
205
+ if (existsSync(input.promptFile)) {
206
+ const promptStat = await stat(input.promptFile);
207
+ promptSizeBytes = promptStat.size;
208
+ if (promptSizeBytes === 0) {
209
+ errors.push(`Prompt file is empty: ${input.promptFile}`);
210
+ }
211
+ }
212
+ else {
213
+ errors.push(`Prompt file not found: ${input.promptFile}`);
214
+ }
215
+ const gitBranchResult = spawnSync("git", ["-C", input.cwd, "branch", "--show-current"], { encoding: "utf8", timeout: 10_000 });
216
+ const gitBranch = gitBranchResult.stdout?.trim() ?? "unknown";
217
+ const gitStatusResult = spawnSync("git", ["-C", input.cwd, "status", "--porcelain=v1", "-uall"], { encoding: "utf8", timeout: 30_000 });
218
+ const gitDirtyFiles = (gitStatusResult.stdout?.trim().split("\n").filter(Boolean) ?? []).length;
219
+ let diskFreeMb = 0;
220
+ try {
221
+ if (process.platform === "win32") {
222
+ const driveLetter = input.cwd.charAt(0).toUpperCase();
223
+ const psResult = spawnSync("powershell", ["-NoProfile", "-Command", `(Get-PSDrive ${driveLetter}).Free`], { encoding: "utf8", timeout: 10_000, windowsHide: true });
224
+ const freeBytes = parseInt((psResult.stdout ?? "").trim(), 10);
225
+ if (Number.isFinite(freeBytes) && freeBytes > 0) {
226
+ diskFreeMb = Math.floor(freeBytes / (1024 * 1024));
227
+ }
228
+ else {
229
+ const wmicResult = spawnSync("wmic", ["logicaldisk", "where", `DeviceID='${driveLetter}:'`, "get", "FreeSpace", "/value"], { encoding: "utf8", timeout: 10_000, windowsHide: true });
230
+ const match = wmicResult.stdout?.match(/FreeSpace=(\d+)/);
231
+ diskFreeMb = match ? Math.floor(parseInt(match[1], 10) / (1024 * 1024)) : 0;
232
+ }
233
+ }
234
+ else {
235
+ let dfResult = spawnSync("df", ["-m", input.cwd], { encoding: "utf8", timeout: 10_000, windowsHide: true });
236
+ if (dfResult.status !== 0) {
237
+ dfResult = spawnSync("df", ["-k", input.cwd], { encoding: "utf8", timeout: 10_000, windowsHide: true });
238
+ const dfLine = dfResult.stdout?.split("\n")[1] ?? "";
239
+ const dfParts = dfLine.trim().split(/\s+/);
240
+ diskFreeMb = Math.floor((parseInt(dfParts[3] ?? "0", 10) || 0) / 1024);
241
+ }
242
+ else {
243
+ const dfLine = dfResult.stdout?.split("\n")[1] ?? "";
244
+ const dfParts = dfLine.trim().split(/\s+/);
245
+ diskFreeMb = parseInt(dfParts[3] ?? "0", 10) || 0;
246
+ }
247
+ }
248
+ if (diskFreeMb > 0 && diskFreeMb < 500) {
249
+ warnings.push(`Low disk space: ${diskFreeMb}MB free`);
250
+ }
251
+ }
252
+ catch {
253
+ warnings.push("Could not determine disk space");
254
+ }
255
+ if (input.iterations < 1 || input.iterations > 50) {
256
+ errors.push(`Iterations must be between 1 and 50, got ${input.iterations}`);
257
+ }
258
+ return {
259
+ cwd: input.cwd,
260
+ provider: input.provider,
261
+ providerBin: input.providerHealth.bin,
262
+ providerAvailable: input.providerHealth.available,
263
+ model: input.model,
264
+ nodeVersion: process.version,
265
+ gitBranch,
266
+ gitDirtyFiles,
267
+ diskFreeMb,
268
+ promptFile: input.promptFile,
269
+ promptSizeBytes,
270
+ iterations: input.iterations,
271
+ iterationTimeoutSeconds: input.iterationTimeoutSeconds,
272
+ maxRetries: input.maxRetries,
273
+ disallowDestructiveGit: input.config.safety.disallowDestructiveGit,
274
+ errors,
275
+ warnings,
276
+ };
277
+ }
278
+ // ── Run directory rotation ───────────────────────────────────────────────────
279
+ async function rotateRunDirs(artifactsDir, maxDirs) {
280
+ if (!existsSync(artifactsDir))
281
+ return;
282
+ if (maxDirs < 1)
283
+ return;
284
+ try {
285
+ const resolvedArtifactsDir = path.resolve(artifactsDir);
286
+ const entries = await readdir(resolvedArtifactsDir, { withFileTypes: true });
287
+ const dirs = entries
288
+ .filter((e) => e.isDirectory())
289
+ .map((e) => {
290
+ const fullPath = path.join(resolvedArtifactsDir, e.name);
291
+ return { name: e.name, path: fullPath };
292
+ })
293
+ .filter((d) => {
294
+ const normalized = path.resolve(d.path);
295
+ return normalized.startsWith(resolvedArtifactsDir + path.sep) && normalized !== resolvedArtifactsDir;
296
+ });
297
+ if (dirs.length <= maxDirs)
298
+ return;
299
+ const withStats = await Promise.all(dirs.map(async (d) => {
300
+ const s = await stat(d.path).catch(() => null);
301
+ return { ...d, mtime: s?.mtimeMs ?? 0 };
302
+ }));
303
+ withStats.sort((a, b) => a.mtime - b.mtime || a.name.localeCompare(b.name));
304
+ const toRemove = withStats.slice(0, withStats.length - maxDirs);
305
+ for (const dir of toRemove) {
306
+ await rm(dir.path, { recursive: true, force: true }).catch(() => undefined);
307
+ }
308
+ }
309
+ catch {
310
+ // Best-effort rotation
311
+ }
312
+ }
313
+ // ── Manifest writer ──────────────────────────────────────────────────────────
314
+ export async function writeSweepManifest(runDir, result, config) {
315
+ const attribution = getAttribution();
316
+ const manifest = {
317
+ version: 2,
318
+ runId: result.runId,
319
+ model: result.model,
320
+ provider: result.provider,
321
+ cwd: result.cwd,
322
+ runDir: path.relative(result.cwd, result.runDir) || ".",
323
+ started: result.startedAt,
324
+ finished: result.finishedAt,
325
+ totalIterations: result.totalIterations,
326
+ succeeded: result.succeeded,
327
+ failed: result.failed,
328
+ elapsedMs: result.totalDurationMs,
329
+ promptFile: result.promptFile,
330
+ safety: { disallowDestructiveGit: config.safety.disallowDestructiveGit },
331
+ attribution: {
332
+ generatedBy: `prompts-gpt@${attribution.version}`,
333
+ accountId: attribution.accountId,
334
+ buildFingerprint: `${attribution.version}/${attribution.accountId}`,
335
+ },
336
+ iterations: result.iterations.map((iter) => ({
337
+ iteration: iter.iteration,
338
+ status: iter.status,
339
+ exitCode: iter.exitCode,
340
+ durationMs: iter.durationMs,
341
+ attempts: iter.attempts,
342
+ toolCounts: iter.toolCounts,
343
+ tokenUsage: iter.tokenUsage,
344
+ summaryLines: iter.summary.length,
345
+ })),
346
+ tokenUsage: result.tokenUsage,
347
+ };
348
+ const manifestPath = path.join(runDir, "sweep-manifest.json");
349
+ await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
350
+ return manifestPath;
351
+ }
352
+ // ── Main sweep engine ────────────────────────────────────────────────────────
353
+ export async function sweepPrompt(input) {
354
+ const cwd = path.resolve(input.cwd ?? process.cwd());
355
+ const config = await loadRunConfig(cwd);
356
+ const providers = await detectProviders(cwd);
357
+ const agent = normalizeOrchestrationAgent(input.agent ?? config.defaultAgent);
358
+ const provider = resolveRunProvider(agent, providers, config.providerOrder);
359
+ const providerHealth = providers.find((p) => p.provider === provider);
360
+ if (!providerHealth || !providerHealth.available) {
361
+ throw new Error(`Provider ${provider} is not available. ` +
362
+ `Run \`prompts-gpt doctor\` to check provider installation, ` +
363
+ `or use \`--agent <provider>\` to select a different provider.`);
364
+ }
365
+ const model = input.model?.trim() || config.modelOverrides[provider]?.trim() || DEFAULT_MODELS[provider] || "auto";
366
+ const promptFile = input.promptFile
367
+ ? path.resolve(cwd, input.promptFile)
368
+ : await resolveDefaultPromptFile(cwd, config);
369
+ const rawIterations = input.iterations ?? DEFAULT_ITERATIONS;
370
+ const iterations = Math.max(1, Math.min(Math.trunc(rawIterations), 50));
371
+ const iterationTimeoutSeconds = resolveTimeoutSeconds(input.iterationTimeoutSeconds, DEFAULT_ITERATION_TIMEOUT);
372
+ const maxRetries = Math.max(0, Math.min(Math.trunc(input.maxRetries ?? DEFAULT_MAX_RETRIES), 10));
373
+ const maxRunDirs = Math.max(1, Math.min(Math.trunc(input.maxRunDirs ?? DEFAULT_MAX_RUN_DIRS), 200));
374
+ const summaryLines = input.summaryLines ?? DEFAULT_SUMMARY_LINES;
375
+ const notify = input.onProgress ?? (() => undefined);
376
+ // Pre-flight
377
+ const preflight = await runPreFlight({
378
+ cwd,
379
+ provider,
380
+ providerHealth,
381
+ model,
382
+ promptFile,
383
+ iterations,
384
+ iterationTimeoutSeconds,
385
+ maxRetries,
386
+ config,
387
+ });
388
+ notify({ type: "preflight", message: JSON.stringify(preflight) });
389
+ if (preflight.errors.length > 0) {
390
+ throw new Error(`Pre-flight failed:\n${preflight.errors.map((e) => ` - ${e}`).join("\n")}`);
391
+ }
392
+ // Dry-run mode — no lock needed
393
+ if (input.dryRun) {
394
+ const dryResult = {
395
+ runId: "dry-run",
396
+ provider,
397
+ model,
398
+ cwd,
399
+ runDir: "",
400
+ manifestFile: "",
401
+ iterations: [],
402
+ totalIterations: iterations,
403
+ succeeded: 0,
404
+ failed: 0,
405
+ startedAt: new Date().toISOString(),
406
+ finishedAt: new Date().toISOString(),
407
+ totalDurationMs: 0,
408
+ worktreeBeforeFile: "",
409
+ worktreeAfterFile: "",
410
+ worktreeDeltaFile: "",
411
+ promptFile,
412
+ dryRun: true,
413
+ tokenUsage: null,
414
+ };
415
+ notify({ type: "sweep_end", result: dryResult });
416
+ return dryResult;
417
+ }
418
+ // Acquire lock after pre-flight passes and dry-run is handled
419
+ await acquireSweepLock(cwd);
420
+ const runId = input.runId?.trim() || `sweep-${new Date().toISOString().replace(/[-:T]/g, "").slice(0, 14)}-${process.pid}`;
421
+ const artifactsDir = input.artifactsDir ? path.resolve(cwd, input.artifactsDir) : config.artifactsDir;
422
+ const runDir = path.resolve(artifactsDir, runId);
423
+ let basePrompt;
424
+ try {
425
+ await mkdir(runDir, { recursive: true });
426
+ basePrompt = await readFile(promptFile, "utf8");
427
+ }
428
+ catch (err) {
429
+ await rm(runDir, { recursive: true, force: true }).catch(() => undefined);
430
+ await releaseSweepLock(cwd);
431
+ throw err;
432
+ }
433
+ await rotateRunDirs(artifactsDir, maxRunDirs);
434
+ const worktreeBeforeFile = path.join(runDir, "worktree-before.txt");
435
+ const worktreeAfterFile = path.join(runDir, "worktree-after.txt");
436
+ const worktreeDeltaFile = path.join(runDir, "worktree-delta.diff");
437
+ const worktreeBefore = captureWorktreeStatus(cwd);
438
+ await writeFile(worktreeBeforeFile, worktreeBefore);
439
+ const startedAt = new Date();
440
+ const startedMs = Date.now();
441
+ const iterationResults = [];
442
+ let previousSummary = null;
443
+ await writeFile(path.join(runDir, "base-prompt.md"), basePrompt);
444
+ let cleanedUp = false;
445
+ let cancelled = false;
446
+ const onExit = () => {
447
+ if (cleanedUp)
448
+ return;
449
+ cleanedUp = true;
450
+ releaseSweepLock(cwd).catch(() => undefined);
451
+ };
452
+ const onSigterm = () => {
453
+ cancelled = true;
454
+ onExit();
455
+ removeSignalHandlers();
456
+ process.kill(process.pid, "SIGTERM");
457
+ };
458
+ const onSigint = () => {
459
+ cancelled = true;
460
+ onExit();
461
+ removeSignalHandlers();
462
+ process.kill(process.pid, "SIGINT");
463
+ };
464
+ const removeSignalHandlers = () => {
465
+ process.removeListener("exit", onExit);
466
+ process.removeListener("SIGTERM", onSigterm);
467
+ process.removeListener("SIGINT", onSigint);
468
+ };
469
+ process.on("exit", onExit);
470
+ process.on("SIGTERM", onSigterm);
471
+ process.on("SIGINT", onSigint);
472
+ try {
473
+ for (let i = 1; i <= iterations; i++) {
474
+ if (cancelled)
475
+ break;
476
+ notify({ type: "iteration_start", iteration: i, total: iterations, provider, model });
477
+ const iterPrompt = buildIterationPrompt(basePrompt, i, iterations, previousSummary, { disallowDestructiveGit: config.safety.disallowDestructiveGit, phase: input.phase });
478
+ const iterPromptFile = path.join(runDir, `iteration-${i}-prompt.md`);
479
+ await writeFile(iterPromptFile, iterPrompt);
480
+ assertPromptFitsLaunch(provider, iterPrompt, iterPromptFile);
481
+ const iterLogFile = path.join(runDir, `iteration-${i}.log`);
482
+ const iterSummaryFile = path.join(runDir, `iteration-${i}.summary.md`);
483
+ // Create log file before starting so tailing can begin
484
+ await writeFile(iterLogFile, "");
485
+ notify({ type: "message", text: ` log: ${iterLogFile}`, elapsed: "0s" });
486
+ // Start live log monitoring
487
+ const logMonitor = startLiveLogMonitor(iterLogFile, Date.now(), notify, i, iterations, provider);
488
+ const iterStartMs = Date.now();
489
+ let exitCode = 1;
490
+ let attempts = 0;
491
+ let commandPreview = "";
492
+ let iterStatus = "failed";
493
+ const maxAttempts = maxRetries + 1;
494
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
495
+ attempts = attempt;
496
+ try {
497
+ const result = await executeProviderCommandWithRetries({
498
+ provider,
499
+ bin: providerHealth.bin,
500
+ model,
501
+ cwd,
502
+ promptText: iterPrompt,
503
+ promptFile: iterPromptFile,
504
+ summaryFile: iterSummaryFile,
505
+ logFile: iterLogFile,
506
+ timeoutSeconds: iterationTimeoutSeconds,
507
+ retryCount: 0,
508
+ approveMcps: input.approveMcps ?? true,
509
+ sandboxMode: input.sandboxMode ?? "workspace-write",
510
+ background: input.background,
511
+ permissionMode: input.permissionMode,
512
+ });
513
+ exitCode = result.exitCode;
514
+ commandPreview = result.commandPreview;
515
+ break;
516
+ }
517
+ catch (error) {
518
+ const errMsg = error instanceof Error ? error.message : String(error);
519
+ if (errMsg.includes("timed out") || errMsg.includes("PROVIDER_TIMEOUT")) {
520
+ iterStatus = "timeout";
521
+ }
522
+ if (attempt < maxAttempts) {
523
+ const backoffMs = Math.min(5_000 * (1 << Math.min(attempt - 1, 4)), 60_000);
524
+ notify({ type: "attempt_retry", iteration: i, attempt, maxAttempts, backoffMs });
525
+ await appendFileSafe(iterLogFile, `\n[retry] iteration ${i}, attempt ${attempt}/${maxAttempts}, backoff ${backoffMs}ms\n`);
526
+ await sleep(backoffMs);
527
+ continue;
528
+ }
529
+ exitCode = 1;
530
+ }
531
+ }
532
+ // Read log for analysis
533
+ let logContent = "";
534
+ try {
535
+ logContent = await readFile(iterLogFile, "utf8");
536
+ }
537
+ catch { /* no log */ }
538
+ // SIGTERM (143) with result in log = success
539
+ if (exitCode === 143 && streamJsonHasResult(logContent)) {
540
+ exitCode = 0;
541
+ }
542
+ // Classify status
543
+ if (exitCode === 0) {
544
+ iterStatus = "success";
545
+ }
546
+ else if (iterStatus !== "timeout") {
547
+ // Check stderr for auth errors (NOT the agent log which contains code discussions)
548
+ let stderrContent = "";
549
+ try {
550
+ const stderrFile = iterLogFile.replace(/\.log$/, ".stderr");
551
+ if (existsSync(stderrFile)) {
552
+ stderrContent = await readFile(stderrFile, "utf8");
553
+ }
554
+ }
555
+ catch { /* no stderr */ }
556
+ const authCheckContent = `${stderrContent}\n${logContent}`;
557
+ if (/unauthorized|authentication.*fail|not.*logged.*in/i.test(authCheckContent)) {
558
+ iterStatus = "auth_error";
559
+ }
560
+ else if (/cannot use this model/i.test(authCheckContent)) {
561
+ iterStatus = "model_error";
562
+ }
563
+ else {
564
+ iterStatus = "failed";
565
+ }
566
+ }
567
+ // Stop live log monitor
568
+ logMonitor.stop();
569
+ const toolCounts = parseStreamJsonToolCounts(logContent);
570
+ const tokenUsage = extractTokenUsageFromLog(logContent);
571
+ const summary = extractIterationSummary(logContent, summaryLines);
572
+ const iterDurationMs = Date.now() - iterStartMs;
573
+ // Write summary to file
574
+ if (summary.length > 0) {
575
+ await writeFile(iterSummaryFile, summary.join("\n"));
576
+ }
577
+ const iterResult = {
578
+ iteration: i,
579
+ status: iterStatus,
580
+ exitCode,
581
+ durationMs: iterDurationMs,
582
+ promptFile: iterPromptFile,
583
+ logFile: iterLogFile,
584
+ summaryFile: iterSummaryFile,
585
+ summary,
586
+ toolCounts,
587
+ tokenUsage,
588
+ attempts,
589
+ commandPreview,
590
+ };
591
+ iterationResults.push(iterResult);
592
+ notify({ type: "iteration_end", iteration: i, status: iterStatus, durationMs: iterDurationMs, provider, model });
593
+ if (summary.length > 0) {
594
+ notify({ type: "summary", iteration: i, lines: summary });
595
+ }
596
+ previousSummary = summary;
597
+ // Fatal errors stop the sweep
598
+ if (iterStatus === "auth_error" || iterStatus === "model_error") {
599
+ break;
600
+ }
601
+ }
602
+ }
603
+ finally {
604
+ removeSignalHandlers();
605
+ const worktreeAfter = captureWorktreeStatus(cwd);
606
+ await writeFile(worktreeAfterFile, worktreeAfter);
607
+ await writeFile(worktreeDeltaFile, buildWorktreeDelta(worktreeBefore, worktreeAfter));
608
+ await releaseSweepLock(cwd);
609
+ }
610
+ const totalDurationMs = Date.now() - startedMs;
611
+ const finishedAt = new Date();
612
+ const succeeded = iterationResults.filter((r) => r.status === "success").length;
613
+ const failed = iterationResults.filter((r) => r.status !== "success").length;
614
+ const result = {
615
+ runId,
616
+ provider,
617
+ model,
618
+ cwd,
619
+ runDir,
620
+ manifestFile: "",
621
+ iterations: iterationResults,
622
+ totalIterations: iterations,
623
+ succeeded,
624
+ failed,
625
+ startedAt: startedAt.toISOString(),
626
+ finishedAt: finishedAt.toISOString(),
627
+ totalDurationMs,
628
+ worktreeBeforeFile,
629
+ worktreeAfterFile,
630
+ worktreeDeltaFile,
631
+ promptFile,
632
+ dryRun: false,
633
+ tokenUsage: aggregateTokenUsage(iterationResults.map((iteration) => iteration.tokenUsage)),
634
+ };
635
+ const manifestFile = await writeSweepManifest(runDir, result, config).catch(() => "");
636
+ result.manifestFile = manifestFile;
637
+ notify({ type: "sweep_end", result });
638
+ return result;
639
+ }
640
+ function startLiveLogMonitor(logFile, startMs, notify, iteration, totalIterations, provider) {
641
+ let stopped = false;
642
+ let lastOffset = 0;
643
+ const counts = { reads: 0, writes: 0, shells: 0, searches: 0, webSearches: 0, total: 0 };
644
+ let lineBuf = "";
645
+ const poll = setInterval(async () => {
646
+ if (stopped)
647
+ return;
648
+ try {
649
+ let fh;
650
+ try {
651
+ fh = await fsOpen(logFile, "r");
652
+ }
653
+ catch {
654
+ return;
655
+ }
656
+ try {
657
+ const fstat = await fh.stat();
658
+ if (fstat.size <= lastOffset) {
659
+ return;
660
+ }
661
+ const readSize = Math.min(fstat.size - lastOffset, 256 * 1024);
662
+ const buf = Buffer.alloc(readSize);
663
+ const { bytesRead } = await fh.read(buf, 0, readSize, lastOffset);
664
+ lastOffset += bytesRead;
665
+ if (bytesRead === 0)
666
+ return;
667
+ const raw = lineBuf + buf.slice(0, bytesRead).toString("utf8");
668
+ const lastNewline = raw.lastIndexOf("\n");
669
+ if (lastNewline === -1) {
670
+ lineBuf = raw;
671
+ return;
672
+ }
673
+ lineBuf = raw.slice(lastNewline + 1);
674
+ const newContent = raw.slice(0, lastNewline);
675
+ for (const line of newContent.split("\n")) {
676
+ const trimmed = line.trim();
677
+ if (!trimmed.startsWith("{"))
678
+ continue;
679
+ let parsed = null;
680
+ try {
681
+ parsed = JSON.parse(trimmed);
682
+ }
683
+ catch {
684
+ continue;
685
+ }
686
+ if (!parsed || !parsed.type)
687
+ continue;
688
+ const elapsed = formatDurationShort(Date.now() - startMs);
689
+ if (parsed.type === "assistant" || parsed.type === "text") {
690
+ const text = parsed.message?.content?.[0]?.text || "";
691
+ if (text.trim()) {
692
+ notify({ type: "message", text: text.trim().slice(0, 120), elapsed });
693
+ }
694
+ }
695
+ else if (parsed.type === "tool_call" && parsed.subtype === "started") {
696
+ const tcKey = parsed.tool_call ? Object.keys(parsed.tool_call)[0] ?? "" : "";
697
+ const tc = parsed.tool_call?.[tcKey];
698
+ const arg = tc?.args?.path || tc?.args?.command || tc?.args?.pattern || tc?.args?.glob_pattern || "";
699
+ let action = "";
700
+ if (tcKey.includes("read") || tcKey === "readToolCall") {
701
+ counts.reads++;
702
+ action = "read";
703
+ }
704
+ else if (tcKey.includes("write") || tcKey.includes("edit") || tcKey === "writeToolCall" || tcKey === "editToolCall") {
705
+ counts.writes++;
706
+ action = "write";
707
+ }
708
+ else if (tcKey.includes("shell") || tcKey === "shellToolCall") {
709
+ counts.shells++;
710
+ action = "shell";
711
+ }
712
+ else if (tcKey.includes("grep") || tcKey.includes("glob") || tcKey.includes("search")) {
713
+ counts.searches++;
714
+ action = "search";
715
+ }
716
+ else {
717
+ action = tcKey.replace("ToolCall", "");
718
+ }
719
+ counts.total++;
720
+ const shortArg = arg.split("/").pop()?.slice(0, 50) || arg.slice(0, 50);
721
+ notify({ type: "tool", action, file: shortArg, counts: { ...counts } });
722
+ }
723
+ else if (parsed.type === "tool_use") {
724
+ const tool = (parsed.tool ?? parsed.toolName ?? "").toLowerCase();
725
+ if (tool.includes("read")) {
726
+ counts.reads++;
727
+ }
728
+ else if (tool.includes("write") || tool.includes("edit") || tool === "str_replace") {
729
+ counts.writes++;
730
+ }
731
+ else if (tool.includes("shell") || tool === "bash") {
732
+ counts.shells++;
733
+ }
734
+ else if (tool.includes("grep") || tool.includes("glob") || tool.includes("search")) {
735
+ counts.searches++;
736
+ }
737
+ counts.total++;
738
+ notify({ type: "tool", action: tool, file: "", counts: { ...counts } });
739
+ }
740
+ }
741
+ }
742
+ finally {
743
+ await fh.close();
744
+ }
745
+ }
746
+ catch { /* file not ready yet */ }
747
+ }, 1000);
748
+ poll.unref?.();
749
+ return {
750
+ stop() {
751
+ stopped = true;
752
+ clearInterval(poll);
753
+ },
754
+ };
755
+ }
756
+ function formatDurationShort(ms) {
757
+ const totalSec = Math.round(ms / 1000);
758
+ const m = Math.floor(totalSec / 60);
759
+ const s = totalSec % 60;
760
+ return m > 0 ? `${m}m${s.toString().padStart(2, "0")}s` : `${s}s`;
761
+ }
762
+ function sleep(ms) {
763
+ return new Promise((resolve) => setTimeout(resolve, ms));
764
+ }
765
+ //# sourceMappingURL=sweep.js.map