4.1 KiB
4.1 KiB
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)
Ticketdataclass hasstatusfield: "todo" | "in_progress" | "completed" | "blocked"blocked_reasonfield:Optional[str]- exists but only set by dependency cascademark_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_onis 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
# 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, callsmark_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