Skip to content

fix(verify): translate spawn OSError to GitSpawnError in _run_git (#343) - #344

Merged
pbean merged 4 commits into
mainfrom
fix/343-spawn-oserror-chokepoint
Jul 28, 2026
Merged

fix(verify): translate spawn OSError to GitSpawnError in _run_git (#343)#344
pbean merged 4 commits into
mainfrom
fix/343-spawn-oserror-chokepoint

Conversation

@pbean

@pbean pbean commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Summary

Closes #343 with the chokepoint shape: _run_git now translates a spawn-level OSError (EMFILE/ENOMEM, git gone from PATH) into GitSpawnError(GitError) the same way it already translates TimeoutExpired, so all 19 OSError-blind except GitError guards hold as written with no per-site edits. GitSpawnError follows the PrunePreserveError precedent; the errno stays reachable on __cause__.

Site-level dispositions that change

  • Commit window (engine._finalize_commit_phase) — a spawn fault now takes the GitError arm: restore the deferred-close ledger + escalate, same as a git timeout there. The BaseException arm's comment named the escaping spawn OSError as a case it restored against; its purpose was ledger protection, and translation completes what it compensated for. Non-spawn OSErrors still restore + re-raise + crash (existing test retained, premise reworded).
  • Unit worktree open (worktree_flow.run_isolated) — a spawn fault is machine-wide, not per-unit: it now pauses the run instead of deferring every unit one notification at a time and ending the run "finished" over a broken environment (the Dev session lost to an API/transport failure is charged as a story timeout, exhausting the attempt budget #194 env-fault doctrine). Per-unit GitErrors still defer.
  • Merge-target reconciliation (worktree_flow.merge_local) — widened to (GitError, OSError): clean_incoming_collisions mutates the checkout directly (unlink/rmdir), a non-spawn FS class the chokepoint cannot translate, and crashing there strands a DONE unit mid-merge past the keep-branch escalation the guard exists for. Precedent: the restore_patch widening in fix(recovery): refuse the rollback reset when the snapshot fails (#340) #341.

The other 15 blind sites need no edits — verified each degrades/escalates/reports correctly once the exception arrives typed. No existing (GitError, OSError) guard is narrowed: snapshot_worktree's TemporaryDirectory and the patch-file reads remain genuine non-spawn OSError sources (docstrings corrected accordingly, including snapshot_worktree's false "before any git child is spawned" claim).

Testing

  • Chokepoint unit test (parametrized ENOENT/EMFILE via exception factories): subprocess.run OSError → GitSpawnError, isinstance GitError, __cause__ chained.
  • One caller per behavioural class with the OSError injected at subprocess.run per the issue's acceptance: observation-degrades (reconcile_orphan_worktrees[]) and escalate (verify_dev → CRITICAL, not retryable).
  • Engine commit-window test (escalate + byte-identical ledger restore, two-layer chain), worktree-open pause test, merge-guard OSError test.
  • All four ablations performed: each new arm/guard deleted, its test(s) observed failing with the exact predicted failure mode, arm restored. Ablation targets recorded in test docstrings.

uv run pytest -q -n auto: 3399 passed. trunk check: clean. uvx pyright@1.1.411: 0 errors.

Summary by CodeRabbit

  • Bug Fixes
    • Improved handling of Git process-start failures so they no longer crash runs unexpectedly.
    • Runs now pause and escalate when worktree setup or merge cleanup encounters blocking system errors.
    • Preserved worktree branches for manual resolution when merge reconciliation fails.
    • Improved recovery and orphan-worktree cleanup behavior during filesystem or Git failures.
  • Tests
    • Added coverage for Git spawn failures, recovery paths, cleanup, and worktree escalation behavior.

A spawn-level OSError (EMFILE/ENOMEM/ENOENT) out of subprocess.run
escaped _run_git untyped, bypassing all 19 OSError-blind 'except
GitError' guards and crashing the run under exactly the resource
pressure the recovery paths owning those guards exist for (#341 found
two unguarded frames this way). Translating at the chokepoint makes the
default correct: every guard now holds as written, and GitSpawnError
(a GitError) lets the rare caller tell an environment fault from git
refusing, with the errno on __cause__.

Site-level dispositions that change: the commit window escalates on a
spawn fault (same as a git timeout there; non-spawn OSErrors still
crash), a spawn fault opening a unit worktree pauses the run instead of
marching the queue into DEFERRED, and merge-target reconciliation
widens to (GitError, OSError) for clean_incoming_collisions' non-spawn
FS ops — the one hole among the 19 the chokepoint cannot close.

All four new guards ablation-verified (arm deleted, test fails, arm
restored).

Closes #343
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@pbean, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 46 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ace63fb5-ccc4-46bd-9a49-7bdf4f3435f1

📥 Commits

Reviewing files that changed from the base of the PR and between 82cae91 and 5043a25.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • src/bmad_loop/recovery_flow.py
  • src/bmad_loop/worktree_flow.py
  • tests/test_engine_worktree.py
  • tests/test_verify.py

Walkthrough

Changes

Git spawn fault recovery

Layer / File(s) Summary
Git spawn error taxonomy
src/bmad_loop/verify.py, tests/test_verify.py, CHANGELOG.md
Adds GitSpawnError, translates subprocess spawn OSError failures, preserves the cause, and verifies escalation behavior.
Worktree failure routing
src/bmad_loop/worktree_flow.py, tests/test_worktree_flow.py, tests/test_engine_worktree.py, tests/test_cleanup.py, tests/test_verify_worktree.py
Pauses on worktree-open spawn failures, escalates merge-cleanup OSErrors while retaining branches, and makes orphan cleanup a no-op on spawn faults.
Commit and recovery safeguards
src/bmad_loop/engine.py, src/bmad_loop/recovery_flow.py, tests/test_engine.py, tests/test_recovery_flow.py
Documents OSError paths and verifies deferred-ledger restoration and escalation during commit spawn failure.

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

Sequence Diagram(s)

sequenceDiagram
  participant Engine
  participant WorktreeFlow
  participant verify._run_git
  participant subprocess.run
  Engine->>WorktreeFlow: open or merge unit worktree
  WorktreeFlow->>verify._run_git: execute git operation
  verify._run_git->>subprocess.run: spawn git
  subprocess.run-->>verify._run_git: OSError
  verify._run_git-->>WorktreeFlow: GitSpawnError
  WorktreeFlow-->>Engine: pause or escalate
Loading

Possibly related PRs

Suggested reviewers: dracic

Poem

I’m a rabbit guarding git’s little gate,
Spawn faults now pause instead of sealing fate.
Branches stay safe, ledgers restore true,
Errors wear names their handlers can view.
Hop, hop—the crash path is gone!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Clear and specific; it describes the core change of translating spawn OSError into GitSpawnError in _run_git.
Linked Issues check ✅ Passed Implements the requested chokepoint translation and updates the affected callers/tests to handle GitSpawnError and OSError as required.
Out of Scope Changes check ✅ Passed Changes stay focused on the spawn-error fix, its downstream handling, and tests; no unrelated features or broad refactors appear.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/343-spawn-oserror-chokepoint

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.

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

Actionable comments posted: 2

🤖 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 `@src/bmad_loop/recovery_flow.py`:
- Around line 408-411: Update the adjacent preservation comment near _run_git to
state that spawn failures are translated into typed GitSpawnError, while
filesystem failures such as EMFILE or ENOMEM may still propagate as plain
OSError. Preserve the explanation that the OSError guard remains necessary for
snapshot_worktree and related rollback paths.

In `@tests/test_verify.py`:
- Around line 125-131: Extend the make_exc parameter matrix in the test
parametrization to include OSError(12, "Cannot allocate memory") and add the
corresponding "enomem" case ID, preserving the existing ENOENT and EMFILE
entries.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 022902d8-67ca-46d4-90a3-6439ec4ac206

📥 Commits

Reviewing files that changed from the base of the PR and between af30dc1 and 82cae91.

📒 Files selected for processing (12)
  • CHANGELOG.md
  • src/bmad_loop/engine.py
  • src/bmad_loop/recovery_flow.py
  • src/bmad_loop/verify.py
  • src/bmad_loop/worktree_flow.py
  • tests/test_cleanup.py
  • tests/test_engine.py
  • tests/test_engine_worktree.py
  • tests/test_recovery_flow.py
  • tests/test_verify.py
  • tests/test_verify_worktree.py
  • tests/test_worktree_flow.py

Comment thread src/bmad_loop/recovery_flow.py
Comment thread tests/test_verify.py Outdated
@greptile-apps

greptile-apps Bot commented Jul 28, 2026

Copy link
Copy Markdown

Greptile Summary

This PR introduces GitSpawnError(GitError) as a typed translation of spawn-level OSError (EMFILE, ENOMEM, ENOENT on git) at _run_git, the sole git spawn chokepoint. The translation ensures all 19 existing except GitError guards that were previously blind to raw OSError from subprocess.run now handle spawn faults correctly without per-site edits.

  • Chokepoint (verify._run_git): a new except OSError arm raises GitSpawnError (chaining the original via __cause__), mirroring the existing TimeoutExpired translation.
  • Caller policy changes: spawn fault in run_isolated now pauses the run instead of deferring one unit at a time; merge_local widens to (GitError, OSError) and branches the error message to avoid false "clean uncommitted files" guidance on environment faults; the commit-window comment in engine.py is corrected to reflect that spawn faults now arrive typed.
  • Tests: parametrized chokepoint unit test (ENOENT/EMFILE/ENOMEM), escalate/degrade acceptance tests injected at subprocess.run, ablation targets recorded in all new test docstrings.

Confidence Score: 5/5

Safe to merge — the change is a well-scoped translation at the sole git spawn point, all downstream guards hold as-is, and four ablation passes verified each new arm.

The chokepoint translation is minimal and mirrors the existing TimeoutExpired handler exactly. Exception ordering in run_isolated is correct (subclass caught before base class). The merge_local isinstance branch correctly separates environment faults from git-refusing faults. All 3399 tests pass, Pyright reports 0 errors, and every new code path has a corresponding ablation-verified test. No pre-existing guard is narrowed.

Files Needing Attention: No files require special attention.

Important Files Changed

Filename Overview
src/bmad_loop/verify.py Adds GitSpawnError(GitError) and translates spawn-level OSError at _run_git; mirrors the existing TimeoutExpired handler; cmd[3] access is safe for all call sites.
src/bmad_loop/worktree_flow.py Adds GitSpawnError arm before GitError arm in run_isolated (correct subclass ordering); widens merge_local guard to (GitError, OSError) and branches error message on isinstance(e, (GitSpawnError, OSError)); logic is correct.
src/bmad_loop/engine.py Comment-only update to _finalize_commit_phase's BaseException arm; correctly documents that spawn faults now arrive as GitSpawnError and take the GitError arm above.
src/bmad_loop/recovery_flow.py Docstring-only corrections updating comments on (GitError, OSError) guards to reflect typed spawn faults while preserving the TemporaryDirectory ENOSPC net.
tests/test_verify.py Parametrized unit test (ENOENT/EMFILE/ENOMEM) verifying subprocess.run OSError to GitSpawnError with isinstance(GitError) and cause chain; acceptance test for verify_dev escalation path.
tests/test_worktree_flow.py New test verifies spawn fault in run_isolated pauses rather than defers; ablation target documented; phase and journal-entry assertions confirm the per-unit GitError path is unchanged.
tests/test_engine_worktree.py Parametrized test (fs-oserror / git-spawn) covering merge_local's widened guard; verifies keep-branch escalation, correct error message branch, and no crash on DONE unit.
tests/test_engine.py New commit-window spawn-fault test confirms ledger restored and escalated (not crashed); existing non-spawn OSError crash test updated in comment to reflect its now-narrower premise.
tests/test_cleanup.py New test confirms reconcile_orphan_worktrees degrades to [] on spawn fault injected at subprocess.run; verifies the full translation chain end-to-end.
tests/test_recovery_flow.py Comment-only updates; wording corrected to reflect spawn faults now arrive typed as GitSpawnError, but the OSError net for TemporaryDirectory faults remains load-bearing.
tests/test_verify_worktree.py Comment-only update; rationale corrected to note GitSpawnError is now typed but the plain-OSError net in worktree_prune stays as belt-and-suspenders.
CHANGELOG.md Accurate changelog entry documenting the spawn-fault translation, run-pause behavior change for worktree open, and corrected merge-reconciliation error message.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[subprocess.run] -->|success| B[CompletedProcess]
    A -->|TimeoutExpired| C[raise GitError]
    A -->|OSError EMFILE/ENOMEM/ENOENT| D[raise GitSpawnError - GitError - __cause__ = original OSError]
    D --> E{Caller site}
    C --> E
    E -->|run_isolated| F{GitSpawnError?}
    F -->|Yes - machine-wide| G[_pause RunPaused not DEFERRED]
    F -->|No - plain GitError| H[defer unit]
    E -->|merge_local| I{GitSpawnError or OSError?}
    I -->|Yes - env fault| J[could not reconcile - keep_branch_and_escalate]
    I -->|No - plain GitError| K[uncommitted changes - keep_branch_and_escalate]
    E -->|_finalize_commit_phase| L{GitError?}
    L -->|Yes incl GitSpawnError| M[restore ledger _escalate]
    L -->|No BaseException| N[restore ledger re-raise crash]
    E -->|15 observation guards| O[degrade no-op]
Loading

Reviews (2): Last reviewed commit: "fix(worktree): neutral merge-blocked mes..." | Re-trigger Greptile

@pbean
pbean merged commit e131bfa into main Jul 28, 2026
11 checks passed
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.

_run_git translates only TimeoutExpired, so a spawn-level OSError bypasses every except GitError guard

1 participant