"""Task specification the worker consumes. MVP source is a JSON file matching what the activity-core issue-core sink emits per rule action (task_template/description/target_repo/labels), plus executor-specific fields. A future source polls issue-core directly. """ from __future__ import annotations import json from dataclasses import dataclass, field from pathlib import Path class TaskSpecError(ValueError): pass @dataclass class TaskSpec: title: str description: str target_repo: Path agent: str = "coach" labels: list[str] = field(default_factory=list) hub_task_id: str | None = None completion_event_type: str = "executor_run" timeout_seconds: int = 900 @classmethod def from_file(cls, path: str | Path) -> "TaskSpec": raw = json.loads(Path(path).read_text()) missing = {"title", "description", "target_repo"} - set(raw) if missing: raise TaskSpecError(f"task spec missing field(s): {', '.join(sorted(missing))}") target = Path(raw["target_repo"]).expanduser() if not (target / ".git").is_dir(): raise TaskSpecError(f"target_repo is not a git repository: {target}") return cls( title=str(raw["title"]), description=str(raw["description"]), target_repo=target, agent=str(raw.get("agent", "coach")), labels=[str(label) for label in raw.get("labels", [])], hub_task_id=raw.get("hub_task_id"), completion_event_type=str(raw.get("completion_event_type", "executor_run")), timeout_seconds=int(raw.get("timeout_seconds", 900)), )