@groupchatai/claude-runner 0.4.1 → 0.4.3
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 +91 -17
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -461,15 +461,24 @@ function getDefaultBranch(repoDir) {
|
|
|
461
461
|
return "main";
|
|
462
462
|
}
|
|
463
463
|
}
|
|
464
|
-
function createWorktree(repoDir, taskId) {
|
|
464
|
+
function createWorktree(repoDir, taskId, existingBranch) {
|
|
465
465
|
const name = worktreeNameForTask(taskId);
|
|
466
|
-
const branchName = `agent/${name}-${Date.now()}`;
|
|
467
466
|
const worktreeBase = path.join(repoDir, WORKTREE_DIR);
|
|
468
467
|
const worktreePath = path.join(worktreeBase, name);
|
|
469
468
|
if (existsSync(worktreePath)) {
|
|
470
469
|
return worktreePath;
|
|
471
470
|
}
|
|
471
|
+
if (existingBranch) {
|
|
472
|
+
try {
|
|
473
|
+
execGit(["fetch", "origin", existingBranch, "--quiet"], repoDir);
|
|
474
|
+
} catch {
|
|
475
|
+
}
|
|
476
|
+
execGit(["worktree", "add", worktreePath, `origin/${existingBranch}`], repoDir);
|
|
477
|
+
writeFileSync(path.join(worktreePath, ".agent-branch"), existingBranch, "utf-8");
|
|
478
|
+
return worktreePath;
|
|
479
|
+
}
|
|
472
480
|
const baseBranch = getDefaultBranch(repoDir);
|
|
481
|
+
const branchName = `agent/${name}-${Date.now()}`;
|
|
473
482
|
try {
|
|
474
483
|
execGit(["fetch", "origin", baseBranch, "--quiet"], repoDir);
|
|
475
484
|
} catch {
|
|
@@ -478,6 +487,39 @@ function createWorktree(repoDir, taskId) {
|
|
|
478
487
|
writeFileSync(path.join(worktreePath, ".agent-branch"), branchName, "utf-8");
|
|
479
488
|
return worktreePath;
|
|
480
489
|
}
|
|
490
|
+
async function resolveExistingPrBranch(detail, workDir) {
|
|
491
|
+
try {
|
|
492
|
+
if (detail.branchName) return detail.branchName;
|
|
493
|
+
if (detail.pullRequestUrl) {
|
|
494
|
+
try {
|
|
495
|
+
const branch2 = await runShellCommand(
|
|
496
|
+
"gh",
|
|
497
|
+
["pr", "view", detail.pullRequestUrl, "--json", "headRefName", "--jq", ".headRefName"],
|
|
498
|
+
workDir
|
|
499
|
+
);
|
|
500
|
+
if (branch2?.trim()) return branch2.trim();
|
|
501
|
+
} catch {
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
const prUrls = [];
|
|
505
|
+
for (const item of detail.activity) {
|
|
506
|
+
if (item.body) {
|
|
507
|
+
const matches = item.body.match(GITHUB_PR_URL_RE);
|
|
508
|
+
if (matches) prUrls.push(...matches);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
const prUrl = prUrls[prUrls.length - 1];
|
|
512
|
+
if (!prUrl) return void 0;
|
|
513
|
+
const branch = await runShellCommand(
|
|
514
|
+
"gh",
|
|
515
|
+
["pr", "view", prUrl, "--json", "headRefName", "--jq", ".headRefName"],
|
|
516
|
+
workDir
|
|
517
|
+
);
|
|
518
|
+
if (branch?.trim()) return branch.trim();
|
|
519
|
+
} catch {
|
|
520
|
+
}
|
|
521
|
+
return void 0;
|
|
522
|
+
}
|
|
481
523
|
async function listOurWorktrees(workDir) {
|
|
482
524
|
const worktreeDir = path.join(workDir, WORKTREE_DIR);
|
|
483
525
|
let entries;
|
|
@@ -640,12 +682,11 @@ Removing ${safe.length} safe worktree(s)\u2026`);
|
|
|
640
682
|
}
|
|
641
683
|
console.log();
|
|
642
684
|
}
|
|
643
|
-
async function processRun(client, run, config, worktreeDir) {
|
|
685
|
+
async function processRun(client, run, config, worktreeDir, detail) {
|
|
644
686
|
const runNum = ++runCounter;
|
|
645
687
|
const runTag = ` ${C.pid}[${runNum}]${C.reset}`;
|
|
646
688
|
const log = (msg) => console.log(`${runTag} ${msg}`);
|
|
647
689
|
const logGreen = (msg) => console.log(`${runTag} ${C.green}${msg}${C.reset}`);
|
|
648
|
-
const detail = await client.getRunDetail(run.id);
|
|
649
690
|
const ownerName = run.owner?.name ?? detail.owner?.name ?? "unknown";
|
|
650
691
|
log(`\u{1F4CB} "${run.taskTitle}" \u2014 delegated by ${ownerName}`);
|
|
651
692
|
if (detail.status !== "PENDING") {
|
|
@@ -782,9 +823,9 @@ Commands:
|
|
|
782
823
|
|
|
783
824
|
Options:
|
|
784
825
|
--work-dir <path> Repo directory for Claude Code to work in (default: cwd)
|
|
785
|
-
--poll Use HTTP polling
|
|
786
|
-
--poll-interval <ms> Polling interval in milliseconds (default:
|
|
787
|
-
--max-concurrent <n> Max concurrent tasks (default:
|
|
826
|
+
--poll Use HTTP polling fallback (default: websocket, see below)
|
|
827
|
+
--poll-interval <ms> Polling interval in milliseconds (default: 30000, only with --poll)
|
|
828
|
+
--max-concurrent <n> Max concurrent tasks (default: 5)
|
|
788
829
|
--model <model> Claude model to use (passed to claude CLI)
|
|
789
830
|
--dry-run Poll and log runs without executing Claude Code
|
|
790
831
|
--once Process one batch of pending runs and exit (implies --poll)
|
|
@@ -808,8 +849,8 @@ function parseArgs() {
|
|
|
808
849
|
apiUrl: process.env.GCA_API_URL ?? API_URL,
|
|
809
850
|
convexUrl: process.env.GCA_CONVEX_URL ?? CONVEX_URL,
|
|
810
851
|
workDir: process.cwd(),
|
|
811
|
-
pollInterval:
|
|
812
|
-
maxConcurrent:
|
|
852
|
+
pollInterval: 3e4,
|
|
853
|
+
maxConcurrent: 5,
|
|
813
854
|
poll: false,
|
|
814
855
|
dryRun: false,
|
|
815
856
|
once: false,
|
|
@@ -823,7 +864,7 @@ function parseArgs() {
|
|
|
823
864
|
config.workDir = path.resolve(args[++i] ?? ".");
|
|
824
865
|
break;
|
|
825
866
|
case "--poll-interval":
|
|
826
|
-
config.pollInterval = parseInt(args[++i] ?? "
|
|
867
|
+
config.pollInterval = parseInt(args[++i] ?? "30000", 10);
|
|
827
868
|
break;
|
|
828
869
|
case "--max-concurrent":
|
|
829
870
|
config.maxConcurrent = parseInt(args[++i] ?? "1", 10);
|
|
@@ -919,6 +960,12 @@ var TaskScheduler = class {
|
|
|
919
960
|
return this.slots.size === 0;
|
|
920
961
|
}
|
|
921
962
|
};
|
|
963
|
+
function drainSchedulerSlot(scheduler, run) {
|
|
964
|
+
let current = run;
|
|
965
|
+
while (current) {
|
|
966
|
+
current = scheduler.complete(current);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
922
969
|
function handlePendingRuns(runs, scheduler, client, config) {
|
|
923
970
|
if (runs.length > 0 && config.verbose) {
|
|
924
971
|
console.log(`\u{1F4EC} ${runs.length} pending run(s)`);
|
|
@@ -943,27 +990,47 @@ function handlePendingRuns(runs, scheduler, client, config) {
|
|
|
943
990
|
}
|
|
944
991
|
}
|
|
945
992
|
async function processRunWithDrain(client, run, scheduler, config) {
|
|
993
|
+
let detail;
|
|
994
|
+
try {
|
|
995
|
+
detail = await client.getRunDetail(run.id);
|
|
996
|
+
} catch (err) {
|
|
997
|
+
console.error(` \u274C Failed to fetch run detail for ${run.id}:`, err);
|
|
998
|
+
drainSchedulerSlot(scheduler, run);
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
946
1001
|
let worktreeDir;
|
|
947
1002
|
if (config.useWorktrees) {
|
|
948
1003
|
try {
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
)
|
|
1004
|
+
const existingBranch = await resolveExistingPrBranch(detail, config.workDir);
|
|
1005
|
+
worktreeDir = createWorktree(config.workDir, run.taskId, existingBranch);
|
|
1006
|
+
const rel = path.relative(config.workDir, worktreeDir);
|
|
1007
|
+
if (existingBranch) {
|
|
1008
|
+
console.log(` \u{1F333} Worktree from existing PR branch ${existingBranch} \u2192 ${rel}`);
|
|
1009
|
+
} else {
|
|
1010
|
+
console.log(` \u{1F333} Worktree created from main \u2192 ${rel}`);
|
|
1011
|
+
}
|
|
953
1012
|
} catch (err) {
|
|
954
1013
|
console.error(` \u26A0 Failed to create worktree, running in-place:`, err);
|
|
955
1014
|
}
|
|
956
1015
|
}
|
|
957
1016
|
let current = run;
|
|
1017
|
+
let currentDetail = detail;
|
|
958
1018
|
while (current) {
|
|
959
1019
|
try {
|
|
960
|
-
await processRun(client, current, config, worktreeDir);
|
|
1020
|
+
await processRun(client, current, config, worktreeDir, currentDetail);
|
|
961
1021
|
} catch (err) {
|
|
962
1022
|
console.error(`Unhandled error processing run ${current.id}:`, err);
|
|
963
1023
|
}
|
|
964
1024
|
current = scheduler.complete(current);
|
|
965
1025
|
if (current) {
|
|
966
1026
|
console.log(` \u23ED Processing queued follow-up\u2026`);
|
|
1027
|
+
try {
|
|
1028
|
+
currentDetail = await client.getRunDetail(current.id);
|
|
1029
|
+
} catch (err) {
|
|
1030
|
+
console.error(` \u274C Failed to fetch detail for follow-up ${current.id}:`, err);
|
|
1031
|
+
drainSchedulerSlot(scheduler, current);
|
|
1032
|
+
break;
|
|
1033
|
+
}
|
|
967
1034
|
}
|
|
968
1035
|
}
|
|
969
1036
|
if (worktreeDir) {
|
|
@@ -984,6 +1051,7 @@ async function runWithWebSocket(client, config, scheduler) {
|
|
|
984
1051
|
({ anyApi } = await import("convex/server"));
|
|
985
1052
|
} catch {
|
|
986
1053
|
console.warn("\u26A0 convex package not found \u2014 falling back to HTTP polling.");
|
|
1054
|
+
console.warn(" WebSocket mode is recommended for instant task pickup and zero idle cost.");
|
|
987
1055
|
console.warn(" Install convex for WebSocket mode: npm i convex\n");
|
|
988
1056
|
await runWithPolling(client, config, scheduler);
|
|
989
1057
|
return;
|
|
@@ -1009,8 +1077,14 @@ async function runWithWebSocket(client, config, scheduler) {
|
|
|
1009
1077
|
});
|
|
1010
1078
|
}
|
|
1011
1079
|
async function runWithPolling(client, config, scheduler) {
|
|
1012
|
-
console.log(`\u{1F4E1} Polling every ${config.pollInterval}
|
|
1013
|
-
|
|
1080
|
+
console.log(`\u{1F4E1} Polling every ${config.pollInterval / 1e3}s \u2014 listening for tasks\u2026`);
|
|
1081
|
+
console.log(
|
|
1082
|
+
`${C.dim} Tip: WebSocket mode (default) picks up tasks instantly with zero idle cost.${C.reset}`
|
|
1083
|
+
);
|
|
1084
|
+
console.log(
|
|
1085
|
+
`${C.dim} Remove --poll unless your network blocks WebSocket connections.${C.reset}
|
|
1086
|
+
`
|
|
1087
|
+
);
|
|
1014
1088
|
let running = true;
|
|
1015
1089
|
const shutdown = () => {
|
|
1016
1090
|
console.log("\n\u{1F6D1} Shutting down\u2026");
|