# AirPlan V2 设计文档 > **版本**: Draft 0.1 > **日期**: 2026-06-09 > **基线**: AirPlan V1 (air-suite-20260518, air_runtime ~4,500 行, 8 插件) > **状态**: 待评审 --- ## 0. 文档目的 本文档记录 AirPlan V1 在真实工程项目(DecodePlayer 系列, AirCoding V1.0.0 Alpha)中暴露的系统性缺陷,并定义 V2 的架构改进方向、新增组件设计和分阶段实施计划。 V2 的核心命题:**V1 证明了制品驱动 + 上下文隔离 + 波次并行的架构方向成立;V2 要解决可靠性、可观测性和规模化问题。** --- ## 1. V1 问题诊断 ### 1.1 缺陷分级 #### P0 — 已造成实际损失 | ID | 缺陷 | 位置 | 影响 | 根因 | |----|------|------|------|------| | P0-1 | AirXDB 假阳性阻塞 | `airxdb_runtime.py` `ensure_xdb_sessions_for_result()` | 11+ 任务触发虚假修复循环,每次需人工覆盖 | 证据门控无任务类型感知 | | P0-2 | 部署验证缺口 | `contracts.py` `validate_for_finalize()` | T-028b 发现 4 个 "done" 任务未实际部署,延迟发布一整天 | 验证只检查结构完整性,不检查部署一致性 | | P0-3 | 非原子写入 | 全部 `_json_dump` (5 份) | 进程崩溃时 state.json 截断,下次加载 JSONDecodeError | `path.write_text()` 直接覆盖,无 tempfile + rename | | P0-4 | 零并发控制 | 整个 `air_runtime/` | 两个 Worker 同时完成时 todo.md 读-改-写竞态 | 无任何锁、互斥量或原子操作 | | P0-5 | AirArc 被 plan 模式劫持 | AirArc SKILL.md / 命令文件 | 架构器频繁被 Agent 内置 plan mode 接管,偏离架构规划职责 | 缺乏 plan mode 阻断机制,SKILL.md 指令不够强硬 | | P0-6 | AirEng 停下来问而不自主决策 | AirEng SKILL.md / 命令文件 | 调度器用英文反复询问用户确认,而非中文自主推进开发进度 | SKILL.md 未强制"以推进为目标自行决策",语言未锁定中文 | | P0-7 | AirEng 无子线程状态轮询 | `engine.py` `monitor_engine()` | 子线程卡死后调度器无限等待,必须用户手动发现并告知 | 轮询逻辑依赖 Agent 自觉执行,无硬编码的定时轮询循环 | | P0-8 | AirDo 不调用专家插件(Dbg/XDB/NDB/SDB/Rvr) | AirDo SKILL.md / `worker.py` `finish_worker()` | 执行器遇到问题或验收时几乎不触发任何专家插件:不调 AirDbg 调试、不调 AirXDB 截图、不调 AirNDB 抓包、不调 AirSDB 静态分析、不调 AirRvr 审查,直接报 blocked 或 false-done | 自动路由为建议性而非强制,Worker 倾向于跳过所有专家插件直接返回 | | P0-9 | 安装器脚本路径错误 | 安装脚本 / 插件注册逻辑 | 安装后插件无法识别,AI 修复后可识别但脚本执行失败,路径不正确 | 安装器未正确解析插件脚本的绝对路径,注册的命令路径与实际文件位置不匹配 | | P0-10 | AirEng 偏离调度亲自写代码 | AirEng SKILL.md / 命令文件 | 调度引擎频繁偏离调度职责自己编写代码,破坏隔离架构。极端情况(子代理循环阻塞需接手合并)允许少量修改,但日常调度中不应发生 | SKILL.md 未明确区分"仅调度"与"极端接管"的边界,无工具限制约束 | #### P1 — 限制可靠性与可维护性 | ID | 缺陷 | 位置 | 影响 | |----|------|------|------| | P1-1 | 硬编码开发者路径 | `debug_runtime.py:130`, `airxdb_runtime.py:156` | `C:\Users\20392\...` 在其他机器静默失败 | | P1-2 | `_json_dump`/`_json_load` 5 份重复 | engine, worker, airxdb, debug, repair | 修一处漏四处 | | P1-3 | `_ordered_unique` 4 份重复 | airxdb, debug, repair, contracts | 同上 | | P1-4 | policy normalization 3 份重复 | airxdb, debug, repair | 同上 | | P1-5 | merge-into-state 3 份重复 | airxdb, debug, repair | 同上 | | P1-6 | marker block upsert 2 份(接口不同) | doc_sync, project_bootstrap | 同上 | | P1-7 | `_session_stamp` 格式不一致 | airxdb, debug vs engine | 时间戳格式在不同模块产生不同文件名 | | P1-8 | todo.md 列索引硬编码 | `doc_sync.py:154-156` | `cells[1]`=status, `cells[6]`=validation, `cells[7]`=adr — 表头变化时全部失效 | | P1-9 | 并发度上限硬编码为 3 | `engine.py:527` | 无法根据项目规模调整 | | P1-10 | 子进程无超时 | airxdb, debug runtime | 挂死时阻塞整个引擎 | | P1-11 | task_id 路径注入 | `worker.py:59` | 无 `../` 遍历校验 | | P1-12 | 标记注入风险 | `doc_sync.py` `_replace_marker_block()` | marker 字段含 `-->` 时可注入内容 | | P1-13 | 静默吞异常 | session 文件损坏时 `except` 后 `continue` | 损坏文件不可见,无日志 | | P1-14 | Arc 重规划后 Eng 无法衔接 | `engine.py` 计划解析 + `todo.md` 同步 | 中途变更需求后 Arc 重新生成规划,Eng 需多轮 AI 迭代才能恢复调度 | 调度引擎基于静态 todo.md 表格,无法增量吸收 Arc 的动态重规划结果 | | P1-15 | 同文件无冲突任务被迫串行 | `review.py` 写集冲突检测 | 同文件不同区域(如 Qt 样式 vs 状态机)被判为冲突,被迫串行执行 | 冲突检测粒度为文件级而非区域级,无 worktree 隔离并行能力 | | P1-16 | AirArc 跳过需求探讨直接生成规划 | AirArc SKILL.md / 命令文件 | 用户刚说一两句就自顾自生成计划并要求执行,未与用户充分探讨需求和分析架构 | SKILL.md 未强制"先探讨后规划"流程,缺少用户确认架构的门控 | | P1-17 | AirDbg 未取证就盲改代码 | AirDbg SKILL.md / `debug_runtime.py` | 调试器不进行任何取证(抓包/截图/代码分析)就猜测原因并修改代码,引入新问题且污染代码库 | 7 步工作流为建议性不强制,无"先读后写"硬性门控——未执行任何取证行为就不允许修改代码 | | P1-18 | 项目缺乏标准化日志体系 | 项目引导 / AirArc 规划 | 生成的代码无统一日志输出,debug/release 无法切换,问题排查困难 | 无项目级日志标准要求,AirArc 规划时未强制 spdlog 集成,AirRvr 审查时未检查日志完备性 | | P1-19 | 边界无测试 + 终审缺高风险检查 | AirDo / AirRvr | 代码边界无接口测试和单元测试,最终审查未着重检查生命周期、空指针、悬垂指针、异常风险,产品交付后短时间内崩溃 | AirArc 规划时未强制测试任务,AirRvr 终审无专项高风险审计环节 | | P1-20 | 界面设计缺乏专业 Skill 支撑 | AirDo / 安装器 | UI/前端任务由通用 Agent 直接编写,界面质量差,布局、配色、交互不符合设计规范 | 未集成 frontend-design Skill,AirDo 遇到 UI 任务时无专业工具可用,安装器未自动检测并配置 | | P1-21 | ADR 变更无级联失效机制 | AirArc / AirEng / TaskGraph | 架构方案变更(如 ffmpeg → gstreamer)后,基于旧 ADR 已完成的任务不会自动失效,旧代码残留与新方案冲突,下游任务基于过期产出继续执行 | TaskGraph 无 ADR→任务的溯源链,无已完成任务的失效判定,无回滚清理流程 | | P1-22 | Dispatch → Worker 启动无桥接 | `eng_mode.py:dispatch_worker_group()` | dispatch 只写 JSON 派发清单,不启动 Worker。Worker 启动依赖 Agent 自觉读 payload 并手动调用 Skill 工具——Agent 不读则 Worker 永不启动,Agent 最终「回退自己执行」 | `dispatch_worker_group()` 与 Worker 启动之间仅有 JSON 文件,无代码层桥接。L1 保障未覆盖 Agent 调度层 | | P1-23 | Dispatch 指令歧义 | `commands/eng.md` | Eng 的 dispatch 步骤(spawn Worker)是意图描述而非可执行伪代码,Agent 每步都在猜:用什么工具?参数格式?task-text 从哪取?——猜错多一轮,猜不出来 Worker 不启动 | 指令未降到操作级。Arc 和 Eng 的约束非对称性是刻意的(Arc 永不写→硬阻断,Eng 保留极端接管→不硬阻断),P1-23 是纯指令层问题 | | P1-24 | AirArc 任务描述歧义导致弱模型破坏性执行 | AirArc `review.py` / SKILL.md | 任务粒度太粗、用词有歧义(如"清理"被弱模型理解为"删除全部"),Worker 严格按字面执行导致误删现有代码。真实案例:screenPlayer CMake 重构中 Worker 删除了整个 src/ | Arc 未针对弱模型优化任务描述,无"保留约束"机制,任务粒度未按操作类型拆分 | | P1-25 | Merge 后 TaskGraph 状态不同步 | `eng_mode.py:merge_worker_result()` | merge 更新 todo.md 和 state.json 但不动 task-graph.json。已完成任务的节点状态仍是 TODO/DISPATCHED,再次 dispatch 重复派发 | `merge_worker_result()` Phase 5/6 未同步 `task-graph.json` 节点 status 字段 | #### P2 — 限制规模化 | ID | 缺陷 | 位置 | 影响 | |----|------|------|------| | P2-1 | 冲突检测 O(n²) | `review.py` `combinations(active_tasks, 2)` | 100 任务时 ~495,000 次路径比较 | | P2-2 | state.json 无界增长 | `engine.py` | `mergedResults` 等列表永不截断 | | P2-3 | todo.md 每次操作全量重解析 | engine 多处调用 `parse_tasks()` | 大 todo 表时性能退化 | | P2-4 | 零测试覆盖 | 整个 `air_runtime/` | 任何重构都有回归风险 | #### P3 — 限制用户体验 | ID | 缺陷 | 位置 | 影响 | |----|------|------|------| | P3-1 | AGENTS.md 膨胀 | AirEng sync 追加无去重 | 同一任务记录重复 2-3 次 | | P3-2 | 写集刚性导致级联任务链 | 写集边界设计 | T-028 衍生 fix-001~005 + T-028b + T-028c | | P3-3 | 并行 Worker 抢占共享硬件 | 无硬件资源感知 | kmsgrab 锁死、负载 7.59 自发重启 | | P3-4 | 环境特定修复不可持久化 | 部署自动化不完整 | MonitorServiceD、cgroup v1 每次重启需手动修复 | | P3-5 | 跨项目知识不迁移 | 无模板继承机制 | 每个项目从零积累运维经验 | ### 1.2 插件级差距 | 插件 | V1 差距 | 影响 | |------|---------|------| | **AirContext** | 压缩质量无监控;Token 估算 `char_div_3.5` 粗糙;续传 prompt 硬编码中文;锁文件无陈旧检测 | 坏摘要静默损坏上下文 | | **AirDbg** | 7 步工作流纯建议性不强制;无不可复现 bug 分支;无回滚能力 | 调试质量依赖模型自觉 | | **AirXDB** | 无 headless CI;无 DRM/KMS 原生截图;无截图 diff;远程探测不含 ffmpeg | 生产渲染路径无法自动验证 | | **AirNDB** | 无 TLS 解密;大 pcap `tail(8000)` 截断;无 pcapng 支持 | 大规模抓包分析能力不足 | | **AirSDB** | 仅 C/C++;无 diff 模式;无 compile_commands.json 生成 | 多语言项目零覆盖 | | **AirArc** | 无规划质量验证;无增量重规划;无执行→规划反馈 | scope 变更必须全量重新生成 | | **AirEng** | 无级联故障保护;无资源耗尽监控;5 分钟固定轮询;无 Worker 总时间上限 | 大规模调度时稳定性不足 | ### 1.3 真实项目痛点汇总 | 痛点 | 频次 | 根因缺陷 | |------|------|---------| | AirXDB 假阳性阻塞 | 11+ 任务 | P0-1 | | 完成但未部署 | 1 次关键事故 | P0-2 | | 写集级联任务链 | 5+ 条链 | P3-2 | | 并行 Worker 抢占硬件 | 3+ 次 | P3-3 | | 空壳修复循环 | 11+ 次 | P0-1 + P3-4 | | AGENTS.md 膨胀 | 持续累积 | P3-1 | | 环境修复不可持久 | 每次重启 | P3-4 | | AirArc 被 plan 模式劫持 | 频繁 | P0-5 | | AirEng 反复询问不自主推进 | 每次调度 | P0-6 | | AirEng 遗忘轮询导致无限等待 | 频繁 | P0-7 | | AirDo 跳过 AirDbg 直接返回 | 频繁 | P0-8 | | 安装后插件无法识别或脚本路径错误 | 用户普遍反馈 | P0-9 | | 需求变更后调度需多轮迭代恢复 | 每次变更 | P1-14 | | 同文件无冲突任务被迫串行 | 频繁 | P1-15 | | 实现偏离设计无对照机制 | 持续累积 | AirRvr 设计缺口 | | AirArc 跳过需求探讨直接生成规划 | 每次启动 | P1-16 | | AirDbg 不取证就猜测修复污染代码 | 频繁 | P1-17 | | AirEng 偏离调度亲自写代码 | 频繁 | P0-10 | | 项目代码缺乏标准化日志体系 | 所有项目 | P1-18 | | 边界无测试 + 终审缺高风险检查 | 所有项目 | P1-19 | | 界面设计缺乏专业 Skill 支撑 | UI 任务 | P1-20 | | ADR 变更后已完成任务不失效 | 架构变更时 | P1-21 | | AirArc 任务描述歧义导致弱模型破坏性执行 | 已造成实际损失 | P1-24 | --- ## 2. V2 设计目标 ### 2.1 核心目标 1. **可靠性**:状态写入不丢失,并发操作不竞态,崩溃后可自愈 2. **可观测性**:所有引擎操作可追溯,指标可导出,异常主动通知 3. **智能化**:证据门控感知任务类型,轮询频率自适应,修复模式可学习 4. **规模化**:支持 100+ 任务、5+ 并行 Worker、多项目知识迁移 ### 2.2 不变量 V2 必须保持 V1 的核心不变量: | 不变量 | V1 定义 | V2 保持方式 | |--------|---------|------------| | INV-1 制品驱动通信 | 插件间通过 AirPlan/ 文件通信 | 保持,增加事件索引层 | | INV-2 上下文隔离 | Worker `fork_context=false` | 保持,增加选择性上下文继承 | | INV-3 架构同步强制 | 不更新架构文档不能 DONE | 保持,增加增量同步 | | INV-4 证据先于修复 | 截图/抓包/静态分析前置 | 保持,增加任务类型感知 | | INV-5 闭环自动修复 | 执行→失败→调试→修复→重执行 | 保持,增加修复模式学习 | --- ## 3. V2 架构改进 ### 3.1 基础设施层重构 #### 3.1.1 `air_runtime.io` — 统一 I/O 模块 消除 5 份 `_json_dump`/`_json_load` 重复,统一为原子写入: ```python # air_runtime/io.py def atomic_json_write(path: Path, data: dict) -> None: """POSIX 原子写入:tempfile + os.replace()""" path.parent.mkdir(parents=True, exist_ok=True) fd, tmp = tempfile.mkstemp(dir=path.parent, suffix=".tmp") try: os.write(fd, json.dumps(data, indent=2, ensure_ascii=False).encode("utf-8")) os.close(fd) os.replace(tmp, path) except BaseException: with contextlib.suppress(OSError): os.unlink(tmp) raise def safe_json_load(path: Path) -> dict | None: """安全加载:处理损坏文件,自动从 .bak 恢复""" try: return json.loads(path.read_text("utf-8")) except (json.JSONDecodeError, UnicodeDecodeError): bak = path.with_suffix(path.suffix + ".bak") if bak.exists(): logging.warning("corrupt %s, restoring from %s", path, bak) return json.loads(bak.read_text("utf-8")) logging.error("corrupt %s with no backup", path) return None ``` 每次写入前自动备份旧文件为 `.bak`(单级轮转),保证至少有一次完整的历史版本。 #### 3.1.2 `air_runtime.lock` — 文件级并发控制 ```python # air_runtime/lock.py class FileLock: """基于 fcntl.flock 的进程级文件锁""" def __init__(self, path: Path, timeout: float = 10.0): self._path = path.with_suffix(path.suffix + ".lock") self._timeout = timeout self._fd = None def __enter__(self): self._fd = os.open(self._path, os.O_CREAT | os.O_RDWR) deadline = time.monotonic() + self._timeout while True: try: fcntl.flock(self._fd, fcntl.LOCK_EX | fcntl.LOCK_NB) return self except OSError: if time.monotonic() >= deadline: raise TimeoutError(f"lock timeout: {self._path}") time.sleep(0.1) def __exit__(self, *exc): fcntl.flock(self._fd, fcntl.LOCK_UN) os.close(self._fd) ``` 所有 `state.json` 和 `todo.md` 的读-改-写操作必须持有对应锁。 #### 3.1.3 `air_runtime.utils` — 消除代码重复 ```python # air_runtime/utils.py def ordered_unique(items: list) -> list: """保序去重""" seen = set() result = [] for item in items: key = item if isinstance(item, str) else item.get("id", str(item)) if key not in seen: seen.add(key) result.append(item) return result def session_stamp() -> str: """统一的文件系统安全时间戳""" return datetime.now(timezone.utc).isoformat().replace(":", "-").replace(".", "-").replace("+", "-") def normalize_policy(defaults: dict, overrides: dict | None) -> dict: """通用的策略合并""" merged = {**defaults} if overrides: for k, v in overrides.items(): if k in merged: expected_type = type(defaults[k]) merged[k] = expected_type(v) if not isinstance(v, expected_type) else v return merged def sanitize_task_id(task_id: str) -> str: """防止路径注入""" if not re.fullmatch(r"[A-Za-z0-9_\-]+", task_id): raise ValueError(f"invalid task_id: {task_id!r}") return task_id def sanitize_marker(marker: str) -> str: """防止 HTML 注释注入""" if "-->" in marker or "