Split out of #502 review (codex P2). Pre-existing shape, newly reachable.
action_review_pause routes PAUSE_STORY_GATE to _review_gate (src/bmad_loop/tui/app.py:546), which builds a SpecReviewModal titled "<label> — review the finalized spec" (:591-606). A story gate fires before the story is recorded in state.tasks — deliberately, so a resume re-picks it and re-asks the ledger (src/bmad_loop/engine.py:839) — so _paused_spec always returns (None, "") for it (app.py:783-793). The operator gets (no spec file resolved) / (empty spec) (src/bmad_loop/tui/screens/modals.py:474,478) under a title promising a spec, with Approve & resume as the only action; if the entry is still open, resuming re-pauses at the same gate.
The actionable payload is state.paused_reason — it names the blocking entries and the remedy (engine.py:939-943). _review_gate never renders it.
Scoping, because codex overstated this:
Fix shape: render paused_reason in _review_gate when there is no spec (or give the spec-less gates their own small viewer), which fixes epic-boundary at the same time.
No TUI test exercises a story-gate pause — tests/test_tui_app.py has no story-gate coverage; its SpecReviewModal tests all use a pause that has a real task.spec_file. Worth adding with the fix.
Split out of #502 review (codex P2). Pre-existing shape, newly reachable.
action_review_pauseroutesPAUSE_STORY_GATEto_review_gate(src/bmad_loop/tui/app.py:546), which builds aSpecReviewModaltitled"<label> — review the finalized spec"(:591-606). A story gate fires before the story is recorded instate.tasks— deliberately, so a resume re-picks it and re-asks the ledger (src/bmad_loop/engine.py:839) — so_paused_specalways returns(None, "")for it (app.py:783-793). The operator gets(no spec file resolved)/(empty spec)(src/bmad_loop/tui/screens/modals.py:474,478) under a title promising a spec, withApprove & resumeas the only action; if the entry is still open, resuming re-pauses at the same gate.The actionable payload is
state.paused_reason— it names the blocking entries and the remedy (engine.py:939-943)._review_gatenever renders it.Scoping, because codex overstated this:
gate:field + two validate checks #502. That branch does not touchtui/app.py;PAUSE_STORY_GATEwas already in the routing tuple and already badged (src/bmad_loop/tui/widgets.py:77). It was a reserved constant nothing raised, so the path was dead until feat(deferred): enforceable hard gates —gate:field + two validate checks #502 made it live.PAUSE_EPIC_BOUNDARYraises without astory_key(engine.py:5390-5394) and hits the identical spec-less path with a worse subtitle ("?",app.py:796). Story-gate at least renders<story-key> — <title>.src/bmad_loop/tui/widgets.py:224-228) and in the resume confirmation (modals.py:308-311) — just not in thepviewer. feat(deferred): enforceable hard gates —gate:field + two validate checks #502 documents that indocs/tui-guide.md:443.Fix shape: render
paused_reasonin_review_gatewhen there is no spec (or give the spec-less gates their own small viewer), which fixes epic-boundary at the same time.No TUI test exercises a story-gate pause —
tests/test_tui_app.pyhas nostory-gatecoverage; itsSpecReviewModaltests all use a pause that has a realtask.spec_file. Worth adding with the fix.