from __future__ import annotations import json from pathlib import Path from typing import Dict, List from .airxdb_runtime import ensure_xdb_sessions_for_result, task_requires_xdb from .contracts import ( DocumentUpdate, WorkerResult, default_worker_result, now_iso, required_project_artifacts, ) from .debug_runtime import ensure_debug_sessions_for_result from .doc_sync import enforce_doc_sync_requirements from .paths import ( agents_path, airdo_root, architecture_adr_dir, architecture_c4_module_path, plan_path, todo_path, ) from .repair_runtime import load_active_repair_attempt from .todo_parser import find_task, parse_tasks def _json_dump(path: Path, payload: Dict[str, object]) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8") def _artifact_health(project_root: Path) -> Dict[str, bool]: return { relative: (project_root / relative).exists() for relative in required_project_artifacts() } def _paths(project_root: Path) -> Dict[str, Path]: worker_root = airdo_root(project_root) return { "root": worker_root, "state": worker_root / "state.json", "tasks": worker_root / "tasks", "results": worker_root / "results", } def _ensure_layout(project_root: Path) -> Dict[str, Path]: paths = _paths(project_root) paths["tasks"].mkdir(parents=True, exist_ok=True) paths["results"].mkdir(parents=True, exist_ok=True) return paths def _task_dir(project_root: Path, task_id: str) -> Path: return _paths(project_root)["tasks"] / task_id def worker_state_path(project_root: Path, task_id: str) -> Path: return _task_dir(project_root, task_id) / "worker-state.json" def _load_task_record(project_root: Path, task_id: str): current_todo = todo_path(project_root) if not current_todo.exists(): return None try: return find_task(parse_tasks(current_todo), task_id) except ValueError: return None def _worker_rules_block() -> List[str]: return [ "## Worker Rules", "", "- Load `AirPlan/AGENTS.md`, `AirPlan/docs/architecture/adr/`, `AirPlan/docs/architecture/c4/module.md`, `AirPlan/plan.md`, and `AirPlan/todo.md` before doing work.", "- Keep the task scoped to this brief.", "- Preserve validation evidence on disk.", "- Do not directly take ownership of global `AirPlan/todo.md`, `AirPlan/AGENTS.md`, ADR, or C4 updates unless explicitly delegated.", "- If this task touches global docs, `result.json` must include executable `documentUpdates`, not just recommendations.", "- If this task is GUI or visual acceptance related, `result.json` must end with successful AirXDB evidence before it can close as done.", "- If the result is blocked, or a validation ends in failed/error, the worker auto-requests AirDbg before finalize.", "- If the task is GUI-related, the worker auto-captures AirXDB evidence before finalize.", "- If an active auto-repair attempt exists, continue repairing immediately instead of stopping at the debug request.", "- Do not stop at an 'about to implement', 'implementation plan ready', or similar midpoint status. Continue editing, validating, and finalizing unless a real blocker or user decision is required.", "- Do not return only a progress update when the task is actionable. Return only after finalize, or after recording a concrete blocked reason that needs intervention.", "- Fill `result.json`, then finalize it for engine merge.", "", ] def _placeholder_result(project_root: Path, task_id: str) -> WorkerResult: task_summary = "" task = _load_task_record(project_root, task_id) if task: task_summary = task.task result = default_worker_result(task_id, task_summary) if task and task.global_doc_paths: result.global_doc_paths = list(task.global_doc_paths) result.recommend_global_doc_updates = list(task.global_doc_paths) result.document_updates = [] for doc_path in task.global_doc_paths: if doc_path == "AirPlan/docs/architecture/adr/": result.document_updates.append( DocumentUpdate( path="AirPlan/docs/architecture/adr/ADR-XXXX-task-sync.md", action="create_file", content=( "# ADR-XXXX: task-sync\n\n" "- Status: Accepted\n" "- Date: YYYY-MM-DD\n\n" "## Context\n" "Replace with the concrete context introduced by this task.\n\n" "## Decision\n" "Replace with the concrete decision introduced by this task.\n\n" "## Consequences\n" "- Replace with the concrete consequences introduced by this task.\n" ), ) ) else: result.document_updates.append( DocumentUpdate( path=doc_path, action="replace_block", marker=f"{task_id}-{Path(doc_path).name}".replace(".", "-").upper(), content=( f"## Sync For {task_id}\n\n" "- Summary: Replace with a concrete summary.\n" "- Code Reality: Replace with the actual code outcome.\n" "- Validation: Replace with the actual validation result.\n" ), ) ) return result def _is_untouched_placeholder_result(project_root: Path, result: WorkerResult) -> bool: placeholder = _placeholder_result(project_root, result.task_id) return result.to_dict() == placeholder.to_dict() def resolve_worker_result_path(project_root: Path, task_id: str) -> Path: state_path = worker_state_path(project_root, task_id) if state_path.exists(): payload = json.loads(state_path.read_text(encoding="utf-8-sig")) raw_path = str(payload.get("resultPath", "")).strip() if raw_path: candidate = Path(raw_path).expanduser() if not candidate.is_absolute(): candidate = (project_root / candidate).resolve() return candidate return _task_dir(project_root, task_id) / "result.json" def _write_brief(project_root: Path, task_id: str) -> Path: task_folder = _task_dir(project_root, task_id) task_folder.mkdir(parents=True, exist_ok=True) task = _load_task_record(project_root, task_id) active_repair = load_active_repair_attempt(project_root, task_id) lines: List[str] = [f"# Worker Brief: {task_id}", ""] if task: requires_xdb = task_requires_xdb(task, None) lines.extend( [ f"- Module: `{task.module}`", f"- Status In TODO: `{task.status}`", f"- Task: {task.task}", f"- Write Scope: {', '.join(f'`{path}`' for path in task.write_paths) or '`(not declared)`'}", f"- Global Doc Paths: {', '.join(f'`{path}`' for path in task.global_doc_paths) or '`none`'}", f"- Validation: {task.validation or '(not declared)'}", f"- Document Sync Required: {'yes' if task.global_doc_paths else 'no'}", f"- AirXDB Required: {'yes' if requires_xdb else 'no'}", "", ] ) if active_repair: lines.extend( [ "## Active Auto Repair", "", f"- Repair ID: `{active_repair.repair_id}`", f"- Attempt Index: `{active_repair.attempt_index}`", f"- Status: `{active_repair.status}`", f"- Reason: {active_repair.reason}", f"- Repair Brief: `{active_repair.repair_brief_path}`", f"- Debug Sessions: {', '.join(f'`{item}`' for item in active_repair.debug_session_ids) or '`none`'}", "", ] ) lines.extend(_worker_rules_block()) brief_path = task_folder / "brief.md" brief_path.write_text("\n".join(lines), encoding="utf-8") return brief_path def _write_handoff(project_root: Path, task_id: str, brief_path: Path, result_path: Path) -> Path: task = _load_task_record(project_root, task_id) current_worker_state_path = worker_state_path(project_root, task_id) handoff_path = _task_dir(project_root, task_id) / "subagent-handoff.md" lines = [ f"# AirDo Subagent Handoff: {task_id}", "", "You are an isolated AirDo execution subagent launched by AirEng.", "", f"- Task ID: `{task_id}`", f"- Project Root: `{project_root}`", f"- Brief Path: `{brief_path}`", f"- Worker State Path: `{current_worker_state_path}`", f"- Template Result Path: `{result_path}`", f"- Task Summary: {task.task if task else '(load from brief)'}", f"- Write Scope: {', '.join(f'`{path}`' for path in (task.write_paths if task else [])) or '`(load from brief)`'}", "", "## Required Reads", "", f"- `{agents_path(project_root)}`", f"- `{architecture_adr_dir(project_root)}`", f"- `{architecture_c4_module_path(project_root)}`", f"- `{plan_path(project_root)}`", f"- `{todo_path(project_root)}`", f"- `{brief_path}`", "", "## Execution Contract", "", "- Stay inside this task scope and its declared write set.", "- Do not assume the parent thread history is available; build context from the files above.", "- Execute the task, gather validation evidence, and update `result.json` with honest status, files changed, risks, blockers, and document updates when required.", "- Run AirDbg or AirXDB automatically when the runtime rules require them.", "- Do not pause for intermediate implementation-status replies. Keep going until the task is finalized unless you hit a real blocker that requires external input.", "- Finalize the result before returning control to AirEng.", "", "## Finalize Command", "", f"`python \"$HOME/plugins/airdo/scripts/airdo_mode.py\" --mode finish --project . --task-id {task_id}`", "", "## Return Contract", "", "- Report the final status and the finalized result path.", "- After `finish`, treat `worker-state.json` `resultPath` as the canonical result location instead of re-reading the template `result.json`.", "- Do not merge global docs yourself unless the task explicitly delegated document updates through `result.json`.", "", ] handoff_path.write_text("\n".join(lines), encoding="utf-8") return handoff_path def enter_worker(project_root: Path, task_id: str) -> Dict[str, object]: paths = _ensure_layout(project_root) task_folder = _task_dir(project_root, task_id) task_folder.mkdir(parents=True, exist_ok=True) result_path = task_folder / "result.json" if not result_path.exists(): result = _placeholder_result(project_root, task_id) _json_dump(result_path, result.to_dict()) brief_path = _write_brief(project_root, task_id) handoff_path = _write_handoff(project_root, task_id, brief_path, result_path) state_path = worker_state_path(project_root, task_id) state = { "enabled": True, "updatedAt": now_iso(), "projectRoot": str(project_root), "artifactHealth": _artifact_health(project_root), "activeTaskId": task_id, } _json_dump(paths["state"], state) _json_dump( state_path, { "taskId": task_id, "status": "entered", "enteredAt": now_iso(), "templateResultPath": str(result_path), "resultPath": str(result_path), "briefPath": str(brief_path), "handoffPath": str(handoff_path), }, ) return { "taskId": task_id, "briefPath": str(brief_path), "handoffPath": str(handoff_path), "resultPath": str(result_path), "workerStatePath": str(state_path), } def handoff_worker(project_root: Path, task_id: str) -> Dict[str, object]: entered = enter_worker(project_root, task_id) return { "taskId": task_id, "briefPath": entered["briefPath"], "handoffPath": entered["handoffPath"], "resultPath": entered["resultPath"], "workerStatePath": entered["workerStatePath"], } def finish_worker(project_root: Path, task_id: str, result_path: Path | None) -> Dict[str, object]: _ensure_layout(project_root) task_folder = _task_dir(project_root, task_id) if result_path is None: result_path = task_folder / "result.json" result = WorkerResult.from_dict(json.loads(result_path.read_text(encoding="utf-8-sig"))) if _is_untouched_placeholder_result(project_root, result): raise ValueError( "worker result is still the untouched default template; update result.json before finalize" ) result.validate_for_finalize() ensure_xdb_sessions_for_result(project_root, result, "worker-finish") ensure_debug_sessions_for_result(project_root, result, "worker-finish") result.validate_for_finalize() enforce_doc_sync_requirements(project_root, result) if result.task_id != task_id: raise ValueError("result taskId does not match --task-id") if not result.finalized_at: result.finalized_at = now_iso() finalized_path = _paths(project_root)["results"] / f"{task_id}.json" _json_dump(finalized_path, result.to_dict()) previous_state: Dict[str, object] = {} current_state_path = worker_state_path(project_root, task_id) if current_state_path.exists(): previous_state = json.loads(current_state_path.read_text(encoding="utf-8-sig")) _json_dump( current_state_path, { "taskId": task_id, "status": "completed", "enteredAt": previous_state.get("enteredAt", ""), "briefPath": previous_state.get("briefPath", ""), "handoffPath": previous_state.get("handoffPath", ""), "templateResultPath": previous_state.get("templateResultPath", str(task_folder / "result.json")), "finalizedAt": result.finalized_at, "resultPath": str(finalized_path), }, ) return { "taskId": task_id, "status": result.status, "finalizedResultPath": str(finalized_path), "workerStatePath": str(current_state_path), } def status_worker(project_root: Path) -> Dict[str, object]: paths = _ensure_layout(project_root) state_path = paths["state"] enabled = state_path.exists() active_task_id = "" if enabled: payload = json.loads(state_path.read_text(encoding="utf-8-sig")) active_task_id = str(payload.get("activeTaskId", "")) task_ids = sorted(path.name for path in paths["tasks"].iterdir() if path.is_dir()) return { "enabled": enabled, "projectRoot": str(project_root), "artifactHealth": _artifact_health(project_root), "activeTaskId": active_task_id, "taskIds": task_ids, }