@autoclawd/autoclawd 1.1.5 → 1.1.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 CHANGED
@@ -31,7 +31,8 @@ var LinearConfigSchema = z.object({
31
31
  statuses: z.array(z.string()).default(["Todo"]),
32
32
  assignToMe: z.boolean().default(true),
33
33
  doneStatus: z.string().default("Done"),
34
- inProgressStatus: z.string().default("In Progress")
34
+ inProgressStatus: z.string().default("In Progress"),
35
+ inReviewStatus: z.string().default("In Review")
35
36
  });
36
37
  var GitHubConfigSchema = z.object({
37
38
  token: z.string(),
@@ -328,6 +329,20 @@ async function fetchTicket(client, identifier) {
328
329
  url: issue.url
329
330
  };
330
331
  }
332
+ async function fetchTicketStatus(client, identifier) {
333
+ const match = identifier.match(/^([A-Za-z]+)-(\d+)$/);
334
+ if (!match) return void 0;
335
+ const [, teamKey, numStr] = match;
336
+ const team = await client.team(teamKey.toUpperCase());
337
+ const result = await team.issues({
338
+ filter: { number: { eq: parseInt(numStr) } },
339
+ first: 1
340
+ });
341
+ const issue = result.nodes[0];
342
+ if (!issue) return void 0;
343
+ const state = await issue.state;
344
+ return state?.name;
345
+ }
331
346
  async function claimTicket(client, config, ticketId) {
332
347
  log.info(`Claiming ticket, moving to "${config.linear.inProgressStatus}"`);
333
348
  await retry(async () => {
@@ -342,17 +357,23 @@ async function claimTicket(client, config, ticketId) {
342
357
  await client.updateIssue(ticketId, { stateId: inProgress.id });
343
358
  }, { label: "Linear claim", retryIf: isTransientError });
344
359
  }
345
- async function completeTicket(client, config, ticketId, comment) {
360
+ async function reviewTicket(client, config, ticketId, comment) {
346
361
  await retry(async () => {
347
362
  await client.createComment({ issueId: ticketId, body: comment });
348
363
  const team = await client.team(config.linear.teamId);
349
364
  const states = await team.states();
350
- const doneTarget = config.linear.doneStatus.toLowerCase();
351
- const done = states.nodes.find((s) => s.name.toLowerCase() === doneTarget);
352
- if (done) {
353
- await client.updateIssue(ticketId, { stateId: done.id });
365
+ const target = config.linear.inReviewStatus.toLowerCase();
366
+ const review = states.nodes.find((s) => s.name.toLowerCase() === target);
367
+ if (review) {
368
+ await client.updateIssue(ticketId, { stateId: review.id });
369
+ } else {
370
+ const doneTarget = config.linear.doneStatus.toLowerCase();
371
+ const done = states.nodes.find((s) => s.name.toLowerCase() === doneTarget);
372
+ if (done) {
373
+ await client.updateIssue(ticketId, { stateId: done.id });
374
+ }
354
375
  }
355
- }, { label: "Linear complete", retryIf: isTransientError });
376
+ }, { label: "Linear review", retryIf: isTransientError });
356
377
  }
357
378
  async function addBranchLabel(client, ticketId, branchName) {
358
379
  try {
@@ -482,20 +503,29 @@ async function ensureRepoExists(octokit, opts) {
482
503
  if (status !== 404) throw err;
483
504
  }
484
505
  log.info(`Repo ${opts.owner}/${opts.repo} does not exist, creating...`);
485
- const { data: authUser } = await octokit.rest.users.getAuthenticated();
486
- if (authUser.login === opts.owner) {
487
- await octokit.rest.repos.createForAuthenticatedUser({
488
- name: opts.repo,
489
- description: opts.description,
490
- auto_init: false
491
- });
492
- } else {
493
- await octokit.rest.repos.createInOrg({
494
- org: opts.owner,
495
- name: opts.repo,
496
- description: opts.description,
497
- auto_init: false
498
- });
506
+ try {
507
+ const { data: authUser } = await octokit.rest.users.getAuthenticated();
508
+ if (authUser.login === opts.owner) {
509
+ await octokit.rest.repos.createForAuthenticatedUser({
510
+ name: opts.repo,
511
+ description: opts.description,
512
+ auto_init: false
513
+ });
514
+ } else {
515
+ await octokit.rest.repos.createInOrg({
516
+ org: opts.owner,
517
+ name: opts.repo,
518
+ description: opts.description,
519
+ auto_init: false
520
+ });
521
+ }
522
+ } catch (err) {
523
+ const msg = err instanceof Error ? err.message : String(err);
524
+ if (msg.includes("name already exists")) {
525
+ log.info(`Repo ${opts.owner}/${opts.repo} was created by another ticket, continuing`);
526
+ return { created: false };
527
+ }
528
+ throw err;
499
529
  }
500
530
  return { created: true };
501
531
  }
@@ -1581,8 +1611,28 @@ async function executeTicket(opts) {
1581
1611
  let workDir;
1582
1612
  let container;
1583
1613
  try {
1584
- await claimTicket(linearClient, config, ticket.id);
1585
- log.ticket(ticket.identifier, `Claimed \u2014 ${ticket.repoUrl}`);
1614
+ if (ticket.baseBranch) {
1615
+ const parentMatch = ticket.baseBranch.match(/^autoclawd\/([A-Z]+-\d+)/i);
1616
+ if (parentMatch) {
1617
+ const parentId = parentMatch[1].toUpperCase();
1618
+ const parentStatus = await fetchTicketStatus(linearClient, parentId);
1619
+ if (parentStatus) {
1620
+ const statusLower = parentStatus.toLowerCase();
1621
+ const reviewTarget = config.linear.inReviewStatus.toLowerCase();
1622
+ const doneTarget = config.linear.doneStatus.toLowerCase();
1623
+ if (statusLower !== reviewTarget && statusLower !== doneTarget) {
1624
+ log.ticket(ticket.identifier, `Parent ${parentId} is "${parentStatus}" \u2014 waiting for "${config.linear.inReviewStatus}" or "${config.linear.doneStatus}"`);
1625
+ return {
1626
+ ticketId: ticket.identifier,
1627
+ success: false,
1628
+ error: `REQUEUE: parent ${parentId} is "${parentStatus}", not yet in review or done`,
1629
+ iterations: 0
1630
+ };
1631
+ }
1632
+ log.ticket(ticket.identifier, `Parent ${parentId} is "${parentStatus}" \u2014 proceeding`);
1633
+ }
1634
+ }
1635
+ }
1586
1636
  const { owner: repoOwner, repo: repoName } = parseRepoUrl(ticket.repoUrl);
1587
1637
  const { created: repoCreated } = await ensureRepoExists(octokit, {
1588
1638
  owner: repoOwner,
@@ -1600,7 +1650,21 @@ async function executeTicket(opts) {
1600
1650
  }
1601
1651
  const detectedBase = repoCreated ? "main" : detectDefaultBranch2(ticket.repoUrl, config.github.token);
1602
1652
  log.ticket(ticket.identifier, `Default branch: ${detectedBase}`);
1603
- workDir = await cloneToTemp(ticket.repoUrl, detectedBase, config.github.token);
1653
+ try {
1654
+ workDir = await cloneToTemp(ticket.repoUrl, detectedBase, config.github.token);
1655
+ } catch (err) {
1656
+ const msg = err instanceof Error ? err.message : String(err);
1657
+ if (msg.includes("not found in upstream") || msg.includes("Could not find remote branch") || msg.includes("Remote branch")) {
1658
+ log.ticket(ticket.identifier, `Branch "${detectedBase}" not ready \u2014 will retry later`);
1659
+ return {
1660
+ ticketId: ticket.identifier,
1661
+ success: false,
1662
+ error: `REQUEUE: branch "${detectedBase}" does not exist yet`,
1663
+ iterations: 0
1664
+ };
1665
+ }
1666
+ throw err;
1667
+ }
1604
1668
  log.ticket(ticket.identifier, "Cloned repo");
1605
1669
  const repoLocal = loadRepoLocalConfig(workDir);
1606
1670
  const actualBase = ticket.baseBranch ?? repoLocal?.base ?? detectedBase;
@@ -1630,6 +1694,8 @@ async function executeTicket(opts) {
1630
1694
  }
1631
1695
  }
1632
1696
  execSync3(`git checkout -B ${branchName} --`, { cwd: workDir, stdio: "pipe" });
1697
+ await claimTicket(linearClient, config, ticket.id);
1698
+ log.ticket(ticket.identifier, `Claimed \u2014 ${ticket.repoUrl}`);
1633
1699
  container = await createContainer({
1634
1700
  dockerConfig: docker2,
1635
1701
  workspacePath: workDir,
@@ -1769,7 +1835,7 @@ async function executeTicket(opts) {
1769
1835
  }
1770
1836
  }
1771
1837
  }
1772
- await completeTicket(linearClient, config, ticket.id, `PR opened: ${pr.url}`);
1838
+ await reviewTicket(linearClient, config, ticket.id, `PR opened: ${pr.url}`);
1773
1839
  await addBranchLabel(linearClient, ticket.id, branchName);
1774
1840
  log.success(`${ticket.identifier} done \u2014 ${pr.url}`);
1775
1841
  const result = {