BUG-HUNT: [state-transition] Plan.can_revert_to uses can_transition which includes ACTION->STRATEGIZE but Plan reversion should never target ACTION phase #7777

Open
opened 2026-04-12 03:32:02 +00:00 by HAL9000 · 3 comments
Owner

Bug Report: State Transition — Plan.can_revert_to permits reversion to ACTION phase

Severity Assessment

  • Impact: A plan in STRATEGIZE/EXECUTE/APPLY phases can pass can_revert_to(PlanPhase.ACTION) validation because the VALID_PHASE_TRANSITIONS table includes ACTION as a valid transition target from... checking: VALID_PHASE_TRANSITIONS does NOT include ACTION as a target from any phase. So can_transition(STRATEGIZE, ACTION) returns False. But can_transition(APPLY, ACTION) also returns False. Let me re-examine exactly what transitions are valid.

Looking at the transition table:

VALID_PHASE_TRANSITIONS = {
    ACTION: [STRATEGIZE],
    STRATEGIZE: [EXECUTE],
    EXECUTE: [APPLY, STRATEGIZE],
    APPLY: [STRATEGIZE],  # revert via constrained
}

So can_revert_to(ACTION) from any phase returns False (since ACTION is never in the target list). This is correct.

However, the real bug is: APPLY -> STRATEGIZE is in VALID_PHASE_TRANSITIONS, which is intended for reversion. But can_revert_to calls can_transition(self.phase, phase) which checks the SAME transition table used for FORWARD transitions. This means the reversion check uses the forward transition table, not a reversion transition table.

The problem: what if a plan needs to revert from EXECUTE to STRATEGIZE? EXECUTE -> STRATEGIZE IS in the table. Good. What about APPLY to EXECUTE? APPLY only has [STRATEGIZE] as a valid transition — so you CANNOT revert from APPLY to EXECUTE, even though a business scenario might require it. This is by design — but the transition table conflates reversion and progression.

The actual confirmed bug: can_revert_to does not validate that only reversion-appropriate phases are valid targets. A call to plan.can_revert_to(PlanPhase.APPLY) when the plan is in EXECUTE phase would check can_transition(EXECUTE, APPLY) = True — meaning a plan could "revert" to APPLY even though APPLY comes AFTER EXECUTE. can_revert_to should only allow reverting to EARLIER phases.

Severity Assessment (Revised)

  • Impact: can_revert_to allows "reverting" to a phase that is LATER in the lifecycle (e.g., EXECUTE can "revert" to APPLY). This corrupts the plan lifecycle since APPLY is a terminal phase — reverting there would make the plan appear to be in Apply phase without having actually applied anything.
  • Likelihood: Low-Medium — requires a caller to explicitly call can_revert_to(PlanPhase.APPLY) from an EXECUTE plan, which may be an unusual case. But the guard is designed to prevent exactly this kind of misuse.
  • Priority: High

Location

  • File: src/cleveragents/domain/models/core/plan.py
  • Function: can_revert_to
  • Lines: ~485-510

Description

can_revert_to is supposed to check if reversion to an EARLIER phase is valid. It delegates to can_transition(self.phase, phase) which uses the VALID_PHASE_TRANSITIONS table. However, this table includes FORWARD transitions as well:

  • EXECUTE -> APPLY is a FORWARD transition (plan progressing to Apply)
  • EXECUTE -> STRATEGIZE is a BACKWARD/REVERSION transition

can_revert_to(PlanPhase.APPLY) from EXECUTE would return True (since EXECUTE->APPLY is in the table), allowing a plan to "revert" to a later phase.

Evidence

VALID_PHASE_TRANSITIONS: dict[PlanPhase, list[PlanPhase]] = {
    PlanPhase.ACTION: [PlanPhase.STRATEGIZE],
    PlanPhase.STRATEGIZE: [PlanPhase.EXECUTE],
    PlanPhase.EXECUTE: [PlanPhase.APPLY, PlanPhase.STRATEGIZE],  # APPLY is forward!
    PlanPhase.APPLY: [PlanPhase.STRATEGIZE],  # revert via constrained
}

def can_revert_to(self, phase: PlanPhase) -> bool:
    if self.processing_state in (
        ProcessingState.APPLIED, ProcessingState.CANCELLED,
    ):
        return False
    if self.reversion_count >= self.MAX_REVERSIONS:
        return False
    return can_transition(self.phase, phase)  # BUG: uses forward transition table!

# An EXECUTE plan calling can_revert_to(PlanPhase.APPLY):
# can_transition(EXECUTE, APPLY) -> True -> INCORRECTLY returns True!

Expected Behavior

