Skip to content

chore(release): 0.6.0 — rollback never wipes non-automator files#3

Merged
pbean merged 2 commits into
mainfrom
release/0.6.0
Jun 21, 2026
Merged

chore(release): 0.6.0 — rollback never wipes non-automator files#3
pbean merged 2 commits into
mainfrom
release/0.6.0

Conversation

@pbean

@pbean pbean commented Jun 21, 2026

Copy link
Copy Markdown
Collaborator

0.6.0 — data-loss fix + opt-in auto-rollback

Why

A user's project (~/dev/notey) had its _bmad-output/ wiped. Root cause: on a failed in-place attempt, the engine ran git reset --hard <baseline> + a blanket git clean -fd over the whole checkout, sparing only .automator/ and two artifact subdirs. Any other untracked file (incl. _bmad-output/ when not git-tracked) could be deleted.

Fixed

  • The orchestrator never runs a blanket git clean again. New verify.safe_rollback reverts the attempt's tracked changes and removes only the untracked files that run created (diffed against a baseline-time snapshot, StoryTask.baseline_untracked). Pre-existing untracked files and the entire _bmad-output/ tree are preserved (now exposed wholesale via ProjectPaths.output_folder).

Changed

  • Auto-rollback is opt-in: [scm] rollback_on_failure (default off).
    • Off: the engine never touches the tree on failure — it pauses with bold manual-recovery instructions (back up untracked → git reset --hard <baseline> → restore).
    • On: the safe rollback above runs, and warns that it discards the attempt's uncommitted work.
    • Worktree isolation (scm.isolation = "worktree") sidesteps the path entirely.

Tests

  • New safe_rollback unit tests (preserve pre-existing untracked, remove run-created, keep-dir protection, none-snapshot no-op, empty-dir prune) + engine ON/OFF behavior tests. In-place continuation tests opt into rollback_on_failure=True. Full suite 862 passing, trunk check clean.

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes – Version 0.6.0

  • New Features

    • Added configurable output_folder path for output directory customization
    • Auto-rollback is now opt-in via [scm] rollback_on_failure (default: disabled)
  • Bug Fixes

    • Fixed rollback to preserve pre-existing untracked files and output directory during failed attempts
    • Added manual recovery instructions when auto-rollback is disabled

pbean and others added 2 commits June 20, 2026 21:35
…d opt-in

A failed in-place attempt previously ran `git reset --hard` + a blanket
`git clean -fd` over the whole checkout, sparing only `.automator/` and two
artifact subdirs. That could delete a project's `_bmad-output/` and any other
untracked files (the cause of the reported notey data loss).

The orchestrator now never runs a blanket `git clean`. New
`verify.safe_rollback` reverts the attempt's tracked changes to baseline and
removes only the untracked files THIS run created (diffed against a
baseline-time snapshot, `StoryTask.baseline_untracked`); pre-existing untracked
files and the whole `_bmad-output/` are preserved. The output folder is now
exposed wholesale via `ProjectPaths.output_folder`.

Auto-rollback is gated behind `[scm] rollback_on_failure` (default off). Off:
the engine never touches the tree — it pauses with bold manual-recovery
instructions (back up untracked files -> git reset --hard <baseline> ->
restore). On: the safe rollback above runs and warns. Sweep migration uses a
non-pausing `_safe_reset` for internal ledger restore. Worktree isolation
sidesteps the path entirely.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Version 0.6.0 replaces the previous unconditional reset_hard/git clean rollback with a bounded safe_rollback that reverts only tracked changes and run-created untracked files. A new ScmPolicy.rollback_on_failure flag (default off) switches failed in-place attempts between auto rollback and a manual-recovery pause. StoryTask gains a baseline_untracked snapshot field; ProjectPaths gains output_folder.

Changes

Safe Rollback & Opt-in Policy

