feat(plan): enforce decision type phase-gating at recording time #973
No reviewers
Labels
No labels
auto/needs-reevaluation
controller-managed
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
Bounty
$100
Bounty
$1000
Bounty
$10000
Bounty
$20
Bounty
$2000
Bounty
$250
Bounty
$50
Bounty
$500
Bounty
$5000
Bounty
$750
MoSCoW
Could have
MoSCoW
Must have
MoSCoW
Should have
Needs Feedback
Points
1
Points
13
Points
2
Points
21
Points
3
Points
34
Points
5
Points
55
Points
8
Points
88
Priority
Backlog
Priority
CI Blocker
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Signed-off: Owner
Signed-off: Scrum Master
Signed-off: Tech Lead
Spike
State
Completed
State
Duplicate
State
In Progress
State
In Review
State
Paused
State
Unverified
State
Verified
State
Wont Do
Type
Automation
Type
Bug
Type
Discussion
Type
Documentation
Type
Epic
Type
Feature
Type
Legendary
Type
Refactor
Type
Support
Type
Task
Type
Testing
No project
No assignees
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
cleveragents/cleveragents-core!973
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
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.
031339fe0620b6f0d5b9Self-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.
20b6f0d5b9cb7edf2227Code 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.cb7edf2227ef37aa3dfcReview 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
ef37aa3dfc1f016bea331f016bea3335eb7b762aCode 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.
35eb7b762a52f1bb2abbNew commits pushed, approval review dismissed automatically according to repository settings
52f1bb2abb1ec6b2ac271ec6b2ac27231c3656e0231c3656e0296daebe59Coverage 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