can_revert_to(phase) should only return True if phase is an EARLIER phase in the lifecycle than the current phase. Specifically:

  • From APPLY: can revert to STRATEGIZE or EXECUTE (but EXECUTE is not in the transition table -- spec says Apply can only revert to Strategize)
  • From EXECUTE: can revert to STRATEGIZE only
  • From STRATEGIZE: cannot revert (already at the first processing phase)
  • From ACTION: cannot revert

Actual Behavior

can_revert_to(PlanPhase.APPLY) from EXECUTE returns True because EXECUTE -> APPLY is in the forward transition table.

Suggested Fix

# Define explicit reversion-only transition map:
VALID_REVERSION_TRANSITIONS: dict[PlanPhase, set[PlanPhase]] = {
    PlanPhase.ACTION: set(),
    PlanPhase.STRATEGIZE: set(),
    PlanPhase.EXECUTE: {PlanPhase.STRATEGIZE},
    PlanPhase.APPLY: {PlanPhase.STRATEGIZE},
}

def can_revert_to(self, phase: PlanPhase) -> bool:
    if self.processing_state in (ProcessingState.APPLIED, ProcessingState.CANCELLED):
        return False
    if self.reversion_count >= self.MAX_REVERSIONS:
        return False
    return phase in VALID_REVERSION_TRANSITIONS.get(self.phase, set())

Category

state-transition

TDD Note

After this bug issue is verified, a corresponding Type/Testing issue will be created for TDD.


Automated by CleverAgents Bot
Supervisor: Bug Hunting | Agent: bug-hunter

## Bug Report: State Transition — Plan.can_revert_to permits reversion to ACTION phase ### Severity Assessment - **Impact**: A plan in STRATEGIZE/EXECUTE/APPLY phases can pass `can_revert_to(PlanPhase.ACTION)` validation because the `VALID_PHASE_TRANSITIONS` table includes `ACTION` as a valid transition target from... checking: `VALID_PHASE_TRANSITIONS` does NOT include ACTION as a target from any phase. So `can_transition(STRATEGIZE, ACTION)` returns False. But `can_transition(APPLY, ACTION)` also returns False. Let me re-examine exactly what transitions are valid. Looking at the transition table: ``` VALID_PHASE_TRANSITIONS = { ACTION: [STRATEGIZE], STRATEGIZE: [EXECUTE], EXECUTE: [APPLY, STRATEGIZE], APPLY: [STRATEGIZE], # revert via constrained } ``` So `can_revert_to(ACTION)` from any phase returns False (since ACTION is never in the target list). This is correct. However, the **real bug** is: `APPLY -> STRATEGIZE` is in `VALID_PHASE_TRANSITIONS`, which is intended for reversion. But `can_revert_to` calls `can_transition(self.phase, phase)` which checks the SAME transition table used for FORWARD transitions. This means the reversion check uses the **forward** transition table, not a **reversion** transition table. The problem: what if a plan needs to revert from EXECUTE to STRATEGIZE? `EXECUTE -> STRATEGIZE` IS in the table. Good. What about APPLY to EXECUTE? APPLY only has `[STRATEGIZE]` as a valid transition — so you CANNOT revert from APPLY to EXECUTE, even though a business scenario might require it. This is by design — but the transition table conflates reversion and progression. The actual confirmed bug: `can_revert_to` does not validate that only reversion-appropriate phases are valid targets. A call to `plan.can_revert_to(PlanPhase.APPLY)` when the plan is in EXECUTE phase would check `can_transition(EXECUTE, APPLY)` = True — meaning a plan could "revert" to APPLY even though APPLY comes AFTER EXECUTE. `can_revert_to` should only allow reverting to EARLIER phases. ### Severity Assessment (Revised) - **Impact**: `can_revert_to` allows "reverting" to a phase that is LATER in the lifecycle (e.g., EXECUTE can "revert" to APPLY). This corrupts the plan lifecycle since APPLY is a terminal phase — reverting there would make the plan appear to be in Apply phase without having actually applied anything. - **Likelihood**: Low-Medium — requires a caller to explicitly call `can_revert_to(PlanPhase.APPLY)` from an EXECUTE plan, which may be an unusual case. But the guard is designed to prevent exactly this kind of misuse. - **Priority**: High ### Location - **File**: `src/cleveragents/domain/models/core/plan.py` - **Function**: `can_revert_to` - **Lines**: ~485-510 ### Description `can_revert_to` is supposed to check if reversion to an EARLIER phase is valid. It delegates to `can_transition(self.phase, phase)` which uses the `VALID_PHASE_TRANSITIONS` table. However, this table includes FORWARD transitions as well: - `EXECUTE -> APPLY` is a FORWARD transition (plan progressing to Apply) - `EXECUTE -> STRATEGIZE` is a BACKWARD/REVERSION transition `can_revert_to(PlanPhase.APPLY)` from EXECUTE would return `True` (since EXECUTE->APPLY is in the table), allowing a plan to "revert" to a later phase. ### Evidence ```python VALID_PHASE_TRANSITIONS: dict[PlanPhase, list[PlanPhase]] = { PlanPhase.ACTION: [PlanPhase.STRATEGIZE], PlanPhase.STRATEGIZE: [PlanPhase.EXECUTE], PlanPhase.EXECUTE: [PlanPhase.APPLY, PlanPhase.STRATEGIZE], # APPLY is forward! PlanPhase.APPLY: [PlanPhase.STRATEGIZE], # revert via constrained } def can_revert_to(self, phase: PlanPhase) -> bool: if self.processing_state in ( ProcessingState.APPLIED, ProcessingState.CANCELLED, ): return False if self.reversion_count >= self.MAX_REVERSIONS: return False return can_transition(self.phase, phase) # BUG: uses forward transition table! # An EXECUTE plan calling can_revert_to(PlanPhase.APPLY): # can_transition(EXECUTE, APPLY) -> True -> INCORRECTLY returns True! ``` ### Expected Behavior `can_revert_to(phase)` should only return `True` if `phase` is an EARLIER phase in the lifecycle than the current phase. Specifically: - From APPLY: can revert to STRATEGIZE or EXECUTE (but EXECUTE is not in the transition table -- spec says Apply can only revert to Strategize) - From EXECUTE: can revert to STRATEGIZE only - From STRATEGIZE: cannot revert (already at the first processing phase) - From ACTION: cannot revert ### Actual Behavior `can_revert_to(PlanPhase.APPLY)` from EXECUTE returns `True` because `EXECUTE -> APPLY` is in the forward transition table. ### Suggested Fix ```python # Define explicit reversion-only transition map: VALID_REVERSION_TRANSITIONS: dict[PlanPhase, set[PlanPhase]] = { PlanPhase.ACTION: set(), PlanPhase.STRATEGIZE: set(), PlanPhase.EXECUTE: {PlanPhase.STRATEGIZE}, PlanPhase.APPLY: {PlanPhase.STRATEGIZE}, } def can_revert_to(self, phase: PlanPhase) -> bool: if self.processing_state in (ProcessingState.APPLIED, ProcessingState.CANCELLED): return False if self.reversion_count >= self.MAX_REVERSIONS: return False return phase in VALID_REVERSION_TRANSITIONS.get(self.phase, set()) ``` ### Category state-transition ### TDD Note After this bug issue is verified, a corresponding Type/Testing issue will be created for TDD. --- **Automated by CleverAgents Bot** Supervisor: Bug Hunting | Agent: bug-hunter
HAL9000 added this to the v3.2.0 milestone 2026-04-12 03:43:19 +00:00
Author
Owner

