Files
AirCoding/AirPlan/docs/spec/AirPlanV2/airplanV2-Qwen3.7-Max设计.md
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

1843 lines
76 KiB
Markdown
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 不调用 AirDbg | AirDo SKILL.md / `worker.py` `finish_worker()` | 执行器遇到问题或验收时几乎不触发调试,直接报 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 SkillAirDo 遇到 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 | 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 |
---
## 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 "<!--" in marker:
raise ValueError(f"marker contains comment delimiters: {marker!r}")
return marker
```
### 3.2 引擎层改进
#### 3.2.1 任务类型感知的证据门控
```python
# 替代 airxdb_runtime.py 中的无差别触发
class EvidenceGatePolicy:
"""基于任务特征的差异化证据要求"""
GUI_INDICATORS = {"gui", "ui", "render", "layout", "dialog", "osd",
"overlay", "visual", "screenshot", "display",
"widget", "pane", "toolbar", "settings_dialog"}
NETWORK_INDICATORS = {"network", "rtsp", "http", "tcp", "udp", "tls",
"dns", "proxy", "socket", "stream", "port"}
def classify(self, task: TaskRecord) -> EvidenceClass:
text = f"{task.task} {task.files_dirs} {task.done_when}".lower()
has_gui = any(kw in text for kw in self.GUI_INDICATORS)
has_net = any(kw in text for kw in self.NETWORK_INDICATORS)
if has_gui:
return EvidenceClass.GUI_REQUIRED
elif has_net:
return EvidenceClass.NETWORK_REQUIRED
else:
return EvidenceClass.CODE_ONLY # 不需要截图/抓包
```
#### 3.2.2 部署验证强制
```python
# contracts.py 扩展
class WorkerResult:
def validate_for_finalize(self, brief: dict) -> None:
# ... 现有验证 ...
# V2 新增: 部署一致性
if brief.get("deploy_required"):
deploy_validations = [
v for v in self.validations
if v.kind in ("remote-deploy-verify", "remote-binary-md5")
]
if not deploy_validations:
raise ValidationError(
"deploy_required but no deploy verification in validations"
)
if any(v.status != "passed" for v in deploy_validations):
raise ValidationError(
"deploy verification did not pass"
)
```
#### 3.2.3 自适应轮询
```python
# engine.py 监控循环改进
class AdaptivePoller:
"""根据 Worker 状态动态调整轮询间隔"""
def __init__(self, min_interval=30, max_interval=300):
self.min_interval = min_interval
self.max_interval = max_interval
self._worker_phases: dict[str, str] = {}
def interval_for(self, worker: dict) -> int:
age_seconds = (now() - worker["spawnedAt"]).total_seconds()
# 刚派发: 快速检测早期失败
if age_seconds < 120:
return self.min_interval
# Worker 报告接近完成
state = self._read_worker_state(worker)
if state and state.get("phase") in ("finalizing", "running-validations"):
return self.min_interval
# 稳定执行中
return self.max_interval
```
#### 3.2.4 Worker 超时与级联保护
```python
# engine.py 新增
WORKER_MAX_WALL_TIME = 7200 # 2 小时硬上限, 可配置
def _check_worker_timeout(self, worker: dict) -> bool:
age = (now() - worker["spawnedAt"]).total_seconds()
if age > WORKER_MAX_WALL_TIME:
self._record_intervention(
taskId=worker["taskId"],
reason="wall-time-exceeded",
action="terminate-and-block",
)
return True
return False
def _check_resource_pressure(self) -> bool:
"""系统资源压力检测"""
load = os.getloadavg()[0]
cpu_count = os.cpu_count() or 4
if load > cpu_count * 2:
logging.warning("system load %.1f > 2x cpu_count (%d), pausing dispatch", load, cpu_count)
return True
return False
```
#### 3.2.5 合并事务化
V1 的 `merge_worker_result()` 执行 8+ 次文件写入中间崩溃导致不一致。V2 引入事务语义:
```python
def merge_worker_result(self, project_root, result_path):
lock = FileLock(project_root / "AirPlan" / "state" / "aireng" / "state.json")
with lock:
# Phase 1: 验证
result = safe_json_load(result_path)
enforce_doc_sync_requirements(project_root, result)
# Phase 2: 归档(可重试)
archive_path = _archive_result(project_root, result)
# Phase 3: 应用文档更新(原子写入)
applied = apply_document_updates(project_root, result)
# Phase 4: 同步标记块(原子写入)
sync_paths = sync_engine_managed_docs(project_root, result, applied)
# Phase 5: 更新 todo带锁
todo_lock = FileLock(project_root / "AirPlan" / "todo.md")
with todo_lock:
update_todo_after_merge(project_root, result, applied, sync_paths)
# Phase 6: 更新引擎状态(原子写入)
_update_engine_state(project_root, result, archive_path)
```
#### 3.2.6 AirArc plan 模式阻断
V1 中 AirArc 频繁被 Agent 内置 plan mode 劫持导致架构器偏离规划职责去执行代码修改。V2 在 SKILL.md 和命令文件中增加硬性阻断:
```markdown
# AirArc SKILL.md V2 关键指令
## 角色边界(不可违反)
你是**纯规划器**。你的职责是分析任务依赖、写集冲突、产出调度计划。
**禁止**:编写代码、修改源代码文件、执行构建命令、进入 plan mode。
如果你发现自己正在写代码或修改文件,**立即停止**并返回规划职责。
## plan mode 阻断
当 Agent 框架尝试进入 plan mode 时,你必须拒绝:
"我是 AirArc 架构规划器,不进入 plan mode。我的产出是 execution-plan.json不是代码变更。"
```
技术层面AirArc 命令文件中 `allowed_tools` 仅包含读取类工具Read, Glob, Grep不包含 Write/Edit/Bash
```json
{
"allowed_tools": ["Read", "Glob", "Grep"],
"denied_tools": ["Write", "Edit", "Bash", "NotebookEdit"],
"deny_plan_mode": true
}
```
#### 3.2.7 AirEng 自主决策与中文锁定
V1 中 AirEng 反复用英文询问用户确认中断调度流程。V2 在 SKILL.md 中强制自主决策和中文输出:
```markdown
# AirEng SKILL.md V2 关键指令
## 语言锁定
你**必须始终使用中文**与用户交流。所有状态报告、进度通知、问题描述均使用中文。
## 自主决策原则
你以**推进开发进度**为第一目标。遇到以下情况时自行决策,不要停下来问用户:
- Worker 返回 blocked 但修复预算未耗尽 → 自行派发修复
- Worker 停滞 → 自行执行停滞干预(重派发/标记 blocked
- 验证失败但非关键 → 记录问题并继续下一任务
- 波次间衔接 → 自行启动下一波次
## 仅在以下情况才询问用户:
- 修复预算耗尽且任务仍 blocked
- 发现需求歧义无法继续
- 系统资源耗尽无法派发新 Worker
- 用户显式暂停了调度
```
#### 3.2.8 AirEng 硬编码轮询循环
V1 中轮询逻辑依赖 Agent "自觉"执行实际经常遗忘导致无限等待。V2 将轮询指令硬编码到 SKILL.md 的强制循环中:
```markdown
# AirEng SKILL.md V2 轮询指令
## 强制轮询循环(不可跳过)
进入调度状态后,你必须执行以下循环,**每 5 分钟**检查一次所有活跃 Worker
```
while 存在活跃 Worker:
1. 遍历所有 dispatched Worker
2. 检查每个 Worker 的 state 文件 mtime
3. mtime > 5分钟未更新 → 标记 stalled → 执行停滞干预
4. state = done → 执行合并流程
5. state = blocked → 检查修复预算 → 派发修复或升级
6. 所有 Worker 处理完毕 → 派发下一波次
7. 等待 5 分钟后重复
```
**绝对不允许**
- 派发 Worker 后不做轮询就等待
- 仅检查一次就声称"等待 Worker 完成"
- 轮询间隔超过 10 分钟
```
技术补充:引擎 `monitor_engine()` 增加 wall-clock 超时检测Worker 超过 `WORKER_MAX_WALL_TIME` 自动 terminate。
#### 3.2.8b AirEng 调度职责边界
V1 中 AirEng 频繁偏离调度职责自己编写代码破坏上下文隔离架构。V2 明确区分"仅调度"与"极端接管"的边界:
```markdown
# AirEng SKILL.md V2 职责边界指令
## 核心职责:调度,不是编码
你是**调度引擎**,不是执行器。你的职责是:
- 解析执行计划,派发 AirDo Worker
- 轮询 Worker 状态,处理停滞和合并
- 管理波次衔接和修复预算
**默认禁止**:编写源代码、修改项目文件。这些是 AirDo Worker 的职责。
## 极端接管(唯一例外)
仅在以下条件**全部满足**时,你才可以少量修改代码:
1. 子代理陷入循环阻塞,修复预算已耗尽
2. 问题已通过 AirDbg 定位到明确的根因
3. 修复范围极小≤5 行改动,如配置修正、路径修复)
4. 继续等待 Worker 重派发已无意义(至少尝试过 2 次)
进入极端接管前,必须在引擎日志中记录:
"EXTREME_TAKEOVER: taskId=X, reason=Y, changes=Z"
## 违规判定
如果你在以下场景编写代码,视为违规:
- 正常调度流程中Worker 可用且未阻塞)
- 修复预算未耗尽时
- 改动超过 5 行
- 未经 AirDbg 定位就猜测修复
```
#### 3.2.9 AirDo 强制 AirDbg 路由
V1 中 AirDo 遇到问题时倾向直接报 blocked 或 false-done跳过 AirDbg 调试。V2 将 AirDbg 调用从"建议"升级为"强制"
```python
# worker.py finish_worker() V2 改进
def finish_worker(result: WorkerResult, brief: dict) -> RoutingDecision:
if result.status == "done":
# done 也必须有实质验证,否则路由到 AirDbg
if not result.validations and not result.filesChanged:
return RoutingDecision(
target="airdbg",
reason="done without evidence — mandatory debug review",
forced=True,
)
return RoutingDecision(target="merge")
if result.status in ("blocked", "failed"):
# blocked/failed 必须先经 AirDbg不允许直接返回
return RoutingDecision(
target="airdbg",
reason=f"status={result.status} — AirDbg mandatory before return",
forced=True,
)
```
SKILL.md 配合强制指令:
```markdown
## AirDbg 调用强制规则
- 任务状态为 blocked 或 failed 时,**必须**先调用 AirDbg 进行调试
- 任务状态为 done 但无验证证据时,**必须**调用 AirDbg 验证
- **禁止**直接返回 blocked 而不经过调试
- **禁止**在没有截图/测试结果的情况下声称 GUI 任务完成
```
#### 3.2.10 安装器路径修正
V1 安装器存在两个问题:(1) 安装后插件命令不被识别;(2) AI 修复注册后脚本执行路径不正确。
根因:安装器使用相对路径或硬编码路径注册插件命令,未根据实际安装位置动态生成绝对路径。
V2 修正:
```python
# installer.py V2 改进
def resolve_plugin_paths(install_dir: Path) -> dict[str, str]:
"""根据实际安装目录动态生成所有插件脚本的绝对路径"""
scripts_dir = install_dir / "scripts"
return {
plugin_name: str(scripts_dir / f"{plugin_name}.py")
for plugin_name in PLUGIN_NAMES
}
def register_plugin_commands(paths: dict[str, str]) -> None:
"""注册时使用已解析的绝对路径,并验证文件存在"""
for name, path in paths.items():
if not Path(path).exists():
raise InstallError(f"plugin script not found: {path}")
register_command(name, command=path)
def post_install_verify() -> VerifyResult:
"""安装后自动验证:命令可识别 + 脚本可执行"""
errors = []
for name in PLUGIN_NAMES:
if not is_command_registered(name):
errors.append(f"{name}: command not registered")
elif not can_execute(name):
errors.append(f"{name}: script not executable")
return VerifyResult(ok=not errors, errors=errors)
```
验证标准:安装完成后 `post_install_verify()` 必须全部通过,否则安装器自动报错并输出修复建议,而非让用户自行发现。
#### 3.2.11 动态图调度(替代静态表格)
V1 的 AirArc 产出静态 `todo.md` 表格AirEng 基于该表格调度。当需求变更时 Arc 重新生成表格Eng 无法增量吸收差异,需多轮 AI 迭代才能恢复。
V2 将调度结构从表格升级为**动态有向图 (DAG)**
```python
# air_runtime/task_graph.py
class TaskGraph:
"""动态任务依赖图,支持增量更新"""
def __init__(self):
self.nodes: dict[str, TaskNode] = {}
self.edges: list[Edge] = []
def apply_delta(self, delta: PlanDelta) -> None:
"""增量吸收 Arc 的重规划结果,无需全量重建"""
for removed in delta.removed_tasks:
self._remove_node(removed)
for added in delta.added_tasks:
self._add_node(added)
for modified in delta.modified_tasks:
self._update_node(modified)
for edge_change in delta.edge_changes:
self._update_edge(edge_change)
def ready_tasks(self) -> list[str]:
"""返回当前入度为 0 且未调度的任务"""
return [n.id for n in self.nodes.values()
if n.in_degree == 0 and n.status == "TODO"]
class PlanDelta:
"""Arc 重规划产出的增量差异"""
removed_tasks: list[str]
added_tasks: list[TaskNode]
modified_tasks: list[TaskNode]
edge_changes: list[EdgeChange]
```
Arc 重规划时不再全量覆盖 `todo.md`,而是产出 `plan-delta.json`Eng 调用 `apply_delta()` 增量更新图结构,保持已调度任务不受影响。
#### 3.2.11b Dispatch → Worker 启动桥接
V2 初版中,`dispatch_worker_group()` 完成冲突检测、选定 ready 任务后,仅将派发清单写入 `state/aireng/dispatch/wave-*.json`**不执行任何 Worker 启动操作**。Worker 的实际启动依赖当前运行的 Claude Agent 自觉读取 dispatch payload 并手动调用 `Skill` 工具逐个 spawn `/do` 子代理。
**缺陷**:设计与 L1 代码级保障原则矛盾。`commands/eng.md` 第 59 行写「读取派发清单,为每个任务 spawn 隔离的 /do Worker 子代理」——但这是对 LLM 的**意图描述**而非**操作步骤**。Agent 需要多轮推理才能翻译成具体操作用什么工具参数格式是什么task-text 从哪取?每一步都是 Agent 在猜,猜错就多一轮,猜不出来就「回退自己执行」。
**根因**dispatch 与 Worker 启动之间只有 JSON 文件没有代码层桥接。L1 代码级保障覆盖了 Python 层(门控、路由、审计),但**未覆盖 Agent 调度层**——即「Claude Agent 如何把 dispatch payload 变成实际的 Skill 调用」这一环节。
**V2 修正**:从两个层面补齐——
**(a) 指令操作化**`commands/eng.md` 中 dispatch 段必须给出可执行的伪代码而非意图描述:
```markdown
### dispatch
1. 运行 `python ... --sub dispatch`,得到 `{waveId, taskIds, dispatchPath}`
2. 对 taskIds 中的每个 tid:
a. 从 task-graph.json 读 `nodes[tid].task` 获取任务描述
b. 调用 `Skill` 工具:
- skill: "airplan"
- args: "do --sub enter --task-id {tid} --task-text '{task}' --project ."
c. 每个 Skill 调用是独立的子代理(自动 fork_context=false
3. 所有 Worker spawn 完成后,记录 wave 启动,进入 monitor 状态
```
**(b) 工具白名单对齐**`commands/eng.md``allowed-tools` 当前为 `[Read, Glob, Grep, Bash, Write, Edit]`,与 Arc 的 `[Read, Glob, Grep]` 形成鲜明对比。Eng 的「不能写代码」INV-6仅有自然语言约束没有工具白名单硬阻断。
Eng 模式需要**分层工具权限**
- 调度操作plan/dispatch/monitor/merge允许 Bash + Write写 AirPlan/ 目录下的状态文件)
- 任务代码修改:禁止 Write/Edit由 Skill 子代理在 /do 上下文中执行)
但 Claude Code CLI 的 `allowed-tools` 是 per-command 而非 per-context 的,无法在一个 Agent 内动态切换。因此 V2 的折中方案:
1. 保持 `allowed-tools: [Read, Glob, Grep, Bash, Write, Edit]`
2. 在 SKILL.md 中将 INV-6 升级为**硬编码检查**:每次 Write/Edit 调用前校验目标路径是否在 `AirPlan/` 目录下,不在则拒绝并提示「由 /do Worker 执行」
3. `commands/eng.md` 中增加明确的操作步骤伪代码(如上述),消除 Agent 的推理歧义
**(c) Worker spawn 函数**:新增 `air_runtime/modes/eng_mode.py:spawn_workers(project_root, task_ids)` —— 不替代 Agent 决策层,但提供标准化数据准备:
```python
def spawn_workers(project_root: Path, task_ids: list[str]) -> list[dict]:
"""为每个 ready 任务准备 spawn 指令,返回 Agent 可直接消费的 Skill 调用参数列表。
不实际启动子进程——启动由 Agent 框架的 Skill 工具完成。"""
tg_json = airplan_root(project_root) / "state" / "airarc" / "reviews" / "task-graph.json"
graph = TaskGraph.load(tg_json) if tg_json.exists() else TaskGraph()
instructions = []
for tid in task_ids:
node = graph.nodes.get(tid)
task_text = node.task if node else ""
instructions.append({
"skill": "airplan-v2:do",
"args": f"--sub enter --task-id {tid} --task-text '{task_text}' --project .",
"taskId": tid,
"taskText": task_text,
})
return instructions
```
调用方Eng Agent只需遍历返回的列表逐个调用 `Skill` 工具即可,无需自行从 task-graph.json 提取 task_text。
#### 3.2.11c Merge → TaskGraph 状态同步
V2 初版的 `merge_worker_result()` 在 Phase 5 更新 `todo.md`、Phase 6 更新 `state.json`,但**不更新 `task-graph.json` 中对应节点的 status**。导致 merge 完成后task-graph 中已完成任务的 status 仍为 TODO/DISPATCHED再次 dispatch 时会**重复派发已完成任务**。
**修正**:在 `merge_worker_result()` 的 Phase 6写回引擎状态中追加 task-graph 状态同步:
```python
# Phase 6.5: 同步 task-graph.json 节点状态
tg_json = airplan_root(project_root) / "state" / "airarc" / "reviews" / "task-graph.json"
if tg_json.exists():
graph = TaskGraph.load(tg_json)
if task_id in graph.nodes:
graph.nodes[task_id].status = "DONE" if status == "done" else status.upper()
_export_task_graph_json(graph, tg_json)
```
**不变量**`task-graph.json` 中的节点 status 是 Eng 调度决策的权威来源(`_select_ready_tasks` 依赖 `ready_tasks()` 筛选 `status == "TODO"`),必须与 merge 结果保持同步。
#### 3.2.12 Worktree 隔离并行(同文件不同区域)
V1 的冲突检测粒度为文件级。实际场景中,同一文件的不同区域可能互不影响(如 Qt 组件样式 vs 状态机逻辑),可以安全并行。
V2 引入**区域级冲突检测 + git worktree 隔离**
```python
# review.py V2 改进
class RegionConflictDetector:
"""区域级写集冲突检测"""
def detect(self, task_a: TaskNode, task_b: TaskNode) -> ConflictLevel:
file_overlap = set(task_a.write_set) & set(task_b.write_set)
if not file_overlap:
return ConflictLevel.NONE
region_overlap = self._check_region_overlap(task_a, task_b, file_overlap)
if region_overlap:
return ConflictLevel.HARD # 同区域,必须串行
else:
return ConflictLevel.SOFT # 同文件不同区域,可 worktree 隔离
class WorktreeIsolation:
"""为 SOFT 冲突任务创建 worktree 隔离,完成后合并"""
def create_worker_worktree(self, task_id: str, base_branch: str) -> Path:
wt_path = Path(f".git/worktrees/air-{task_id}")
subprocess.run(["git", "worktree", "add", str(wt_path), base_branch],
check=True, capture_output=True)
return wt_path
def merge_back(self, task_id: str, wt_path: Path) -> MergeResult:
"""worktree 完成后合并回主分支"""
subprocess.run(["git", "merge", f"air-{task_id}"], check=True)
subprocess.run(["git", "worktree", "remove", str(wt_path)],
check=True, capture_output=True)
```
调度策略:`NONE` 直接并行,`SOFT` 使用 worktree 隔离并行,`HARD` 强制串行。合并冲突时自动升级到 AirDbg。
#### 3.2.13 AirArc 需求探讨门控
V1 中 AirArc 在用户仅说一两句时就立即生成执行计划未与用户充分探讨需求和分析架构。V2 强制"先探讨、后确认、再规划"的三阶段流程:
```markdown
# AirArc SKILL.md V2 流程指令
## 三阶段流程(不可跳过)
### 阶段一:需求探讨
- 与用户反复讨论需求细节、边界条件、隐含约束
- 主动提问澄清模糊点,不要假设用户意图
- 分析现有代码库的结构、技术栈、约束条件
- 提出多种架构方案及其优劣势对比
- **此阶段禁止生成 execution-plan.json**
### 阶段二:架构确认
- 向用户呈现推荐的架构方案(模块划分、依赖关系、技术选型)
- 明确等待用户确认:"请确认此架构方案是否符合预期,确认后我将生成执行规划"
- 用户有异议时回到阶段一修订
- **此阶段禁止生成 execution-plan.json**
### 阶段三:生成规划
- 仅在用户明确确认架构无误后,才生成 execution-plan.json
- 规划产出必须严格对应用户确认的架构方案
```
技术门控AirArc 运行时增加 `phase` 状态追踪,`execution-plan.json` 仅在 `phase == "confirmed"` 时允许写入:
```python
class ArcPhaseGate:
PHASES = ["discussing", "proposing", "confirmed"]
def can_write_plan(self) -> bool:
return self.current_phase == "confirmed"
def confirm_architecture(self, user_confirmation: str) -> None:
if "确认" in user_confirmation or "可以" in user_confirmation:
self.current_phase = "confirmed"
```
#### 3.2.14 项目级 spdlog 日志标准
V1 生成的代码缺乏统一日志体系debug/release 无法切换问题排查困难。V2 强制所有项目集成 spdlog 日志库,支持 debug/release 级别切换。
**AirArc 规划阶段强制要求**
```markdown
# AirArc SKILL.md V2 日志标准指令
## 项目日志标准(所有 C++ 项目强制)
- 必须使用 spdlog 作为日志库
- 如果项目未集成 spdlog首个任务必须为"集成 spdlog 到项目"
- 所有生成的代码必须使用 spdlog 输出日志,禁止 std::cout/qDebug 等
- 必须支持 debug/release 编译开关切换日志级别
- 关键路径(初始化、网络、文件 I/O、错误必须有日志输出
```
**AirRvr 审查阶段检查项**
```python
# AirRvr 审查清单扩展
LOGGING_CHECKS = [
"spdlog 是否已集成到项目依赖",
"代码中是否存在 std::cout / qDebug / printf 等非标准日志",
"是否有 CMAKE_BUILD_TYPE 或等效的 debug/release 编译开关",
"关键路径(初始化、网络、文件 I/O、错误处理是否有日志输出",
"日志格式是否统一(时间戳 + 级别 + 模块 + 消息)",
]
```
**CMake 集成模板**
```cmake
# 自动检测并集成 spdlog
find_package(spdlog QUIET)
if(NOT spdlog_FOUND)
include(FetchContent)
FetchContent_Declare(spdlog
GIT_REPOSITORY https://github.com/gabime/spdlog.git
GIT_TAG v1.14.1)
FetchContent_MakeAvailable(spdlog)
endif()
# debug/release 日志级别切换
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
target_compile_definitions(${PROJECT_NAME} PRIVATE SPDLOG_ACTIVE_LEVEL=SPDLOG_LEVEL_TRACE)
else()
target_compile_definitions(${PROJECT_NAME} PRIVATE SPDLOG_ACTIVE_LEVEL=SPDLOG_LEVEL_INFO)
endif()
```
#### 3.2.15 边界测试强制 + 终审高风险审计
V1 生成的代码在模块边界缺乏接口测试和单元测试最终审查未着重检查高风险问题产品交付后短时间内崩溃。V2 从规划和审查两端强制保障。
**AirArc 规划阶段——测试任务强制**
```markdown
# AirArc SKILL.md V2 测试要求指令
## 边界测试(每个模块边界强制)
- 每个模块的公共接口必须有对应的接口测试任务
- 每个模块的核心逻辑必须有对应的单元测试任务
- 规划时自动生成测试任务,标记为 test-required
- Done When 条件必须包含"测试通过"
```
**AirRvr 终审阶段——高风险专项审计**
```
终审检查清单(里程碑审查时强制执行):
生命周期风险:
- 资源申请与释放是否配对new/delete, malloc/free, open/close
- RAII 是否覆盖所有资源持有
- 异步操作的回调/完成是否保证执行
空指针/悬垂指针:
- 指针使用前是否判空
- 智能指针生命周期是否覆盖使用范围
- 回调闭包中捕获的指针是否仍然有效
- 容器元素删除后迭代器是否失效
异常安全:
- 异常路径中资源是否正确释放
- 是否存在异常吞没catch 后无处理)
- 跨模块边界是否有异常传播保护
并发风险:
- 共享状态是否有锁保护
- 锁顺序是否一致(防死锁)
- 条件变量是否有虚假唤醒保护
报告结构扩展:
{
"highRiskAudit": {
"lifecycle": [
{"file": "src/network/connection.cpp", "line": 145,
"severity": "critical", "issue": "socket fd 在异常路径未关闭"}
],
"nullPointer": [
{"file": "src/auth/handler.cpp", "line": 78,
"severity": "high", "issue": "getUser() 返回 nullptr 后直接调用 ->name()"}
],
"danglingPointer": [],
"exceptionSafety": [],
"concurrency": [],
"overallRisk": "critical | high | medium | low",
"deliveryVerdict": "safe-to-ship | needs-fix | block-release"
}
}
```
**与 AirEng 集成**`deliveryVerdict = block-release` 时阻止任何后续派发,要求立即修复。
#### 3.2.16 界面设计 frontend-design Skill 集成
V1 中 UI/前端任务由通用 Agent 直接编写界面质量差。V2 强制 UI 任务使用 frontend-design Skill未安装时自动配置。
**AirDo SKILL.md 指令**
```markdown
## 界面设计任务处理
- 当任务涉及 UI/前端/界面设计时,必须使用 frontend-design Skill
- 如果 frontend-design Skill 不存在,先执行自动安装再继续
- 禁止在无 frontend-design Skill 的情况下直接编写 UI 代码
```
**自动安装检测**
```python
# worker.py 新增
def ensure_frontend_design_skill() -> bool:
"""检测 frontend-design Skill 是否存在,不存在则自动安装"""
skill_path = get_skill_path("frontend-design")
if skill_path and skill_path.exists():
return True
logging.info("frontend-design skill not found, auto-installing...")
result = subprocess.run(
["qoderclicn", "skill", "install", "frontend-design"],
capture_output=True, text=True, timeout=60
)
if result.returncode == 0:
logging.info("frontend-design skill installed successfully")
return True
logging.error("frontend-design skill install failed: %s", result.stderr)
return False
def route_ui_task(task: TaskRecord) -> RoutingDecision:
if is_ui_task(task) and not ensure_frontend_design_skill():
return RoutingDecision(
target="blocked",
reason="UI task requires frontend-design skill but installation failed"
)
return RoutingDecision(target="execute", skill="frontend-design")
```
#### 3.2.17 ADR 变更级联失效
V1/V2 初版中,当架构方案变更(如 ffmpeg → gstreamer基于旧 ADR 已完成的任务不会自动失效旧代码残留与新方案冲突。V2 增加完整的级联失效机制。
**ADR→任务溯源链**
```python
# air_runtime/task_graph.py 扩展
class TaskNode:
adr_refs: list[str] # 该任务依赖的 ADR 列表,如 ["ADR-0005", "ADR-0012"]
class TaskGraph:
def tasks_by_adr(self, adr_id: str) -> list[TaskNode]:
"""查找所有依赖指定 ADR 的任务(含已完成)"""
return [n for n in self.nodes.values() if adr_id in n.adr_refs]
def invalidate_by_adr(self, adr_id: str, delta: PlanDelta) -> CascadeReport:
"""ADR 变更时级联失效所有相关任务"""
affected = self.tasks_by_adr(adr_id)
completed = [t for t in affected if t.status == "DONE"]
in_progress = [t for t in affected if t.status == "DOING"]
pending = [t for t in affected if t.status == "TODO"]
# 1. 暂停调度:阻止新任务派发直到重规划完成
self.dispatch_frozen = True
# 2. 中止进行中的 Worker
for t in in_progress:
self._terminate_worker(t.id)
# 3. 标记已完成任务为 invalidated
for t in completed:
t.status = "INVALIDATED"
delta.removed_tasks.append(t.id)
# 4. 级联失效下游任务
downstream = self._find_downstream(completed + in_progress)
for t in downstream:
if t.status in ("TODO", "DOING"):
t.status = "INVALIDATED"
delta.removed_tasks.append(t.id)
# 5. 记录回滚点
delta.rollback_ref = self._create_rollback_snapshot(completed)
return CascadeReport(
invalidated_completed=len(completed),
terminated_in_progress=len(in_progress),
cascaded_downstream=len(downstream),
rollback_ref=delta.rollback_ref,
)
```
**回滚清理流程**
```
ADR 变更处理流程:
1. AirArc 检测到 ADR 变更ADR-0005 从 ffmpeg 改为 gstreamer
2. AirArc 产出 PlanDelta标记旧 ADR 为 superseded
3. AirEng 调用 invalidate_by_adr("ADR-0005")
4. 冻结调度dispatch_frozen = true
5. 中止进行中的相关 Worker
6. git revert 已合并的旧代码(基于 rollback_ref
7. AirArc 基于新 ADR 重新生成受影响部分的任务
8. AirEng 调用 apply_delta() 吸收新任务
9. 解冻调度dispatch_frozen = false
10. 继续正常调度流程
```
**ADR 变更自动检测**
```python
# air_runtime/adr_watcher.py
class ADRWatcher:
"""监控 ADR 文件变更,自动触发级联失效"""
def __init__(self, adr_dir: Path):
self._adr_dir = adr_dir
self._known_hashes: dict[str, str] = {} # adr_id -> content_hash
def snapshot(self) -> None:
"""启动时记录所有 ADR 的内容 hash"""
for adr_file in self._adr_dir.glob("ADR-*.md"):
adr_id = adr_file.stem # e.g. "ADR-0005-ffmpeg-decode"
self._known_hashes[adr_id] = hashlib.sha256(
adr_file.read_bytes()
).hexdigest()
def detect_changes(self) -> list[ADRChange]:
"""对比当前 ADR hash 与已知 hash返回变更列表"""
changes = []
for adr_file in self._adr_dir.glob("ADR-*.md"):
adr_id = adr_file.stem
current_hash = hashlib.sha256(adr_file.read_bytes()).hexdigest()
old_hash = self._known_hashes.get(adr_id)
if old_hash is None:
changes.append(ADRChange(adr_id=adr_id, kind="new"))
elif current_hash != old_hash:
status = self._parse_status(adr_file)
if status == "superseded":
changes.append(ADRChange(adr_id=adr_id, kind="superseded"))
else:
changes.append(ADRChange(adr_id=adr_id, kind="modified"))
self._known_hashes[adr_id] = current_hash
return changes
```
AirEng 在每轮轮询时调用 `adr_watcher.detect_changes()`,发现 `superseded``modified` 变更时自动触发 `invalidate_by_adr()`
**局部重规划(替代全量重规划)**
```python
# air_runtime/review.py 扩展
class PartialReplanner:
"""仅重新生成受 ADR 变更影响的任务子集"""
def replan(self, graph: TaskGraph, invalidated_ids: list[str],
new_adr: ADRDocument) -> PlanDelta:
delta = PlanDelta()
# 1. 收集受影响任务的上下文(原始需求、依赖关系、写集)
affected_context = []
for tid in invalidated_ids:
node = graph.nodes.get(tid)
if node:
affected_context.append(node.to_replan_context())
# 2. 仅对受影响部分调用 Arc 重新规划
# 传入新 ADR + 受影响任务的上下文 + 未受影响任务的接口约束
new_tasks = self._call_arc_partial_replan(
new_adr=new_adr,
affected_context=affected_context,
stable_interfaces=self._extract_stable_interfaces(graph, invalidated_ids),
)
# 3. 生成增量 delta仅包含变更部分
delta.added_tasks = new_tasks
# removed_tasks 已在 invalidate_by_adr 中填充
return delta
def _extract_stable_interfaces(self, graph: TaskGraph,
invalidated_ids: set) -> list[Interface]:
"""提取未受影响任务暴露的接口,确保重规划不破坏依赖"""
stable = [n for nid, n in graph.nodes.items()
if nid not in invalidated_ids and n.status == "DONE"]
return [n.public_interface for n in stable]
```
AirArc SKILL.md 配合指令:收到局部重规划请求时,**仅重新生成指定任务列表**,不触碰其他任务的规划。
**与 AirRvr 集成**:终审时检查所有 `INVALIDATED` 任务的代码是否已清理,防止旧方案代码残留。
### 3.3 AirContext V2 改进
#### 3.3.1 压缩质量校验
```python
# compactor.py 新增
class CompressionValidator:
"""验证压缩摘要保留了关键信息"""
MUST_PRESERVE_PATTERNS = [
r"[A-Za-z0-9_\-/]+\.(py|ts|js|cpp|h|md|json|yaml)", # 文件路径
r"ADR-\d{4}", # ADR 引用
r"TODO|FIXME|HACK", # 未完成项
r"INV-\d+", # 不变量引用
]
def validate(self, original_text: str, summary: str) -> ValidationResult:
missing = []
for pattern in self.MUST_PRESERVE_PATTERNS:
original_matches = set(re.findall(pattern, original_text))
summary_matches = set(re.findall(pattern, summary))
lost = original_matches - summary_matches
if len(lost) > len(original_matches) * 0.3: # 丢失 >30%
missing.append({"pattern": pattern, "lost": list(lost)})
if missing:
return ValidationResult(ok=False, missing=missing)
return ValidationResult(ok=True, missing=[])
```
如果校验失败,重试一次压缩(换 prompt 或换模型),仍失败则放弃压缩(保留原始上下文)并通知用户。
#### 3.3.2 Token 估算改进
```python
# token_estimator.py 改进
class AdaptiveTokenEstimator:
"""按内容类型分比率"""
RATIOS = {
"chinese": 1.5, # 中文字符 → token
"english": 4.0, # 英文单词字符 → token
"code": 3.0, # 代码字符 → token
"markup": 5.0, # Markdown/HTML 标记 → token
}
def estimate(self, text: str) -> int:
chinese = len(re.findall(r"[\u4e00-\u9fff]", text))
code = len(re.findall(r"[{}()\[\];=<>]", text))
markup = len(re.findall(r"[#*\-`|]", text))
english = len(text) - chinese - code - markup
tokens = (
chinese / self.RATIOS["chinese"]
+ code / self.RATIOS["code"]
+ markup / self.RATIOS["markup"]
+ english / self.RATIOS["english"]
)
return int(tokens)
```
#### 3.3.3 陈旧锁检测
```python
def acquire_compactor_lock(lock_path: Path) -> bool:
try:
fd = os.open(lock_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
os.write(fd, str(os.getpid()).encode())
os.close(fd)
return True
except FileExistsError:
# 检查持有锁的进程是否存活
try:
pid = int(lock_path.read_text().strip())
os.kill(pid, 0) # 不发信号,只检查进程存在
return False # 进程存活,锁有效
except (ValueError, ProcessLookupError, PermissionError):
# 进程不存在或无法访问 → 陈旧锁,清理
logging.warning("stale lock detected (pid=%s), removing", pid)
lock_path.unlink(missing_ok=True)
return acquire_compactor_lock(lock_path) # 重试
```
### 3.4 AirSDB V2多语言静态分析
```python
# airsdb 架构扩展
class StaticAnalyzerBackend(Protocol):
"""静态分析后端协议(类似 AirContext 的 CompressionBackend"""
name: str
def detect(self) -> bool:
"""检测分析器是否可用"""
def analyze(self, source_paths: list[str], compile_db: Path | None) -> AnalysisResult:
"""执行分析"""
def parse_report(self, report_path: Path) -> list[Finding]:
"""解析报告为统一格式"""
class CppcheckBackend(StaticAnalyzerBackend):
"""现有 cppcheck 实现"""
class ClangTidyBackend(StaticAnalyzerBackend):
"""clang-tidy 集成"""
class RustClippyBackend(StaticAnalyzerBackend):
"""cargo clippy 集成"""
class GoVetBackend(StaticAnalyzerBackend):
"""go vet + staticcheck 集成"""
class TypeScriptBackend(StaticAnalyzerBackend):
"""tsc --noEmit 集成"""
class PythonBackend(StaticAnalyzerBackend):
"""mypy + ruff 集成"""
```
新增 **diff 模式**:比较两次扫描结果,高亮新增/消除的 finding。
```python
class AnalysisDiff:
def compute(self, baseline: list[Finding], current: list[Finding]) -> DiffReport:
baseline_keys = {(f.file, f.line, f.rule_id) for f in baseline}
current_keys = {(f.file, f.line, f.rule_id) for f in current}
return DiffReport(
new_findings=[f for f in current if (f.file, f.line, f.rule_id) not in baseline_keys],
resolved_findings=[f for f in baseline if (f.file, f.line, f.rule_id) not in current_keys],
unchanged_count=len(baseline_keys & current_keys),
)
```
### 3.5 AirDbg V2工作流强制与模式学习
#### 3.5.1 步骤追踪
```python
# airdbg_mode.py 新增
class DebugWorkflowTracker:
"""追踪调试工作流的当前步骤,阻止跳步"""
STEPS = [
"confirm_symptoms",
"load_context",
"reproduce", # 可跳过(标记为 non-reproducible
"locate_root_cause",
"fix",
"verify",
"close_out",
]
def current_step(self, session_id: str) -> str:
state = self._load_session_state(session_id)
return state.get("currentStep", "confirm_symptoms")
def advance(self, session_id: str, evidence: dict) -> None:
"""只有提供了当前步骤要求的证据才能推进"""
step = self.current_step(session_id)
if not self._validate_step_evidence(step, evidence):
raise WorkflowViolation(
f"step '{step}' requires {self._required_evidence(step)}"
)
self._set_step(session_id, self._next_step(step))
def skip_reproduce(self, session_id: str, reason: str) -> None:
"""显式标记为不可复现,进入分析路径"""
self._set_step(session_id, "locate_root_cause")
self._record(session_id, "reproduce_skipped", reason)
```
#### 3.5.2 修复回滚
```python
def pre_fix_snapshot(project_root: Path, task_id: str) -> SnapshotRef:
"""修复前自动创建 git commit 作为回滚点"""
ref = f"airdbg/prefix-{task_id}-{session_stamp()}"
subprocess.run(["git", "commit", "-am", f"AirDbg pre-fix snapshot: {task_id}",
"--allow-empty"], check=True, capture_output=True)
subprocess.run(["git", "tag", ref], check=True, capture_output=True)
return SnapshotRef(ref=ref, timestamp=now_iso())
```
#### 3.5.3 先读后写门控
V1 中 AirDbg 不进行任何取证就猜测原因并修改代码引入新问题且污染代码库。V2 强制"先读后写"原则:必须至少执行一种取证行为后,才允许修改代码。
```python
# debug_runtime.py 新增
class EvidenceFirstGate:
"""取证门控:未执行任何取证行为前,禁止代码修改"""
EVIDENCE_TYPES = [
"screenshot", # AirXDB 截图
"packet_capture", # AirNDB 抓包
"static_analysis", # AirSDB 静态分析
"log_analysis", # 日志分析
"code_trace", # 代码追踪(读取相关文件、调用链分析)
"reproduction", # 复现步骤执行
]
def __init__(self, session_id: str):
self._session_id = session_id
self._collected_evidence: list[str] = []
def record_evidence(self, evidence_type: str, detail: str) -> None:
self._collected_evidence.append(evidence_type)
self._log(f"evidence collected: {evidence_type}{detail}")
def can_modify_code(self) -> bool:
return len(self._collected_evidence) > 0
def gate_check(self) -> None:
if not self.can_modify_code():
raise WorkflowViolation(
"未执行任何取证行为,禁止修改代码。"
"请先至少完成以下一项:截图、抓包、静态分析、日志分析、代码追踪、复现步骤。"
)
```
SKILL.md 配合指令:
```markdown
## 先读后写原则(不可违反)
- 修改任何代码之前,必须至少完成一项取证行为
- 取证行为包括但不限于:截图、抓包、静态分析、日志分析、代码追踪、复现步骤
- 不要求取证覆盖完整(部分问题确实无法完全取证),但必须有至少一项取证作为依据
- 取证结果必须记录到 debug-log.md 后方可开始修复
- **禁止**:未做任何调查就直接修改代码
```
### 3.6 AirXDB V2扩展截图能力
#### 3.6.1 DRM/KMS 原生截图
```python
# airxdb_runtime.py 新增
class KmsGrabCapture(CaptureBackend):
"""Linux DRM/KMS scanout 截图ffmpeg -f kmsgrab"""
def detect(self) -> bool:
result = subprocess.run(["ffmpeg", "-devices"], capture_output=True, text=True, timeout=5)
return "kmsgrab" in result.stdout
def capture(self, output_path: Path, card: str = "/dev/dri/card0") -> CaptureResult:
cmd = [
"sudo", "ffmpeg", "-y", "-f", "kmsgrab", "-framerate", "1",
"-i", card, "-vframes", "1",
"-vf", "hwdownload,format=bgr0",
"-f", "image2", str(output_path),
]
return self._run_with_timeout(cmd, timeout=15)
```
#### 3.6.2 Headless CI 支持
```python
class XvfbCapture(CaptureBackend):
"""Xvfb 虚拟帧缓冲截图"""
def setup(self) -> None:
if not self._display_exists():
subprocess.run(["Xvfb", ":99", "-screen", "0", "1920x1080x24"],
check=True, start_new_session=True)
os.environ["DISPLAY"] = ":99"
def capture(self, output_path: Path) -> CaptureResult:
return subprocess.run(
["xwd", "-root", "-out", str(output_path.with_suffix(".xwd"))],
check=True, capture_output=True, timeout=10,
)
```
### 3.7 新增组件
#### 3.7.1 AirDep — 部署插件
```
职责: SSH 远程构建 + 部署 + systemd 生命周期管理 + 部署验证
工作流:
1. 远程构建 (cmake --build / cargo build / npm build)
2. 二进制传输 (scp + MD5 校验)
3. systemd 操作 (daemon-reload + restart + is-active 验证)
4. 部署产物记录 (binary md5, service status, journal excerpt)
5. 冒烟验证 (可选: 运行预定义的 smoke test)
制品:
AirPlan/state/airdep/
sessions/{session_id}.json # 部署会话完整记录
deploy-log.md # 人类可读的部署日志
```
#### 3.7.2 AirTst — 测试运行器插件
```
职责: 统一测试执行 + 结构化结果报告
支持:
- CTest / GoogleTest (C++)
- pytest (Python)
- jest / vitest (TypeScript)
- go test (Go)
- cargo test (Rust)
制品:
AirPlan/state/airtst/
reports/{task_id}-{ts}.json # 结构化测试结果
test-summary.md # 人类可读摘要
结果格式:
{
"framework": "googletest",
"totalTests": 128,
"passed": 127,
"failed": 0,
"disabled": 1,
"duration": "4.2s",
"failures": []
}
```
#### 3.7.3 AirSec — 安全扫描插件
```
职责: 制品敏感数据扫描 + 自动脱敏
扫描目标:
- ADR/debug-log 中的 API 密钥、令牌、密码
- pcap 文件中的明文凭据
- 截图中的敏感 UI 内容(标注但不自动处理)
集成点:
- Worker finalize 前自动扫描 result.json
- 引擎 merge 前扫描 documentUpdates
- 发现敏感数据时阻止合并并通知用户
```
#### 3.7.4 AirRvr — 需求审查器插件
```
职责: 基于原始需求文档对已完成任务进行独立审查,验证交付物与需求的一致性
核心问题:
V1 中任务"完成"的判定仅依赖 Worker 自报 + AirEng 结构验证。
没有任何组件将交付物与原始需求 (requirements.md / plan.md / 用户指令) 进行
独立比对。Worker 可能:
- 实现了代码但偏离了需求意图
- 满足了 Done When 字面条件但遗漏了隐含需求
- 验证通过但解决的是错误的问题
工作流:
1. 加载原始需求上下文:
- AirPlan/docs/analysis/requirements.md
- AirPlan/plan.md
- AirPlan/todo.md (含当前任务的 Task/Files/Done When/Validation)
- 用户的原始指令 (如果有记录)
2. 加载交付物:
- Worker result.json (summary, filesChanged, validations)
- 变更文件的 diff (git diff 或文件内容)
- 验证证据 (测试输出、截图、静态分析报告)
3. 多维度审查:
- 需求覆盖度: 原始需求中的每个要求点是否被交付物覆盖
- 意图一致性: 交付物是否解决了需求想解决的真正问题
- 边界完整性: 是否遗漏了需求的隐含边界条件
- 回归风险: 变更是否破坏了需求的已有功能
- 代码质量: 复杂度、可读性、重复率、异常处理完备性
- 生命周期健壮性: 资源释放、连接管理、超时/重试/降级策略
- 运行时稳定性: 内存泄漏风险、竞态条件、崩溃路径、错误传播
- 用户影响评估: 变更是否引入用户可感知的崩溃或卡顿风险
- **Code-to-Design 一致性(强制)**: 代码实现是否严格遵循设计文档,逐行对照
4. 产出审查报告:
AirPlan/state/airrvr/reviews/{task_id}-{ts}.json
AirPlan/docs/reviews/{task_id}-review.md
报告结构:
{
"taskId": "T-001",
"verdict": "pass | conditional-pass | fail",
"coverage": [
{"requirement": "...", "status": "covered | partial | missing",
"evidence": "具体文件或代码位置"}
],
"intentAlignment": "aligned | divergent",
"divergenceNotes": "...",
"regressionRisk": "none | low | medium | high",
"regressionDetails": "...",
"codeQuality": {
"complexity": "low | medium | high",
"readability": "good | acceptable | poor",
"duplication": "none | low | high",
"errorHandling": "complete | partial | missing"
},
"lifecycleHealth": {
"resourceLeak": "none | suspected | confirmed",
"connectionManagement": "proper | improper | missing",
"timeoutStrategy": "present | absent",
"retryStrategy": "present | absent"
},
"runtimeStability": {
"crashRisk": "none | low | medium | high",
"raceCondition": "none | suspected | confirmed",
"memoryLeak": "none | suspected | confirmed",
"userImpact": "none | minor | major"
},
"codeToDesignTable": [
{"designRef": "ADR-0023: JWT认证", "codeLocation": "auth/jwt.py:L45-78", "status": "aligned | divergent | missing", "notes": "实现了ADR规定的RS256算法"},
{"designRef": "C4: 模块边界", "codeLocation": "services/auth_service.py:L12", "status": "aligned", "notes": "依赖方向符合架构约束"},
{"designRef": "需求: 登录失败锁定", "codeLocation": "auth/login.py:L89-102", "status": "divergent", "notes": "实现了5次失败锁定需求要求3次"}
],
"recommendations": ["..."]
}
5. 与 AirEng 集成:
- AirEng merge 前可选调用 AirRvr 审查
- verdict=fail 时阻止合并,要求 Worker 修订
- verdict=conditional-pass 时允许合并但记录遗留项
- verdict=pass 时正常合并
审查模式:
- 逐任务审查: 单个任务完成后立即审查
- 波次审查: 一个波次所有任务完成后批量审查
- 里程碑审查: 项目阶段结束时全量审查 (比对 requirements.md 全文)
**Code-to-Design 逐行对照审查**(每次审查必含):
对每个任务的交付代码,与对应的设计文档进行逐行级别的对照:
- 实现 ↔ 需求: 代码是否覆盖了需求中的每个功能点
- 实现 ↔ ADR: 代码是否遵循了架构决策记录中的约束
- 实现 ↔ C4: 代码是否符合模块边界和职责划分
- 实现 ↔ Done When: 代码是否满足了完成条件的字面要求
- 偏离标记: 发现实现偏离设计时,标记具体行号和偏离类型
报告结构扩展:
{
"codeToDesign": [
{"designItem": "ADR-0003: 使用 JWT 认证",
"implementationStatus": "aligned | divergent | missing",
"codeLocation": "src/auth/token.py:45-78",
"designLocation": "docs/architecture/adr/ADR-0003-jwt.md",
"divergenceDetail": "实现使用了 HS256 而非 ADR 规定的 RS256"}
]
}
制品:
AirPlan/state/airrvr/
reviews/{task_id}-{ts}.json # 结构化审查报告
review-summary.md # 累积审查摘要
AirPlan/docs/reviews/
{task_id}-review.md # 人类可读审查报告
```
#### 3.7.4 事件索引层
```python
# air_runtime/events.py
class EventLog:
"""结构化事件日志 (JSONL)"""
def __init__(self, path: Path):
self._path = path
def emit(self, event_type: str, payload: dict) -> None:
entry = {
"ts": now_iso(),
"type": event_type,
**payload,
}
with open(self._path, "a") as f:
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
# 事件类型:
# task.dispatched — 任务派发
# task.completed — 任务完成
# task.blocked — 任务阻塞
# merge.started — 合并开始
# merge.completed — 合并完成
# repair.created — 修复创建
# repair.resolved — 修复解决
# intervention.stall — 停滞干预
# xdb.captured — 截图采集
# debug.session — 调试会话
# context.compacted — 上下文压缩
# deploy.completed — 部署完成
```
事件日志与 `state.json` 互补:`state.json` 是当前快照,事件日志是完整时间线。
### 3.8 `air_runtime` 模块重组
```
air_runtime/
__init__.py
contracts.py # 数据契约(保持,增加 DeploymentRecord
io.py # NEW: 原子 I/O (替代 5 份 _json_dump)
lock.py # NEW: 文件锁
utils.py # NEW: 公共工具 (替代重复代码)
events.py # NEW: 事件日志
task_graph.py # NEW: 动态任务依赖图 (替代静态 todo 表格)
adr_watcher.py # NEW: ADR 文件变更监控 + 自动触发级联失效
worktree.py # NEW: git worktree 隔离并行
paths.py # 路径约定(保持)
todo_parser.py # TODO 解析(保持,修复列索引硬编码)
review.py # 并行审查(保持,优化算法)
engine.py # 调度引擎(重构:事务化合并、自适应轮询、资源检测)
worker.py # Worker 生命周期(保持,增加 task_id 校验)
doc_sync.py # 文档同步(保持,增加去重)
project_bootstrap.py # 项目引导(保持)
evidence_gate.py # NEW: 任务类型感知证据门控 (替代无差别 AirXDB 触发)
deploy_runtime.py # NEW: AirDep 运行时
test_runtime.py # NEW: AirTst 运行时
airxdb_runtime.py # 扩展: kmsgrab, xvfb, 截图 diff
debug_runtime.py # 扩展: 步骤追踪, 回滚
repair_runtime.py # 扩展: 修复模式学习
review_runtime.py # NEW: AirRvr 需求审查运行时
```
---
## 4. 分阶段实施计划
### Phase 1 — 可靠性基础 (P0 修复)
**目标**:消除已造成实际损失的缺陷
| 任务 | 内容 | 对应缺陷 | 预估工作量 |
|------|------|---------|-----------|
| T-1.1 | 实现 `air_runtime.io` (原子写入 + 安全加载) | P0-3 | 0.5d |
| T-1.2 | 实现 `air_runtime.lock` (文件锁) | P0-4 | 0.5d |
| T-1.3 | 实现 `air_runtime.utils` (消除重复) | P1-2~P1-7 | 1d |
| T-1.4 | 实现 `EvidenceGatePolicy` (任务类型感知) | P0-1 | 0.5d |
| T-1.5 | 实现部署验证强制 | P0-2 | 0.5d |
| T-1.6 | 消除硬编码路径 | P1-1 | 0.5d |
| T-1.7 | 子进程超时 | P1-10 | 0.5d |
| T-1.8 | task_id / marker 注入防护 | P1-11, P1-12 | 0.5d |
| T-1.9 | 异常处理改进 (不静默吞) | P1-13 | 0.5d |
| T-1.10 | AirArc plan 模式阻断 + 工具白名单 | P0-5 | 0.5d |
| T-1.11 | AirEng 中文锁定 + 自主决策指令 | P0-6 | 0.5d |
| T-1.12 | AirEng 强制轮询循环 (5分钟周期) | P0-7 | 0.5d |
| T-1.13 | AirDo 强制 AirDbg 路由 | P0-8 | 0.5d |
| T-1.14 | 安装器路径修正 + 安装后验证 | P0-9 | 1d |
| T-1.15 | AirArc 需求探讨门控(三阶段流程) | P1-16 | 0.5d |
| T-1.16 | AirDbg 先读后写门控(取证前置) | P1-17 | 0.5d |
| T-1.17 | 项目级 spdlog 日志标准AirArc 强制 + AirRvr 检查) | P1-18 | 0.5d |
| T-1.18 | AirEng 调度职责边界(仅调度 + 极端接管例外) | P0-10 | 0.5d |
| T-1.19 | 边界测试强制AirArc 规划) + 终审高风险审计AirRvr 审查) | P1-19 | 1d |
| T-1.20 | frontend-design Skill 集成 + 自动安装检测 | P1-20 | 0.5d |
| T-1.21 | Dispatch → Worker 桥接spawn_workers + 指令操作化 + 工具白名单对齐) | P1-22, P1-23 | 1d |
| T-1.22 | Merge → TaskGraph 状态同步merge 后更新 task-graph.json 节点 status | P1-24 | 0.5d |
**验证标准**所有现有项目DecodePlayer 系列)的 state.json 在 V2 引擎下不损坏AirXDB 假阳性率降至 0。
### Phase 2 — 引擎增强
**目标**:提升调度质量和可观测性
| 任务 | 内容 | 对应缺陷 |
|------|------|---------|
| T-2.1 | 自适应轮询 (`AdaptivePoller`) | P3-3 部分 |
| T-2.2 | Worker 超时与资源压力检测 | P3-3 |
| T-2.3 | 合并事务化 | P0-4 深化 |
| T-2.4 | AGENTS.md 去重与压缩 | P3-1 |
| T-2.5 | 事件日志 (`EventLog`) | 可观测性 |
| T-2.6 | todo.md 列索引从表头推导 | P1-8 |
| T-2.7 | 并发度可配置 | P1-9 |
| T-2.8 | state.json 历史列表上限 | P2-2 |
| T-2.9 | 动态图调度 (TaskGraph + PlanDelta) | P1-14 |
| T-2.10 | 区域级冲突检测 + worktree 隔离并行 | P1-15 |
| T-2.11 | ADR 变更级联失效(溯源链 + 回滚清理 + 调度冻结) | P1-21 |
### Phase 3 — 新插件
**目标**:填补最大的功能空白
| 任务 | 内容 | 对应差距 |
|------|------|---------|
| T-3.1 | AirDep 部署插件 | 部署缺口 |
| T-3.2 | AirTst 测试运行器 | 测试标准化 |
| T-3.3 | AirSDB 多语言后端 | AirSDB 差距 |
| T-3.4 | AirXDB kmsgrab + xvfb | AirXDB 差距 |
| T-3.5 | AirDbg 步骤追踪 + 回滚 | AirDbg 差距 |
| T-3.6 | AirRvr 需求审查器 | 交付物与需求一致性验证缺失 |
| T-3.7 | AirSec 安全扫描 | 制品敏感数据泄露风险 |
### Phase 4 — 规模化
**目标**:支持大规模项目和多项目知识迁移
| 任务 | 内容 | 对应缺陷 |
|------|------|---------|
| T-4.1 | 冲突检测算法优化 (O(n²) → O(n log n)) | P2-1 |
| T-4.2 | todo.md 缓存 (避免全量重解析) | P2-3 |
| T-4.3 | AirContext 压缩质量校验 | AirContext 差距 |
| T-4.4 | AirContext Token 估算改进 | AirContext 差距 |
| T-4.5 | AirContext 陈旧锁检测 | AirContext 差距 |
| T-4.6 | AirArc 增量重规划 | AirArc 差距 |
| T-4.7 | 跨项目运维模式库 | P3-5 |
### Phase 5 — 测试覆盖
**贯穿所有阶段**,每新增/重构模块必须附带测试:
| 模块 | 测试重点 |
|------|---------|
| `todo_parser.py` | Markdown 表格格式变化、缺失列、转义字符 |
| `review.py` | 依赖环检测、写集冲突正确性、大规模任务性能 |
| `doc_sync.py` | 标记块嵌套/缺失/重叠、路径遍历防护 |
| `contracts.py` | from_dict/to_dict 往返、验证边界 |
| `engine.py` | 状态机转换、派发选择、停滞检测 |
| `io.py` | 原子写入、损坏恢复、备份轮转 |
| `lock.py` | 锁超时、陈旧锁清理 |
| `evidence_gate.py` | 任务分类正确率 |
| `task_graph.py` | 增量 delta 应用、依赖环检测、ready 任务计算 |
| `worktree.py` | worktree 创建/合并/清理、冲突升级 |
---
## 5. 迁移策略
### 5.1 向后兼容
V2 必须能读取 V1 的 `state.json``todo.md``result.json`。迁移方式:
```python
def migrate_state_v1_to_v2(state: dict) -> dict:
"""V1 → V2 状态迁移"""
v2 = {**state}
# 新增字段使用默认值
v2.setdefault("evidenceGatePolicy", {"guiIndicators": [], "networkIndicators": []})
v2.setdefault("deployVerificationRequired", False)
v2.setdefault("adaptivePolling", True)
v2.setdefault("workerMaxWallTimeSeconds", 7200)
# 历史列表截断
for key in ("mergedResults", "xdbSessions", "debugSessions", "repairAttempts"):
if key in v2 and len(v2[key]) > 100:
v2[key] = v2[key][-100:]
return v2
```
### 5.2 渐进式迁移
不需要一次性迁移所有项目。V2 引擎可以混合运行 V1 插件:
- V2 引擎 + V1 WorkerWorker 不感知证据门控变化,引擎侧过滤
- V2 AirContext + V1 其他AirContext 独立运行
- V1 引擎 + V2 AirDepAirDep 作为独立插件,不依赖引擎版本
### 5.3 回滚方案
V2 的 `state.json` 保持 V1 的 JSON 结构,新增字段使用 `setdefault` 填充。回滚到 V1 引擎时V1 忽略不认识的新字段。
---
## 6. 关键设计决策
### 6.1 为什么选择文件锁而非数据库
V1 的核心优势是**所有状态都是人类可读文件**。引入 SQLite 会破坏这一属性——`state.json` 可以直接 `cat` 查看SQLite 不行。文件锁在保持可检查性的同时提供足够的并发安全。
如果项目规模超过 500+ 任务或 20+ 并行 Worker再考虑引入嵌入式数据库。
### 6.2 为什么证据门控用关键词匹配而非 LLM 分类
关键词匹配的优势:
- **确定性**:相同输入总是产生相同输出,可测试
- **零成本**:不需要额外 API 调用
- **可解释**:用户可以理解为什么某个任务被分类为 GUI 任务
- **可覆盖**:用户可以在 todo.md 中用 `[no-xdb]` 标记显式跳过
LLM 分类的优势是更准确,但引入了不确定性和额外成本。在 V2 初期使用关键词匹配,积累足够标注数据后可考虑 LLM 分类作为增强。
### 6.3 为什么新插件不合并到 air_runtime
AirDep/AirTst/AirSec 作为独立插件而非 `air_runtime` 模块:
- 独立版本和发布节奏
- 用户可按需安装
- 保持 `air_runtime` 作为核心库的精简性
- 遵循 V1 的插件边界不变量
---
## 7. 度量指标
V2 应追踪以下 KPI 以验证改进效果:
| 指标 | V1 基线 | V2 目标 |
|------|---------|---------|
| AirXDB 假阳性率 | ~60%11/18 任务) | < 5% |
| 状态文件损坏率 | 未量化(已知发生) | 0% |
| 部署一致性事故 | 1 次关键事故 | 0 次 |
| 空壳修复循环 | 11+ 次 | 0 次 |
| 代码重复度 | `_json_dump` 5 份等 | 每函数 1 份 |
| 测试覆盖率 | 0% | 核心模块 > 80% |
| AGENTS.md 大小 | 持续膨胀 | 自动去重/压缩 |
| 平均任务合并耗时 | 未量化 | 量化基线 + 优化 |
| AirArc plan 模式劫持 | 频繁发生 | 0 次 |
| AirArc 跳过需求探讨直接生成规划 | 每次启动 | 0 次(三阶段门控强制) |
| AirEng 非中文输出 | 频繁发生 | 0 次 |
| AirEng 轮询遗忘 | 频繁发生 | 0 次 |
| AirEng 偏离调度亲自写代码 | 频繁发生 | 0 次(仅极端接管例外,需日志记录) |
| AirDo 跳过 AirDbg | 频繁发生 | 0 次 |
| AirDbg 未取证就修改代码 | 频繁发生 | 0 次(先读后写门控强制) |
| 项目无标准化日志 | 所有项目 | spdlog 覆盖率 100%AirArc 强制 + AirRvr 检查) |
| 边界无测试覆盖 | 所有项目 | 模块边界接口测试覆盖率 100% |
| 终审未检查高风险问题 | 无专项审计 | 高风险审计通过率 100%deliveryVerdict != block-release |
| UI 任务无专业 Skill | 所有 UI 任务 | frontend-design Skill 覆盖率 100%(自动安装 + 强制路由) |
| 需求偏离未检出 | 无审查机制 | AirRvr 覆盖率 > 80% |
| 实现偏离设计未检出 | 无对照机制 | Code-to-Design 对照覆盖率 100% |
| 安装后插件不可用 | 用户普遍反馈 | 0 次(安装后自动验证通过) |
| 需求变更后调度恢复时间 | 多轮 AI 迭代 | < 1 次(增量吸收 delta |
| 同文件无冲突任务串行率 | 100% 串行 | < 20%worktree 并行) |
| ADR 变更后旧代码残留 | 无自动清理 | 0 次(级联失效 + git revert 自动清理) |
| Dispatch → Worker 断链 | Agent 停止调度,回退自己写代码 | 0 次spawn_workers 标准化 + 指令操作化消除歧义) |
| Merge 后重复派发 | 已完成任务再次被 dispatch | 0 次task-graph.json 节点 status 实时同步) |
| Eng 编码越界(非接管场景) | 调度器在正常调度中越界写任务代码 | 0 次(指令伪代码消除 spawn 歧义Agent 不再因「Worker 不启动」而回退自己执行) |