Skip to content

fix(engine): defer validator fail when the judged workspace predates the merged code#1929

Open
flexi767 wants to merge 1 commit into
Runfusion:mainfrom
flexi767:fix/validator-stale-workspace
Open

fix(engine): defer validator fail when the judged workspace predates the merged code#1929
flexi767 wants to merge 1 commit into
Runfusion:mainfrom
flexi767:fix/validator-stale-workspace

Conversation

@flexi767

@flexi767 flexi767 commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Problem

The mission validator runs its read-only judge session with cwd: this.rootDir — the engine's main working copy. When a task's merge landed on the remote (or in another worktree) and rootDir was never fetched/reset to that commit, the judge reads pre-merge files and returns a spurious fail.

#1917's premerge column guard does not catch this case: by the time validation runs the task column is already done, so execution falls through to handleValidationFail and mints a bogus Fix Feature for code that is actually correct and merged.

Fix

A symmetric second guard in the fail branch, placed after the #1917 premerge column check:

  • isValidationWorkspaceStale(feature) resolves the task's integration SHA and runs git merge-base --is-ancestor <sha> HEAD in rootDir.
  • Only affirmative staleness evidence defers. --is-ancestor exit 1 (the SHA is not an ancestor of HEAD → the workspace predates the merge) → defer the fail to inconclusive, so a later validation judges the merged code.
  • Every other outcome trusts the fail: exit 0 (ancestor → workspace is fresh), no integration SHA available, or a bad/unknown object (exit 128).

Fail-open doctrine, matching #1917: a guard may only ever defer a fail, never suppress one on missing or unreadable data.

Tests

Four real-git cases in mission-execution-loop.test.ts (skipped when git is unavailable):

  1. Judged checkout predates the merged commit → fail deferred to inconclusive, no Fix Feature minted, emits validation:inconclusive.
  2. Merged commit is an ancestor of HEAD (fresh workspace) → normal fail path, Fix Feature minted, emits validation:failed.
  3. Task carries no integration SHA → fail open (normal fail).
  4. Integration SHA is an unknown object (exit 128) → fail open (normal fail).

Verified with stash-red/restore-green discipline: with the production guard stashed, case (1) goes red while the three fail-open guardrails stay green — proving case (1) exercises the fix. Full file: 62 passed. tsc --noEmit: clean.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Validation failures are now deferred when the workspace appears to be out of date with merged changes, reducing incorrect failure reports.
    • Tasks with linked work continue to use the usual failure path when the current workspace is up to date.
    • Staleness checks now avoid masking real validation failures when no merge reference is available or when the reference can’t be verified.

…the merged code

The mission validator runs its read-only judge session with cwd:
this.rootDir — the engine's main working copy. When a task's merge landed
on the remote or in another worktree and rootDir was never fetched/reset to
it, the judge reads PRE-merge files and returns a spurious `fail`. Runfusion#1917's
premerge column guard doesn't catch this: the task column is already `done`,
so it falls through to handleValidationFail and mints a bogus Fix Feature.

Add a symmetric second guard in the fail branch, after the premerge column
check: isValidationWorkspaceStale resolves the task's integration SHA and
runs `git merge-base --is-ancestor <sha> HEAD` in rootDir. Only affirmative
staleness evidence (exit 1 = NOT an ancestor) defers the fail to
inconclusive; exit 0 (ancestor/fresh), a missing SHA, or a bad object
(exit 128) all trust the fail. Fail-open: a guard may DEFER a fail, never
SUPPRESS one on missing or unreadable data.

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

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 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a git-ancestry-based staleness check to the validation fail path in mission-execution-loop.ts, deferring validation to an inconclusive outcome when the workspace HEAD predates a task's merged integration commit. Adds corresponding tests using real git fixtures.

Changes

Stale-workspace premerge guard

Layer / File(s) Summary
Git exec plumbing and staleness check
packages/engine/src/mission-execution-loop.ts
Adds execAsync and a shell-quoting helper for git invocation, extends the fail-verdict branch with a second staleness guard, and introduces isValidationWorkspaceStale using git merge-base --is-ancestor to decide whether to defer to the inconclusive path.
Git fixture helpers and stale-workspace guard tests
packages/engine/src/__tests__/mission-execution-loop.test.ts
Adds git-availability detection, a git() exec wrapper, initGitRepo()/makeGitRepo() fixture helpers with per-test cleanup, and a stale-workspace guard test suite covering done/inconclusive, ancestor/fail, missing integrationSha, and unknown SHA cases.

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

