Skip to content

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

Merged
pbean merged 2 commits into
mainfrom
feat/awaiting-operator-vocabulary
Jul 28, 2026
Merged

feat(model): add awaiting-operator phase, sprint token, and surfaces (inert)#347
pbean merged 2 commits 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 that need human action, with persisted operator instructions.
    • Updated CLI/TUI status, run summaries, and sprint/stories boards to display parked awaiting-operator counts and the ⏸ indicator.
    • Adjusted sprint-status ordering so awaiting-operator is treated as a terminal “parked” state.
  • Bug Fixes

    • Fixed status table alignment for the longest phase token.
    • Prevented tasks from regressing from done back to awaiting-operator, and ensured parked tasks aren’t selectable for new work.
  • Documentation

    • Updated changelog notes covering state compatibility and the awaiting-operator regression behavior.

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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1512ab1e-be74-4913-b684-2119283e375f

📥 Commits

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

📒 Files selected for processing (3)
  • CHANGELOG.md
  • src/bmad_loop/model.py
  • tests/test_cli.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • tests/test_cli.py
  • CHANGELOG.md
  • src/bmad_loop/model.py

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

Sequence Diagram(s)

sequenceDiagram
  participant CommitFlow
  participant StoryTask
  participant Engine
  participant CLI
  participant TUI
  CommitFlow->>StoryTask: transition from COMMITTING
  StoryTask-->>Engine: expose AWAITING_OPERATOR phase
  Engine->>CLI: render parked-task count and phase
  Engine->>TUI: render awaiting count
  TUI-->>TUI: show ⏸ glyph with yellow styling
Loading

Possibly related PRs

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 [335] The PR only implements vocabulary, persistence, and display surfaces; required writer, validation, notification, and confirm workflows are still missing. Add the missing park/confirm flows, non-empty operator action enforcement, notifications, registry cleanup, and the remaining integrations required by #335.
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 clearly summarizes the main addition: the awaiting-operator phase and related surfaces.
Out of Scope Changes check ✅ Passed The changes stay within the awaiting-operator feature scope and its tests, docs, and display updates.
✨ 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 thread tests/test_engine.py
@greptile-apps

greptile-apps Bot commented Jul 28, 2026

Copy link
Copy Markdown

Greptile Summary

This PR introduces the awaiting-operator vocabulary at every layer of the stack — model, state machine, sprint status, engine, CLI, and TUI — as a new terminal phase for stories whose agent-doable work is committed but which still owe human-only external actions. No writer exists yet; nothing in the engine can reach the new phase after this PR.

  • Phase.AWAITING_OPERATOR added to model.py as a terminal phase (joining TERMINAL_PHASES), with a StoryTask.operator_actions: list[str] field that round-trips through state.json and defaults safely for pre-upgrade state.
  • State machine gains one edge (COMMITTING → AWAITING_OPERATOR) and a new dead-end entry; a cross-module invariant test pins TERMINAL_PHASES to the transition table's actual dead-ends so neither can drift silently.
  • One real behavior change (deliberate): awaiting-operator joining STATUS_ORDER means a review session that writes it onto a board the orchestrator already advanced to done is now classified as a sign-off regression (escalate), not an unknown-token retry.

Confidence Score: 5/5

Safe to merge — vocabulary-only change, no writer lands, the engine cannot reach the new phase after this PR.

Every layer is wired consistently: the phase enum, terminal-phases frozenset, transition table dead-end, sprint-status order, engine count, CLI column width, and TUI glyphs all agree. The cross-module invariant test ensures neither TERMINAL_PHASES nor the transition table can drift silently in future PRs. Backwards-compatibility is handled: state.json round-trips default empty operator_actions for pre-upgrade states, and the --json schema version stays at 1. No logic bugs or unsafe defaults found.

Files Needing Attention: No files require special attention.

Important Files Changed

