feat: add Blocked → Backlog legal transition (Demote)

Pipeline gap: the state machine refused `move_story(... target='backlog')`
from a Blocked story, leaving stuck items with no way to be parked while
waiting on dependent fixes — operators had to either Unblock (which
re-enters the active flow) or Archive (which loses the item).

Extend the existing Demote rule so `Blocked + Demote → Backlog` is a
legal transition, alongside the existing `Coding/Qa/Merge + Demote`.
Also update `map_stage_move_to_event` in agents/lifecycle.rs so the
chat/MCP `move_story` API recognises Blocked → backlog and routes it
through `PipelineEvent::Demote`.

Tests:
  - `blocked_demote_returns_to_backlog` — happy path.
  - `cannot_demote_from_done` / `cannot_demote_from_upcoming` — sanity
    checks that the broadened rule does NOT permit Demote from
    terminal or pre-triage stages.

Pattern follows 892 (MergeFailure → Done) and 893 (MergeFailure →
Coding) — pure transition.rs extension plus matching event mapping in
lifecycle.rs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Timmy
2026-05-12 13:13:18 +01:00
parent e955250474
commit f06492f540
2 changed files with 43 additions and 1 deletions
+7 -1
View File
@@ -217,7 +217,13 @@ pub fn transition(state: Stage, event: PipelineEvent) -> Result<Stage, Transitio
}),
// ── Demote: send an active item back to backlog ────────────────
(Coding, Demote) | (Qa, Demote) | (Merge { .. }, Demote) => Ok(Backlog),
// `Blocked + Demote → Backlog` lets operators park a stuck story in
// the backlog while waiting on dependent fixes, without losing it to
// Archived. Unlike `Unblock` (Blocked → Coding), this does not
// re-enter the active flow.
(Coding, Demote) | (Qa, Demote) | (Merge { .. }, Demote) | (Blocked { .. }, Demote) => {
Ok(Backlog)
}
// ── Close: direct completion from any active stage ─────────────
(Backlog, Close) | (Coding, Close) | (Qa, Close) | (Merge { .. }, Close) => Ok(Done {