Layer / File(s) Summary
Policy, task, and path contracts
src/automator/policy.py, src/automator/model.py, src/automator/bmadconfig.py
ScmPolicy gains rollback_on_failure: bool = False with TOML loader wiring and template update. StoryTask gains baseline_untracked: list[str] | None with to_dict/from_dict serialization. ProjectPaths gains output_folder with defaulting in __post_init__, rebasing, and load_paths.
git primitives: untracked_files and safe_rollback
src/automator/verify.py
Adds untracked_files (reads git ls-files --others --exclude-standard), safe_rollback (hard-resets tracked state then deletes only run-created untracked files outside keep dirs), and _prune_empty_parents. Removes reset_hard.
Engine recovery helpers and call-site updates
src/automator/engine.py
Adds _rollback_or_pause (branches on rollback_on_failure), _safe_reset (calls verify.safe_rollback with snapshot), and _pause_for_manual_recovery (emits journal entry, raises RunPaused). Snapshots baseline_untracked in _dev_phase. Replaces _reset_to calls in _finish_inflight, dev retry loop, and _defer.
Sweep call-site migration
src/automator/sweep.py
_run_bundle calls _rollback_or_pause instead of _reset_to; _ensure_migration uses _safe_reset on dirty-worktree resume and failed validation, and records task.baseline_untracked when capturing the baseline commit.
Tests
tests/test_verify.py, tests/test_engine.py, tests/test_hook_bus.py, tests/test_plugin_workflows.py, tests/test_sweep.py
Five test_verify.py tests cover safe_rollback behaviors. Two test_engine.py tests cover rollback-off pause and rollback-on untracked preservation. All make_engine/make_sweep harness helpers updated with ScmPolicy(rollback_on_failure=True) in the default policy.
Settings UI, changelog, and version bumps
src/automator/data/settings/core.toml, CHANGELOG.md, pyproject.toml, src/automator/__init__.py, module.yaml, src/automator/data/skills/bmad-auto-setup/assets/module.yaml, .claude-plugin/marketplace.json
Adds rollback_on_failure switch widget to the SCM settings UI. Adds the 0.6.0 changelog entry. Bumps version to 0.6.0 across all manifests and the Python package.

Sequence Diagram(s)

sequenceDiagram
    participant Engine
    participant verify
    participant StoryTask
    participant Journal

    Engine->>StoryTask: snapshot baseline_untracked = untracked_files(repo)
    Note over Engine: attempt runs...
    Engine->>Engine: attempt fails → _rollback_or_pause(task)
    alt rollback_on_failure = True
        Engine->>verify: safe_rollback(repo, baseline_commit, baseline_untracked)
        verify->>verify: git reset --hard baseline
        verify->>verify: compute new_files = current − baseline_untracked
        verify->>verify: unlink new_files (skipping keep dirs)
        verify->>verify: _prune_empty_parents
        Engine->>Engine: task defers
    else rollback_on_failure = False
        Engine->>Journal: record rollback-manual-required entry
        Engine->>Engine: emit notification with manual recovery instructions
        Engine->>Engine: raise RunPaused
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐇 Hop hop, no more blasting clean!
Only the new files vanish from the scene.
A snapshot taken before the run,
keeps old untracked safe when things come undone.
With rollback_on_failure set to your will,
the warren stays tidy — no accidental spill! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.44% 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 accurately summarizes the main change: version 0.6.0 release with the critical fix preventing rollback from wiping non-automator files.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch release/0.6.0

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 and usage tips.

@augmentcode

augmentcode Bot commented Jun 21, 2026

Copy link
Copy Markdown
🤖 Augment PR Summary

Summary: This release bumps bmad-auto to 0.6.0 and fixes a data-loss risk in in-place failure recovery by replacing blanket cleaning with a safe, snapshot-based rollback.

Changes:

  • Added verify.safe_rollback() to reset tracked changes to a baseline commit and delete only untracked files created since a baseline snapshot (no git clean -fd).
  • Introduced StoryTask.baseline_untracked and capture of baseline untracked paths at dev/sweep start for diff-based rollback.
  • Made auto-rollback opt-in via [scm] rollback_on_failure (default off); when off, the engine pauses with manual recovery instructions.
  • Added ProjectPaths.output_folder (default _bmad-output) and protects it wholesale during rollback.
  • Updated engine/sweep flows to use the new rollback-or-pause behavior and expanded tests to cover ON/OFF modes and safe rollback semantics.

Technical Notes: Rollback now preserves pre-existing untracked files and protects BMAD output directories; worktree isolation continues to avoid in-place rollback paths entirely.

🤖 Was this summary useful? React with 👍 or 👎

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

Review completed. 1 suggestion posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

