Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"name": "bmad-loop",
"source": "./src/bmad_loop/data/skills",
"description": "Automation-mode skills driven by the bmad-loop orchestrator: interactive escalation resolution (bmad-loop-resolve) and deferred-work sweep triage (bmad-loop-sweep) — the inner dev primitive (which self-reviews and commits) is the upstream bmad-dev-auto skill",
"version": "0.8.0",
"version": "0.8.1",
"author": {
"name": "pinkyd"
},
Expand Down
45 changes: 45 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,50 @@ 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.

## [0.8.1] — 2026-07-05

### Fixed

- **A session that finished its work but crashed before the run recorded it no longer loses that work
to a restart-and-rollback.** The engine now records a completed dev/review session behind a
durability barrier _before_ running post-session hooks (usage is folded in afterward as best-effort
metadata); on resume it consumes that durably-recorded result straight into verify/decide instead of
restarting the attempt from baseline. A raw `KeyboardInterrupt` now records a controlled stop, and
replay preserves the attempt/cycle/baseline. (#62, closes #57)
- **Several narrower resume-replay edges opened by that durability work are closed.** A host death in
the post-session window of the _last_ allowed review cycle no longer drops a recorded clean review to
a defer; a reconcile early-return no longer persists a pre-reconcile dict that could silently skip a
recommended follow-up review; and triage/sweep and labeled plugin-workflow sessions no longer persist
large, never-consumed result payloads into `state.json`. (#63)
- **A dev session is now completed only on a real Stop or window death, never on a terminal artifact
glimpsed while the agent window is still alive.** The idle-tick and grace-expiry shortcuts could
return "completed" mid-turn and let the run's cleanup kill a working agent, handing the engine
half-written state. Both shortcuts are removed, liveness is re-probed immediately before the
grace-expiry stall verdict, and a re-driven spec has its stale `## Auto Run Result` section stripped
on both re-arm paths so a resolved re-drive can't be misread as its own terminal result. Result scans
are now fence-aware, so an `## Auto Run Result` heading quoted inside a fenced code block is never
mistaken for a real section (nor destructively stripped). (#53, closes #48 #52)
- **Long journal fields in the TUI now wrap with a hanging indent instead of spilling back under the
timestamp and kind columns.** Each row renders as a fixed-width grid whose fields cell folds within
its own column.

### Added

- **Attempt-preserve recovery refs are now bounded.** With `scm.rollback_on_failure` on, auto-rollback
parks a failed attempt's committed work under `attempt-preserve/*` and its dirty worktree snapshot
under `attempt-preserve-dirty/*`; nothing pruned them, so they grew unbounded. Run start now keeps
only the newest `scm.preserve_keep` (default 20, `0` = never prune) per family by committer date and
best-effort-deletes the tail — a stuck ref never wedges the ones behind it, and prune failures are
journaled but never block the run. (#50 #54, closes #32 #49)

### Changed

- **The adapter shell-dialect seam is now an explicit, documented contract.** `new_window` /
`new_parked_window` factor their shell-dialect fragments into overridable hooks (POSIX output stays
byte-identical), and the `command` parameter's semantics are pinned in the ABC docstring
(shlex-joined argv; operator handling is backend-defined). Relevant only to authors porting the
adapter to a non-POSIX backend. (#47 #60)

## [0.8.0] — 2026-07-03

### Changed
Expand Down Expand Up @@ -945,6 +989,7 @@ enforced in CI.
implementation phase, driven by a Python control loop with hook-based session transport and
resumable on-disk run state.

[0.8.1]: https://github.com/bmad-code-org/bmad-loop/releases/tag/v0.8.1
[0.8.0]: https://github.com/bmad-code-org/bmad-loop/releases/tag/v0.8.0
[0.7.12]: https://github.com/bmad-code-org/bmad-auto/releases/tag/v0.7.12
[0.7.11]: https://github.com/bmad-code-org/bmad-auto/releases/tag/v0.7.11
Expand Down
Binary file modified docs/images/dashboard.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
232 changes: 116 additions & 116 deletions docs/images/dashboard.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/decision-answer.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
244 changes: 122 additions & 122 deletions docs/images/decision-answer.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/deferred-modal.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
240 changes: 120 additions & 120 deletions docs/images/deferred-modal.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/settings-scm.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
208 changes: 104 additions & 104 deletions docs/images/settings-scm.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
192 changes: 96 additions & 96 deletions docs/images/settings.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/start-run-modal.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
262 changes: 131 additions & 131 deletions docs/images/start-run-modal.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/sweep-decision.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
234 changes: 117 additions & 117 deletions docs/images/sweep-decision.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion module.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
code: bmad-loop
name: BMAD Loop Skills
description: "Automation-mode skills driven by the bmad-loop orchestrator: interactive escalation resolution (bmad-loop-resolve) and deferred-work sweep triage (bmad-loop-sweep) — the inner dev primitive (which self-reviews and commits) is the upstream bmad-dev-auto skill"
module_version: 0.8.0
module_version: 0.8.1
default_selected: false
module_greeting: >
BMAD Loop installed — both the automation skills and the
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "bmad-loop"
version = "0.8.0"
version = "0.8.1"
description = "Deterministic ralph-loop orchestrator for the BMAD implementation phase"
readme = "README.md"
license = "MIT"
Expand Down
2 changes: 1 addition & 1 deletion src/bmad_loop/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@
spec files, and the per-run directory under .bmad-loop/runs/.
"""

__version__ = "0.8.0"
__version__ = "0.8.1"
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
code: bmad-loop
name: BMAD Loop Skills
description: "Automation-mode skills driven by the bmad-loop orchestrator: interactive escalation resolution (bmad-loop-resolve) and deferred-work sweep triage (bmad-loop-sweep) — the inner dev primitive (which self-reviews and commits) is the upstream bmad-dev-auto skill"
module_version: 0.8.0
module_version: 0.8.1
default_selected: false
module_greeting: >
BMAD Loop installed — both the automation skills and the
Expand Down
26 changes: 19 additions & 7 deletions src/bmad_loop/tui/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from pathlib import Path
from typing import Any

from rich.table import Table
from rich.text import Text
from textual.selection import Selection
from textual.widgets import RichLog, Static, Tree
Expand Down Expand Up @@ -166,23 +167,34 @@ def show_run(
# the journal -> log jump, not the human
_JOURNAL_HIDDEN_FIELDS = ("ts", "kind", "log_task", "log_pos")

# Row-grid geometry. The fields column's left edge sits at
# _JOURNAL_CLOCK_WIDTH + _JOURNAL_COL_PAD + _JOURNAL_KIND_WIDTH + _JOURNAL_COL_PAD;
# the hanging-indent test derives its indent from the same constants so the two
# can't silently drift apart.
_JOURNAL_CLOCK_WIDTH = 8
_JOURNAL_KIND_WIDTH = 24
_JOURNAL_COL_PAD = 1 # per-column right pad in the row grid

def journal_line(entry: dict[str, Any]) -> Text:

def journal_line(entry: dict[str, Any]) -> Table:
kind = str(entry.get("kind", "?"))
style = next((s for sub, s in _JOURNAL_STYLES if sub in kind), "dim")
ts = entry.get("ts")
clock = ""
if isinstance(ts, (int, float)):
clock = time.strftime("%H:%M:%S", time.localtime(ts))
text = Text()
text.append(f"{clock:8s} ", style="dim")
text.append(f"{kind:24s}", style=style)
fields = " ".join(
f"{k}={_short(v)}" for k, v in entry.items() if k not in _JOURNAL_HIDDEN_FIELDS
)
if fields:
text.append(" " + fields)
return text
# A grid per row so the fields cell folds within its own column (hanging
# indent) instead of wrapping back under the clock/kind columns. A long kind
# likewise folds within its own column rather than spilling into the fields.
grid = Table.grid(padding=(0, _JOURNAL_COL_PAD, 0, 0))
grid.add_column(width=_JOURNAL_CLOCK_WIDTH)
grid.add_column(width=_JOURNAL_KIND_WIDTH, overflow="fold")
grid.add_column(overflow="fold")
grid.add_row(Text(clock, style="dim"), Text(kind, style=style), Text(fields))
return grid


class JournalEntryOption(Option):
Expand Down
44 changes: 42 additions & 2 deletions tests/test_tui_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import pytest
from conftest import install_bmad_config, write_sprint
from rich.console import Console
from rich.text import Text
from textual.geometry import Offset
from textual.selection import Selection
Expand Down Expand Up @@ -39,7 +40,15 @@
StartSweepModal,
TextOutputModal,
)
from bmad_loop.tui.widgets import RunHeader, SelectableRichLog, SprintTree
from bmad_loop.tui.widgets import (
_JOURNAL_CLOCK_WIDTH,
_JOURNAL_COL_PAD,
_JOURNAL_KIND_WIDTH,
RunHeader,
SelectableRichLog,
SprintTree,
journal_line,
)


def make_run(
Expand Down Expand Up @@ -270,7 +279,14 @@ async def test_token_weight_falls_back_to_default(project):


def journal_rows(journal: OptionList) -> list[str]:
return [str(journal.get_option_at_index(i).prompt) for i in range(journal.option_count)]
# Journal prompts are Rich Table grids, so render them to plain text.
console = Console(width=400)
rows = []
for i in range(journal.option_count):
with console.capture() as capture:
console.print(journal.get_option_at_index(i).prompt)
rows.append(capture.get())
return rows


def log_text(screen: DashboardScreen) -> str:
Expand All @@ -296,6 +312,30 @@ def has_entry() -> bool:
assert any("1-2-search" in row for row in journal_rows(journal))


def test_journal_line_wraps_fields_with_hanging_indent():
entry = {
"ts": 1_750_000_000,
"kind": "session-start",
"task_id": "6-1-sound-as-information-audio-layer-dev-1",
"role": "dev",
"prompt": "/bmad-dev-auto 6-1-sound-as-information-audio-layer",
}
console = Console(width=60)
with console.capture() as capture:
console.print(journal_line(entry))
lines = capture.get().splitlines()
assert len(lines) > 1 # fields are long enough to wrap at width 60
assert "session-start" in lines[0]
# continuation lines stay in the fields column, never spilling back under
# the clock/kind columns. The fields column's left edge is derived from the
# same width constants journal_line lays the grid out with.
indent = _JOURNAL_CLOCK_WIDTH + _JOURNAL_COL_PAD + _JOURNAL_KIND_WIDTH + _JOURNAL_COL_PAD
for line in lines[1:]:
assert line[:indent] == " " * indent
# and the wrapped fields carry real content past the indent
assert any(line[indent:].strip() for line in lines[1:])


async def test_log_pane_shows_emulated_content(project):
from test_tui_data import ink_stream

Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading