Skip to content

feat(engine): park dev-declared awaiting-operator stories through commit - #348

Merged
pbean merged 3 commits into
mainfrom
feat/awaiting-operator-entry
Jul 28, 2026
Merged

feat(engine): park dev-declared awaiting-operator stories through commit#348
pbean merged 3 commits into
mainfrom
feat/awaiting-operator-entry

Conversation

@pbean

@pbean pbean commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

refs #335 (2/4)

Part 2 of the four-PR awaiting-operator program: the entry path. PR-1 named the state at every layer with no writer; this makes it reachable. bmad-loop confirm is part 3, review-demotion composition is part 4.

#335 stays open after this merges — parts 3 and 4 are still outstanding.

Phrased to avoid putting a closing keyword next to #335 at all. GitHub's parser
ignores negation, so part 1's "does not <keyword> #335" disclaimer auto-closed the
issue one second after #347 merged. A disclaimer cannot be written with the keyword in it.

The change

A dev session whose story needs an action only a human can take outside the repo — buy a domain, publish a DNS record, grant an API key — completes everything an agent can do, commits it, enumerates what is owed under operator_actions: in the spec frontmatter, and parks. The run continues to the next story.

Before this, such a story had two outcomes and both were dishonest: done hides the outstanding work behind a green board (the reporter's original 40-unticked-entries artifact), and blocked halts the entire run over work the loop was never going to do.

Layer Change
frontmatter.py operator_actions_of — strict container, lenient scalar items. Lives here because verify cannot import devcontract
devcontract.py AWAITING_OPERATOR terminal beside DONE/BLOCKED (synthesis, missing-marker scan); folds the validated list into the result. RECONCILABLE_FROM unchanged
verify.py verify_dev / verify_review accept the (awaiting-operator, awaiting-operator) pair and require a non-empty list on it
engine.py _park_awaiting_operator, the final-phase rule in _finalize_commit_phase, the sprint mirror, the injected prompt contract, _operator_park_enabled
worktree_flow.py integrate_unit merges on DONE or AWAITING_OPERATOR
policy.py + core.toml [operator] enabled = true
stories_engine.py, sweep.py opt out (see below)

Design notes

No _defer machinery. A park is a success that commits — no stash, no rollback, no ledger snapshot to unwind. What separates it from DEFERRED/ESCALATED is precisely that it carries a commit.

The pair is selected by the observed spec status. The skill decides whether it parked; the gate then holds it to the matching board state and to a non-empty action list. A park declaring nothing is refused with fixable=True, so the reason goes to a repair session as feedback rather than throwing a real tree away — a park is defined by owing at least one action, and confirming a blank later would be meaningless.

The park still verifies. It skips only the review loop, because a review pass is bmad-dev-auto re-invoked to second-guess the diff, and a park's outstanding work is not in the diff at all — every cycle would either re-park (budget burned, no progress) or "fix" the park away by finalizing done, which is the exact false-green the state exists to prevent. It delegates to the same skip-review commit path, so it clears the spec/board pair, the action list, and the project's verify commands. No commit path in the engine skips verification.

The board mirror is a forward advance. awaiting-operator sits immediately below done in STATUS_ORDER, so _post_dev_state_sync targeting it is an ordinary forward move through the sole writer — no exception to never-regress, and part 3's confirm advances the same board the same way. Mirrored on the status alone, before the actions are validated: verify_dev owns the "declared nothing" retry, and its feedback reads far better against a board that already agrees with the spec.

Crash-resume needed no new code. The actions are latched onto the task before advance(COMMITTING), so the existing #115 resume arm re-enters _finalize_commit_phase and its final-phase rule (AWAITING_OPERATOR iff task.operator_actions) reaches the same verdict the pre-crash run would have.

The dev prompt instruction is engine-injected, and interim. The durable home is upstream — bmad-dev-auto's spec template and step-03/04 finalize rules — and that PR is not landed (part of the program's upstream track). The "never use blocked for this" clause is the load-bearing half: blocked is the skill's existing escape for "I cannot finish", it is exactly the shape a human-only action tempts, and it halts the run. The instruction is deliberately backtick-free — it is appended after the repair prompt's feedback pointer, and the last backticked token in a dev prompt is by convention that path.

