claude-teammate 0.1.313 → 0.1.314

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-teammate",
3
- "version": "0.1.313",
3
+ "version": "0.1.314",
4
4
  "description": "CLI bootstrapper for Claude Teammate.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/repo.js CHANGED
@@ -770,6 +770,31 @@ export async function branchHasChangesAgainstMain(repoPath, baseRef) {
770
770
  return Number.isFinite(count) && count > 0;
771
771
  }
772
772
 
773
+ /**
774
+ * True when HEAD's own work has already landed on the base branch (e.g. the
775
+ * branch was merged via another MR / a human merged it directly). In that case
776
+ * HEAD is an ancestor of the base tip while still being a distinct commit, so a
777
+ * branch-vs-base diff is empty and re-running Claude can never produce changes.
778
+ * Callers use this to terminate instead of looping on "no changes to commit".
779
+ * A brand-new branch that merely points at the base tip (HEAD === base) returns
780
+ * false — that is an unstarted PR, not a merged one. Best-effort; never throws.
781
+ */
782
+ export async function branchMergedIntoBase(repoPath, baseRef) {
783
+ const branch = baseRef || (await resolveRemoteDefaultBranch(repoPath));
784
+ try {
785
+ const headSha = (await execGitOutput(repoPath, ["rev-parse", "HEAD"])).trim();
786
+ const baseSha = (await execGitOutput(repoPath, ["rev-parse", `origin/${branch}`])).trim();
787
+ if (!headSha || !baseSha || headSha === baseSha) {
788
+ return false;
789
+ }
790
+ // exit 0 → HEAD is an ancestor of the base tip → already merged.
791
+ await execGit(repoPath, ["merge-base", "--is-ancestor", "HEAD", `origin/${branch}`]);
792
+ return true;
793
+ } catch {
794
+ return false;
795
+ }
796
+ }
797
+
773
798
  async function resolveRemoteDefaultBranch(repoPath) {
774
799
  try {
775
800
  const output = await execGitOutput(repoPath, ["symbolic-ref", "refs/remotes/origin/HEAD"]);
@@ -6,6 +6,7 @@ import {
6
6
  } from "../claude.js";
7
7
  import {
8
8
  branchHasChangesAgainstMain,
9
+ branchMergedIntoBase,
9
10
  commitAndPushRepoChanges,
10
11
  ensureWorktreeForBranch,
11
12
  getBranchDiffAgainstBase,
@@ -331,6 +332,16 @@ export async function processPullRequestImplementation({
331
332
  pr: detail.number
332
333
  });
333
334
  result = { result: "implemented", summary: "Implementation already committed on previous run." };
335
+ } else if (!latestComment && (await branchMergedIntoBase(repoPath, detail.baseRef).catch(() => false))) {
336
+ // The branch's work already landed on the base (merged via another MR or
337
+ // by a human). There is nothing left to implement, so skip Claude
338
+ // entirely — running it would only report "no changes" and loop. A human
339
+ // comment overrides this so feedback can still be addressed.
340
+ await logger.info("Head branch already merged into base; skipping Claude (nothing to implement)", {
341
+ repo: repo.url,
342
+ pr: detail.number
343
+ });
344
+ result = { result: "implemented", summary: "Changes already merged into the base branch." };
334
345
  } else {
335
346
  result = await services.runClaudeImplementation({
336
347
  pullRequest: { ...detail, body: nextBody },
@@ -440,7 +451,53 @@ export async function processPullRequestImplementation({
440
451
  );
441
452
 
442
453
  if (!commitResult.committed && !commitResult.branchHasImplementation) {
443
- throw new Error("Claude reported success but no repository changes were available to commit.");
454
+ // No diff to commit. If the branch's work already landed on the base
455
+ // (merged via another MR / by a human), re-running Claude can never
456
+ // produce changes — treat it as terminally implemented instead of
457
+ // looping. Otherwise stop without preserving STUCK_AT so the bot waits
458
+ // for human guidance rather than machine-gun retrying an unchangeable
459
+ // no-op every retry window.
460
+ const mergedIntoBase = await branchMergedIntoBase(repoPath, detail.baseRef).catch(() => false);
461
+ if (mergedIntoBase) {
462
+ const mergedBody = setPullRequestStatus(nextBody, "IMPLEMENTED");
463
+ await github.updatePullRequest(repo.url, detail.number, { title: detail.title, body: mergedBody });
464
+ await services.clearForgePullRequestBlocked(github, { ...detail, repoUrl: repo.url });
465
+ if (markReadyOnSuccess) {
466
+ await github.markPullRequestReady(repo.url, detail.number);
467
+ await services.transitionLinkedJiraIssueToReview(detail.title, jira, logger, detail.number);
468
+ }
469
+ const mergedComment =
470
+ successCommentBody ||
471
+ formatPullRequestOutcomeComment(
472
+ result.summary,
473
+ "The requested changes are already merged into the base branch; nothing left to implement."
474
+ );
475
+ await postPullRequestComment(github, repo.url, detail.number, mergedComment);
476
+ await postLinkedJiraComment(extractJiraKey(detail.title), mergedComment, jira, logger);
477
+ await logger.info("Branch already merged into base; PR marked IMPLEMENTED", {
478
+ repo: repo.url,
479
+ pr: detail.number
480
+ });
481
+ return buildDraftPrState(
482
+ { ...detail, body: mergedBody, draft: markReadyOnSuccess ? false : detail.draft },
483
+ "IMPLEMENTED",
484
+ "implemented"
485
+ );
486
+ }
487
+ const noChangeBody = clearStuckAt(setPullRequestStatus(nextBody, "STUCK"));
488
+ await github.updatePullRequest(repo.url, detail.number, { title: detail.title, body: noChangeBody });
489
+ await services.setForgePullRequestBlocked(github, { ...detail, repoUrl: repo.url });
490
+ const noChangeComment = formatPullRequestOutcomeComment(
491
+ result.summary,
492
+ "Claude reported success but produced no repository changes, so there was nothing to commit. Halting automatic retries until there is new guidance."
493
+ );
494
+ await postPullRequestComment(github, repo.url, detail.number, noChangeComment);
495
+ await postLinkedJiraComment(extractJiraKey(detail.title), noChangeComment, jira, logger);
496
+ await logger.error("Pull request produced no repository changes; halting auto-retry", {
497
+ repo: repo.url,
498
+ pr: detail.number
499
+ });
500
+ return buildDraftPrState({ ...detail, body: noChangeBody }, "STUCK", "stuck");
444
501
  }
445
502
 
446
503
  const implementedBody = setPullRequestStatus(nextBody, "IMPLEMENTED");