Skip to content

fix(engine): self-heal failed in-review cards whose PR merged on the remote#1922

Open
flexi767 wants to merge 1 commit into
Runfusion:mainfrom
flexi767:fix/self-heal-merged-pr-stale-base
Open

fix(engine): self-heal failed in-review cards whose PR merged on the remote#1922
flexi767 wants to merge 1 commit into
Runfusion:mainfrom
flexi767:fix/self-heal-merged-pr-stale-base

Conversation

@flexi767

@flexi767 flexi767 commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Problem

A transient error at merge time can flag an in-review card failed even when its PR actually squash-merged on the remote (human merge, merge-train, etc.). recoverAlreadyMergedReviewTasks runs the already-merged evidence detector only against the local base ref. If this process never fetched the merge, the owned commit is absent locally → the detector returns nulllanded is null → the card never finalizes and holds its file-scope lease forever, wedging every other task that touches the same files.

Fix — fetch-then-prove

When a failed in-review candidate has a recorded PR (getPrimaryPrInfo) and the local base yields no owned commit:

  1. best-effort git fetch origin <base> (new refreshRemoteBaseRef helper), then
  2. re-run the same evidence detector against origin/<base>.

The detector's owned-commit proof and every foreign-ownership guard inside it remain the sole finalize gate, so this only un-wedges a genuinely-merged task — it never phantom-finalizes on unproven state.

Safety:

  • Gated on a recorded PR — no PR ⇒ nothing could have merged remotely ⇒ no fetch.
  • Fail-closed — a fetch error (offline / auth / no remote) is swallowed; if origin/<base> can't be resolved the card is left untouched.
  • No new dependency, no github client seam — direct git only.

Tests

Two real-git tests in self-healing-already-merged.real-git.test.ts, both verified to fail without the prod change:

  • fetch-then-prove positive: a PR squash-merges on a bare remote while the local base stays stale → recovery fetches, proves the owned SHA against origin/main, and finalizes the card to done (mergeConfirmed: true, worktree removed).
  • phantom-finalize guard: remote base advances with a commit owned by a different task → the fetch still runs, but the detector proves nothing → the card is left failed/in-review, never healed.

Full self-heal suite: 594 passed. Engine typecheck clean.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved recovery for review tasks that appear already merged when the local base branch is stale.
    • The app now refreshes the remote base branch before re-checking merge status, helping finalize tasks correctly and clean up completed worktrees.
    • Added coverage for cases where the remote base has moved forward with either the merged commit or unrelated changes.

…remote

A transient error at merge time can flag an in-review card `failed` even
when its PR actually squash-merged on the remote. `recoverAlreadyMergedReviewTasks`
only ran the already-merged evidence detector against the LOCAL base ref, so
when this process never fetched the merge, the owned commit was absent locally,
the detector returned null, and the card held its file-scope lease forever.

Fetch-then-prove: when a failed candidate has a recorded PR and the local base
yields no owned commit, best-effort `git fetch origin <base>` and re-run the
SAME evidence detector against `origin/<base>`. The owned-commit proof and every
foreign-ownership guard inside the detector remain the sole finalize gate, so
this only un-wedges a genuinely-merged task — it never phantom-finalizes on
unproven state. Gated on a recorded PR (no PR ⇒ nothing merged remotely ⇒ no
fetch); fail-closed on fetch error.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 5, 2026 18:41

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a refreshRemoteBaseRef helper in self-healing.ts that best-effort fetches and verifies origin/<baseBranch>, integrates it as a retry path in recoverAlreadyMergedReviewTasks when the initial already-merged commit check fails, and adds real-git tests covering both successful healing and non-healing scenarios.

Changes

Stale base ref refresh for already-merged detection