Commit message suffix (awaiting operator) is appended to a rendered commit_message_template too: the template governs the message's shape, but whether the story is finished is a fact about the commit, and git log is where an operator looks long after the run summary scrolled past.

The sketch's open questions — choices made here

Question Choice
_close_declared_deferred at park commit, or only at confirm? At park commit. It falls out of reusing _finalize_commit_phase, and it is right: the code that closes those entries is in this commit. Closure is advisory and sweep-re-verified.
Action value shape — strings, or strings-or-objects now? Plain strings in v1. Strings-now/objects-later is the cheaper migration than the reverse; the per-action check: command stays a deliberate v2 question. Non-scalar items are dropped rather than str()-ed, so a v2-shaped declaration reads as malformed and gets the retry, instead of handing a human a line of Python repr as their instruction.
Stories mode in PR-2 or a follow-up? Follow-up. Stories mode has no board, so the pair does not exist and verify_review_stories still demands done; parking there would commit on the spec's word alone. StoriesEngine (and SweepEngine, for the same reason on bundles) override _operator_park_enabled to False — an explicit opt-out, not an accident of where the branch sits.
ATTENTION file, or notify-only? Notify-only — a parked story is non-blocking by design. Ships with part 3 (see scope note).
[operator] enabled default-on or opt-in? Default-on. Off, the token means nothing rather than something else: no prompt injection, no board target, and the gates reject it, so a session that writes it anyway is retried with the mismatch as feedback.

Scope note — registry + notification moved to part 3

Whole-PR size with tests came to ~1040 lines, over the 800 cap. Per the phase plan's own instruction I split out operatoractions.py (the .bmad-loop/operator-actions.json registry writer) and the park notification, which now land with bmad-loop confirm in part 3. That is also the better seam: the registry is confirm's data store and the notification is its prompt, so shipping them here would have added a store nothing reads.

For now a parked story's obligations live in its spec frontmatter and in the story-awaiting-operator journal entry, which carries the actions themselves rather than a count. The run summary, status, and the TUI already surface the phase (part 1). Noted on #335.

Tests

At the lowest layer that catches each regression: devcontract unit (terminal acceptance, the seven action shapes, no synthesized escalation, fold-only-on-park, the missing-marker scan); verify unit (pair selection both directions, malformed → fixable retry, policy-off, regression arm stays scoped to the done pair); engine mock-adapter E2E (park commits + commit_sha stamped + board reaches the token + run continues to the next story + no rollback/preserve-ref, malformed park repaired not committed, policy-off defers); worktree merge; crash-resume through COMMITTING.

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

  1. Delete the non-empty actions gate → the four malformed-park tests and the engine repair test fail (the blank park verifies green and commits).
  2. Delete the review-skip branch in _review_and_commit → the park E2E fails requesting an unscripted review session, leaving the story at REVIEW_RUNNING. The E2E uses review.trigger = "always" precisely so this bites — under the recommended default a story recommending no follow-up already skips the loop, and the branch could have been deleted with no test noticing.
  3. Narrow integrate_unit back to == Phase.DONE → the worktree test fails with the story's change absent from the target branch.
  4. Latch the actions after advance(COMMITTING) → the resumed story lands DONE, silently dropping the obligation.
  5. Make verify_review ignore operator_park → the "refused without the engine flag" test fails by verifying a park green.

Behavior changes to existing paths

  • Every dev prompt gains the park contract while [operator] enabled (two exact-prompt assertions updated to match).
  • The missing-marker repair (engine.py) accepts the new terminal, kept in lockstep with devcontract.is_frontmatter_candidate — a status accepted by the scan but refused by the repair would leave a park harvested but permanently un-markered.
  • _verify_shared_gates gained an optional fm= passthrough so verify_dev can read frontmatter once to choose the expected pair without breaking the single-read contract.

Review round

Both Greptile P2s accepted, plus one CodeRabbit finding.

  • verify_review read policy.operator.enabled directly while verify_dev took an engine-supplied flag. Not a live bug — every other _commit caller is gated on done, and both subclasses override _verify_review — but _operator_park_enabled is an override seam, so the asymmetry meant a mode opting out of parking could still find the base gate accepting a park the engine refuses to take, and part 4's demotion knob edits exactly there. Now one flag, conservative default, with ablation 5 proving it bites.
  • The settings description and policy template still promised .bmad-loop/operator-actions.json, which moved to part 3 in the size split — they name the spec frontmatter instead. Same for a stale test docstring (CodeRabbit).

