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
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,23 @@ breaking changes may land in a minor release.

### Fixed

- **Path-resolution refusals no longer tear down worktree runs mid-provisioning (#556).**
Observation probes report coarse unknown entries, each refused seed is skipped independently,
provisioning-root uncertainty raises a typed escalation and pauses before any write or session,
and mount uncertainty defers only the affected unit. Required upstream-skill absence still
escalates; this boundary does not include `ProjectPaths` or confined cleanup resolution.

- **Rollback path uncertainty now triggers a pre-destructive rollback pause (#557).** Trusted
spec containment fails closed, observational exclude and story derivation use explicit
fail-open or empty fallbacks, collision uncertainty keeps and escalates the branch, and exact
commits omit only an uncertain candidate after a trusted root is established. This is limited
to the audited verify and recovery boundaries, not every path-resolution call.

- **The dashboard now survives project-root resolution refusal with unavailable panes (#558).**
Startup and polling retain empty or unavailable views through one stable lexical/canonical
cache spelling. This preserves the dashboard around a dead provider; it does not recover the
provider or make it operational.

- **Advancing a sprint board now preserves every authored line ending (#576).** A valid UTF-8
board keeps each CRLF, LF, bare-CR or mixed per-line terminator while the requested story,
conditional parent-epic and optional `last_updated` values still change. Previously the
Expand Down
13 changes: 10 additions & 3 deletions src/bmad_loop/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -5287,12 +5287,19 @@ def _carry_isolated_ledger_writes(self, task: StoryTask) -> None:
self._carry_board_advance(task)

def _harvest_carry_commit_may_degrade(self, ledger: Path) -> bool:
"""Whether a carry may remain uncommitted because git cannot own its path."""
"""Whether a carry may remain uncommitted because git cannot own its path.

Only a path proven external may degrade. Resolution uncertainty must keep
the durable commit-pending latch set rather than guess that git cannot own
a possibly tracked ledger and silently disable its retry.
"""
repo = self.paths.repo_root
try:
rel = ledger.resolve().relative_to(repo.resolve()).as_posix()
except (OSError, RuntimeError, ValueError):
return True # external or unresolvable ledgers are advisory artifacts
except (OSError, RuntimeError):
return False
except ValueError:
return True # a proven external ledger is an advisory artifact
if verify.path_tracked(repo, rel):
return False
return rel not in verify.untracked_files(repo)
Expand Down
8 changes: 7 additions & 1 deletion src/bmad_loop/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -1300,7 +1300,13 @@ def _walk_traversable_files(
an unreadable source directory never leaves an empty destination behind.
"""
if _is_dir(src):
real = str(src.resolve()) if isinstance(src, Path) else None
try:
real = str(src.resolve()) if isinstance(src, Path) else None
except (OSError, RuntimeError):
if not _suppress_errors:
raise
yield rel, src
return
if real is not None and real in _seen:
return
if _should_descend is not None and not _should_descend(rel, src):
Expand Down
27 changes: 19 additions & 8 deletions src/bmad_loop/recovery_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,15 +230,26 @@ def safe_reset(self, task: StoryTask, *, preserve: tuple[str, ...] = ()) -> None
pause. The BMAD artifact folders are always kept from untracked deletion;
``preserve`` (set only on a resolved re-drive) additionally keeps their
*tracked* content alive through the reset, so a just-corrected spec is not
reverted. Sweep passes no ``preserve`` — it wants the broken ledger gone."""
reverted. Sweep passes no ``preserve`` — it wants the broken ledger gone.
A cleanup-preflight refusal is journaled and routed through the injected
pause before the re-drive can continue."""
workspace = self._workspace_get()
verify.safe_rollback(
workspace.root,
task.baseline_commit or "",
baseline_untracked=task.baseline_untracked,
keep=(".bmad-loop", *self.protected_relpaths()),
preserve=preserve,
)
try:
verify.safe_rollback(
workspace.root,
task.baseline_commit or "",
baseline_untracked=task.baseline_untracked,
keep=(".bmad-loop", *self.protected_relpaths()),
preserve=preserve,
)
except verify.RollbackPreflightError as e:
self.journal.append("rollback-reset-failed", story_key=task.story_key, error=str(e))
self._pause(
f"automatic rollback for {task.story_key} could not safely start: {e}. "
"Fix the underlying filesystem fault, then resume the run.",
task.story_key,
cause=e,
)

def restore_patch(self, task: StoryTask) -> None:
"""Re-apply the latched intent-gap patch (BMAD-METHOD #2564) onto the
Expand Down
3 changes: 2 additions & 1 deletion src/bmad_loop/tui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
PAUSE_STORY_GATE,
RunState,
)
from ..platform_util import resolve_or_lexical
from ..policy import POLICY_FILE
from ..process_host import ProcessHostError
from ..runs import RUNS_DIR, RearmError, StopRunError
Expand Down Expand Up @@ -144,7 +145,7 @@ class BmadLoopApp(App[None]):

def __init__(self, project: Path):
super().__init__()
self.project = project.resolve()
self.project = resolve_or_lexical(project)
self.sub_title = str(self.project)
self._dashboard = DashboardScreen(self.project)

Expand Down
5 changes: 3 additions & 2 deletions src/bmad_loop/tui/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from ..gates import ATTENTION_FILE
from ..journal import JOURNAL_FILE, LOGS_DIR, STATE_FILE, load_state
from ..model import RunState
from ..platform_util import resolve_or_lexical
from ..process_host import ProcessHostError
from ..runs import (
STOP_REQUEST_FILE,
Expand Down Expand Up @@ -811,7 +812,7 @@ def pending_decision(journal_entries: list[dict[str, Any]]) -> tuple[str, str] |
def _project_paths(project: Path) -> bmadconfig.ProjectPaths | None:
"""BMAD artifact paths, stat-gated on config.yaml; None when the project
is not initialized (or the config is unreadable)."""
project = project.resolve()
project = resolve_or_lexical(project)
config_sig = _stat_sig(project / "_bmad" / "bmm" / "config.yaml")
cached_paths = _paths_cache.get(project)
if config_sig is not None and cached_paths is not None and cached_paths[0] == config_sig:
Expand Down Expand Up @@ -946,7 +947,7 @@ def pending_missed_decisions(project: Path) -> list:
paths = _project_paths(project)
if paths is None:
return []
project = project.resolve()
project = paths.project
sig = (
_stat_sig(paths.deferred_work),
_stat_sig(decisions.store_path(project)),
Expand Down
Loading