@groupchatai/claude-runner 0.4.5 → 0.4.7
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/index.js +159 -214
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -154,8 +154,7 @@ function pidTag(pid) {
|
|
|
154
154
|
return ` ${C.pid}[pid ${pid}]${C.reset}`;
|
|
155
155
|
}
|
|
156
156
|
function padForTag(pid) {
|
|
157
|
-
|
|
158
|
-
return " ".repeat(tagLen);
|
|
157
|
+
return " ".repeat(8 + String(pid).length);
|
|
159
158
|
}
|
|
160
159
|
function wrapLines(tag, pad, text, color) {
|
|
161
160
|
const lines = text.split("\n").filter((l) => l.trim());
|
|
@@ -203,6 +202,16 @@ function formatStreamEvent(event, pid) {
|
|
|
203
202
|
}
|
|
204
203
|
case "tool_result":
|
|
205
204
|
return null;
|
|
205
|
+
case "rate_limit_event": {
|
|
206
|
+
const info = event.rate_limit_info;
|
|
207
|
+
if (info && typeof info === "object" && !Array.isArray(info)) {
|
|
208
|
+
const payload = safeFormatRateLimitPayload(info);
|
|
209
|
+
const blocking = isRateLimitEventBlocking(event);
|
|
210
|
+
const icon = blocking ? "\u{1F6AB}" : "\u2139\uFE0F ";
|
|
211
|
+
return `${tag} ${C.dim}${icon} ${payload}${blocking ? "" : " (non-blocking)"}${C.reset}`;
|
|
212
|
+
}
|
|
213
|
+
return `${tag} ${C.dim}rate_limit_event (no info payload)${C.reset}`;
|
|
214
|
+
}
|
|
206
215
|
case "result": {
|
|
207
216
|
const lines = [];
|
|
208
217
|
if (event.cost_usd !== void 0 || event.total_cost_usd !== void 0) {
|
|
@@ -267,22 +276,16 @@ function clampUserFacingDetail(text, maxChars) {
|
|
|
267
276
|
function stripAnsi(text) {
|
|
268
277
|
return text.replace(/\x1b\[[0-9;]*m/g, "");
|
|
269
278
|
}
|
|
270
|
-
function
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
}
|
|
277
|
-
return null;
|
|
279
|
+
function errMsg(err) {
|
|
280
|
+
return err instanceof Error ? err.message : String(err);
|
|
281
|
+
}
|
|
282
|
+
function normalizeEpochMs(raw) {
|
|
283
|
+
if (typeof raw !== "number" || !Number.isFinite(raw)) return null;
|
|
284
|
+
return raw > 1e10 ? raw : raw * 1e3;
|
|
278
285
|
}
|
|
279
286
|
function formatAnthropicRateLimitPayload(o) {
|
|
280
287
|
const rateLimitTypeRaw = typeof o.rateLimitType === "string" ? o.rateLimitType.replace(/_/g, " ") : "unknown";
|
|
281
|
-
const
|
|
282
|
-
let resetMs = null;
|
|
283
|
-
if (typeof rawReset === "number" && Number.isFinite(rawReset)) {
|
|
284
|
-
resetMs = rawReset > 1e10 ? rawReset : rawReset * 1e3;
|
|
285
|
-
}
|
|
288
|
+
const resetMs = normalizeEpochMs(o.resetsAt);
|
|
286
289
|
const status = typeof o.status === "string" ? o.status : null;
|
|
287
290
|
const overageReason = typeof o.overageDisabledReason === "string" ? o.overageDisabledReason : null;
|
|
288
291
|
const parts = [`Rate limit (${rateLimitTypeRaw})`];
|
|
@@ -303,9 +306,8 @@ function safeFormatRateLimitPayload(o) {
|
|
|
303
306
|
return formatAnthropicRateLimitPayload(o);
|
|
304
307
|
} catch {
|
|
305
308
|
const rt = typeof o.rateLimitType === "string" ? o.rateLimitType : "unknown";
|
|
306
|
-
const
|
|
307
|
-
if (
|
|
308
|
-
const ms = rawReset > 1e10 ? rawReset : rawReset * 1e3;
|
|
309
|
+
const ms = normalizeEpochMs(o.resetsAt);
|
|
310
|
+
if (ms !== null) {
|
|
309
311
|
return `Rate limit (${rt}) \u2014 resets at ${new Date(ms).toISOString()} UTC`;
|
|
310
312
|
}
|
|
311
313
|
return `Rate limit (${rt})`;
|
|
@@ -326,88 +328,40 @@ function parseClaudeNdjsonEvents(rawOutput) {
|
|
|
326
328
|
}
|
|
327
329
|
return events;
|
|
328
330
|
}
|
|
329
|
-
function
|
|
330
|
-
const
|
|
331
|
-
if (!
|
|
332
|
-
|
|
333
|
-
if (!Array.isArray(content)) return null;
|
|
334
|
-
const parts = [];
|
|
335
|
-
for (const block of content) {
|
|
336
|
-
if (!block || typeof block !== "object") continue;
|
|
337
|
-
const b = block;
|
|
338
|
-
if (b.type === "text" && typeof b.text === "string" && b.text.trim()) {
|
|
339
|
-
parts.push(b.text.trim());
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
if (parts.length === 0) return null;
|
|
343
|
-
return parts.join("\n");
|
|
331
|
+
function isRateLimitEventBlocking(ev) {
|
|
332
|
+
const info = ev.rate_limit_info;
|
|
333
|
+
if (!info || typeof info !== "object" || Array.isArray(info)) return true;
|
|
334
|
+
return info.status !== "allowed";
|
|
344
335
|
}
|
|
345
|
-
var USAGE_LIMIT_RE = /you['\u2019]?ve hit your limit/i;
|
|
346
336
|
function extractRateLimitRetryInfo(events) {
|
|
347
337
|
for (const ev of events) {
|
|
348
338
|
if (ev.type !== "rate_limit_event") continue;
|
|
339
|
+
if (!isRateLimitEventBlocking(ev)) continue;
|
|
349
340
|
const info = ev.rate_limit_info;
|
|
350
341
|
if (!info || typeof info !== "object" || Array.isArray(info)) continue;
|
|
351
|
-
const
|
|
352
|
-
if (
|
|
353
|
-
const ms = raw > 1e10 ? raw : raw * 1e3;
|
|
342
|
+
const ms = normalizeEpochMs(info.resetsAt);
|
|
343
|
+
if (ms === null) continue;
|
|
354
344
|
return { errorType: "rate_limit", retryAfterMs: ms };
|
|
355
345
|
}
|
|
356
346
|
return null;
|
|
357
347
|
}
|
|
358
|
-
function
|
|
359
|
-
return USAGE_LIMIT_RE.test(text);
|
|
360
|
-
}
|
|
361
|
-
function extractPlainTextCliErrorFromOutput(rawOutput) {
|
|
362
|
-
const limitMatch = rawOutput.match(new RegExp(USAGE_LIMIT_RE.source + "[^\\n\\r]*", "gi"));
|
|
363
|
-
if (limitMatch?.[0]) return limitMatch[0].trim();
|
|
364
|
-
for (const line of rawOutput.split("\n")) {
|
|
365
|
-
const t = line.trim();
|
|
366
|
-
if (!t || t.startsWith("{")) continue;
|
|
367
|
-
if (/^API error:/i.test(t) || /^Error:/i.test(t) || /^Anthropic/i.test(t)) {
|
|
368
|
-
return t.length <= 2e3 ? t : `${t.slice(0, 1999)}\u2026`;
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
return null;
|
|
372
|
-
}
|
|
373
|
-
function deriveClaudeFailureSummary(exitCode, rawOutput, stderr, events) {
|
|
374
|
-
const stderrText = stripAnsi(stderr).trim();
|
|
375
|
-
if (stderrText.length > 0) {
|
|
376
|
-
return clampUserFacingDetail(stderrText, 2e3);
|
|
377
|
-
}
|
|
348
|
+
function deriveClaudeFailureSummary(exitCode, stderr, events) {
|
|
378
349
|
for (let i = events.length - 1; i >= 0; i--) {
|
|
379
350
|
const ev = events[i];
|
|
380
351
|
if (ev.type !== "result") continue;
|
|
381
|
-
if (typeof ev.error === "string" && ev.error.trim()) {
|
|
382
|
-
return clampUserFacingDetail(ev.error.trim(), 2e3);
|
|
383
|
-
}
|
|
384
|
-
const nested = errorMessageFromUnknown(ev.error);
|
|
385
|
-
if (nested) return clampUserFacingDetail(nested, 2e3);
|
|
386
352
|
if (ev.is_error === true && typeof ev.result === "string" && ev.result.trim()) {
|
|
387
353
|
return clampUserFacingDetail(ev.result.trim(), 2e3);
|
|
388
354
|
}
|
|
389
|
-
}
|
|
390
|
-
for (let i = events.length - 1; i >= 0; i--) {
|
|
391
|
-
const ev = events[i];
|
|
392
|
-
if (ev.type !== "error") continue;
|
|
393
|
-
if (typeof ev.message === "string" && ev.message.trim()) {
|
|
394
|
-
return clampUserFacingDetail(ev.message.trim(), 2e3);
|
|
395
|
-
}
|
|
396
355
|
if (typeof ev.error === "string" && ev.error.trim()) {
|
|
397
356
|
return clampUserFacingDetail(ev.error.trim(), 2e3);
|
|
398
357
|
}
|
|
399
|
-
const nested = errorMessageFromUnknown(ev.error);
|
|
400
|
-
if (nested) return clampUserFacingDetail(nested, 2e3);
|
|
401
358
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
if (ev.error === void 0 || ev.error === null || ev.error === "") continue;
|
|
406
|
-
const assistantText = extractAssistantUserVisibleText(ev);
|
|
407
|
-
if (assistantText) return clampUserFacingDetail(assistantText, 2e3);
|
|
359
|
+
const stderrText = stripAnsi(stderr).trim();
|
|
360
|
+
if (stderrText.length > 0) {
|
|
361
|
+
return clampUserFacingDetail(stderrText, 2e3);
|
|
408
362
|
}
|
|
409
363
|
for (const ev of events) {
|
|
410
|
-
if (ev.type === "rate_limit_event") {
|
|
364
|
+
if (ev.type === "rate_limit_event" && isRateLimitEventBlocking(ev)) {
|
|
411
365
|
const info = ev.rate_limit_info;
|
|
412
366
|
if (info && typeof info === "object" && !Array.isArray(info)) {
|
|
413
367
|
return clampUserFacingDetail(
|
|
@@ -417,23 +371,13 @@ function deriveClaudeFailureSummary(exitCode, rawOutput, stderr, events) {
|
|
|
417
371
|
}
|
|
418
372
|
}
|
|
419
373
|
}
|
|
420
|
-
|
|
421
|
-
${rawOutput}`);
|
|
422
|
-
const plain = extractPlainTextCliErrorFromOutput(combined);
|
|
423
|
-
if (plain) return clampUserFacingDetail(plain, 2e3);
|
|
424
|
-
return `Claude Code ended with exit code ${exitCode}. No detailed error was returned; if this persists, run the runner with verbose logging or try again later.`;
|
|
374
|
+
return `Claude Code ended with exit code ${exitCode}`;
|
|
425
375
|
}
|
|
426
|
-
function claudeRunShouldReportAsError(
|
|
427
|
-
if (
|
|
428
|
-
for (
|
|
429
|
-
if (
|
|
430
|
-
if (ev.type === "assistant" && ev.error !== void 0 && ev.error !== null && ev.error !== "") {
|
|
431
|
-
return true;
|
|
432
|
-
}
|
|
433
|
-
if (ev.type === "result" && ev.is_error === true) return true;
|
|
376
|
+
function claudeRunShouldReportAsError(exitCode, events) {
|
|
377
|
+
if (exitCode !== 0) return true;
|
|
378
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
379
|
+
if (events[i].type === "result" && events[i].is_error === true) return true;
|
|
434
380
|
}
|
|
435
|
-
if (isClaudeUsageLimitText(options.resultSummaryText)) return true;
|
|
436
|
-
if (isClaudeUsageLimitText(options.combinedText)) return true;
|
|
437
381
|
return false;
|
|
438
382
|
}
|
|
439
383
|
function compactTaskErrorForApi(detail) {
|
|
@@ -490,31 +434,32 @@ function spawnClaudeCode(prompt, config, runOptions, resumeSessionId, cwdOverrid
|
|
|
490
434
|
const match = combined.match(GITHUB_PR_URL_RE);
|
|
491
435
|
if (match) capturedPrUrl = match[match.length - 1];
|
|
492
436
|
}
|
|
437
|
+
function processLine(trimmed) {
|
|
438
|
+
try {
|
|
439
|
+
const event = JSON.parse(trimmed);
|
|
440
|
+
if (event.type === "system" && event.subtype === "init" && event.session_id) {
|
|
441
|
+
capturedSessionId = event.session_id;
|
|
442
|
+
}
|
|
443
|
+
if (event.type === "result") lastResultJson = trimmed;
|
|
444
|
+
checkEventForPrUrl(event);
|
|
445
|
+
if (config.verbose) {
|
|
446
|
+
const formatted = formatStreamEvent(event, pid);
|
|
447
|
+
if (formatted) console.log(formatted);
|
|
448
|
+
}
|
|
449
|
+
} catch {
|
|
450
|
+
if (config.verbose) {
|
|
451
|
+
console.log(`${pidTag(pid)} ${C.dim}${trimmed}${C.reset}`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
493
455
|
child.stdout?.on("data", (data) => {
|
|
494
456
|
chunks.push(data);
|
|
495
|
-
|
|
496
|
-
lineBuf += text;
|
|
457
|
+
lineBuf += data.toString("utf-8");
|
|
497
458
|
const lines = lineBuf.split("\n");
|
|
498
459
|
lineBuf = lines.pop() ?? "";
|
|
499
460
|
for (const line of lines) {
|
|
500
461
|
const trimmed = line.trim();
|
|
501
|
-
if (
|
|
502
|
-
try {
|
|
503
|
-
const event = JSON.parse(trimmed);
|
|
504
|
-
if (event.type === "system" && event.subtype === "init" && event.session_id) {
|
|
505
|
-
capturedSessionId = event.session_id;
|
|
506
|
-
}
|
|
507
|
-
if (event.type === "result") lastResultJson = trimmed;
|
|
508
|
-
checkEventForPrUrl(event);
|
|
509
|
-
if (config.verbose) {
|
|
510
|
-
const formatted = formatStreamEvent(event, pid);
|
|
511
|
-
if (formatted) console.log(formatted);
|
|
512
|
-
}
|
|
513
|
-
} catch {
|
|
514
|
-
if (config.verbose) {
|
|
515
|
-
console.log(`${pidTag(pid)} ${C.dim}${trimmed}${C.reset}`);
|
|
516
|
-
}
|
|
517
|
-
}
|
|
462
|
+
if (trimmed) processLine(trimmed);
|
|
518
463
|
}
|
|
519
464
|
});
|
|
520
465
|
child.stderr?.on("data", (data) => {
|
|
@@ -526,24 +471,8 @@ function spawnClaudeCode(prompt, config, runOptions, resumeSessionId, cwdOverrid
|
|
|
526
471
|
});
|
|
527
472
|
child.on("error", (err) => reject(new Error(`Failed to spawn claude: ${err.message}`)));
|
|
528
473
|
child.on("close", (code) => {
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
const event = JSON.parse(lineBuf.trim());
|
|
532
|
-
if (event.type === "system" && event.subtype === "init" && event.session_id) {
|
|
533
|
-
capturedSessionId = event.session_id;
|
|
534
|
-
}
|
|
535
|
-
if (event.type === "result") lastResultJson = lineBuf.trim();
|
|
536
|
-
checkEventForPrUrl(event);
|
|
537
|
-
if (config.verbose) {
|
|
538
|
-
const formatted = formatStreamEvent(event, pid);
|
|
539
|
-
if (formatted) console.log(formatted);
|
|
540
|
-
}
|
|
541
|
-
} catch {
|
|
542
|
-
if (config.verbose) {
|
|
543
|
-
console.log(`${pidTag(pid)} ${C.dim}${lineBuf.trim()}${C.reset}`);
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
}
|
|
474
|
+
const remaining = lineBuf.trim();
|
|
475
|
+
if (remaining) processLine(remaining);
|
|
547
476
|
const rawOutput = Buffer.concat(chunks).toString("utf-8");
|
|
548
477
|
const stdout = config.verbose ? lastResultJson || rawOutput : rawOutput;
|
|
549
478
|
const stderr = Buffer.concat(errChunks).toString("utf-8");
|
|
@@ -613,10 +542,9 @@ function extractPullRequestUrlFromText(text) {
|
|
|
613
542
|
return void 0;
|
|
614
543
|
}
|
|
615
544
|
function extractPullRequestUrlFromOutput(stdout) {
|
|
616
|
-
const
|
|
617
|
-
if (
|
|
618
|
-
const
|
|
619
|
-
const found = extractPullRequestUrlFromText(text);
|
|
545
|
+
const resultText = extractResultText(stdout);
|
|
546
|
+
if (resultText !== stdout) {
|
|
547
|
+
const found = extractPullRequestUrlFromText(resultText);
|
|
620
548
|
if (found) return found;
|
|
621
549
|
}
|
|
622
550
|
return extractPullRequestUrlFromText(stdout);
|
|
@@ -808,7 +736,7 @@ async function removeWorktree(workDir, info) {
|
|
|
808
736
|
}
|
|
809
737
|
return true;
|
|
810
738
|
} catch (err) {
|
|
811
|
-
console.error(` Failed to remove ${info.name}: ${err
|
|
739
|
+
console.error(` Failed to remove ${info.name}: ${errMsg(err)}`);
|
|
812
740
|
return false;
|
|
813
741
|
}
|
|
814
742
|
}
|
|
@@ -892,23 +820,51 @@ Removing ${safe.length} safe worktree(s)\u2026`);
|
|
|
892
820
|
}
|
|
893
821
|
console.log();
|
|
894
822
|
}
|
|
895
|
-
function logClaudeRunFailureDiagnostics(log,
|
|
896
|
-
const se = stripAnsi(stderr).trimEnd();
|
|
897
|
-
const so = stripAnsi(rawOutput);
|
|
823
|
+
function logClaudeRunFailureDiagnostics(log, opts) {
|
|
824
|
+
const se = stripAnsi(opts.stderr).trimEnd();
|
|
825
|
+
const so = stripAnsi(opts.rawOutput);
|
|
898
826
|
log(`${C.dim}\u2500\u2500 Claude failure diagnostics (--verbose) \u2500\u2500${C.reset}`);
|
|
827
|
+
log(`${C.dim} exit code: ${opts.exitCode}${C.reset}`);
|
|
828
|
+
log(`${C.dim} session: ${opts.sessionId ?? "(none)"}${C.reset}`);
|
|
829
|
+
log(`${C.dim} derived error: ${opts.derivedError}${C.reset}`);
|
|
830
|
+
log(`${C.dim} api error: ${opts.apiError}${C.reset}`);
|
|
831
|
+
const rateLimitEvents = opts.streamEvents.filter((e) => e.type === "rate_limit_event");
|
|
832
|
+
if (rateLimitEvents.length > 0) {
|
|
833
|
+
log(`${C.dim} rate_limit_event(s): ${rateLimitEvents.length}${C.reset}`);
|
|
834
|
+
for (const rl of rateLimitEvents) {
|
|
835
|
+
const info = rl.rate_limit_info;
|
|
836
|
+
const blocking = isRateLimitEventBlocking(rl);
|
|
837
|
+
if (info && typeof info === "object" && !Array.isArray(info)) {
|
|
838
|
+
log(`${C.dim} blocking=${blocking} payload=${JSON.stringify(info)}${C.reset}`);
|
|
839
|
+
} else {
|
|
840
|
+
log(`${C.dim} blocking=${blocking} (no rate_limit_info)${C.reset}`);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
const typeCounts = /* @__PURE__ */ new Map();
|
|
845
|
+
for (const ev of opts.streamEvents) {
|
|
846
|
+
const t = typeof ev.type === "string" ? ev.type : "?";
|
|
847
|
+
typeCounts.set(t, (typeCounts.get(t) ?? 0) + 1);
|
|
848
|
+
}
|
|
849
|
+
if (typeCounts.size > 0) {
|
|
850
|
+
const timeline = Array.from(typeCounts.entries()).map(([t, c]) => c > 1 ? `${t}\xD7${c}` : t).join(", ");
|
|
851
|
+
log(`${C.dim} events: ${opts.streamEvents.length} total (${timeline})${C.reset}`);
|
|
852
|
+
} else {
|
|
853
|
+
log(`${C.dim} events: 0 parsed${C.reset}`);
|
|
854
|
+
}
|
|
899
855
|
if (se.length > 0) {
|
|
900
|
-
log(`${C.dim}stderr:${C.reset}`);
|
|
856
|
+
log(`${C.dim} stderr:${C.reset}`);
|
|
901
857
|
for (const ln of se.split("\n")) {
|
|
902
|
-
log(`${C.dim}
|
|
858
|
+
log(`${C.dim} ${ln}${C.reset}`);
|
|
903
859
|
}
|
|
904
860
|
} else {
|
|
905
|
-
log(`${C.dim}(
|
|
861
|
+
log(`${C.dim} stderr: (empty)${C.reset}`);
|
|
906
862
|
}
|
|
907
863
|
const lines = so.length > 0 ? so.split("\n") : [];
|
|
908
864
|
const tailLines = lines.length > 200 ? lines.slice(-200) : lines;
|
|
909
|
-
log(`${C.dim}stdout: ${lines.length} line(s), showing last ${tailLines.length}${C.reset}`);
|
|
865
|
+
log(`${C.dim} stdout: ${lines.length} line(s), showing last ${tailLines.length}${C.reset}`);
|
|
910
866
|
for (const ln of tailLines) {
|
|
911
|
-
log(`${C.dim}
|
|
867
|
+
log(`${C.dim} ${ln}${C.reset}`);
|
|
912
868
|
}
|
|
913
869
|
log(`${C.dim}\u2500\u2500 end diagnostics \u2500\u2500${C.reset}`);
|
|
914
870
|
}
|
|
@@ -958,7 +914,7 @@ async function processRun(client, run, config, worktreeDir, detail) {
|
|
|
958
914
|
try {
|
|
959
915
|
await client.startRun(run.id, startMsg);
|
|
960
916
|
} catch (err) {
|
|
961
|
-
const msg =
|
|
917
|
+
const msg = errMsg(err);
|
|
962
918
|
if (msg.includes("not pending") || msg.includes("not PENDING") || msg.includes("400")) {
|
|
963
919
|
log(`\u23ED Run was already claimed, skipping.`);
|
|
964
920
|
return;
|
|
@@ -967,11 +923,13 @@ async function processRun(client, run, config, worktreeDir, detail) {
|
|
|
967
923
|
}
|
|
968
924
|
log("\u25B6 Run started");
|
|
969
925
|
const effectiveCwd = worktreeDir ?? config.workDir;
|
|
970
|
-
let
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
926
|
+
let lastClaude = {
|
|
927
|
+
stderr: "",
|
|
928
|
+
stdout: "",
|
|
929
|
+
rawOutput: "",
|
|
930
|
+
exitCode: null,
|
|
931
|
+
sessionId: void 0
|
|
932
|
+
};
|
|
975
933
|
try {
|
|
976
934
|
if (!worktreeDir && runOptions.branch) {
|
|
977
935
|
log(`\u{1F33F} Checking out branch: ${runOptions.branch}`);
|
|
@@ -1001,45 +959,23 @@ async function processRun(client, run, config, worktreeDir, detail) {
|
|
|
1001
959
|
);
|
|
1002
960
|
log(`\u{1F916} Claude Code spawned (pid ${child.pid})${isFollowUp ? " (follow-up)" : ""}`);
|
|
1003
961
|
let { stdout, rawOutput, stderr, exitCode, sessionId, streamPrUrl } = await output;
|
|
1004
|
-
|
|
1005
|
-
lastClaudeStdout = stdout;
|
|
1006
|
-
lastClaudeRawOutput = rawOutput;
|
|
1007
|
-
lastClaudeExitCode = exitCode;
|
|
1008
|
-
lastClaudeSessionId = sessionId;
|
|
962
|
+
lastClaude = { stderr, stdout, rawOutput, exitCode, sessionId };
|
|
1009
963
|
if (exitCode !== 0 && isFollowUp) {
|
|
1010
964
|
log(`\u26A0 Session resume failed, retrying with fresh session\u2026`);
|
|
1011
965
|
sessionCache.delete(run.taskId);
|
|
1012
966
|
const retry = spawnClaudeCode(prompt, config, runOptions, void 0, effectiveCwd);
|
|
1013
967
|
log(`\u{1F916} Claude Code spawned (pid ${retry.process.pid}) (fresh)`);
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
rawOutput = retryResult.rawOutput;
|
|
1017
|
-
stderr = retryResult.stderr;
|
|
1018
|
-
exitCode = retryResult.exitCode;
|
|
1019
|
-
sessionId = retryResult.sessionId;
|
|
1020
|
-
streamPrUrl = retryResult.streamPrUrl;
|
|
1021
|
-
lastClaudeStderr = stderr;
|
|
1022
|
-
lastClaudeStdout = stdout;
|
|
1023
|
-
lastClaudeRawOutput = rawOutput;
|
|
1024
|
-
lastClaudeExitCode = exitCode;
|
|
1025
|
-
lastClaudeSessionId = sessionId;
|
|
968
|
+
({ stdout, rawOutput, stderr, exitCode, sessionId, streamPrUrl } = await retry.output);
|
|
969
|
+
lastClaude = { stderr, stdout, rawOutput, exitCode, sessionId };
|
|
1026
970
|
}
|
|
1027
971
|
if (sessionId) {
|
|
1028
972
|
sessionCache.set(run.taskId, sessionId);
|
|
1029
973
|
}
|
|
1030
974
|
const pullRequestUrl = streamPrUrl ?? await detectPullRequestUrl(effectiveCwd) ?? extractPullRequestUrlFromOutput(stdout) ?? extractPullRequestUrlFromOutput(rawOutput);
|
|
1031
975
|
const streamEvents = parseClaudeNdjsonEvents(rawOutput);
|
|
1032
|
-
const
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
const usageCap = claudeRunShouldReportAsError({
|
|
1036
|
-
exitCode,
|
|
1037
|
-
events: streamEvents,
|
|
1038
|
-
combinedText: combinedForLimit,
|
|
1039
|
-
resultSummaryText
|
|
1040
|
-
});
|
|
1041
|
-
if (exitCode !== 0 || usageCap) {
|
|
1042
|
-
const detail2 = deriveClaudeFailureSummary(exitCode, rawOutput, stderr, streamEvents);
|
|
976
|
+
const isError = claudeRunShouldReportAsError(exitCode, streamEvents);
|
|
977
|
+
if (isError) {
|
|
978
|
+
const detail2 = deriveClaudeFailureSummary(exitCode, stderr, streamEvents);
|
|
1043
979
|
const errorMsg = compactTaskErrorForApi(detail2);
|
|
1044
980
|
if (process.env.GCA_DEBUG_CLAUDE_RAW === "1") {
|
|
1045
981
|
const stderrDbg = truncateClaudeDebugString(stripAnsi(stderr));
|
|
@@ -1050,7 +986,6 @@ ${rawOutput}`);
|
|
|
1050
986
|
taskId: run.taskId,
|
|
1051
987
|
taskTitle: run.taskTitle,
|
|
1052
988
|
exitCode,
|
|
1053
|
-
usageCap,
|
|
1054
989
|
sessionId: sessionId ?? null,
|
|
1055
990
|
streamPrUrl: streamPrUrl ?? null,
|
|
1056
991
|
pullRequestUrl: pullRequestUrl ?? null,
|
|
@@ -1062,8 +997,7 @@ ${rawOutput}`);
|
|
|
1062
997
|
stdout: stdoutDbg.text,
|
|
1063
998
|
stdoutMeta: { fullLength: stdoutDbg.fullLength, truncated: stdoutDbg.truncated },
|
|
1064
999
|
rawOutput: rawDbg.text,
|
|
1065
|
-
rawOutputMeta: { fullLength: rawDbg.fullLength, truncated: rawDbg.truncated }
|
|
1066
|
-
resultTextFromExtract: resultSummaryText || null
|
|
1000
|
+
rawOutputMeta: { fullLength: rawDbg.fullLength, truncated: rawDbg.truncated }
|
|
1067
1001
|
});
|
|
1068
1002
|
}
|
|
1069
1003
|
const retryInfo = extractRateLimitRetryInfo(streamEvents);
|
|
@@ -1087,7 +1021,15 @@ ${rawOutput}`);
|
|
|
1087
1021
|
log(`${C.red} ${line}${C.reset}`);
|
|
1088
1022
|
}
|
|
1089
1023
|
if (config.verbose) {
|
|
1090
|
-
logClaudeRunFailureDiagnostics(log,
|
|
1024
|
+
logClaudeRunFailureDiagnostics(log, {
|
|
1025
|
+
exitCode,
|
|
1026
|
+
sessionId,
|
|
1027
|
+
stderr,
|
|
1028
|
+
rawOutput,
|
|
1029
|
+
streamEvents,
|
|
1030
|
+
derivedError: detail2,
|
|
1031
|
+
apiError: errorMsg
|
|
1032
|
+
});
|
|
1091
1033
|
}
|
|
1092
1034
|
return;
|
|
1093
1035
|
}
|
|
@@ -1097,11 +1039,11 @@ ${rawOutput}`);
|
|
|
1097
1039
|
if (pullRequestUrl) logGreen(`\u{1F517} PR: ${pullRequestUrl}`);
|
|
1098
1040
|
logGreen(`\u2705 Run completed`);
|
|
1099
1041
|
} catch (err) {
|
|
1100
|
-
const message =
|
|
1042
|
+
const message = errMsg(err);
|
|
1101
1043
|
const stack = err instanceof Error ? err.stack : void 0;
|
|
1102
|
-
const stderrDbg = truncateClaudeDebugString(stripAnsi(
|
|
1103
|
-
const stdoutDbg = truncateClaudeDebugString(stripAnsi(
|
|
1104
|
-
const rawDbg = truncateClaudeDebugString(stripAnsi(
|
|
1044
|
+
const stderrDbg = truncateClaudeDebugString(stripAnsi(lastClaude.stderr));
|
|
1045
|
+
const stdoutDbg = truncateClaudeDebugString(stripAnsi(lastClaude.stdout));
|
|
1046
|
+
const rawDbg = truncateClaudeDebugString(stripAnsi(lastClaude.rawOutput));
|
|
1105
1047
|
if (process.env.GCA_DEBUG_CLAUDE_RAW === "1") {
|
|
1106
1048
|
logClaudeRawFailureJson({
|
|
1107
1049
|
runId: run.id,
|
|
@@ -1109,8 +1051,8 @@ ${rawOutput}`);
|
|
|
1109
1051
|
taskTitle: run.taskTitle,
|
|
1110
1052
|
thrownMessage: message,
|
|
1111
1053
|
thrownStack: stack ?? null,
|
|
1112
|
-
exitCode:
|
|
1113
|
-
sessionId:
|
|
1054
|
+
exitCode: lastClaude.exitCode,
|
|
1055
|
+
sessionId: lastClaude.sessionId ?? null,
|
|
1114
1056
|
note: "Thrown before or after Claude finished; fields may be empty.",
|
|
1115
1057
|
stderr: stderrDbg.text,
|
|
1116
1058
|
stderrMeta: { fullLength: stderrDbg.fullLength, truncated: stderrDbg.truncated },
|
|
@@ -1214,6 +1156,7 @@ function parseArgs() {
|
|
|
1214
1156
|
workDir: process.cwd(),
|
|
1215
1157
|
pollInterval: 3e4,
|
|
1216
1158
|
maxConcurrent: 5,
|
|
1159
|
+
command: "run",
|
|
1217
1160
|
poll: false,
|
|
1218
1161
|
dryRun: false,
|
|
1219
1162
|
once: false,
|
|
@@ -1263,20 +1206,23 @@ function parseArgs() {
|
|
|
1263
1206
|
config.useWorktrees = false;
|
|
1264
1207
|
break;
|
|
1265
1208
|
case "cleanup":
|
|
1209
|
+
config.command = "cleanup";
|
|
1266
1210
|
break;
|
|
1267
1211
|
default:
|
|
1268
1212
|
console.error(`Unknown argument: ${arg}`);
|
|
1269
1213
|
process.exit(1);
|
|
1270
1214
|
}
|
|
1271
1215
|
}
|
|
1272
|
-
if (
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1216
|
+
if (config.command !== "cleanup") {
|
|
1217
|
+
if (!config.token) {
|
|
1218
|
+
console.error("Error: No agent token found.");
|
|
1219
|
+
console.error(" Add GCA_TOKEN=gca_... to .env.local, or pass --token gca_...");
|
|
1220
|
+
process.exit(1);
|
|
1221
|
+
}
|
|
1222
|
+
if (!config.token.startsWith("gca_")) {
|
|
1223
|
+
console.error(`Error: Token must start with gca_ (got "${config.token.slice(0, 8)}\u2026")`);
|
|
1224
|
+
process.exit(1);
|
|
1225
|
+
}
|
|
1280
1226
|
}
|
|
1281
1227
|
return config;
|
|
1282
1228
|
}
|
|
@@ -1477,7 +1423,7 @@ async function runWithPolling(client, config, scheduler) {
|
|
|
1477
1423
|
const pending = await client.listPendingRuns();
|
|
1478
1424
|
handlePendingRuns(pending, scheduler, client, config);
|
|
1479
1425
|
} catch (err) {
|
|
1480
|
-
console.error(`Poll error: ${err
|
|
1426
|
+
console.error(`Poll error: ${errMsg(err)}`);
|
|
1481
1427
|
}
|
|
1482
1428
|
while (!scheduler.isEmpty()) await sleep(1e3);
|
|
1483
1429
|
return;
|
|
@@ -1487,7 +1433,7 @@ async function runWithPolling(client, config, scheduler) {
|
|
|
1487
1433
|
const pending = await client.listPendingRuns();
|
|
1488
1434
|
handlePendingRuns(pending, scheduler, client, config);
|
|
1489
1435
|
} catch (err) {
|
|
1490
|
-
const msg =
|
|
1436
|
+
const msg = errMsg(err);
|
|
1491
1437
|
if (config.verbose || !msg.includes("fetch")) {
|
|
1492
1438
|
console.error(`Poll error: ${msg}`);
|
|
1493
1439
|
}
|
|
@@ -1495,29 +1441,18 @@ async function runWithPolling(client, config, scheduler) {
|
|
|
1495
1441
|
await sleep(config.pollInterval);
|
|
1496
1442
|
}
|
|
1497
1443
|
}
|
|
1498
|
-
function wantsVersionOnly(argv) {
|
|
1499
|
-
return argv.some((a) => a === "--version" || a === "-v" || a === "-version");
|
|
1500
|
-
}
|
|
1501
1444
|
async function main() {
|
|
1502
|
-
const
|
|
1503
|
-
if (
|
|
1504
|
-
|
|
1505
|
-
}
|
|
1506
|
-
if (process.argv.includes("cleanup")) {
|
|
1507
|
-
loadEnvFile();
|
|
1508
|
-
const workDir = process.cwd();
|
|
1509
|
-
const workDirIdx = process.argv.indexOf("--work-dir");
|
|
1510
|
-
const resolvedDir = workDirIdx >= 0 ? path.resolve(process.argv[workDirIdx + 1] ?? ".") : workDir;
|
|
1511
|
-
await interactiveCleanup(resolvedDir);
|
|
1445
|
+
const config = parseArgs();
|
|
1446
|
+
if (config.command === "cleanup") {
|
|
1447
|
+
await interactiveCleanup(config.workDir);
|
|
1512
1448
|
return;
|
|
1513
1449
|
}
|
|
1514
|
-
const config = parseArgs();
|
|
1515
1450
|
const client = new GroupChatAgentClient(config.apiUrl, config.token);
|
|
1516
1451
|
let me;
|
|
1517
1452
|
try {
|
|
1518
1453
|
me = await client.getMe();
|
|
1519
1454
|
} catch (err) {
|
|
1520
|
-
console.error("Failed to authenticate:", err
|
|
1455
|
+
console.error("Failed to authenticate:", errMsg(err));
|
|
1521
1456
|
process.exit(1);
|
|
1522
1457
|
}
|
|
1523
1458
|
console.log(`
|
|
@@ -1540,10 +1475,20 @@ async function main() {
|
|
|
1540
1475
|
await runWithWebSocket(client, config, scheduler);
|
|
1541
1476
|
}
|
|
1542
1477
|
if (!scheduler.isEmpty()) {
|
|
1543
|
-
console.log(
|
|
1544
|
-
|
|
1478
|
+
console.log(
|
|
1479
|
+
`\u23F3 Waiting for ${scheduler.activeTaskCount} in-flight task(s)\u2026 (Ctrl+C to force quit)`
|
|
1480
|
+
);
|
|
1481
|
+
let forceQuit = false;
|
|
1482
|
+
const forceHandler = () => {
|
|
1483
|
+
forceQuit = true;
|
|
1484
|
+
};
|
|
1485
|
+
process.once("SIGINT", forceHandler);
|
|
1486
|
+
process.once("SIGTERM", forceHandler);
|
|
1487
|
+
while (!scheduler.isEmpty() && !forceQuit) {
|
|
1545
1488
|
await sleep(1e3);
|
|
1546
1489
|
}
|
|
1490
|
+
process.removeListener("SIGINT", forceHandler);
|
|
1491
|
+
process.removeListener("SIGTERM", forceHandler);
|
|
1547
1492
|
}
|
|
1548
1493
|
console.log("\u{1F44B} Goodbye.");
|
|
1549
1494
|
}
|