Comment thread src/automator/verify.py
repo = repo.resolve()
keep_roots = [(repo / k).resolve() for k in keep]
for rel in sorted(created):
path = (repo / rel).resolve()

@augmentcode augmentcode Bot Jun 21, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

src/automator/verify.py:160: path = (repo / rel).resolve() will follow symlinks, so if a run-created untracked path is a symlink the rollback may unlink() the symlink target (potentially outside the repo) rather than removing the symlink itself. Consider avoiding resolve() on deletion targets and ensuring rollback only deletes within the repo while unlinking the repo entry, not the resolved target.

Severity: high

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

@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: 4

Caution

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

⚠️ Outside diff range comments (1)
src/automator/engine.py (1)

1350-1356: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not stash deferred artifacts before the rollback-off pause.

_stash_deferred_artifacts() moves the spec out of the artifact directory before _rollback_or_pause() decides to pause, so the default rollback_on_failure = false path still modifies the tree instead of leaving it exactly as-is. Gate the stash behind the auto-rollback path, or perform it only after manual recovery has been acknowledged.

🤖 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/automator/engine.py` around lines 1350 - 1356, The
_stash_deferred_artifacts method is being called unconditionally before
_rollback_or_pause, which modifies the tree even when rollback_on_failure is
false and the task is paused instead of rolled back. Move the
_stash_deferred_artifacts call so it only executes within the auto-rollback code
path (when rollback_on_failure is true) rather than before _rollback_or_pause is
called, ensuring the tree remains unmodified when pausing without auto-rollback
enabled.
🤖 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/automator/engine.py`:
- Around line 650-651: The manual recovery pause logic lacks a check to
determine if recovery has already been acknowledged, causing unconditional
re-pausing on resume even when manual recovery is complete. Add a recovered or
acknowledged state check before calling `_pause_for_manual_recovery()` in the
`_finish_inflight()` and `_rollback_or_pause()` methods to skip the pause if
recovery has been verified. Additionally, in the `_defer()` method, defer
marking the task as terminal until after the recovery pause is completed, rather
than marking it terminal before the pause, so that resuming the workflow can
properly verify recovery status before continuing execution.

In `@src/automator/policy.py`:
- Line 428: The rollback_on_failure parameter assignment uses bool() conversion
which silently converts string values like "false" to True, enabling unintended
auto-rollback behavior. Instead of using bool(scm_d.get(...)), extract the value
and validate that it is actually a boolean type, raising a PolicyError if a
non-boolean value is provided. Only assign the rollback_on_failure parameter
when the value is confirmed to be a genuine boolean from the TOML configuration
or fall back to the ScmPolicy.rollback_on_failure default.

In `@src/automator/verify.py`:
- Around line 157-164: The issue is that path.resolve() is being called on line
160 before unlink(), which follows symlinks to their targets. This causes
unlink() to delete the target file instead of the symlink itself, and can allow
symlinks to bypass the keep_roots protection by resolving outside the designated
keep areas. Remove the .resolve() call on the path variable used in the unlink()
operation, so that comparisons with keep_roots and the unlink() call work on the
original repo-relative lexical path rather than the resolved target path. Keep
the comparison logic but ensure deletion operates on the unresolved path.

In `@tests/test_engine.py`:
- Around line 379-381: The test assertions are not sufficiently verifying
tracked-file rollback behavior. In the first test block around line 380-381, add
an explicit assertion that checks the content of src.txt to verify tracked edits
were properly reset (beyond just checking that the worktree is dirty and the
precious untracked file exists). Similarly, in the second test block around line
405-408, add an explicit assertion that checks the src.txt content to confirm
working-tree content matches expectations, rather than only verifying the commit
pointer position with HEAD. Both test sections need to include explicit src.txt
content verification to strengthen the assertions and properly validate the
tracked-file rollback behavior.

---

Outside diff comments:
In `@src/automator/engine.py`:
- Around line 1350-1356: The _stash_deferred_artifacts method is being called
unconditionally before _rollback_or_pause, which modifies the tree even when
rollback_on_failure is false and the task is paused instead of rolled back. Move
the _stash_deferred_artifacts call so it only executes within the auto-rollback
code path (when rollback_on_failure is true) rather than before
_rollback_or_pause is called, ensuring the tree remains unmodified when pausing
without auto-rollback enabled.
🪄 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

Run ID: 06b63fc1-413c-40e4-a966-a770f7f49dbe