Layer / File(s) Summary
refreshRemoteBaseRef helper and integration
packages/engine/src/self-healing.ts
Adds refreshRemoteBaseRef(baseBranch) to fetch and verify origin/<baseBranch>, swallowing fetch errors and returning null on failure; wires it into recoverAlreadyMergedReviewTasks as a retry against the refreshed ref when the initial findAlreadyMergedTaskCommit call finds nothing and the task has PR info.
Real-git tests for fetch-then-prove healing
packages/engine/src/__tests__/self-healing-already-merged.real-git.test.ts
Adds setupRepoWithRemote and landOnRemoteMain helpers to simulate a stale local base branch, plus tests verifying a task is healed when the remote base gains the owned commit and remains unhealed when the remote base only advances with a foreign commit.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SelfHealingManager
  participant findAlreadyMergedTaskCommit
  participant refreshRemoteBaseRef
  participant Git

  SelfHealingManager->>findAlreadyMergedTaskCommit: check baseBranch for landed commit
  findAlreadyMergedTaskCommit-->>SelfHealingManager: landed = null
  SelfHealingManager->>refreshRemoteBaseRef: refreshRemoteBaseRef(baseBranch)
  refreshRemoteBaseRef->>Git: git fetch origin baseBranch
  Git-->>refreshRemoteBaseRef: fetch result (best-effort)
  refreshRemoteBaseRef->>Git: git rev-parse --verify origin/baseBranch
  Git-->>refreshRemoteBaseRef: verified ref or null
  refreshRemoteBaseRef-->>SelfHealingManager: origin/baseBranch or null
  SelfHealingManager->>findAlreadyMergedTaskCommit: retry with origin/baseBranch
  findAlreadyMergedTaskCommit-->>SelfHealingManager: landed commit or null
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main fix: self-healing failed in-review cards after their PR has merged remotely.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR teaches self-healing to recover failed review cards whose PR landed remotely. The main changes are:

  • A new helper fetches the remote base branch.
  • Already-merged recovery retries evidence detection against origin/<base>.
  • Real-git tests cover stale local bases and foreign remote commits.

Confidence Score: 4/5

The changed recovery path needs a fix before merging.

  • A failed fetch can still reuse an old remote-tracking ref.
  • That stale ref can feed the finalize path for failed in-review tasks.

packages/engine/src/self-healing.ts

Important Files Changed

Filename Overview
packages/engine/src/self-healing.ts Adds remote-base refresh and remote-ref retry before finalizing already-merged failed review tasks.
packages/engine/src/tests/self-healing-already-merged.real-git.test.ts Adds real-git coverage for stale local base recovery and foreign-commit rejection.

Reviews (1): Last reviewed commit: "fix(engine): self-heal failed in-review ..." | Re-trigger Greptile

