Files
AirCoding ae44be31d5 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>
2026-06-12 17:12:29 +08:00

618 lines
22 KiB
Python
Executable File

from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any, Dict, List
from .paths import required_project_artifacts
KNOWN_TASK_STATUSES = {"TODO", "DOING", "DONE", "BLOCKED"}
ACTIVE_TASK_STATUSES = {"TODO", "DOING"}
FINAL_WORKER_STATUSES = {"done", "blocked", "skipped"}
DOCUMENT_UPDATE_ACTIONS = {"replace_block", "append_lines", "create_file"}
DEFAULT_WORKER_RESULT_NOTE = (
"Do not merge this result until summary, filesChanged, validations, and risks are reviewed."
)
DEFAULT_DEBUG_POLICY = {
"enabled": True,
"triggerOnBlocked": True,
"triggerValidationStatuses": ["failed", "error"],
"maxSessionsPerTask": 1,
"preferOriginalAirDbg": True,
}
DEFAULT_XDB_POLICY = {
"enabled": True,
"requireForGuiTasks": True,
"captureOnBlocked": True,
"captureOnDone": True,
"triggerValidationStatuses": ["failed", "error"],
"maxSessionsPerTask": 1,
"preferOriginalAirXDB": True,
"remoteFirstIfConfigured": True,
"taskKeywords": [
"gui",
"ui",
"desktop",
"browser",
"screen",
"screenshot",
"visual",
"layout",
"popup",
"focus",
"canvas",
"acceptance",
"midscene",
"airxdb",
"lvgl",
"lcd",
"qt-app",
],
}
DEFAULT_REPAIR_POLICY = {
"enabled": True,
"triggerOnBlocked": True,
"triggerValidationStatuses": ["failed", "error"],
"maxAttemptsPerTask": 2,
"requeueTodoStatus": "DOING",
"autoPrepareWorker": True,
}
REQUIRED_PROJECT_ARTIFACTS = required_project_artifacts()
def now_iso() -> str:
return datetime.now(timezone.utc).isoformat()
def _dedupe(items: List[str]) -> List[str]:
seen = set()
ordered: List[str] = []
for item in items:
cleaned = str(item).strip()
if not cleaned or cleaned in seen:
continue
seen.add(cleaned)
ordered.append(cleaned)
return ordered
def _ensure_str_list(value: Any) -> List[str]:
if value is None:
return []
if isinstance(value, list):
return _dedupe([str(item) for item in value])
return _dedupe([str(value)])
@dataclass
class ValidationRecord:
kind: str
status: str
command: str = ""
evidence: List[str] = field(default_factory=list)
@classmethod
def from_dict(cls, payload: Dict[str, Any]) -> "ValidationRecord":
return cls(
kind=str(payload.get("kind", "")).strip(),
status=str(payload.get("status", "")).strip(),
command=str(payload.get("command", "")).strip(),
evidence=_ensure_str_list(payload.get("evidence")),
)
def to_dict(self) -> Dict[str, Any]:
return asdict(self)
@dataclass
class TaskRecord:
task_id: str
status: str
module: str
task: str
files_dirs: str
done_when: str
validation: str
adr_c4_update: str
line_number: int
dependencies: List[str] = field(default_factory=list)
write_paths: List[str] = field(default_factory=list)
global_doc_paths: List[str] = field(default_factory=list)
def normalized_write_set(self) -> List[str]:
return _dedupe(self.write_paths + self.global_doc_paths)
def touches_global_docs(self) -> bool:
return bool(self.global_doc_paths)
def to_dict(self) -> Dict[str, Any]:
payload = asdict(self)
payload["taskId"] = payload.pop("task_id")
payload["filesDirs"] = payload.pop("files_dirs")
payload["doneWhen"] = payload.pop("done_when")
payload["adrC4Update"] = payload.pop("adr_c4_update")
payload["lineNumber"] = payload.pop("line_number")
payload["writeSet"] = payload.pop("write_paths")
payload["globalDocPaths"] = payload.pop("global_doc_paths")
return payload
@classmethod
def from_dict(cls, payload: Dict[str, Any]) -> "TaskRecord":
return cls(
task_id=str(payload.get("taskId", "")).strip(),
status=str(payload.get("status", "")).strip().upper(),
module=str(payload.get("module", "")).strip(),
task=str(payload.get("task", "")).strip(),
files_dirs=str(payload.get("filesDirs", "")).strip(),
done_when=str(payload.get("doneWhen", "")).strip(),
validation=str(payload.get("validation", "")).strip(),
adr_c4_update=str(payload.get("adrC4Update", "")).strip(),
line_number=int(payload.get("lineNumber", 0) or 0),
dependencies=_ensure_str_list(payload.get("dependencies")),
write_paths=_ensure_str_list(payload.get("writeSet")),
global_doc_paths=_ensure_str_list(payload.get("globalDocPaths")),
)
@dataclass
class WorkerResult:
task_id: str
status: str
summary: str
files_changed: List[str] = field(default_factory=list)
validations: List[ValidationRecord] = field(default_factory=list)
evidence_paths: List[str] = field(default_factory=list)
risks: List[str] = field(default_factory=list)
blockers: List[str] = field(default_factory=list)
recommend_global_doc_updates: List[str] = field(default_factory=list)
global_doc_paths: List[str] = field(default_factory=list)
document_updates: List["DocumentUpdate"] = field(default_factory=list)
debug_sessions: List["DebugSession"] = field(default_factory=list)
xdb_sessions: List["XdbSession"] = field(default_factory=list)
repair_attempts: List["RepairAttempt"] = field(default_factory=list)
notes: List[str] = field(default_factory=list)
finalized_at: str = ""
@classmethod
def from_dict(cls, payload: Dict[str, Any]) -> "WorkerResult":
return cls(
task_id=str(payload.get("taskId", "")).strip(),
status=str(payload.get("status", "")).strip().lower(),
summary=str(payload.get("summary", "")).strip(),
files_changed=_ensure_str_list(payload.get("filesChanged")),
validations=[
ValidationRecord.from_dict(item)
for item in payload.get("validations", [])
],
evidence_paths=_ensure_str_list(payload.get("evidencePaths")),
risks=_ensure_str_list(payload.get("risks")),
blockers=_ensure_str_list(payload.get("blockers")),
recommend_global_doc_updates=_ensure_str_list(
payload.get("recommendGlobalDocUpdates")
),
global_doc_paths=_ensure_str_list(payload.get("globalDocPaths")),
document_updates=[
DocumentUpdate.from_dict(item)
for item in payload.get("documentUpdates", [])
],
debug_sessions=[
DebugSession.from_dict(item)
for item in payload.get("debugSessions", [])
],
xdb_sessions=[
XdbSession.from_dict(item)
for item in payload.get("xdbSessions", [])
],
repair_attempts=[
RepairAttempt.from_dict(item)
for item in payload.get("repairAttempts", [])
],
notes=_ensure_str_list(payload.get("notes")),
finalized_at=str(payload.get("finalizedAt", "")).strip(),
)
def validate(self) -> None:
if not self.task_id:
raise ValueError("worker result missing taskId")
if self.status not in FINAL_WORKER_STATUSES:
raise ValueError(
f"worker result status must be one of {sorted(FINAL_WORKER_STATUSES)}"
)
if not self.summary:
raise ValueError("worker result summary must not be empty")
if self.status == "blocked" and not self.blockers:
raise ValueError("blocked worker result must include blockers")
for item in self.document_updates:
item.validate()
for item in self.debug_sessions:
item.validate()
for item in self.xdb_sessions:
item.validate()
for item in self.repair_attempts:
item.validate()
def has_meaningful_done_payload(self) -> bool:
return bool(
self.files_changed
or self.validations
or self.evidence_paths
or self.document_updates
or self.debug_sessions
or self.xdb_sessions
)
def validate_for_finalize(self) -> None:
self.validate()
if self.status == "done" and not self.has_meaningful_done_payload():
raise ValueError(
"done worker result must include filesChanged, validations, evidencePaths, "
"documentUpdates, debugSessions, or xdbSessions before finalize"
)
def to_dict(self) -> Dict[str, Any]:
return {
"taskId": self.task_id,
"status": self.status,
"summary": self.summary,
"filesChanged": self.files_changed,
"validations": [item.to_dict() for item in self.validations],
"evidencePaths": self.evidence_paths,
"risks": self.risks,
"blockers": self.blockers,
"recommendGlobalDocUpdates": self.recommend_global_doc_updates,
"globalDocPaths": self.global_doc_paths,
"documentUpdates": [item.to_dict() for item in self.document_updates],
"debugSessions": [item.to_dict() for item in self.debug_sessions],
"xdbSessions": [item.to_dict() for item in self.xdb_sessions],
"repairAttempts": [item.to_dict() for item in self.repair_attempts],
"notes": self.notes,
"finalizedAt": self.finalized_at,
}
def default_worker_result(task_id: str, summary: str = "") -> WorkerResult:
return WorkerResult(
task_id=task_id,
status="done",
summary=summary,
files_changed=[],
validations=[],
evidence_paths=[],
risks=[],
blockers=[],
recommend_global_doc_updates=[],
global_doc_paths=[],
document_updates=[],
debug_sessions=[],
xdb_sessions=[],
repair_attempts=[],
notes=[
DEFAULT_WORKER_RESULT_NOTE
],
finalized_at="",
)
@dataclass
class DebugSession:
session_id: str
task_id: str
source: str
trigger: str
reason: str
request_path: str
debug_log_path: str
state_path: str = ""
mode: str = "original-airdbg"
status: str = "requested"
created_at: str = ""
@classmethod
def from_dict(cls, payload: Dict[str, Any]) -> "DebugSession":
return cls(
session_id=str(payload.get("sessionId", "")).strip(),
task_id=str(payload.get("taskId", "")).strip(),
source=str(payload.get("source", "")).strip(),
trigger=str(payload.get("trigger", "")).strip(),
reason=str(payload.get("reason", "")).strip(),
request_path=str(payload.get("requestPath", "")).strip(),
debug_log_path=str(payload.get("debugLogPath", "")).strip(),
state_path=str(payload.get("statePath", "")).strip(),
mode=str(payload.get("mode", "original-airdbg")).strip(),
status=str(payload.get("status", "requested")).strip(),
created_at=str(payload.get("createdAt", "")).strip(),
)
def validate(self) -> None:
if not self.session_id:
raise ValueError("debug session missing sessionId")
if not self.task_id:
raise ValueError("debug session missing taskId")
if not self.request_path:
raise ValueError("debug session missing requestPath")
if not self.debug_log_path:
raise ValueError("debug session missing debugLogPath")
def to_dict(self) -> Dict[str, Any]:
return {
"sessionId": self.session_id,
"taskId": self.task_id,
"source": self.source,
"trigger": self.trigger,
"reason": self.reason,
"requestPath": self.request_path,
"debugLogPath": self.debug_log_path,
"statePath": self.state_path,
"mode": self.mode,
"status": self.status,
"createdAt": self.created_at,
}
@dataclass
class XdbSession:
session_id: str
task_id: str
source: str
trigger: str
reason: str
request_path: str
gui_debug_log_path: str
report_path: str = ""
state_path: str = ""
mode: str = "local-screenshot"
status: str = "captured"
screenshots: List[str] = field(default_factory=list)
created_at: str = ""
remote_target: str = ""
@classmethod
def from_dict(cls, payload: Dict[str, Any]) -> "XdbSession":
return cls(
session_id=str(payload.get("sessionId", "")).strip(),
task_id=str(payload.get("taskId", "")).strip(),
source=str(payload.get("source", "")).strip(),
trigger=str(payload.get("trigger", "")).strip(),
reason=str(payload.get("reason", "")).strip(),
request_path=str(payload.get("requestPath", "")).strip(),
gui_debug_log_path=str(payload.get("guiDebugLogPath", "")).strip(),
report_path=str(payload.get("reportPath", "")).strip(),
state_path=str(payload.get("statePath", "")).strip(),
mode=str(payload.get("mode", "local-screenshot")).strip(),
status=str(payload.get("status", "captured")).strip(),
screenshots=_ensure_str_list(payload.get("screenshots")),
created_at=str(payload.get("createdAt", "")).strip(),
remote_target=str(payload.get("remoteTarget", "")).strip(),
)
def validate(self) -> None:
if not self.session_id:
raise ValueError("xdb session missing sessionId")
if not self.task_id:
raise ValueError("xdb session missing taskId")
if not self.request_path:
raise ValueError("xdb session missing requestPath")
if not self.gui_debug_log_path:
raise ValueError("xdb session missing guiDebugLogPath")
def to_dict(self) -> Dict[str, Any]:
return {
"sessionId": self.session_id,
"taskId": self.task_id,
"source": self.source,
"trigger": self.trigger,
"reason": self.reason,
"requestPath": self.request_path,
"guiDebugLogPath": self.gui_debug_log_path,
"reportPath": self.report_path,
"statePath": self.state_path,
"mode": self.mode,
"status": self.status,
"screenshots": self.screenshots,
"createdAt": self.created_at,
"remoteTarget": self.remote_target,
}
@dataclass
class RepairAttempt:
repair_id: str
task_id: str
attempt_index: int
source_status: str
source_result_path: str
reason: str
repair_brief_path: str
state_path: str = ""
worker_brief_path: str = ""
debug_session_ids: List[str] = field(default_factory=list)
status: str = "queued"
created_at: str = ""
@classmethod
def from_dict(cls, payload: Dict[str, Any]) -> "RepairAttempt":
return cls(
repair_id=str(payload.get("repairId", "")).strip(),
task_id=str(payload.get("taskId", "")).strip(),
attempt_index=int(payload.get("attemptIndex", 0) or 0),
source_status=str(payload.get("sourceStatus", "")).strip(),
source_result_path=str(payload.get("sourceResultPath", "")).strip(),
reason=str(payload.get("reason", "")).strip(),
repair_brief_path=str(payload.get("repairBriefPath", "")).strip(),
state_path=str(payload.get("statePath", "")).strip(),
worker_brief_path=str(payload.get("workerBriefPath", "")).strip(),
debug_session_ids=_ensure_str_list(payload.get("debugSessionIds")),
status=str(payload.get("status", "queued")).strip(),
created_at=str(payload.get("createdAt", "")).strip(),
)
def validate(self) -> None:
if not self.repair_id:
raise ValueError("repair attempt missing repairId")
if not self.task_id:
raise ValueError("repair attempt missing taskId")
if self.attempt_index <= 0:
raise ValueError("repair attempt must have positive attemptIndex")
if not self.repair_brief_path:
raise ValueError("repair attempt missing repairBriefPath")
def to_dict(self) -> Dict[str, Any]:
return {
"repairId": self.repair_id,
"taskId": self.task_id,
"attemptIndex": self.attempt_index,
"sourceStatus": self.source_status,
"sourceResultPath": self.source_result_path,
"reason": self.reason,
"repairBriefPath": self.repair_brief_path,
"statePath": self.state_path,
"workerBriefPath": self.worker_brief_path,
"debugSessionIds": self.debug_session_ids,
"status": self.status,
"createdAt": self.created_at,
}
@dataclass
class DocumentUpdate:
path: str
action: str
content: str = ""
marker: str = ""
append_lines: List[str] = field(default_factory=list)
@classmethod
def from_dict(cls, payload: Dict[str, Any]) -> "DocumentUpdate":
return cls(
path=str(payload.get("path", "")).strip(),
action=str(payload.get("action", "")).strip(),
content=str(payload.get("content", "")).rstrip(),
marker=str(payload.get("marker", "")).strip(),
append_lines=_ensure_str_list(payload.get("appendLines")),
)
def validate(self) -> None:
if not self.path:
raise ValueError("document update missing path")
if self.action not in DOCUMENT_UPDATE_ACTIONS:
raise ValueError(
f"document update action must be one of {sorted(DOCUMENT_UPDATE_ACTIONS)}"
)
if self.action == "replace_block" and not self.marker:
raise ValueError("replace_block document update requires marker")
if self.action == "append_lines" and not self.append_lines:
raise ValueError("append_lines document update requires appendLines")
if self.action in {"replace_block", "create_file"} and not self.content:
raise ValueError(f"{self.action} document update requires content")
if self.action in {"replace_block", "create_file"} and "TODO" in self.content:
raise ValueError(
f"{self.action} document update for {self.path} still contains TODO placeholders"
)
if self.action == "append_lines" and any("TODO" in line for line in self.append_lines):
raise ValueError(
f"append_lines document update for {self.path} still contains TODO placeholders"
)
def to_dict(self) -> Dict[str, Any]:
payload = {
"path": self.path,
"action": self.action,
}
if self.content:
payload["content"] = self.content
if self.marker:
payload["marker"] = self.marker
if self.append_lines:
payload["appendLines"] = self.append_lines
return payload
@dataclass
class ReviewConflict:
task_ids: List[str]
reason: str
overlap_paths: List[str] = field(default_factory=list)
def to_dict(self) -> Dict[str, Any]:
return {
"taskIds": self.task_ids,
"reason": self.reason,
"overlapPaths": self.overlap_paths,
}
@classmethod
def from_dict(cls, payload: Dict[str, Any]) -> "ReviewConflict":
return cls(
task_ids=_ensure_str_list(payload.get("taskIds")),
reason=str(payload.get("reason", "")).strip(),
overlap_paths=_ensure_str_list(payload.get("overlapPaths")),
)
@dataclass
class ParallelGroup:
name: str
task_ids: List[str]
reason: str
def to_dict(self) -> Dict[str, Any]:
return {
"name": self.name,
"taskIds": self.task_ids,
"reason": self.reason,
}
@classmethod
def from_dict(cls, payload: Dict[str, Any]) -> "ParallelGroup":
return cls(
name=str(payload.get("name", "")).strip(),
task_ids=_ensure_str_list(payload.get("taskIds")),
reason=str(payload.get("reason", "")).strip(),
)
@dataclass
class ParallelReview:
source_todo: str
generated_at: str
tasks_considered: List[TaskRecord]
dependency_edges: List[Dict[str, str]]
parallel_groups: List[ParallelGroup]
conflicts: List[ReviewConflict]
serialization_points: List[Dict[str, Any]]
notes: List[str]
def to_dict(self) -> Dict[str, Any]:
return {
"sourceTodo": self.source_todo,
"generatedAt": self.generated_at,
"tasksConsidered": [task.to_dict() for task in self.tasks_considered],
"dependencyEdges": self.dependency_edges,
"parallelGroups": [group.to_dict() for group in self.parallel_groups],
"conflicts": [conflict.to_dict() for conflict in self.conflicts],
"serializationPoints": self.serialization_points,
"notes": self.notes,
}
@classmethod
def from_dict(cls, payload: Dict[str, Any]) -> "ParallelReview":
return cls(
source_todo=str(payload.get("sourceTodo", "")).strip(),
generated_at=str(payload.get("generatedAt", "")).strip(),
tasks_considered=[
TaskRecord.from_dict(item)
for item in payload.get("tasksConsidered", [])
],
dependency_edges=list(payload.get("dependencyEdges", [])),
parallel_groups=[
ParallelGroup.from_dict(item)
for item in payload.get("parallelGroups", [])
],
conflicts=[
ReviewConflict.from_dict(item)
for item in payload.get("conflicts", [])
],
serialization_points=list(payload.get("serializationPoints", [])),
notes=_ensure_str_list(payload.get("notes")),
)