forked from Rockachopa/Timmy-time-dashboard
feat: add task queue with human-in-the-loop approval + work orders + UI bug fixes
Task Queue system: - New /tasks page with three-column layout (Pending/Active/Completed) - Full CRUD API at /api/tasks with approve/veto/modify/pause/cancel/retry - SQLite persistence in task_queue table - WebSocket live updates via ws_manager - Create task modal with agent assignment and priority - Auto-approve rules for low-risk tasks - HTMX polling for real-time column updates - HOME TASK buttons now link to task queue with agent pre-selected - MARKET HIRE buttons link to task queue with agent pre-selected Work Order system: - External submission API for agents/users (POST /work-orders/submit) - Risk scoring and configurable auto-execution thresholds - Dashboard at /work-orders/queue with approve/reject/execute flow - Integration with swarm task system for execution UI & Dashboard bug fixes: - EVENTS: add startup event so page is never empty - LEDGER: fix empty filter params in URL - MISSION CONTROL: LLM backend and model now read from /health - MISSION CONTROL: agent count fallback to /swarm/agents - SWARM: HTMX fallback loads initial data if WebSocket is slow - MEMORY: add edit/delete buttons for personal facts - UPGRADES: add empty state guidance with links - BRIEFING: add regenerate button and POST /briefing/regenerate endpoint Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1
src/work_orders/__init__.py
Normal file
1
src/work_orders/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Work Order system for external and internal task submission."""
|
||||
49
src/work_orders/executor.py
Normal file
49
src/work_orders/executor.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""Work order execution — bridges work orders to self-modify and swarm."""
|
||||
|
||||
import logging
|
||||
|
||||
from work_orders.models import WorkOrder, WorkOrderCategory
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WorkOrderExecutor:
|
||||
"""Dispatches approved work orders to the appropriate execution backend."""
|
||||
|
||||
def execute(self, wo: WorkOrder) -> tuple[bool, str]:
|
||||
"""Execute a work order.
|
||||
|
||||
Returns:
|
||||
(success, result_message) tuple
|
||||
"""
|
||||
if self._is_code_task(wo):
|
||||
return self._execute_via_swarm(wo, code_hint=True)
|
||||
return self._execute_via_swarm(wo)
|
||||
|
||||
def _is_code_task(self, wo: WorkOrder) -> bool:
|
||||
"""Check if this work order involves code changes."""
|
||||
code_categories = {WorkOrderCategory.BUG, WorkOrderCategory.OPTIMIZATION}
|
||||
if wo.category in code_categories:
|
||||
return True
|
||||
if wo.related_files:
|
||||
return any(f.endswith(".py") for f in wo.related_files)
|
||||
return False
|
||||
|
||||
def _execute_via_swarm(self, wo: WorkOrder, code_hint: bool = False) -> tuple[bool, str]:
|
||||
"""Dispatch as a swarm task for agent bidding."""
|
||||
try:
|
||||
from swarm.coordinator import coordinator
|
||||
prefix = "[Code] " if code_hint else ""
|
||||
description = f"{prefix}[WO-{wo.id[:8]}] {wo.title}"
|
||||
if wo.description:
|
||||
description += f": {wo.description}"
|
||||
task = coordinator.post_task(description)
|
||||
logger.info("Work order %s dispatched as swarm task %s", wo.id[:8], task.id)
|
||||
return True, f"Dispatched as swarm task {task.id}"
|
||||
except Exception as exc:
|
||||
logger.error("Failed to dispatch work order %s: %s", wo.id[:8], exc)
|
||||
return False, str(exc)
|
||||
|
||||
|
||||
# Module-level singleton
|
||||
work_order_executor = WorkOrderExecutor()
|
||||
286
src/work_orders/models.py
Normal file
286
src/work_orders/models.py
Normal file
@@ -0,0 +1,286 @@
|
||||
"""Database models for Work Order system."""
|
||||
|
||||
import json
|
||||
import sqlite3
|
||||
import uuid
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
DB_PATH = Path("data/swarm.db")
|
||||
|
||||
|
||||
class WorkOrderStatus(str, Enum):
|
||||
SUBMITTED = "submitted"
|
||||
TRIAGED = "triaged"
|
||||
APPROVED = "approved"
|
||||
IN_PROGRESS = "in_progress"
|
||||
COMPLETED = "completed"
|
||||
REJECTED = "rejected"
|
||||
|
||||
|
||||
class WorkOrderPriority(str, Enum):
|
||||
CRITICAL = "critical"
|
||||
HIGH = "high"
|
||||
MEDIUM = "medium"
|
||||
LOW = "low"
|
||||
|
||||
|
||||
class WorkOrderCategory(str, Enum):
|
||||
BUG = "bug"
|
||||
FEATURE = "feature"
|
||||
IMPROVEMENT = "improvement"
|
||||
OPTIMIZATION = "optimization"
|
||||
SUGGESTION = "suggestion"
|
||||
|
||||
|
||||
@dataclass
|
||||
class WorkOrder:
|
||||
"""A work order / suggestion submitted by a user or agent."""
|
||||
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
||||
title: str = ""
|
||||
description: str = ""
|
||||
priority: WorkOrderPriority = WorkOrderPriority.MEDIUM
|
||||
category: WorkOrderCategory = WorkOrderCategory.SUGGESTION
|
||||
status: WorkOrderStatus = WorkOrderStatus.SUBMITTED
|
||||
submitter: str = "unknown"
|
||||
submitter_type: str = "user" # user | agent | system
|
||||
estimated_effort: Optional[str] = None # small | medium | large
|
||||
related_files: list[str] = field(default_factory=list)
|
||||
execution_mode: Optional[str] = None # auto | manual
|
||||
swarm_task_id: Optional[str] = None
|
||||
result: Optional[str] = None
|
||||
rejection_reason: Optional[str] = None
|
||||
created_at: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
|
||||
triaged_at: Optional[str] = None
|
||||
approved_at: Optional[str] = None
|
||||
started_at: Optional[str] = None
|
||||
completed_at: Optional[str] = None
|
||||
updated_at: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
|
||||
|
||||
|
||||
def _get_conn() -> sqlite3.Connection:
|
||||
"""Get database connection with schema initialized."""
|
||||
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
conn = sqlite3.connect(str(DB_PATH))
|
||||
conn.row_factory = sqlite3.Row
|
||||
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS work_orders (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
priority TEXT NOT NULL DEFAULT 'medium',
|
||||
category TEXT NOT NULL DEFAULT 'suggestion',
|
||||
status TEXT NOT NULL DEFAULT 'submitted',
|
||||
submitter TEXT NOT NULL DEFAULT 'unknown',
|
||||
submitter_type TEXT NOT NULL DEFAULT 'user',
|
||||
estimated_effort TEXT,
|
||||
related_files TEXT,
|
||||
execution_mode TEXT,
|
||||
swarm_task_id TEXT,
|
||||
result TEXT,
|
||||
rejection_reason TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
triaged_at TEXT,
|
||||
approved_at TEXT,
|
||||
started_at TEXT,
|
||||
completed_at TEXT,
|
||||
updated_at TEXT NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_wo_status ON work_orders(status)")
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_wo_priority ON work_orders(priority)")
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_wo_submitter ON work_orders(submitter)")
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_wo_created ON work_orders(created_at)")
|
||||
conn.commit()
|
||||
return conn
|
||||
|
||||
|
||||
def _row_to_work_order(row: sqlite3.Row) -> WorkOrder:
|
||||
"""Convert a database row to a WorkOrder."""
|
||||
return WorkOrder(
|
||||
id=row["id"],
|
||||
title=row["title"],
|
||||
description=row["description"],
|
||||
priority=WorkOrderPriority(row["priority"]),
|
||||
category=WorkOrderCategory(row["category"]),
|
||||
status=WorkOrderStatus(row["status"]),
|
||||
submitter=row["submitter"],
|
||||
submitter_type=row["submitter_type"],
|
||||
estimated_effort=row["estimated_effort"],
|
||||
related_files=json.loads(row["related_files"]) if row["related_files"] else [],
|
||||
execution_mode=row["execution_mode"],
|
||||
swarm_task_id=row["swarm_task_id"],
|
||||
result=row["result"],
|
||||
rejection_reason=row["rejection_reason"],
|
||||
created_at=row["created_at"],
|
||||
triaged_at=row["triaged_at"],
|
||||
approved_at=row["approved_at"],
|
||||
started_at=row["started_at"],
|
||||
completed_at=row["completed_at"],
|
||||
updated_at=row["updated_at"],
|
||||
)
|
||||
|
||||
|
||||
def create_work_order(
|
||||
title: str,
|
||||
description: str = "",
|
||||
priority: str = "medium",
|
||||
category: str = "suggestion",
|
||||
submitter: str = "unknown",
|
||||
submitter_type: str = "user",
|
||||
estimated_effort: Optional[str] = None,
|
||||
related_files: Optional[list[str]] = None,
|
||||
) -> WorkOrder:
|
||||
"""Create a new work order."""
|
||||
wo = WorkOrder(
|
||||
title=title,
|
||||
description=description,
|
||||
priority=WorkOrderPriority(priority),
|
||||
category=WorkOrderCategory(category),
|
||||
submitter=submitter,
|
||||
submitter_type=submitter_type,
|
||||
estimated_effort=estimated_effort,
|
||||
related_files=related_files or [],
|
||||
)
|
||||
|
||||
conn = _get_conn()
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO work_orders (
|
||||
id, title, description, priority, category, status,
|
||||
submitter, submitter_type, estimated_effort, related_files,
|
||||
created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
wo.id, wo.title, wo.description,
|
||||
wo.priority.value, wo.category.value, wo.status.value,
|
||||
wo.submitter, wo.submitter_type, wo.estimated_effort,
|
||||
json.dumps(wo.related_files) if wo.related_files else None,
|
||||
wo.created_at, wo.updated_at,
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return wo
|
||||
|
||||
|
||||
def get_work_order(wo_id: str) -> Optional[WorkOrder]:
|
||||
"""Get a work order by ID."""
|
||||
conn = _get_conn()
|
||||
row = conn.execute(
|
||||
"SELECT * FROM work_orders WHERE id = ?", (wo_id,)
|
||||
).fetchone()
|
||||
conn.close()
|
||||
if not row:
|
||||
return None
|
||||
return _row_to_work_order(row)
|
||||
|
||||
|
||||
def list_work_orders(
|
||||
status: Optional[WorkOrderStatus] = None,
|
||||
priority: Optional[WorkOrderPriority] = None,
|
||||
category: Optional[WorkOrderCategory] = None,
|
||||
submitter: Optional[str] = None,
|
||||
limit: int = 100,
|
||||
) -> list[WorkOrder]:
|
||||
"""List work orders with optional filters."""
|
||||
conn = _get_conn()
|
||||
conditions = []
|
||||
params: list = []
|
||||
|
||||
if status:
|
||||
conditions.append("status = ?")
|
||||
params.append(status.value)
|
||||
if priority:
|
||||
conditions.append("priority = ?")
|
||||
params.append(priority.value)
|
||||
if category:
|
||||
conditions.append("category = ?")
|
||||
params.append(category.value)
|
||||
if submitter:
|
||||
conditions.append("submitter = ?")
|
||||
params.append(submitter)
|
||||
|
||||
where = "WHERE " + " AND ".join(conditions) if conditions else ""
|
||||
rows = conn.execute(
|
||||
f"SELECT * FROM work_orders {where} ORDER BY created_at DESC LIMIT ?",
|
||||
params + [limit],
|
||||
).fetchall()
|
||||
conn.close()
|
||||
return [_row_to_work_order(r) for r in rows]
|
||||
|
||||
|
||||
def update_work_order_status(
|
||||
wo_id: str,
|
||||
new_status: WorkOrderStatus,
|
||||
**kwargs,
|
||||
) -> Optional[WorkOrder]:
|
||||
"""Update a work order's status and optional fields."""
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
sets = ["status = ?", "updated_at = ?"]
|
||||
params: list = [new_status.value, now]
|
||||
|
||||
# Auto-set timestamp fields based on status transition
|
||||
timestamp_map = {
|
||||
WorkOrderStatus.TRIAGED: "triaged_at",
|
||||
WorkOrderStatus.APPROVED: "approved_at",
|
||||
WorkOrderStatus.IN_PROGRESS: "started_at",
|
||||
WorkOrderStatus.COMPLETED: "completed_at",
|
||||
WorkOrderStatus.REJECTED: "completed_at",
|
||||
}
|
||||
ts_field = timestamp_map.get(new_status)
|
||||
if ts_field:
|
||||
sets.append(f"{ts_field} = ?")
|
||||
params.append(now)
|
||||
|
||||
# Apply additional keyword fields
|
||||
allowed_fields = {
|
||||
"execution_mode", "swarm_task_id", "result",
|
||||
"rejection_reason", "estimated_effort",
|
||||
}
|
||||
for key, val in kwargs.items():
|
||||
if key in allowed_fields:
|
||||
sets.append(f"{key} = ?")
|
||||
params.append(val)
|
||||
|
||||
params.append(wo_id)
|
||||
conn = _get_conn()
|
||||
cursor = conn.execute(
|
||||
f"UPDATE work_orders SET {', '.join(sets)} WHERE id = ?",
|
||||
params,
|
||||
)
|
||||
conn.commit()
|
||||
updated = cursor.rowcount > 0
|
||||
conn.close()
|
||||
|
||||
if not updated:
|
||||
return None
|
||||
return get_work_order(wo_id)
|
||||
|
||||
|
||||
def get_pending_count() -> int:
|
||||
"""Get count of submitted/triaged work orders awaiting review."""
|
||||
conn = _get_conn()
|
||||
row = conn.execute(
|
||||
"SELECT COUNT(*) as count FROM work_orders WHERE status IN (?, ?)",
|
||||
(WorkOrderStatus.SUBMITTED.value, WorkOrderStatus.TRIAGED.value),
|
||||
).fetchone()
|
||||
conn.close()
|
||||
return row["count"]
|
||||
|
||||
|
||||
def get_counts_by_status() -> dict[str, int]:
|
||||
"""Get work order counts grouped by status."""
|
||||
conn = _get_conn()
|
||||
rows = conn.execute(
|
||||
"SELECT status, COUNT(*) as count FROM work_orders GROUP BY status"
|
||||
).fetchall()
|
||||
conn.close()
|
||||
return {r["status"]: r["count"] for r in rows}
|
||||
74
src/work_orders/risk.py
Normal file
74
src/work_orders/risk.py
Normal file
@@ -0,0 +1,74 @@
|
||||
"""Risk scoring and auto-execution threshold logic for work orders."""
|
||||
|
||||
from work_orders.models import WorkOrder, WorkOrderCategory, WorkOrderPriority
|
||||
|
||||
|
||||
PRIORITY_WEIGHTS = {
|
||||
WorkOrderPriority.CRITICAL: 4,
|
||||
WorkOrderPriority.HIGH: 3,
|
||||
WorkOrderPriority.MEDIUM: 2,
|
||||
WorkOrderPriority.LOW: 1,
|
||||
}
|
||||
|
||||
CATEGORY_WEIGHTS = {
|
||||
WorkOrderCategory.BUG: 3,
|
||||
WorkOrderCategory.FEATURE: 3,
|
||||
WorkOrderCategory.IMPROVEMENT: 2,
|
||||
WorkOrderCategory.OPTIMIZATION: 2,
|
||||
WorkOrderCategory.SUGGESTION: 1,
|
||||
}
|
||||
|
||||
SENSITIVE_PATHS = [
|
||||
"swarm/coordinator",
|
||||
"l402",
|
||||
"lightning/",
|
||||
"config.py",
|
||||
"security",
|
||||
"auth",
|
||||
]
|
||||
|
||||
|
||||
def compute_risk_score(wo: WorkOrder) -> int:
|
||||
"""Compute a risk score for a work order. Higher = riskier.
|
||||
|
||||
Score components:
|
||||
- Priority weight: critical=4, high=3, medium=2, low=1
|
||||
- Category weight: bug/feature=3, improvement/optimization=2, suggestion=1
|
||||
- File sensitivity: +2 per related file in security-sensitive areas
|
||||
"""
|
||||
score = PRIORITY_WEIGHTS.get(wo.priority, 2)
|
||||
score += CATEGORY_WEIGHTS.get(wo.category, 1)
|
||||
|
||||
for f in wo.related_files:
|
||||
if any(s in f for s in SENSITIVE_PATHS):
|
||||
score += 2
|
||||
|
||||
return score
|
||||
|
||||
|
||||
def should_auto_execute(wo: WorkOrder) -> bool:
|
||||
"""Determine if a work order can auto-execute without human approval.
|
||||
|
||||
Checks:
|
||||
1. Global auto-execute must be enabled
|
||||
2. Work order priority must be at or below the configured threshold
|
||||
3. Total risk score must be <= 3
|
||||
"""
|
||||
from config import settings
|
||||
|
||||
if not settings.work_orders_auto_execute:
|
||||
return False
|
||||
|
||||
threshold_map = {"none": 0, "low": 1, "medium": 2, "high": 3}
|
||||
max_auto = threshold_map.get(settings.work_orders_auto_threshold, 1)
|
||||
|
||||
priority_values = {
|
||||
WorkOrderPriority.LOW: 1,
|
||||
WorkOrderPriority.MEDIUM: 2,
|
||||
WorkOrderPriority.HIGH: 3,
|
||||
WorkOrderPriority.CRITICAL: 4,
|
||||
}
|
||||
if priority_values.get(wo.priority, 2) > max_auto:
|
||||
return False
|
||||
|
||||
return compute_risk_score(wo) <= 3
|
||||
Reference in New Issue
Block a user