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:
Alexander Payne
2026-02-26 10:27:08 -05:00
parent 4e78f7102e
commit 5f9bbb8435
31 changed files with 3159 additions and 47 deletions

View File

@@ -0,0 +1 @@
"""Work Order system for external and internal task submission."""

View 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
View 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
View 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