uv run pytest -q -n auto: 3499 passed. Full trunk check: clean. pyright@1.1.411: 0 errors.

Makes the state PR-1 named reachable. A dev session whose story needs an
action only a human can take outside the repo finishes and COMMITS everything
an agent can do, enumerates what is owed in the spec's `operator_actions:`
frontmatter, and parks — the run moves on instead of stopping. The two
outcomes such a story had before were both dishonest: `done` hides the
outstanding work behind a green board, `blocked` halts the run over work the
loop was never going to do.

- devcontract: `awaiting-operator` is a terminal beside done/blocked, folding
  a validated action list into the result; no escalation is synthesized, which
  is what keeps it out of the run-halting pause channel
- verify_dev/verify_review accept the (awaiting-operator, awaiting-operator)
  pair, selected by the OBSERVED spec status, and refuse a park that
  enumerates nothing with a fixable retry naming the shape
- the park rides the normal commit path: review loop skipped (nothing in the
  diff for it to converge on), same deterministic gates, and
  `_finalize_commit_phase` picks the final phase from the task's actions
- actions are latched before advance(COMMITTING), so the crash-resume arm
  re-derives AWAITING_OPERATOR with no code of its own
- worktree units merge on a park like a done one — it carries a commit
- `[operator] enabled` (default true); stories mode and sweep bundles opt out,
  their verify tails cannot gate the pair

refs #335 (2/4)
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Stories can now commit and continue when human-only actions remain, using an awaiting-operator status with recorded operator_actions. Policy, verification, synthesis, sprint bookkeeping, journaling, worktree merging, prompts, and tests support the new park path.

Changes

Awaiting-operator parking

Layer / File(s) Summary
Terminal state and policy contracts
src/bmad_loop/policy.py, src/bmad_loop/frontmatter.py, src/bmad_loop/devcontract.py, src/bmad_loop/data/settings/core.toml, tests/test_policy.py, tests/test_settings_schema.py, tests/test_devcontract.py
Adds operator policy configuration, normalized action parsing, terminal-status synthesis, and marker discovery for awaiting-operator.
Operator-action verification gates
src/bmad_loop/verify.py, tests/test_verify.py
Validates matching board state and requires usable operator actions for enabled development and review park flows.
Engine park, commit, and merge flow
src/bmad_loop/engine.py, src/bmad_loop/stories_engine.py, src/bmad_loop/sweep.py, src/bmad_loop/worktree_flow.py
Detects valid parks, commits and journals operator obligations, updates sprint state and prompts, disables parking in stories and sweep modes, and merges parked worktree units.
End-to-end workflow validation
tests/conftest.py, tests/test_engine.py, tests/test_engine_worktree.py, CHANGELOG.md
Adds fixtures and tests for park commits, repair, disabled policy, crash recovery, prompt contracts, commit messages, and worktree merging.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers: dracic

Poem

A rabbit parks where operators must go,
Leaving neat little action notes in a row.
The commit hops onward, the board marks its place,
While journals record every whiskered trace.
“Human hands next!” says the bunny with cheer—
Then bounds to the next story, ears in the air.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 59.65% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding an engine path to park awaiting-operator stories through commit.
✨ 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-entry

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.

Comment thread src/bmad_loop/data/settings/core.toml Outdated
Comment thread src/bmad_loop/verify.py Outdated
@greptile-apps

greptile-apps Bot commented Jul 28, 2026

Copy link
Copy Markdown

Greptile Summary

