feat(plan): enforce decision type phase-gating at recording time #973
Merged
hurui200320
merged 2 commits from 2026-03-19 07:53:44 +00:00
feature/m4-decision-phase-gating into master
Dismiss Review
Are you sure you want to dismiss this review?
Labels
Clear labels
auto/needs-reevaluation
controller-managed
overdue
auto/blocked-by-deps
auto/ci-timeout
auto/claimed-implementer
auto/claimed-merge
auto/claimed-reviewer
auto/driver-down
auto/invariant-violation
auto/last-attempt-tier-0
auto/last-attempt-tier-1
auto/last-attempt-tier-2
auto/last-attempt-tier-min
Automation Tracking
auto/needs-conflict-resolution
auto/needs-implementer
auto/postmortem
auto/ready-to-merge
auto/restart-throttled
auto/revert
auto/sentinel
auto/stale-inactivity
auto/unstable
Blocked
Needs Feedback
Signed-off: Owner
Signed-off: Scrum Master
Signed-off: Tech Lead
Spike
Controller deferred this PR; awaiting Phase 6+ scope-evaluator or operator re-enablement.
Auto-agents controller manages this PR/issue (see tools/controller/deploy/RUNBOOK.md). Remove this label to abandon controller management.
PR blocked by an open issue dependency. Operator must close the dep (or remove the dependency link) before the merge driver can act. Auto-cleared by merge_drive when no open deps remain.
Most recent merge cycle hit CI timeout. Driver excludes this PR while last merge_cycle row is < 30 min old; label persists thereafter as visible history.
Currently being processed by an implementer worker.
Currently being processed by the merge driver.
Currently being processed by a reviewer worker.
Merge driver heartbeat stale; pipeline halted. Closed automatically on next clean tick.
Detected master commit violating the strict merge invariant. Tracked as an issue (not a PR label); kept here for label completeness.
In-cycle escalation: most recent attempt ran at the Tier 0 slot (`tier-0`). Slot's model defined in .opencode/models/tiers.yaml.
In-cycle escalation: most recent attempt ran at the Tier 1 slot (`tier-1`). Slot's model defined in .opencode/models/tiers.yaml.
In-cycle escalation: most recent attempt ran at the Tier 2 slot (`tier-2`). Slot's model defined in .opencode/models/tiers.yaml. Gated behind IMPLEMENTER_ESCALATION_TIER2_ENABLED.
In-cycle escalation: most recent attempt ran at the Tier -1 slot (`tier-min`). Slot's model defined in .opencode/models/tiers.yaml. Suffix is ``-min`` (not ``--1``) so the Forgejo UI reads naturally.
Tracking issues used by the AI Automation system for agents to communicate and report.
Rebase conflict needs LLM conflict-resolver.
Failing CI needs implementer attention.
Documenting a driver incident or rollback.
Reviewer has APPROVED this PR and no later REQUEST_CHANGES is outstanding. The merge driver requires this label to even consider a PR for merging. Set by the reviewer worker on APPROVE; cleared on REQUEST_CHANGES.
Train repeatedly lost master-tempo races. Driver excludes via merge_cycle until cooldown elapses; label persists as visible history.
Revert PR backing out an invariant violation. Fast-tracked through the merge driver.
Sentinel PR duplicated from upstream into a personal fork by tools/duplicate_prs_to_fork.py for pipeline testing. Lives only in the fork; the canonical pipeline never sees it.
No implementer activity for N days. Flagged for human review. Auto-cleared on next push to head branch.
Repeatedly fails on current master (>= 3 ci-fail-on-rebased-sha releases in 12 h). Excluded from driver until human triage.
A ticket in a blocked state and unable to complete until some other task is completed first.
Bounty
$100
A bounty of $100 for any open-source contributor who provides a MR that solves this issue
Bounty
$1000
A bounty of $1000 for any open-source contributor who provides a MR that solves this issue
Bounty
$10000
A bounty of $10000 for any open-source contributor who provides a MR that solves this issue
Bounty
$20
A bounty of $20 for any open-source contributor who provides a MR that solves this issue
Bounty
$2000
A bounty of $2000 for any open-source contributor who provides a MR that solves this issue
Bounty
$250
A bounty of $250 for any open-source contributor who provides a MR that solves this issue
Bounty
$50
A bounty of $50 for any open-source contributor who provides a MR that solves this issue
Bounty
$500
A bounty of $500 for any open-source contributor who provides a MR that solves this issue
Bounty
$5000
A bounty of $5000 for any open-source contributor who provides a MR that solves this issue
Bounty
$750
A bounty of $750 for any open-source contributor who provides a MR that solves this issue
MoSCoW
Could have
Could have feature in order to satisfy the epic/legendary.
MoSCoW
Must have
Must have feature in order to satisfy the epic/legendary.
MoSCoW
Should have
Should have feature in order to satisfy the epic/legendary.
There are questions in the ticket that can not be completed until the project owner provides clarity.
Points
1
1 man-hours worth of work for an expert with no learning curve.
Points
13
13 man-hours worth of work for an expert with no learning curve.
Points
2
2 man-hours worth of work for an expert with no learning curve.
Points
21
21 man-hours worth of work for an expert with no learning curve.
Points
3
3 man-hours worth of work for an expert with no learning curve.
Points
34
34 man-hours worth of work for an expert with no learning curve.
Points
5
5 man-hours worth of work for an expert with no learning curve.
Points
55
55 man-hours worth of work for an expert with no learning curve.
Points
8
8 man-hours worth of work for an expert with no learning curve.
Points
88
88 man-hours worth of work for an expert with no learning curve.
Priority
Backlog
This ticket has backlogged priority and is not to be worked on yet
Priority
CI Blocker
Critical priority issue that blocks CI/CD pipeline and prevents PR merges
Priority
Critical
The priority is critical
Priority
High
The priority is high
Priority
Low
The priority is low
Priority
Medium
The priority is medium
When an epic or legendary is in review it must be signed off by owner, tech lead, and scrum master before being marked as completed.
When an epic or legendary is in review it must be signed off by owner, tech lead, and scrum master before being marked as completed.
When an epic or legendary is in review it must be signed off by owner, tech lead, and scrum master before being marked as completed.
A ticket for learning a tool or technology that is needed to be able to do future planning and design.
State
Completed
The ticket has been fully implemented, completed, and merged with the source code. This label should only be applied once a ticket is closed.
State
Duplicate
A ticket that represents the same content as an existing ticket.
State
In Progress
A ticket that is actively being developed.
State
In Review
A ticket that has had some code completed to implement but is waiting to pass peer review and is not yet merged in.
State
Paused
This ticket's work started but wasn't finished. It's on hold (likely in a feature branch) and will be resumed later, either due to a blocker or a delay.
State
Unverified
All new tickets start in this state. A developer may set it to show the ticket is unverified. This means we haven't agreed to work on it. It will either move to a verified state or be closed as wontdo.
State
Verified
The issue has been verified by a developer as legitimate. It will be worked on and verified tickets are now considered part of the backlog.
State
Wont Do
This ticket has been decided it wont be done. This may mean the bug has been determined to not be real (cant verify) or the feature is one we have decided we dont want to adopt.
Type
Automation
Any edits or discussion about the AI automated coding system.
Type
Bug
Something that doesnt work as intended.
Type
Discussion
Anytime a ticket represents a discussion about a subject and doesnt fall into one of the other categories.
Type
Documentation
An error or improvement needed in the documentation.
Type
Epic
Any first tier epic. That is, an epic which contains only issues as children and will not have sub-epics.
Type
Feature
Some new functionality not present.
Type
Legendary
A type of Epic which will contain other Epics.
Type
Refactor
A code change that restructures existing code without changing its external behavior.
Type
Support
Someone needs help using the project.
Type
Task
A generic task that doesnt fit into the other type categories.
Type
Testing
Work exclusively focusing on fixing or expanding testing.
No Label
Type
Feature
Projects
Clear projects
No project
Assignees
aditya (Aditya Chhabra)
aleenaumair (Aleena Umair)
brent.edwards (Brent Edwards)
CoreRasurae (Luis Mendes)
drew (Drew Morris)
eugen.thaci (Eugen Thaci)
freemo (Jeffrey Phillips Freeman)
HAL9000 (HAL 9000)
HAL9001 (HAL9001)
hamza.khyari (Hamza Khyari)
hurui200320 (Rui Hu)
justin.morris
khird (Kyle Hird)
org.cleveragents
Clear assignees
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: cleveragents/cleveragents-core#973
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Delete Branch "feature/m4-decision-phase-gating"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Adds phase-gating validation to
DecisionService.record_decision()that enforces the specification's constraint: certain decision types are only valid during specific plan phases. This prevents invalid decisions (e.g.,tool_invocationduring Strategize,strategy_choiceduring Execute) from being persisted.Changes
cleveragents.core.exceptions): AddedDecisionPhaseViolationError(BusinessRuleViolation)withdecision_type,plan_phase, andallowed_typesattributes.cleveragents.domain.models.core.decision):resource_selectionadded toEXECUTE_TYPES— now phase-agnostic (Strategize or Execute) per ADR-007 L72 and ADR-033 L74.subplan_spawn/subplan_parallel_spawnin both sets; code comment documents divergence from ADRs per M4 subplan model (ticket #931).USER_INTERVENTIONremains phase-agnostic (both sets).is_any_phase_typeproperty updated to check membership in both sets dynamically (was hardcoded toUSER_INTERVENTIONonly).cleveragents.application.services.phase_gating):DecisionServiceto reducedecision_service.pyline count (1010 → 913) and isolate the phase-gating concern.PHASE_ALLOWED_TYPEStyped asMapping[PlanPhase, frozenset[DecisionType]].resolve_plan_phase()helper: supports explicit parameter, DB lookup, and graceful skip.validate_phase_gating()enforcement raisesDecisionPhaseViolationError.(DatabaseError, OperationalError, OSError)instead of bareexcept Exception— only absorbs infrastructure failures, not programming errors.# TODO(pg-migration):marker on TOCTOU race documentation for future PostgreSQL migration.cleveragents.application.services.decision_service):plan_phaseparameter torecord_decision().plan_phasestring now raisesValidationError(was uncaughtValueError).phase_gatingmodule for all phase-gating logic.PHASE_ALLOWED_TYPESre-exported in__all__for backward compatibility.resource_selectionreclassification.plan_phaseis provided nor a UnitOfWork is wired, validation is skipped, preserving all existing callers.ULID_PATTERNfromdecision.py__all__(was an unrelated export addition).resolve_plan_phase.is_any_phase_type: 4 dual-phase types (true) + 7 single-phase types (false), includingprompt_definitionroot test.consolidated_decision.featurefor newEXECUTE_TYPESmember count (8 members).uow.engine.dispose()before file deletion.tempfile.mktemp()replaced withtempfile.mkstemp().subplan_execution_steps.py.Review Round 1 + 2 Fixes
except Exceptiontoo broad in_resolve_plan_phase(DatabaseError, OperationalError, OSError)— matches codebase patterndecision_service.pyat 1010 linesphase_gating.pymodule (1010 → 913 lines)# TODO(pg-migration):marker with actionable guidanceresource_selectionreclassification needs CHANGELOGis_any_phase_typeBDD gap for dual-phase typesULID_PATTERNexport is unrelated drive-bydecision.py__all__decision.pyat 514 lines (now 513)Coverage Fix (Round 3)
Coverage was at 96.9446% (displayed as 97% but rounded to 96.9% at 1-decimal precision, failing the ≥97% threshold). Added 2 new Behave scenarios to cover previously untested paths in
phase_gating.py:"not_a_real_phase"toplan_phaseraisesValidationErrorwith the invalid value in the messageresolve_plan_phase()catchesDatabaseErrorfrom a corrupted SQLite DB and returnsNone(skip gating) instead of propagatingCoverage moved from 96.9446% → 96.9587%, which rounds to 97.0% and passes the threshold.
Quality Gates
Closes #931
PM Day 36 Triage: Decision phase-gating implementation. Closes #931. M4 scope. Reviewer needed: @freemo (decision framework expert). Verify alignment with ADR-007 decision tree and ADR-033 decision recording protocol.
031339fe06to20b6f0d5b9Self-QA Review — ✅ Approved
Iterations: 2 review/fix cycles
Final verdict: Approve
Cycle 1: 16 issues found → 16 fixed
The initial review identified 1 critical, 4 major, 6 minor, and 4 nit issues. The critical finding was that
resource_selectionwas incorrectly excluded fromEXECUTE_TYPES, violating ADR-007, ADR-033, and the specification. All 16 issues were fixed in a single amended commit.Key fixes applied:
DecisionType.RESOURCE_SELECTIONtoEXECUTE_TYPES; updated BDD scenarios and consolidated feature member countssubplan_spawn/subplan_parallel_spawnADR divergence with ticket reference; added TOCTOU race condition comment; added 2 missing DB-resolution test scenarios (empty DB + Execute phase)is_any_phase_typeto use dynamic set membership; wrappedPlanPhase()in try/except forValidationError; added DB error resilience; updated stale docstring table; added engine disposal in test cleanup; added PlanPhase enum direct-pass testtempfile.mktemp(); changed toMappingtype hint; moved imports to top-level; added stderr checks in Robot testsCycle 2: 0 critical/major issues — Approved
All 16 previous fixes verified correct. 8 minor style/coverage gaps and 5 nits remain — all non-blocking (defensive code path coverage, Robot Framework conventions, cosmetic code patterns).
Quality Gates
nox -e lintnox -e typechecknox -e unit_testsnox -e integration_testsnox -e e2e_testsnox -e coverage_reportFull implementation notes posted on ticket #931.
20b6f0d5b9tocb7edf2227Code Review — PR #973
feat(plan): enforce decision type phase-gating at recording timeReviewer: @brent.edwards | Size: L (+802/−9, 8 files) | Focus: Domain invariants, service design, backward compat
P1:must-fix (1)
1.
except Exceptiontoo broad in_resolve_plan_phasedecision_service.py:~807— The comment says "database errors" but catches everything includingTypeError,AttributeError, and other programming errors. The opt-in contract ("don't break if DB is unavailable") should only absorb infrastructure failures. Narrow to(OperationalError, DatabaseError, OSError)— the same pattern PR #971 correctly uses in_build_skill_service.P2:should-fix (3)
2.
decision_service.pyat 1010 lines — well over the 500-line guideline._resolve_plan_phase+_validate_phase_gating(~50 lines) could be extracted to aPhaseGatingPolicyclass orphase_gating.pymodule.3. TOCTOU race is documented but not programmatically guarded — the code assumes SQLite single-writer serialization. If someone switches to PostgreSQL, this assumption silently breaks. Add a
# TODO(pg-migration):marker.4.
resource_selectionreclassification to both-phases is a semantic breaking change —is_strategize_typeandis_execute_typenow returnTruefor more types. The ticket #931 rationale is documented in code, but this deserves a CHANGELOG entry as a behavioral change.P3:nit (2)
5.
ULID_PATTERNexport added to__all__is an unrelated drive-by fix.6.
decision.pyat 514 lines — marginally over guideline, acceptable.Positive Observations
DecisionPhaseViolationErrorwith structured attributes (decision_type,plan_phase,allowed_types: frozenset) — excellent for programmatic error handlingplan_phase+ no UoW = skip — backward compatiblePHASE_ALLOWED_TYPESasMapping[PlanPhase, frozenset[DecisionType]]— immutable, O(1) lookuptempfile.mktemp()→tempfile.mkstemp()anduow.engine.dispose()before file deletion — good cleanup fixesVerdict: REQUEST_CHANGES — P1-1 is a targeted fix.
Code Review Round 2 — PR #973
feat(plan): enforce decision type phase-gatingReviewer: @brent.edwards | Focus: Verification of Round 1 fixes + deep second pass
Prior Findings: 0 of 6 resolved
All Round 1 findings remain open:
except Exceptiontoo broad in_resolve_plan_phaseexcept Exceptionat line ~807. Comment added explaining intent but catch scope unchanged.decision_service.pyat 1010 linesresource_selectionreclassification needs CHANGELOGULID_PATTERNexport is unrelateddecision.pyat 514 linesNew Finding (1)
P2:
is_any_phase_typeBDD gap for 3 newly dual-phase typesThe PR promotes
resource_selection,subplan_spawn, andsubplan_parallel_spawnfrom single-phase to both-phase, changingis_any_phase_typefromFalse→Truefor all three. But the only BDD scenario testingis_any_phase_typeusesuser_intervention. If someone later removes one of these from a phase set, the regression goes undetected.Fix: Add a parametrized scenario covering all 4 dual-phase types.
Confirmed Clean from Second Pass
record_decisionbackward compatplan_phaseis keyword-only, defaults toNone, gating skipped whenNoneDecisionPhaseViolationErrorupstream handlingBusinessRuleViolationhandlers (a2a, CLI, error_handling)is_any_phase_typeimplementationDecisionServicewith realUnitOfWork+ SQLite, no mocking of service layerVerdict: REQUEST_CHANGES — P1-1 (
except Exceptionat line ~807) remains the sole merge-blocker. Narrowing toexcept (OperationalError, DatabaseError, OSError)resolves it. All other findings are P2/P3 and can be tracked as follow-up.cb7edf2227toef37aa3dfcReview Fix Summary — Addressing Rounds 1 & 2 (@brent.edwards)
All 7 findings from both review rounds have been addressed. Branch rebased onto latest
masterand force-pushed.P1-1 (must-fix):
except Exceptiontoo broad — ✅ RESOLVEDNarrowed the catch in
resolve_plan_phase()from bareexcept Exceptiontoexcept (DatabaseError, OperationalError, OSError). This follows the established codebase pattern fromrepositories.py:DatabaseError(fromcleveragents.core.exceptions) — catches repository-wrapped DB errorsOperationalError(fromsqlalchemy.exc) — catches raw SQLAlchemy connection/operation errors from the UoW layerOSError— catches filesystem-level SQLite access failuresProgramming errors (
TypeError,AttributeError, etc.) now correctly propagate instead of being silently swallowed.P2-2 (should-fix):
decision_service.pyat 1010 lines — ✅ RESOLVEDExtracted phase-gating concern to new module
cleveragents.application.services.phase_gating:PHASE_ALLOWED_TYPESconstantresolve_plan_phase()(was_resolve_plan_phaseinstance method)validate_phase_gating()(was_validate_phase_gatingstatic method)decision_service.pyreduced from 1010 → 913 lines. The service imports and delegates to the new module.PHASE_ALLOWED_TYPESis re-exported indecision_service.__all__for backward compatibility.P2-3 (should-fix): TOCTOU race — no programmatic guard — ✅ RESOLVED
Added
# TODO(pg-migration):marker to the TOCTOU comment with actionable guidance: "At minimum, a single-writer assertion or advisory lock should guard this section under multi-writer engines."P2-4 (should-fix):
resource_selectionreclassification needs CHANGELOG — ✅ RESOLVEDAdded CHANGELOG entry under
## Unreleaseddocumenting the behavioral change:resource_selectionreclassified from Execute-only to phase-agnostic, with impact note for code relying onis_strategize_type/is_execute_type.P2-7 (new in Round 2):
is_any_phase_typeBDD gap — ✅ RESOLVEDAdded 11 new Behave scenarios to
consolidated_decision.feature:resource_selection,subplan_spawn,subplan_parallel_spawn,user_intervention(4 scenarios)invariant_enforced,strategy_choice,implementation_choice,tool_invocation,error_recovery,validation_response(6 scenarios)is_any_phase_type should be falseassertionP3-5 (nit):
ULID_PATTERNexport is unrelated drive-by — ✅ RESOLVEDRemoved
ULID_PATTERNfromdecision.py__all__.P3-6 (nit):
decision.pyat 514 lines — No actionReviewer accepted as marginally over guideline.
Quality Gates
ef37aa3dfcto1f016bea331f016bea33to35eb7b762aCode Review — PR #973
feat(plan): enforce decision type phase-gating at recording timeCleanly scoped feature with good architectural separation. The extraction of phase-gating logic into
phase_gating.py(148 lines) with clear API boundary (resolve_plan_phase(),validate_phase_gating(),PHASE_ALLOWED_TYPES) is well-done. The opt-in design (gating skipped when neitherplan_phasenor UoW is provided) ensures backward compatibility.The
resource_selectionreclassification to phase-agnostic is a breaking behavioral change, but it's properly documented in the CHANGELOG with ADR references (ADR-007 L72, ADR-033 L74). The TOCTOU race condition is documented with a clearTODO(pg-migration)marker.36 Behave scenarios + 6 Robot tests + proper exception hierarchy (
DecisionPhaseViolationError(BusinessRuleViolation)) demonstrate thorough implementation.Approved. No issues found.
35eb7b762ato52f1bb2abbNew commits pushed, approval review dismissed automatically according to repository settings
52f1bb2abbto1ec6b2ac271ec6b2ac27to231c3656e0231c3656e0to296daebe59Coverage Fix — Branch rebased and pushed
What changed
The coverage gate was failing at 96.9% (displayed as 97% but the precise value 96.9446% rounds to 96.9% at 1 decimal). Two untested paths in
phase_gating.pywere the gap:plan_phase="not_a_real_phase"raisesValidationErrorwith the invalid value in the message.resolve_plan_phase()catches theDatabaseErrorand returnsNone(graceful skip).Rebase
Branch rebased onto latest
origin/master(cbf8bcc9). Resolved CHANGELOG.md conflict (kept both entries).All quality gates pass (post-rebase)
CI running: https://git.cleverthis.com/cleveragents/cleveragents-core/actions/runs/2381