from dataclasses import dataclass, field from typing import List, Optional, Dict, Any from datetime import datetime from pathlib import Path import os import tomllib from src import project_manager CONFIG_PATH: Path = Path(os.environ.get("SLOP_CONFIG", "config.toml")) DISC_ROLES: list[str] = ["User", "AI", "Vendor API", "System"] AGENT_TOOL_NAMES: list[str] = [ "run_powershell", "read_file", "list_directory", "search_files", "get_file_summary", "web_search", "fetch_url", "py_get_skeleton", "py_get_code_outline", "get_file_slice", "py_get_definition", "py_get_signature", "py_get_class_summary", "py_get_var_declaration", "get_git_diff", "py_find_usages", "py_get_imports", "py_check_syntax", "py_get_hierarchy", "py_get_docstring", "get_tree", "get_ui_performance", # Mutating tools — disabled by default "set_file_slice", "py_update_definition", "py_set_signature", "py_set_var_declaration", ] def load_config() -> dict[str, Any]: with open(CONFIG_PATH, "rb") as f: return tomllib.load(f) def parse_history_entries( history: list[str], roles: list[str] | None = None ) -> list[dict[str, Any]]: known = roles if roles is not None else DISC_ROLES entries = [] for raw in history: entry = project_manager.str_to_entry(raw, known) entries.append(entry) return entries @dataclass class Ticket: """ Represents a discrete unit of work within a track. """ id: str description: str status: str = "todo" assigned_to: str = "unassigned" target_file: Optional[str] = None context_requirements: List[str] = field(default_factory=list) depends_on: List[str] = field(default_factory=list) blocked_reason: Optional[str] = None step_mode: bool = False retry_count: int = 0 def mark_blocked(self, reason: str) -> None: """Sets the ticket status to 'blocked' and records the reason.""" self.status = "blocked" self.blocked_reason = reason def mark_complete(self) -> None: """Sets the ticket status to 'completed'.""" self.status = "completed" def get(self, key: str, default: Any = None) -> Any: """Helper to provide dictionary-like access to dataclass fields.""" return getattr(self, key, default) def to_dict(self) -> Dict[str, Any]: return { "id": self.id, "description": self.description, "status": self.status, "assigned_to": self.assigned_to, "target_file": self.target_file, "context_requirements": self.context_requirements, "depends_on": self.depends_on, "blocked_reason": self.blocked_reason, "step_mode": self.step_mode, "retry_count": self.retry_count, } @classmethod def from_dict(cls, data: Dict[str, Any]) -> "Ticket": return cls( id=data["id"], description=data.get("description", ""), status=data.get("status", "todo"), assigned_to=data.get("assigned_to", ""), target_file=data.get("target_file"), context_requirements=data.get("context_requirements", []), depends_on=data.get("depends_on", []), blocked_reason=data.get("blocked_reason"), step_mode=data.get("step_mode", False), retry_count=data.get("retry_count", 0), ) @dataclass class Track: """ Represents a collection of tickets that together form an architectural track or epic. """ id: str description: str tickets: List[Ticket] = field(default_factory=list) def get_executable_tickets(self) -> List[Ticket]: """ Returns all 'todo' tickets whose dependencies are all 'completed'. """ # Map ticket IDs to their current status for efficient lookup status_map = {t.id: t.status for t in self.tickets} executable = [] for ticket in self.tickets: if ticket.status != "todo": continue # Check if all dependencies are completed all_deps_completed = True for dep_id in ticket.depends_on: # If a dependency is missing from the track, we treat it as not completed (or we could raise an error) if status_map.get(dep_id) != "completed": all_deps_completed = False break if all_deps_completed: executable.append(ticket) return executable @dataclass class WorkerContext: """ Represents the context provided to a Tier 3 Worker for a specific ticket. """ ticket_id: str model_name: str messages: List[Dict[str, Any]] @dataclass class Metadata: id: str name: str status: Optional[str] = None created_at: Optional[datetime] = None updated_at: Optional[datetime] = None def to_dict(self) -> Dict[str, Any]: return { "id": self.id, "name": self.name, "status": self.status, "created_at": self.created_at.isoformat() if self.created_at else None, "updated_at": self.updated_at.isoformat() if self.updated_at else None, } @classmethod def from_dict(cls, data: Dict[str, Any]) -> "Metadata": return cls( id=data["id"], name=data["name"], status=data.get("status"), created_at=datetime.fromisoformat(data["created_at"]) if data.get("created_at") else None, updated_at=datetime.fromisoformat(data["updated_at"]) if data.get("updated_at") else None, ) @dataclass class TrackState: metadata: Metadata discussion: List[Dict[str, Any]] tasks: List[Ticket] def to_dict(self) -> Dict[str, Any]: return { "metadata": self.metadata.to_dict(), "discussion": [ { k: v.isoformat() if isinstance(v, datetime) else v for k, v in item.items() } for item in self.discussion ], "tasks": [task.to_dict() for task in self.tasks], } @classmethod def from_dict(cls, data: Dict[str, Any]) -> "TrackState": metadata = Metadata.from_dict(data["metadata"]) tasks = [Ticket.from_dict(task_data) for task_data in data["tasks"]] return cls( metadata=metadata, discussion=[ { k: datetime.fromisoformat(v) if isinstance(v, str) and "T" in v else v # Basic check for ISO format for k, v in item.items() } for item in data["discussion"] ], tasks=tasks, )