chore: push all design docs, V2 plan specs, and current working state
Includes AirPlan design documents, AircOding-alpha1-plan, AirPlanV2, AirPlan-ParaV2, AirPlan-Para V1 reference docs, and all working code changes across packages. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
372
AirPlan/docs/spec/AirPlan-ParaV2/lib/air_runtime/worker.py
Executable file
372
AirPlan/docs/spec/AirPlan-ParaV2/lib/air_runtime/worker.py
Executable file
@@ -0,0 +1,372 @@
|
||||
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,
|
||||
}
|
||||
Reference in New Issue
Block a user