📥 Commits

Reviewing files that changed from the base of the PR and between e08bdbf and 804dd77.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (18)
  • .claude-plugin/marketplace.json
  • CHANGELOG.md
  • module.yaml
  • pyproject.toml
  • src/automator/__init__.py
  • src/automator/bmadconfig.py
  • src/automator/data/settings/core.toml
  • src/automator/data/skills/bmad-auto-setup/assets/module.yaml
  • src/automator/engine.py
  • src/automator/model.py
  • src/automator/policy.py
  • src/automator/sweep.py
  • src/automator/verify.py
  • tests/test_engine.py
  • tests/test_hook_bus.py
  • tests/test_plugin_workflows.py
  • tests/test_sweep.py
  • tests/test_verify.py

Comment thread src/automator/engine.py
Comment on lines +650 to +651
if not self.policy.scm.rollback_on_failure:
self._pause_for_manual_recovery(task, task.baseline_commit or "")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Make the manual-rollback pause resumable before continuing.

With rollback disabled, _pause_for_manual_recovery() saves the current task state. On retry/resume, _finish_inflight() calls _rollback_or_pause() again and pauses unconditionally, even if the operator already reset manually. In _defer(), the task is already terminal before the pause, so a resume can skip recovery entirely and continue on the dirty tree. Add a recovered/acknowledged state check before re-pausing, and avoid marking the task terminal until recovery has been verified or completed.

Also applies to: 742-743, 1339-1356

🤖 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/automator/engine.py` around lines 650 - 651, The manual recovery pause
logic lacks a check to determine if recovery has already been acknowledged,
causing unconditional re-pausing on resume even when manual recovery is
complete. Add a recovered or acknowledged state check before calling
`_pause_for_manual_recovery()` in the `_finish_inflight()` and
`_rollback_or_pause()` methods to skip the pause if recovery has been verified.
Additionally, in the `_defer()` method, defer marking the task as terminal until
after the recovery pause is completed, rather than marking it terminal before
the pause, so that resuming the workflow can properly verify recovery status
before continuing execution.

Comment thread src/automator/policy.py
merge_strategy=str(scm_d.get("merge_strategy", ScmPolicy.merge_strategy)),
delete_branch=bool(scm_d.get("delete_branch", ScmPolicy.delete_branch)),
keep_failed=bool(scm_d.get("keep_failed", ScmPolicy.keep_failed)),
rollback_on_failure=bool(scm_d.get("rollback_on_failure", ScmPolicy.rollback_on_failure)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject non-boolean rollback_on_failure values instead of truthifying them.

bool(scm_d.get(...)) makes rollback_on_failure = "false" evaluate to True, silently opting into auto rollback. Since this flag controls whether the checkout is modified on failure, require a real TOML boolean and raise PolicyError otherwise.

Proposed fix
+    raw_rollback_on_failure = scm_d.get(
+        "rollback_on_failure", ScmPolicy.rollback_on_failure
+    )
+    if not isinstance(raw_rollback_on_failure, bool):
+        raise PolicyError(
+            "scm.rollback_on_failure must be a boolean: "
+            f"got {raw_rollback_on_failure!r}"
+        )
     scm = ScmPolicy(
         isolation=str(scm_d.get("isolation", ScmPolicy.isolation)),
         branch_per=str(scm_d.get("branch_per", ScmPolicy.branch_per)),
         target_branch=str(scm_d.get("target_branch", ScmPolicy.target_branch)),
         merge_strategy=str(scm_d.get("merge_strategy", ScmPolicy.merge_strategy)),
         delete_branch=bool(scm_d.get("delete_branch", ScmPolicy.delete_branch)),
         keep_failed=bool(scm_d.get("keep_failed", ScmPolicy.keep_failed)),
-        rollback_on_failure=bool(scm_d.get("rollback_on_failure", ScmPolicy.rollback_on_failure)),
+        rollback_on_failure=raw_rollback_on_failure,
📝 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
rollback_on_failure=bool(scm_d.get("rollback_on_failure", ScmPolicy.rollback_on_failure)),
raw_rollback_on_failure = scm_d.get(
"rollback_on_failure", ScmPolicy.rollback_on_failure
)
if not isinstance(raw_rollback_on_failure, bool):
raise PolicyError(
"scm.rollback_on_failure must be a boolean: "
f"got {raw_rollback_on_failure!r}"
)
scm = ScmPolicy(
isolation=str(scm_d.get("isolation", ScmPolicy.isolation)),
branch_per=str(scm_d.get("branch_per", ScmPolicy.branch_per)),
target_branch=str(scm_d.get("target_branch", ScmPolicy.target_branch)),
merge_strategy=str(scm_d.get("merge_strategy", ScmPolicy.merge_strategy)),
delete_branch=bool(scm_d.get("delete_branch", ScmPolicy.delete_branch)),
keep_failed=bool(scm_d.get("keep_failed", ScmPolicy.keep_failed)),
rollback_on_failure=raw_rollback_on_failure,
🤖 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/automator/policy.py` at line 428, The rollback_on_failure parameter
assignment uses bool() conversion which silently converts string values like
"false" to True, enabling unintended auto-rollback behavior. Instead of using
bool(scm_d.get(...)), extract the value and validate that it is actually a
boolean type, raising a PolicyError if a non-boolean value is provided. Only
assign the rollback_on_failure parameter when the value is confirmed to be a
genuine boolean from the TOML configuration or fall back to the
ScmPolicy.rollback_on_failure default.

