Repository navigation
Separate plan-review verdict policy from application confirmation #193
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| # review/plan_gate.py — plan-review policy | ||
|
|
||
| <!-- verified-against: 2026-09-26 --> | ||
|
|
||
| This component builds the authoritative mode-aware context for a planned | ||
| playbook and interprets the injected plan reviewer's verdict. Exact reused | ||
| plans that do not require review pass without a reviewer call. A `block` | ||
| verdict always stops; `revise` or `unavailable` stops an unattended `--yes` | ||
| run because no person remains to inspect the plan. Interactive runs surface | ||
| those verdicts and leave final confirmation to `app.core`. | ||
|
|
||
| The mode context uses the executor's `when` evaluation and resolved rebase | ||
| mode, so inactive write/push steps are not presented as active. It does not | ||
| execute a plan, print notices, prompt for confirmation, create a run directory, | ||
| or own a transport. It returns a `PlanGateResult` with the decision and notices; | ||
| `app.core` renders them and keeps `_mode_review_context` and | ||
| `_plan_review_gate` compatibility entry points while supplying the reviewer. | ||
|
|
||
| Verification: `test_planner_playbooks.py`, `test_curator.py`, and | ||
| `test_phase_b.py` cover verdicts, mode context, and the application gate. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| """Plan-review verdict policy and mode-aware context for generated playbooks. | ||
|
|
||
| The application supplies its reviewer and retains the human confirmation gate. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from dataclasses import dataclass | ||
|
|
||
| import yaml | ||
|
|
||
| from ..playbooks.store import playbook_to_doc | ||
|
|
||
|
|
||
| def _mode_review_context(playbook, spec) -> str: | ||
| """Plan-review context for MODE-AWARE playbooks: the reviewer sees the | ||
| raw yaml's FULL step list, but the `when:` gates resolve against the | ||
| ALREADY-RESOLVED mode (`resolve_effective_mode` runs before the review | ||
| gate) — without this context a report-only plan looks like it runs its | ||
| write/push steps and gets spuriously blocked. The active-step set is | ||
| the same mechanical truth the executor computes, never prose.""" | ||
| if not getattr(playbook, "mode_aware", False): | ||
| return "" | ||
| mode = str((getattr(spec, "params", None) or {}) | ||
| .get("rebase_mode", "") or "") | ||
| if not mode: | ||
| return "" | ||
| from ..engine.executor import _eval_when | ||
| from ..rebase_engine.modes import mode_state_flags | ||
|
|
||
| flags = {"task_spec": {}, **mode_state_flags(mode)} | ||
| active = [s.get("id", s.get("step", "?")) | ||
| for s in playbook_to_doc(playbook).get("steps", []) | ||
| if "when" not in s or _eval_when(s["when"], flags)] | ||
| repo = str(getattr(spec, "repo", "") or "") | ||
| repo_line = (f"\nTarget repo (authoritative): {repo!r} — bound at " | ||
| "runtime from the TaskSpec; the yaml `repos:` list is a " | ||
| "planner RECALL FILTER where empty means repo-neutral, " | ||
| "never untargeted." if repo else "") | ||
| return (f"\n\nResolved mode context (authoritative): " | ||
| f"rebase_mode={mode}. Under this mode the `when:` gates run " | ||
| f"ONLY these steps: {active}. Every other listed step is " | ||
| "statically gated OFF for this run — judge the plan for THIS " | ||
| "mode's step set." | ||
| + repo_line + | ||
| "\nWrite/push governance (authoritative): the mode's own " | ||
| "push/CI steps are governed at runtime by the push-gate " | ||
| "ruling, guard_push, and the ALLOW_PUSH env double-gate — " | ||
| "the task tier does not forbid steps this mode activates.") | ||
|
|
||
|
|
||
| @dataclass(frozen=True) | ||
| class PlanGateResult: | ||
| allowed: bool | ||
| notices: tuple[str, ...] = () | ||
|
|
||
|
|
||
| def review_plan_gate( | ||
| resolution, spec, llm, model, assume_yes, *, review_fn | ||
| ) -> PlanGateResult: | ||
| """Interpret a reviewer verdict without owning confirmation or output.""" | ||
| if not resolution.requires_review: | ||
| return PlanGateResult(True) | ||
| doc = yaml.safe_dump(playbook_to_doc(resolution.playbook), sort_keys=False) | ||
| task_text = spec.describe() + _mode_review_context(resolution.playbook, spec) | ||
| verdict = review_fn(llm, playbook_doc=doc, task=task_text, model=model) | ||
| notices = [] | ||
| if verdict.verdict != "unavailable": | ||
| notices.append(f" plan review: {verdict.verdict}" | ||
| + (f" — {verdict.critiques}" if verdict.critiques else "")) | ||
| if verdict.verdict == "block": | ||
| notices.append("✋ plan blocked by reviewer.") | ||
| return PlanGateResult(False, tuple(notices)) | ||
| if verdict.passing: | ||
| return PlanGateResult(True, tuple(notices)) | ||
| # A non-passing verdict can only proceed through explicit human review. | ||
| # --yes removes that confirmation, so revise/unavailable must block. | ||
| if assume_yes: | ||
| reason = ("no reviewer LLM" if verdict.verdict == "unavailable" | ||
| else f"plan review returned {verdict.verdict}") | ||
| notices.append(f"✋ {reason} and --yes leaves no human to gate it — blocked.") | ||
| return PlanGateResult(False, tuple(notices)) | ||
| if verdict.verdict == "unavailable": | ||
| notices.append(" ⚠ no reviewer LLM — your confirmation is the plan-review gate") | ||
| return PlanGateResult(True, tuple(notices)) | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a mode-aware plan is reviewed, this new
review/leaf importsengine.executor, violating the explicit dependency rule indoc/architecture/SPEC/_ARCHITECTURE.md:66-67thatreview/must never importengine/. This also creates a bidirectional package dependency because engine code such asengine/steps/workspace.py:8already imports fromreview/, undermining the boundary this refactor introduces; move the mode-context assembly back to an orchestration layer or inject the condition evaluator instead.Useful? React with 👍 / 👎.