Sequence Diagram(s)

sequenceDiagram
  participant MissionLoop as mission-execution-loop
  participant Git as git CLI
  participant Handler as Validation Handler

  MissionLoop->>MissionLoop: processTaskOutcome receives fail verdict
  MissionLoop->>MissionLoop: getPremergeTaskColumn check
  MissionLoop->>MissionLoop: isValidationWorkspaceStale(rootDir, integrationSha)
  MissionLoop->>Git: git merge-base --is-ancestor integrationSha HEAD
  Git-->>MissionLoop: exit code
  alt exit code 1 (workspace stale)
    MissionLoop->>Handler: defer to inconclusive path
  else ancestor confirmed or unresolvable
    MissionLoop->>Handler: proceed with normal fail path
  end
Loading

Possibly related PRs

  • Runfusion/Fusion#1910: Both PRs modify the processTaskOutcome validation flow in mission-execution-loop.ts to conditionally defer validator outcomes based on mission/task state.
🚥 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 describes the main behavior change: deferring validator failures for stale workspaces.
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 6, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR defers validator failures when the judge ran against an older checkout. The main changes are:

  • A new git ancestry check before the normal fail path.
  • Inconclusive handling when rootDir predates the task's merged code.
  • Real-git tests for stale, fresh, missing-SHA, and bad-SHA cases.

Confidence Score: 4/5

The stale-workspace path can still reach the normal fail handler when task data uses the production SHA field.

  • The new ancestry check has the right fail-open shape.
  • The SHA resolver can miss real task data and skip the new deferral.
  • The test covers the intended behavior through a synthetic field, so it can pass while production tasks still bypass the guard.

packages/engine/src/mission-execution-loop.ts

Important Files Changed

Filename Overview
packages/engine/src/mission-execution-loop.ts Adds the stale-workspace fail deferral, but the SHA resolver can miss the production task field and bypass the guard.
packages/engine/src/tests/mission-execution-loop.test.ts Adds real-git coverage for the new guard branches, though the stale case uses a synthetic integrationSha field.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Validator returns fail] --> B{Linked task still premerge?}
  B -- yes --> C[Mark validation inconclusive]
  B -- no, task is done --> D{Root checkout contains merge SHA?}
  D -- no --> C
  D -- yes or unknown --> E[Normal fail path]
  E --> F[Create generated Fix Feature]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
  A[Validator returns fail] --> B{Linked task still premerge?}
  B -- yes --> C[Mark validation inconclusive]
  B -- no, task is done --> D{Root checkout contains merge SHA?}
  D -- no --> C
  D -- yes or unknown --> E[Normal fail path]
  E --> F[Create generated Fix Feature]
Loading

Reviews (1): Last reviewed commit: "fix(engine): defer validator fail when t..." | Re-trigger Greptile

Comment on lines +581 to +582
const integrationSha = await this.resolveIntegrationSha(feature);
if (!integrationSha) return false; // no evidence → trust the fail

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 Production SHA Field Is Missed

When a done task only carries the modeled task SHA field, resolveIntegrationSha returns no SHA because it looks for duck-typed integrationSha or baseCommit fields. That makes this new guard return false, so the stale-workspace fail still enters handleValidationFail and mints the bogus Fix Feature this change is meant to defer.

@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.

Actionable comments posted: 1

🤖 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.

Inline comments:
In `@packages/engine/src/__tests__/mission-execution-loop.test.ts`:
- Around line 486-496: The test fixture setup in initGitRepo uses git init -b
main without checking the installed Git version, so it can fail on older Git
releases. Update the repo initialization in initGitRepo to be version-agnostic
by either gating the main-branch initialization on a Git version check or
initializing the repo normally and renaming the branch afterward, while keeping
the rest of the fixture setup unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b9f12b0b-95a4-478b-bf7f-2d8b3b23e292

📥 Commits

Reviewing files that changed from the base of the PR and between 6c9989c and 58683f9.

📒 Files selected for processing (2)
  • packages/engine/src/__tests__/mission-execution-loop.test.ts
  • packages/engine/src/mission-execution-loop.ts

Comment on lines +486 to +496
/** A throwaway git repo with one initial commit; HEAD on `main`. */
function initGitRepo(): string {
const repo = mkdtempSync(join(tmpdir(), "fn-stale-ws-"));
git(repo, "git init -b main");
git(repo, 'git config user.email "test@example.com"');
git(repo, 'git config user.name "Test User"');
git(repo, "git config commit.gpgsign false");
writeFileSync(join(repo, "foo.ts"), "line1\n");
git(repo, "git add foo.ts && git commit -m init");
return repo;
}

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