Filename Overview
src/bmad_loop/model.py Adds Phase.AWAITING_OPERATOR enum member, expands TERMINAL_PHASES frozenset, and adds operator_actions list field to StoryTask with correct default_factory and safe from_dict deserialization.
src/bmad_loop/statemachine.py Adds COMMITTING → AWAITING_OPERATOR edge and AWAITING_OPERATOR dead-end entry; the new cross-module test pins TERMINAL_PHASES to the table's actual dead-ends.
src/bmad_loop/sprintstatus.py Inserts awaiting-operator into STATUS_ORDER immediately before done; removes the now-dead STORY_STATUSES set (confirmed unused); awaiting-operator is correctly absent from ACTIONABLE_STATUSES.
src/bmad_loop/engine.py Adds RunSummary.awaiting_operator count (with correct default=0 rationale), extends the summary() constructor, and appends the render clause only when non-zero.
src/bmad_loop/cli.py Widens the phase column from :16s to :17s; awaiting-operator is 17 chars and without the fix its row overflows, shifting all subsequent columns on that row.
src/bmad_loop/tui/widgets.py Adds ⏸/yellow glyph+style entries to SPRINT_GLYPHS/SPRINT_STYLES/STORY_GLYPHS/STORY_STYLES, and adds the conditional awaiting count to RunHeader; a new coverage test pins STATUS_ORDER ⊆ SPRINT_GLYPHS.
tests/test_statemachine.py Adds terminal-phases invariant test, park path sequence, and exhaustive reachability-only-from-COMMITTING parametrized test; all three are ablation-validated.
tests/test_verify.py Corrects an existing parametrized case (replaces awaiting-operator with a genuinely unknown token) and adds a new test asserting the now-ordered token is classified as a sign-off regression.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    PENDING --> DEV_RUNNING
    PENDING --> TRIAGE_RUNNING
    DEV_RUNNING --> DEV_VERIFY
    DEV_VERIFY --> DEV_RUNNING
    DEV_VERIFY --> REVIEW_RUNNING
    DEV_VERIFY --> COMMITTING
    DEV_VERIFY --> DEFERRED
    DEV_VERIFY --> ESCALATED
    REVIEW_RUNNING --> REVIEW_VERIFY
    REVIEW_VERIFY --> REVIEW_RUNNING
    REVIEW_VERIFY --> DEV_RUNNING
    REVIEW_VERIFY --> COMMITTING
    REVIEW_VERIFY --> DEFERRED
    REVIEW_VERIFY --> ESCALATED
    COMMITTING --> DONE
    COMMITTING --> ESCALATED
    COMMITTING -->|new: operator_actions present| AWAITING_OPERATOR
    TRIAGE_RUNNING --> TRIAGE_VERIFY
    TRIAGE_VERIFY --> TRIAGE_RUNNING
    TRIAGE_VERIFY --> DONE
    TRIAGE_VERIFY --> ESCALATED
    DONE([DONE]):::terminal
    DEFERRED([DEFERRED]):::terminal
    ESCALATED([ESCALATED]):::terminal
    AWAITING_OPERATOR([AWAITING_OPERATOR]):::newTerminal
    classDef terminal fill:#ccc,stroke:#999
    classDef newTerminal fill:#ffcc00,stroke:#cc9900,color:#000
Loading

Reviews (2): Last reviewed commit: "docs(model): scope the operator_actions ..." | Re-trigger Greptile

Review feedback on #347. The field comment stated "a park is defined by
owing at least one action" as a live invariant, but this PR ships no
writer and no validator, so nothing can hold it — which is exactly why a
reviewer read the empty-list fixtures as invalid state. Attribute the
rule to the park path that will enforce it and say plainly that the field
is unvalidated on both load and write.

Also: fix the article before `awaiting-operator` in the CHANGELOG, and
pin that the v1 status document deliberately does not project the actions
themselves (the fixture set them but asserted nothing).
@pbean

pbean commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator Author

Review response pushed in 67f98a3 (no force-push — the review pass is still diffable against 18f9643). Both inline threads answered and resolved: the CHANGELOG article was valid and is fixed; the fixture ask is declined with reasons, but the mismatch it detected was real and is fixed at its root.

Answering the two failed pre-merge checks, since neither is actionable here:

Linked Issues. Working as intended. This is a deliberate 1-of-4 slice of #335 — the body opens with refs #335 (1/4) and states "Does not close #335". The confirm command, the registry and review-demotion routing are parts 3 and 4; shipping them here would be the 800-LOC PR CONTRIBUTING rules out. The check compared a quarter against the whole; its own suggested resolution ("narrow the linked issue scope") is already what refs rather than Closes does.

Docstring Coverage (50% vs 80%). No such gate exists in this repo, and the threshold is the bot's, not ours. pyproject.toml selects ["E4","E7","E9","F"] — no D/pydocstyle, with a standing comment not to ratchet ruff stricter than CI; .trunk/trunk.yaml's linter list contains no interrogate or docstr-coverage; pyright excludes tests/ entirely. Repo-wide grep for interrogate|docstr-coverage|pydocstyle: zero hits.

The house style in tests/ is also deliberately bimodal rather than uniform — test_model.py sits at 1/42 docstrings, because the test name carries the mechanical claim and a docstring is spent only on the invariant a reader would otherwise miss. The new tests follow that: the round-trip tests are bare, while the default_factory sharing test and the TERMINAL_PHASES-vs-transition-table guard both carry one. Adding docstrings to the mechanical tests to clear an external threshold would make this PR the outlier.

@pbean
pbean merged commit d53ba63 into main Jul 28, 2026
11 checks passed
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