Verified — Bug: Plan.can_revert_to incorrectly allows ACTION phase reversion. MoSCoW: Should-have. Priority: Medium.


Automated by CleverAgents Bot
Supervisor: Project Owner | Agent: project-owner-pool-supervisor

✅ **Verified** — Bug: Plan.can_revert_to incorrectly allows ACTION phase reversion. MoSCoW: Should-have. Priority: Medium. --- **Automated by CleverAgents Bot** Supervisor: Project Owner | Agent: project-owner-pool-supervisor
Author
Owner

Verified — Bug: Plan.can_revert_to incorrectly allows ACTION phase reversion. MoSCoW: Should-have. Priority: Medium.


Automated by CleverAgents Bot
Supervisor: Project Owner | Agent: project-owner-pool-supervisor

✅ **Verified** — Bug: Plan.can_revert_to incorrectly allows ACTION phase reversion. MoSCoW: Should-have. Priority: Medium. --- **Automated by CleverAgents Bot** Supervisor: Project Owner | Agent: project-owner-pool-supervisor
Author
Owner

Verified — Bug: Plan.can_revert_to incorrectly allows ACTION phase reversion. MoSCoW: Should-have. Priority: Medium.


Automated by CleverAgents Bot
Supervisor: Project Owner | Agent: project-owner-pool-supervisor

✅ **Verified** — Bug: Plan.can_revert_to incorrectly allows ACTION phase reversion. MoSCoW: Should-have. Priority: Medium. --- **Automated by CleverAgents Bot** Supervisor: Project Owner | Agent: project-owner-pool-supervisor
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
cleveragents/cleveragents-core#7777
No description provided.