git init --initial-branch -b flag introduced version

💡 Result:

The --initial-branch and -b flags for the git init command were introduced in Git version 2.28.0 [1][2][3]. These options allow users to specify the name of the initial branch in a newly created repository [4][5]. If these flags are not used, Git defaults to the name defined by the init.defaultBranch configuration variable, or to master if that variable is not set [5][6].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the test file around the reported lines.
sed -n '440,530p' packages/engine/src/__tests__/mission-execution-loop.test.ts

# Find the helper used to gate git-dependent tests.
rg -n "hasGit|git init -b main|initial-branch|branch -M main" packages/engine/src/__tests__ packages/engine/src -S

# Locate the hasGit implementation.
fd -a "hasGit" packages/engine/src packages/engine/src/__tests__

Repository: Runfusion/Fusion

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for any explicit Git version checks in the test suite or helpers.
rg -n "git version|--version|2\.28|initialBranch|init\.defaultBranch" packages/engine/src -S

# If there's a shared test utility, inspect it for Git capability checks.
rg -n "function hasGit|const hasGit|export .*hasGit" . -S

Repository: Runfusion/Fusion

Length of output: 13456


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find every call site of initGitRepo in the file.
rg -n "initGitRepo\(" packages/engine/src/__tests__/mission-execution-loop.test.ts -n -S

# Read the surrounding test blocks for those call sites.
python3 - <<'PY'
from pathlib import Path
p = Path("packages/engine/src/__tests__/mission-execution-loop.test.ts")
lines = p.read_text().splitlines()
targets = []
for i,l in enumerate(lines, start=1):
    if "initGitRepo(" in l:
        targets.append(i)
for t in targets:
    start = max(1, t-25)
    end = min(len(lines), t+35)
    print(f"\n--- lines {start}-{end} around {t} ---")
    for n in range(start, end+1):
        print(f"{n:4d}: {lines[n-1]}")
PY

Repository: Runfusion/Fusion

Length of output: 5149


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the skip gating around the real-git tests that use initGitRepo.
sed -n '2030,2185p' packages/engine/src/__tests__/mission-execution-loop.test.ts

Repository: Runfusion/Fusion

Length of output: 6239


Avoid git init -b main without a Git-version gate
hasGit only checks that git is installed, so these fixtures still run on Git < 2.28 and git init -b main will fail. Gate on the git version or initialize normally and rename the branch afterward.

♻️ Version-agnostic branch setup
-  git(repo, "git init -b main");
+  git(repo, "git init");
   git(repo, 'git config user.email "test@example.com"');
   git(repo, 'git config user.name "Test User"');
   git(repo, "git config commit.gpgsign false");
   writeFileSync(join(repo, "foo.ts"), "line1\n");
   git(repo, "git add foo.ts && git commit -m init");
+  git(repo, "git branch -M main");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/** A throwaway git repo with one initial commit; HEAD on `main`. */
function initGitRepo(): string {
const repo = mkdtempSync(join(tmpdir(), "fn-stale-ws-"));
git(repo, "git init -b main");
git(repo, 'git config user.email "test@example.com"');
git(repo, 'git config user.name "Test User"');
git(repo, "git config commit.gpgsign false");
writeFileSync(join(repo, "foo.ts"), "line1\n");
git(repo, "git add foo.ts && git commit -m init");
return repo;
}
/** A throwaway git repo with one initial commit; HEAD on `main`. */
function initGitRepo(): string {
const repo = mkdtempSync(join(tmpdir(), "fn-stale-ws-"));
git(repo, "git init");
git(repo, 'git config user.email "test@example.com"');
git(repo, 'git config user.name "Test User"');
git(repo, "git config commit.gpgsign false");
writeFileSync(join(repo, "foo.ts"), "line1\n");
git(repo, "git add foo.ts && git commit -m init");
git(repo, "git branch -M main");
return repo;
}
🤖 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__/mission-execution-loop.test.ts` around lines
486 - 496, The test fixture setup in initGitRepo uses git init -b main without
checking the installed Git version, so it can fail on older Git releases. Update
the repo initialization in initGitRepo to be version-agnostic by either gating
the main-branch initialization on a Git version check or initializing the repo
normally and renaming the branch afterward, while keeping the rest of the
fixture setup unchanged.

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