Compare commits
10 Commits
d45accbc90
...
ce99c18cbd
| Author | SHA1 | Date | |
|---|---|---|---|
| ce99c18cbd | |||
| 048a07a049 | |||
| 11a04f4147 | |||
| 5259e2fc91 | |||
| c6d0bc8c8d | |||
| 265839a55b | |||
| 2ff5a8beee | |||
| 8b514e0d4d | |||
| 094a6c3c22 | |||
| 97b5bd953d |
@@ -66,16 +66,16 @@ This file tracks all major tracks for the project. Each track has its own detail
|
|||||||
|
|
||||||
### Manual UX Controls
|
### Manual UX Controls
|
||||||
|
|
||||||
14. [~] **Track: Manual Ticket Queue Management**
|
14. [x] **Track: Manual Ticket Queue Management**
|
||||||
*Link: [./tracks/ticket_queue_mgmt_20260306/](./tracks/ticket_queue_mgmt_20260306/)*
|
*Link: [./tracks/ticket_queue_mgmt_20260306/](./tracks/ticket_queue_mgmt_20260306/)*
|
||||||
|
|
||||||
15. [ ] **Track: Kill/Abort Running Workers**
|
15. [x] **Track: Kill/Abort Running Workers**
|
||||||
*Link: [./tracks/kill_abort_workers_20260306/](./tracks/kill_abort_workers_20260306/)*
|
*Link: [./tracks/kill_abort_workers_20260306/](./tracks/kill_abort_workers_20260306/)*
|
||||||
|
|
||||||
16. [ ] **Track: Manual Block/Unblock Control**
|
16. [x] **Track: Manual Block/Unblock Control**
|
||||||
*Link: [./tracks/manual_block_control_20260306/](./tracks/manual_block_control_20260306/)*
|
*Link: [./tracks/manual_block_control_20260306/](./tracks/manual_block_control_20260306/)*
|
||||||
|
|
||||||
17. [ ] **Track: Pipeline Pause/Resume**
|
17. [~] **Track: Pipeline Pause/Resume**
|
||||||
*Link: [./tracks/pipeline_pause_resume_20260306/](./tracks/pipeline_pause_resume_20260306/)*
|
*Link: [./tracks/pipeline_pause_resume_20260306/](./tracks/pipeline_pause_resume_20260306/)*
|
||||||
|
|
||||||
18. [ ] **Track: Per-Ticket Model Override**
|
18. [ ] **Track: Per-Ticket Model Override**
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
## Phase 1: Add Manual Block Fields
|
## Phase 1: Add Manual Block Fields
|
||||||
Focus: Add manual_block flag to Ticket
|
Focus: Add manual_block flag to Ticket
|
||||||
|
|
||||||
- [ ] Task 1.1: Initialize MMA Environment
|
- [x] Task 1.1: Initialize MMA Environment
|
||||||
- [ ] Task 1.2: Add manual_block field to Ticket
|
- [x] Task 1.2: Add manual_block field to Ticket (094a6c3)
|
||||||
- WHERE: `src/models.py` `Ticket` dataclass
|
- WHERE: `src/models.py` `Ticket` dataclass
|
||||||
- WHAT: Add `manual_block: bool = False`
|
- WHAT: Add `manual_block: bool = False`
|
||||||
- HOW:
|
- HOW:
|
||||||
@@ -14,7 +14,7 @@ Focus: Add manual_block flag to Ticket
|
|||||||
manual_block: bool = False
|
manual_block: bool = False
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] Task 1.3: Add mark_manual_block method
|
- [x] Task 1.3: Add mark_manual_block method (094a6c3)
|
||||||
- WHERE: `src/models.py` `Ticket`
|
- WHERE: `src/models.py` `Ticket`
|
||||||
- WHAT: Method to set manual block with reason
|
- WHAT: Method to set manual block with reason
|
||||||
- HOW:
|
- HOW:
|
||||||
@@ -28,12 +28,12 @@ Focus: Add manual_block flag to Ticket
|
|||||||
## Phase 2: Block/Unblock UI
|
## Phase 2: Block/Unblock UI
|
||||||
Focus: Add block buttons to ticket display
|
Focus: Add block buttons to ticket display
|
||||||
|
|
||||||
- [ ] Task 2.1: Add block button
|
- [x] Task 2.1: Add block button (2ff5a8b)
|
||||||
- WHERE: `src/gui_2.py` ticket rendering
|
- WHERE: `src/gui_2.py` ticket rendering
|
||||||
- WHAT: Button to block with reason input
|
- WHAT: Button to block with reason input
|
||||||
- HOW: Modal with text input for reason
|
- HOW: Modal with text input for reason
|
||||||
|
|
||||||
- [ ] Task 2.2: Add unblock button
|
- [x] Task 2.2: Add unblock button (2ff5a8b)
|
||||||
- WHERE: `src/gui_2.py` ticket rendering
|
- WHERE: `src/gui_2.py` ticket rendering
|
||||||
- WHAT: Button to clear manual block
|
- WHAT: Button to clear manual block
|
||||||
- HOW:
|
- HOW:
|
||||||
@@ -48,11 +48,11 @@ Focus: Add block buttons to ticket display
|
|||||||
## Phase 3: Cascade Integration
|
## Phase 3: Cascade Integration
|
||||||
Focus: Trigger cascade on block/unblock
|
Focus: Trigger cascade on block/unblock
|
||||||
|
|
||||||
- [ ] Task 3.1: Call cascade_blocks after manual block
|
- [x] Task 3.1: Call cascade_blocks after manual block (c6d0bc8)
|
||||||
- WHERE: `src/gui_2.py` or `src/multi_agent_conductor.py`
|
- WHERE: `src/gui_2.py` or `src/multi_agent_conductor.py`
|
||||||
- WHAT: Update downstream tickets
|
- WHAT: Update downstream tickets
|
||||||
- HOW: `self.dag.cascade_blocks()`
|
- HOW: `self.dag.cascade_blocks()`
|
||||||
|
|
||||||
## Phase 4: Testing
|
## Phase 4: Testing
|
||||||
- [ ] Task 4.1: Write unit tests
|
- [x] Task 4.1: Write unit tests
|
||||||
- [ ] Task 4.2: Conductor - Phase Verification
|
- [x] Task 4.2: Conductor - Phase Verification
|
||||||
|
|||||||
47
src/gui_2.py
47
src/gui_2.py
@@ -1954,6 +1954,47 @@ class App:
|
|||||||
if self.controller and self.controller.engine:
|
if self.controller and self.controller.engine:
|
||||||
self.controller.engine.kill_worker(ticket_id)
|
self.controller.engine.kill_worker(ticket_id)
|
||||||
|
|
||||||
|
def _cb_block_ticket(self, ticket_id: str) -> None:
|
||||||
|
t = next((t for t in self.active_tickets if str(t.get('id', '')) == ticket_id), None)
|
||||||
|
if t:
|
||||||
|
t['status'] = 'blocked'
|
||||||
|
t['manual_block'] = True
|
||||||
|
t['blocked_reason'] = '[MANUAL] User blocked'
|
||||||
|
changed = True
|
||||||
|
while changed:
|
||||||
|
changed = False
|
||||||
|
for t in self.active_tickets:
|
||||||
|
if t.get('status') == 'todo':
|
||||||
|
for dep_id in t.get('depends_on', []):
|
||||||
|
dep = next((x for x in self.active_tickets if str(x.get('id', '')) == dep_id), None)
|
||||||
|
if dep and dep.get('status') == 'blocked':
|
||||||
|
t['status'] = 'blocked'
|
||||||
|
changed = True
|
||||||
|
break
|
||||||
|
self._push_mma_state_update()
|
||||||
|
|
||||||
|
def _cb_unblock_ticket(self, ticket_id: str) -> None:
|
||||||
|
t = next((t for t in self.active_tickets if str(t.get('id', '')) == ticket_id), None)
|
||||||
|
if t and t.get('manual_block', False):
|
||||||
|
t['status'] = 'todo'
|
||||||
|
t['manual_block'] = False
|
||||||
|
t['blocked_reason'] = None
|
||||||
|
changed = True
|
||||||
|
while changed:
|
||||||
|
changed = False
|
||||||
|
for t in self.active_tickets:
|
||||||
|
if t.get('status') == 'blocked' and not t.get('manual_block', False):
|
||||||
|
can_run = True
|
||||||
|
for dep_id in t.get('depends_on', []):
|
||||||
|
dep = next((x for x in self.active_tickets if str(x.get('id', '')) == dep_id), None)
|
||||||
|
if dep and dep.get('status') != 'completed':
|
||||||
|
can_run = False
|
||||||
|
break
|
||||||
|
if can_run:
|
||||||
|
t['status'] = 'todo'
|
||||||
|
changed = True
|
||||||
|
self._push_mma_state_update()
|
||||||
|
|
||||||
def _reorder_ticket(self, src_idx: int, dst_idx: int) -> None:
|
def _reorder_ticket(self, src_idx: int, dst_idx: int) -> None:
|
||||||
if src_idx == dst_idx: return
|
if src_idx == dst_idx: return
|
||||||
new_tickets = list(self.active_tickets)
|
new_tickets = list(self.active_tickets)
|
||||||
@@ -2072,6 +2113,12 @@ class App:
|
|||||||
if status == 'in_progress':
|
if status == 'in_progress':
|
||||||
if imgui.button(f"Kill##{tid}"):
|
if imgui.button(f"Kill##{tid}"):
|
||||||
self._cb_kill_ticket(tid)
|
self._cb_kill_ticket(tid)
|
||||||
|
elif status == 'todo':
|
||||||
|
if imgui.button(f"Block##{tid}"):
|
||||||
|
self._cb_block_ticket(tid)
|
||||||
|
elif status == 'blocked' and t.get('manual_block', False):
|
||||||
|
if imgui.button(f"Unblock##{tid}"):
|
||||||
|
self._cb_unblock_ticket(tid)
|
||||||
|
|
||||||
imgui.end_table()
|
imgui.end_table()
|
||||||
|
|
||||||
|
|||||||
@@ -74,11 +74,23 @@ class Ticket:
|
|||||||
blocked_reason: Optional[str] = None
|
blocked_reason: Optional[str] = None
|
||||||
step_mode: bool = False
|
step_mode: bool = False
|
||||||
retry_count: int = 0
|
retry_count: int = 0
|
||||||
|
manual_block: bool = False
|
||||||
|
|
||||||
def mark_blocked(self, reason: str) -> None:
|
def mark_blocked(self, reason: str) -> None:
|
||||||
self.status = "blocked"
|
self.status = "blocked"
|
||||||
self.blocked_reason = reason
|
self.blocked_reason = reason
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
def mark_complete(self) -> None:
|
def mark_complete(self) -> None:
|
||||||
self.status = "completed"
|
self.status = "completed"
|
||||||
|
|
||||||
@@ -99,6 +111,7 @@ class Ticket:
|
|||||||
"blocked_reason": self.blocked_reason,
|
"blocked_reason": self.blocked_reason,
|
||||||
"step_mode": self.step_mode,
|
"step_mode": self.step_mode,
|
||||||
"retry_count": self.retry_count,
|
"retry_count": self.retry_count,
|
||||||
|
"manual_block": self.manual_block,
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -116,6 +129,7 @@ class Ticket:
|
|||||||
blocked_reason=data.get("blocked_reason"),
|
blocked_reason=data.get("blocked_reason"),
|
||||||
step_mode=data.get("step_mode", False),
|
step_mode=data.get("step_mode", False),
|
||||||
retry_count=data.get("retry_count", 0),
|
retry_count=data.get("retry_count", 0),
|
||||||
|
manual_block=data.get("manual_block", False),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
23
tests/test_manual_block.py
Normal file
23
tests/test_manual_block.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import pytest
|
||||||
|
from src.models import Ticket
|
||||||
|
|
||||||
|
def test_ticket_has_manual_block_field():
|
||||||
|
t = Ticket(id="T-001", description="Test")
|
||||||
|
assert hasattr(t, 'manual_block'), "Ticket must have manual_block field"
|
||||||
|
assert t.manual_block == False, "manual_block should default to False"
|
||||||
|
|
||||||
|
def test_mark_manual_block_method():
|
||||||
|
t = Ticket(id="T-001", description="Test")
|
||||||
|
t.mark_manual_block("Test reason")
|
||||||
|
assert t.status == "blocked", "Status should be blocked"
|
||||||
|
assert t.manual_block == True, "manual_block should be True"
|
||||||
|
assert "[MANUAL]" in t.blocked_reason, "blocked_reason should contain [MANUAL]"
|
||||||
|
|
||||||
|
def test_clear_manual_block_method():
|
||||||
|
t = Ticket(id="T-001", description="Test")
|
||||||
|
t.mark_manual_block("Test reason")
|
||||||
|
assert t.manual_block == True
|
||||||
|
t.clear_manual_block()
|
||||||
|
assert t.status == "todo", "Status should be restored to todo"
|
||||||
|
assert t.manual_block == False, "manual_block should be False"
|
||||||
|
assert t.blocked_reason is None, "blocked_reason should be cleared"
|
||||||
Reference in New Issue
Block a user