Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,22 @@ All notable changes to `bmad-loop` are documented here. The format is based on
[Semantic Versioning](https://semver.org/spec/v2.0.0.html). While the project is pre-1.0,
breaking changes may land in a minor release.

## [Unreleased]

### Added

- **Intent-gap patch-restore recovery.** When review halts on an `intent gap`, `bmad-dev-auto`
now saves the attempted change as a patch before reverting (BMAD-METHOD#2564). If the attempted
reading was correct, `bmad-loop resolve` re-arms the spec to `in-review` and re-applies the patch
onto baseline after every reset, so the re-driven session resumes review on the restored diff
instead of re-implementing. New `--restore-patch <path>` flag for the `--no-interactive` path; a
patch that fails to apply escalates instead of running on a half-restored tree.
- **Preflight covers the inline review layers.** `bmad-loop validate` (and run-start) now require the
three upstream review-hunter skills `bmad-dev-auto`'s step-04 invokes — `bmad-review-adversarial-general`,
`bmad-review-edge-case-hunter`, and `bmad-review-verification-gap` (new in BMAD-METHOD#2550) — plus a
`customize.toml` in `bmad-dev-auto` (its review-layer config, BMAD-METHOD#2535/#2550). A pre-July bmm
install missing any is reported with remediation before a run stalls.

## [0.8.1] — 2026-07-05

### Fixed
Expand Down
21 changes: 13 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Inspired by the original [bmad-automator](https://github.com/bmad-code-org/bmad-

- **Python 3.11+**, **tmux**, and a supported coding CLI — `claude` by default; `codex` and `gemini` via [profiles](#other-coding-clis).
- **Linux or macOS** (or **Windows via WSL**, which _is_ Linux — it runs as-is). tmux is the one terminal-multiplexer backend today, but it now sits behind a pluggable **registry** of OS seams (transport, process lifecycle, hook interpreter), so a native-Windows backend slots in as new files + a registration line each, with no engine edits — see [Porting bmad-loop to a new OS](docs/porting-to-a-new-os.md). Native Windows is not yet shipped.
- A **BMAD v6 project** (`_bmad/bmm/config.yaml`, a `sprint-status.yaml` from `bmad-sprint-planning`) with the upstream `bmad-dev-auto` skill and the bmad-loop skill module from this repo installed (`bmad-loop-resolve`, `bmad-loop-sweep` — see [Installing the skill module](#installing-the-skill-module)). Standard BMAD skills stay untouched.
- A **BMAD v6 project** (`_bmad/bmm/config.yaml`, a `sprint-status.yaml` from `bmad-sprint-planning`) with the upstream `bmad-dev-auto` skill (and the three review-hunter skills its step-04 invokes inline: `bmad-review-adversarial-general`, `bmad-review-edge-case-hunter`, `bmad-review-verification-gap`) and the bmad-loop skill module from this repo installed (`bmad-loop-resolve`, `bmad-loop-sweep` — see [Installing the skill module](#installing-the-skill-module)). Standard BMAD skills stay untouched.

## Quick start

Expand Down Expand Up @@ -215,14 +215,19 @@ Bundle dev sessions can themselves append new deferred entries (split-off goals,

## Installing the skill module

The orchestrator drives the upstream `bmad-dev-auto` skill as its inner dev primitive — unmodified, so there is no fork to keep in sync; it both implements and (re-invoked on the done spec) runs the follow-up review — plus its own bundled `bmad-loop-*` skills for escalation, sweep, and setup. Your standard BMAD install is never modified. The three bundled skills ship in the `bmad-loop` wheel (canonical source: `src/bmad_loop/data/skills/`, BMAD module code `bmad-loop`) so `bmad-loop init` lays them down for you; `bmad-dev-auto` is a prerequisite installed by the BMad Method (bmm) module:
The orchestrator drives the upstream `bmad-dev-auto` skill as its inner dev primitive — unmodified, so there is no fork to keep in sync; it both implements and (re-invoked on the done spec) runs the follow-up review — plus its own bundled `bmad-loop-*` skills for escalation, sweep, and setup. Your standard BMAD install is never modified. The three bundled skills ship in the `bmad-loop` wheel (canonical source: `src/bmad_loop/data/skills/`, BMAD module code `bmad-loop`) so `bmad-loop init` lays them down for you; `bmad-dev-auto` and the three review-hunter skills its step-04 invokes inline are prerequisites installed by the BMad Method (bmm) module:

| Skill | Role |
| ------------------- | ------------------------------------------------------------------------------------------- |
| `bmad-dev-auto` | unattended implementation + follow-up review (**upstream** — bmm prerequisite, not bundled) |
| `bmad-loop-resolve` | interactive CRITICAL-escalation resolution (`/bmad-loop-resolve <story>`) |
| `bmad-loop-sweep` | deferred-work ledger triage (automation-only) |
| `bmad-loop-setup` | registers the module in `_bmad/` config + help |
| Skill | Role |
| --------------------------------- | ----------------------------------------------------------------------------------------------- |
| `bmad-dev-auto` | unattended implementation + follow-up review (**upstream** — bmm prerequisite, not bundled) |
| `bmad-review-adversarial-general` | inline step-04 review layer (**upstream** — bmm prerequisite, not bundled) |
| `bmad-review-edge-case-hunter` | inline step-04 review layer (**upstream** — bmm prerequisite, not bundled) |
| `bmad-review-verification-gap` | inline step-04 review layer, newest (BMAD-METHOD#2550) (**upstream** — bmm prereq, not bundled) |
| `bmad-loop-resolve` | interactive CRITICAL-escalation resolution (`/bmad-loop-resolve <story>`) |
| `bmad-loop-sweep` | deferred-work ledger triage (automation-only) |
| `bmad-loop-setup` | registers the module in `_bmad/` config + help |

`bmad-loop validate` preflights all four upstream skills; a target project on a pre-July bmm install missing `bmad-review-verification-gap` (or a `bmad-dev-auto` without its `customize.toml` review-layer config) is reported with bmm-module remediation before any run starts.

**Via uv + `bmad-loop init` (self-sufficient).** Installing the tool and running `init` is all you need — `init` installs the `bmad-loop-*` skills into `.claude/skills/` (claude) and/or `.agents/skills/` (codex/gemini) for the CLIs you select, alongside the hooks and policy:

Expand Down
5 changes: 3 additions & 2 deletions docs/FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ See [README.md](../README.md) for the narrative overview and [setup-guide.md](se
### Adversarial review (review stage)

- The follow-up review is a re-invocation of `bmad-dev-auto` on the `done` spec — a fresh-context session with no anchoring bias from the implementer (BMAD-METHOD#2508 routes a `done` spec to a fresh step-04 review pass), so there is no separate review skill.
- Two parallel adversarial layers (Blind Hunter / Edge Case Hunter) → verify findings against code → triage → auto-apply patches → log → defer ambiguity → commit.
- Parallel adversarial layers resolved from the skill's `customize.toml` (defaults: Adversarial-General, Edge-Case-Hunter, and Verification-Gap — the third added by BMAD-METHOD#2550) → verify findings against code → triage → auto-apply patches → log → defer ambiguity → commit. Each layer invokes an upstream `bmad-review-*` skill, so all three are bmm prerequisites the `bmad-loop validate` preflight checks for.
- Bounded review loop (`limits.max_review_cycles`, default 3 cycles); done when the pass finishes `done` and no longer recommends a follow-up. This bound is also the oscillation guard for skill-recommended follow-up review.
- Optional (`[review].enabled`, default `true`): set `false` to skip the follow-up review session. The dev pass's own inline review (same two layers, in-context) is then the only review and it finalizes the story to `done` — one session per story instead of two. Verify commands still gate the commit. Applies to story runs and deferred-work sweeps alike.
- Optional (`[review].enabled`, default `true`): set `false` to skip the follow-up review session. The dev pass's own inline review (same layers, in-context) is then the only review and it finalizes the story to `done` — one session per story instead of two. Verify commands still gate the commit. Applies to story runs and deferred-work sweeps alike.
- Trigger (`[review].trigger`, default `recommended`): when review is enabled, decides _when_ the follow-up pass runs. `recommended` runs it only when `bmad-dev-auto` sets `followup_review_recommended` on a `done` spec (it self-reviews inline and flags an independent pass only when its review-driven changes were significant — BMAD-METHOD#2505). `always` runs it on every story (pre-0.7.0 behavior).

### Failure handling & resilience
Expand All @@ -59,6 +59,7 @@ See [README.md](../README.md) for the narrative overview and [setup-guide.md](se
- Plateau-defer: when review won't converge, the story is skipped, the spec stashed into the run dir, deferred-work preserved, the run continues.
- Typed escalations: `CRITICAL` pauses the run + notifies (desktop + `ATTENTION` file); `PREFERENCE` is journaled and continues.
- CRITICAL resolution: `bmad-loop resolve <run-id>` opens an interactive resolve agent seeded with the escalation + frozen spec; you disambiguate, it re-arms the story (`escalated → pending`, spec reset to `ready-for-dev`) and resumes. `--no-interactive` skips to re-arm if you fixed the spec yourself.
- Intent-gap patch-restore (BMAD-METHOD#2564): when review halts on an `intent gap`, `bmad-dev-auto` saves the attempted change as a patch file (in the implementation-artifacts folder, referenced from the halt output) before reverting the tree. If the attempted reading turns out to be correct, the resolve agent adds `"restore_patch": "<path>"` to its `resolution.json`; the orchestrator then re-arms the spec to `in-review` (not `ready-for-dev`) and re-applies the patch onto the baseline after every reset, so the re-driven session resumes _review_ on the restored diff instead of re-implementing from scratch. A hand-driven `bmad-loop resolve --no-interactive --restore-patch <path>` does the same. A patch that fails to apply escalates rather than dispatching a session onto a half-restored tree.

### Git worktree isolation (opt-in)

Expand Down
54 changes: 52 additions & 2 deletions src/bmad_loop/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,38 @@ def _confirm(question: str) -> bool:
return ans in ("y", "yes")


def _resolve_restore_patch(
project: Path, run_dir: Path, story_key: str, args: argparse.Namespace
) -> tuple[str | None, str | None]:
"""Determine the intent-gap patch-restore latch (BMAD-METHOD #2564) for a re-arm.

Precedence: the explicit ``--restore-patch`` flag (hand-driven recovery) wins;
otherwise, on the interactive path, the resolve agent may have recorded a
``restore_patch`` field in resolution.json. Returns ``(latch, error)``: a
validated absolute patch path to latch (None = ordinary from-scratch re-drive),
or an error string when a supplied path is missing / outside the project — the
caller aborts strictly rather than silently re-driving from scratch when the
operator asked to restore."""
raw = getattr(args, "restore_patch", None)
if raw is None and args.interactive:
doc = resolve.read_resolution(run_dir, story_key)
if doc is not None:
val = doc.get("restore_patch")
raw = str(val) if val else None
if not raw:
return None, None
patch = Path(raw)
if not patch.is_absolute():
patch = project / patch
patch = patch.resolve()

@augmentcode augmentcode Bot Jul 6, 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/bmad_loop/cli.py:627 — Path.resolve() can raise in some filesystem edge cases (e.g., symlink loops or permission errors); since --restore-patch is user-supplied input, an uncaught exception here would crash bmad-loop resolve instead of producing the intended validation failure.

Severity: low

Fix This in Augment

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

if not patch.is_file() or not patch.is_relative_to(project.resolve()):
return None, (
f"restore patch {raw!r} is not a file under the project — refusing to "
"re-arm (fix the path, or re-run without a restore to re-drive from scratch)"
)
return str(patch), None


def cmd_resolve(args: argparse.Namespace) -> int:
from .model import PAUSE_ESCALATION, Phase

Expand Down Expand Up @@ -671,16 +703,26 @@ def cmd_resolve(args: argparse.Namespace) -> int:
file=sys.stderr,
)

# intent-gap patch-restore latch (#2564): validate a supplied patch path before
# prompting, so a bad path fails loudly instead of after a confirmation.
restore_patch, err = _resolve_restore_patch(project, run_dir, story_key, args)
if err is not None:
print(err, file=sys.stderr)
return 1

# confirm-then-resume (args.resume: None = ask, True = auto, False = re-arm only)
if args.resume is None and not _confirm(f"re-arm {story_key} and resume run {args.run_id}?"):
print("cancelled — run is still paused at the escalation")
return 0
try:
runs.rearm_escalation(run_dir, story_key)
runs.rearm_escalation(run_dir, story_key, restore_patch=restore_patch)
except runs.RearmError as e:
print(f"error: {e}", file=sys.stderr)
return 1
print(f"re-armed {story_key}")
print(
f"re-armed {story_key}"
+ (" (restoring the attempted change for review)" if restore_patch else "")
)
if args.resume is False:
print(f"resume when ready: bmad-loop resume {args.run_id}")
return 0
Expand Down Expand Up @@ -1298,6 +1340,14 @@ def add(name: str, func, help: str, *, aliases=()) -> argparse.ArgumentParser:
action="store_false",
help="skip the resolve agent (spec already fixed by hand); just re-arm + resume",
)
resolve_p.add_argument(
"--restore-patch",
metavar="PATH",
help="intent-gap patch-restore (#2564): re-arm the spec to `in-review` and "
"re-apply this saved patch before the re-drive, resuming review on the "
"attempted change instead of re-implementing (hand-driven; the interactive "
"agent supplies it via resolution.json)",
)
resolve_p.add_argument(
"--resume",
action=argparse.BooleanOptionalAction,
Expand Down
36 changes: 35 additions & 1 deletion src/bmad_loop/data/skills/bmad-loop-resolve/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,14 @@ action of a successful resolution. Schema:
"story_key": "<key>",
"decision": "<one or two sentences: the rule you and the human chose>",
"spec_file": "<the spec you edited>",
"spec_updated": true
"spec_updated": true,
"restore_patch": "<optional: path to a saved intent-gap patch to re-apply>"
}
```

`restore_patch` is **optional** and used only for the intent-gap patch-restore
case below — omit it entirely for an ordinary resolution.

## What you MUST do

1. **Read the context**, then read the **frozen spec** at `spec_file` in full —
Expand All @@ -79,6 +83,36 @@ action of a successful resolution. Schema:
orchestrator will offer to **re-arm the story and resume the run** (a clean
rebuild against the corrected spec).

## Special case: a review-stage `intent gap` with a saved patch

When the escalation came from the **review step** halting on an `intent gap`, the
dev session first **saved its attempted change as a patch file** (in the
implementation-artifacts folder) before reverting the tree — the escalation
`detail` and the spec's `## Review Triage Log` reference the patch path. That
patch is concrete evidence: it shows exactly which reading of the intent the run
implemented. Use it two ways:

- **As evidence.** Read the patch (and the diff it represents) to see what the
guessed reading produced — often clearer input for writing the clarification
than the questions alone.
- **When the attempted reading was actually correct.** Sometimes the run's guess
is the right one and only the _intent_ was silent. Present this as an explicit
option to the human: _"the implementation read it as X, which is in fact what we
want — amend the intent to say X, and resume **review** on the already-written
change instead of re-implementing it."_ If the human chooses this:
1. Still **amend the intent** in the spec so it unambiguously says X (step 4
above is unchanged — the frozen intent must match the restored code).
2. Add **`"restore_patch": "<the saved patch path>"`** to `resolution.json`
(copy the path verbatim from the escalation detail / triage log).

The orchestrator then re-arms the spec to `in-review`, re-applies the patch onto
the baseline, and re-dispatches — the session resumes at the review step on the
restored diff. **Do NOT `git apply` the patch yourself and do NOT set the spec
status** — the orchestrator does both deterministically at re-arm.

If the attempted reading was wrong (the common case), omit `restore_patch`
entirely: the orchestrator re-drives from scratch against the corrected intent.

## What you MUST NOT do

- **Do NOT** write the orchestrator's `result.json` — that is a dev/review
Expand Down
13 changes: 8 additions & 5 deletions src/bmad_loop/devcontract.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,14 @@
# allowlist: anything else (already-`done`, `blocked`, or an unknown custom token)
# is left untouched, so reconciliation can never override a status the skill set on
# purpose. `""` covers a blank or missing frontmatter `status:` — `reset_spec_status`
# fills/inserts the line in that case. `in-review` is included because on the sole
# (generic `bmad-dev-auto`) path it is only ever the transient marker step-04 sets at
# its start; the skill self-finalizes to `done`. The legacy `bmad-loop-dev` fork that
# used `in-review` as a deliberate review-handoff terminal is retired, so nothing
# leaves `in-review` on purpose anymore.
# fills/inserts the line in that case. `in-review` is included because step-04 sets
# it transiently at the start of a review pass; the skill self-finalizes to `done`,
# so a spec left AT `in-review` with a prose `done` result is a mid-review interrupt
# safe to reconcile forward. The intent-gap patch-restore re-drive (BMAD-METHOD
# #2564) deliberately re-arms the spec TO `in-review` before a session (so step-01
# routes straight to step-04 on the restored diff) — but that is the pre-session
# status the re-driven skill then advances past; it never LEAVES a spec at
# `in-review` as a terminal, so the reconcile allowlist semantics are unchanged.
RECONCILABLE_FROM = frozenset({"", "draft", "ready-for-dev", "in-progress", "in-review"})

# The leading `---\n …frontmatter… \n---` block, captured in three parts so the
Expand Down
Loading
Loading