From a419c53592ed13a8289a4ea9890e3202b2f4fdbb Mon Sep 17 00:00:00 2001 From: jordan Date: Sun, 8 Feb 2026 14:21:05 -0700 Subject: [PATCH] fix(sdlc): make phase transitions idempotent Allow transitioning to the current phase (no-op success) instead of rejecting it as a "backward" transition. This fixes issues where external systems retry transition commands. Before: draft -> draft returned error After: draft -> draft returns nil (already there) Co-Authored-By: Claude Sonnet 4.5 --- internal/sdlc/feature.go | 8 +++++++- internal/sdlc/feature_test.go | 6 +++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/internal/sdlc/feature.go b/internal/sdlc/feature.go index 164d3b0..bbfb0bc 100644 --- a/internal/sdlc/feature.go +++ b/internal/sdlc/feature.go @@ -140,7 +140,13 @@ func (f *Feature) CanTransitionTo(target FeaturePhase, cfg *Config) error { currentIdx := PhaseIndex(f.Phase) targetIdx := PhaseIndex(target) - if targetIdx <= currentIdx { + // Idempotent: if already in target phase, return success + if targetIdx == currentIdx { + return nil + } + + // Reject backward transitions + if targetIdx < currentIdx { return fmt.Errorf("%w: cannot move from %s to %s (backward)", ErrInvalidTransition, f.Phase, target) } diff --git a/internal/sdlc/feature_test.go b/internal/sdlc/feature_test.go index ea6d37b..0624841 100644 --- a/internal/sdlc/feature_test.go +++ b/internal/sdlc/feature_test.go @@ -128,9 +128,9 @@ func TestCanTransitionTo(t *testing.T) { t.Error("CanTransitionTo(planned) = nil, want error (skip)") } - // Invalid: draft -> draft (backward) - if err := f.CanTransitionTo(PhaseDraft, cfg); err == nil { - t.Error("CanTransitionTo(draft) = nil, want error (backward)") + // Idempotent: draft -> draft (allowed, returns nil) + if err := f.CanTransitionTo(PhaseDraft, cfg); err != nil { + t.Errorf("CanTransitionTo(draft) = %v, want nil (idempotent)", err) } // Invalid phase