Contracts

planned-rollback-message.md

Contract: move-task --to planned refusal message

Behavioral contract for _guard_planned_rollback (src/specify_cli/cli/commands/agent/tasks_transition_core.py). This contract defines observable behavior; it is deliberately expressed without a YAML/JSON schema block (no serialized artifact is defined here).

Preconditions

(not (feedback_provided and feedback_exists and feedback_is_file and feedback_content.strip())).

  • The move request targets planned (target_lane == Lane.PLANNED).
  • No valid, non-empty review-feedback file is supplied

Guarantees (invariant across all source lanes and force values)

package lane is unchanged.

rewind event out of a terminal lane).

state machine is consulted; no source lane reaches build_transition_plan.

  • G-1: The decision is RefuseExit1 (the CLI exits non-zero); the work
  • G-2: No status transition event is emitted (in particular, no force=true
  • G-3: The refusal is produced by _guard_planned_rollback before the

Message shape (the only behavior that changes)

Let reachable = Lane.PLANNED in wp_state_for(resolve_lane_alias(old_lane)).allowed_targets().

Arm A — reachable is true (in_review, in_progress, approved, genesis)

The message is the existing text, unchanged:

  • contains requires review feedback
  • contains the --review-feedback-file feedback.md guidance
  • contains This requirement cannot be bypassed with --force.

Arm B — reachable is false (blocked, canceled, done, uninitialized)

The message:

the lane is terminal when the set is empty (done, canceled).

(and MAY name --to canceled).

  • MUST NOT contain requires review feedback.
  • MUST state that planned is not reachable from the source lane.
  • MUST name the source lane's legal targets from allowed_targets(), or state
  • For blocked specifically, MUST name --to in_progress as the resume path

Worked cases (acceptance)

Source--forcefeedback fileResultMessage arm
blockednononerefuse, stays blockedB — names --to in_progress
blockedyesnonerefuse, stays blockedB
blockednofabricated non-emptyrefuse, stays blockedB (feedback cannot launder an illegal transition)
donenononerefuse, stays done, no rewind eventB — terminal
doneyesnonerefuse, stays doneB — terminal
canceledno/yesnonerefuse, stays canceledB — terminal
genesisno/yesnonerefuse, stays genesisA
in_reviewno/yesnonerefuse, stays in_reviewA — review-feedback text
in_revieweithervalid non-emptysucceed, now planned (exit 0)N/A (guard passes)

Non-goals (contract explicitly does NOT change)

fsm_parity_baseline.jsonl.

  • The four feedback-field allow/deny checks.
  • The _GUARDS tuple order/identity.
  • wp_state.py allow/deny (allowed_targets, force override) and
  • Legalizing blocked → planned (explicitly declined, decision 01M2Q4F897ANB84HA6EHA9DG98).