@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.
Files changed (2) hide show
  1. package/dist/index.js +91 -17
  2. 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 instead of WebSocket (default: websocket)
786
- --poll-interval <ms> Polling interval in milliseconds (default: 5000, only with --poll)
787
- --max-concurrent <n> Max concurrent tasks (default: 1)
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: 5e3,
812
- maxConcurrent: 1,
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] ?? "5000", 10);
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
- worktreeDir = createWorktree(config.workDir, run.taskId);
950
- console.log(
951
- ` \u{1F333} Worktree created from main \u2192 ${path.relative(config.workDir, worktreeDir)}`
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}ms \u2014 listening for tasks\u2026
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");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groupchatai/claude-runner",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "Run GroupChat AI agent tasks locally with Claude Code",
5
5
  "type": "module",
6
6
  "bin": {