# Track Specification: Manual Block/Unblock Control (manual_block_control_20260306) ## Overview Allow user to manually block or unblock tickets with custom reasons. Currently blocked tickets rely solely on dependency resolution; add manual override capability. ## Current State Audit ### Already Implemented (DO NOT re-implement) #### Ticket Status (src/models.py) - **`Ticket` dataclass** has `status` field: "todo" | "in_progress" | "completed" | "blocked" - **`blocked_reason` field**: `Optional[str]` - exists but only set by dependency cascade - **`mark_blocked(reason: str)` method**: Sets status="blocked", stores reason #### DAG Blocking (src/dag_engine.py) - **`cascade_blocks()` method**: Transitively marks tickets as blocked when dependencies are blocked - **Dependency resolution**: Tickets blocked if any `depends_on` is not "completed" - **No manual override exists** #### GUI Display (src/gui_2.py) - **`_render_ticket_dag_node()`**: Renders ticket nodes with status colors - **Blocked nodes shown in distinct color** - **No block/unblock buttons** ### Gaps to Fill (This Track's Scope) - No way to manually set blocked status - No way to add custom block reason - No way to manually unblock (clear blocked status) - Visual indicator for manual vs dependency blocking ## Architectural Constraints ### DAG Validity - Manual block MUST trigger cascade to downstream tickets - Manual unblock MUST check dependencies are satisfied - Cannot unblock if dependencies still blocked ### Audit Trail - Block reason MUST be stored in Ticket - Distinguish manual vs dependency blocking ### State Synchronization - Block/unblock MUST update GUI immediately - MUST persist to track state ## Architecture Reference ### Key Integration Points | File | Lines | Purpose | |------|-------|---------| | `src/models.py` | 40-60 | `Ticket.mark_blocked()`, `blocked_reason` | | `src/dag_engine.py` | 30-50 | `cascade_blocks()` - call after manual block | | `src/gui_2.py` | 2700-2800 | `_render_ticket_dag_node()` - add buttons | | `src/project_manager.py` | 238-260 | Track state persistence | ### Proposed Ticket Enhancement ```python # Add to Ticket dataclass: manual_block: bool = False # True if blocked manually, False if dependency def mark_manual_block(self, reason: str) -> None: self.status = "blocked" self.blocked_reason = f"[MANUAL] {reason}" self.manual_block = True def clear_manual_block(self) -> None: if self.manual_block: self.status = "todo" self.blocked_reason = None self.manual_block = False ``` ## Functional Requirements ### FR1: Block Button - Button on each ticket node to block - Opens text input for block reason - Sets `manual_block=True`, calls `mark_manual_block()` ### FR2: Unblock Button - Button on blocked tickets to unblock - Only enabled if dependencies are satisfied - Clears manual block, sets status to "todo" ### FR3: Reason Display - Show block reason on hover or in node - Different visual for manual vs dependency block - Show "[MANUAL]" prefix for manual blocks ### FR4: Cascade Integration - Manual block triggers `cascade_blocks()` - Manual unblock recalculates blocked status ## Non-Functional Requirements | Requirement | Constraint | |-------------|------------| | Response Time | Block/unblock takes effect immediately | | Persistence | Block state saved to track state | | Visual Clarity | Manual blocks clearly distinguished | ## Testing Requirements ### Unit Tests - Test `mark_manual_block()` sets correct fields - Test `clear_manual_block()` restores todo status - Test cascade after manual block ### Integration Tests (via `live_gui` fixture) - Block ticket via GUI, verify status changes - Unblock ticket, verify status restored - Verify cascade affects downstream tickets ## Out of Scope - Blocking during execution (kill first, then block) - Scheduled/conditional blocking - Block templates ## Acceptance Criteria - [ ] Block button on each ticket - [ ] Unblock button on blocked tickets - [ ] Reason input saves to ticket - [ ] Visual indicator distinguishes manual vs dependency - [ ] Reason displayed in UI - [ ] Cascade triggered on block/unblock - [ ] State persisted to track state - [ ] 1-space indentation maintained