Comment on lines +1987 to +1995
try {
await execAsync(`git fetch origin ${shellQuote(baseBranch)}`, {
cwd: this.options.rootDir,
timeout: 60_000,
});
} catch {
// Swallow: fall through to the existing remote-tracking ref if present.
}
try {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Stale Remote Ref Still Proves

When git fetch fails, this helper still returns an existing origin/<base> ref. That lets the new recovery path run the destructive finalize detector against a remote-tracking ref that was not refreshed, so an offline/auth failure can still move a failed in-review task to done, remove its worktree, and unblock dependents based on stale evidence.

Suggested change
try {
await execAsync(`git fetch origin ${shellQuote(baseBranch)}`, {
cwd: this.options.rootDir,
timeout: 60_000,
});
} catch {
// Swallow: fall through to the existing remote-tracking ref if present.
}
try {
try {
await execAsync(`git fetch origin ${shellQuote(baseBranch)}`, {
cwd: this.options.rootDir,
timeout: 60_000,
});
} catch {
return null;
}
try {

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
packages/engine/src/self-healing.ts (1)

8671-8701: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Consider throttling the new remote fetch to avoid repeated network calls on persistently-unmerged candidates.

refreshRemoteBaseRef runs unconditionally whenever a retry-exhausted failed in-review task has a PR and no local evidence. Since these candidates persist across sweeps until resolved, a genuinely-unmerged task will trigger a git fetch origin <base> (60s timeout) on every maintenance tick and every startup recovery run. Other reconcilers in this file (e.g. deadlockRecoveryCooldown) use a per-task cooldown map to avoid this kind of repeated work; the same pattern (or a per-sweep cache keyed by baseBranch to dedupe fetches across candidates sharing the same base) would reduce redundant I/O without weakening the fail-closed guarantee.

♻️ Example: per-sweep fetch dedup
   async recoverAlreadyMergedReviewTasks(): Promise<number> {
     try {
       const settings = await this.store.getSettings();
       if (settings.globalPause || settings.enginePaused) return 0;
+      const refreshedBaseRefsThisSweep = new Map<string, string | null>();
       const maxAutoMergeRetries = resolveMaxAutoMergeRetries(settings);
           if (!landed && getPrimaryPrInfo(task)) {
-            const refreshedBaseRef = await this.refreshRemoteBaseRef(baseBranch);
+            let refreshedBaseRef: string | null;
+            if (refreshedBaseRefsThisSweep.has(baseBranch)) {
+              refreshedBaseRef = refreshedBaseRefsThisSweep.get(baseBranch)!;
+            } else {
+              refreshedBaseRef = await this.refreshRemoteBaseRef(baseBranch);
+              refreshedBaseRefsThisSweep.set(baseBranch, refreshedBaseRef);
+            }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/engine/src/self-healing.ts` around lines 8671 - 8701, The new remote
fetch in the landed-task check can repeat on every sweep for persistently
unmerged candidates, causing unnecessary network work. Update the logic around
`refreshRemoteBaseRef` in `self-healing.ts` to throttle or dedupe fetches, using
the same cooldown-style approach as `deadlockRecoveryCooldown` or a per-sweep
cache keyed by `baseBranch`. Ensure `findAlreadyMergedTaskCommit` still runs
after a successful refresh and the fail-closed behavior remains unchanged.
packages/engine/src/__tests__/self-healing-already-merged.real-git.test.ts (1)

480-480: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

Static analysis flags dynamic execSync commands as a command-injection pattern.

Both calls build the command via string interpolation of ${JSON.stringify(...)}-quoted paths. Since remote/clone originate from mkdtempSync under test control (not external/untrusted input), the actual injection risk here is negligible — but JSON.stringify is not a hardened shell-escaping mechanism in general. Consider execFileSync with an argument array for consistency with the tool's recommendation, though this is optional given the existing file already uses this same pattern elsewhere for test-local paths.

Also applies to: 494-494

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/engine/src/__tests__/self-healing-already-merged.real-git.test.ts`
at line 480, Static analysis is flagging the `execSync` shell command
construction in the git setup calls, so update the
`self-healing-already-merged.real-git.test.ts` test helpers to avoid
interpolating paths into a shell string. Use `execFileSync` (or the same
non-shell pattern used elsewhere in the test file) for the `git init --bare -b
main` and related command near `remote`/`clone`, passing arguments separately
while keeping the temp-path behavior unchanged.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/engine/src/__tests__/self-healing-already-merged.real-git.test.ts`:
- Line 480: Static analysis is flagging the `execSync` shell command
construction in the git setup calls, so update the
`self-healing-already-merged.real-git.test.ts` test helpers to avoid
interpolating paths into a shell string. Use `execFileSync` (or the same
non-shell pattern used elsewhere in the test file) for the `git init --bare -b
main` and related command near `remote`/`clone`, passing arguments separately
while keeping the temp-path behavior unchanged.

In `@packages/engine/src/self-healing.ts`:
- Around line 8671-8701: The new remote fetch in the landed-task check can
repeat on every sweep for persistently unmerged candidates, causing unnecessary
network work. Update the logic around `refreshRemoteBaseRef` in
`self-healing.ts` to throttle or dedupe fetches, using the same cooldown-style
approach as `deadlockRecoveryCooldown` or a per-sweep cache keyed by
`baseBranch`. Ensure `findAlreadyMergedTaskCommit` still runs after a successful
refresh and the fail-closed behavior remains unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: fa1a9859-cbfc-4db3-a8cb-b99dc0fa8101

📥 Commits

Reviewing files that changed from the base of the PR and between 72b77bf and 15c2c83.

📒 Files selected for processing (2)
  • packages/engine/src/__tests__/self-healing-already-merged.real-git.test.ts
  • packages/engine/src/self-healing.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants