Skip to content

fix: preserve seed file modes + pause on verify env faults (rc 126/127)#130

Merged
pbean merged 3 commits into
mainfrom
fix/seed-modes-env-fault-verify
Jul 12, 2026
Merged

fix: preserve seed file modes + pause on verify env faults (rc 126/127)#130
pbean merged 3 commits into
mainfrom
fix/seed-modes-env-fault-verify

Conversation

@pbean

@pbean pbean commented Jul 12, 2026

Copy link
Copy Markdown
Collaborator

Fixes #126.

Problem

Both claims in #126 validated against HEAD:

  1. _copy_traversable copied every seeded file via dst.write_bytes(src.read_bytes()), discarding file modes. Every [scm] worktree_seed / plugin seed_globs tree (e.g. vendor/, node_modules/) arrived in the worktree with non-executable binaries, and the first verify command died rc=127.
  2. The engine collapsed verify exit codes to a boolean, so rc 126/127 (the shell's found-but-not-executable / not-found environment faults) was charged against max_dev_attempts exactly like broken story code — spawning no-op repair sessions and, on the reference run, falsely deferring a story.

Fix

Commit 1 — root cause. Real-filesystem sources now copy via shutil.copy2 (mode-preserving, matching every other copy site in the repo); the byte-only walk remains solely for zip-imported Traversables, which expose no stat. Seeded vendor/bin/* keep +x.

Commit 2 — misclassification. verify_commands_outcome classifies rc 126/127 as a CRITICAL escalate carrying a new VerifyOutcome.env_fault flag, so the run pauses at the first environment fault instead of burning story budgets. This deliberately diverges from the issue's "retry without decrementing" proposal: these rcs are deterministic for a given tree and shared by every story's verify commands, so uncharged retries either loop forever or replay the same wall on all N stories. The uncharged semantics land via existing machinery — rearm_escalation resets task.attempt = 0, so the story re-drives with a fresh budget once the environment is fixed. The review loop, skip-review path, and _fix_phase gain matching escalate branches (no fix sessions dispatched against an unfixable environment; also closes a latent hole where a shared-gate GitError escalate burned review cycles). Journal dev-decision / review-verify-failed / fix-decision events carry env_fault.

Out of scope, documented in-code: timeouts (rc -1) stay charged (a hang is plausibly the story's own tests); Windows cmd signals these faults as 9009/1 and keeps today's behavior.

The issue's shipped workarounds (chmod +x vendor/bin/* guard, @php vendor/bin/...) become unnecessary.

Tests

  • Seed exec-bit preservation (seed_files + seed_globs), plus a zipfile.Path fallback guard; the exec-bit tests fail on the old copy code.
  • rc classification unit tests (126/127 escalate, rc 1 stays fixable retry, timeout stays charged, real missing binary yields 127).
  • Engine scenarios: dev-gate rc 127 pauses with exactly one dev session and attempt == 1; review-gate rc 126 escalates with no fix session; skip-review rc 126 escalates instead of deferring; mid-fix-loop rc 126 stops the loop with budget left. All verified to fail without the fix.
  • Full suite: 2223 passed. trunk check clean.

Summary by CodeRabbit

  • Bug Fixes
    • Treat environment-related verification failures (including missing/non-executable commands) as non-retryable and critical.
    • Runs now pause/escalate immediately instead of continuing through fix/repair loops or deferring to later review steps.
    • Environment-fault details are recorded consistently across development, review, and repair stages.
    • Seed provisioning now preserves executable permissions for seeded files and globbed content, including content copied from packaged sources.
  • Tests
    • Added coverage for environment-fault escalation behavior and verify exit-code classification/precedence.

pbean added 2 commits July 11, 2026 23:23
_copy_traversable copied every file via write_bytes, which drops the
source mode: seeded gitignored trees (vendor/bin/*, node_modules/.bin/*)
arrived non-executable and the first verify command died rc=127.

Real-filesystem sources now go through shutil.copy2 (mode-preserving,
matching the repo's other copy sites); the byte-only walk remains as the
fallback for zip-imported Traversables, which expose no stat. Side
effects: copy2 also carries mtimes (nothing reads seeded mtimes), and
install_into/_copy_skills inherit the same mode preservation. Symlinked
seeds are still materialized as target content, now with the target's
mode.
… run (#126)

The shell's 126 (found, not executable) and 127 (not found) are the run
environment's failures, not the story's: deterministic for a given tree
and shared by every story's verify commands. They were collapsed into
the generic fixable retry, burning max_dev_attempts on repair sessions
that could not fix anything and eventually deferring the story.

verify_commands_outcome now escalates them (new VerifyOutcome.env_fault
flag): the dev leg already pauses on CRITICAL regardless of budget, and
runs.rearm_escalation resets attempt=0, so the story re-drives with a
fresh budget once the human fixes the environment. The review loop,
skip-review path, and _fix_phase gain matching escalate branches so an
env fault surfacing there pauses instead of burning review cycles /
dispatching doomed fix sessions (this also stops a shared-gate GitError
escalate from silently burning review cycles). Journal events carry the
env_fault flag.

Timeouts (rc -1) keep the charged retry: a hang is plausibly the
story's own tests. Windows cmd signals these faults as 9009/1, so it
keeps today's behavior — a documented no-op, not a regression.
@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

No new commits to review since the last review.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3393ee56-947a-4627-b5cc-5553f7dc212c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

The changes preserve executable modes during worktree seeding, classify verification return codes 126 and 127 as environment faults, and pause development, review, commit, and repair flows without continuing additional repair attempts.

Changes

Environment fault handling

Layer / File(s) Summary
Preserve seeded file permissions
src/bmad_loop/install.py, tests/test_install.py
Filesystem paths are copied with preserved modes, while non-filesystem traversables remain content-only copies; tests cover executable seed files, glob-seeded files, and zip sources.
Classify verification environment failures
src/bmad_loop/verify.py, tests/test_verify.py, tests/test_escalation.py
VerifyOutcome records env_fault, and verification return codes 126 and 127 produce non-retryable critical escalations while ordinary failures and timeouts remain retryable.
Pause engine flows on environment faults
src/bmad_loop/engine.py, tests/test_engine.py
Development, review, disabled-review commit, and fix phases journal environment faults and escalate without continuing repair attempts or consuming remaining development budget.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant VerifyCommand
  participant verify_commands_outcome
  participant VerifyOutcome
  participant EnginePhase
  VerifyCommand->>verify_commands_outcome: return rc=126 or rc=127
  verify_commands_outcome->>VerifyOutcome: create non-retryable env_fault outcome
  VerifyOutcome->>EnginePhase: deliver verification failure
  EnginePhase->>EnginePhase: journal env_fault and pause
Loading

Poem

I’m a rabbit with a patch in my paw,
Executable bits now hop by the law.
Bad shells raise a flag, repairs take a rest,
The loop pauses calmly when faults are confessed.
Squeak, test, and burrow—everything’s blessed!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.06% 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 The title clearly summarizes the two main changes: preserving seed file modes and pausing on verify env faults.
Linked Issues check ✅ Passed The code matches issue #126 by preserving seeded file modes and treating rc 126/127 as environment faults that pause the run.
Out of Scope Changes check ✅ Passed The added journaling parity and test coverage stay aligned with the issue’s verify-fault and seeding behavior changes.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/seed-modes-env-fault-verify

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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/bmad_loop/verify.py (1)

1431-1451: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not let an earlier ordinary failure mask a later environment fault.

run_verify_commands collects every command result, but this loop returns on the first non-zero result. With commands such as ("exit 1", "missing-command"), the rc=1 result is classified as fixable, so the engine can start a repair session even though the same verification run already observed rc=127. Scan all results for ENV_FAULT_RCS before selecting an ordinary retryable failure, and add a mixed-command regression test.

Proposed fix
-    for result in run_verify_commands(policy, cwd):
+    results = run_verify_commands(policy, cwd)
+    for result in results:
         if result.returncode in ENV_FAULT_RCS:
             return VerifyOutcome.escalate(
                 ...
                 env_fault=True,
             )
+    for result in results:
         if result.returncode != 0:
             return VerifyOutcome.retry(
                 ...
             )
🤖 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 `@src/bmad_loop/verify.py` around lines 1431 - 1451, Update
verify_commands_outcome to scan all results from run_verify_commands for
ENV_FAULT_RCS before returning any ordinary retry outcome, ensuring a later
environment fault takes precedence over earlier failures. Preserve the existing
escalation details and ordinary retry behavior when no environment fault exists,
and add a regression test covering an ordinary failure followed by a
missing-command environment fault.
src/bmad_loop/engine.py (1)

1844-1853: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Record env_fault on the skip-review verification failure path.

When review is disabled, this branch escalates directly without emitting a verification-failure journal event. Consequently, journal consumers cannot distinguish an environment fault from another non-retryable failure, while the review-enabled and fix paths record env_fault.

Add a failure event here, or extend the existing skip-review event, with env_fault=outcome.env_fault; add the corresponding assertion to the skip-review engine test.

Proposed fix
         if not outcome.ok:
+            self.journal.append(
+                "review-verify-failed",
+                story_key=task.story_key,
+                reason=outcome.reason,
+                env_fault=outcome.env_fault,
+            )
             if not outcome.retryable:
🤖 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 `@src/bmad_loop/engine.py` around lines 1844 - 1853, Update the skip-review
failure handling in the engine flow around _verify_review and _defer to emit or
extend the verification-failure journal event with env_fault=outcome.env_fault
before escalation or deferral. Add the corresponding assertion to the
skip-review engine test, preserving existing behavior for retryable and
non-retryable outcomes.
🤖 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.

Outside diff comments:
In `@src/bmad_loop/engine.py`:
- Around line 1844-1853: Update the skip-review failure handling in the engine
flow around _verify_review and _defer to emit or extend the verification-failure
journal event with env_fault=outcome.env_fault before escalation or deferral.
Add the corresponding assertion to the skip-review engine test, preserving
existing behavior for retryable and non-retryable outcomes.

In `@src/bmad_loop/verify.py`:
- Around line 1431-1451: Update verify_commands_outcome to scan all results from
run_verify_commands for ENV_FAULT_RCS before returning any ordinary retry
outcome, ensuring a later environment fault takes precedence over earlier
failures. Preserve the existing escalation details and ordinary retry behavior
when no environment fault exists, and add a regression test covering an ordinary
failure followed by a missing-command environment fault.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 64528143-28fc-47ea-9459-d64557f8f476

📥 Commits

Reviewing files that changed from the base of the PR and between ff88fb4 and 2278bd5.

📒 Files selected for processing (7)
  • src/bmad_loop/engine.py
  • src/bmad_loop/install.py
  • src/bmad_loop/verify.py
  • tests/test_engine.py
  • tests/test_escalation.py
  • tests/test_install.py
  • tests/test_verify.py

…ip-review env_fault journal (#126)

CodeRabbit review round on PR #130, both findings confirmed:
- verify_commands_outcome returned on the first non-zero rc, so an
  ordinary failure (rc=1) earlier in the command list masked an env
  fault (rc=126/127) later in the same run — charging a repair session
  in a broken environment, the exact #126 failure mode. Scan the
  collected results for ENV_FAULT_RCS first.
- _skip_review_and_commit escalated/deferred without a structured
  env_fault journal field; emit the same review-verify-failed event as
  the review-enabled loop.
@pbean

pbean commented Jul 12, 2026

Copy link
Copy Markdown
Collaborator Author

Validated both review findings against the code — both confirmed, fixed in bc391b4:

  1. verify.py masking (Major) — CONFIRMED. run_verify_commands collects results from all commands (no fail-fast), but verify_commands_outcome returned on the first non-zero rc, so an earlier ordinary failure (rc=1) masked a later env fault (rc=126/127) already sitting in the results list — charging a repair session in a broken environment, the exact Worktree seeding drops file modes; verify rc=126/127 is charged as a story failure #126 failure mode this PR addresses. Fixed with a two-pass scan (env faults first), plus the mixed-command regression test test_env_fault_takes_precedence_over_earlier_ordinary_failure.

  2. engine.py skip-review journal parity (Minor) — CONFIRMED. The dev (dev-decision), review (review-verify-failed), and fix (fix-decision) failure paths all journal a structured env_fault field; _skip_review_and_commit did not. It now emits the same review-verify-failed event as the review-enabled loop (one kind for consumers to key on, e.g. the TUI's red highlight), asserted in test_skip_review_env_fault_escalates_not_defers.

Full suite 2224 green + trunk clean.

@pbean

pbean commented Jul 12, 2026

Copy link
Copy Markdown
Collaborator Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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.

Worktree seeding drops file modes; verify rc=126/127 is charged as a story failure

1 participant