@groupchatai/claude-runner 0.4.2 → 0.4.5
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 +484 -30
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4,8 +4,18 @@
|
|
|
4
4
|
import { spawn, execFileSync } from "child_process";
|
|
5
5
|
import { readFileSync, readdirSync, statSync, writeFileSync, existsSync, rmSync } from "fs";
|
|
6
6
|
import path from "path";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
7
8
|
var API_URL = "https://groupchat.ai";
|
|
8
9
|
var CONVEX_URL = "https://fantastic-jay-464.convex.cloud";
|
|
10
|
+
function sanitizeAgentJsonPayload(payload) {
|
|
11
|
+
const out = {};
|
|
12
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
13
|
+
if (value === void 0 || value === null) continue;
|
|
14
|
+
if (typeof value === "number" && !Number.isFinite(value)) continue;
|
|
15
|
+
out[key] = value;
|
|
16
|
+
}
|
|
17
|
+
return out;
|
|
18
|
+
}
|
|
9
19
|
var GroupChatAgentClient = class {
|
|
10
20
|
baseUrl;
|
|
11
21
|
token;
|
|
@@ -15,13 +25,14 @@ var GroupChatAgentClient = class {
|
|
|
15
25
|
}
|
|
16
26
|
async request(method, endpoint, body) {
|
|
17
27
|
const url = `${this.baseUrl}${endpoint}`;
|
|
28
|
+
const jsonBody = body ? JSON.stringify(sanitizeAgentJsonPayload(body)) : void 0;
|
|
18
29
|
const res = await fetch(url, {
|
|
19
30
|
method,
|
|
20
31
|
headers: {
|
|
21
32
|
Authorization: `Bearer ${this.token}`,
|
|
22
33
|
"Content-Type": "application/json"
|
|
23
34
|
},
|
|
24
|
-
body:
|
|
35
|
+
body: jsonBody,
|
|
25
36
|
redirect: "follow"
|
|
26
37
|
});
|
|
27
38
|
if (res.status === 401 && res.redirected) {
|
|
@@ -31,7 +42,7 @@ var GroupChatAgentClient = class {
|
|
|
31
42
|
Authorization: `Bearer ${this.token}`,
|
|
32
43
|
"Content-Type": "application/json"
|
|
33
44
|
},
|
|
34
|
-
body:
|
|
45
|
+
body: jsonBody
|
|
35
46
|
});
|
|
36
47
|
if (!retry.ok) {
|
|
37
48
|
const text = await retry.text().catch(() => "");
|
|
@@ -104,8 +115,16 @@ ${comment.body}`
|
|
|
104
115
|
}
|
|
105
116
|
}
|
|
106
117
|
if (detail.task.dueDate) {
|
|
118
|
+
let dueStr = new Date(detail.task.dueDate).toLocaleDateString();
|
|
119
|
+
if (detail.task.dueTime != null) {
|
|
120
|
+
const h = Math.floor(detail.task.dueTime / 60);
|
|
121
|
+
const m = detail.task.dueTime % 60;
|
|
122
|
+
const period = h >= 12 ? "PM" : "AM";
|
|
123
|
+
const h12 = h === 0 ? 12 : h > 12 ? h - 12 : h;
|
|
124
|
+
dueStr += ` at ${h12}:${String(m).padStart(2, "0")} ${period}`;
|
|
125
|
+
}
|
|
107
126
|
parts.push(`
|
|
108
|
-
Due: ${
|
|
127
|
+
Due: ${dueStr}`);
|
|
109
128
|
}
|
|
110
129
|
parts.push(
|
|
111
130
|
[
|
|
@@ -240,6 +259,190 @@ var ALLOWED_TOOLS = [
|
|
|
240
259
|
"TodoWrite",
|
|
241
260
|
"NotebookEdit"
|
|
242
261
|
];
|
|
262
|
+
function clampUserFacingDetail(text, maxChars) {
|
|
263
|
+
const t = text.trim();
|
|
264
|
+
if (t.length <= maxChars) return t;
|
|
265
|
+
return `${t.slice(0, maxChars - 1)}\u2026`;
|
|
266
|
+
}
|
|
267
|
+
function stripAnsi(text) {
|
|
268
|
+
return text.replace(/\x1b\[[0-9;]*m/g, "");
|
|
269
|
+
}
|
|
270
|
+
function errorMessageFromUnknown(err) {
|
|
271
|
+
if (typeof err === "string" && err.trim()) return err.trim();
|
|
272
|
+
if (err && typeof err === "object") {
|
|
273
|
+
const o = err;
|
|
274
|
+
if (typeof o.message === "string" && o.message.trim()) return o.message.trim();
|
|
275
|
+
if (typeof o.error === "string" && o.error.trim()) return o.error.trim();
|
|
276
|
+
}
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
function formatAnthropicRateLimitPayload(o) {
|
|
280
|
+
const rateLimitTypeRaw = typeof o.rateLimitType === "string" ? o.rateLimitType.replace(/_/g, " ") : "unknown";
|
|
281
|
+
const rawReset = o.resetsAt;
|
|
282
|
+
let resetMs = null;
|
|
283
|
+
if (typeof rawReset === "number" && Number.isFinite(rawReset)) {
|
|
284
|
+
resetMs = rawReset > 1e10 ? rawReset : rawReset * 1e3;
|
|
285
|
+
}
|
|
286
|
+
const status = typeof o.status === "string" ? o.status : null;
|
|
287
|
+
const overageReason = typeof o.overageDisabledReason === "string" ? o.overageDisabledReason : null;
|
|
288
|
+
const parts = [`Rate limit (${rateLimitTypeRaw})`];
|
|
289
|
+
if (status) parts.push(`status ${status}`);
|
|
290
|
+
if (resetMs !== null) {
|
|
291
|
+
const when = new Date(resetMs);
|
|
292
|
+
const local = when.toLocaleString(void 0, {
|
|
293
|
+
dateStyle: "medium",
|
|
294
|
+
timeStyle: "short"
|
|
295
|
+
});
|
|
296
|
+
parts.push(`resets at ${local} (${when.toISOString()} UTC)`);
|
|
297
|
+
}
|
|
298
|
+
if (overageReason) parts.push(`overage: ${overageReason}`);
|
|
299
|
+
return parts.join(" \u2014 ");
|
|
300
|
+
}
|
|
301
|
+
function safeFormatRateLimitPayload(o) {
|
|
302
|
+
try {
|
|
303
|
+
return formatAnthropicRateLimitPayload(o);
|
|
304
|
+
} catch {
|
|
305
|
+
const rt = typeof o.rateLimitType === "string" ? o.rateLimitType : "unknown";
|
|
306
|
+
const rawReset = o.resetsAt;
|
|
307
|
+
if (typeof rawReset === "number" && Number.isFinite(rawReset)) {
|
|
308
|
+
const ms = rawReset > 1e10 ? rawReset : rawReset * 1e3;
|
|
309
|
+
return `Rate limit (${rt}) \u2014 resets at ${new Date(ms).toISOString()} UTC`;
|
|
310
|
+
}
|
|
311
|
+
return `Rate limit (${rt})`;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
function parseClaudeNdjsonEvents(rawOutput) {
|
|
315
|
+
const events = [];
|
|
316
|
+
for (const line of stripAnsi(rawOutput).split("\n")) {
|
|
317
|
+
const t = line.trim();
|
|
318
|
+
if (!t.startsWith("{")) continue;
|
|
319
|
+
try {
|
|
320
|
+
const ev = JSON.parse(t);
|
|
321
|
+
if (ev && typeof ev === "object" && typeof ev.type === "string") {
|
|
322
|
+
events.push(ev);
|
|
323
|
+
}
|
|
324
|
+
} catch {
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return events;
|
|
328
|
+
}
|
|
329
|
+
function extractAssistantUserVisibleText(event) {
|
|
330
|
+
const msg = event.message;
|
|
331
|
+
if (!msg || typeof msg !== "object" || Array.isArray(msg)) return null;
|
|
332
|
+
const content = msg.content;
|
|
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");
|
|
344
|
+
}
|
|
345
|
+
var USAGE_LIMIT_RE = /you['\u2019]?ve hit your limit/i;
|
|
346
|
+
function extractRateLimitRetryInfo(events) {
|
|
347
|
+
for (const ev of events) {
|
|
348
|
+
if (ev.type !== "rate_limit_event") continue;
|
|
349
|
+
const info = ev.rate_limit_info;
|
|
350
|
+
if (!info || typeof info !== "object" || Array.isArray(info)) continue;
|
|
351
|
+
const raw = info.resetsAt;
|
|
352
|
+
if (typeof raw !== "number" || !Number.isFinite(raw)) continue;
|
|
353
|
+
const ms = raw > 1e10 ? raw : raw * 1e3;
|
|
354
|
+
return { errorType: "rate_limit", retryAfterMs: ms };
|
|
355
|
+
}
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
function isClaudeUsageLimitText(text) {
|
|
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
|
+
}
|
|
378
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
379
|
+
const ev = events[i];
|
|
380
|
+
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
|
+
if (ev.is_error === true && typeof ev.result === "string" && ev.result.trim()) {
|
|
387
|
+
return clampUserFacingDetail(ev.result.trim(), 2e3);
|
|
388
|
+
}
|
|
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
|
+
if (typeof ev.error === "string" && ev.error.trim()) {
|
|
397
|
+
return clampUserFacingDetail(ev.error.trim(), 2e3);
|
|
398
|
+
}
|
|
399
|
+
const nested = errorMessageFromUnknown(ev.error);
|
|
400
|
+
if (nested) return clampUserFacingDetail(nested, 2e3);
|
|
401
|
+
}
|
|
402
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
403
|
+
const ev = events[i];
|
|
404
|
+
if (ev.type !== "assistant") continue;
|
|
405
|
+
if (ev.error === void 0 || ev.error === null || ev.error === "") continue;
|
|
406
|
+
const assistantText = extractAssistantUserVisibleText(ev);
|
|
407
|
+
if (assistantText) return clampUserFacingDetail(assistantText, 2e3);
|
|
408
|
+
}
|
|
409
|
+
for (const ev of events) {
|
|
410
|
+
if (ev.type === "rate_limit_event") {
|
|
411
|
+
const info = ev.rate_limit_info;
|
|
412
|
+
if (info && typeof info === "object" && !Array.isArray(info)) {
|
|
413
|
+
return clampUserFacingDetail(
|
|
414
|
+
safeFormatRateLimitPayload(info),
|
|
415
|
+
2e3
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
const combined = stripAnsi(`${stderr}
|
|
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.`;
|
|
425
|
+
}
|
|
426
|
+
function claudeRunShouldReportAsError(options) {
|
|
427
|
+
if (options.exitCode !== 0) return true;
|
|
428
|
+
for (const ev of options.events) {
|
|
429
|
+
if (ev.type === "rate_limit_event") return true;
|
|
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;
|
|
434
|
+
}
|
|
435
|
+
if (isClaudeUsageLimitText(options.resultSummaryText)) return true;
|
|
436
|
+
if (isClaudeUsageLimitText(options.combinedText)) return true;
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
function compactTaskErrorForApi(detail) {
|
|
440
|
+
const s = detail.trim();
|
|
441
|
+
if (/^rate limit \(/i.test(s)) {
|
|
442
|
+
return s.split(/\s*—\s*/)[0]?.trim() ?? s;
|
|
443
|
+
}
|
|
444
|
+
return s;
|
|
445
|
+
}
|
|
243
446
|
function spawnClaudeCode(prompt, config, runOptions, resumeSessionId, cwdOverride) {
|
|
244
447
|
const format = config.verbose ? "stream-json" : "json";
|
|
245
448
|
const args = [];
|
|
@@ -350,6 +553,7 @@ function spawnClaudeCode(prompt, config, runOptions, resumeSessionId, cwdOverrid
|
|
|
350
553
|
resolve({
|
|
351
554
|
stdout,
|
|
352
555
|
rawOutput,
|
|
556
|
+
stderr,
|
|
353
557
|
exitCode: code ?? 0,
|
|
354
558
|
sessionId: capturedSessionId,
|
|
355
559
|
streamPrUrl: capturedPrUrl
|
|
@@ -445,6 +649,12 @@ function runShellCommand(cmd, args, cwd) {
|
|
|
445
649
|
}
|
|
446
650
|
var sessionCache = /* @__PURE__ */ new Map();
|
|
447
651
|
var runCounter = 0;
|
|
652
|
+
var claudeRunnerSignalTeardownDone = false;
|
|
653
|
+
function tryBeginClaudeRunnerSignalTeardown() {
|
|
654
|
+
if (claudeRunnerSignalTeardownDone) return false;
|
|
655
|
+
claudeRunnerSignalTeardownDone = true;
|
|
656
|
+
return true;
|
|
657
|
+
}
|
|
448
658
|
var WORKTREE_DIR = ".agent-worktrees";
|
|
449
659
|
var WORKTREE_PREFIX = "task-";
|
|
450
660
|
function worktreeNameForTask(taskId) {
|
|
@@ -461,15 +671,24 @@ function getDefaultBranch(repoDir) {
|
|
|
461
671
|
return "main";
|
|
462
672
|
}
|
|
463
673
|
}
|
|
464
|
-
function createWorktree(repoDir, taskId) {
|
|
674
|
+
function createWorktree(repoDir, taskId, existingBranch) {
|
|
465
675
|
const name = worktreeNameForTask(taskId);
|
|
466
|
-
const branchName = `agent/${name}-${Date.now()}`;
|
|
467
676
|
const worktreeBase = path.join(repoDir, WORKTREE_DIR);
|
|
468
677
|
const worktreePath = path.join(worktreeBase, name);
|
|
469
678
|
if (existsSync(worktreePath)) {
|
|
470
679
|
return worktreePath;
|
|
471
680
|
}
|
|
681
|
+
if (existingBranch) {
|
|
682
|
+
try {
|
|
683
|
+
execGit(["fetch", "origin", existingBranch, "--quiet"], repoDir);
|
|
684
|
+
} catch {
|
|
685
|
+
}
|
|
686
|
+
execGit(["worktree", "add", worktreePath, `origin/${existingBranch}`], repoDir);
|
|
687
|
+
writeFileSync(path.join(worktreePath, ".agent-branch"), existingBranch, "utf-8");
|
|
688
|
+
return worktreePath;
|
|
689
|
+
}
|
|
472
690
|
const baseBranch = getDefaultBranch(repoDir);
|
|
691
|
+
const branchName = `agent/${name}-${Date.now()}`;
|
|
473
692
|
try {
|
|
474
693
|
execGit(["fetch", "origin", baseBranch, "--quiet"], repoDir);
|
|
475
694
|
} catch {
|
|
@@ -478,6 +697,39 @@ function createWorktree(repoDir, taskId) {
|
|
|
478
697
|
writeFileSync(path.join(worktreePath, ".agent-branch"), branchName, "utf-8");
|
|
479
698
|
return worktreePath;
|
|
480
699
|
}
|
|
700
|
+
async function resolveExistingPrBranch(detail, workDir) {
|
|
701
|
+
try {
|
|
702
|
+
if (detail.branchName) return detail.branchName;
|
|
703
|
+
if (detail.pullRequestUrl) {
|
|
704
|
+
try {
|
|
705
|
+
const branch2 = await runShellCommand(
|
|
706
|
+
"gh",
|
|
707
|
+
["pr", "view", detail.pullRequestUrl, "--json", "headRefName", "--jq", ".headRefName"],
|
|
708
|
+
workDir
|
|
709
|
+
);
|
|
710
|
+
if (branch2?.trim()) return branch2.trim();
|
|
711
|
+
} catch {
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
const prUrls = [];
|
|
715
|
+
for (const item of detail.activity) {
|
|
716
|
+
if (item.body) {
|
|
717
|
+
const matches = item.body.match(GITHUB_PR_URL_RE);
|
|
718
|
+
if (matches) prUrls.push(...matches);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
const prUrl = prUrls[prUrls.length - 1];
|
|
722
|
+
if (!prUrl) return void 0;
|
|
723
|
+
const branch = await runShellCommand(
|
|
724
|
+
"gh",
|
|
725
|
+
["pr", "view", prUrl, "--json", "headRefName", "--jq", ".headRefName"],
|
|
726
|
+
workDir
|
|
727
|
+
);
|
|
728
|
+
if (branch?.trim()) return branch.trim();
|
|
729
|
+
} catch {
|
|
730
|
+
}
|
|
731
|
+
return void 0;
|
|
732
|
+
}
|
|
481
733
|
async function listOurWorktrees(workDir) {
|
|
482
734
|
const worktreeDir = path.join(workDir, WORKTREE_DIR);
|
|
483
735
|
let entries;
|
|
@@ -640,12 +892,52 @@ Removing ${safe.length} safe worktree(s)\u2026`);
|
|
|
640
892
|
}
|
|
641
893
|
console.log();
|
|
642
894
|
}
|
|
643
|
-
|
|
895
|
+
function logClaudeRunFailureDiagnostics(log, stderr, rawOutput) {
|
|
896
|
+
const se = stripAnsi(stderr).trimEnd();
|
|
897
|
+
const so = stripAnsi(rawOutput);
|
|
898
|
+
log(`${C.dim}\u2500\u2500 Claude failure diagnostics (--verbose) \u2500\u2500${C.reset}`);
|
|
899
|
+
if (se.length > 0) {
|
|
900
|
+
log(`${C.dim}stderr:${C.reset}`);
|
|
901
|
+
for (const ln of se.split("\n")) {
|
|
902
|
+
log(`${C.dim} ${ln}${C.reset}`);
|
|
903
|
+
}
|
|
904
|
+
} else {
|
|
905
|
+
log(`${C.dim}(stderr empty)${C.reset}`);
|
|
906
|
+
}
|
|
907
|
+
const lines = so.length > 0 ? so.split("\n") : [];
|
|
908
|
+
const tailLines = lines.length > 200 ? lines.slice(-200) : lines;
|
|
909
|
+
log(`${C.dim}stdout: ${lines.length} line(s), showing last ${tailLines.length}${C.reset}`);
|
|
910
|
+
for (const ln of tailLines) {
|
|
911
|
+
log(`${C.dim} ${ln}${C.reset}`);
|
|
912
|
+
}
|
|
913
|
+
log(`${C.dim}\u2500\u2500 end diagnostics \u2500\u2500${C.reset}`);
|
|
914
|
+
}
|
|
915
|
+
var CLAUDE_DEBUG_JSON_MAX_CHARS = 1e6;
|
|
916
|
+
function logClaudeRawFailureJson(payload) {
|
|
917
|
+
const stamped = {
|
|
918
|
+
_tag: "claude-runner-claude-raw-failure",
|
|
919
|
+
_capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
920
|
+
...payload
|
|
921
|
+
};
|
|
922
|
+
console.log(JSON.stringify(stamped, null, 2));
|
|
923
|
+
}
|
|
924
|
+
function truncateClaudeDebugString(text) {
|
|
925
|
+
const fullLength = text.length;
|
|
926
|
+
if (fullLength <= CLAUDE_DEBUG_JSON_MAX_CHARS) {
|
|
927
|
+
return { text, fullLength, truncated: false };
|
|
928
|
+
}
|
|
929
|
+
return {
|
|
930
|
+
text: `${text.slice(0, CLAUDE_DEBUG_JSON_MAX_CHARS)}
|
|
931
|
+
\u2026 [truncated; copy from logs or raise CLAUDE_DEBUG_JSON_MAX_CHARS]`,
|
|
932
|
+
fullLength,
|
|
933
|
+
truncated: true
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
async function processRun(client, run, config, worktreeDir, detail) {
|
|
644
937
|
const runNum = ++runCounter;
|
|
645
938
|
const runTag = ` ${C.pid}[${runNum}]${C.reset}`;
|
|
646
939
|
const log = (msg) => console.log(`${runTag} ${msg}`);
|
|
647
940
|
const logGreen = (msg) => console.log(`${runTag} ${C.green}${msg}${C.reset}`);
|
|
648
|
-
const detail = await client.getRunDetail(run.id);
|
|
649
941
|
const ownerName = run.owner?.name ?? detail.owner?.name ?? "unknown";
|
|
650
942
|
log(`\u{1F4CB} "${run.taskTitle}" \u2014 delegated by ${ownerName}`);
|
|
651
943
|
if (detail.status !== "PENDING") {
|
|
@@ -675,6 +967,11 @@ async function processRun(client, run, config, worktreeDir) {
|
|
|
675
967
|
}
|
|
676
968
|
log("\u25B6 Run started");
|
|
677
969
|
const effectiveCwd = worktreeDir ?? config.workDir;
|
|
970
|
+
let lastClaudeStderr = "";
|
|
971
|
+
let lastClaudeStdout = "";
|
|
972
|
+
let lastClaudeRawOutput = "";
|
|
973
|
+
let lastClaudeExitCode = null;
|
|
974
|
+
let lastClaudeSessionId;
|
|
678
975
|
try {
|
|
679
976
|
if (!worktreeDir && runOptions.branch) {
|
|
680
977
|
log(`\u{1F33F} Checking out branch: ${runOptions.branch}`);
|
|
@@ -703,7 +1000,12 @@ async function processRun(client, run, config, worktreeDir) {
|
|
|
703
1000
|
effectiveCwd
|
|
704
1001
|
);
|
|
705
1002
|
log(`\u{1F916} Claude Code spawned (pid ${child.pid})${isFollowUp ? " (follow-up)" : ""}`);
|
|
706
|
-
let { stdout, rawOutput, exitCode, sessionId, streamPrUrl } = await output;
|
|
1003
|
+
let { stdout, rawOutput, stderr, exitCode, sessionId, streamPrUrl } = await output;
|
|
1004
|
+
lastClaudeStderr = stderr;
|
|
1005
|
+
lastClaudeStdout = stdout;
|
|
1006
|
+
lastClaudeRawOutput = rawOutput;
|
|
1007
|
+
lastClaudeExitCode = exitCode;
|
|
1008
|
+
lastClaudeSessionId = sessionId;
|
|
707
1009
|
if (exitCode !== 0 && isFollowUp) {
|
|
708
1010
|
log(`\u26A0 Session resume failed, retrying with fresh session\u2026`);
|
|
709
1011
|
sessionCache.delete(run.taskId);
|
|
@@ -712,21 +1014,81 @@ async function processRun(client, run, config, worktreeDir) {
|
|
|
712
1014
|
const retryResult = await retry.output;
|
|
713
1015
|
stdout = retryResult.stdout;
|
|
714
1016
|
rawOutput = retryResult.rawOutput;
|
|
1017
|
+
stderr = retryResult.stderr;
|
|
715
1018
|
exitCode = retryResult.exitCode;
|
|
716
1019
|
sessionId = retryResult.sessionId;
|
|
717
1020
|
streamPrUrl = retryResult.streamPrUrl;
|
|
1021
|
+
lastClaudeStderr = stderr;
|
|
1022
|
+
lastClaudeStdout = stdout;
|
|
1023
|
+
lastClaudeRawOutput = rawOutput;
|
|
1024
|
+
lastClaudeExitCode = exitCode;
|
|
1025
|
+
lastClaudeSessionId = sessionId;
|
|
718
1026
|
}
|
|
719
1027
|
if (sessionId) {
|
|
720
1028
|
sessionCache.set(run.taskId, sessionId);
|
|
721
1029
|
}
|
|
722
1030
|
const pullRequestUrl = streamPrUrl ?? await detectPullRequestUrl(effectiveCwd) ?? extractPullRequestUrlFromOutput(stdout) ?? extractPullRequestUrlFromOutput(rawOutput);
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
1031
|
+
const streamEvents = parseClaudeNdjsonEvents(rawOutput);
|
|
1032
|
+
const combinedForLimit = stripAnsi(`${stderr}
|
|
1033
|
+
${rawOutput}`);
|
|
1034
|
+
const resultSummaryText = extractResultText(stdout);
|
|
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);
|
|
1043
|
+
const errorMsg = compactTaskErrorForApi(detail2);
|
|
1044
|
+
if (process.env.GCA_DEBUG_CLAUDE_RAW === "1") {
|
|
1045
|
+
const stderrDbg = truncateClaudeDebugString(stripAnsi(stderr));
|
|
1046
|
+
const stdoutDbg = truncateClaudeDebugString(stripAnsi(stdout));
|
|
1047
|
+
const rawDbg = truncateClaudeDebugString(stripAnsi(rawOutput));
|
|
1048
|
+
logClaudeRawFailureJson({
|
|
1049
|
+
runId: run.id,
|
|
1050
|
+
taskId: run.taskId,
|
|
1051
|
+
taskTitle: run.taskTitle,
|
|
1052
|
+
exitCode,
|
|
1053
|
+
usageCap,
|
|
1054
|
+
sessionId: sessionId ?? null,
|
|
1055
|
+
streamPrUrl: streamPrUrl ?? null,
|
|
1056
|
+
pullRequestUrl: pullRequestUrl ?? null,
|
|
1057
|
+
formattedDetail: detail2,
|
|
1058
|
+
taskErrorMessageSentToApi: errorMsg,
|
|
1059
|
+
parsedEventTypes: streamEvents.map((e) => e.type),
|
|
1060
|
+
stderr: stderrDbg.text,
|
|
1061
|
+
stderrMeta: { fullLength: stderrDbg.fullLength, truncated: stderrDbg.truncated },
|
|
1062
|
+
stdout: stdoutDbg.text,
|
|
1063
|
+
stdoutMeta: { fullLength: stdoutDbg.fullLength, truncated: stdoutDbg.truncated },
|
|
1064
|
+
rawOutput: rawDbg.text,
|
|
1065
|
+
rawOutputMeta: { fullLength: rawDbg.fullLength, truncated: rawDbg.truncated },
|
|
1066
|
+
resultTextFromExtract: resultSummaryText || null
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
const retryInfo = extractRateLimitRetryInfo(streamEvents);
|
|
1070
|
+
await client.errorRun(run.id, errorMsg, {
|
|
1071
|
+
pullRequestUrl,
|
|
1072
|
+
errorType: retryInfo?.errorType,
|
|
1073
|
+
retryAfter: retryInfo?.retryAfterMs
|
|
1074
|
+
});
|
|
1075
|
+
if (retryInfo) {
|
|
1076
|
+
const retryAt = new Date(retryInfo.retryAfterMs).toLocaleString(void 0, {
|
|
1077
|
+
dateStyle: "medium",
|
|
1078
|
+
timeStyle: "short"
|
|
1079
|
+
});
|
|
1080
|
+
log(`${C.red}\u23F8 Rate limited \u2014 server will retry at ${retryAt}${C.reset}`);
|
|
1081
|
+
} else {
|
|
1082
|
+
log(
|
|
1083
|
+
`${C.red}\u274C Run errored${exitCode !== 0 ? ` (exit code ${exitCode})` : " (usage limit)"}${C.reset}`
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
for (const line of detail2.split("\n")) {
|
|
1087
|
+
log(`${C.red} ${line}${C.reset}`);
|
|
1088
|
+
}
|
|
1089
|
+
if (config.verbose) {
|
|
1090
|
+
logClaudeRunFailureDiagnostics(log, stderr, rawOutput);
|
|
1091
|
+
}
|
|
730
1092
|
return;
|
|
731
1093
|
}
|
|
732
1094
|
const resultText = extractResultText(stdout);
|
|
@@ -736,6 +1098,28 @@ ${stdout.slice(0, 2e3)}
|
|
|
736
1098
|
logGreen(`\u2705 Run completed`);
|
|
737
1099
|
} catch (err) {
|
|
738
1100
|
const message = err instanceof Error ? err.message : String(err);
|
|
1101
|
+
const stack = err instanceof Error ? err.stack : void 0;
|
|
1102
|
+
const stderrDbg = truncateClaudeDebugString(stripAnsi(lastClaudeStderr));
|
|
1103
|
+
const stdoutDbg = truncateClaudeDebugString(stripAnsi(lastClaudeStdout));
|
|
1104
|
+
const rawDbg = truncateClaudeDebugString(stripAnsi(lastClaudeRawOutput));
|
|
1105
|
+
if (process.env.GCA_DEBUG_CLAUDE_RAW === "1") {
|
|
1106
|
+
logClaudeRawFailureJson({
|
|
1107
|
+
runId: run.id,
|
|
1108
|
+
taskId: run.taskId,
|
|
1109
|
+
taskTitle: run.taskTitle,
|
|
1110
|
+
thrownMessage: message,
|
|
1111
|
+
thrownStack: stack ?? null,
|
|
1112
|
+
exitCode: lastClaudeExitCode,
|
|
1113
|
+
sessionId: lastClaudeSessionId ?? null,
|
|
1114
|
+
note: "Thrown before or after Claude finished; fields may be empty.",
|
|
1115
|
+
stderr: stderrDbg.text,
|
|
1116
|
+
stderrMeta: { fullLength: stderrDbg.fullLength, truncated: stderrDbg.truncated },
|
|
1117
|
+
stdout: stdoutDbg.text,
|
|
1118
|
+
stdoutMeta: { fullLength: stdoutDbg.fullLength, truncated: stdoutDbg.truncated },
|
|
1119
|
+
rawOutput: rawDbg.text,
|
|
1120
|
+
rawOutputMeta: { fullLength: rawDbg.fullLength, truncated: rawDbg.truncated }
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
739
1123
|
const errorBody = `Claude Code runner error:
|
|
740
1124
|
\`\`\`
|
|
741
1125
|
${message.slice(0, 2e3)}
|
|
@@ -772,6 +1156,25 @@ function loadEnvFile() {
|
|
|
772
1156
|
}
|
|
773
1157
|
}
|
|
774
1158
|
}
|
|
1159
|
+
function getRunnerPackageInfo() {
|
|
1160
|
+
try {
|
|
1161
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
1162
|
+
const pkgPath = path.join(here, "..", "package.json");
|
|
1163
|
+
const raw = readFileSync(pkgPath, "utf-8");
|
|
1164
|
+
const pkg = JSON.parse(raw);
|
|
1165
|
+
return {
|
|
1166
|
+
name: pkg.name ?? "@groupchatai/claude-runner",
|
|
1167
|
+
version: pkg.version ?? "unknown"
|
|
1168
|
+
};
|
|
1169
|
+
} catch {
|
|
1170
|
+
return { name: "@groupchatai/claude-runner", version: "unknown" };
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
function showVersion() {
|
|
1174
|
+
const { name, version } = getRunnerPackageInfo();
|
|
1175
|
+
console.log(`${name} ${version}`);
|
|
1176
|
+
process.exit(0);
|
|
1177
|
+
}
|
|
775
1178
|
function showHelp() {
|
|
776
1179
|
console.log(`
|
|
777
1180
|
Usage: npx @groupchatai/claude-runner [command] [options]
|
|
@@ -792,6 +1195,7 @@ Options:
|
|
|
792
1195
|
--token <token> Agent token (or set GCA_TOKEN env var)
|
|
793
1196
|
--no-worktree Disable git worktree isolation for runs
|
|
794
1197
|
-h, --help Show this help message
|
|
1198
|
+
-v, -version, --version Print version and exit
|
|
795
1199
|
|
|
796
1200
|
Environment variables:
|
|
797
1201
|
GCA_TOKEN Agent token (gca_...)
|
|
@@ -850,6 +1254,11 @@ function parseArgs() {
|
|
|
850
1254
|
case "-h":
|
|
851
1255
|
showHelp();
|
|
852
1256
|
break;
|
|
1257
|
+
case "--version":
|
|
1258
|
+
case "-v":
|
|
1259
|
+
case "-version":
|
|
1260
|
+
showVersion();
|
|
1261
|
+
break;
|
|
853
1262
|
case "--no-worktree":
|
|
854
1263
|
config.useWorktrees = false;
|
|
855
1264
|
break;
|
|
@@ -919,6 +1328,12 @@ var TaskScheduler = class {
|
|
|
919
1328
|
return this.slots.size === 0;
|
|
920
1329
|
}
|
|
921
1330
|
};
|
|
1331
|
+
function drainSchedulerSlot(scheduler, run) {
|
|
1332
|
+
let current = run;
|
|
1333
|
+
while (current) {
|
|
1334
|
+
current = scheduler.complete(current);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
922
1337
|
function handlePendingRuns(runs, scheduler, client, config) {
|
|
923
1338
|
if (runs.length > 0 && config.verbose) {
|
|
924
1339
|
console.log(`\u{1F4EC} ${runs.length} pending run(s)`);
|
|
@@ -943,27 +1358,47 @@ function handlePendingRuns(runs, scheduler, client, config) {
|
|
|
943
1358
|
}
|
|
944
1359
|
}
|
|
945
1360
|
async function processRunWithDrain(client, run, scheduler, config) {
|
|
1361
|
+
let detail;
|
|
1362
|
+
try {
|
|
1363
|
+
detail = await client.getRunDetail(run.id);
|
|
1364
|
+
} catch (err) {
|
|
1365
|
+
console.error(` \u274C Failed to fetch run detail for ${run.id}:`, err);
|
|
1366
|
+
drainSchedulerSlot(scheduler, run);
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
946
1369
|
let worktreeDir;
|
|
947
1370
|
if (config.useWorktrees) {
|
|
948
1371
|
try {
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
)
|
|
1372
|
+
const existingBranch = await resolveExistingPrBranch(detail, config.workDir);
|
|
1373
|
+
worktreeDir = createWorktree(config.workDir, run.taskId, existingBranch);
|
|
1374
|
+
const rel = path.relative(config.workDir, worktreeDir);
|
|
1375
|
+
if (existingBranch) {
|
|
1376
|
+
console.log(` \u{1F333} Worktree from existing PR branch ${existingBranch} \u2192 ${rel}`);
|
|
1377
|
+
} else {
|
|
1378
|
+
console.log(` \u{1F333} Worktree created from main \u2192 ${rel}`);
|
|
1379
|
+
}
|
|
953
1380
|
} catch (err) {
|
|
954
1381
|
console.error(` \u26A0 Failed to create worktree, running in-place:`, err);
|
|
955
1382
|
}
|
|
956
1383
|
}
|
|
957
1384
|
let current = run;
|
|
1385
|
+
let currentDetail = detail;
|
|
958
1386
|
while (current) {
|
|
959
1387
|
try {
|
|
960
|
-
await processRun(client, current, config, worktreeDir);
|
|
1388
|
+
await processRun(client, current, config, worktreeDir, currentDetail);
|
|
961
1389
|
} catch (err) {
|
|
962
1390
|
console.error(`Unhandled error processing run ${current.id}:`, err);
|
|
963
1391
|
}
|
|
964
1392
|
current = scheduler.complete(current);
|
|
965
1393
|
if (current) {
|
|
966
1394
|
console.log(` \u23ED Processing queued follow-up\u2026`);
|
|
1395
|
+
try {
|
|
1396
|
+
currentDetail = await client.getRunDetail(current.id);
|
|
1397
|
+
} catch (err) {
|
|
1398
|
+
console.error(` \u274C Failed to fetch detail for follow-up ${current.id}:`, err);
|
|
1399
|
+
drainSchedulerSlot(scheduler, current);
|
|
1400
|
+
break;
|
|
1401
|
+
}
|
|
967
1402
|
}
|
|
968
1403
|
}
|
|
969
1404
|
if (worktreeDir) {
|
|
@@ -1000,13 +1435,22 @@ async function runWithWebSocket(client, config, scheduler) {
|
|
|
1000
1435
|
}
|
|
1001
1436
|
);
|
|
1002
1437
|
await new Promise((resolve) => {
|
|
1003
|
-
|
|
1438
|
+
function shutdown() {
|
|
1439
|
+
if (!tryBeginClaudeRunnerSignalTeardown()) return;
|
|
1440
|
+
process.removeListener("SIGINT", shutdown);
|
|
1441
|
+
process.removeListener("SIGTERM", shutdown);
|
|
1004
1442
|
console.log("\n\u{1F6D1} Shutting down\u2026");
|
|
1005
|
-
void
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1443
|
+
void (async () => {
|
|
1444
|
+
try {
|
|
1445
|
+
await convex.close();
|
|
1446
|
+
} catch {
|
|
1447
|
+
} finally {
|
|
1448
|
+
resolve();
|
|
1449
|
+
}
|
|
1450
|
+
})();
|
|
1451
|
+
}
|
|
1452
|
+
process.once("SIGINT", shutdown);
|
|
1453
|
+
process.once("SIGTERM", shutdown);
|
|
1010
1454
|
});
|
|
1011
1455
|
}
|
|
1012
1456
|
async function runWithPolling(client, config, scheduler) {
|
|
@@ -1019,12 +1463,15 @@ async function runWithPolling(client, config, scheduler) {
|
|
|
1019
1463
|
`
|
|
1020
1464
|
);
|
|
1021
1465
|
let running = true;
|
|
1022
|
-
|
|
1466
|
+
function shutdownPoll() {
|
|
1467
|
+
if (!tryBeginClaudeRunnerSignalTeardown()) return;
|
|
1468
|
+
process.removeListener("SIGINT", shutdownPoll);
|
|
1469
|
+
process.removeListener("SIGTERM", shutdownPoll);
|
|
1023
1470
|
console.log("\n\u{1F6D1} Shutting down\u2026");
|
|
1024
1471
|
running = false;
|
|
1025
|
-
}
|
|
1026
|
-
process.
|
|
1027
|
-
process.
|
|
1472
|
+
}
|
|
1473
|
+
process.once("SIGINT", shutdownPoll);
|
|
1474
|
+
process.once("SIGTERM", shutdownPoll);
|
|
1028
1475
|
if (config.once) {
|
|
1029
1476
|
try {
|
|
1030
1477
|
const pending = await client.listPendingRuns();
|
|
@@ -1048,7 +1495,14 @@ async function runWithPolling(client, config, scheduler) {
|
|
|
1048
1495
|
await sleep(config.pollInterval);
|
|
1049
1496
|
}
|
|
1050
1497
|
}
|
|
1498
|
+
function wantsVersionOnly(argv) {
|
|
1499
|
+
return argv.some((a) => a === "--version" || a === "-v" || a === "-version");
|
|
1500
|
+
}
|
|
1051
1501
|
async function main() {
|
|
1502
|
+
const argv = process.argv.slice(2);
|
|
1503
|
+
if (wantsVersionOnly(argv)) {
|
|
1504
|
+
showVersion();
|
|
1505
|
+
}
|
|
1052
1506
|
if (process.argv.includes("cleanup")) {
|
|
1053
1507
|
loadEnvFile();
|
|
1054
1508
|
const workDir = process.cwd();
|