Comment thread src/automator/verify.py
Comment on lines +157 to +164
repo = repo.resolve()
keep_roots = [(repo / k).resolve() for k in keep]
for rel in sorted(created):
path = (repo / rel).resolve()
if any(path == root or path.is_relative_to(root) for root in keep_roots):
continue
try:
path.unlink(missing_ok=True)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Do not resolve rollback candidates before unlinking.

Line 160 follows symlinks before unlink(). If the run creates junk -> /home/user/file, rollback deletes /home/user/file instead of the junk symlink; a symlink under _bmad-output can also resolve outside the keep root and bypass protection. Keep comparisons should be repo-relative/lexical, and deletion should unlink the original path.

Proposed fix
+from pathlib import PurePosixPath
+
 def safe_rollback(
     repo: Path,
     baseline: str,
     *,
     baseline_untracked: list[str] | None,
@@
-    repo = repo.resolve()
-    keep_roots = [(repo / k).resolve() for k in keep]
+    repo = repo.resolve()
+    keep_roots = tuple(PurePosixPath(k) for k in keep)
     for rel in sorted(created):
-        path = (repo / rel).resolve()
-        if any(path == root or path.is_relative_to(root) for root in keep_roots):
+        rel_path = PurePosixPath(rel)
+        if rel_path.is_absolute() or ".." in rel_path.parts:
+            continue
+        if any(rel_path == root or rel_path.is_relative_to(root) for root in keep_roots):
             continue
+        path = repo.joinpath(*rel_path.parts)
         try:
             path.unlink(missing_ok=True)
         except OSError:
             continue
🤖 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/automator/verify.py` around lines 157 - 164, The issue is that
path.resolve() is being called on line 160 before unlink(), which follows
symlinks to their targets. This causes unlink() to delete the target file
instead of the symlink itself, and can allow symlinks to bypass the keep_roots
protection by resolving outside the designated keep areas. Remove the .resolve()
call on the path variable used in the unlink() operation, so that comparisons
with keep_roots and the unlink() call work on the original repo-relative lexical
path rather than the resolved target path. Keep the comparison logic but ensure
deletion operates on the unresolved path.

Comment thread tests/test_engine.py
Comment on lines +379 to +381
# the orchestrator left the tree exactly as-is — no reset, nothing deleted
assert not worktree_clean(project.project)
assert precious.read_text() == "precious\n"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Strengthen assertions to verify tracked-file rollback behavior.

Line 380 can stay dirty from the deliberate untracked file even if tracked edits were reset, and Line 407 only checks commit pointer (HEAD), not working-tree content. Add explicit src.txt content checks in both tests.

Suggested test-tightening patch
 def test_rollback_off_pauses_with_manual_notice(project):
@@
-    # the orchestrator left the tree exactly as-is — no reset, nothing deleted
+    # the orchestrator left tracked edits exactly as-is — no reset
+    assert "change for 1-1-a" in (project.project / "src.txt").read_text()
+    # and the tree is still dirty
     assert not worktree_clean(project.project)
     assert precious.read_text() == "precious\n"
@@
 def test_rollback_on_preserves_preexisting_untracked(project):
@@
-    assert rev_parse_head(project.project) == task.baseline_commit  # tracked reverted
+    assert rev_parse_head(project.project) == task.baseline_commit
+    assert (project.project / "src.txt").read_text() == "original\n"  # tracked reverted
     assert precious.read_text() == "keep me\n"  # pre-existing untracked survives

Also applies to: 405-408

🤖 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 `@tests/test_engine.py` around lines 379 - 381, The test assertions are not
sufficiently verifying tracked-file rollback behavior. In the first test block
around line 380-381, add an explicit assertion that checks the content of
src.txt to verify tracked edits were properly reset (beyond just checking that
the worktree is dirty and the precious untracked file exists). Similarly, in the
second test block around line 405-408, add an explicit assertion that checks the
src.txt content to confirm working-tree content matches expectations, rather
than only verifying the commit pointer position with HEAD. Both test sections
need to include explicit src.txt content verification to strengthen the
assertions and properly validate the tracked-file rollback behavior.

@pbean
pbean merged commit 82efed3 into main Jun 21, 2026
7 checks passed
@pbean
pbean deleted the release/0.6.0 branch June 21, 2026 04:45
dracic added a commit to dracic/bmad-auto that referenced this pull request Jun 24, 2026
…d string

window_command validated env names but not env values; a newline/control char in a value could desync psmux command-string parsing before pwsh. Add _validate_env_value to reject control characters fail-safe (mirrors the existing _validate_env_key guard). POSIX path unchanged.

Refs: _bmad-output/implementation-artifacts/deferred-work.md (deferred-work.md, Adversarial-review deferrals bmad-code-org#3)

Claude-Session: https://claude.ai/code/session_019ohWeC44Mto4upsYNNaVh8
dracic added a commit to dracic/bmad-auto that referenced this pull request Jun 24, 2026
…e-aware

Three Windows branches asserting a real-dir Library result held only on an unprivileged runner; on a Windows host with SeCreateSymbolicLinkPrivilege (Developer Mode/admin) the product symlinks and the asserts would fail. Probe the actual symlink capability and assert a symlink when held, else the real-dir fallback. The forced-denial host-agnostic test and the real-dir priming tests are unchanged.

Refs: _bmad-output/implementation-artifacts/deferred-work.md (deferred-work.md, Windows test-suite greening residuals bmad-code-org#3)

Claude-Session: https://claude.ai/code/session_019ohWeC44Mto4upsYNNaVh8
pbean added a commit that referenced this pull request Jul 23, 2026
…findings (#276)

A second-model adversarial review of PR #277 surfaced 7 valid findings. This
lands the fixes for six of them (the 7th, a repo-wide policy-coercion
consistency issue, is deferred to a follow-up):

- #1 (High) M2 transition now outranks the M1 hash gate. A clean review that
  round-trips done->in-review->done back to byte-identical launch bytes while
  omitting its marker no longer false-refuses; the hash gate refuses only when
  no transition was observed. Safe: the dead-window false positive never records
  a transition (status never leaves `done`).
- #2 (High) The stories-mode folder+id read-back now applies the same
  launch-snapshot gate, closing the identical false completion it previously
  bypassed (it accepted on the mtime floor alone). Inert on a dev leg (no
  snapshot).
- #3 (High) The three in-place spec rewriters (append/strip/reset) now write
  atomically (temp + atomic_replace), so an interrupted/disk-full repair leaves
  the original spec intact instead of truncated.
- #4 (Med) Adds engine-level coverage of the synthesized-REVIEW repair path
  (including a follow-up review that rewrites the spec) plus regressions for
  #1/#2/#3/#5/#7 and a _snapshot_verdict truth-table.
- #5 (Low) Snapshot/candidate identity is now by resolved filesystem path
  (_same_spec), not raw string spelling, so a `..`/symlink/case alias can't
  disable M1/M2.
- #7 (Low) A spec ending in a bare `\r` no longer receives an invisible heading
  (completed to CRLF so the scan's `^` regex sees it).

#1 and #2 share a single `_snapshot_verdict` helper so the "M2 outranks M1"
rule can't drift between the two paths. All edits sit on `_DevSynthesisMixin`,
covering both the tmux and OpenCode adapters.
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.

1 participant