Adds the entry path for the awaiting-operator park state (#335, part 2 of 4): a dev session that has completed all agent-doable work can now commit it, record outstanding human-only actions under operator_actions: in the spec frontmatter, and park — letting the run continue to the next story rather than forcing a false done or a run-halting blocked.

  • frontmatter.py introduces operator_actions_of (strict container, lenient scalars), shared by both devcontract and verify because the dependency chain prevents verify from importing devcontract.
  • devcontract.py / verify.py add AWAITING_OPERATOR as a third terminal alongside DONE/BLOCKED, with verify_dev / verify_review accepting the (awaiting-operator, awaiting-operator) spec/sprint pair and requiring a non-empty action list.
  • engine.py adds _park_awaiting_operator, a final-phase rule in _finalize_commit_phase, sprint mirror, and dev-prompt injection; StoriesEngine and SweepEngine explicitly return False from _operator_park_enabled() as a typed opt-out.

Confidence Score: 5/5

Safe to merge. The park path is well-gated at both the dev-verify and review-verify layers, crash recovery is correct, and subclass opt-outs are typed.

Every invariant is enforced by a separate gate layer with a corresponding ablation test. The repair path for blank-action parks routes through Action.RETRY in _drive_dev, so task.operator_actions is always set from already-validated frontmatter before _park_awaiting_operator is called. 3498 tests pass and pyright reports zero errors.

Files Needing Attention: No files require special attention.

Important Files Changed

Filename Overview
src/bmad_loop/engine.py Core park entry path: _park_awaiting_operator, _finalize_commit_phase final-phase rule, sprint mirror, dev prompt injection, and _operator_park_enabled override seam. Crash-safety is correct — actions are latched before advance(COMMITTING).
src/bmad_loop/verify.py Adds AWAITING_OPERATOR constant, _operator_actions_gate, and the (awaiting-operator, awaiting-operator) pair acceptance in verify_dev and verify_review. The fm= passthrough preserves the single-read contract.
src/bmad_loop/frontmatter.py Adds operator_actions_of: strict container check, lenient scalar normalization, dict/list/None items dropped. Deduplication via dict.fromkeys preserves order.
src/bmad_loop/devcontract.py AWAITING_OPERATOR added as a third terminal; operator_actions folded into result only on that status. No escalation synthesized — deliberate to preserve the done-vs-park distinction.
src/bmad_loop/worktree_flow.py integrate_unit now merges on DONE or AWAITING_OPERATOR. A parked story carries a committed change that must not be stranded on a torn-down unit branch.
src/bmad_loop/policy.py Adds OperatorPolicy with enabled=True default. Template and dataclass kept in sync (tested).
src/bmad_loop/stories_engine.py Overrides _operator_park_enabled to False — explicit typed opt-out. Stories mode has no sprint board.
src/bmad_loop/sweep.py Overrides _operator_park_enabled to False — bundles carry no sprint-status entry, so the verify pair does not exist.

Reviews (2): Last reviewed commit: "docs(test): drop the split-out registry ..." | Re-trigger Greptile

Greptile review, two P2s.

`verify_review` read `policy.operator.enabled` directly while `verify_dev`
took an engine-supplied `operator_park`. `_operator_park_enabled` is an
override seam, so a mode that opts out of parking while still reaching the
base gate would have found it accepting a park the engine refuses to take —
and PR-4's demotion knob edits exactly there. One flag, conservative default,
with the ablation to prove the gate bites.

The settings description and policy template also promised
`.bmad-loop/operator-actions.json`, which now ships with `bmad-loop confirm`
in part 3 — they name the spec frontmatter instead.

refs #335 (2/4)

@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: 1

🤖 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 `@tests/test_engine.py`:
- Around line 1250-1254: Update the test docstring describing the end-to-end
session scenario to remove the claim that the registry and notification record
what is owed. Keep the statements about the story committing, the board reaching
the token, and the run advancing to the next story, matching the assertions
covered by this test.
🪄 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: fd491a75-0cb2-4613-b7c0-a5ebabda7d14

📥 Commits

Reviewing files that changed from the base of the PR and between d53ba63 and 32355d0.

📒 Files selected for processing (17)
  • CHANGELOG.md
  • src/bmad_loop/data/settings/core.toml
  • src/bmad_loop/devcontract.py
  • src/bmad_loop/engine.py
  • src/bmad_loop/frontmatter.py
  • src/bmad_loop/policy.py
  • src/bmad_loop/stories_engine.py
  • src/bmad_loop/sweep.py
  • src/bmad_loop/verify.py
  • src/bmad_loop/worktree_flow.py
  • tests/conftest.py
  • tests/test_devcontract.py
  • tests/test_engine.py
  • tests/test_engine_worktree.py
  • tests/test_policy.py
  • tests/test_settings_schema.py
  • tests/test_verify.py

Comment thread tests/test_engine.py Outdated
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.

1 participant