Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
1f4510e
feat(stories): engine, adapter + HITL checkpoints for folder+id dispa…
pbean Jul 6, 2026
9c754ab
fix(verify): port stories proof-of-work exclusions onto the file-gran…
pbean Jul 7, 2026
d3af212
fix(stories): stop a bare resume leapfrogging a wedged story (MAJOR-A)
pbean Jul 7, 2026
5aeed94
fix(stories): a spec_checkpoint story can never commit without a plan…
pbean Jul 7, 2026
38848fd
fix(stories): withhold story-spec env from injected workflow sessions…
pbean Jul 7, 2026
d89ab4a
fix(runs): gate sentinel-clear on stories mode; snapshot baseline aft…
pbean Jul 7, 2026
e410408
feat(stories): shared status-projection helpers + mode-aware status/d…
pbean Jul 7, 2026
ab10a0f
feat(cli): stories-aware validate + selector preflight + dry-run parity
pbean Jul 7, 2026
4804774
feat(resolve): stories escalation context + SKILL sentinel guidance
pbean Jul 7, 2026
d7748a8
feat(stories): engine polish — selector/sentinel/max-stories (item 10)
pbean Jul 7, 2026
80c9f5e
test(stories): re-add PR #65 review-fix regression tests after phase1…
pbean Jul 7, 2026
ee35c83
fix(install): catch UnicodeDecodeError in stories-support probe (C1)
pbean Jul 7, 2026
f7fb6c5
fix(stories): launch-floor + ambiguous guard in adapter read-back (A1…
pbean Jul 7, 2026
2c6f39f
fix(engine): make --max-stories cap durable across pause/resume (A5)
pbean Jul 7, 2026
25297b5
fix(stories): clear sentinels by recorded verdict, not basename (C2, C3)
pbean Jul 7, 2026
cf50a8c
fix(cli): warn when --epic is passed with stories mode (A4)
pbean Jul 7, 2026
9e04256
fix(stories): catch UnicodeDecodeError in spec/sentinel read paths
pbean Jul 8, 2026
0f2b10d
fix(stories): convert non-UTF-8 manifest/spec reads to clean errors
pbean Jul 8, 2026
1b0c320
fix(stories): re-pause an in-run escalation on bare resume, never re-…
pbean Jul 8, 2026
a456381
fix(stories): sweep the last three UnicodeDecodeError read paths
pbean Jul 8, 2026
f9e466e
fix(stories): make the dry-run --max-stories cap count dispatchable s…
pbean Jul 8, 2026
4418245
fix(stories): fire the done_checkpoint even when the manifest goes un…
pbean Jul 8, 2026
69cf675
docs(resolve-skill): distinguish the multi-file ambiguous-id wedge fr…
pbean Jul 8, 2026
98e2546
test(engine): sprint-mode --max-stories durability across a pause/resume
pbean Jul 8, 2026
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
79 changes: 79 additions & 0 deletions src/bmad_loop/adapters/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,12 @@ def _artifact_dirs(self, cwd: Path) -> list[Path]:
return dirs

def _result_json(self, handle: SessionHandle, spec: SessionSpec, *, wait: bool) -> dict | None:
# Stories mode (folder+id dispatch): the story spec lives at a
# deterministic id-keyed path, so resolve it directly instead of the
# mtime-floor scan. The engine exports BMAD_LOOP_SPEC_FOLDER only for
# stories runs, so sprint/sweep runs keep the scan path below unchanged.
if spec.env.get("BMAD_LOOP_SPEC_FOLDER"):
return self._stories_result_json(handle, spec, wait=wait)
# Mirror the base _await_result poll: the skill's terminal spec may not be
# flushed to disk the instant the Stop event fires, so briefly await it when
# wait=True instead of reading once and mis-reporting a stall.
Expand All @@ -481,6 +487,79 @@ def _result_json(self, handle: SessionHandle, spec: SessionSpec, *, wait: bool)
return None
time.sleep(RESULT_POLL_S)

def _stories_result_json(
self, handle: SessionHandle, spec: SessionSpec, *, wait: bool
) -> dict | None:
"""Deterministic stories-mode read-back: resolve ``<spec-folder>/stories/
<id>-*.md`` by id (never the mtime scan) and synthesize from it.

``BMAD_LOOP_SPEC_FOLDER`` carries the project-relative (or absolute) spec
folder; rebase a relative one against ``spec.cwd`` exactly like
``_artifact_dirs`` so worktree isolation resolves inside the live checkout.
A PRESENT or SENTINEL spec synthesizes (a blocked sentinel becomes a
CRITICAL escalation → PAUSE, same as any block) — but only when the spec was
(re)written by THIS session: like the mtime-scan path's ``since_ns`` floor, a
spec whose mtime predates ``handle.launched_ns`` is a stale prior artifact
(e.g. the dev's ``done`` spec a follow-up review session re-opens) and must
not be read as this session's result. A still-PENDING spec, an AMBIGUOUS
match (>1 file — an anomaly no wait can collapse; ``_pick_next`` re-classifies
it into an actionable wedge), or a stale terminal spec → None (a result-less
Stop the dev-stall grace handles).

On a plan-halt leg (``BMAD_LOOP_PLAN_HALT`` set by the engine for a
spec_checkpoint story's first dispatch) the skill HALTs at
``ready-for-dev``; pass ``plan_halt=True`` so synthesize treats that as a
successful terminal (marked ``plan_halt``) rather than died-mid-flight."""
from .. import stories

story_key = spec.env.get("BMAD_LOOP_STORY_KEY") or ""
folder = Path(spec.env["BMAD_LOOP_SPEC_FOLDER"])
base = folder if folder.is_absolute() else Path(spec.cwd) / folder
plan_halt = bool(spec.env.get("BMAD_LOOP_PLAN_HALT"))
deadline = time.monotonic() + RESULT_GRACE_S
while True:
state = stories.resolve_story_spec(base, story_key)

@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/adapters/generic.py:510: _stories_result_json() no longer uses handle.launched_ns (unlike the mtime-scan path), so an already-terminal story spec from a previous step (notably the pre-existing done spec when starting a follow-up review session) could be misread as this session’s completion even if the session produced no new output.

Severity: high

Fix This in Augment

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in c0f6215. `_stories_result_json` now applies the same session-launch mtime floor the mtime-scan path uses (`st_mtime_ns >= handle.launched_ns`), so a spec left terminal by a prior step — notably the dev's `done` spec a follow-up review session re-opens — is no longer read as this session's completion. Confirmed reachable: the review session also runs `GenericDevAdapter` with `BMAD_LOOP_SPEC_FOLDER` set. Regression test added in tests/test_generic_tmux.py.

if state.kind == stories.KIND_AMBIGUOUS:
# >1 matching file — waiting can't make it collapse to one. Return now
# (don't burn the grace); the engine's next _pick_next re-classifies
# AMBIGUOUS and raises the actionable wedge for resolve.
return None
if (
state.kind in (stories.KIND_PRESENT, stories.KIND_SENTINEL)
and state.path
and self._written_this_session(state.path, handle.launched_ns)
):
try:
result_json = devcontract.synthesize_result(
state.path, story_key=story_key or None, plan_halt=plan_halt
).result_json
except UnicodeDecodeError:
# A non-UTF-8 read is either a torn glimpse of a spec still
# being written (keep polling — a later pass sees the finished
# write) or a genuinely corrupt file: then the grace expires
# result-less and the next _pick_next re-classifies it as a
# wedge (resolve_story_spec degrades an undecodable PRESENT
# spec to status "" → pause for resolve), never a crash of
# the read-back poll.
result_json = None
if result_json is not None:
return result_json
if not wait or time.monotonic() >= deadline:

@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/adapters/generic.py:515: If resolve_story_spec() yields KIND_AMBIGUOUS (multiple <id>-*.md matches), this currently just waits and then returns None, which will look like a generic stall rather than an actionable “ambiguous story file” error requiring operator intervention.

Severity: medium

Fix This in Augment

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in c0f6215. A `KIND_AMBIGUOUS` read-back now returns `None` immediately instead of polling the full grace; the engine's next `_pick_next` re-classifies AMBIGUOUS into an actionable wedge for resolve.

return None
time.sleep(RESULT_POLL_S)

@staticmethod
def _written_this_session(spec_path: Path, launched_ns: int) -> bool:
"""Whether ``spec_path`` was (re)written at/after the session launched — the
same launch-floor guard ``devcontract.find_result_artifact`` applies on the
scan path, so a stale terminal spec from a prior step (a dev ``done`` a
follow-up review re-opens) is not mistaken for this session's output. A spec
that vanished between resolve and stat is treated as not-yet-written."""
try:
return spec_path.stat().st_mtime_ns >= launched_ns
except OSError:
return False


# Back-compat alias: the adapter was ``GenericTmuxAdapter`` before tmux moved
# behind the multiplexer seam. Keeps existing imports stable.
Expand Down
Loading
Loading