Skip to content

feat(model): add awaiting-operator phase, sprint token, and surfaces (inert) - #347

Open
pbean wants to merge 1 commit into
mainfrom
feat/awaiting-operator-vocabulary
Open

feat(model): add awaiting-operator phase, sprint token, and surfaces (inert)#347
pbean wants to merge 1 commit into
mainfrom
feat/awaiting-operator-vocabulary

Conversation

@pbean

@pbean pbean commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

refs #335 (1/4)

Part 1 of the four-PR awaiting-operator program: vocabulary only. It names the state at every layer and wires the display surfaces, but ships no writer — nothing in the engine can reach the phase after this PR. The park path is part 2, bmad-loop confirm is part 3, review-demotion composition is part 4.

Does not close #335.

What lands

Layer Change
model.py Phase.AWAITING_OPERATOR + TERMINAL_PHASES; StoryTask.operator_actions: list[str] with to_dict/from_dict
statemachine.py one new edge: COMMITTING → AWAITING_OPERATOR (plus the phase's own empty outgoing set)
sprintstatus.py awaiting-operator in STATUS_ORDER, immediately before done; not in ACTIONABLE_STATUSES; drops the dead STORY_STATUSES set
engine.py RunSummary.awaiting_operator + the summary() count + a render clause
cli.py status phase column widened 16 → 17 (awaiting-operator is the longest phase token)
tui/widgets.py SPRINT_* / STORY_* glyph+style entries (, yellow) and the run-header count

Design notes

Terminal, not paused. A story owing external human actions must not block the stories behind it, so it is terminal like DONE rather than run-halting like the spec's Block If:blocked → CRITICAL → pause channel. Terminal says nothing about success: DONE and AWAITING_OPERATOR carry a commit, DEFERRED and ESCALATED do not.

Reachable only from COMMITTING. Parking rides the normal commit path — the work commits, then the final phase is chosen by whether the task carries operator_actions. Making COMMITTING the sole inbound edge is what stops a future park from skipping the gates and the commit a DONE story clears.

Token position is the whole contract. Sitting immediately below done means part 3's confirm is an ordinary forward advance() — no exception to the sole-writer invariant — while done → awaiting-operator is refused by the existing never-regress guard.

documents.py needed no edit, despite being on the plan's touch list: the --json projection is already phase-agnostic (str(task.phase); there are no aggregate per-phase counts in the document). Schema stays at 1. A test pins that claim rather than leaving it as an assertion in a PR body.

Two consequences worth reviewing

  1. state.json is forward-only. A run that records the new phase is rejected by an older binary. Standard for phase additions; noted in the CHANGELOG.

  2. One real behavior change, and it is deliberate. The sprint token is now ordered rather than unknown, so verify._is_signoff_regression (Escalate deliberate sprint-status regressions at review-verify instead of burning review cycles #334) classifies a review writing awaiting-operator onto a board the orchestrator had already advanced to done as a deliberate sign-off regression → escalate, where it previously fell through to a retry. Until the park path exists nothing legitimately writes that token, so a review that writes it has revoked a sign-off — and this is exactly part 4's default operator.on_review_demotion = escalate, arriving early. This flipped an existing parametrized case in test_verify.py that used awaiting-operator as its example of an out-of-STATUS_ORDER token; that case now uses a genuinely unknown token, and the new semantics gets its own test.

Tests

Statemachine (park path, reachable-only-from-COMMITTING, and a new TERMINAL_PHASES == dead-ends guard — the two spellings of terminality live in different modules with nothing previously linking them); model round-trip incl. pre-upgrade state.json defaults and no shared mutable default; sprint token ordering, confirm-forward and never-regress-into; parked story never picked / never targetable; summary counts and render; status text + --json; TUI glyph coverage and header count.

Ablations run (repo rule) — each confirmed to make the named test fail, then reverted:

# Ablation Test that must break
1 add the token to ACTIONABLE_STATUSES (inverse form — the gate is an absence) never-picked + never-targetable
2 drop the token from STATUS_ORDER advance never-regresses-done-into-parked
3 add an extra inbound edge from DEV_VERIFY reachable-only-from-COMMITTING
4 drop the phase from TERMINAL_PHASES terminal-phases-are-the-dead-ends
5 make the summary clause unconditional render-only-when-non-zero
6 revert the phase column to :16s status column alignment

Ablation 6 caught a real defect in my own test first: the original assertion used _story_row, which splits on whitespace and is therefore blind to column shift, so it passed under ablation. Rewritten to compare two rows' dev× offsets, which does bite.

uv run pytest -q -n auto → 3467 passed, 24 skipped. Full trunk check clean; pyright@1.1.411 0 errors.

Summary by CodeRabbit

  • New Features
    • Added an awaiting-operator workflow phase for tasks requiring manual action.
    • Preserved operator instructions when pausing and resuming tasks.
    • Added status summaries and terminal UI indicators for awaiting-operator tasks.
    • Updated sprint progression to prevent paused tasks from being selected for new work.
  • Bug Fixes
    • Improved status display alignment for the new phase.
    • Prevented completed tasks from regressing to awaiting-operator.
  • Documentation
    • Documented workflow behavior, status ordering, and state compatibility considerations.

Names, at every layer, the state a story reaches when its agent-doable work
is finished and committed but its acceptance criteria include external
actions only a human can perform. Nothing writes any of it yet: the park
path is part 2 of the program, so no run can currently reach the phase.

- Phase.AWAITING_OPERATOR, terminal, reachable only from COMMITTING so a
  park can never skip the gates and the commit a DONE story clears
- sprint token ordered immediately before `done` — confirming a parked
  story is then a forward advance through the sole writer, and nothing can
  regress `done` back into it — and deliberately not actionable
- StoryTask.operator_actions, round-tripped through state.json
- run-summary count, `status` text + --json, TUI glyphs and header count
- drops the dead STORY_STATUSES set (no readers since STATUS_ORDER landed)

refs #335 (1/4)
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Changes

Awaiting-operator lifecycle

Layer / File(s) Summary
Lifecycle state and persistence contract
src/bmad_loop/model.py, src/bmad_loop/statemachine.py, tests/test_model.py, tests/test_statemachine.py
Adds the terminal AWAITING_OPERATOR phase, restricts entry to COMMITTING, and persists operator_actions with legacy defaults.
Sprint ordering and actionability
src/bmad_loop/sprintstatus.py, tests/test_sprintstatus*.py, tests/test_verify.py, CHANGELOG.md
Places awaiting-operator before done, excludes it from actionable selection, validates forward advancement, and classifies late board writes as regressions.
Summary, status, and UI surfaces
src/bmad_loop/engine.py, src/bmad_loop/cli.py, src/bmad_loop/tui/widgets.py, tests/test_engine.py, tests/test_cli.py, tests/test_tui_app.py
Reports parked-task counts and renders the new phase in CLI and TUI outputs with aligned columns, pause glyphs, and yellow styling.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: dracic

Poem

A rabbit pauses by the gate,
With operator notes to wait.
The phase turns yellow, calm and bright,
While done stays done and paths stay right.
“Hop back when sign-off’s in sight!”

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR adds the new state and surface updates, but omits required issue #335 items like the confirm command, registry, and review-demotion handling. Implement the missing #335 requirements or narrow the linked issue scope to match this PR’s vocabulary-and-surface-only work.
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the new awaiting-operator feature.
Out of Scope Changes check ✅ Passed All changes stay within the awaiting-operator feature, its rendering, persistence, and tests.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/awaiting-operator-vocabulary

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CHANGELOG.md`:
- Around line 12-25: In the CHANGELOG entry describing the sprint-status token,
replace the article “a” before “awaiting-operator” with “an”; leave the
surrounding wording unchanged.

In `@tests/test_engine.py`:
- Around line 1196-1201: Update the StoryTask fixtures in both affected tests to
use valid AWAITING_OPERATOR states: populate representative outstanding
operator_actions for each parked task, and include a commit SHA wherever the
fixture represents committed work. Keep DONE fixtures unchanged and ensure the
tests no longer rely on the default empty action list.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 387401d0-baf7-4cb9-9e8e-494dcba6522d

📥 Commits

Reviewing files that changed from the base of the PR and between cf0d6ec and 18f9643.

📒 Files selected for processing (15)
  • CHANGELOG.md
  • src/bmad_loop/cli.py
  • src/bmad_loop/engine.py
  • src/bmad_loop/model.py
  • src/bmad_loop/sprintstatus.py
  • src/bmad_loop/statemachine.py
  • src/bmad_loop/tui/widgets.py
  • tests/test_cli.py
  • tests/test_engine.py
  • tests/test_model.py
  • tests/test_sprintstatus.py
  • tests/test_sprintstatus_advance.py
  • tests/test_statemachine.py
  • tests/test_tui_app.py
  • tests/test_verify.py

Comment thread CHANGELOG.md
Comment on lines +12 to +25
- **`awaiting-operator` vocabulary, no writer yet (#335, part 1 of 4).** Names, at every layer, the
state a story reaches when its agent-doable work is finished and committed but its acceptance
criteria include external actions only a human can perform: `Phase.AWAITING_OPERATOR` (terminal,
reachable only from `COMMITTING`), a `awaiting-operator` sprint-status token ordered immediately
before `done` and deliberately not actionable, `operator_actions` on each task, and the matching
run-summary count, `status` output and TUI glyphs. Nothing writes any of it yet — the park path
is part 2 — so no run can currently reach the phase.

Two consequences worth knowing. A `state.json` carrying the new phase is forward-only: an older
binary rejects it, as with every phase addition. And because the sprint token is now _ordered_
rather than unknown, a review session writing `awaiting-operator` onto a board the orchestrator
had already advanced to `done` is classified as a deliberate sign-off regression and escalates
(#334), where it previously fell through to a retry.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the article before awaiting-operator.

Line 15 reads “a awaiting-operator sprint-status token”; use “an” instead.

Proposed wording
-  a `awaiting-operator` sprint-status token
+  an `awaiting-operator` sprint-status token
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- **`awaiting-operator` vocabulary, no writer yet (#335, part 1 of 4).** Names, at every layer, the
state a story reaches when its agent-doable work is finished and committed but its acceptance
criteria include external actions only a human can perform: `Phase.AWAITING_OPERATOR` (terminal,
reachable only from `COMMITTING`), a `awaiting-operator` sprint-status token ordered immediately
before `done` and deliberately not actionable, `operator_actions` on each task, and the matching
run-summary count, `status` output and TUI glyphs. Nothing writes any of it yet — the park path
is part 2 — so no run can currently reach the phase.
Two consequences worth knowing. A `state.json` carrying the new phase is forward-only: an older
binary rejects it, as with every phase addition. And because the sprint token is now _ordered_
rather than unknown, a review session writing `awaiting-operator` onto a board the orchestrator
had already advanced to `done` is classified as a deliberate sign-off regression and escalates
(#334), where it previously fell through to a retry.
- **`awaiting-operator` vocabulary, no writer yet (`#335`, part 1 of 4).** Names, at every layer, the
state a story reaches when its agent-doable work is finished and committed but its acceptance
criteria include external actions only a human can perform: `Phase.AWAITING_OPERATOR` (terminal,
reachable only from `COMMITTING`), a `awaiting-operator` sprint-status token ordered immediately
before `done` and deliberately not actionable, `operator_actions` on each task, and the matching
run-summary count, `status` output and TUI glyphs. Nothing writes any of it yet — the park path
is part 2 — so no run can currently reach the phase.
Two consequences worth knowing. A `state.json` carrying the new phase is forward-only: an older
binary rejects it, as with every phase addition. And because the sprint token is now _ordered_
rather than unknown, a review session writing `awaiting-operator` onto a board the orchestrator
had already advanced to `done` is classified as a deliberate sign-off regression and escalates
(`#334`), where it previously fell through to a retry.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CHANGELOG.md` around lines 12 - 25, In the CHANGELOG entry describing the
sprint-status token, replace the article “a” before “awaiting-operator” with
“an”; leave the surrounding wording unchanged.

Comment thread tests/test_engine.py
Comment on lines +1196 to +1201
for key, phase in (
("1-1-a", Phase.DONE),
("1-2-b", Phase.AWAITING_OPERATOR),
("1-3-c", Phase.AWAITING_OPERATOR),
):
engine.state.tasks[key] = StoryTask(story_key=key, epic=1, phase=phase)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Use valid AWAITING_OPERATOR fixtures.

Both tests rely on the default empty operator_actions list, even though the model contract defines a parked task by its outstanding human actions. Populate representative actions (and a commit SHA where the fixture models committed work) so these tests cannot pass with an invalid parked state.

Example adjustment
-        ("1-2-b", Phase.AWAITING_OPERATOR),
-        ("1-3-c", Phase.AWAITING_OPERATOR),
+        ("1-2-b", Phase.AWAITING_OPERATOR, ["publish the required record"]),
+        ("1-3-c", Phase.AWAITING_OPERATOR, ["complete the external setup"]),

Also applies to: 1217-1219

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_engine.py` around lines 1196 - 1201, Update the StoryTask fixtures
in both affected tests to use valid AWAITING_OPERATOR states: populate
representative outstanding operator_actions for each parked task, and include a
commit SHA wherever the fixture represents committed work. Keep DONE fixtures
unchanged and ensure the tests no longer rely on the default empty action list.

@greptile-apps

greptile-apps Bot commented Jul 28, 2026

Copy link
Copy Markdown

Greptile Summary

This PR introduces the awaiting-operator vocabulary across every layer of the stack — model, state machine, sprint status, engine summary, CLI output, and TUI — without shipping a writer. No run can currently reach the new phase; the park path arrives in part 2.

  • Model layer: Phase.AWAITING_OPERATOR (StrEnum value "awaiting-operator") is added as a terminal phase; TERMINAL_PHASES is updated; StoryTask.operator_actions: list[str] is added with correct default_factory, to_dict/from_dict round-trip, and backward-compatible deserialization from pre-upgrade state.json.
  • Sprint status: awaiting-operator is inserted immediately before done in STATUS_ORDER (making confirmation a legal forward advance) and deliberately excluded from ACTIONABLE_STATUSES (preventing re-drive of committed work). The dead STORY_STATUSES set is removed.
  • One intentional behavior change: placing the token in STATUS_ORDER means a review session writing awaiting-operator onto a board already at done is now classified as a deliberate sign-off regression (escalate), where it previously fell through to a retry as an unknown token.

Confidence Score: 5/5

Vocabulary-only change; no writer exists yet so no run can reach the new phase in this release.

Every changed layer is consistent and the cross-module invariants (TERMINAL_PHASES vs. transition table dead-ends, STATUS_ORDER position, column width) are each pinned by a dedicated test. The one real behavior change is intentional, well-documented, and has its own targeted test. The operator_actions field uses default_factory, deserializes defensively, and round-trips correctly from pre-upgrade state.

Files Needing Attention: No files require special attention.

Important Files Changed

Filename Overview
src/bmad_loop/model.py Adds Phase.AWAITING_OPERATOR to the Phase StrEnum, updates TERMINAL_PHASES to include it, and adds operator_actions list to StoryTask with correct default_factory, serialization, and backward-compatible deserialization.
src/bmad_loop/statemachine.py Adds Phase.AWAITING_OPERATOR as an outgoing option from COMMITTING and registers it in TRANSITIONS with an empty outgoing set (terminal). The sole-inbound-edge design is enforced by the new parametrized test.
src/bmad_loop/sprintstatus.py Inserts awaiting-operator immediately before done in STATUS_ORDER, deliberately excludes it from ACTIONABLE_STATUSES, and removes the now-dead STORY_STATUSES set.
src/bmad_loop/engine.py Adds awaiting_operator: int = 0 to RunSummary (correctly defaulted) and renders it conditionally in render() between escalated and tokens.
src/bmad_loop/cli.py Widens the phase column format from :16s to :17s to accommodate the new longest token awaiting-operator (17 chars).
src/bmad_loop/tui/widgets.py Adds awaiting-operator glyph and style to both SPRINT and STORY maps; adds a conditional awaiting N count to RunHeader shown only when non-zero.
tests/test_statemachine.py Adds dead-end/TERMINAL_PHASES cross-module consistency guard, full park-path sequence, and parametrized single-inbound-edge invariant.
tests/test_verify.py Updates the unrecognized-token retry case to use a genuinely-unknown token and adds a test verifying awaiting-operator now triggers the sign-off regression path.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    PENDING --> DEV_RUNNING
    DEV_RUNNING --> DEV_VERIFY
    DEV_VERIFY --> COMMITTING
    DEV_VERIFY --> DEFERRED
    DEV_VERIFY --> ESCALATED
    REVIEW_RUNNING --> REVIEW_VERIFY
    REVIEW_VERIFY --> COMMITTING
    REVIEW_VERIFY --> DEFERRED
    REVIEW_VERIFY --> ESCALATED
    COMMITTING --> DONE
    COMMITTING --> ESCALATED
    COMMITTING -->|new edge| AWAITING_OPERATOR
    DONE:::terminal
    DEFERRED:::terminal
    ESCALATED:::terminal
    AWAITING_OPERATOR:::newTerminal
    classDef terminal fill:#6b7280,color:#fff,stroke:none
    classDef newTerminal fill:#ca8a04,color:#fff,stroke:none
Loading

Reviews (1): Last reviewed commit: "feat(model): add awaiting-operator phase..." | Re-trigger Greptile

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

awaiting-operator: terminal state for stories owing human external actions

1 participant