- YAML frontmatter 新增 denied-tools: [Write, Edit, Bash, NotebookEdit, Task, Skill, Agent] - 新增 Hard Rule 4: Pipeline handoff — 规划产出后立即停止,禁止调用执行类工具 - 新增"流水线交接强制"章节:生成execution-plan.json后工作立即结束,调度由AirEng接管 - 设计文档同步:P0-11+T-1.24 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
92 KiB
Executable File
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 未明确区分"仅调度"与"极端接管"的边界,无工具限制约束 |
| P0-11 | AirArc 跳过 AirEng 直接执行 | AirArc SKILL.md / 命令文件 | 架构器生成规划后不交给 Eng 调度,直接跳到 Do 执行任务,破坏 Arc→Eng→Do 流水线 | SKILL.md 未强制"规划产出后必须交给 Eng",Arc 的 allowed_tools 未限制执行类工具 |
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 迭代才能恢复调度 |
| P1-15 | 同文件无冲突任务被迫串行 | review.py 写集冲突检测 |
同文件不同区域(如 Qt 样式 vs 状态机)被判为冲突,被迫串行执行 |
| P1-16 | AirArc 跳过需求探讨直接生成规划 | AirArc SKILL.md / 命令文件 | 用户刚说一两句就自顾自生成计划并要求执行,未与用户充分探讨需求和分析架构 |
| P1-17 | AirDbg 未取证就盲改代码 | AirDbg SKILL.md / debug_runtime.py |
调试器不进行任何取证(抓包/截图/代码分析)就猜测原因并修改代码,引入新问题且污染代码库 |
| P1-18 | 项目缺乏标准化日志体系 | 项目引导 / AirArc 规划 | 生成的代码无统一日志输出,debug/release 无法切换,问题排查困难 |
| P1-19 | 边界无测试 + 终审缺高风险检查 | AirDo / AirRvr | 代码边界无接口测试和单元测试,最终审查未着重检查生命周期、空指针、悬垂指针、异常风险,产品交付后短时间内崩溃 |
| P1-20 | 界面设计缺乏专业 Skill 支撑 | AirDo / 安装器 | UI/前端任务由通用 Agent 直接编写,界面质量差,布局、配色、交互不符合设计规范 |
| P1-21 | ADR 变更无级联失效机制 | AirArc / AirEng / TaskGraph | 架构方案变更(如 ffmpeg → gstreamer)后,基于旧 ADR 已完成的任务不会自动失效,旧代码残留与新方案冲突,下游任务基于过期产出继续执行 |
| P1-22 | Dispatch → Worker 启动无桥接 | eng_mode.py:dispatch_worker_group() |
dispatch 只写 JSON 派发清单,不启动 Worker。Worker 启动依赖 Agent 自觉读 payload 并手动调用 Skill 工具——Agent 不读则 Worker 永不启动,Agent 最终「回退自己执行」 |
| P1-23 | Dispatch 指令歧义 | commands/eng.md |
Eng 的 dispatch 步骤(spawn Worker)是意图描述而非可执行伪代码,Agent 每步都在猜:用什么工具?参数格式?task-text 从哪取?——猜错多一轮,猜不出来 Worker 不启动 |
| P1-24 | AirArc 任务描述歧义导致弱模型破坏性执行 | AirArc review.py / SKILL.md |
任务粒度太粗、用词有歧义(如"清理"被弱模型理解为"删除全部"),Worker 严格按字面执行导致误删现有代码。真实案例:screenPlayer CMake 重构中 Worker 删除了整个 src/ |
| P1-25 | Merge 后 TaskGraph 状态不同步 | eng_mode.py:merge_worker_result() |
merge 更新 todo.md 和 state.json 但不动 task-graph.json。已完成任务的节点状态仍是 TODO/DISPATCHED,再次 dispatch 重复派发 |
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 |
| AirArc 跳过 AirEng 直接执行 | 频繁 | P0-11 |
| 项目代码缺乏标准化日志体系 | 所有项目 | P1-18 |
| 边界无测试 + 终审缺高风险检查 | 所有项目 | P1-19 |
| 界面设计缺乏专业 Skill 支撑 | UI 任务 | P1-20 |
| ADR 变更后已完成任务不失效 | 架构变更时 | P1-21 |
| AirArc 任务描述歧义导致弱模型破坏性执行 | 已造成实际损失 | P1-24 |
2. V2 设计目标
2.1 核心目标
- 可靠性:状态写入不丢失,并发操作不竞态,崩溃后可自愈
- 可观测性:所有引擎操作可追溯,指标可导出,异常主动通知
- 智能化:证据门控感知任务类型,轮询频率自适应,修复模式可学习
- 规模化:支持 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 重复,统一为原子写入:
# 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 — 文件级并发控制
# 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 — 消除代码重复
# 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 任务类型感知的证据门控
# 替代 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 部署验证强制
# 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 自适应轮询
# 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 超时与级联保护
# 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 引入事务语义:
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)
# Phase 7: git 强制操作(不可跳过)
_git_squash_merge_and_tag(project_root, result)
Phase 7: git 强制操作:
def _git_squash_merge_and_tag(project_root: Path, result: WorkerResult) -> None:
"""合并后强制执行 git squash merge + tag,失败则回滚整个合并"""
task_id = result.taskId
branch = f"air-{task_id}"
# 1. 确认 worktree 分支存在且有 commit
wt_path = project_root / ".git" / "worktrees" / f"air-{task_id}"
if not _branch_has_commits(branch):
raise MergeError(
f"task {task_id}: worktree branch has no commits, "
f"worker must commit changes before merge"
)
# 2. squash merge 回主线
subprocess.run(["git", "merge", "--squash", branch],
cwd=project_root, check=True, capture_output=True)
subprocess.run(["git", "commit", "-m", f"{task_id}: {result.summary}"],
cwd=project_root, check=True, capture_output=True)
# 3. 打 tag(用于后续 revert 追溯)
tag = f"air/done/{task_id}"
subprocess.run(["git", "tag", tag],
cwd=project_root, check=True, capture_output=True)
# 4. 清理 worktree
subprocess.run(["git", "worktree", "remove", str(wt_path), "--force"],
cwd=project_root, capture_output=True) # 清理失败不阻塞
# 5. 验证 tag 存在
check = subprocess.run(["git", "tag", "-l", tag],
cwd=project_root, capture_output=True, text=True)
if tag not in check.stdout:
raise MergeError(f"git tag {tag} creation failed, merge is unreliable")
3.2.6 AirArc plan 模式阻断
V1 中 AirArc 频繁被 Agent 内置 plan mode 劫持,导致架构器偏离规划职责去执行代码修改。V2 在 SKILL.md 和命令文件中增加硬性阻断:
# 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:
{
"allowed_tools": ["Read", "Glob", "Grep"],
"denied_tools": ["Write", "Edit", "Bash", "NotebookEdit", "Task", "Skill", "Agent"],
"deny_plan_mode": true
}
流水线交接强制(P0-11):
Arc 生成 execution-plan.json 后,必须交给 Eng 调度,禁止直接调用 AirDo 或自行执行任务。
# AirArc SKILL.md V2 流水线交接指令
## 规划产出后的唯一动作
- 生成 execution-plan.json 后,你的工作**立即结束**
- **禁止**调用 AirDo、Skill、Agent 或任何执行类工具
- **禁止**自行派发 Worker 或执行任务
- 调度由 AirEng 接管,你不参与执行
## 违规判定
如果你在生成 execution-plan.json 后执行了任何非读取类操作,视为违规。
3.2.7 AirEng 自主决策与中文锁定
V1 中 AirEng 反复用英文询问用户确认,中断调度流程。V2 在 SKILL.md 中强制自主决策和中文输出:
# AirEng SKILL.md V2 关键指令
## 语言锁定
你**必须始终使用中文**与用户交流。所有状态报告、进度通知、问题描述均使用中文。
## 自主决策原则
你以**推进开发进度**为第一目标。遇到以下情况时自行决策,不要停下来问用户:
- Worker 返回 blocked 但修复预算未耗尽 → 自行派发修复
- Worker 停滞 → 自行执行停滞干预(重派发/标记 blocked)
- 验证失败但非关键 → 记录问题并继续下一任务
- 波次间衔接 → 自行启动下一波次
## 仅在以下情况才询问用户:
- 修复预算耗尽且任务仍 blocked
- 发现需求歧义无法继续
- 系统资源耗尽无法派发新 Worker
- 用户显式暂停了调度
3.2.8 AirEng 硬编码轮询循环
V1 中轮询逻辑依赖 Agent "自觉"执行,实际经常遗忘导致无限等待。V2 将轮询指令硬编码到 SKILL.md 的强制循环中:
# AirEng SKILL.md V2 轮询指令
## 强制轮询循环(不可跳过)
进入调度状态后,你必须执行以下循环,**每 5 分钟**检查一次所有活跃 Worker:
while 存在活跃 Worker:
- 遍历所有 dispatched Worker
- 检查每个 Worker 的 state 文件 mtime
- mtime > 5分钟未更新 → 标记 stalled → 执行停滞干预
- state = done → 执行合并流程
- state = blocked → 检查修复预算 → 派发修复或升级
- 所有 Worker 处理完毕 → 派发下一波次
- 等待 5 分钟后重复
**绝对不允许**:
- 派发 Worker 后不做轮询就等待
- 仅检查一次就声称"等待 Worker 完成"
- 轮询间隔超过 10 分钟
技术补充:引擎 monitor_engine() 增加 wall-clock 超时检测,Worker 超过 WORKER_MAX_WALL_TIME 自动 terminate。
3.2.8b AirEng 调度职责边界
V1 中 AirEng 频繁偏离调度职责自己编写代码,破坏上下文隔离架构。V2 明确区分"仅调度"与"极端接管"的边界:
# 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"
## 极端接管时的专家插件调用(强制)
即使进入极端接管,你也必须像 AirDo 一样调用相关专家插件:
- 修改代码前:**必须**调用 AirDbg 定位根因
- GUI 相关变更:**必须**调用 AirXDB 采集修改前后截图
- 网络相关变更:**必须**调用 AirNDB 采集抓包证据
- C/C++ 代码变更:**必须**调用 AirSDB 执行静态分析
- 修改完成后:**必须**调用 AirRvr 进行需求一致性审查
- **禁止**跳过专家插件直接修改代码
## 违规判定
如果你在以下场景编写代码,视为违规:
- 正常调度流程中(Worker 可用且未阻塞)
- 修复预算未耗尽时
- 改动超过 5 行
- 未调用相关专家插件就猜测修复
3.2.9 AirDo 强制专家插件路由
V1 中 AirDo 不调用任何专家插件(Dbg/XDB/NDB/SDB/Rvr),遇到问题直接报 blocked 或 false-done。V2 将所有专家插件调用从"建议"升级为"强制":
# worker.py finish_worker() V2 改进 — 全专家插件强制路由
def finish_worker(result: WorkerResult, brief: dict) -> list[RoutingDecision]:
decisions = []
# 1. GUI 任务 → 强制 AirXDB 截图
if brief.get("taskType") == "gui" or _has_gui_indicators(brief):
if not result.xdb_sessions:
decisions.append(RoutingDecision(
target="airxdb", forced=True,
reason="GUI task requires screenshot evidence"
))
# 2. 网络任务 → 强制 AirNDB 抓包
if brief.get("taskType") == "network" or _has_network_indicators(brief):
if not result.ndb_sessions:
decisions.append(RoutingDecision(
target="airndb", forced=True,
reason="network task requires packet capture evidence"
))
# 3. C/C++ 任务 → 强制 AirSDB 静态分析
if _has_cpp_files(result.filesChanged):
if not result.sdb_reports:
decisions.append(RoutingDecision(
target="airsdb", forced=True,
reason="C/C++ task requires static analysis"
))
# 4. blocked/failed → 强制 AirDbg 调试
if result.status in ("blocked", "failed"):
decisions.append(RoutingDecision(
target="airdbg", forced=True,
reason=f"status={result.status} — AirDbg mandatory before return"
))
# 5. done 无实质验证 → 强制 AirDbg 审查
if result.status == "done" and not result.validations and not result.filesChanged:
decisions.append(RoutingDecision(
target="airdbg", forced=True,
reason="done without evidence — mandatory debug review"
))
# 6. 所有 done 任务 → 强制 AirRvr 审查
if result.status == "done":
decisions.append(RoutingDecision(
target="airrvr", forced=True,
reason="completed task requires requirements review"
))
# 无强制路由时才允许合并
if not decisions:
return [RoutingDecision(target="merge")]
return decisions
SKILL.md 配合强制指令:
## 专家插件调用强制规则(不可跳过)
### AirDbg(调试)
- blocked 或 failed 时,**必须**先调用 AirDbg
- done 但无验证证据时,**必须**调用 AirDbg
### AirXDB(GUI 截图)
- GUI 任务完成时,**必须**调用 AirXDB 采集截图证据
- **禁止**在没有截图的情况下声称 GUI 任务完成
### AirNDB(网络抓包)
- 网络任务完成时,**必须**调用 AirNDB 采集抓包证据
- **禁止**在没有抓包的情况下声称网络任务完成
### AirSDB(静态分析)
- C/C++ 任务完成时,**必须**调用 AirSDB 执行静态分析
- **禁止**跳过 cppcheck 直接报完成
### AirRvr(需求审查)
- 所有 done 任务,**必须**调用 AirRvr 进行需求一致性审查
- **禁止**跳过审查直接合并
3.2.9a Worker git 操作强制
整个调度的中途变更处理(3.2.17)和合并追溯(3.2.5 Phase 7)依赖 git 操作链完整。Worker 必须在 worktree 分支上正确执行 git 操作,否则后续流程无法运行。
# AirDo SKILL.md V2 git 操作强制规则
## 任务开始时
- 确认当前在 worktree 分支 air-{task_id} 上工作
- 如果不在 worktree 分支上,禁止开始编码
## 任务执行中
- 每完成一个有意义的变更点,**必须** git commit
- commit message 必须包含 task_id: "T-001: 实现了 xxx"
- **禁止**将所有变更积攒到最后一次性提交
## 任务完成时(finish_worker 前)
- **必须**确认所有变更已 commit(git status 无未提交文件)
- 如有未提交文件,自动 git add + commit
- **禁止**在未 commit 的情况下报 done
## 违规检测
合并流水线 Phase 7 会检测 worktree 分支是否有 commit:
- 无 commit → MergeError,合并失败,Worker 被要求重新提交
- 这确保了即使 Worker 跳过 commit,系统也能在合并时拦截
# worker.py finish_worker() 新增 git 检查
def _ensure_all_committed(wt_path: Path, task_id: str) -> None:
"""任务完成前强制所有变更已 commit"""
status = subprocess.run(
["git", "status", "--porcelain"],
cwd=wt_path, capture_output=True, text=True
)
if status.stdout.strip():
logging.warning("uncommitted changes detected, auto-committing")
subprocess.run(["git", "add", "-A"], cwd=wt_path, check=True)
subprocess.run(
["git", "commit", "-m", f"{task_id}: auto-commit remaining changes"],
cwd=wt_path, check=True
)
3.2.9b 禁止降级方案与回退实现
V1 中 AirEng 和 AirDo 频繁使用"兜底方案"、"先这样做"、"以后再补"等理由绕过设计方案,导致技术债累积和实现偏离设计。V2 在 SKILL.md 中硬性禁止所有降级语言。
# AirEng / AirDo SKILL.md V2 禁止降级指令
## 禁止使用的语言模式(触发即违规)
以下表述及其变体在任何场景下均被禁止:
- "兜底方案" / "fallback"
- "先这样做" / "先这样跑通" / "先这样实现"
- "以后再删" / "以后再补" / "以后再改" / "以后再优化"
- "先回退,以后再..."
- "临时方案" / "temporary workaround"
- "hack 一下" / "quick fix"
- "MVP 先上" / "先 ship 再迭代"
## 强制要求
- 必须完全遵循 AirArc 产出的设计方案实现
- 如果设计方案无法执行(如依赖缺失、环境不支持),必须报 blocked 并说明原因
- 不允许自行发明替代方案绕过设计约束
- 不允许以"能跑就行"为标准降低实现质量
## 违规处置
AirRvr 在 code-to-design 审查中检测到降级语言痕迹时:
- verdict 直接判定为 fail
- 要求 Worker 按原始设计重新实现
- 记录到 review report 中作为模式学习样本
3.2.10 安装器路径修正
V1 安装器存在两个问题:(1) 安装后插件命令不被识别;(2) AI 修复注册后脚本执行路径不正确。
根因:安装器使用相对路径或硬编码路径注册插件命令,未根据实际安装位置动态生成绝对路径。
V2 修正:
# 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):
# 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 段必须给出可执行的伪代码而非意图描述:
### 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 的折中方案:
- 保持
allowed-tools: [Read, Glob, Grep, Bash, Write, Edit] - 在 SKILL.md 中将 INV-6 升级为硬编码检查:每次 Write/Edit 调用前校验目标路径是否在
AirPlan/目录下,不在则拒绝并提示「由 /do Worker 执行」 commands/eng.md中增加明确的操作步骤伪代码(如上述),消除 Agent 的推理歧义
(c) Worker spawn 函数:新增 air_runtime/modes/eng_mode.py:spawn_workers(project_root, task_ids) —— 不替代 Agent 决策层,但提供标准化数据准备:
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 状态同步:
# 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 隔离:
# 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 强制"先探讨、后确认、再规划"的三阶段流程:
# AirArc SKILL.md V2 流程指令
## 三阶段流程(不可跳过)
### 阶段一:需求探讨
- 与用户反复讨论需求细节、边界条件、隐含约束
- 主动提问澄清模糊点,不要假设用户意图
- 分析现有代码库的结构、技术栈、约束条件
- 提出多种架构方案及其优劣势对比
- **此阶段禁止生成 execution-plan.json**
### 阶段二:架构确认
- 向用户呈现推荐的架构方案(模块划分、依赖关系、技术选型)
- 明确等待用户确认:"请确认此架构方案是否符合预期,确认后我将生成执行规划"
- 用户有异议时回到阶段一修订
- **此阶段禁止生成 execution-plan.json**
### 阶段三:生成规划
- 仅在用户明确确认架构无误后,才生成 execution-plan.json
- 规划产出必须严格对应用户确认的架构方案
技术门控:AirArc 运行时增加 phase 状态追踪,execution-plan.json 仅在 phase == "confirmed" 时允许写入:
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"
Arc 规划前 git 初始化检测:
AirArc 在生成执行计划前,必须确认项目已初始化 git。因为后续的 worktree 隔离、中途变更回滚、任务追溯全部依赖 git。
# air_runtime/project_bootstrap.py 扩展
def ensure_git_initialized(project_root: Path) -> None:
"""检测并初始化 git,确保后续调度的 git 依赖可用"""
git_dir = project_root / ".git"
if git_dir.exists():
return
logging.info("git not initialized, initializing...")
subprocess.run(["git", "init"], cwd=project_root, check=True, capture_output=True)
subprocess.run(["git", "add", "."], cwd=project_root, check=True, capture_output=True)
subprocess.run(
["git", "commit", "-m", "AirArc: initial project state before planning"],
cwd=project_root, check=True, capture_output=True,
)
# 创建基线 tag,后续 revert 以此为锚点
subprocess.run(
["git", "tag", "air/baseline"],
cwd=project_root, check=True, capture_output=True,
)
# AirArc SKILL.md V2 git 前置检查
## 规划前置条件(不可跳过)
- 开始规划前,**必须**检查项目是否已初始化 git
- 如果未初始化,自动执行 git init + 初始 commit + air/baseline tag
- **禁止**在无 git 环境下生成执行计划(后续 worktree 隔离和中途变更回滚依赖 git)
3.2.14 项目级 spdlog 日志标准
V1 生成的代码缺乏统一日志体系,debug/release 无法切换,问题排查困难。V2 强制所有项目集成 spdlog 日志库,支持 debug/release 级别切换。
AirArc 规划阶段强制要求:
# AirArc SKILL.md V2 日志标准指令
## 项目日志标准(所有 C++ 项目强制)
- 必须使用 spdlog 作为日志库
- 如果项目未集成 spdlog,首个任务必须为"集成 spdlog 到项目"
- 所有生成的代码必须使用 spdlog 输出日志,禁止 std::cout/qDebug 等
- 必须支持 debug/release 编译开关切换日志级别
- 关键路径(初始化、网络、文件 I/O、错误)必须有日志输出
AirRvr 审查阶段检查项:
# AirRvr 审查清单扩展
LOGGING_CHECKS = [
"spdlog 是否已集成到项目依赖",
"代码中是否存在 std::cout / qDebug / printf 等非标准日志",
"是否有 CMAKE_BUILD_TYPE 或等效的 debug/release 编译开关",
"关键路径(初始化、网络、文件 I/O、错误处理)是否有日志输出",
"日志格式是否统一(时间戳 + 级别 + 模块 + 消息)",
]
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 规划阶段——测试任务强制:
# 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 指令:
## 界面设计任务处理
- 当任务涉及 UI/前端/界面设计时,必须使用 frontend-design Skill
- 如果 frontend-design Skill 不存在,先执行自动安装再继续
- 禁止在无 frontend-design Skill 的情况下直接编写 UI 代码
自动安装检测:
# 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 中途变更处理(git 驱动)
V1/V2 初版中,当架构方案变更(如 ffmpeg → gstreamer)时,基于旧 ADR 已完成的任务不会自动失效。V2 利用 git 作为状态管理基础设施,简化中途变更处理。
核心思路:每个任务在独立 git worktree 分支上工作,合并时 squash merge 回主线并打 tag。变更发生时,用 git 操作处理各状态任务,无需复杂的状态矩阵。
任务与 git 的映射:
- 每个 Worker 在 .git/worktrees/air-{task_id} 分支上工作
- 合并时: git merge --squash → commit -m "T-001: ..." → git tag air/done/T-001
- 每个 commit 关联 task_id,可追溯
变更处理流程(三阶段):
Phase 1: 变更分类 + 影响定位
1. ADRWatcher 检测到 ADR 变更
2. ChangeClassifier 分类爆炸半径 (IMPLEMENTATION → GLOBAL_CONSTRAINT)
3. ImpactPropagator BFS 标记受影响任务 (IMPACTED / BOUNDARY / SAFE)
4. 仅对 IMPACTED 和 BOUNDARY 任务执行后续操作,SAFE 任务不受影响
Phase 2: git 操作处理各状态任务
冻结调度(dispatch_frozen = true),然后:
已合并的 (IMPACTED):
→ git revert air/done/{task_id}(撤销对应 commit)
→ 标记 INVALIDATED
已合并的 (BOUNDARY):
→ 生成验证任务,确认兼容性后再决定是否 revert
执行中的 (IMPACTED):
→ 终止 Worker
→ git worktree remove .git/worktrees/air-{task_id} --force(丢弃分支)
→ 标记 INVALIDATED
执行中的 (BOUNDARY):
→ 等待 Worker 完成后评估
未派发的:
→ 直接标记 INVALIDATED,无需 git 操作
部分完成的 (IMPACTED):
→ 终止 Worker
→ 保留 worktree 分支(可能有可复用的部分产出)
→ 标记 INVALIDATED,重规划时参考已有产出
阻塞的:
→ 标记 INVALIDATED
→ git worktree remove 或保留(视情况)
Phase 3: 局部重规划 + 解冻
1. 提取 SAFE 已完成任务的接口作为冻结约束
2. AirArc 局部重规划(变更描述 + 冻结约束 + 受影响上下文)
3. apply_delta() 吸收新任务
4. 为 BOUNDARY 已完成任务生成验证任务
5. 解冻调度(dispatch_frozen = false)
ADR→任务溯源链与变更检测(保持不变):
# 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]
ADR 变更自动检测:
# 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()。
局部重规划(替代全量重规划):
# 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.2.18 AirArc 任务描述弱模型优化
V1 中 Arc 生成的任务描述粒度太粗、用词有歧义,弱模型严格按字面执行导致破坏性后果。真实案例:
任务描述: "清理旧产品实现并重建新 CMake 骨架"
完成标准: "screenPlayer 目标只链接 Qt5 + RanderWidget/media_streaming;不链接 sipclient"
文件范围: CMakeLists.txt, cmake/, main.cpp, src/, tests/
→ Worker 理解: "清理" = 删除旧代码, "src/" 在文件范围内 → 删除整个 src/ 目录
→ 正确理解: 只重构 CMakeLists.txt 去掉 sipclient 依赖, src/ 现有模块保留不动
V2 从三个层面修复:
1. 操作类型拆分(按动词分类):
# review.py 任务生成改进
AMBIGUOUS_VERBS = {
"清理": "歧义——可能是删除、重构、或移除依赖",
"优化": "歧义——可能是性能优化、代码重构、或简化逻辑",
"整理": "歧义——可能是格式化、重命名、或删除",
"更新": "歧义——可能是修改现有代码、或替换为新实现",
}
SAFE_VERBS = {
"重构": "修改实现但保持外部接口不变",
"新增": "添加新功能,不修改现有代码",
"删除": "移除指定文件或函数(必须列出具体目标)",
"修改": "修改指定文件的具体部分(必须指明改什么)",
"保留": "明确标记为不可修改的文件/目录",
}
def validate_task_description(task: TaskRecord) -> list[str]:
"""检测任务描述中的歧义词并建议替换"""
warnings = []
for verb, explanation in AMBIGUOUS_VERBS.items():
if verb in task.task:
warnings.append(
f"任务描述包含歧义词「{verb}」({explanation}),"
f"请拆分为具体操作(重构/新增/删除/修改/保留)"
)
return warnings
2. 保留约束机制(显式声明不可修改的内容):
# AirArc SKILL.md V2 任务描述指令
## 任务描述规范(面向弱模型优化)
### 每个任务必须包含:
1. **操作指令**: 用具体动词描述要做什么(重构/新增/删除/修改)
2. **保留约束**: 明确列出不可修改的文件、目录或函数
3. **变更边界**: 精确到文件级别,每个文件标注"新建|修改|删除|保留"
4. **完成标准**: 可验证的条件,避免主观判断
### 禁止的写法:
- "清理旧实现" → 改为 "重构 CMakeLists.txt 去掉 sipclient 依赖,保留 src/ 下所有现有模块"
- "优化模块结构" → 改为 "将 auth/login.py 中的 validate() 函数提取到 auth/validator.py"
### 示例(正确写法):
任务: "重构 screenPlayer CMake 构建配置"
完成标准: "screenPlayer 目标只链接 Qt5 + RanderWidget/media_streaming;不链接 sipclient"
文件范围:
- CMakeLists.txt: 修改(去掉 sipclient 相关 find_package 和 target_link_libraries)
- cmake/: 修改(清理 sipclient 相关的 .cmake 文件)
- main.cpp: 保留(不修改)
- src/: 保留(不修改,现有模块保持不动)
- tests/: 保留(不修改)
保留约束: src/ 目录下所有现有源文件不得删除或修改
3. Arc 自检环节:
Arc 生成任务后,对所有任务执行 validate_task_description() 自检。发现歧义词时自动拆分任务或补充保留约束,不将歧义任务传递给 Eng。
3.3 AirContext V2 改进
3.3.1 压缩质量校验
# 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 估算改进
# 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 陈旧锁检测
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:多语言静态分析
# 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。
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 步骤追踪
# 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 修复回滚
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 强制"先读后写"原则:必须至少执行一种取证行为后,才允许修改代码。
# 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 配合指令:
## 先读后写原则(不可违反)
- 修改任何代码之前,必须至少完成一项取证行为
- 取证行为包括但不限于:截图、抓包、静态分析、日志分析、代码追踪、复现步骤
- 不要求取证覆盖完整(部分问题确实无法完全取证),但必须有至少一项取证作为依据
- 取证结果必须记录到 debug-log.md 后方可开始修复
- **禁止**:未做任何调查就直接修改代码
3.6 AirXDB V2:扩展截图能力
3.6.1 DRM/KMS 原生截图
# 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 支持
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 审查(P0-8 全专家插件路由)
- 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"}
]
}
**审查放行标准(不可降级)**:
禁止以下表面原因直接判定 pass:
- "测试 pass" / "测试全绿" / "all tests passed"
- "实现存在" / "函数存在" / "文件已创建"
- "编译通过" / "无报错"
- "能跑通" / "功能可用"
以上仅为必要不充分条件。放行必须同时满足全部三层:
第一层(必选,最高优先级): Code-to-Design 逐行对照
- 逐条对照设计方案与用户需求,对代码实现逐行审查
- codeToDesignTable 中不得有 status=divergent 或 status=missing 的条目
- 任何 divergent/missing 必须修复后才能放行
第二层(必选): 静态分析通过
- AirSDB 报告无 critical/high severity finding
- 生命周期、空指针、悬垂指针、异常安全专项审计通过
第三层(必选): 测试通过
- 单元测试全部通过
- 接口测试全部通过
- 如有 GUI 任务,AirXDB 截图证据与设计稿一致
三层全部通过 → verdict=pass
第一层有 divergent/missing → verdict=fail(即使测试全绿)
第一层通过但第二/三层有问题 → verdict=conditional-pass
制品:
AirPlan/state/airrvr/
reviews/{task_id}-{ts}.json # 结构化审查报告
review-summary.md # 累积审查摘要
AirPlan/docs/reviews/
{task_id}-review.md # 人类可读审查报告
3.7.4 事件索引层
# 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 文件变更监控 + 自动触发级联失效
change_classifier.py # NEW: 变更爆炸半径分类 (Phase 1)
impact_propagator.py # NEW: BFS 影响传播标记 (Phase 2)
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 | AirArc 任务描述弱模型优化(歧义词检测 + 保留约束 + 自检) | P1-24 | 1d |
| T-1.22 | Dispatch → Worker 桥接(spawn_workers + 指令操作化 + 工具白名单对齐) | P1-22, P1-23 | 1d |
| T-1.23 | Merge → TaskGraph 状态同步(merge 后更新 task-graph.json 节点 status) | P1-25 | 0.5d |
| T-1.24 | AirArc 流水线交接强制(denied_tools 扩展 + SKILL.md 交接指令) | P0-11 | 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。迁移方式:
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 Worker:Worker 不感知证据门控变化,引擎侧过滤
- V2 AirContext + V1 其他:AirContext 独立运行
- V1 引擎 + V2 AirDep:AirDep 作为独立插件,不依赖引擎版本
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 跳过 Eng 直接执行 | 频繁发生 | 0 次(denied_tools 硬阻断 + 交接指令) |
| AirArc 跳过需求探讨直接生成规划 | 每次启动 | 0 次(三阶段门控强制) |
| AirEng 非中文输出 | 频繁发生 | 0 次 |
| AirEng 轮询遗忘 | 频繁发生 | 0 次 |
| AirEng 偏离调度亲自写代码 | 频繁发生 | 0 次(仅极端接管例外,需日志记录) |
| AirDo 跳过专家插件(Dbg/XDB/NDB/SDB/Rvr) | 频繁发生 | 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 自动清理) |
| 任务描述歧义导致破坏性执行 | 已发生 1 次 | 0 次(歧义词检测 + 保留约束 + Arc 自检) |
| Dispatch → Worker 断链 | Agent 停止调度,回退自己写代码 | 0 次(spawn_workers 标准化 + 指令操作化消除歧义) |
| Merge 后重复派发 | 已完成任务再次被 dispatch | 0 次(task-graph.json 节点 status 实时同步) |
| Eng 编码越界(非接管场景) | 调度器在正常调度中越界写任务代码 | 0 次(指令伪代码消除 spawn 歧义,Agent 不再因「Worker 不启动」而回退自己执行) |