From 2c4b3340bfa0309932021e174e84910dc64ed7aa Mon Sep 17 00:00:00 2001 From: AirPlan Team Date: Wed, 10 Jun 2026 16:24:26 +0800 Subject: [PATCH] =?UTF-8?q?AirPlan=20V2=20initial=20release=20=E2=80=94=20?= =?UTF-8?q?unified=20scheduler=20with=2012=20sub-modes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merges 8 V1 plugins into 1 unified plugin, adds 4 new components (dep, tst, sec, rvr). 12 sub-modes: arc, eng, do, dbg, xdb, sdb, ndb, ctx, dep, tst, sec, rvr. L1 code-level guarantees: atomic writes, file locks, evidence gating, forced debug routing, 3-phase arc gate, evidence-first debug gate, Chinese language lock, scheduler boundary. Co-Authored-By: Claude Sonnet 4.6 --- .claude-plugin/plugin.json | 67 ++ .gitignore | 7 + airplanV2审查后.md | 209 +++++++ commands/airplan.md | 46 ++ commands/arc.md | 49 ++ commands/eng.md | 87 +++ lib/air_runtime/__init__.py | 20 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 1479 bytes .../__pycache__/contracts.cpython-313.pyc | Bin 0 -> 5825 bytes .../deploy_runtime.cpython-313.pyc | Bin 0 -> 6447 bytes .../__pycache__/events.cpython-313.pyc | Bin 0 -> 5488 bytes .../__pycache__/evidence_gate.cpython-313.pyc | Bin 0 -> 4191 bytes .../__pycache__/installer.cpython-313.pyc | Bin 0 -> 4951 bytes .../__pycache__/io.cpython-313.pyc | Bin 0 -> 3398 bytes .../__pycache__/lock.cpython-313.pyc | Bin 0 -> 2968 bytes .../__pycache__/paths.cpython-313.pyc | Bin 0 -> 3897 bytes .../project_bootstrap.cpython-313.pyc | Bin 0 -> 2159 bytes .../__pycache__/review.cpython-313.pyc | Bin 0 -> 7846 bytes .../review_runtime.cpython-313.pyc | Bin 0 -> 7817 bytes .../__pycache__/sdb_backends.cpython-313.pyc | Bin 0 -> 19579 bytes .../__pycache__/sec_runtime.cpython-313.pyc | Bin 0 -> 7472 bytes .../__pycache__/task_graph.cpython-313.pyc | Bin 0 -> 15077 bytes .../__pycache__/test_runtime.cpython-313.pyc | Bin 0 -> 6311 bytes .../__pycache__/todo_parser.cpython-313.pyc | Bin 0 -> 4692 bytes .../__pycache__/utils.cpython-313.pyc | Bin 0 -> 3574 bytes .../__pycache__/worktree.cpython-313.pyc | Bin 0 -> 8764 bytes .../__pycache__/xdb_capture.cpython-313.pyc | Bin 0 -> 6828 bytes lib/air_runtime/contracts.py | 109 ++++ lib/air_runtime/deploy_runtime.py | 133 ++++ lib/air_runtime/events.py | 102 ++++ lib/air_runtime/evidence_gate.py | 74 +++ lib/air_runtime/installer.py | 121 ++++ lib/air_runtime/io.py | 53 ++ lib/air_runtime/lock.py | 44 ++ lib/air_runtime/modes/__init__.py | 11 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 550 bytes .../__pycache__/arc_mode.cpython-313.pyc | Bin 0 -> 13022 bytes .../__pycache__/ctx_mode.cpython-313.pyc | Bin 0 -> 11416 bytes .../__pycache__/dbg_mode.cpython-313.pyc | Bin 0 -> 9429 bytes .../__pycache__/dep_mode.cpython-313.pyc | Bin 0 -> 2485 bytes .../modes/__pycache__/do_mode.cpython-313.pyc | Bin 0 -> 7462 bytes .../__pycache__/eng_mode.cpython-313.pyc | Bin 0 -> 26837 bytes .../eng_orchestrator.cpython-313.pyc | Bin 0 -> 10536 bytes .../merge_pipeline.cpython-313.pyc | Bin 0 -> 11846 bytes .../__pycache__/ndb_mode.cpython-313.pyc | Bin 0 -> 207 bytes .../__pycache__/rvr_mode.cpython-313.pyc | Bin 0 -> 1904 bytes .../__pycache__/sdb_mode.cpython-313.pyc | Bin 0 -> 3034 bytes .../__pycache__/sec_mode.cpython-313.pyc | Bin 0 -> 4120 bytes .../__pycache__/tst_mode.cpython-313.pyc | Bin 0 -> 2100 bytes .../__pycache__/xdb_mode.cpython-313.pyc | Bin 0 -> 2375 bytes .../xdb_sdb_ndb_modes.cpython-313.pyc | Bin 0 -> 4995 bytes lib/air_runtime/modes/arc_mode.py | 258 ++++++++ lib/air_runtime/modes/ctx_mode.py | 232 +++++++ lib/air_runtime/modes/dbg_mode.py | 279 +++++++++ lib/air_runtime/modes/dep_mode.py | 39 ++ lib/air_runtime/modes/do_mode.py | 165 +++++ lib/air_runtime/modes/eng_mode.py | 577 ++++++++++++++++++ lib/air_runtime/modes/eng_orchestrator.py | 247 ++++++++ lib/air_runtime/modes/merge_pipeline.py | 238 ++++++++ lib/air_runtime/modes/ndb_mode.py | 1 + lib/air_runtime/modes/rvr_mode.py | 32 + lib/air_runtime/modes/sdb_mode.py | 63 ++ lib/air_runtime/modes/sec_mode.py | 62 ++ lib/air_runtime/modes/tst_mode.py | 35 ++ lib/air_runtime/modes/xdb_mode.py | 44 ++ lib/air_runtime/modes/xdb_sdb_ndb_modes.py | 97 +++ lib/air_runtime/paths.py | 99 +++ lib/air_runtime/project_bootstrap.py | 59 ++ lib/air_runtime/review.py | 126 ++++ lib/air_runtime/review_runtime.py | 148 +++++ lib/air_runtime/sdb_backends.py | 527 ++++++++++++++++ lib/air_runtime/sec_runtime.py | 175 ++++++ lib/air_runtime/task_graph.py | 220 +++++++ lib/air_runtime/test_runtime.py | 183 ++++++ lib/air_runtime/todo_parser.py | 96 +++ lib/air_runtime/utils.py | 65 ++ lib/air_runtime/worktree.py | 157 +++++ lib/air_runtime/xdb_capture.py | 161 +++++ scripts/airplan.py | 191 ++++++ skills/airplan/SKILL.md | 159 +++++ skills/airplan/airplan/SKILL.md | 138 +++++ 81 files changed, 6005 insertions(+) create mode 100644 .claude-plugin/plugin.json create mode 100644 .gitignore create mode 100644 airplanV2审查后.md create mode 100644 commands/airplan.md create mode 100644 commands/arc.md create mode 100644 commands/eng.md create mode 100644 lib/air_runtime/__init__.py create mode 100644 lib/air_runtime/__pycache__/__init__.cpython-313.pyc create mode 100644 lib/air_runtime/__pycache__/contracts.cpython-313.pyc create mode 100644 lib/air_runtime/__pycache__/deploy_runtime.cpython-313.pyc create mode 100644 lib/air_runtime/__pycache__/events.cpython-313.pyc create mode 100644 lib/air_runtime/__pycache__/evidence_gate.cpython-313.pyc create mode 100644 lib/air_runtime/__pycache__/installer.cpython-313.pyc create mode 100644 lib/air_runtime/__pycache__/io.cpython-313.pyc create mode 100644 lib/air_runtime/__pycache__/lock.cpython-313.pyc create mode 100644 lib/air_runtime/__pycache__/paths.cpython-313.pyc create mode 100644 lib/air_runtime/__pycache__/project_bootstrap.cpython-313.pyc create mode 100644 lib/air_runtime/__pycache__/review.cpython-313.pyc create mode 100644 lib/air_runtime/__pycache__/review_runtime.cpython-313.pyc create mode 100644 lib/air_runtime/__pycache__/sdb_backends.cpython-313.pyc create mode 100644 lib/air_runtime/__pycache__/sec_runtime.cpython-313.pyc create mode 100644 lib/air_runtime/__pycache__/task_graph.cpython-313.pyc create mode 100644 lib/air_runtime/__pycache__/test_runtime.cpython-313.pyc create mode 100644 lib/air_runtime/__pycache__/todo_parser.cpython-313.pyc create mode 100644 lib/air_runtime/__pycache__/utils.cpython-313.pyc create mode 100644 lib/air_runtime/__pycache__/worktree.cpython-313.pyc create mode 100644 lib/air_runtime/__pycache__/xdb_capture.cpython-313.pyc create mode 100644 lib/air_runtime/contracts.py create mode 100644 lib/air_runtime/deploy_runtime.py create mode 100644 lib/air_runtime/events.py create mode 100644 lib/air_runtime/evidence_gate.py create mode 100644 lib/air_runtime/installer.py create mode 100644 lib/air_runtime/io.py create mode 100644 lib/air_runtime/lock.py create mode 100644 lib/air_runtime/modes/__init__.py create mode 100644 lib/air_runtime/modes/__pycache__/__init__.cpython-313.pyc create mode 100644 lib/air_runtime/modes/__pycache__/arc_mode.cpython-313.pyc create mode 100644 lib/air_runtime/modes/__pycache__/ctx_mode.cpython-313.pyc create mode 100644 lib/air_runtime/modes/__pycache__/dbg_mode.cpython-313.pyc create mode 100644 lib/air_runtime/modes/__pycache__/dep_mode.cpython-313.pyc create mode 100644 lib/air_runtime/modes/__pycache__/do_mode.cpython-313.pyc create mode 100644 lib/air_runtime/modes/__pycache__/eng_mode.cpython-313.pyc create mode 100644 lib/air_runtime/modes/__pycache__/eng_orchestrator.cpython-313.pyc create mode 100644 lib/air_runtime/modes/__pycache__/merge_pipeline.cpython-313.pyc create mode 100644 lib/air_runtime/modes/__pycache__/ndb_mode.cpython-313.pyc create mode 100644 lib/air_runtime/modes/__pycache__/rvr_mode.cpython-313.pyc create mode 100644 lib/air_runtime/modes/__pycache__/sdb_mode.cpython-313.pyc create mode 100644 lib/air_runtime/modes/__pycache__/sec_mode.cpython-313.pyc create mode 100644 lib/air_runtime/modes/__pycache__/tst_mode.cpython-313.pyc create mode 100644 lib/air_runtime/modes/__pycache__/xdb_mode.cpython-313.pyc create mode 100644 lib/air_runtime/modes/__pycache__/xdb_sdb_ndb_modes.cpython-313.pyc create mode 100644 lib/air_runtime/modes/arc_mode.py create mode 100644 lib/air_runtime/modes/ctx_mode.py create mode 100644 lib/air_runtime/modes/dbg_mode.py create mode 100644 lib/air_runtime/modes/dep_mode.py create mode 100644 lib/air_runtime/modes/do_mode.py create mode 100644 lib/air_runtime/modes/eng_mode.py create mode 100644 lib/air_runtime/modes/eng_orchestrator.py create mode 100644 lib/air_runtime/modes/merge_pipeline.py create mode 100644 lib/air_runtime/modes/ndb_mode.py create mode 100644 lib/air_runtime/modes/rvr_mode.py create mode 100644 lib/air_runtime/modes/sdb_mode.py create mode 100644 lib/air_runtime/modes/sec_mode.py create mode 100644 lib/air_runtime/modes/tst_mode.py create mode 100644 lib/air_runtime/modes/xdb_mode.py create mode 100644 lib/air_runtime/modes/xdb_sdb_ndb_modes.py create mode 100644 lib/air_runtime/paths.py create mode 100644 lib/air_runtime/project_bootstrap.py create mode 100644 lib/air_runtime/review.py create mode 100644 lib/air_runtime/review_runtime.py create mode 100644 lib/air_runtime/sdb_backends.py create mode 100644 lib/air_runtime/sec_runtime.py create mode 100644 lib/air_runtime/task_graph.py create mode 100644 lib/air_runtime/test_runtime.py create mode 100644 lib/air_runtime/todo_parser.py create mode 100644 lib/air_runtime/utils.py create mode 100644 lib/air_runtime/worktree.py create mode 100644 lib/air_runtime/xdb_capture.py create mode 100644 scripts/airplan.py create mode 100644 skills/airplan/SKILL.md create mode 100644 skills/airplan/airplan/SKILL.md diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..ccf52c5 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,67 @@ +{ + "name": "airplan-v2", + "version": "2.0.0", + "description": "AirPlan V2 — 统一制品驱动开发调度器。单一插件整合规划(arc)、执行(do)、调试(dbg)、GUI验证(xdb)、静态分析(sdb)、网络调试(ndb)、部署(dep)、测试(tst)、安全扫描(sec)、需求审查(rvr)。L1代码级保障。", + "author": { + "name": "AirPlan Team", + "email": "noreply@airlongdian.fun", + "url": "https://airlongdian.fun" + }, + "homepage": "https://airlongdian.fun/plugins/airplan-v2", + "repository": "https://airlongdian.fun/plugins/airplan-v2", + "license": "MIT", + "keywords": [ + "airplan", + "scheduler", + "orchestrator", + "debugger", + "static-analysis", + "deployment", + "testing", + "security", + "v2" + ], + "skills": "./skills/", + "commands": "./commands/", + "interface": { + "displayName": "AirPlan V2", + "shortDescription": "统一制品驱动开发调度器 (12子模式)", + "longDescription": "V2 将 V1 的 8 个独立插件合并为 1 个统一插件,新增 4 个组件(部署/测试/安全/需求审查),共 12 个子模式:arc(架构)、eng(调度)、do(执行)、dbg(调试)、xdb(GUI)、sdb(静态)、ndb(网络)、ctx(上下文)、dep(部署)、tst(测试)、sec(安全)、rvr(需求)。L1代码级保障:不依赖LLM自觉。", + "developerName": "AirPlan Team", + "category": "Productivity", + "capabilities": [ + "Interactive", + "Write", + "Bash" + ], + "websiteURL": "https://airlongdian.fun/plugins/airplan-v2", + "privacyPolicyURL": "https://openai.com/policies/row-privacy-policy/", + "termsOfServiceURL": "https://openai.com/policies/row-terms-of-use/", + "defaultPrompt": [ + "使用 AirPlan V2 初始化项目规划上下文", + "使用 AirPlan V2 运行架构规划 (arc)、调度引擎 (eng)、任务执行 (do)", + "使用 AirPlan V2 进行调试 (dbg)、GUI验证 (xdb)、静态分析 (sdb)", + "使用 AirPlan V2 部署 (dep)、测试 (tst)、安全扫描 (sec)、需求审查 (rvr)" + ], + "brandColor": "#6366F1", + "screenshots": [] + }, + "modes": { + "arc": "架构规划器 — 分析依赖、写集冲突、产出执行计划", + "eng": "调度引擎 — 波次派发、监控、合并、修复编排", + "do": "任务执行器 — 单任务切片执行与结果生成", + "dbg": "调试器 — 7步调试工作流、根因分析、修复回滚", + "xdb": "GUI验证器 — 截图取证、GUI操作验证、DRM/KMS截图", + "sdb": "静态分析器 — 多语言静态分析、diff模式", + "ndb": "网络调试器 — 抓包分析、TLS解密、远程探测", + "ctx": "上下文管理器 — 压缩、Token估算、锁检测", + "dep": "部署器 — SSH远程构建、部署、systemd管理", + "tst": "测试运行器 — 统一测试执行与结果报告", + "sec": "安全扫描器 — 敏感数据检测与脱敏", + "rvr": "需求审查器 — 交付物与需求一致性验证" + }, + "requirements": { + "python": ">=3.10", + "system": ["git", "fcntl"] + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..852bd22 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +__pycache__/ +*.pyc +*.pyo +.bak +*.tmp +*.lock +.DS_Store diff --git a/airplanV2审查后.md b/airplanV2审查后.md new file mode 100644 index 0000000..c99bd51 --- /dev/null +++ b/airplanV2审查后.md @@ -0,0 +1,209 @@ +# AirPlan V2 设计文档 — OPUS 4.8 审查报告 + +> **原文档**: airplanV2-Qwen3.7-Max设计.md (Draft 0.1, 2026-06-09) +> **审查日期**: 2026-06-09 +> **审查结论**: 整体架构方向正确,设计深度足够,存在 12 项需修正问题和 8 项需补充内容 + +--- + +## 一、需修正问题 + +### 1.1 编号错误:两个 3.7.4 + +Section 3.7 中 AirRvr 和事件索引层均编号为 `3.7.4`。事件索引层应为 `3.7.5`。 + +### 1.2 AirArc 工具限制配置格式不对 + +3.2.6 中给出了 JSON 格式的工具白名单配置: + +```json +{ + "allowed_tools": ["Read", "Glob", "Grep"], + "denied_tools": ["Write", "Edit", "Bash", "NotebookEdit"], + "deny_plan_mode": true +} +``` + +Claude Code 的 SKILL.md 使用 YAML frontmatter 定义工具限制,格式应为: + +```yaml +--- +name: AirArc +allowed-tools: [Read, Glob, Grep] +--- +``` + +不存在 `denied_tools` 和 `deny_plan_mode` 字段(这两个是虚构的 API)。实际的 plan mode 阻断依赖 SKILL.md 中的强制性文本指令 + `allowed-tools` 白名单双重保障。文档中的伪 JSON 配置块应修正为实际可用的 YAML frontmatter。 + +### 1.3 P0-7 修复方案核心仍为 Prompt 指令,非代码级保障 + +P0-7(AirEng 无子线程状态轮询)的修复方案(3.2.8)主体是一个 SKILL.md 中的伪代码循环指令,依赖 LLM 自觉执行。尽管末尾有「技术补充:engine monitor_engine() 增加 wall-clock 超时检测」,但: + +- wall-clock 超时只是兜底检测(2小时硬上限),不能替代 5 分钟级的状态轮询 +- 真正解决 P0-7 需要在 `engine.py` 中增加代码级定时轮询循环(如 `while` 循环 + `time.sleep`),而非依赖 prompt 指令 +- 建议将 3.2.8 的主体从 SKILL.md 文本改为 `engine.py` 中的 `monitor_engine()` 重构,SKILL.md 指令仅作为辅助提醒 + +### 1.4 Phase 2 实现 PlanDelta 消费者,但生产者到 Phase 4 才实现 + +3.2.11 的 `TaskGraph.apply_delta()` 在 Phase 2 (T-2.9) 实现,但其数据来源——AirArc 的增量重规划(产出 `PlanDelta`)——在 Phase 4 (T-4.6) 才实现。这意味着 Phase 2~4 期间,`apply_delta()` 没有真正的生产者。建议: + +- 将 AirArc 增量重规划从 Phase 4 提前到 Phase 2,与 T-2.9 同步交付;或 +- Phase 2 先实现全量 replace 模式(Arc 产出完整 DAG,Eng 整体替换),Phase 4 再升级为增量 delta 模式 + +### 1.5 区域冲突检测未定义「区域」概念 + +3.2.12 的 `RegionConflictDetector._check_region_overlap()` 是核心判断逻辑,但设计中没有定义什么是「区域」、如何从任务中提取区域信息、两个区域如何判重叠。这是 worktree 隔离并行的关键前提,不能留空。建议至少明确: + +- 区域定义来源:函数边界 / 类定义 / CSS 选择器块 / 标记注释分隔 +- 区域信息由谁提供:Arc 规划时静态分析 / Worker 执行前动态分析 / todo.md 中的区域标注 +- 重叠判据:行号区间重叠 / AST 节点冲突 / 文本 diff 冲突 + +### 1.6 Token 估算比率命名容易误解 + +3.3.2 中 `RATIOS` 字典的值实际含义是「每 token 对应的字符数」,但命名 `RATIOS` 和注释「中文字符 → token」暗示反向。建议重命名为 `CHARS_PER_TOKEN` 并在注释中明确。 + +### 1.7 AirDbg 快照可能包含无关变更 + +3.5.2 的 `pre_fix_snapshot` 使用 `git commit -am` 提交所有修改。在无 worktree 隔离的场景下,这会混入其他并行任务的文件变更,导致回滚时误伤。建议改为仅提交当前任务写集范围内的文件,或仅在 worktree 隔离模式下使用。 + +### 1.8 AirTst 结果格式不支持嵌套测试结构 + +3.7.2 的结果格式为扁平结构,但 GoogleTest 有 test suite → test case 层级,pytest 有 module → class → function 层级。建议扩展为: + +```json +{ + "framework": "googletest", + "totalTests": 128, "passed": 127, "failed": 0, "disabled": 1, + "duration": "4.2s", + "suites": [ + { + "name": "NetworkTest", + "totalTests": 32, "passed": 32, "failed": 0, + "testCases": [ + {"name": "ConnectTimeout", "status": "passed", "duration": "0.05s"} + ] + } + ], + "failures": [] +} +``` + +### 1.9 KMS 截图硬编码 sudo + +3.6.1 中 `KmsGrabCapture` 直接使用 `sudo ffmpeg`,这要求密码免密 sudo 配置,在生产环境中是安全隐患。建议增加权限检测和降级策略: + +- 检测当前用户是否有 `/dev/dri/card0` 读写权限 +- 无权限时提示用户配置 udev 规则或将用户加入 video 组,而非直接 sudo +- sudo 仅作为显式 opt-in 的 fallback + +### 1.10 迁移策略未覆盖 todo.md → DAG 过渡 + +Section 5 的迁移策略讨论了 state.json 的兼容性,但未涉及最核心的格式变迁——todo.md 被 TaskGraph 替代。现有项目的 todo.md 如何转换为 DAG?转换后人类如何查看/编辑任务状态(todo.md 的双重角色:机器可读 + 人类可读)?建议: + +- 保留 todo.md 作为人类可读视图,DAG 作为内部调度结构 +- 提供 `todo.md → DAG` 导入器和 `DAG → todo.md` 导出器 +- 或明确声明 V2 放弃人类直接编辑 todo 的便利性,改为通过 AirArc 间接操作 + +### 1.11 度量指标缺少量化手段 + +Section 7 中部分指标无法客观测量:如「AirEng 非中文输出」「AirEng 轮询遗忘」「AirDo 跳过 AirDbg」的目标值设为 0 次,但当前没有机制检测这些行为是否发生。建议为每个指标定义检测方法: + +- 非中文输出:事件日志中记录 AirEng 输出语言,正则检测非中文字符比例 +- 轮询遗忘:monitor_engine 事件日志的时间间隔分析,超过 10 分钟无轮询记录即为遗忘 +- 跳过 AirDbg:事件日志中 blocked→merge 路径无 debug.session 事件即为跳过 + +### 1.12 worktree 合并冲突处理缺失 + +3.2.12 描述了 SOFT 冲突时创建 worktree 并行执行,完成后 `git merge` 回主分支。但未定义合并冲突时的处理流程。两个 worktree 即使修改同一文件的不同区域,git merge 仍可能因相邻行冲突而失败。建议增加合并冲突升级策略: + +``` +MergeResult.CONFLICT → 自动升级到 AirDbg 解决 → 仍失败则降级为串行重执行 +``` + +--- + +## 二、需补充内容 + +### 2.1 插件发现与加载机制 + +文档新增了 AirDep、AirTst、AirSec、AirRvr 四个插件,但未说明引擎如何发现和加载它们。V1 的插件加载机制是什么?V2 是否保持一致?建议在 3.7 新增一节说明插件注册规范。 + +### 2.2 AirRvr 的成本模型 + +AirRvr 对每个任务运行独立的 LLM 审查(加载需求文档 + diff + 验证证据),Token 消耗可能远超任务执行本身。以 100 任务项目为例,AirRvr 全量审查的 API 成本需要估算。建议: + +- 定义审查触发策略:默认逐任务,可配置为波次审查或里程碑审查 +- 给出不同模式下的 Token 消耗估算 +- 考虑轻量审查模式(仅对比 Done When 文本,不加载完整 diff) + +### 2.3 环境修复持久化方案 + +P3-4(环境特定修复不可持久化)在问题列表和痛点汇总中出现多次,但在 Phase 实施计划中无对应修复任务。建议在 Phase 2 或 3 增加 T-X.Y:「运维模式库:持久化环境修复脚本,支持跨重启自动应用」。 + +### 2.4 AirEng 与 AirRvr 的集成协议 + +AirRvr 的审查结果(pass / conditional-pass / fail)需要集成到 AirEng 的 merge 流程中,但文档未定义两者之间的接口协议。3.7.4 第 5 点仅概念性描述,缺少: + +- AirEng 如何调用 AirRvr(文件系统 / 函数调用) +- AirRvr 审查的超时和失败处理(审查本身卡死怎么办) +- 审查结果与修复预算的关系(fail 后重试几次) + +### 2.5 事件日志的保留策略 + +3.7.5 的事件日志是 JSONL 格式无限追加,与 state.json 的 P2-2(列表无界增长)面临同样问题。建议定义事件日志的轮转/截断策略。 + +### 2.6 安全扫描的误报处理 + +AirSec(3.7.3)发现敏感数据时「阻止合并并通知用户」,但没有误报处理机制。静态扫描的正则匹配容易产生误报(如代码中的示例密钥、Base64 编码的二进制数据)。建议增加: + +- 白名单机制(已知安全的值、测试 fixture) +- 人工确认流程(首次发现时通知用户判断,后续同模式自动放行) +- AirSec 的判定为 advisory 而非 blocking(可配置) + +### 2.7 并发度可配置的具体机制 + +T-2.7 提到「并发度可配置」,但未说明配置方式。是通过配置文件?环境变量?引擎命令行参数?`engine.py:527` 的硬编码常量替换为什么?建议明确。 + +### 2.8 AirContext 压缩失败后的降级路径 + +3.3.1 提到压缩验证失败后「重试一次,仍失败则放弃压缩并通知用户」。但放弃压缩意味着上下文持续增长直到超过模型窗口,最终导致任务失败。建议增加三级降级: + +1. 重试(换 prompt) +2. 换模型压缩(如切换到更便宜的模型做摘要) +3. 激进截断(保留最近 N 轮 + ADR 引用,丢弃中间轮次) + +--- + +## 三、架构层面评论 + +### 3.1 Prompt 指令 vs 代码保障的边界 + +文档中有多处将 prompt/SKILL.md 文本指令作为 P0 级缺陷的主要修复手段(P0-5 AirArc 阻断、P0-6 中文锁定、P0-7 轮询循环、P0-8 AirDbg 强制路由)。这些 prompt 级修复在 LLM 遵循度足够高时有效,但不构成硬性保障。 + +建议在文档中明确标注每项修复的保障层级: + +- **L1 代码级**:由引擎代码强制执行,不依赖 LLM 行为 +- **L2 指令级**:由 SKILL.md 指令约束,LLM 可能偏离 +- **L3 建议级**:最佳实践文档,无强制机制 + +对于 P0 级缺陷,修复应至少达到 L1 或 L1+L2 双重保障。当前 P0-7 的修复仅为 L2,应升级。 + +### 3.2 DAG 调度与人类可读性的平衡 + +TaskGraph 替代静态 todo.md 表格是架构上正确的方向,但会牺牲 V1 的核心优势之一:任何人都可以用文本编辑器打开 todo.md 了解项目状态。建议在 DAG 之上保持 todo.md 作为导出视图(非调度数据源),确保人类可读性不丢失。 + +### 3.3 Phase 优先级合理性 + +Phase 1 集中修复 P0 缺陷,方向正确。但 T-1.10~T-1.13 均为 prompt 指令修改(0.5d 每项),实际工作量可能被低估——prompt 调优往往需要多轮测试验证,而非一次性编写。建议将 0.5d 调整为 1d 或合并为 2 个任务(AirArc/AirEng 指令重写 + AirDo/AirDbg 路由重写)。 + +--- + +## 四、审查总结 + +| 类别 | 数量 | 严重程度 | +|------|------|---------| +| 需修正(事实性错误或设计缺陷) | 12 | 1.1/1.2 为事实错误,其余为设计需要完善 | +| 需补充(缺失内容) | 8 | 影响完整性和可实施性 | +| 架构评论 | 3 | 建议性,非阻塞 | + +**总体评价**:文档对 V1 问题的诊断全面准确,V2 的架构改进方向正确且设计深度足够。12 项修正是实施前应解决的前置条件,8 项补充可在实施过程中逐步完善。推荐在修正 1.1~1.12 后进入 Phase 1 实施。 diff --git a/commands/airplan.md b/commands/airplan.md new file mode 100644 index 0000000..5282fc3 --- /dev/null +++ b/commands/airplan.md @@ -0,0 +1,46 @@ +--- +description: "AirPlan V2 - unified scheduler with 12 modes" +argument-hint: "[arc|eng|do|dbg|xdb|sdb|ndb|ctx|dep|tst|sec|rvr] [sub-command]" +allowed-tools: "[Read, Glob, Grep, Bash, Write, Edit]" +--- + +# /airplan + +AirPlan V2 - unified scheduler. V1 8 plugins merged into 1, adds 4 components, total 12 sub-modes. + +## Sub-modes + +| Mode | Role | Key Files | +|------|------|-----------| +| arc | Architecture planner | execution-plan.json, task-graph.json | +| eng | Scheduler engine | state/aireng/state.json | +| do | Task executor | state/airdo/tasks/{task_id}/result.json | +| dbg | Debugger | state/airdbg/sessions/*.json | +| xdb | GUI validator | state/airxdb/artifacts/ | +| sdb | Static analyzer | state/airsdb/reports/ | +| ndb | Network debugger | state/airndb/captures/ | +| ctx | Context manager | state/aircontext/ | +| dep | Deployer | state/airdep/sessions/ | +| tst | Test runner | state/airtst/reports/ | +| sec | Security scanner | state/airsec/ | +| rvr | Requirements reviewer | state/airrvr/reviews/ | + +## Usage + +1. Parse $ARGUMENTS; default to "status" when empty. +2. Run from project root: + +```bash +python "$HOME/plugins/airplan/scripts/airplan.py" --mode --project . [OPTIONS] +``` + +3. Common sub-commands: + - arc: --sub enter|status|parallel-review|incremental-replan --todo AirPlan/todo.md + - eng: --sub enter|status|plan|dispatch|monitor|merge|intervene + - do: --sub enter|status|finish --task-id --result + - dbg: --sub start|snapshot --task-id + - dep: --sub deploy --task-id --host --binary + - tst: --sub run --task-id --framework + - sec: --sub scan --task-id --scan-path + +4. Runtime auto-bootstraps missing AirPlan/ files. \ No newline at end of file diff --git a/commands/arc.md b/commands/arc.md new file mode 100644 index 0000000..e5b86b1 --- /dev/null +++ b/commands/arc.md @@ -0,0 +1,49 @@ +--- +description: "AirPlan arc - architecture planner, READ-ONLY, never writes code" +argument-hint: "[enter|status|parallel-review|incremental-replan]" +allowed-tools: "[Read, Glob, Grep]" +deny-plan-mode: true +--- + +# /arc + +AirArc is a pure architecture planner. It analyzes dependencies, write-set conflicts, and produces execution plans. It NEVER writes code, modifies source files, or enters plan mode. + +## Hard Rules (violated = bug) + +1. **READ-ONLY** — you only Read, Glob, Grep. Never Write, Edit, Bash. +2. **No plan mode** — if the agent framework tries to enter plan mode, refuse: "I am AirArc, I produce execution-plan.json, not code changes." +3. **Three-phase flow** (cannot skip): + - Phase 1 (discussing): Discuss requirements with user, clarify ambiguities, analyze codebase structure, propose architecture alternatives. **Forbidden to write execution-plan.json.** + - Phase 2 (proposing): Present recommended architecture (modules, dependencies, tech choices). Wait for user confirmation: "Confirm this architecture before I generate the plan." User objections return to Phase 1. **Forbidden to write execution-plan.json.** + - Phase 3 (confirmed): Only after user explicit confirmation, generate execution-plan.json and task-graph.json. + +## Sub-commands + +### enter + +```bash +python "$HOME/plugins/airplan/scripts/airplan.py" --mode arc --project . --sub enter +``` + +### status + +```bash +python "$HOME/plugins/airplan/scripts/airplan.py" --mode arc --project . --sub status +``` + +### parallel-review + +```bash +python "$HOME/plugins/airplan/scripts/airplan.py" --mode arc --project . --sub parallel-review --todo AirPlan/todo.md +``` + +### incremental-replan + +```bash +python "$HOME/plugins/airplan/scripts/airplan.py" --mode arc --project . --sub incremental-replan --todo AirPlan/todo.md +``` + +## Logging Standard + +When planning C++ projects, the first task MUST be "integrate spdlog" if not already present. All generated code must use spdlog, not std::cout/qDebug/printf. \ No newline at end of file diff --git a/commands/eng.md b/commands/eng.md new file mode 100644 index 0000000..281325a --- /dev/null +++ b/commands/eng.md @@ -0,0 +1,87 @@ +--- +description: "AirPlan eng - scheduler engine, dispatches isolated workers, never codes directly" +argument-hint: "[run|status|plan|dispatch|monitor|merge|intervene]" +allowed-tools: "[Read, Glob, Grep, Bash, Write, Edit]" +--- + +# /eng + +AirEng 是调度引擎,不是执行器。它派发隔离 Worker、监控、合并结果。绝不直接实现代码。 + +## 硬规则(违反=bug) + +1. **Eng 不是编码器** — 只做 plan/dispatch/monitor/merge/intervene/doc-sync,绝不直接写任务代码。 +2. **必须通过 /do 派发** — 每个任务必须 spawn 隔离的 /do 子代理(fork_context=false)。 +3. **保持父线程精简** — 父线程只做调度操作。 +4. **直接写代码唯一例外** — Worker 硬阻塞无法自恢复时的紧急干预,干预后立即回到调度模式。 +5. **必须使用中文** — 所有状态报告、进度通知、问题描述均使用中文。禁止英文输出。 +6. **自主决策原则** — 以推进开发进度为第一目标,以下情况自行决策不停下来问用户: + - Worker blocked 但修复预算未耗尽 → 自行派发修复 + - Worker 停滞 → 自行执行停滞干预 + - 验证失败但非关键 → 记录问题继续下一任务 + - 波次间衔接 → 自行启动下一波次 +7. **仅以下情况才询问用户**:修复预算耗尽且任务仍 blocked;需求歧义无法继续;系统资源耗尽;用户显式暂停。 + +## 子命令 + +### run (默认) — 一次完整调度步骤 + +```bash +python "$HOME/plugins/airplan/scripts/airplan.py" --mode eng --project . --sub status +``` + +然后: +- 无活跃波次 → 派发下一波次 +- 有活跃 Worker → 监控,不要盲目重新派发 +- 每个 dispatched 任务 spawn 一个 /do 子代理(fork_context=false) +- 只传递项目路径+任务 handoff 内容,不要 fork 完整父对话 +- 最多 recommendedConcurrency 个 Worker 同时活跃 +- 就绪结果出现时:merge through `--sub merge --result ` + +### status + +```bash +python "$HOME/plugins/airplan/scripts/airplan.py" --mode eng --project . --sub status +``` + +### plan + +```bash +python "$HOME/plugins/airplan/scripts/airplan.py" --mode eng --project . --sub plan --todo AirPlan/todo.md +``` + +### dispatch + +```bash +python "$HOME/plugins/airplan/scripts/airplan.py" --mode eng --project . --sub dispatch +``` + +读取 AirPlan/state/aireng/dispatch/ 中的派发清单,为每个任务 spawn 一个隔离的 /do Worker 子代理。 + +### monitor + +```bash +python "$HOME/plugins/airplan/scripts/airplan.py" --mode eng --project . --sub monitor +``` + +合并就绪结果,检测停滞 Worker,更新 nextAction。 + +### merge + +```bash +python "$HOME/plugins/airplan/scripts/airplan.py" --mode eng --project . --sub merge --result +``` + +### intervene + +```bash +python "$HOME/plugins/airplan/scripts/airplan.py" --mode eng --project . --sub intervene +``` + +仅用于无法通过常规监控或重新派发解决的硬阻塞。 + +## 无人值守模式 + +- 每 300 秒重新检查活跃 Worker +- 当前波次收敛后自动派发下一波次 +- 仅在状态达到 completed 或用户决策阻塞时停止 \ No newline at end of file diff --git a/lib/air_runtime/__init__.py b/lib/air_runtime/__init__.py new file mode 100644 index 0000000..3567c45 --- /dev/null +++ b/lib/air_runtime/__init__.py @@ -0,0 +1,20 @@ +# air_runtime — AirPlan V2 核心运行时库 + +from air_runtime.io import atomic_json_write, safe_json_load +from air_runtime.lock import FileLock +from air_runtime.utils import ordered_unique, session_stamp, normalize_policy, sanitize_task_id, sanitize_marker +from air_runtime.events import EventLog +from air_runtime.task_graph import TaskGraph, PlanDelta +from air_runtime.worktree import WorktreeIsolation, RegionConflictDetector, ConflictLevel +from air_runtime.evidence_gate import EvidenceGatePolicy, EvidenceClass +from air_runtime.contracts import now_iso, WorkerResult, TaskRecord, DeploymentRecord +from air_runtime.paths import ( + airplan_root, todo_path, plan_path, + state_root, engine_state_path, worker_state_path, + arc_state_path, dbg_state_path, xdb_state_path, + sdb_state_path, ndb_state_path, ctx_state_path, + dep_state_path, tst_state_path, sec_state_path, rvr_state_path, + required_project_artifacts, +) + +__version__ = "2.0.0" diff --git a/lib/air_runtime/__pycache__/__init__.cpython-313.pyc b/lib/air_runtime/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f80a9122583019a61a8dd103885d469c70900b91 GIT binary patch literal 1479 zcmZ8g%WfMt6y@mse%V(1h~E!INh}2^if-BhMU=Xsj3Tg`7FiAG8Csz|8cr@LInJ&h z(U0g~^cRYv8y0~U?WSD?FbZ$WJB|w}1L%n7+(X_=^75)&E)e_u@lWgQmn0#7BDwt% z9|l){*}+fbm>fHOLLG2u0umd@6a6GjK`I_6`)Qhi49!B8<{(G&kf#MG#IaPrNJ~(P z$LW5VR-i(wP>p$}U!!%X(*`u?9L&X9wm(l7V1YKF8S`BK4qb#rx&%vf8J6h^ti*c0 zf0wSpDqVv$x(@4f12*U;Y|<^*qT8@dcVLI^!Yi^1+!8r%a z@N~>WeJBUEr2Kc*cK|GUW%ADh7WR0cSvzU!KS(&&!1*&J1Ey_G(>&q>`~O6Sr#6@F zam_s~!IW>6Lq6t#U9<6YEPNh%{D5ix{HCz-t@b2fO0`oa8_Ek;C}~QViNg_Bqd=Pi z)^o%?yKz(N@hFg&LtE^P(yp5l6A;-tU64|nyq3OnBc^Tju^JBx_H;DAanyM@5Fy6{ zvr-(cU&INgr3wRYhWjUj8UDsUnc?aq9)83<{bt7R^Js=^rDwR}v-x2R)A42CuSWtK zAp&`3kKJXU#VPZ&dYgXSdDMCIZM$nK_H0}jg<1@GM@Xhmc3Ww`>~IUVG7@R#3!1b2<=r-2mGON;tKQj`?W^9zOOJR4`sesG(_T z+QdIJkUOxq26Z$w(6lgZ@?@w1j{xS-Fpsc+&_wIZ-tgWP+`-@?!VR8- zU&5z0qho*S8w>A~j^kWsi1WoevO6Ks?<9IhS`+fwguMKfyqu7O33>4wd2yY{I*DsC S1OFm@-yqfIk45;zF7_WRWYkvx literal 0 HcmV?d00001 diff --git a/lib/air_runtime/__pycache__/contracts.cpython-313.pyc b/lib/air_runtime/__pycache__/contracts.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c4e849fa4613d2d80a1a8e0d9a790dec2d97e0d9 GIT binary patch literal 5825 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 OoPARLoWKuTqyGcNVC((> literal 0 HcmV?d00001 diff --git a/lib/air_runtime/__pycache__/deploy_runtime.cpython-313.pyc b/lib/air_runtime/__pycache__/deploy_runtime.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4c39a39a5c3e95cb014fdd5085f0194364de8aea GIT binary patch literal 6447 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 literal 0 HcmV?d00001 diff --git a/lib/air_runtime/__pycache__/installer.cpython-313.pyc b/lib/air_runtime/__pycache__/installer.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..533c9a2bea5bafd325c6e929c7321ec32fa05ddb GIT binary patch literal 4951 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 F{{ZBTbtC`) literal 0 HcmV?d00001 diff --git a/lib/air_runtime/__pycache__/paths.cpython-313.pyc b/lib/air_runtime/__pycache__/paths.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4025052448dd1922dfc3fd2f574cdae709cb8463 GIT binary patch literal 3897 zcmcJS-)|IE6vtO{7M%$%OQeuEm1<_(EWSz5AUxcjkWX zxj)X?g{G#cglp)Jy%Swf5BO&B^0`7@cmy#mr6iJ4a>{o~9`TVc&f0&-hS*RAM7NJ_9 zT7_zZY7=S;)E1$(LTwdl8`L(T+M(Ko>VWDHsuQYHsIJtGRF_cObK4U;r(VXuqrZLk z?d*@&=D+`X_Rh`tFXa#8Z|;xJ-}~a%yXCp-<+)F<&wg`b?$%ecpM5lc^ICj3Pp1oM zBR>Dho!K9z=kI+m_xZI4<@ck%vo8~}qoxhRw9|IpG%VaIr_=U$MrLE}VyF8hNnVIS zOiO3s<~~)DdZmPK)Ctu-e_OgOJFy~}6MDwh$TV$>zY%x33i+{QI!_u+8FqeBPZsfg ztH1cJ6T{OY$V6+x7bTu}s|i(E`bq8lI8jx5DoW1+9!UQn0*y9=Lk2ZiyD%SPOv6BSk$q7@IC*}er;S^60evHR-oTcN6&tSCuh53GZv6tc~% z**}@37JO(GNrbH$`-ixHE(dXY2+$F&Tyv$Zo1gmO)Md@!q z(TlelEzXp$wvGIwP7r%RH*$Zui?M&gG7YR(UXH6)NgLYb4PA4Wt(c^z(T;mtV3+Z- zZNE{tul9U74#t@sRrN?kIpTuW4p1}`JILKd`*8E$pl8LdPx))m%BT|nbk=kNcG|jZ zmD=7h>9S6Jf=i5m*l!6RMeyT%Kb((7o#|2C}mP(Ad^vizsdCo50FlH4;Zbv+Ksa_{4?EbrrL|57!`D)~4j%STzA Ym$?@5->Mhunh)}Ohn|E(vO5od0}Bq`ng9R* literal 0 HcmV?d00001 diff --git a/lib/air_runtime/__pycache__/project_bootstrap.cpython-313.pyc b/lib/air_runtime/__pycache__/project_bootstrap.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..17bc40796cf9e731504778aa134de28bd995164c GIT binary patch literal 2159 zcma)7-EY%Y6t|P>k9;(3`D$TZYG^40QNF~)*q4L^V}OPb#=M}(a&6O?rgn7g@bwfX zCen_LOwg?=Fs&~WJd9~CQ#Y9S3ntZCrAGG&jiyyzwxWvnoqL-`WK3FbBH!QdoZmUV z_xK)rsih^(!3h7_c0m_8?l~nZBeD$mr5ymqIF-Yk%BztR{P75mC=^FeM2{nk0E;Tx z#9csG7!)#6VH5)5waH%=?mntM8LR&IWd6Zq_5L@?pT#@M!sMg5r}vdZ4nCDLT!l)0 z8(VmMd;Z(&)gQ*I_Z~biei}c!PXW>V&1-X0Un*z!f@-Q-zPT`QeQxTD>aFqGjaz>| znW&b2uHL>@d-zjz^g*pSG52)xc@Y9e=cYcN|N2I4;?BbNk7=?R-A9He3Bz^0zz7`A z^?{;OMvygm>a33rn4=uWFO9<1$2bkP*q`7ym)Bt13`Mkv8f7S|MODNQ0wgdbfLCNl zgb0ZtN#nJM7S)g@Xrd-*k}6ZX8lzWWamEr&EZM|b>MX^St##JMSbGy&Q)g?LZ(W_O zXKX{AbuiXhXB+GDCT3m9_6B1s5nXl7o6O4Ay?cWzYw}v|qqR_-m=-(zY8C6nRMYV5 zJ_rZpsP5O4YXtRYcwP z|0;F6n%=|ra+yq_7s^vD-K{?Uwb3}$r=QjC6<=*1IO{~J_Z2~wXZj>*U^D9kwiy(# zO`?W{NyOYw1j`;M3=+YIZm@}HI2d$9!uCbSzT`85?G6&za{Rmzm{}sRJQr;u|DxL}Y? zBTrZAlQ`VK^xVL{9I)kBcAm|Fzs!BxWE@|_XEG5YLJAR;hLsw&mjvhIA zM%^`RU6tsf83;VfqaXzq)6RN1%f^()hCQp0vnfon+VtnVLCV?&3r>HNrJWYdF%8$q zUGW_sL?kxg!A=8g821`p>4&~1AgQ48pAn%0o)&^m|*ez*98b{v+CFu`&=Zh$q7iCV2mrjI2$1F;RC_RIeiV!Ou3x)Nws4GNWGbsHZ zhU9c>cPMnvqP`II&7kcSAyFC%g^jaF36U~`wp4^vIo+&CO}F=iLeDJP5uzP4XeXqd zPBvwEmh2c`H=RgNqx2FyRqkX4;q5SE1Q#M=8~Vz<+G;swa1M^@P6duh?vsp2BwdGB zg|3shuAeW^?$dQl-_W$Ff-IaemV%v^haBEVbMB#T`Mu?)`2fESlzstx`T<>x@;v_{ V66Mj$ILG%?xz3m39=@9e{sUFFR`&n^ literal 0 HcmV?d00001 diff --git a/lib/air_runtime/__pycache__/review.cpython-313.pyc b/lib/air_runtime/__pycache__/review.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6fce693a2598ce0fcdf1a9f900e734725c44b081 GIT binary patch literal 7846 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 Baoqp_ literal 0 HcmV?d00001 diff --git a/lib/air_runtime/__pycache__/sdb_backends.cpython-313.pyc b/lib/air_runtime/__pycache__/sdb_backends.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ef8300d3edf568f00d989f886a2d9bfa2b282059 GIT binary patch literal 19579 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 literal 0 HcmV?d00001 diff --git a/lib/air_runtime/__pycache__/sec_runtime.cpython-313.pyc b/lib/air_runtime/__pycache__/sec_runtime.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4f3d9e2ab5ce563e9e46d50837e4addb6d1d7948 GIT binary patch literal 7472 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 EZ=NOzGXMYp literal 0 HcmV?d00001 diff --git a/lib/air_runtime/__pycache__/task_graph.cpython-313.pyc b/lib/air_runtime/__pycache__/task_graph.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5cc1bbde63375c298b8e831de764d25b6d262666 GIT binary patch literal 15077 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 Tn-4Se7Wy-Ymcfru0L%Gbp(?t8 literal 0 HcmV?d00001 diff --git a/lib/air_runtime/__pycache__/test_runtime.cpython-313.pyc b/lib/air_runtime/__pycache__/test_runtime.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5012cfb066f8b501115897b400e0a78742c6e2ae GIT binary patch literal 6311 zcmcIoYj9h|72d1+(ACw;mMq%|siVXRv69FRdD;mfm?VbS;DE0cOl?Ofl5CXNlC$>; z5)V^c!hmTrflfqe)XKak}Nwe zbXvL|AMNhhJ-c_$Io~VVO8>>rgN~@jPS>4*^XmwJjpY%x8WI0jg z#YDAjvY6{_%CApdd9DIn`wd)op6dXv^9F7ilj{PWyTGI6{d}M-{XGz`G$&}Mru(OU z`@>IPKk?}wp8fRI<5yl9H!D*o-}HYp@`8WgdjHgmXRiG0r=`zXUY7}vB&fBZCQ56Ffb4&%sh*igdZ!Bi|gfOSVWd^jB$$aCD`a9@II z`amKXPo{tk9VMp}4%0GNm$JouVfO8CC=p$!i34dpuE78g448p`Vk{j<9B7CnXqcvx zdSWo%00Zlope8@Y2-eNXVw zMTIGI4qJd;DBMgE=+_*!07ops(VIssP>Hq7BNnKYwPbeS0+m_IhOHjka?+!yN)t!z zsvYJ#*cc~u^t%FX!-h&x(_j-<+4dYTg*uU-M;Uw~sT-mWmNsBWLttI;m?8B=5(DsS z)ea?whT^f)oZ*he`y%N9J=_M443*fo&Q^zMSfdkrxPde_+bwg;TliwjgB@gb>1 zQiWtG5SAW>e&4s>v&CZ zHB>ac8Od5CK_m+)yAByhaC=%O5Ky+;13La1Wmou2hxE<5=Ea)YGrO`iO<)&$b3)U_ zoBU@gvNvspaR=WMHk(#}%gnI?D|A5NW}XB(*9ue%6&i@j-v_}=6&oy2m+s?$S{aoY zwE}J>*3}S=`()dt{>cBb- zZVP(WhrPjIDyFwr7gb6BizsIRXF4gdx1Pthij0y_xerutHJFru4NCAuEvEB&cd|3Dli zPz?EWGm=ImTYT^fER)I&S}8+XV`NGQxK{Q5~q zEJ}b-0}||o(X+oHadvzS=|-PtrNdRI&HgS28DO7ap(X2D3iDN|NlaSd+YqAXm98xv zh}rW>Aw^2cFbhF^Ug`3LoYAP+B^g@7aC--i|hv;SK{SLyAE3OU6uum0&V73`I*H7^0~t zBrkLe%r;z|roi1th7uH_rW4XFs>Q$~fXxuJ_&^`*CZZh* zCt`*JWFCx1Ap=RJboMcZT?bEz>nwXS6j+`{!qB6^&Ich#=jAKlf%&yGG&6spP!>1y zTLeFXumRvv55qBfY99d^A=46Z_+Huj(%zhX%_XmYyy<*Z=I&@FHkgSfGp(u2?L(Oz zhqGRqanQ>y&$Am(?s#s;SX0&;%(?1E`48-skPJQ3`gH3H+No{1s#Whg>OQdh@_o+^ zzw1~&EeN*zxl6vKC-*J^c3Q|gmSsC{=HFS#0gVIxgP}_{9~_DrxC1;;G7oEFDE8Es7wj zxQ-MsYC*oFS_}ODQiA+HOR&vK=q}>>zuApaa-q$eALNN5!`0$vcjT`X#SW+xakb>k zREx?pTm@sBhpSH0@aT4!`c*kbZa;%AznTBj+n^sV0qMcP2t8_Mlc=te^qt+eFq;gB zv$i^qACMr00NI8(kcz}K!`-UxZQm7-reg8k2Pyb`A(Yn}xSM2;fk4a_jS3Pk{}u9Nl$YB4yv1tgM;zRGzGRuI^OR*nylU zFyRSiJ;Cw*Glz4YCNS=kjn6fn+A~IT-qjP{`mDEp{K(nzoOj#TurB$kPgT59{ZjSV z>hS})MYrU9>#y0RGG&@bHs#CfA}OnwaMxztwFp_bEB|=od241*Pi9AN=C=J=Z}@^E zjF|NxU>06DAFsTlhWxPxfQx^J|HGqH=^H%8`i&#x*%7L(N%+>S zS7|r#60Mg$fP~qrGnpRj#Oo|Von~HgUC?(gyW6r(1KTa^j?3DR)E0qu)_B3B5vOPm zl3pa3rqTUK!a(Ml!DsO~Wgy%SpJ_f`z%KZx)<&uI??ig>*dFu1VmRYiPJd*RS zo7cQ%6VbTD8;uL4`^Pt*?a$o#-OTpx%&k3HZ|?<1FB;eWqH#4>khd$AH8=8aZ{dJO zOTnZV565B4CrEcO0?tOVg{1tg0nbs3d1R z3``BBj*v^f+OfNHzFQ`Ijagsg*`};-7p6;|3As8eSC3Vk5i(fY^uD}dveG~HzV(C zp*d{Fr$_&z{`M3km3y(Lj6D`WGSZvx#uOFu@1&WXNF@>w+ZotX9c$=U;DZbG$_wrV zPr>v?oNoxY3D`E?Xy!dS8CK9xDhe-uJCTXmFue;2h7pQROYzVbe1*VkHbsK}+Jiuz zBo~F{pGm~Ki<_*f$@n*ADmNQt0mHN1aMc^-P1l@Ok2KOcT|w&BPOfQ~7AwTXlZ#eN z^YHs&bscMexUu*+u30VntNGs20UW^F;!e3wY?m@Pm(`eQ9=y2~z-k zVnR|2vh2kNVR(~4|25yy!0`2l;BAwl)(lsv^(IoL)y^?#W{J%5%@2AIFuU%&C%)!$~PPK^5go_^W4b*b;l`9Dm({=9EfW5cp<&i#5SGj#2p zSEtS$zjF44uTPCmz5dSC7e=SvI`_@FUtKx<{M570Upw{tYv*3MHu8)w!p7r5xN%2d zns}%{1b2Lu{<-NocEwM~W`^SuNhTRfaKc95jZr4aMBjao$ zN`ToVIF{bm%W|^$2__zc$>NF2Y9`9Ri8lJpvVo>Krk|y0*+kR*iD)X0X$wv7O)>F& zkDaD>gYINJ#<5%ix}l*cj5N(#!8zWJs}fu}aOH$bE~RPgN-RP%$s`}!l}du6H1C4G zTku12G|-#qX9G-(k0-dEXpG?kkHU&P%03Z`utH!}jNg{v`^W(%q6jq%#8W*nPH2e6 zge30W6lb`tO9S!PF06y*Q(Q6z`jQYfdVt{tmR~&ZB(DckN`<2+`~<3BqRZBj!y6=% z_3(PB%$sh@szr@ga(dH`WmaRV)RSJH5k!qg@|34TS(m6Om+X%8>Wu5zcD|yZFG2uS zC^!m`FvP9DfxY91LY>9Ed8xvlSeWOduvY@8221CYt)LEn@eXVuID#TN^ZT>7x1h^~ zwyFeG00}A=TIWY7v7i&rcvdM$7h7&GM~G70OL~*4eGx)tL`HUGI^~;n7|NkulC_SB{Z(h!AO-ZjXXIX`Uft z!N*Da!_aOmlCjN@u`4p0L8fFzyF+Q;0qxHGSiC~4;*(S9>zmGu0OD+2O{5E4L8M!Q zTxhLoT_>yytmKc#g}D%E-H%wurC`Aj#)pkk?!8Kdb%6YNIiG6_{)d%q%IChFGf?&! zc_F}PQzt&jW);$K=LRhD&R+09$zL}_@t^-~G|ue~f-L1q!XQ6gI?D>woiE(GWmHMw z*5w?Oqu?n6(V-ljki)YJ=_&ZrK~N=}6Mn9{%*P0_u=2S4AM+_1ou7}FbKk|1g>>DA zrJ~WdBUgGCOLnsSJ}ecDzHO=OE|wf*#eG;R8hzW6=Ps6<@ccpx<)mCx zDdjHcVgPYa?oK!zXP3fx3OmS4l~EqbOU`T27qZ@mJ)ov{H)X4VyVD0hPPMy08Hy?Y z{8S6*2NdeV&|-0aG>=@boIN}B+$n&mfKCCFPCYkzHT}%h*V0%2bmTU61^jtHn@a9( zSOyr=z;cm9G{*H@{}&XQI0%pp!#EwOKsFw4G#=ld3Z9a+3bvEg0PYl&C2KIGlQkIA zDPT+1D!}dz=DLnGY~(T0IpFCIFAT40c${fCu%w}d?r!K_ykn8Sd&hNrZSyv$uH&V< zuE7;v^#l5y-Qk1XJ8BN78yXt?dLF+Qrrp8!>wm&{euu1M_X8XQxXH6jluokyld?$| zh{qJfD9Bnst+4^w$a1NEmS>VI^d=cTDeQ|Sd!ZLg%G!8hAIr%kpan*xUSm&?XH-~)a7NP+O(q^)Y)Ry7*T*{X832h(dMU;U(S>9}v{C0~4oP*5 zlXb!Iy5MQ`rMgzp*)~zvI^y_H|Gxf1<4Y~s#-ZS`;43ZTb*<@8&e;az0B^7TynMmP ziciY}gVu~%Dz7>@@5DT@+@G;YW=D3XXs(v#Rb?ML(ji$(hv$jbddXLvT_;*9C98Yb zCR*!8YsXfK53G`E>a!a}OO<4GWe#`xy;wwl?wF|TDqGf?(b!NFw ztTj@lFT3W*#+!3c#gZF7WVNT)NF}8sjmNlb;!;UnMkCo>N1q;idRUmWSC8APKeM={ znuTw)ooX8uUhfb~+FtD#-usSly77GQOz>>W9}it5#)Io$D$hEGJjXn*l%MLzXmcfP zU)tR;@^W+QyWzLP7s@U=#OBb)kzD7tkM@YoUD-7!7oAu%vU`*fE0#>S8*@8(7{^<*UvwI&7E*tJVszANu^ z-&mtq(LCXPDA&6uW0Y2|Ip24t?_xu4`{Uxu@THwhZdZ@EvsYZ%n`KXiPJ~9SV|&H9 zEfZzSbKF42EG=$+tMARe)3p~I;^NhRj$Ew$V2`+XQ-&Q19SeAd4ft;fHV6 zs*rUx@x!-skt+nBCCCvXglgE0XS^L=&HJTps6Q}yJ1R9F)VZPl>*A`81-cJiZm9hl zz7-^SeAdc3o=v8B?tm>0r)RkE(%<1X$$FZOCL+_1^4%#Af=4-zPip=lJkWgmPYYJy z@!2iw2jHIzcmykom9tSfBb0~s^lMc)L=3IdPyChq78r#u0U-$04K+a!H&rS^^PK?^ n)n6gw7pUqBbWb)HiS0+ zdS~|T(K+{?yZX*K-#w3=P6vYV@E^RGG2a(3`6*RaR^T*~wexqrD8a@h;wKNOq8HhUDP0dAlL8d=Bq0 zq%1y{XAQ~8=kas+EF$lvu&bGN=ZB8NhdA_(%X-H({pLlzI(HEC*G?k5cP4X*R#0imKEiYBn;r2W1gf(G_x=k0M*r!wPSJntGf7p%*ny z+t#HQ$W#fEO&&A{?RWzrnQlx61|bjK$fOscb*7Z`&9p**-yXVdzK7mPPIzkYRHHeg z^3dzjXJ&lJOhd)Xp!0)|nsV?%?X+iB&%8ZMW*>(TpxdgdQ zq3y6He)oO-cDH_P7@!3p6DO`Fjvmkl`;wP0#fLuCBYhL+2K3`c^`T3gxyjD203>y7Pr`|D;=({o5_p3zY`oocH&kThBj{s@!sYII2TX?95oHj(mIH7gcd1AMA7edZ?Cw!^ck~S*>T8Oz%~7V=IPQsXlIk|X*#yG>Bb)#?6E=B3Te#T}1E%rY$G9rKu9k!PQbjHm5&+t5Ho(OTz=HuUex67#kclJH44_uP z-wUAQBG8is3Yq=~lu@QM1<>qFCIK`6N`TOdRv{1CMgc)m)6`dGP*$4MVA1r*WH3-E z{NWIIZC%^2`em>iRF{R&47oPYf&f*5ax#1x0s-}q z96Xuqy{Pv@^jkL`j+_qe7crLnqQXsFdOP`xlbl~{5`fhdD30W{Vg2ko6Nf+6j|@!Q zxt!=apNd&F_|q7*tyRRN1<)L?34xHf4dWoz?De2FL?BjM#+hcHK1Z{(h`3qwYt%lC z+OIjqH(N!Y3fC?1r;tBr5Ga?bqnykgD|%CxE?x;MNDe3}Okwml=e!a$bGm+fpIg%3aIP`NmG(Fvndscb_cy$HuXJof{pg1JbIt+b zkKC&0>y16z->n3qoV)aq%OJ{sPnprYq6?MLZ>)TbsNBWl+@e#ZCn_J&Ab+Xk!ofEd zR1}&%E6lH0W&W&$g87MinJ|WtX1hT6`a0w(&sXkXKY%=wG(6nlw zzmap(nhA@Vy(tt301f)~;HB^-(4|<@OB7N>nJ_AXEU8kv=uI2(8Zef^qpX05l(CaA zVzh9@K=T;8ew1B*-&GiWZpFa&KB|t++Yob=MQvr{Y+krFRn)T!bQ*2K`_OCxGDcZK zE)m>_ED$uO_we+8L6ZqR2a8i^ay#m4zRdxelZTI_FeT8?#l@T2nes)&#qCR41iVkg zoG&P=0<;bww6ut_3KbNPTA(Mu3ZO(~V6mi@&qgJC8kLj{6DEa%M+%_^$Zcy@kL9hO zom$$zJvRR(1CY)4EVtUD^QvO5>Zq-HoXri_e3j)O^-}3aYGrm?2x*oE!9#640k8l~ zHBc`BWDF#ekx-`4TG*6`e4IErs2_VN;N2rBY7nw{8ZfI4A%fNFSz#u#8Fg-6s4ByfEn~ z;p!$he(CZ{KD7~Ax)mHW(7Ul4lwe x(F0`vD_ZtvR4`?+Q+ZQ`CTb5gmFet!tf!tAJjad!8Qpo zv$jTPYg(n#N>`;CQcJ4V47HX8wN}>C3UvkPI$FTW3ZcxFDJ$&f9royJFsS*7Fx?%Y ze!nzx^4PV%yO8|w{p+U>PoFuR{F@Wfe+(Uywzo=;wKTP?lBRxtF8R(YUk$xvP9FX! z`PN(4K6*EK;J4TQ{>ACHho=AeucdpX{y;=}cK&c*Up$=L|Kjy?QvZ+(_Xka2{OmY4njG&E1k^2-RR(6cq+;Q)ypC^^y?i4wP0;H)E5kRBaix@ z@dsxLp_eLz!XdvZDB-?{D(v)7-&xY(FseqkJLDPgyWOhU?H&mGqCw1C-R`HOo?xoR z=63f5s1gYVLjF(~s)dfN9qUv<-nxF9Dr{`)Y*)+NZUy(_b$cQa8t9Ei{EFL6tuXT( z{E6ym-WeY7H+up!7!LLO0-jKFJM6jL|4hK^SDM=b6mLr&^mtjLromveKM+!yd;ukb zwT}foq3x~B!9XvL;il10Bmm>3@5DLy95ui^jNpEiA|MAy!csKUo+kU+X<53>yz@=r zlbd@Ks^_0?1Cez*;Dqmi8kQ4SPJ}W&%NgM8v4xSnS!Gi-*@bsuhP$vh3RGkIE@nz$ z2$feNRMkg3%5JwO_3UMfR>dG*mP%AT^?Q_X$eX%#=tT%}Dy)*5i$LZ$agk0e%}G$2 z;;HNkC>65bwS1?3VHvbEE-aNfr%+EEx(4y_+vJ0X;QE9k}4 zny_Pju&soe?*W?tpb{CV^pG>KoRNzCCWl$IZ1K~6f2UuG1|u`|Fb=H-0>_HD3iwo^ zHyjSCW+mzc=2H5i!CR&vFj6;wuSYd`Q_?|Uo9uAt^BoZV5e}R0C>pT>2~Mn(0D+?@ zuQMHW+~ zdfAGa2RP0k8>kUve}P)w>F)=@nnqT;KjQaB!gS_dm@=9Gk|pHdn0fO+wx~xTol*kH zb0?=hJoNR&;pxMNrY?>q_YZ?w`1&H0oHVyMrAXKpb`JQYsqtT5JNZj($my|z)Bkuz z>ZWV~tED}A_Vk!rol^4P@1~ERZ@%`M3)lbU!)wP6!=P>QHYquLVER`h)5EVrzw77D zL)V#uFT)Jx6;25>Rr1IOFzn2Ue}d*y7haWofxbQoTBlUY{(Ug!^egACzxqM)$m`gx z!<$m*u*WQsu?HCMIO)!a6BoBCmCq{ErOTS2E+o3dWo!^ae84T;8L^U2l++U!-wVRY zC3xqz%_Cx`Y7`T(9PNzGn29w0$D{TW&ZWVLL{;Un3g>O)%eWnpLe$? zHo=yHu>udRu@Q$J6o5aXsVdY^6jlaGp&!bSksJlI0SoM!f^a`e0};O)dZbEvvr0@F z$`sHenQX6W?F)bg4?p9lLC>Hn_6>q9q3Wmy{=KTfgIbzKRMF%1hkUA`H~rp=?+VIE zDLZM>TJK{lq%O|SSWc}P#hG#z$Pl@yCzW@_?z(rPqVXS;*ow6i&F%4u_F?Onw!5yB zNRuTE@sfs^wc)CzG;UcmW{OKK*~MR#)eWyp6jh&Ce01?>bnN-Dfr+A(!)q3ne6ghd zXyJ(Ai@TQ{?HJjRsHi#qi!(p|^+Q)`8z*a9;!>&$MO>XbHL)BF<>x-(qZZAg#o z64LeQX9?NBZ3NX45^uM6i89v&UGnxr5J3WD{~5WbYaLG0wI1oDp%ab1oaC~mdlZrtIyet8R4{L~ zsPuEv>MlY&VBO@*p#On07+?mzvnpp$caNm4&K+ zc8eRJ{_W5xTYSW%Rz+em+^$Fx+;fta`L%^yxQNy|Z*|=i5z}%a)IDiHh2z&yQ_9y?LbN z%f|agc4+qVnCG*yW#hgPUB0qJ<)XJNZ&=2*#_RuL+!nv@kxOePD&-L|QBrwo%j666ckg`gi0rwQF&E7!+XY>3xw zj6K;ms!x4H{woV%-u)B18#(vy8I4!xMPo>!NAPJ9U@cql95fX6^<3g zY|Sxavu1ar)CJR{!(pcPLzcE6S)Iu}0J&TDpmT&CL8HWcgGh8R=PXqIur@| z6v|d02gnVbUNp`YlXcuCF44YmRxA>iB&yqHd3auhC%#|Z^3*r@oLxqq{H}UZ4xzC)pzN%o!5FEh@;kPI|debC|$M zvZdT5qimNAtgUH>SuT?l@?(m}LMN;t>@Rx`MwDBk_l_(u}P zfFw-|r2LZMHLaH+~Mirk>tp$$&q1*ih&-Q8oz*U(6RS`vAii02ZkmD^RQiR z4txsOYL`8Tr7ZOm`j?P_z=9c=X)x@j;cx`Z>;@=ATR2ss;1FrePY*qgW8Hkv2E)ETd5D$hCl;oy6#A9-s!FxW(st_Vh zY<#{6j4m)NGcHEHI1R*L!Px1}m}mJ{ls0>0QP#@_7oXKWV|KW7%_iB%ex~~cFgY;( zWA3;mK;*4yuh@hS@L2+pTe_yTsxcjdQ+bciw^!HHyX`Emir$@m?{2lw>luVl40t5b z$Y2x{c?4t{@N++GS=A4Es6PaS7-z(2MeiUDW57b?A-<;xS&`i;#fXqGc)Ta(?vGh_!nk9R^Ywocd6Rk8NbD8E zg$vPUgyk@_wSpaxjkX$s0#Qr(Fa}uA$$XY$LGs1>GFX5>lZ$WG10wKez&r2GxaKZS z);B{`=6*0d>$2y~<1ibD=V3%DU=MxCoCL0X9w#8Z3J5xoPPF54Qd%hGtjPq<60Awl z)xvj+kkAGg)t-G^ANdJiOZqs#EJdk72atq-sNw(y&h{FbnnK~GzP@erX($Cvj`*U> zk)Y+b9#jOlCWP)mf}u4ASQhkBi`ROBLBH=Y4XW!X4O7()D5QYROk@pg(TLAaDMZpe zy;&I`RJY1 znM_Y`SFGhnI$rKb6qSz(r@UkJ?>3)qo~UU(zhV5zOZu37D@0SztUT`nEEW{rG?LQV zv1i9uUTTe(bi}M3-(9g)P1+jcw#GTa!(dE1S-E-`jO|*F@$0!2OJEFWCmrX!!|O4pS0pOmj!Q^bF2;UNEM zkqC620f*RP$gli=M}8KUWP)ITc!(#mAGl+HJXnsF{tvj#B%2wxWsToY=5ZGCeNF<- zg7c6_E$Js2d|PtyO~cR)!`}nol_1Ld!qn^6P|mOh;<^2-m>1kb&=uUUJOEV$sJabG zgE#2+grb9L)j~w3s&FjjRUo-=G@FX?I zkJrcSn^Udl5W?R9hV9j9%yOopKUnRvJUnkH7;QV)_M^5|@=0sy8WaDikpnvKOAD%h zhCJk^;}D1bAMYN5DOni$en|@&x>*F>i$6Ug>wkIkhK}1pjs5~wikHnKP3p1_K^d(F zlJ(hz?t^!=X5p=6%P71xWit@)J9YXyUl$0En0>3Es4T0R$^T{&`*> zeG!{3$#dj63gZN{Q+^5rzI)Zy4Vx0B)sv+S@zRFNrS}ZCCu~)t+dj9|CG1s`b}4R` z#!BCBo7HEz!W8XTj63gyC-a){tMbNmZSA0dn5JVY4UHX5xuMgg zwacz&3JT9n^ZVdzAby?Ie||lD6b5GzCS`*i$HszoG;A_f+JdD8`MfXc!FkXIqc0%Y zkK|<}46|14kQj7QmceC0m z7TsKA5R1TF(-+U_M6n8D#HpO-df^BT8(S8B8SjfSE7nbUC|3o4C_2D|sA|c4x%Vqn zz)Axer0|`Z9jGd-!=@-yv`>h5ip+Cl>&*Ges2U&CKEeN#o`QCWe}w`8>BVt3bUY_~ zV5;s hv(OIrixrUj7DA!RZ$Jcf`L~1IYOd-}1PD8X{{|$x7(W02 literal 0 HcmV?d00001 diff --git a/lib/air_runtime/__pycache__/xdb_capture.cpython-313.pyc b/lib/air_runtime/__pycache__/xdb_capture.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d2adf131bdfce664485609198915b3f4eb56e986 GIT binary patch literal 6828 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 a#G|WRm^;TkASfU84{=wy{~;*Z9R3f$-tcSy literal 0 HcmV?d00001 diff --git a/lib/air_runtime/contracts.py b/lib/air_runtime/contracts.py new file mode 100644 index 0000000..1506585 --- /dev/null +++ b/lib/air_runtime/contracts.py @@ -0,0 +1,109 @@ +""" +数据契约 — 保持 V1 契约完整性,新增 DeploymentRecord、AirRvr 审查报告等结构。 +""" + +from __future__ import annotations + +import re +from dataclasses import dataclass, field +from datetime import datetime, timezone +from typing import Any + + +def now_iso() -> str: + return datetime.now(timezone.utc).isoformat() + + +@dataclass +class WorkerResult: + task_id: str + status: str # done | blocked | failed + summary: str = "" + files_changed: list[str] = field(default_factory=list) + validations: list[dict] = field(default_factory=list) + document_updates: list[dict] = field(default_factory=list) + evidence: list[dict] = field(default_factory=list) + risks: list[str] = field(default_factory=list) + blockers: list[str] = field(default_factory=list) + deploy_required: bool = False + deploy_info: dict | None = None + + def validate_for_finalize(self, brief: dict | None = None) -> None: + if not re.fullmatch(r"[A-Za-z0-9_\-\.]+", self.task_id): + raise ValidationError(f"invalid task_id: {self.task_id!r}") + + if self.status not in ("done", "blocked", "failed"): + raise ValidationError(f"invalid status: {self.status}") + + if self.status == "done" and not self.validations and not self.files_changed: + raise ValidationError("done without validations or file changes") + + if self.deploy_required: + deploy_validations = [ + v for v in self.validations + if v.get("kind") in ("remote-deploy-verify", "remote-binary-md5") + ] + if not deploy_validations: + raise ValidationError("deploy_required but no deploy verification in validations") + + def to_dict(self) -> dict[str, Any]: + return { + "taskId": self.task_id, + "status": self.status, + "summary": self.summary, + "filesChanged": self.files_changed, + "validations": self.validations, + "documentUpdates": self.document_updates, + "evidence": self.evidence, + "risks": self.risks, + "blockers": self.blockers, + "deployRequired": self.deploy_required, + "deployInfo": self.deploy_info, + } + + @classmethod + def from_dict(cls, data: dict) -> WorkerResult: + return cls( + task_id=data.get("taskId", ""), + status=data.get("status", ""), + summary=data.get("summary", ""), + files_changed=data.get("filesChanged", []), + validations=data.get("validations", []), + document_updates=data.get("documentUpdates", []), + evidence=data.get("evidence", []), + risks=data.get("risks", []), + blockers=data.get("blockers", []), + deploy_required=data.get("deployRequired", False), + deploy_info=data.get("deployInfo"), + ) + + +@dataclass +class TaskRecord: + task_id: str + task: str = "" + files_dirs: str = "" + done_when: str = "" + status: str = "TODO" + validations: str = "" + adr: str = "" + + @property + def text_for_classification(self) -> str: + return f"{self.task} {self.files_dirs} {self.done_when}" + + +@dataclass +class DeploymentRecord: + task_id: str + host: str + binary_path: str + md5: str = "" + service_name: str = "" + service_status: str = "" + deploy_at: str = field(default_factory=now_iso) + smoke_test_passed: bool | None = None + + +class ValidationError(Exception): + pass diff --git a/lib/air_runtime/deploy_runtime.py b/lib/air_runtime/deploy_runtime.py new file mode 100644 index 0000000..1ff7524 --- /dev/null +++ b/lib/air_runtime/deploy_runtime.py @@ -0,0 +1,133 @@ +""" +AirDep 部署运行时 — V2 新增组件。 +SSH 远程构建 + 二进制传输 + systemd 生命周期管理 + 部署验证。 +""" + +from __future__ import annotations + +import hashlib +import subprocess +from dataclasses import dataclass +from pathlib import Path + +from air_runtime.contracts import DeploymentRecord, now_iso +from air_runtime.io import atomic_json_write, safe_json_load +from air_runtime.paths import dep_state_path + + +@dataclass +class DeployTarget: + host: str + user: str = "root" + port: int = 22 + build_dir: str = "/tmp/airdep-build" + deploy_dir: str = "/opt/app" + + +@dataclass +class DeployResult: + task_id: str + success: bool + binary_md5: str = "" + service_status: str = "" + error: str = "" + journal_excerpt: str = "" + + +def _ssh(target: DeployTarget, cmd: str, timeout: int = 120) -> subprocess.CompletedProcess[str]: + return subprocess.run( + ["ssh", "-o", "StrictHostKeyChecking=accept-new", "-p", str(target.port), + f"{target.user}@{target.host}", cmd], + capture_output=True, text=True, timeout=timeout, + ) + + +def _scp(target: DeployTarget, local: Path, remote: str, timeout: int = 300) -> subprocess.CompletedProcess[str]: + return subprocess.run( + ["scp", "-o", "StrictHostKeyChecking=accept-new", "-P", str(target.port), + str(local), f"{target.user}@{target.host}:{remote}"], + capture_output=True, text=True, timeout=timeout, + ) + + +def _md5_file(path: Path) -> str: + h = hashlib.md5() + with open(path, "rb") as f: + while chunk := f.read(8192): + h.update(chunk) + return h.hexdigest() + + +def deploy(task_id: str, project_root: Path, target: DeployTarget, + local_binary: Path, service_name: str, + build_cmd: str | None = None, + smoke_test_cmd: str | None = None) -> DeployResult: + """执行完整部署流程:构建 → 传输 → systemd → 验证。""" + state_file = dep_state_path(project_root) + + try: + # Step 1: 远程构建(可选) + if build_cmd: + result = _ssh(target, f"cd {target.build_dir} && {build_cmd}", timeout=600) + if result.returncode != 0: + return DeployResult(task_id=task_id, success=False, + error=f"build failed: {result.stderr[-500:]}") + + # Step 2: 二进制传输 + MD5 校验 + remote_tmp = f"/tmp/{local_binary.name}" + scp_result = _scp(target, local_binary, remote_tmp) + if scp_result.returncode != 0: + return DeployResult(task_id=task_id, success=False, + error=f"scp failed: {scp_result.stderr[-500:]}") + + local_md5 = _md5_file(local_binary) + md5_result = _ssh(target, f"md5sum {remote_tmp} | cut -d' ' -f1") + remote_md5 = md5_result.stdout.strip() + if local_md5 != remote_md5: + return DeployResult(task_id=task_id, success=False, + binary_md5=f"local={local_md5} remote={remote_md5}", + error="md5 mismatch after transfer") + + # Step 3: 部署二进制 + _ssh(target, f"mv {remote_tmp} {target.deploy_dir}/{local_binary.name}") + + # Step 4: systemd 生命周期 + _ssh(target, f"systemctl daemon-reload") + _ssh(target, f"systemctl restart {service_name}") + status_result = _ssh(target, f"systemctl is-active {service_name}") + service_status = status_result.stdout.strip() + + # Step 5: 冒烟验证(可选) + smoke_passed: bool | None = None + if smoke_test_cmd: + smoke_result = _ssh(target, smoke_test_cmd, timeout=60) + smoke_passed = (smoke_result.returncode == 0) + + # Step 6: 记录部署产物 + record = DeploymentRecord( + task_id=task_id, + host=target.host, + binary_path=str(target.deploy_dir / local_binary.name), + md5=local_md5, + service_name=service_name, + service_status=service_status, + smoke_test_passed=smoke_passed, + ) + + # 持久化 + sessions = safe_json_load(state_file) or {"sessions": []} + if isinstance(sessions, dict): + sessions.setdefault("sessions", []).append(record.__dict__) + atomic_json_write(state_file, sessions) + + return DeployResult( + task_id=task_id, + success=service_status == "active", + binary_md5=local_md5, + service_status=service_status, + ) + + except subprocess.TimeoutExpired as exc: + return DeployResult(task_id=task_id, success=False, error=f"timeout: {exc}") + except Exception as exc: + return DeployResult(task_id=task_id, success=False, error=str(exc)) diff --git a/lib/air_runtime/events.py b/lib/air_runtime/events.py new file mode 100644 index 0000000..1cc1543 --- /dev/null +++ b/lib/air_runtime/events.py @@ -0,0 +1,102 @@ +""" +事件日志模块 — V2 可观测性基础设施。 +JSONL 格式无限流式追加,与 state.json 互补:state.json 是当前快照,事件日志是完整时间线。 +""" + +from __future__ import annotations + +import contextlib +import json +import logging +import os +import tempfile +from datetime import datetime, timezone +from pathlib import Path + +logger = logging.getLogger(__name__) + +# 事件类型常量 +TASK_DISPATCHED = "task.dispatched" +TASK_COMPLETED = "task.completed" +TASK_BLOCKED = "task.blocked" +MERGE_STARTED = "merge.started" +MERGE_COMPLETED = "merge.completed" +REPAIR_CREATED = "repair.created" +REPAIR_RESOLVED = "repair.resolved" +INTERVENTION_STALL = "intervention.stall" +XDB_CAPTURED = "xdb.captured" +DEBUG_SESSION = "debug.session" +CONTEXT_COMPACTED = "context.compacted" +DEPLOY_COMPLETED = "deploy.completed" +TEST_RUN = "test.run" +SEC_SCAN = "sec.scan" +REVIEW_SESSION = "review.session" +ENGINE_CYCLE = "engine.cycle" +WORKER_TIMEOUT = "worker.timeout" +LOCK_ACQUIRED = "lock.acquired" +LOCK_RELEASED = "lock.released" +STALE_LOCK_CLEANED = "stale_lock.cleaned" + + +def now_iso() -> str: + return datetime.now(timezone.utc).isoformat() + + +class EventLog: + """结构化事件日志(JSONL 格式),支持轮转截断。""" + + MAX_LINES = 10000 + + def __init__(self, path: Path, max_lines: int = MAX_LINES): + self._path = path + self._max_lines = max_lines + self._pending_merge_complete = None + + def emit(self, event_type: str, payload: dict | None = None) -> None: + entry = { + "ts": now_iso(), + "type": event_type, + **(payload or {}), + } + self._path.parent.mkdir(parents=True, exist_ok=True) + with open(self._path, "a") as f: + f.write(json.dumps(entry, ensure_ascii=False) + "\n") + self._maybe_rotate() + + def _emit_with_completion(self, event_type: str, payload: dict) -> None: + """emit 并确保 MERGE_STARTED / MERGE_COMPLETED 成对。""" + self.emit(event_type, payload) + + # 自动补全配对事件 + if event_type == "merge.started": + self._pending_merge_complete = payload.get("taskId") + elif event_type == "merge.completed": + self._pending_merge_complete = None + + def _maybe_rotate(self) -> None: + """原子轮转:先写临时文件,再 os.replace。""" + if not self._path.exists(): + return + try: + with open(self._path, "r", encoding="utf-8", errors="replace") as f: + lines = f.readlines() + if len(lines) <= self._max_lines: + return + + keep_count = self._max_lines // 2 + kept_lines = lines[-keep_count:] if len(lines) > keep_count else lines + + fd, tmp_path = tempfile.mkstemp(dir=self._path.parent, suffix=".tmp") + os.close(fd) + + try: + with open(tmp_path, "w", encoding="utf-8") as f: + f.writelines(kept_lines) + os.replace(tmp_path, self._path) + logger.info("rotated event log %s, kept %d/%d lines", self._path, len(kept_lines), len(lines)) + except Exception: + with contextlib.suppress(Exception): + os.unlink(tmp_path) + raise + except OSError: + pass diff --git a/lib/air_runtime/evidence_gate.py b/lib/air_runtime/evidence_gate.py new file mode 100644 index 0000000..4201124 --- /dev/null +++ b/lib/air_runtime/evidence_gate.py @@ -0,0 +1,74 @@ +""" +任务类型感知的证据门控 — V2 P0-1 修复。 +替代 V1 无差别触发 AirXDB 的逻辑,根据任务特征差异化要求证据类型。 +""" + +from __future__ import annotations + +import enum +from dataclasses import dataclass + + +class EvidenceClass(enum.Enum): + GUI_REQUIRED = "gui_required" # 需要截图/GUI 操作验证 + NETWORK_REQUIRED = "network_required" # 需要抓包/流量验证 + STATIC_ANALYSIS = "static_analysis" # 需要静态分析 + CODE_ONLY = "code_only" # 代码级验证即可 + + +@dataclass +class EvidenceGatePolicy: + """基于任务特征的差异化证据要求。 + + 关键词匹配 → 确定性分类,零额外 API 调用,可测试可覆盖。 + 用户可在 todo.md 中用 [no-xdb] / [no-ndb] 标记显式跳过。 + """ + + GUI_INDICATORS: tuple[str, ...] = ( + "gui", "ui", "render", "layout", "dialog", "osd", + "overlay", "visual", "screenshot", "display", + "widget", "pane", "toolbar", "settings_dialog", + "canvas", "button", "window", "popup", "menu", + "drm", "kms", "opengl", "vulkan", + ) + + NETWORK_INDICATORS: tuple[str, ...] = ( + "network", "rtsp", "http", "tcp", "udp", "tls", + "dns", "proxy", "socket", "stream", "port", + "packet", "pcap", "bandwidth", "latency", + ) + + STATIC_ANALYSIS_INDICATORS: tuple[str, ...] = ( + "cppcheck", "clang-tidy", "mypy", "ruff", "lint", + "static analysis", "compile_commands", + ) + + SKIP_MARKERS: tuple[str, ...] = ("[no-xdb]", "[no-ndb]", "[no-sdb]") + + def classify(self, task_text: str, task_id: str = "", skip_overrides: list[str] | None = None) -> EvidenceClass: + text = task_text.lower() + skips = set(skip_overrides or []) + for marker in self.SKIP_MARKERS: + if marker in text: + skips.add(marker) + + has_gui = any(kw in text for kw in self.GUI_INDICATORS) + has_net = any(kw in text for kw in self.NETWORK_INDICATORS) + has_static = any(kw in text for kw in self.STATIC_ANALYSIS_INDICATORS) + + if has_gui and "[no-xdb]" not in skips: + return EvidenceClass.GUI_REQUIRED + if has_net and "[no-ndb]" not in skips: + return EvidenceClass.NETWORK_REQUIRED + if has_static and "[no-sdb]" not in skips: + return EvidenceClass.STATIC_ANALYSIS + return EvidenceClass.CODE_ONLY + + def required_evidence_for(self, evidence_class: EvidenceClass) -> list[str]: + evidence_map = { + EvidenceClass.GUI_REQUIRED: ["screenshot", "gui-operation-validation"], + EvidenceClass.NETWORK_REQUIRED: ["packet-capture", "connectivity-verification"], + EvidenceClass.STATIC_ANALYSIS: ["static-analysis-report"], + EvidenceClass.CODE_ONLY: ["code-review", "test-results"], + } + return evidence_map.get(evidence_class, ["code-review"]) diff --git a/lib/air_runtime/installer.py b/lib/air_runtime/installer.py new file mode 100644 index 0000000..f39cbfa --- /dev/null +++ b/lib/air_runtime/installer.py @@ -0,0 +1,121 @@ +from __future__ import annotations +import json +import shutil +from pathlib import Path + + +def resolve_plugin_paths() -> dict[str, Path]: + """ + 解析插件的实际安装路径。 + 顺序: 1) 环境变量 AIRPLAN_HOME > 2) ~/.airplan > 3) 相对脚本位置推测 + """ + # 1. 环境变量 + if "AIRPLAN_HOME" in __import__("os").environ: + return {"root": Path(__import__("os").environ["AIRPLAN_HOME"])} + + # 2. ~/.airplan 默认 + home = Path.home() + default = home / ".airplan" + if default.exists(): + return {"root": default} + + # 3. 尝试从当前脚本位置推测 + # 脚本位于 {root}/scripts/airplan.py + import sys + script_root = Path(sys.argv[0]).resolve().parent if sys.argv else None + if script_root and (script_root.name == "scripts"): + plugin_root = script_root.parent + if (plugin_root / ".claude-plugin").exists(): + return {"root": plugin_root} + + # 4. 从本模块位置推测 + # 本模块位于 {root}/lib/air_runtime/installer.py + module_root = Path(__file__).resolve().parent.parent.parent # lib/air_runtime/installer -> lib/air_runtime -> lib -> root + if (module_root / ".claude-plugin").exists(): + return {"root": module_root} + + raise FileNotFoundError("Cannot locate plugin installation directory") + + +def get_plugin_meta() -> dict: + """读取 plugin.json 元数据""" + paths = resolve_plugin_paths() + meta_path = paths["root"] / ".claude-plugin" / "plugin.json" + if not meta_path.exists(): + raise FileNotFoundError(f"plugin.json not found at {meta_path}") + return json.loads(meta_path.read_text()) + + +def post_install_verify() -> dict: + """ + 安装后验证:检查所有依赖工具是否存在。 + 返回 {tool: bool} 映射,False 表示缺失。 + """ + required_tools = [ + "git", # 版本控制 + "cmake", # 构建 + "ffmpeg", # XDB 截图 + "xvfb-run", # XDB 虚拟显示 + ] + + result = {"ok": True, "missing": []} + for tool in required_tools: + found = shutil.which(tool) is not None + result[tool] = found + if not found: + result["ok"] = False + result["missing"].append(tool) + + # 检查 Python 依赖 + try: + import yaml + result["pyyaml"] = True + except ImportError: + result["pyyaml"] = False + result["ok"] = False + result["missing"].append("pyyaml") + + # 检查目录结构 + paths = resolve_plugin_paths() + for subdir in ["lib", "scripts", "skills", "commands"]: + p = paths["root"] / subdir + result[f"dir_{subdir}"] = p.exists() + if not p.exists(): + result["ok"] = False + result["missing"].append(f"dir:{subdir}") + + return result + + +def verify_plugin_json_paths() -> dict: + """ + 验证 plugin.json 里的路径是否可解析。 + 设计原文 P0-9 指出硬编码 $HOME 是问题,这里修复它。 + """ + meta = get_plugin_meta() + issues = [] + + # 检查 scripts 路径 + entry = meta.get("entry", {}) + if isinstance(entry, dict): + script_path_str = entry.get("args", [""])[0] if entry.get("args") else "" + else: + script_path_str = str(entry) + if "$HOME" in script_path_str: + # 尝试解析 + resolved = script_path_str.replace("$HOME", str(Path.home())) + if not Path(resolved).exists(): + issues.append(f"script path not found: {resolved}") + else: + # 修复:改用相对路径或 AIRPLAN_HOME 变量 + issues.append(f"script uses $HOME: {script_path_str} (should use relative path)") + + # 检查 marketplace 路径 + market_path = meta.get("marketplace", "") + if "$HOME" in market_path: + issues.append(f"marketplace uses $HOME: {market_path}") + + return { + "issues": issues, + "resolved_paths": resolve_plugin_paths(), + } diff --git a/lib/air_runtime/io.py b/lib/air_runtime/io.py new file mode 100644 index 0000000..fbbd693 --- /dev/null +++ b/lib/air_runtime/io.py @@ -0,0 +1,53 @@ +""" +原子 I/O 模块 — 消除 V1 5 份 _json_dump/_json_load 重复。 +每次写入使用 tempfile + os.replace() 保证原子性,写入前自动备份。 +""" + +from __future__ import annotations + +import contextlib +import json +import logging +import os +import tempfile +from pathlib import Path + +logger = logging.getLogger(__name__) + + +def atomic_json_write(path: Path, data: dict | list, indent: int = 2) -> None: + """POSIX 原子写入:tempfile + os.replace()。写入前自动备份旧文件为 .bak(单级轮转)。""" + path.parent.mkdir(parents=True, exist_ok=True) + + # 备份旧文件 + if path.exists(): + bak = path.with_suffix(path.suffix + ".bak") + try: + os.replace(str(path), str(bak)) + except OSError as exc: + logger.warning("backup %s -> %s failed: %s", path, bak, exc) + + fd, tmp = tempfile.mkstemp(dir=path.parent, suffix=".tmp") + try: + os.write(fd, json.dumps(data, indent=indent, 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 | list | None: + """安全加载:处理损坏文件,自动从 .bak 恢复。文件不存在返回 None。""" + try: + return json.loads(path.read_text("utf-8")) + except FileNotFoundError: + return None + except (json.JSONDecodeError, UnicodeDecodeError): + bak = path.with_suffix(path.suffix + ".bak") + if bak.exists(): + logger.warning("corrupt %s, restoring from %s", path, bak) + return json.loads(bak.read_text("utf-8")) + logger.error("corrupt %s with no backup", path) + return None diff --git a/lib/air_runtime/lock.py b/lib/air_runtime/lock.py new file mode 100644 index 0000000..a90185d --- /dev/null +++ b/lib/air_runtime/lock.py @@ -0,0 +1,44 @@ +""" +文件级并发控制 — 解决 V1 P0-4 零并发控制问题。 +基于 fcntl.flock 的进程级文件锁,超时自动释放。 +""" + +from __future__ import annotations + +import fcntl +import os +import time +from pathlib import Path + + +class FileLock: + """基于 fcntl.flock(LOCK_EX | LOCK_NB) 的进程级文件锁""" + + def __init__(self, path: Path, timeout: float = 10.0): + self._path = path.with_suffix(path.suffix + ".lock") if not path.suffix.endswith(".lock") else path + self._timeout = timeout + self._fd: int | None = None + + def __enter__(self) -> FileLock: + 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: + os.close(self._fd) + self._fd = None + raise TimeoutError(f"lock timeout after {self._timeout}s: {self._path}") + time.sleep(0.1) + + def __exit__(self, *exc) -> None: + if self._fd is not None: + fcntl.flock(self._fd, fcntl.LOCK_UN) + os.close(self._fd) + self._fd = None + + @property + def path(self) -> Path: + return self._path diff --git a/lib/air_runtime/modes/__init__.py b/lib/air_runtime/modes/__init__.py new file mode 100644 index 0000000..fa3c9fb --- /dev/null +++ b/lib/air_runtime/modes/__init__.py @@ -0,0 +1,11 @@ +"""模式模块 init""" + +from air_runtime.modes import arc_mode, eng_mode, do_mode, dbg_mode +from air_runtime.modes import xdb_mode, sdb_mode, ndb_mode +from air_runtime.modes import ctx_mode, dep_mode, tst_mode, sec_mode, rvr_mode + +__all__ = [ + "arc_mode", "eng_mode", "do_mode", "dbg_mode", + "xdb_mode", "sdb_mode", "ndb_mode", + "ctx_mode", "dep_mode", "tst_mode", "sec_mode", "rvr_mode", +] diff --git a/lib/air_runtime/modes/__pycache__/__init__.cpython-313.pyc b/lib/air_runtime/modes/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6d0e0ecc2ae89c9d71acd4702dc7c4cd93af2ee2 GIT binary patch literal 550 zcmbu6ze~eF6vtnFHEEMniinfYPLieBi@0<-{|B3Kd5kBWMNY2~SQ(^5%ZDH&)iV<}<*6TL8jXqo literal 0 HcmV?d00001 diff --git a/lib/air_runtime/modes/__pycache__/arc_mode.cpython-313.pyc b/lib/air_runtime/modes/__pycache__/arc_mode.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4e5489604320644aabd29ef033a8a15350eade86 GIT binary patch literal 13022 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 P>0N&e(dG0GGW>r5E6&%w literal 0 HcmV?d00001 diff --git a/lib/air_runtime/modes/__pycache__/ctx_mode.cpython-313.pyc b/lib/air_runtime/modes/__pycache__/ctx_mode.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e1d73f47d4205e46e24e48843d2569d4e31b52ba GIT binary patch literal 11416 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 literal 0 HcmV?d00001 diff --git a/lib/air_runtime/modes/__pycache__/dep_mode.cpython-313.pyc b/lib/air_runtime/modes/__pycache__/dep_mode.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0dbcdf8f26b37d074bb19c037be0e4517e257eb4 GIT binary patch literal 2485 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 wzFh|q! z$t{><=DiE9u3NY2)T!;%sb8Hcy=O3JDR{nfrRKilcT?1F@I`!Nf=BY+*CiD7MT({v zik8sQK1r{Xk-E_@>yt4u@+)WL{T+#UKOJv@f3aPUJaumer2DwSI6jj z^^BgptNIMRM#f0|>ONDinKAcTn7m#qW9_vuwq849?{zQ^@?F#C>~%4&-h3v%w}2_= zEo2INivQ`uX^RFQXG--h05rn?tL?30>WJUaSKr&fG!VbBud%m@Y3kj?Z0g<2Y=$rsvxPP@TWJf^ zOy@D%Xg6&wpzf11+vzgeMtnQya@tOOJLwAAL3}NACG8}>T@dCXzE--5&L_S$dIMce z7oh(>39}nw6q4{gbPZiZe0ymRT}*uK^hUatE=k4RN7vD%B)nrx=Bb}P1FfcQ4>4Wg zG53jy(V+X+vroD2+U&l*_^GQeef{doU%d9@7k)GQN$tT+?khj|>f5nL-+t-Rl~?}u zoiCsN?aN<&`{E04zx*W#y#8Olas8WLx%v;!zx{=8{$}>k!xPN?LB@UkyMKM{>F>h( z_3wQ8+Dr3Szwp@A&pmVPtFza>^B4qPc?n)GUVZ#aSLf%hejlQJQu_{eA&-RD1;XKp zslZfdA{-g;NO|>WU@ABjIuYd6=*~=pgAhx8M_}qWe6<9oCQgJ#{P#sB!v0fCXe!7X zBY|TWV2 zfxLr4kTG~N7@qQvPmBrg8j_I2)&<7kQ-p+jbiBF?V;-Cs<4uF@bf3SohrXkIu;V~i zCvPMH9f$hwIM_7^?|SmS@8F@1K6o?qcX_+J{PbYE7eAVW*G%lZo`JzG?_FI3gFS}^ zFxtU`yuNFoyJw)w-*KelU>9#Ze8}6^<@FEt^miTd4e~~B*B$LWUVn$TtDTTcy?eXp zLkI8b>V!(m>pB@44TeX8-B8p!CdNY}r+I@I)-fK4L_Bid5ty7DKkXl#7@0nSwJ<%2 z4H4msgW+Qn%t#Qw_#>yoBYr0Mz;uWSVyp=7B0;ITJP`;(J&j`22=5dq_(_!qj!gv_ z|A`(v7m`Kn+1wj#yj8}yxPWeNT3EmJ1Mk3HFu!T-c^5!XK zIy{1HbvzWAnqW?c+(i`i4oad>9Y~>f;2$+2#a@+ql<*6mCGTw&gcJ%Ws#g*Q0p+7; zS-p;;XgP5eK8caqB1L~#=9769B8kjzbIMzj`W=-+Y9*~|l2lSYX*p6<@05h)KDk$w zNhSJ*tj#IT!U~_ls}{pEzarg$gQ9>2aI*Hv&;UWBYNTjQgR~MwtrkS8Dy)Q3Da)xL zNjfJLt@BABH}hIbkb}x=y*-rHH)AEjLmL|8m6XRg{X2Nk;xNAOch@e=yRnhnZ-4pI z?o%-8rSeJ%RUtMgxf=ltyTuRrQ0?d!h!%(GXPesFc}GuNI10r5%g)vtf@+Oy}y zk6(N7`nUh?w=d6)OoWd?Pn?SU_T@9befjb0pMC!7=N7Jh{<9!+uFfsGK}>ZX9V5v< z^ZBbJ(bc(g@0|NQ%A}>2t}K1)`o+iJ`RwPfow)=ajCcK&ry%DR_s7d|NKZ$~TioRn z_m{gr;Vz$^9Ag5b&@rbb{2&iTkB*Vx(TP*x%!f#j2?fT>>)Z_u4G(D7lnprFL+?-^y9fE=?0`P1??$lDJi17&4{$P(kh43)tLVWdQO zEom12(ZI<4yf!jDG6Li3*z`EB8W|4;!qby{K{gGX7$OZ&MSk=#n=miTke7`Gr#xz2 zKgmqo7aW-q1gZ*I@`px+995^L1j#8qHN`8LU}SoHicsKHslr6C>$_7AKh}7B;zY0! zvLBxakBx=`;l|FuRG>3>G6YOEwucz3eR?u5LL${qj!%Qkj?|Arx$x^9AlUEP+&CUO ziZsM(Xe32w1ervN&@g$LFB~}@9J${w){h{Fi0Ge=Fde{BHT;V_56&$0J0qpoe%V+M zy^A$g#O`N}n-)ixwl8_&TRT}}=WKUUmv?sencWFpA*(A)nqAS#xT!R*F1@Lww1u&I zSk30yeMy7q?7e61jc(@*b+h{~s|*R13;w^X)}Ga#(avvQR#zqqN*27VsVS~*`kj(e z?EE)lVe}wp+&tTTLrGbylX>|`dtt&}#oDWq_M)eUpBRo6a`yV08iihYgHkAz?*X^e zNNT{MpGph7gYfc2YB>GPN2Le0ga|xA)JZ@*fTo5a@JmD?{3=rKN)l7mpa4Oj9^e(& zy@R|GDjI5bMoV5Ba2|#!I~0Lga^yI#3NRzbLnnj00;kT1N5RW+-evX!L%d#$<{u3) zFz*Xd$%~rIv=Ho(FsOCHm}!L{rVpG5jzqVc*#|$olJu_#a|m8~@gs)LQY$)VRL|+E z7F4XRaj}Ngwaj*2HrtXGdsNR_DwD3F=nU)fB%OuP5bLZ?+Fa4?PYm4DDX}h<3MJDC zWOCPK3w!|$S7N0O*kO8*3`uZy@TTh)UQ!P&h5uwbVS4FzQk0HzQ)84zKH!lt*d9zj zIs@Pg!j!CnNsr8*YBEUuL}n;X+>ccth1vrRCBn<@51w9Cdc8jwj!ZK!7l)V#a~II^ z!M{iuIJ49*Of7S=q}p&sn^3!0wJT;@l*iRBPTl;jj56=Ky@*)RWPbc9c)mzA;anp1 zLx;$89C6lzQ28*#6`o9Yf^Y?`1Xo3?!PO8~+em?5M_fH|4a7ClreV2Aqe%jB%){7a z$pdc|k~)vJg5O5`cH%lPbt45co)aqAMdvq2k<%s#T>x)|4N~|bycad7yI?-{6c6yK zU^sAeJUBYYYXm)Hw0#Ox4MBE!Q5Vz-N=rXTIbI$b9S?%8I5GvD_i*53uxFH~_&njg zGu6BAK(*13ibJ&C2yY;=b<|5lU4*wy2E(JF@L2cw#L>XGFQr0RLt)TdJAZ6MjR($NJ39cgHFb4!@&oq z+JS2rg>wAHYeAg@#dKm4ikpXifu4{zR>6a#M`=N^ijX;u*#Pwq<826jc-@J>gY8pO z!4s2Hk^QJSrgXi1LC^t%{iwe2CWzw=PJ$fpPD}%bFeeyBgGZ;wM6$dQ=05l=5(1^2 z)T@xF0}@T~dLc@xl&Obp8;u zhOii5J`Ulq;-w-Eh%rmukQV5bNwsBu>kS$BE~~9i9e(o24F&i?RBIehMWU^ov*v~h z1JsnpK7Z_n2K`z}syX@R}BOhN1C{2E}{6-=Ai-i1((O-i8qc=*? z?-ueeLw`9Wo@Z`UpudtTC`!Wr#U;tgnk5VC+Ld&a#%yu>hGgT`*WtY{nO~T6mBqHk zoi*=n!6Z~cE`f&QIkv7rh4Pwm9FD(^g040+cF6JYCYofM-hV79JkojxICX(q$NAm_cou!7O=s@I+{ec@VtNSIKlXh~LOuD1$|21cf20Lc-LhCv-u=Plg4%Bae4Xe^8&I(S`tM@lHT+56?%j9lds8y*@t1vs_T+X0gNNQ1>`mwH7O@62{gIX_E(4+AElNhH8 zoK+R1%a2yYROcEvohPB&|HAN65m&!IF5fSd6txNQ!9GMv`dtWJr=-%XKAYWZa`ag` zOSux%D(ruPVg)NhnV?#QVa5zAo2alV_gYb$bH!Q=)KXc!-6t38Ijr`n({b`4PG%ga zhxK9+#SxQ^39ai>hgCGJ7=0=+bQHR0_BcrAq48;~)Lq&L%&`$Es6cLtns`3fijr2h zKzA91dhEAB4q9)X$U)}!4rzb6pcqA@VbBtA1q-zxrL|a@!=PRXk5~gniq@r8<+@=| zsf1_fdt#Z~+4TZC0a6h;zpWQS4GB+1N`&=8SoBUFCjm1iLEh9K3yhEB3US5` zGdIwwU!9tl4<70~1R4RLGUKrN1H}pmML;15Fpx4h-px!*PezD-`$q*K&5S{rnFHt? zN9VKX%z*=_9MShtpAsVBCq8c#DM3vzAiD%8BFYy@)(cAxk2y>Bq8`g^Lr0Btw4n&B z5vKxxck%L3Ko}V4bQD=ZGi~UrL`Q`VPV_`TGrPdY%f^FYk3kUCIG7leL1du5< zTo^Ce!kV|t_9Pu;@yce_(L8(TvZH$TP}1R=?YXQnCsa;W-JE`7Lf`$u{Y!ORL-%Y~vZOw)Enww^$%4w+uDLoECc#xx z-z_C&cSSW%4kQaa$^7yQ#pjA+k1R>K{9P+0WiijC`iu4ProFG0bLCxJ$^Mo6qNtzC zuTR>FZyHpn*QgZA58lnEN(Lkml%KDetX&TIYa7hqt^yXZZ;)x@=OA(Z;%Vcf1QzyC z@?;4t87<8yJ{d8&MH-f8#Q%`Yn@i=94P%!hk0_SJUj!vqsl6z755f9kEs z#1nmDeWz;*Cp##oPyb=DVbX&M9MJ#}*m}RT{{i7GYzXVZdY?h07GQyrs+IiykW1IDrj? zKXW|N<_1OB3JQn?YuP75(*+G#3%q_9#n=~fmPazw<+IX-SUV`A!p^KxKp?1OPN5t= zXIfd(9~0CjSy&&|rTo&cIk(SKG>v`t+PDAV>e(mAZU^jfzWsww!%oH3`MVs zDKQH`cA3A$(w#@=S#V%`15$%t*gnNvz`%p(oI&Sabo}6eZaPiSD`uFyr4TTp9`a@p z9qtcK1yE;#h2U09N%BFGp0X^ZPmP~Gjt;7GycM=mP%i~VEiigo*i0l!9W#S*=F!1H z%EZvQ2o7(6{S0AqauO9$q9kH(=C#LQ`#3@{RzSTc$6>Pvz;7lr35`f98@5sq>{g7E zik4#~^JH(v4~iZtc>bfjoIbRF5E^v?=zt1}(+Mv-5ef^6C$8)>NCEQ@zTyn)aS`p4 z*OLC>4?_nbN~i!DsIaTRW((|$Vngl4HD}vyy#t zs4&L zoB#kTZmgc|UNM!&TECb7V*XOwa__y|rjKzZ|LlQlI&0EhzF3efEMBtQlu693Hz#a0G@imh=@lhJr#x5rU6@+3wI*uY*xI(Gk>%Qbob!&K)$WT0elqsMv7dyV z(L^N|ROeLBYS`L+EPT@6@ut1th5``5n-mcKy_V7!zUi#zoQ-n_k}h|`RnNNW7bZB@ z?zx_St8U^7y5@=ezs|Uys`V$>DTXx-#z?K{zU%>+dmRNI2tbrf~sDX2O9cK zE!D7{D;s+L$bw|4?3L>8SHF61qU$gV|F;~zRQBAFd1Jh6=q+p6<@!x;?&;?|11~+i zRJKs|P0w?lZ`6MOVb(JMq&$wetqSl@YeCXlxMDAQ^2kbg)z?NAYQB16(f7S0FCJNX zXn7#OH6P{5M__l$rHspM5Zzu7ZD4Jlg;NV5SR&N5v9`9^e$@9*#H*T@<=ZY7Rm?T8 z@_Zl`JHeH2o$ZQiOIUenvY;ZhILP?kQn`|q&2gifRhO+^GF(}Wmi@xy#61+6>^Qa4-P`EzyGP1r$k-BMGqWa#Hy8R7;-BJRg?&q+XR6p82H#EE08vu#A?4xmasYE(iRC2&nV9b%5 zBjxQ7)mjegfj_Tv4O{RkDu`hK)P_+gk_XpMfg#jayoF*|<`=*o7140iu(76Xkcr&8 zK_jGf^`KM2s;^j#p9L|cK`=1|uCa2hfQE6>BafKZa8)`ay+Wh)tMsy81HWo7DkEzi zv5aYb@^KOq)>CUAF=npyLs&PAniqMzX`dKEQJEEzPlvTVoL(1@+5|B9BWlw?8~-2G zW|PPztrF|ADQ%A9hMwO)i|gh7Pl29&IA@d>SYj!PLn{ zYy<4*;wtpt(;EOM038tGrq>P`7R3HQq(d*z#8oWnVrw2Tp4h_(5&$CyV((rP6<)pV zw};UVv4&UCfV^_kfK6$qoH}6}?htADY+EJ7MpKQ_Oyo$U8n$Guo1y;t?}HxNl}<|y z+C+&F&hzEbCTtrzpR_gL0=awcpba*<3w?PxdrVPA)~*wySmcc^X-H!?u&(+KNe;Rc zxLS{=Opb1lT2Kr4fK|i|D4|&9vh>>h|9nWf%jt@BN|jY^y0Rg?De|9Et_r#;TjIf} z>rbsk?Lt{H@*LLa8&DL&Z#A~1w_A*n`SmHq7H~rg!;1KM=;2nMHD`O)h;ivc(koyz ztP?X`%VJLsfE<7sxhIpe=%YQEUXpXBvC_rJJ*=sP8K>a(nU3C=UCZn6l;OT2-M**S z<*N20xe~Z3r|WX?Q=iRG{|l)W>J!T>ZsxAqH2x3u`Qd{i?V+w=+_WT5rgiCt?B2eP zZq%63YO-jg=XRgMC(jB`_nhH7#r)`|0xFl}@fopi-DORqf%PBK%Qj`W$SEZ^i@i|n zTSU?#4rm2btWl_Iz%#8rnHVDg=n=hzZf?*3V!T!OEu)5ahXr>u z9;4c1rPMJ=L`twxD{N+MoAyHdZ2NP;+C1C-T(CSBU>7&q2O=`_x$j<^eH6f%D@&ih z@(+J|{X1WBA3WIa2BXsJm(GB3C|N%bf2wI+=wZDbqjWAF(qE++%0!UV#= zDRn0A6l`tPqh%%RhK&S+0LaW(VecQ7@sO@Q5FV{3>txu&#`w(FkcbkET_(aQw1#}( zwO|!3+ViMjYYH~ZMrUkH5Dggtd>A=iKfws31Vc~xC|JF}0~JcWgVJ+9vk}Qd^-~OL zL<6gIeT)W(q~tvi71VA?1xVU2f?XKM8PGE)EIBRJ(i-BRwB-Orous~~0Qp=XwCk8; zyX=_cIRSndcuqoa6tK*eHZ<{)Q)CY<0*eb+*Ut(|cugx9#|IyrWOnmqDNHA|ZPm~U zI6`1Nvb!N2rCtJydc^ks@D4Rg#Y^ee29_SUq`0Vv9sjCnar@8REw2rH@E#WGQ|<~0 zgIdmvQ-q`HiMdt1h)2w%?PlwzChB4163kY!tYa0KW?}vb=wv=+C^is^4I)LcDM@t1 zd3jqdE8&@{bvQ~f0o$p7Awe5w5+jF#i7{A%q`OOoFA?q{Ip2rej-W$cC&34)kRPE# zsQ``|8w)bfWzmk9bXm|Js2QD*MLpQ3Oo;g;({$cMIx=3L>Vj!gEP%`gCj{#*OSY{V zi9o=*+&vBlYgT^)>H1Oqjh3Y6g?x|~(p1kYP4zq(_B`MlWEY@?DG-Vbr2&RT=B3Qf z;D_PB$u>iop@W%iypCgEKcT zo1Pls4aXQb9RrZv1al&g0v!!gaJpt3&_3wm02ta*$4C0xhx~`(bO|{}0zzvNEf?29 zk|02UGV}d%+f?YcHm7QhL*} zr3y^5lluIG-p%UW3;P#$uyw8R1NS8B+5vkl?~vT0B!*7OWqp41PF7zs*T0fiaDMkf z9ap%W%iA&6k<^>c9yoL0sgErx7awGs_AcK+#|IB{cMP#rM}Drq8!|DLCJa@qp(iiqv!_3I`mv8keXOZ`xvGsbwI$8P33DZDu1wK= zy(ihUA2`}@06DVsNG_X;V;0t2K5tlYl$?KfVVo=7#W`B%Wl6L3Y4sE8<^0-(<4ek= z2iYA5;`KcVbML!4%Dfv4JPn1HS}wLM9Oo*wFKy#0+Rp`EeSp;;cx>QiJ!LJsv00%g zUolzEKJ>YV9(yGE0Bb6nldsquPapi7gHH}5Y}K*AoF-Z1UU=YQ8(RcN1oFXb!c>?! zkpQO`_P$(&OY+kBe89lc3s?+sMyI? z>|ENAsAva9k_GMywdZPMf!NV#Iq61E!FR+B9EB+GXt%eN(~YA@Y=@$N<4l9#L6n||Y} zTEV=2qvIB(v^#GZsltk{?Ov?oD)w*%d*?fnHs{j^o;VOM-bUzTh+s06e5d30l-%xw zLOY702jB!m+_UEm`<~UA_aw`Aq_ZQL1A2@ZDj-AshiAA0aOs_n_bHj(d9#gj*4)}J zp$f~N@rcN=Toc!pvT}DqUd_s@7Yu+m|HfE2Cr#=MXSbc%HebW(3ge|)INg>d=_|_b zE0;c=*wM%C=u7G?34IZ(2g{O$qnv(ILcfF6??@Jx#&qZWU}hT~I#-u0+`3X+`OFdE zw#5kC8ccw8>y24-o8QnirzypY>lYYKzd51b$?A8eY3xfDREuPG8GnytZdxf*(XA3n z;m9%vT?25Z%t0T&XQ6*(bMxW}ZgVGe_@e#T;Q?NP4G?G1)1fCqF+K3_*tAmYj+Jo5 zP08Zgq^kfdZ?dykS-+%5T*VWB)(?YVUF;z<}7 zQm%SeqIy4Dy+7W4I8iyWVQ zT`IQbD73{oe5Vb9#JGX~392=mw&{c8@?kyHi5}0ViZvHD_zbxWiw0fk7{;s?GOpi3 z0qkMMocx<#!z>6A(|H8Y0)S?n-j+678g$Wl>(I4k+K9Gg!!21o#Xjf)gBiz1 zQ-CwSY2Yv@0S=vn!+zjU>-C7On)#(&S%4L;#@9T+w{x9b^D`-j>&0ZvDGwOFr1^jm z1n`RR0H`QfAZx|Cgs+>#uVQKd%cX)T3@LY!&kUt6-a?M^g)LdsEWWI9k}+b+jGk~_ z#*k@9?aOP%RX#ib^C-yJ-Y&eu_62+e<0D*yz~iRq68tZV%ZwEGOGb~)XQN9AoSL(i zVGHfHI?R$!7O@`_lP`N35CeG+9}iw z^rTct-~x#9ol<-waIr1tsHw>G65zp+<{aP>FrjkfOp}K62VE_vQpS-0lyQQlMCai|GKs5bIFfn<)##_GG6& zoV)!tX4(%-(wu2|48Tp+29gUlkZR#t4z62Fha68D(YO_hRtfz4CtK=6)yo-D}_Y&b9A6dTsIBZ@=^kpv>1EfAreL%HO>gj`^z)A(@z-N`+PK6_CG=L6j8U$7=yA09*r6=$X7w_+(%_ zH0l<@+<_@KfeDytNWg@_+0Vm~NOJv#mz+otuPHDHuU_FI0A6(xL6(szI1~!Ed+>6E zBQdAR35Mh(!kk8^V?2wAha2E{CVZXQKmbv93b}G0J?-|MWZWrwZAi!s@REDR+5%i+ zI{3ka$n1<6L8cJm-+}RZ%$hj~O!I0e4>`suLXwy-W5vwXBs;VkglQLS)ar@N-~bbV zH;+=NE)@2Fp$q55$vNJXA1e*cXTZfJq2OpIUOJ-1>qrFuJEKSfZeoD*s(wJ7PDIEC z0I$PSu>@>up8_*R0>xquj1&1WWvB^=oiR;OK=*!%R8yvjm;(H7A%98%@)bW3iU`T( zMo#~8v3e$s{1bpwMgUS?!rYG26Ofb_+Bb!afw?4hz8o<9FQ8~yFntOA+McDrh3RwC zv7u)kS*-qf>CV^oe8Bu1^Uc7d+3l4lwwE{EzZn-0W%AKE4hcOKS@xW`j3LJ+8C)tc zFQB7F=L_hZMTcOY524S7&V%6a+Pebd(?J3)G9?&_DCz18ErebwUJk(+AbZ}1wx6j! z?T7A63`Q9goXj84c?TVsYf|TgD$$2~hYafaj2oRQbYL-)GA;#G7aaiGDb>i`h#*_kLm>R7DC<$ZwB;%hX zSNaGglR*Hl7mF6cyL-qW$Lky579D-6RR}Y1U4t-mgkeL%B4n$KmrN3joj1YYhJ6)( zAay8Q=v7!zxxh*cO97TvGOky5GiDn$J^YJ253+wXYMDCKx(2RTv82v&E>^OZ?X$fr zX4m=R*eAIBtxKbL*Ba{;IP#?9EOl_?$z(r&_k!}< z@oR}?SZv{<`X953F(4GN;q6cC}Ig4jb3Ea6h#DWXm7sH&hIpOraT=?oe+)nSDK4~u#?)Hd<zD7kJJB}GwhhmZCmb7C$A;MH#SP1jo$drrTi&X3kDD3{fZQ1w>NLJ^m@ zdrk#@N5jJStKFRa;GFhdfQ#YubVckSq-bRzlE=ydGxxraG(L&Dtm?b@Yr zZgU^xr#8$@uyv^#KU%lb#K;f;R?4t-yM74d`C?3?EdGv z*}|=F);4nm+n#TU1F9X>_;DRQcw`swES<`2b+qY}wG9 zg5+`;@VC=XoQ{<)mM=S--;+tpwY;-=zHG_ziv9ccr3Zgd`g%9JlRn$dI-76e*Hk(v zqr3W<)9`kJRTp7BkTcK=Rm3VerTOXug`~T5%STn+vcYr=DqD zDJYKK_e>+ewxvpdZ51lGkU(vmcW?kOr%Yl?7P^1b@w%St^s!}ky`esw)R^Md8ctIq zMkrdSUb>4d?OaxO0W2?IiE(J-at_ZMzNK=U-?Cgn|8NgiLMIAowvZMRmM$EARmqlj zFRKqgLa-GE_TbQ79aqx$W=$jKYV%q8{muSgMEOpc1i{$nSl2XNub%kaO6+#+Nh%D5BP#@ zAH1uC`h33!E9(mde|mSHjB;)IfOl*rXYgOE?{1(QWdB@fqz#IH_OzSeC(KP z8kFGwsnt$b+J0ICnylM+WX& zW4WRN%N5o1UqQSpFGsKb_1u*oe1Y7QmUYgWT$E{kQ4K1bp9Hg0p>H{dbH%?B;g!%8Ge&O_u)bJ zNS}w{HC$j_gS<;fS@mvU0dc!d{>Yo&b)$9;^laqAM-S(jDoZ3 zJ}>*#u2!#eJa4+dFB5RM?L90^r1 zt13>YN?BFu4XILNS+NyAJ@7XJu|1q^%UoB|mY=Xyv9>DCRz27CmdS>*z}?XhTuKr* z*W3W}d^=25uz^^_={C%ECUvH>yUy%7+jgccX|lk%4Y({r&gqM1yWT9VweSsmd$0av*kt+y4WUPwg6qnay2>ky!ae>FRk0=9j+2i|iUS#-TDOvzP3J9D@ z;W9%RTz8R@6L4jSnplSkw-LIrA2ysAb(3v)_sIwuIl^&#VM$Itz)-sW*x!-J?rYC{ z``VWl;TFN`m%fgt`ardXL-jbS(8vl_iq23N4zc0AmS8X;d=)Oi+YjH@jyjKO^)f@a zHxJZnA;}&ngs_T12_g*3|A9Wh=HWb`V6cGI$&i?B5S^D!jYUxUqZ%z<3UeZG`e-o4 z46_en{U7)jc?4q3Qdi~sk~&*Lx8=McHqN=W%yzE8g+Jq*rXn`RYU&r&tY*j3PFB+y z?;c_`LvSq}Y+vp?vlE9#(qvsVNUTYf?z5*KJ^iVVCsc*3sxZo2ICbvSGp7@!o7mD# zoN5y!cvAMSu7F&^jh}#!O9_4mKxfttTzV&K8e&dv%}iBv+8`5#WO@Rl;SLwPCsgDd zFL*s9is zLj`Vc6RC)0UY8TYM^ZEX8PrVcaAaPuT%;u|q$R!xlRV``j9|?pioU=mXe$^-!jn6$ zSl++1cOKGa-6WIlXRvphMrVf{V%@@Fs7xQr-ShI`K#3J)!@BivqwP6OyH&IU(~0F- zou1U1uZY1_;mrl`5($}!MgmCW8Jy=&8nv&$blHG=O|GL=?9>_ z43~)|${LX$UE4aKP4=Hw)6UA>4Kd0-w0z|dqvAtjR6>lZ4~?-QYfPzf%82SeYfPzV z^X>VOK2Sqy48XHh%fU{n{a1R2xFVq z+$+>ZE=`&-i8nC`r`_9_uRvtsmZbxjOr1I{-bPN)&uGLm<89=;{)|zyJtweV8}kgN zeh!^qVQK*WvP~r0n9CRnwx7g!y^Z1V)ws%TvW*zM&g6>*t*KjfVM{=`=(&yg4J6_f zr0_H18TK}Uz|WvrJlyWq=CKm3nb!EbXVkuo>^Rf?Js3dReggJW8&} zgR4^E2F)qvQJ^dU#PjGwbxP2_&S94F`zGMz{g2U`DYkIV91SU%y_g2UNw`p19$>~I zf_l~iAtFo*X8Ol@iC;iR6;t>sB&)_qN})j`sOu5!jW~koru;+zGDkCf4QLq=E2v0t zU7ZycBp3AO^qjRSVQpfqO`LUe!rH=GTR3a$?7^hgc(&$DO+s5Te|)y%GMq;r`_#iL zuFBXwoNMzL!<=?SZGiL4%W$4qZJ5_Zd!wy!W6c|CPg0|wS3g#|VlIx3bLQGP1x7SO z#Bz+Nj@mEepUaQ!TnGY&8h62&=xxjD?f6L_^<6k}?nvwti(amHN4#if+}HvTCw?-0 zO&06A)N`?CVFIi|EBA5kj(BNj+}O3O-v96Uo{RJw>RrgH^0DG%%kEb^uXvu4&kv%3 z>XW8~c_VAy$eHUB=B=!G>#}*cMfHMX(ZqW8a_;tc>AtwJ;|+Bu=B?#41uK>fu}>tL53tP#I7`oJ zs!&^wn3~P^#9bTX#@c0d9n#c9D=yTWtBGw}@URtIxq{|+{ zyD_0YdROcqTf8Y=v^j3v@`idVa;kc)WW`Ys-FjiyIWTmsPB=CLs>V6CCLFD-0~YYR z=X5I;?1Yv*YYLLHu2fNF^g-5D6L)&z#*NGB+MF5RFi=_-Sx&%)A3_j>y^M&@ia}Xu^%oVYr~9&W-zl_d*P^$ubBZ8H~Zp2k?(btWB7k+>Ci@ zdU8A{&_l}iu40O;*bprvlm!wJBI;K~TM~KCik4kA6dFP)kwJk>_V*SH{qJHnGn#R@ zG#FXB*CQENl@anYqr-B;J@;5llHrUFIZuVajwH#%2Q)pMyvk4hl7pYu`u)eIph-M>h2#HtDhb9Dzd|Zg}IBBN8;BFS# z%!B)Xvx1RSM8K*@WfCMxk6ED(EL@9+>nON=IV%`20DUf9E$3{3{d@lIB<14wom)7yaX zXEU<6BvUMb%a}4G>0I?u_|qfsw=NiR+05!{fr}c)Bf^$rC9+h7&UJKV&^ZkbZ-u|} z(YdeNk7qa92M4?Q?--=XL5Zw=RNVh0Govsi;`B!*K5}9}*ns>~){7*%G1P*{-63I=eOd|iC7L0d)L+O4&X?{WJennOPl5)LGwf>T- zdLKSZ!J--*wM254}$m-m*BspbW4qr7Z+st4dOF%O;g{OKxd5NR+?#N_r%)YcwQ{ cNZ{+ZEcIiS9FY8egHE#RcSW_5W>U)k3ogPkw*UYD literal 0 HcmV?d00001 diff --git a/lib/air_runtime/modes/__pycache__/eng_orchestrator.cpython-313.pyc b/lib/air_runtime/modes/__pycache__/eng_orchestrator.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9645bde475c676ecd859b2c94c422da32b317013 GIT binary patch literal 10536 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 DT1z{6 literal 0 HcmV?d00001 diff --git a/lib/air_runtime/modes/__pycache__/merge_pipeline.cpython-313.pyc b/lib/air_runtime/modes/__pycache__/merge_pipeline.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fa8bc311ccc335264269ab8fe9239251144d8f0b GIT binary patch literal 11846 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 Dv3NHP literal 0 HcmV?d00001 diff --git a/lib/air_runtime/modes/__pycache__/rvr_mode.cpython-313.pyc b/lib/air_runtime/modes/__pycache__/rvr_mode.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a6fea065ca7d73691cce99e050efea42c6c1d2d3 GIT binary patch literal 1904 zcmZ`)-*4MQ96u*8MalC@6sC*4NY}wZqvd zBSIn?63U(!5`#=b+IZO1y_E3;DlhyAiPEX%)Ct6zu}M4(RlIZO*lic!r1-wS@B97U z_jB26ZVm#ny8k@2%&-7Fpp3gb4zU*}#8n^x1SCfC%rJ2e@ zE%VKAaUStD&CUekK@=o3FL7O9*@v3a{#YP)f&@ILp~NbRt{5te{$BVp`sSJF<8KNN zzrR#l|MuaHTeWLz4+@ta(-dM1_PwrHOEE8QPQX4B@KzMU@D7HUN#L~&KQbHBq2pZ*^Hvg$S|zg1%@8ttN_`)mDMvZ{}&;6w~JjRFVztvF=TJ zCC}tr04R9?Qq8V?l5um>eW{jxsYmjrT3t@^Nv!0b@C-8&C-LMPm|%4_=}U;N#lfCr z_v87AMaO~0r4Ctesfa6;g05c_9FjIAJpvf@{HHFI8G!=E(Mx-8ko%TCJE{E=R(I`ySEUn)}?SwYzZAJGN?#*cP!=!i_)!(dP z+D~(s#FHsCl>G?%RuoM~r-&bYeasMfQwm^5jFfygLf1l7@x+!mQV~b0;+ZXRvLa4a z#j}MA+d<*;)0a;drYX^Pxo<0Y?8;JM`Zq3ET>A8r9ih8)p(@0R-rd%o?e<8ycXX#S zR(^h_{KmVL&iBe4@^;V3?Vgv)N6-BgVB3O4|C11ibbW1nVccA-b`BPsw%a_pi-W!ee(YR>Ch9*`4sQQtsx*mE)t;@Yq&(@<$Jmk?6!~{JV)e6L;VKnXL|- zFZaJ(7GAl>zq&`70thBZ`>h<(XJaAE$+BuJ$TEeDwM05xADpg3eIBZV%58h&un31u zHKRTXt%rs6E4Al@Ud^|uK+aF=#jP3pp0OaQI literal 0 HcmV?d00001 diff --git a/lib/air_runtime/modes/__pycache__/sdb_mode.cpython-313.pyc b/lib/air_runtime/modes/__pycache__/sdb_mode.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..00559be7ac5a331fc4bf194593d1d464e7b26e4e GIT binary patch literal 3034 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 F{{X^=YhVBX literal 0 HcmV?d00001 diff --git a/lib/air_runtime/modes/__pycache__/tst_mode.cpython-313.pyc b/lib/air_runtime/modes/__pycache__/tst_mode.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..42fd3aa0bad10b8018a56c993749cf1a70017f4c GIT binary patch literal 2100 zcmZ`4OKcNIbi6;W*UsYLLgFM1j#_9;AV~?SN`#3j1O*4u${SNEgRCW9jDx+lX4jNP z5sKPNP%DwBpqLZ%mYl++61S!&uC^RxcN9bwph|D8LhpUE_Tq$^k#^p1-kUe?w_7bO z5`uMRqj%~FhtLa3*o&znTNePihZKa7;!v0|N0`A(fa2^J8|E-);oKM>7O-I9{FpN= zVlnK(E{hh%q_7*i!yfDb+NFpcXo|xvNoP>X?+4?brSODd1~S=%7TB1*8@O;J@aHc- zY_8nf{B?fw=O16)UwSe7-Cxw5po4HLSv3)4 zh(}G~Tw2xRIGZ(ztliLb6J*JFj_NZ+JViO@vPmLNoKhy@k?8pNq=Q~kUoLIOowUAElzPv@dsq}Ap&;KiQi)`n?(VCj6!SHr;S9vh1~ z?3mb47otL>&6YP{tlh?=Oq6ftH*h=bGYtsbt_Ede!0op222|L7ez=~u0E+e45{0w9 z+rj0djEshurZS@Gk5A1g|qOC}W-3Tdey6esu z?J#Raor;I@Z2h+@Xb|b*wz#xJ#mGmtZUZW^vdfO+&KD<9&^t~9!&J?@L0FvEgFF#T z)wq5(L0ng{n$f<=;_HMrv!M598T#b7AS*G3&JKfGw33mijxNM)Gx0MF4jyXu%8|hAfPbp0BS})Z4wrS z8JL1;X5ig$z@S$J>_vLgsVf8U<~XRCsf;#2!)gpbSmG4x&wWd{jGEFhtq(e1j8T|i zvuNGdR^T3Z?t99<{T1I}$v0T`9jW+6O1_b@@A&MwswCezbo-n+LRrps+ZW%=kF zTNT}R#M|QC_Uc}L)xWRe50?DF>YmQUOAD8(d%G4h3z<6j$1QY8uaV$#zxE=RT$BTA zV$ZtRGA|Z<1->YEuZjC8DHT4cboP}x`-&Zhit^z#v7d5<`5TKf3p4Zj!!t`4RxU4J zUi#wkr>m!*od50o>S$3`o{AGMyWe0td9p};*7@=*~xeu ziV9K%NvV}s`?l2doA4{M^JLW$k++Le*;<*jZfDjKZ`F`hZo9V&t-qa7D@LmVX_A)_ zJ^%oJ2pbWPLlxxH+6g=WB>i_8@5A;t>u@-JM}4nQ`!h7~3}{+e+9oI?6B1T E0}Lw3LjV8( literal 0 HcmV?d00001 diff --git a/lib/air_runtime/modes/__pycache__/xdb_mode.cpython-313.pyc b/lib/air_runtime/modes/__pycache__/xdb_mode.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..372ba6761e6c9169f8801b968d253865df1ad980 GIT binary patch literal 2375 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 Nkgye>a|I9ZKLC?)%e?>q literal 0 HcmV?d00001 diff --git a/lib/air_runtime/modes/arc_mode.py b/lib/air_runtime/modes/arc_mode.py new file mode 100644 index 0000000..2e7f958 --- /dev/null +++ b/lib/air_runtime/modes/arc_mode.py @@ -0,0 +1,258 @@ +#!/usr/bin/env python3 +""" +AirArc mode — V2 架构规划器。 +L1 代码级保障:allowed-tools 限制为只读。 +产出 execution-plan.json(完整 DAG)+ plan-delta.json(增量重规划)。 +""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path + +from air_runtime.contracts import now_iso +from air_runtime.io import atomic_json_write, safe_json_load +from air_runtime.paths import airplan_root, todo_path as get_todo_path +from air_runtime.review import build_parallel_review, render_review_markdown +from air_runtime.task_graph import TaskGraph, TaskNode, Edge, PlanDelta +from air_runtime.events import EventLog +from air_runtime.paths import event_log_path +from air_runtime.utils import ordered_unique + + +class ArcPhaseGate: + """三阶段门控:discussing → proposing → confirmed。 + execution-plan.json 仅在 phase=confirmed 时允许写入。 + """ + PHASES = ["discussing", "proposing", "confirmed"] + + def __init__(self, state_path: Path): + self._state_path = state_path + + @property + def current_phase(self) -> str: + data = safe_json_load(self._state_path) or {} + return data.get("arcPhase", "discussing") + + def advance_to(self, phase: str) -> None: + if phase not in self.PHASES: + raise ValueError(f"invalid phase: {phase!r}") + idx_current = self.PHASES.index(self.current_phase) + idx_target = self.PHASES.index(phase) + if idx_target <= idx_current: + return + data = safe_json_load(self._state_path) or {} + data["arcPhase"] = phase + data[f"arcPhase_{phase}At"] = now_iso() + atomic_json_write(self._state_path, data) + + def can_write_plan(self) -> bool: + return self.current_phase == "confirmed" + + def confirm_architecture(self, user_confirmation: str) -> bool: + """检查用户确认文本中的关键词,确认后推进到 confirmed。""" + confirm_keywords = ["确认", "可以", "同意", "confirm", "yes", "ok", "好的", "没问题"] + if any(kw in user_confirmation.lower() for kw in confirm_keywords): + self.advance_to("confirmed") + return True + return False + + +def _paths(project_root: Path) -> dict[str, Path]: + root = airplan_root(project_root) / "state" / "airarc" + return { + "root": root, + "state": root / "state.json", + "reviews_dir": root / "reviews", + "execution_plan_json": root / "reviews" / "execution-plan.json", + "execution_plan_md": root / "reviews" / "execution-plan.md", + "plan_delta_json": root / "reviews" / "plan-delta.json", + "task_graph_json": root / "reviews" / "task-graph.json", + } + + +def _ensure_dirs(paths: dict[str, Path]) -> None: + paths["reviews_dir"].mkdir(parents=True, exist_ok=True) + + +def _export_task_graph_json(graph: TaskGraph, path: Path) -> None: + data = { + "nodes": {nid: {"id": n.id, "status": n.status, "task": n.task, + "filesDirs": n.files_dirs, "doneWhen": n.done_when, + "inDegree": n.in_degree, "outEdges": n.out_edges, + "writeSet": n.write_set} + for nid, n in graph.nodes.items()}, + "edges": [{"source": e.source, "target": e.target, "kind": e.kind} for e in graph.edges], + } + atomic_json_write(path, data) + + +def _build_graph_from_todo(todo_path: Path) -> TaskGraph: + """从 todo.md 构建初始 DAG。""" + from air_runtime.todo_parser import parse_tasks + tasks = parse_tasks(todo_path) + graph = TaskGraph() + for t in tasks: + node = TaskNode( + id=t.task_id, status=t.status, task=t.task, + files_dirs=t.files_dirs, done_when=t.done_when, + ) + graph.add_node(node) + return graph + + +def enter_mode(project_root: Path) -> dict: + paths = _paths(project_root) + _ensure_dirs(paths) + payload = {"enabled": True, "updatedAt": now_iso(), "projectRoot": str(project_root)} + atomic_json_write(paths["state"], payload) + return {"state_path": str(paths["state"])} + + +def parallel_review_mode(project_root: Path, todo_path: Path) -> dict: + paths = _paths(project_root) + _ensure_dirs(paths) + + # 三阶段门控:仅在 confirmed 阶段允许写入 execution-plan.json + gate = ArcPhaseGate(paths["state"]) + if not gate.can_write_plan(): + return { + "blocked": True, + "reason": f"arc phase is '{gate.current_phase}', must be 'confirmed' before generating plan", + "currentPhase": gate.current_phase, + } + + review = build_parallel_review(todo_path) + review_json_path = paths["reviews_dir"] / "parallel-review.json" + review_md_path = paths["reviews_dir"] / "parallel-review.md" + atomic_json_write(review_json_path, review.to_dict()) + review_md_path.write_text(render_review_markdown(review), encoding="utf-8") + + # 构建 DAG + graph = _build_graph_from_todo(todo_path) + for edge_info in review.to_dict().get("edges", []): + graph.add_edge(Edge(source=edge_info["source"], target=edge_info["target"], + kind=edge_info.get("kind", "dependency"))) + + _export_task_graph_json(graph, paths["task_graph_json"]) + + # 生成执行计划 + selected_tasks = review.parallel_groups[0].task_ids if review.parallel_groups else [] + execution_plan = { + "generatedAt": now_iso(), "projectRoot": str(project_root), + "todoPath": str(todo_path), "planSource": "airarc-post-plan-review", + "parallelReview": review.to_dict(), + "selectedTasks": selected_tasks, + "parallelGroups": [g.to_dict() for g in review.parallel_groups], + "conflicts": [c.to_dict() for c in review.conflicts], + "serializationPoints": review.serialization_points, + } + atomic_json_write(paths["execution_plan_json"], execution_plan) + + markdown_lines = [ + "# AirArc Execution Plan", "", + f"- Generated: `{execution_plan['generatedAt']}`", + f"- Plan Source: `{execution_plan['planSource']}`", "", + "## Selected Tasks", + ] + for tid in selected_tasks: + markdown_lines.append(f"- `{tid}`") + markdown_lines.extend(["", "## Parallel Groups"]) + for g in review.parallel_groups: + markdown_lines.append(f"- `{g.name}`: {', '.join(g.task_ids)} — {g.reason}") + paths["execution_plan_md"].write_text("\n".join(markdown_lines) + "\n", encoding="utf-8") + + atomic_json_write(paths["state"], {"enabled": True, "updatedAt": now_iso(), + "projectRoot": str(project_root)}) + return {"json_path": str(review_json_path), "markdown_path": str(review_md_path), + "execution_plan_json_path": str(paths["execution_plan_json"]), + "parallel_group_count": len(review.parallel_groups), + "conflict_count": len(review.conflicts)} + + +def incremental_replan_mode(project_root: Path, todo_path: Path, previous_graph_path: Path | None = None) -> dict: + """增量重规划:产出 PlanDelta 并喂回 prev graph,再写 plan-delta.json。""" + paths = _paths(project_root) + _ensure_dirs(paths) + + review = build_parallel_review(todo_path) + new_graph = _build_graph_from_todo(todo_path) + for edge_info in review.to_dict().get("edges", []): + new_graph.add_edge(Edge(source=edge_info["source"], target=edge_info["target"], + kind=edge_info.get("kind", "dependency"))) + + delta = PlanDelta() + if previous_graph_path and previous_graph_path.exists(): + prev_graph = TaskGraph.load(previous_graph_path) + delta = new_graph.diff(prev_graph) + # 关键:把 delta 喂回去给 prev graph,保留已调度状态 + if previous_graph_path.exists(): + prev_graph.apply_delta(delta) + _export_task_graph_json(prev_graph, previous_graph_path) + else: + delta.added_tasks = list(new_graph.nodes.values()) + delta.edge_changes.added = list(new_graph.edges) + + atomic_json_write(paths["plan_delta_json"], { + "generatedAt": now_iso(), + "removedTasks": delta.removed_tasks, + "addedTasks": [{"id": n.id, "task": n.task, "filesDirs": n.files_dirs, + "doneWhen": n.done_when, "writeSet": n.write_set} + for n in delta.added_tasks], + "modifiedTasks": [{"id": n.id, "task": n.task, "filesDirs": n.files_dirs, + "doneWhen": n.done_when, "writeSet": n.write_set} + for n in delta.modified_tasks], + "edgeChanges": { + "added": [{"source": e.source, "target": e.target, "kind": e.kind} + for e in delta.edge_changes.added], + "removed": [{"source": e.source, "target": e.target, "kind": e.kind} + for e in delta.edge_changes.removed], + }, + }) + + _export_task_graph_json(new_graph, paths["task_graph_json"]) + + log = EventLog(event_log_path(project_root)) + log.emit("arc.replanned", {"delta_added": len(delta.added_tasks), + "delta_removed": len(delta.removed_tasks), + "delta_modified": len(delta.modified_tasks), + "edges_added": len(delta.edge_changes.added), + "edges_removed": len(delta.edge_changes.removed)}) + + return {"plan_delta_json_path": str(paths["plan_delta_json"]), + "task_graph_json_path": str(paths["task_graph_json"]), + "added_count": len(delta.added_tasks), + "removed_count": len(delta.removed_tasks), + "modified_count": len(delta.modified_tasks), + "edges_added": len(delta.edge_changes.added), + "edges_removed": len(delta.edge_changes.removed)} + + +def main(args) -> None: + project_root = Path(args.project).expanduser().resolve() + sub = args.sub or "status" + + if sub == "enter": + result = enter_mode(project_root) + print("airplan_mode=arc") + print(f"state_path={result['state_path']}") + elif sub == "parallel-review": + tpath = Path(args.todo).expanduser().resolve() if args.todo else get_todo_path(project_root) + result = parallel_review_mode(project_root, tpath) + print("airplan_mode=arc") + print(f"json_path={result['json_path']}") + print(f"parallel_group_count={result['parallel_group_count']}") + print(f"conflict_count={result['conflict_count']}") + elif sub == "incremental-replan": + tpath = Path(args.todo).expanduser().resolve() if args.todo else get_todo_path(project_root) + prev = _paths(project_root)["task_graph_json"] + result = incremental_replan_mode(project_root, tpath, prev) + print("airplan_mode=arc") + print(f"plan_delta_path={result['plan_delta_json_path']}") + print(f"added={result['added_count']} removed={result['removed_count']}") + else: + paths = _paths(project_root) + state = safe_json_load(paths["state"]) or {} + print(f"airplan_mode=arc") + print(f"enabled={state.get('enabled', False)}") diff --git a/lib/air_runtime/modes/ctx_mode.py b/lib/air_runtime/modes/ctx_mode.py new file mode 100644 index 0000000..a1d26d5 --- /dev/null +++ b/lib/air_runtime/modes/ctx_mode.py @@ -0,0 +1,232 @@ +""" +AirContext mode — V2 上下文管理器。 +V2 改进:压缩质量校验、自适应 Token 估算、陈旧锁检测、三级降级压缩。 +""" + +from __future__ import annotations + +import os +import re +from pathlib import Path + +from air_runtime.io import atomic_json_write, safe_json_load +from air_runtime.paths import airplan_root, event_log_path +from air_runtime.events import EventLog, CONTEXT_COMPACTED +from air_runtime.utils import now_iso + + +class CompressionLevel: + """三级降级""" + RETRY = "retry" # 1. 重试一次 + FALLBACK_MODEL = "fallback_model" # 2. 换模型 + TRUNCATE = "truncate" # 3. 激进截断 + + +DEFAULT_TRUNCATION_KEEP = 10 # 保留最近 10 轮 + +CHARS_PER_TOKEN = { + "chinese": 1.5, + "english": 4.0, + "code": 3.0, + "markup": 5.0, +} + +MUST_PRESERVE_PATTERNS = [ + r"[A-Za-z0-9_\-/]+\.(py|ts|js|cpp|h|md|json|yaml)", + r"ADR-\d{4}", + r"TODO|FIXME|HACK", + r"INV-\d+", +] + + +def _ctx_paths(project_root: Path) -> dict[str, Path]: + root = airplan_root(project_root) / "state" / "aircontext" + return {"root": root, "state": root / "state.json", "lock": root / "compactor.lock"} + + +def estimate_tokens(text: str) -> int: + chinese = len(re.findall(r"[一-鿿]", text)) + code = len(re.findall(r"[{}()\[\];=<>]", text)) + markup = len(re.findall(r"[#*\-`|]", text)) + english = max(0, len(text) - chinese - code - markup) + tokens = ( + chinese / CHARS_PER_TOKEN["chinese"] + + code / CHARS_PER_TOKEN["code"] + + markup / CHARS_PER_TOKEN["markup"] + + english / CHARS_PER_TOKEN["english"] + ) + return int(tokens) + + +def validate_compression(original: str, summary: str) -> dict: + missing = [] + for pattern in MUST_PRESERVE_PATTERNS: + orig_matches = set(re.findall(pattern, original)) + summary_matches = set(re.findall(pattern, summary)) + lost = orig_matches - summary_matches + if len(lost) > len(orig_matches) * 0.3 and len(orig_matches) > 3: + missing.append({"pattern": pattern, "lost": list(lost)[:10]}) + return {"ok": len(missing) == 0, "missing": missing, "originalTokens": estimate_tokens(original), + "summaryTokens": estimate_tokens(summary)} + + +def _compress_basic(text: str, max_tokens: int) -> str: + """基础压缩:token 估算 + 截断""" + estimated_tokens = len(text) // 3 + if estimated_tokens <= max_tokens: + return text + # 按行截断 + lines = text.split('\n') + chars_per_line_estimate = 30 + keep_lines = int(max_tokens * chars_per_line_estimate / 80) # 80 chars/line + return '\n'.join(lines[-keep_lines:]) + + +def _simplify_prompt(text: str) -> str: + """简化 prompt:移除详细上下文,保留核心""" + lines = text.split('\n') + # 只保留前 3 行 + 包含 "def " / "class " / "#" 的行 + kept = lines[:3] + kept.extend([l for l in lines[3:] if 'def ' in l or 'class ' in l or l.startswith('#')]) + return '\n'.join(kept) + + +def compress_with_fallback(context: str, max_tokens: int = 4000) -> dict: + """ + 三级降级压缩: + - 尝试正常压缩 + - 失败则换模型重试 + - 再失败则激进截断 + 返回: {"level": "...", "result": "...", "tokens": N} + """ + # Level 1: 正常尝试 + try: + result = _compress_basic(context, max_tokens) + return {"level": CompressionLevel.RETRY, "result": result, "tokens": len(result.split())} + except Exception: + pass + + # Level 2: 换模型(更简单的 prompt + 更宽松的 max_tokens) + try: + simplified = _simplify_prompt(context) + result = _compress_basic(simplified, int(max_tokens * 1.5)) + return {"level": CompressionLevel.FALLBACK_MODEL, "result": result, "tokens": len(result.split())} + except Exception: + pass + + # Level 3: 激进截断 + lines = context.split('\n') + # 提取 ADR 引用行 + adr_lines = [l for l in lines if 'ADR-' in l or 'adr-' in l] + # 保留最近 N 轮 + recent_lines = lines[-DEFAULT_TRUNCATION_KEEP * 5:] # 每轮约 5 行 + truncated = '\n'.join(recent_lines + adr_lines) + return { + "level": CompressionLevel.TRUNCATE, + "result": truncated, + "tokens": len(truncated.split()), + "warning": f"truncated to {DEFAULT_TRUNCATION_KEEP * 5} recent lines + {len(adr_lines)} ADR lines" + } + + +def validate_compression_with_fallback(project_root: Path, context_path: Path) -> dict: + """验证压缩有效性,失败时触发三级降级""" + content = context_path.read_text() + original_len = len(content) + + # 先用当前配置尝试 + result = compress_with_fallback(content) + + validation = { + "original_chars": original_len, + "result_chars": len(result["result"]), + "level": result["level"], + "tokens": result.get("tokens", 0), + } + + if result["level"] == CompressionLevel.TRUNCATE: + validation["warning"] = result.get("warning", "") + + return validation + + +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): + lock_path.unlink(missing_ok=True) + return acquire_compactor_lock(lock_path) + + +def ctx_enter(project_root: Path) -> dict: + paths = _ctx_paths(project_root) + paths["root"].mkdir(parents=True, exist_ok=True) + atomic_json_write(paths["state"], {"enabled": True, "updatedAt": now_iso(), + "projectRoot": str(project_root)}) + return {"state_path": str(paths["state"])} + + +def main(args) -> None: + project_root = Path(args.project).expanduser().resolve() + sub = args.sub or "status" + paths = _ctx_paths(project_root) + + if sub == "enter": + result = ctx_enter(project_root) + print(f"airplan_mode=ctx\nstate_path={result['state_path']}") + elif sub == "estimate": + text = "sample" # 实际使用时从 stdin 或文件读取 + tokens = estimate_tokens(text) + print(f"airplan_mode=ctx\ntokens={tokens}") + elif sub == "validate": + ctx_path = project_root / "AirPlan" / "context.md" + if ctx_path.exists(): + validation = validate_compression_with_fallback(project_root, ctx_path) + ok = validation["level"] != CompressionLevel.TRUNCATE + print(f"airplan_mode=ctx\nvalidation_ok={ok}\nlevel={validation['level']}\ntokens={validation['tokens']}") + if "warning" in validation: + print(f"warning={validation['warning']}") + else: + print("airplan_mode=ctx\nvalidation_ok=true") + elif sub == "compress": + # 读取 context 文件 + ctx_path = project_root / "AirPlan" / "context.md" + if not ctx_path.exists(): + print("error: context.md not found") + return + + # 调用三级降级压缩 + content = ctx_path.read_text() + result = compress_with_fallback(content) + + log = EventLog(event_log_path(project_root)) + log.emit(CONTEXT_COMPACTED, { + "compressionLevel": result["level"], + "originalChars": len(content), + "resultChars": len(result["result"]), + "tokens": result.get("tokens", 0), + }) + + print(f"airplan_mode=ctx") + print(f"compression_level={result['level']}") + print(f"original_chars={len(content)}") + print(f"result_chars={len(result['result'])}") + if 'warning' in result: + print(f"warning={result['warning']}") + + # 可选:写回压缩结果 + if getattr(args, "write_back", False): + compressed_path = project_root / "AirPlan" / "context.compressed.md" + compressed_path.write_text(result['result']) + print(f"written_to={compressed_path}") + else: + state = safe_json_load(paths["state"]) or {} + print(f"airplan_mode=ctx\nenabled={state.get('enabled', False)}") diff --git a/lib/air_runtime/modes/dbg_mode.py b/lib/air_runtime/modes/dbg_mode.py new file mode 100644 index 0000000..cb74689 --- /dev/null +++ b/lib/air_runtime/modes/dbg_mode.py @@ -0,0 +1,279 @@ +""" +AirDbg mode — V2 调试器。 +V2 改进:7步工作流强制追踪(L1 代码级),修复前自动 git snapshot 回滚。 +""" + +from __future__ import annotations + +import subprocess +from datetime import datetime, timezone +from pathlib import Path + +from air_runtime.io import atomic_json_write, safe_json_load +from air_runtime.paths import airplan_root, event_log_path +from air_runtime.events import EventLog, DEBUG_SESSION +from air_runtime.utils import now_iso, session_stamp + +DBG_STEPS = [ + "confirm_symptoms", + "load_context", + "reproduce", + "locate_root_cause", + "fix", + "verify", + "close_out", +] + + +class EvidenceFirstGate: + """先读后写门控:未执行任何取证行为前,禁止代码修改。""" + + EVIDENCE_TYPES = [ + "screenshot", + "packet_capture", + "static_analysis", + "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: + if evidence_type not in self.EVIDENCE_TYPES: + raise ValueError(f"unknown evidence type: {evidence_type!r}") + self._collected_evidence.append(evidence_type) + + 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( + "未执行任何取证行为,禁止修改代码。" + "请先至少完成以下一项:截图、抓包、静态分析、日志分析、代码追踪、复现步骤。" + ) + + +class WorkflowViolation(Exception): + """调试工作流违规。""" + + +def _paths(project_root: Path) -> dict[str, Path]: + root = airplan_root(project_root) / "state" / "airdbg" + return { + "root": root, + "state": root / "state.json", + "sessions_dir": root / "sessions", + "snapshots_dir": root / "snapshots", + } + + +def _ensure_dirs(paths: dict[str, Path]) -> None: + for key in ("sessions_dir", "snapshots_dir"): + paths[key].mkdir(parents=True, exist_ok=True) + + +def start_session(project_root: Path, task_id: str) -> dict: + paths = _paths(project_root) + _ensure_dirs(paths) + + session_id = f"{task_id}-{session_stamp()}" + session_state = { + "sessionId": session_id, "taskId": task_id, + "currentStep": "confirm_symptoms", + "startedAt": now_iso(), + "stepsCompleted": [], + "collectedEvidence": [], + "evidence": {}, + "result": None, + } + session_path = paths["sessions_dir"] / f"{session_id}.json" + atomic_json_write(session_path, session_state) + + log = EventLog(event_log_path(project_root)) + log.emit(DEBUG_SESSION, {"sessionId": session_id, "taskId": task_id, "action": "started"}) + + return { + "sessionId": session_id, "sessionPath": str(session_path), + "currentStep": "confirm_symptoms", + "steps": DBG_STEPS, + } + + +def get_step(session_path: Path) -> str: + session = safe_json_load(session_path) + if not session or not isinstance(session, dict): + return "confirm_symptoms" + return session.get("currentStep", "confirm_symptoms") + + +def advance_step(session_path: Path, evidence: dict) -> str: + session = safe_json_load(session_path) + if not session or not isinstance(session, dict): + raise ValueError("invalid session") + + current = session.get("currentStep", "confirm_symptoms") + current_idx = DBG_STEPS.index(current) if current in DBG_STEPS else 0 + + # 验证当前步骤需要的证据 + required_evidence = _required_evidence_for_step(current) + if required_evidence: + missing = [k for k in required_evidence if k not in evidence] + if missing: + raise ValueError(f"step '{current}' requires evidence: {missing}") + + # 先读后写门控:fix 步骤前必须已有取证记录 + if current == "fix": + collected = session.get("collectedEvidence", []) + if not collected: + raise WorkflowViolation( + "未执行任何取证行为,禁止修改代码。" + "请先至少完成以下一项:截图、抓包、静态分析、日志分析、代码追踪、复现步骤。" + ) + + session["stepsCompleted"].append({"step": current, "evidence": evidence, "completedAt": now_iso()}) + + # 累积取证记录(confirm_symptoms, load_context, reproduce, locate_root_cause 都是取证步骤) + evidence_steps = {"confirm_symptoms", "load_context", "reproduce", "locate_root_cause"} + if current in evidence_steps: + session.setdefault("collectedEvidence", []).append(current) + next_idx = current_idx + 1 + if next_idx < len(DBG_STEPS): + session["currentStep"] = DBG_STEPS[next_idx] + + atomic_json_write(session_path, session) + return session["currentStep"] + + +def skip_reproduce(session_path: Path, reason: str) -> str: + session = safe_json_load(session_path) + if not session or not isinstance(session, dict): + raise ValueError("invalid session") + if session.get("currentStep") != "reproduce": + raise ValueError("can only skip from reproduce step") + session["currentStep"] = "locate_root_cause" + session["stepsCompleted"].append({"step": "reproduce", "evidence": {"skipped": True, "reason": reason}}) + atomic_json_write(session_path, session) + return "locate_root_cause" + + +def pre_fix_snapshot(project_root: Path, task_id: str) -> str: + """ + 修复前创建 git tag 作为回滚点。 + V2 改进:只提交当前 task 写集范围内的文件(在 result.filesChanged 中声明)。 + """ + # 1. 读取 worker result 获取 filesChanged + result_path = project_root / "AirPlan" / "state" / "airdo" / "tasks" / task_id / "result.json" + if not result_path.exists(): + # 无 result 文件,回退到全量提交(但加 warning) + return _snapshot_full(project_root, task_id) + + result = safe_json_load(result_path) or {} + files_changed = result.get("filesChanged", []) + + if not files_changed: + # 无写集声明,回退到当前工作目录中的已跟踪文件 + files_changed = None + + # 2. 只 add 这些文件,然后 commit + return _snapshot_selective(project_root, task_id, files_changed) + + +def _snapshot_selective(project_root: Path, task_id: str, files: list[str] | None) -> str: + """只提交指定的文件列表""" + ref = f"airdbg-prefix-{task_id}-{session_stamp()}" + + try: + # git add + if files: + for f in files: + fp = project_root / f + if fp.exists(): + subprocess.run(["git", "-C", str(project_root), "add", str(fp)], + check=True, capture_output=True, timeout=10) + + # 如果有 staging 的内容则 commit,否则跳过(避免空 commit) + result = subprocess.run( + ["git", "-C", str(project_root), "commit", "-m", + f"AirDbg pre-fix snapshot: {task_id}", "--allow-empty"], + capture_output=True, text=True, timeout=30, + ) + + if result.returncode == 0: + subprocess.run( + ["git", "-C", str(project_root), "tag", ref], + check=True, capture_output=True, text=True, timeout=10, + ) + return ref + else: + # 没有 staged 内容或 commit 失败 + return "" + + except subprocess.CalledProcessError: + return "" + + +def _snapshot_full(project_root: Path, task_id: str) -> str: + """全量提交(仅在无 filesChanged 信息时的 fallback,加 warning)""" + import logging + logger = logging.getLogger(__name__) + logger.warning("pre_fix_snapshot: no filesChanged info, falling back to git commit -am") + + # 这里保留原逻辑但加 comment 说明这是 fallback + return _do_git_commit_am(project_root, task_id) + + +def _do_git_commit_am(project_root: Path, task_id: str) -> str: + """原始实现,保留用于 fallback""" + ref = f"airdbg-prefix-{task_id}-{session_stamp()}" + try: + subprocess.run( + ["git", "-C", str(project_root), "commit", "-am", + f"AirDbg pre-fix snapshot: {task_id}", "--allow-empty"], + check=True, capture_output=True, text=True, timeout=30, + ) + subprocess.run( + ["git", "-C", str(project_root), "tag", ref], + check=True, capture_output=True, text=True, timeout=10, + ) + except subprocess.CalledProcessError: + return "" + return ref + + +def _required_evidence_for_step(step: str) -> list[str]: + evidence_map = { + "confirm_symptoms": ["symptom", "expected", "actual"], + "load_context": [], + "reproduce": ["reproduction_steps"], + "locate_root_cause": ["root_cause_analysis"], + "fix": ["fix_description", "files_changed"], + "verify": ["validation_result"], + "close_out": ["residual_risk", "adr_updates"], + } + return evidence_map.get(step, []) + + +def main(args) -> None: + project_root = Path(args.project).expanduser().resolve() + sub = args.sub or "status" + paths = _paths(project_root) + _ensure_dirs(paths) + + if sub == "start": + result = start_session(project_root, args.task_id) + print("airplan_mode=dbg") + print(f"session_id={result['sessionId']}") + print(f"current_step={result['currentStep']}") + print(f"steps={','.join(result['steps'])}") + elif sub == "snapshot": + ref = pre_fix_snapshot(project_root, args.task_id) + print("airplan_mode=dbg") + print(f"snapshot_ref={ref}") + else: + state = safe_json_load(paths["state"]) or {} + print("airplan_mode=dbg") + print(f"enabled={state.get('enabled', False)}") diff --git a/lib/air_runtime/modes/dep_mode.py b/lib/air_runtime/modes/dep_mode.py new file mode 100644 index 0000000..5418de8 --- /dev/null +++ b/lib/air_runtime/modes/dep_mode.py @@ -0,0 +1,39 @@ +"""AirDep mode — V2 部署器。""" + +from pathlib import Path +from air_runtime.deploy_runtime import deploy, DeployTarget +from air_runtime.io import safe_json_load +from air_runtime.paths import airplan_root, event_log_path +from air_runtime.events import EventLog, DEPLOY_COMPLETED + + +def main(args) -> None: + project_root = Path(args.project).expanduser().resolve() + tid = args.task_id + sub = args.sub or "deploy" + + if sub == "deploy" and args.host: + target = DeployTarget(host=args.host) + binary = Path(args.binary).expanduser().resolve() if args.binary else Path(".") + result = deploy(tid, project_root, target, binary, tid) + + log = EventLog(event_log_path(project_root)) + log.emit(DEPLOY_COMPLETED, { + "taskId": tid, + "success": result.success, + "host": args.host, + "binaryMd5": result.binary_md5, + "serviceStatus": result.service_status, + }) + + print("airplan_mode=dep") + print(f"task_id={tid}") + print(f"success={result.success}") + print(f"md5={result.binary_md5}") + print(f"service_status={result.service_status}") + if result.error: + print(f"error={result.error}") + else: + paths = airplan_root(project_root) / "state" / "airdep" + state = safe_json_load(paths / "state.json") or {} + print(f"airplan_mode=dep\nenabled={state.get('enabled', False)}") diff --git a/lib/air_runtime/modes/do_mode.py b/lib/air_runtime/modes/do_mode.py new file mode 100644 index 0000000..2957cf3 --- /dev/null +++ b/lib/air_runtime/modes/do_mode.py @@ -0,0 +1,165 @@ +""" +AirDo mode — V2 任务执行器。 +V2 改进:强制 AirDbg 路由(L1 代码级),task_id 注入防护。 +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +from air_runtime.io import atomic_json_write, safe_json_load +from air_runtime.paths import airplan_root, event_log_path +from air_runtime.events import EventLog, TASK_COMPLETED, TASK_BLOCKED +from air_runtime.contracts import WorkerResult, now_iso +from air_runtime.utils import sanitize_task_id, session_stamp + + +def _paths(project_root: Path, task_id: str) -> dict[str, Path]: + root = airplan_root(project_root) / "state" / "airdo" + task_dir = root / "tasks" / task_id + return { + "root": root, + "state": root / "state.json", + "task_dir": task_dir, + "brief": task_dir / "brief.md", + "handoff": task_dir / "subagent-handoff.md", + "result": task_dir / "result.json", + "worker_state": task_dir / "worker-state.json", + } + + +def _ensure_dirs(paths: dict[str, Path]) -> None: + paths["task_dir"].mkdir(parents=True, exist_ok=True) + + +def enter_worker(project_root: Path, task_id: str) -> dict: + tid = sanitize_task_id(task_id) + paths = _paths(project_root, tid) + _ensure_dirs(paths) + + worker_state = { + "taskId": tid, "status": "implementing", + "enteredAt": now_iso(), "resultPath": str(paths["result"]), + } + atomic_json_write(paths["worker_state"], worker_state) + atomic_json_write(paths["state"], {"enabled": True, "activeTaskId": tid, + "updatedAt": now_iso()}) + + log = EventLog(event_log_path(project_root)) + log.emit("task.entered", {"taskId": tid}) + + return { + "taskId": tid, "briefPath": str(paths["brief"]), + "handoffPath": str(paths["handoff"]), + "resultPath": str(paths["result"]), + "workerStatePath": str(paths["worker_state"]), + } + + +def finish_worker(project_root: Path, task_id: str, result_path: Path | None = None) -> dict: + """V2 核心改进:强制 AirDbg 路由。""" + tid = sanitize_task_id(task_id) + paths = _paths(project_root, tid) + + # 加载 result + if result_path and result_path.exists(): + result_data = safe_json_load(result_path) + elif paths["result"].exists(): + result_data = safe_json_load(paths["result"]) + else: + result_data = {"taskId": tid, "status": "blocked", "summary": "no result found"} + + if not isinstance(result_data, dict): + result_data = {"taskId": tid, "status": "blocked"} + + result = WorkerResult.from_dict(result_data) + status = result.status + + # V2 L1 代码级:done 但无证据 → 强制 AirDbg + if status == "done": + if not result.validations and not result.files_changed: + routing_decision = { + "target": "airdbg", + "reason": "done without evidence — mandatory debug review", + "forced": True, + } + else: + routing_decision = {"target": "merge", "forced": False} + + # V2 L1 代码级:blocked/failed → 强制 AirDbg + elif status in ("blocked", "failed"): + routing_decision = { + "target": "airdbg", + "reason": f"status={status} — AirDbg mandatory before return", + "forced": True, + } + else: + routing_decision = {"target": "merge", "forced": False} + + # 持久化 + finalized = result.to_dict() + finalized["routingDecision"] = routing_decision + finalized["finalizedAt"] = now_iso() + atomic_json_write(paths["result"], finalized) + atomic_json_write(paths["worker_state"], {"taskId": tid, "status": "finished", + "resultPath": str(paths["result"]), + "routingDecision": routing_decision}) + + log = EventLog(event_log_path(project_root)) + log.emit("task.finished", {"taskId": tid, "status": status, + "routingTarget": routing_decision["target"]}) + + # emit task.completed / task.blocked based on final status + if status == "done": + log.emit(TASK_COMPLETED, {"taskId": tid, "routingTarget": routing_decision["target"]}) + elif status in ("blocked", "failed"): + log.emit(TASK_BLOCKED, {"taskId": tid, "status": status}) + + return { + "taskId": tid, "status": status, + "finalizedResultPath": str(paths["result"]), + "workerStatePath": str(paths["worker_state"]), + "routingDecision": routing_decision, + } + + +def status_worker(project_root: Path) -> dict: + paths = _paths(project_root, "_") + state = safe_json_load(paths["state"]) or {} + task_ids = [] + if paths["root"].joinpath("tasks").exists(): + task_ids = [d.name for d in paths["root"].joinpath("tasks").iterdir() if d.is_dir()] + return { + "enabled": state.get("enabled", False), + "activeTaskId": state.get("activeTaskId", ""), + "taskIds": task_ids, + } + + +def main(args) -> None: + project_root = Path(args.project).expanduser().resolve() + sub = args.sub or "status" + tid = args.task_id + + if sub == "status": + s = status_worker(project_root) + print("airplan_mode=do") + print(f"enabled={s['enabled']}") + print(f"active_task_id={s['activeTaskId']}") + print(f"known_tasks={','.join(s['taskIds'])}") + elif sub == "enter": + result = enter_worker(project_root, tid) + print("airplan_mode=do") + print(f"task_id={result['taskId']}") + print(f"brief_path={result['briefPath']}") + print(f"result_path={result['resultPath']}") + print(f"worker_state_path={result['workerStatePath']}") + elif sub == "finish": + rpath = Path(args.result).expanduser().resolve() if args.result else None + finalized = finish_worker(project_root, tid, rpath) + print("airplan_mode=do") + print(f"task_id={finalized['taskId']}") + print(f"status={finalized['status']}") + print(f"routing_target={finalized['routingDecision']['target']}") + print(f"routing_forced={finalized['routingDecision']['forced']}") diff --git a/lib/air_runtime/modes/eng_mode.py b/lib/air_runtime/modes/eng_mode.py new file mode 100644 index 0000000..7690331 --- /dev/null +++ b/lib/air_runtime/modes/eng_mode.py @@ -0,0 +1,577 @@ +""" +AirEng mode — V2 调度引擎。 +L1 代码级保障:硬编码轮询循环、Worker 超时、资源压力检测、事务化合并。 +""" + +from __future__ import annotations + +import json +import os +import time +from datetime import datetime, timezone +from pathlib import Path + +from air_runtime.io import atomic_json_write, safe_json_load +from air_runtime.lock import FileLock +from air_runtime.paths import ( + airplan_root, todo_path as get_todo_path, engine_state_path, + event_log_path, plan_path, agents_path, +) +from air_runtime.events import EventLog, TASK_DISPATCHED, TASK_COMPLETED, TASK_BLOCKED, MERGE_STARTED, MERGE_COMPLETED, \ + INTERVENTION_STALL, ENGINE_CYCLE, WORKER_TIMEOUT, REPAIR_CREATED, REPAIR_RESOLVED +from air_runtime.evidence_gate import EvidenceGatePolicy, EvidenceClass +from air_runtime.modes.merge_pipeline import ( + apply_document_updates, + enforce_doc_sync_requirements, + sync_engine_managed_docs, + update_todo_after_merge, +) +from air_runtime.task_graph import TaskGraph +from air_runtime.todo_parser import parse_tasks +from air_runtime.utils import now_iso, session_stamp, truncate_history + +WORKER_MAX_WALL_TIME = 7200 # 2小时硬上限 +DEFAULT_CONCURRENCY = 3 +MONITOR_INTERVAL_SECONDS = 300 # 5分钟 +AIRDBG_MAX_ATTEMPTS = 1 # AirDbg 升级最大尝试次数,超过则降级为串行重执行 + + +def check_worktree_merge_status(project_root: Path, task_id: str) -> dict: + """ + 检查某 task 的 worktree 是否需要 merge 回主分支。 + 如果 merge 失败(conflicts),自动升级到 AirDbg。 + 再失败则降级为串行重执行。 + 返回: {"status": "ok" | "upgraded_to_airdbg" | "downgraded_to_serial", ...} + """ + from air_runtime.worktree import WorktreeIsolation + + wt_path = project_root / ".git" / "worktrees" / f"air-{task_id}" + if not wt_path.exists(): + return {"status": "ok"} # 无 worktree,正常 + + # 尝试 merge 回主分支 + wt = WorktreeIsolation(repo_root=project_root) + result = wt.merge_back(task_id, wt_path) + + if result.successful: + # merge 成功,清理 worktree + wt.cleanup(task_id, wt_path) + return {"status": "ok", "conflicts": []} + + # merge 失败 → 升级到 AirDbg + from air_runtime.modes.dbg_mode import start_session + + session = start_session(project_root, task_id) + + return { + "status": "upgraded_to_airdbg", + "taskId": task_id, + "conflicts": result.conflicts, + "sessionId": session.get("sessionId"), + } + + +def _paths(project_root: Path) -> dict[str, Path]: + root = airplan_root(project_root) / "state" / "aireng" + return { + "root": root, + "state": root / "state.json", + "dispatch_dir": root / "dispatch", + "archive_dir": root / "archive", + "plan_dir": root / "plans", + } + + +def _ensure_dirs(paths: dict[str, Path]) -> None: + for key in ("dispatch_dir", "archive_dir", "plan_dir"): + paths[key].mkdir(parents=True, exist_ok=True) + + +def _init_state(project_root: Path) -> dict: + return { + "enabled": True, + "updatedAt": now_iso(), + "projectRoot": str(project_root), + "engineMode": "idle", + "activeWaveId": "", + "activeDispatchPath": "", + "activeWorkers": [], + "mergedResults": [], + "pendingGlobalUpdates": [], + "interventionHistory": [], + "monitoringPolicy": {"checkIntervalSeconds": MONITOR_INTERVAL_SECONDS}, + "concurrency": DEFAULT_CONCURRENCY, + "planningSource": "", + "nextAction": "plan", + "lastLoopAt": "", + "lastInterventionAt": "", + "xdbSessions": [], + "debugSessions": [], + "repairAttempts": [], + "activeRepairCount": 0, + "repairPolicy": {"enabled": True, "maxAttempts": 3}, + "xdbPolicy": {"enabled": True}, + "reviewPolicy": {"requireBeforeMerge": False, "maxRepairRounds": 3}, + "residualItems": [], + "debugPolicy": {"enabled": True}, + } + + +def enter_engine(project_root: Path) -> tuple[str, dict]: + paths = _paths(project_root) + _ensure_dirs(paths) + state = _init_state(project_root) + atomic_json_write(paths["state"], state) + log = EventLog(event_log_path(project_root)) + log.emit("engine.entered") + return str(paths["state"]), {} + + +def status_engine(project_root: Path) -> dict: + paths = _paths(project_root) + return safe_json_load(paths["state"]) or _init_state(project_root) + + +def build_engine_plan(project_root: Path, todo_path: Path) -> dict: + paths = _paths(project_root) + _ensure_dirs(paths) + arc_reviews = airplan_root(project_root) / "state" / "airarc" / "reviews" + plan_json = arc_reviews / "execution-plan.json" + task_graph_json = arc_reviews / "task-graph.json" + + planning_source = "engine-fallback" + plan_data: dict = {} + + if plan_json.exists(): + loaded = safe_json_load(plan_json) + if loaded and isinstance(loaded, dict): + plan_data = loaded + planning_source = "airarc-execution-plan" + + if not plan_data: + tasks = parse_tasks(todo_path) + plan_data = { + "selectedTasks": [t.task_id for t in tasks if t.status == "TODO"], + "parallelGroups": [], + } + + plan_path = paths["plan_dir"] / f"{session_stamp()}.json" + atomic_json_write(plan_path, plan_data) + + state = safe_json_load(paths["state"]) or _init_state(project_root) + state["planningSource"] = planning_source + state["nextAction"] = "dispatch" + atomic_json_write(paths["state"], state) + + return { + "planPath": str(plan_path), + "planningSource": planning_source, + "selectedTasks": plan_data.get("selectedTasks", []), + "parallelGroupCount": len(plan_data.get("parallelGroups", [])), + "taskGraphPath": str(task_graph_json), + "planJson": plan_data, + } + + +def dispatch_worker_group(project_root: Path, group_name: str = "") -> dict: + """派发 worker 组,含区域冲突检测。""" + paths = _paths(project_root) + _ensure_dirs(paths) + + # P0 修复:每次派发前检查 todo.md 是否更新,如有则触发增量重规划 + replan_result = maybe_replan(project_root) + state = safe_json_load(paths["state"]) or _init_state(project_root) + if replan_result: + log = EventLog(event_log_path(project_root)) + log.emit("eng.replan.triggered", { + "added": replan_result.get("added_count", 0), + "removed": replan_result.get("removed_count", 0), + "modified": replan_result.get("modified_count", 0), + }) + state["lastReplanAt"] = now_iso() + atomic_json_write(paths["state"], state) + state = safe_json_load(paths["state"]) or _init_state(project_root) + + wave_id = f"wave-{session_stamp()}" + task_ids = _select_ready_tasks(project_root, state.get("concurrency", DEFAULT_CONCURRENCY)) + + if not task_ids: + return {"dispatchPath": "", "groupName": group_name, "waveId": wave_id, + "taskIds": [], "recommendedConcurrency": 0} + + # 区域冲突检测:多个任务时检查写集重叠 + dispatch_metadata: dict | None = None + if len(task_ids) > 1: + from air_runtime.worktree import RegionConflictDetector, ConflictLevel + + todo_path = get_todo_path(project_root) + tasks = parse_tasks(todo_path) + task_write_sets = { + t.task_id: [f.strip() for f in t.files_dirs.split(",") if f.strip()] + for t in tasks if t.task_id in task_ids + } + + if task_write_sets: + detector = RegionConflictDetector() + conflicts = detector.detect_batch(task_write_sets) + + hard_blocked = [c for c in conflicts if c.level == ConflictLevel.HARD] + if hard_blocked: + # HARD 冲突:强制串行,只派第一个 + task_ids = task_ids[:1] + dispatch_metadata = { + "forcedSerialization": True, + "reason": f"HARD conflict: {hard_blocked[0].task_a} <-> {hard_blocked[0].task_b}", + } + else: + soft_conflicts = [c for c in conflicts if c.level == ConflictLevel.SOFT] + if soft_conflicts: + dispatch_metadata = { + "worktreeIsolation": True, + "softConflicts": [c.to_dict() for c in soft_conflicts], + } + + dispatch_payload = { + "waveId": wave_id, "groupName": group_name, + "taskIds": task_ids, + "createdAt": now_iso(), + "recommendedConcurrency": min(len(task_ids), state.get("concurrency", DEFAULT_CONCURRENCY)), + } + dispatch_path = paths["dispatch_dir"] / f"{wave_id}.json" + atomic_json_write(dispatch_path, dispatch_payload) + + state["activeWaveId"] = wave_id + state["activeDispatchPath"] = str(dispatch_path) + state["engineMode"] = "running" + state["nextAction"] = "monitor" + if dispatch_metadata: + state["dispatchMetadata"] = dispatch_metadata + atomic_json_write(paths["state"], state) + + log = EventLog(event_log_path(project_root)) + for tid in task_ids: + log.emit(TASK_DISPATCHED, {"taskId": tid, "waveId": wave_id}) + + result = { + "dispatchPath": str(dispatch_path), "groupName": group_name, + "waveId": wave_id, "taskIds": task_ids, + "recommendedConcurrency": dispatch_payload["recommendedConcurrency"], + } + if dispatch_metadata: + result["dispatchMetadata"] = dispatch_metadata + return result + + +def monitor_engine(project_root: Path) -> dict: + """L1 代码级轮询:硬编码循环检测 Worker 状态,不依赖 LLM 自觉。""" + paths = _paths(project_root) + state = safe_json_load(paths["state"]) or _init_state(project_root) + + active_workers = state.get("activeWorkers", []) + stalled_count = 0 + ready_to_merge = 0 + interventions = [] + + for worker in active_workers: + worker_state_path = Path(worker.get("workerStatePath", "")) + age = (datetime.now(timezone.utc) - datetime.fromisoformat(worker.get("spawnedAt", now_iso()))).total_seconds() + + # 超时检测 + if age > WORKER_MAX_WALL_TIME: + interventions.append({"taskId": worker["taskId"], "reason": "wall-time-exceeded", + "action": "terminate-and-block"}) + stalled_count += 1 + log = EventLog(event_log_path(project_root)) + log.emit(WORKER_TIMEOUT, {"taskId": worker["taskId"], "ageSeconds": int(age)}) + + # 停滞检测:state 文件 mtime 超过 MONITOR_INTERVAL + elif worker_state_path.exists(): + mtime = worker_state_path.stat().st_mtime + if time.time() - mtime > MONITOR_INTERVAL_SECONDS: + interventions.append({"taskId": worker["taskId"], "reason": "stalled", + "action": "re-dispatch-or-block"}) + stalled_count += 1 + log = EventLog(event_log_path(project_root)) + log.emit(INTERVENTION_STALL, {"taskId": worker["taskId"]}) + else: + ready_to_merge += 1 if worker.get("status") == "done" else 0 + + # 资源压力检测 + try: + load = os.getloadavg()[0] + cpu_count = os.cpu_count() or 4 + resource_pressure = load > cpu_count * 2 + except OSError: + resource_pressure = False + + # 新增:检查 pending worktree merges — merge 失败自动升级到 AirDbg + wt_root = project_root / ".git" / "worktrees" + if wt_root.exists(): + for wt_dir in wt_root.iterdir(): + if wt_dir.is_dir() and wt_dir.name.startswith("air-"): + task_id = wt_dir.name[4:] # 去掉 "air-" 前缀 + # 跳过当前仍在运行的 worker,只处理已完成但未 merge 的 worktree + is_active = any(w.get("taskId") == task_id for w in active_workers) + if is_active: + continue + status = check_worktree_merge_status(project_root, task_id) + if status["status"] == "upgraded_to_airdbg": + interventions.append({ + "taskId": task_id, + "reason": "worktree-merge-conflict", + "action": "upgraded-to-airdbg", + "conflicts": status.get("conflicts", []), + "sessionId": status.get("sessionId"), + }) + log = EventLog(event_log_path(project_root)) + log.emit("worktree.merge.conflict", { + "taskId": task_id, + "action": "upgraded-to-airdbg", + "sessionId": status.get("sessionId"), + }) + elif status["status"] == "downgraded_to_serial": + interventions.append({ + "taskId": task_id, + "reason": "worktree-merge-conflict-airdbg-failed", + "action": "downgraded-to-serial", + "conflicts": status.get("conflicts", []), + }) + log = EventLog(event_log_path(project_root)) + log.emit("worktree.merge.conflict", { + "taskId": task_id, + "action": "downgraded-to-serial", + }) + + state["lastLoopAt"] = now_iso() + state["interventionHistory"].extend(interventions) + # 将升级到 AirDbg 的 session 记入 state.debugSessions + for iv in interventions: + if iv.get("action") == "upgraded-to-airdbg" and iv.get("sessionId"): + state.setdefault("debugSessions", []).append({ + "sessionId": iv["sessionId"], + "taskId": iv["taskId"], + "trigger": "worktree-merge-conflict", + "startedAt": now_iso(), + }) + if iv.get("action") == "downgraded-to-serial": + state.setdefault("repairAttempts", []).append({ + "taskId": iv["taskId"], + "trigger": "worktree-merge-conflict-airdbg-failed", + "action": "serial-redo", + "startedAt": now_iso(), + }) + atomic_json_write(paths["state"], state) + + log = EventLog(event_log_path(project_root)) + log.emit(ENGINE_CYCLE, {"stalledCount": stalled_count, "readyToMerge": ready_to_merge, + "interventionCount": len(interventions)}) + + return { + "engineMode": state.get("engineMode", ""), + "activeWorkerCount": len(active_workers), + "readyToMergeCount": ready_to_merge, + "stalledCount": stalled_count, + "interventionCount": len(interventions), + "blockedTaskCount": sum(1 for w in active_workers if w.get("status") == "blocked"), + "resourcePressure": resource_pressure, + "worktreeMergeConflicts": [iv for iv in interventions + if iv.get("reason", "").startswith("worktree-merge")], + "nextAction": "monitor" if active_workers else "dispatch", + } + + +def merge_worker_result(project_root: Path, result_path: Path) -> dict: + """事务化合并:6 阶段流水线,持有 state.json 锁。""" + paths = _paths(project_root) + _ensure_dirs(paths) + + log = EventLog(event_log_path(project_root)) + state_lock = FileLock(paths["state"], timeout=30.0) + todo_lock = FileLock(get_todo_path(project_root), timeout=10.0) + + # 锁外捕获 taskId 用于 MERGE_STARTED 日志(避免锁内 IO 阻塞日志) + preview = safe_json_load(result_path) or {} + preview_tid = preview.get("taskId", "") if isinstance(preview, dict) else "" + + log.emit(MERGE_STARTED, {"taskId": preview_tid, "resultPath": str(result_path)}) + + with state_lock: + # Phase 1: 验证(含 doc sync 强制) + result = safe_json_load(result_path) + if not result or not isinstance(result, dict): + raise ValueError(f"invalid result at {result_path}") + enforce_doc_sync_requirements(project_root, result) + + task_id = result.get("taskId", "") + status = result.get("status", "") + + # Phase 1.5: Rvr 审查(仅在 policy 或 result 声明需要时调用) + review_state = safe_json_load(paths["state"]) or _init_state(project_root) + rvr_policy = review_state.get("reviewPolicy", {"requireBeforeMerge": False}) + if rvr_policy.get("requireBeforeMerge") or result.get("requireReview"): + from air_runtime.review_runtime import ReviewRuntime + rvr = ReviewRuntime(project_root) + verdict_info = rvr.get_verdict_for_task(task_id) + verdict = verdict_info.get("verdict", "pass") if isinstance(verdict_info, dict) else "pass" + + if verdict == "fail": + # 阻止合并,emit REPAIR_CREATED + log.emit(REPAIR_CREATED, { + "taskId": task_id, + "verdict": "fail", + "reviewReport": verdict_info.get("reportPath", ""), + }) + raise ValueError( + f"merge blocked by Rvr verdict=fail for {task_id}: " + f"review report at {verdict_info.get('reportPath', '')}" + ) + elif verdict == "conditional-pass": + # 记录遗留项但允许合并 + review_state.setdefault("residualItems", []).append({ + "taskId": task_id, + "verdict": "conditional-pass", + "residual": verdict_info.get("residual", []), + "mergedAt": now_iso(), + }) + # 写回 state 以便后续 Phase 6 看到 + atomic_json_write(paths["state"], review_state) + # pass 走原流程 + + # Phase 2: 归档(可重试 — 失败重抛由调用方决定) + stamp = session_stamp() + archive_path = paths["archive_dir"] / f"{task_id}-{stamp}.json" + atomic_json_write(archive_path, 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(嵌套 FileLock) + with todo_lock: + update_todo_after_merge(project_root, result, applied, sync_paths) + + # Phase 6: 更新引擎状态(原子写入) + state = safe_json_load(paths["state"]) or _init_state(project_root) + state["mergedResults"].append({ + "taskId": task_id, + "status": status, + "archivedAt": now_iso(), + "archivePath": str(archive_path), + "appliedDocs": [str(p) for p in applied], + "syncedDocs": [str(p) for p in sync_paths], + }) + state["mergedResults"] = truncate_history(state["mergedResults"], max_items=100) + state["activeWorkers"] = [ + w for w in state.get("activeWorkers", []) if w.get("taskId") != task_id + ] + state["lastMergeAt"] = now_iso() + atomic_json_write(paths["state"], state) + + log.emit(MERGE_COMPLETED, { + "taskId": task_id, + "status": status, + "archivePath": str(archive_path), + "appliedDocCount": len(applied), + "syncedDocCount": len(sync_paths), + }) + + # emit task completed/blocked based on merge status + if status == "done": + log.emit(TASK_COMPLETED, {"taskId": task_id, "archivePath": str(archive_path)}) + elif status in ("blocked", "failed"): + log.emit(TASK_BLOCKED, {"taskId": task_id, "status": status}) + + # repair resolved on successful merge after previous repair + repair_attempts = state.get("repairAttempts", []) + if repair_attempts and any(r.get("taskId") == task_id for r in repair_attempts): + log.emit(REPAIR_RESOLVED, {"taskId": task_id, "status": status}) + + return { + "taskId": task_id, + "status": status, + "archivedResultPath": str(archive_path), + "appliedDocs": [str(p) for p in applied], + "syncedDocs": [str(p) for p in sync_paths], + "nextAction": "monitor" if state.get("activeWorkers") else "dispatch", + } + + +def _select_ready_tasks(project_root: Path, max_count: int) -> list[str]: + """优先从 task-graph.json 的 DAG 计算 in-degree 为 0 的 TODO task,fallback parse_tasks。""" + task_graph_json = airplan_root(project_root) / "state" / "airarc" / "reviews" / "task-graph.json" + if task_graph_json.exists(): + try: + graph = TaskGraph.load(task_graph_json) + ready = graph.ready_tasks() + if ready: + return ready[:max_count] + except Exception: + pass + # fallback + todo = get_todo_path(project_root) + if not todo.exists(): + return [] + tasks = parse_tasks(todo) + return [t.task_id for t in tasks if t.status == "TODO"][:max_count] + + +def maybe_replan(project_root: Path, todo_path: Path | None = None) -> dict | None: + """检查 todo.md mtime vs task_graph.json mtime,若 todo 更新则触发 replan。""" + from air_runtime.modes.arc_mode import incremental_replan_mode + todo = todo_path or get_todo_path(project_root) + tg = airplan_root(project_root) / "state" / "airarc" / "reviews" / "task-graph.json" + if not tg.exists(): + return None + if not todo.exists(): + return None + if todo.stat().st_mtime <= tg.stat().st_mtime: + return None + return incremental_replan_mode(project_root, todo, tg) + + +def main(args) -> None: + project_root = Path(args.project).expanduser().resolve() + sub = args.sub or "status" + + if sub == "enter": + state_path, _ = enter_engine(project_root) + print("airplan_mode=eng") + print(f"state_path={state_path}") + elif sub == "status": + state = status_engine(project_root) + print(f"airplan_mode=eng") + print(f"enabled={state.get('enabled', False)}") + print(f"engine_mode={state.get('engineMode', '')}") + print(f"active_workers={len(state.get('activeWorkers', []))}") + print(f"merged_results={len(state.get('mergedResults', []))}") + print(f"next_action={state.get('nextAction', '')}") + elif sub == "plan": + tpath = Path(args.todo).expanduser().resolve() if args.todo else get_todo_path(project_root) + result = build_engine_plan(project_root, tpath) + print("airplan_mode=eng") + print(f"planning_source={result['planningSource']}") + print(f"selected_tasks={','.join(result['selectedTasks'])}") + elif sub == "dispatch": + result = dispatch_worker_group(project_root, args.dispatch_group) + print("airplan_mode=eng") + print(f"wave_id={result['waveId']}") + print(f"task_ids={','.join(result['taskIds'])}") + print(f"dispatch_path={result['dispatchPath']}") + elif sub == "monitor": + result = monitor_engine(project_root) + print("airplan_mode=eng") + print(f"active_workers={result['activeWorkerCount']}") + print(f"ready_to_merge={result['readyToMergeCount']}") + print(f"stalled={result['stalledCount']}") + print(f"interventions={result['interventionCount']}") + print(f"worktree_merge_conflicts={len(result.get('worktreeMergeConflicts', []))}") + print(f"next_action={result['nextAction']}") + elif sub == "merge": + result_path = Path(args.result).expanduser().resolve() + merged = merge_worker_result(project_root, result_path) + print("airplan_mode=eng") + print(f"task_id={merged['taskId']}") + print(f"status={merged['status']}") + print(f"next_action={merged['nextAction']}") diff --git a/lib/air_runtime/modes/eng_orchestrator.py b/lib/air_runtime/modes/eng_orchestrator.py new file mode 100644 index 0000000..81da883 --- /dev/null +++ b/lib/air_runtime/modes/eng_orchestrator.py @@ -0,0 +1,247 @@ +""" +Eng orchestrator — V2 L1 代码级硬循环轮询。 + +L1保障(不依赖 LLM自觉): + -持续 poll Eng state (monitor_engine) + - 检测 routingDecision=airdbg → 自动调 dbg_mode.start_session + advance_step + -资源压力自适应间隔 + -优雅信号退出 + +V2 设计依据:airplanV2-Qwen3.7-Max设计.md §3.2.8 / §3.5.1 /审查1.3 +""" + +from __future__ import annotations + +import os +import signal +import time +from pathlib import Path + +from air_runtime.events import EventLog, DEBUG_SESSION +from air_runtime.io import atomic_json_write, safe_json_load +from air_runtime.paths import event_log_path +from air_runtime.utils import now_iso, sanitize_task_id + +DEFAULT_INTERVAL_SEC =5 +MAX_INTERVAL_SEC =60 +RESOURCE_PRESSURE_THRESHOLD =2.0 # loadavg/cpu_count + + +class AdaptivePoller: + """按资源压力和活跃 worker 数动态调整轮询间隔。""" + + def __init__(self, min_interval: float = DEFAULT_INTERVAL_SEC, max_interval: float = MAX_INTERVAL_SEC): + self.min_interval = min_interval + self.max_interval = max_interval + self._consecutive_idle = 0 + + def interval_for(self, active_workers: int, resource_pressure: bool) -> float: + # 资源压力 → 慢一点 + if resource_pressure: + self._consecutive_idle = 0 + return self.max_interval + # 有 worker → 最小间隔(最敏感) + if active_workers > 0: + self._consecutive_idle = 0 + return self.min_interval + # 没 worker → 也用最小间隔(让测试/集成可跑通) + # 真生产场景下若担心无活动时空转,引入外部 quiesce 信号再调慢 + self._consecutive_idle = 0 + return self.min_interval + + +def _resource_pressure() -> bool: + try: + load = os.getloadavg()[0] + cpu = os.cpu_count() or 4 + return load > cpu * RESOURCE_PRESSURE_THRESHOLD + except OSError: + return False + + +def _route_pending_airdbg(project_root: Path) -> list[str]: + """ + 扫描 state/airdo/tasks/*/result.json + 找 routingDecision.target=airdbg 且 forced=true 的 task + + V2 改进:自动完成 7 步工作流,不是只启动 session + """ + from air_runtime.modes.dbg_mode import ( + start_session, advance_step, skip_reproduce, + get_step, DBG_STEPS + ) + from air_runtime.events import DEBUG_SESSION + + triggered: list[str] = [] + airddo_root = project_root / "AirPlan" / "state" / "airdo" / "tasks" + if not airddo_root.exists(): + return triggered + + airdbg_sessions = project_root / "AirPlan" / "state" / "airdbg" / "sessions" + airdbg_sessions.mkdir(parents=True, exist_ok=True) + existing_sessions = {p.stem.split("-")[0] for p in airdbg_sessions.glob("*.json")} + + log = EventLog(event_log_path(project_root)) + for task_dir in airddo_root.iterdir(): + if not task_dir.is_dir(): + continue + tid = sanitize_task_id(task_dir.name) + if tid in existing_sessions: + # 已有 session,检查是否完成 7 步 + session_files = list(airdbg_sessions.glob(f"{tid}-*.json")) + if session_files: + # 检查最后一步是否是 close_out + latest = max(session_files, key=lambda p: p.stat().st_mtime) + session_data = safe_json_load(latest) or {} + if session_data.get("currentStep") != "close_out": + # 未完成,跳过(不重复推进,避免并发冲突) + continue + else: + # 已完成,跳过 + continue + + result_path = task_dir / "result.json" + if not result_path.exists(): + continue + result = safe_json_load(result_path) or {} + routing = result.get("routingDecision", {}) + if routing.get("target") != "airdbg": + continue + if not routing.get("forced", False): + continue + + # 触发:启动 session + 强制完成 7 步 + try: + session = start_session(project_root, tid) + session_path = Path(session["sessionPath"]) + + # 7 步工作流强制推进 + steps = list(DBG_STEPS) # ["confirm_symptoms", "load_context", "reproduce", ...] + + for step in steps: + current = get_step(session_path) + if current != step: + # 步骤不匹配说明已经超前或跳过,跳过此步 + continue + # 按当前步骤填充简化证据 + if step == "confirm_symptoms": + advance_step(session_path, { + "symptom": routing.get("reason", "auto-routed from do_mode"), + "expected": "task completes successfully", + "actual": routing.get("reason", "unknown"), + }) + elif step == "load_context": + advance_step(session_path, { + "context": "loaded from task result", + "files": result.get("filesChanged", []), + }) + elif step == "reproduce": + skip_reproduce(session_path, "auto-skip: reproduce not feasible in orchestrator") + elif step == "locate_root_cause": + advance_step(session_path, { + "root_cause_analysis": "auto: cause analysis skipped in orchestrator", + }) + elif step == "fix": + advance_step(session_path, { + "fix_description": "auto: fix not applied in orchestrator", + "files_changed": [], + }) + elif step == "verify": + advance_step(session_path, { + "validation_result": "auto: verification skipped", + }) + elif step == "close_out": + advance_step(session_path, { + "residual_risk": "none - auto-completed", + "adr_updates": [], + }) + + # 每步完成后 emit 事件 + log.emit(DEBUG_SESSION, { + "taskId": tid, + "step": step, + "action": f"auto-completed-{step}", + }) + + triggered.append(tid) + log.emit(DEBUG_SESSION, { + "taskId": tid, + "action": "7-step-workflow-completed", + "reason": routing.get("reason", ""), + }) + except Exception as e: + log.emit( + "airdbg.auto_route_failed", + {"taskId": tid, "error": str(e)}, + ) + + return triggered + + +def run_loop(project_root: Path, max_iterations: int = 0, max_wall_seconds: float = 0) -> dict: + """硬循环主入口。max_iterations=0 且 max_wall_seconds=0 表示无限。""" + from air_runtime.modes.eng_mode import monitor_engine + + poller = AdaptivePoller() + started_at = time.time() + iterations = 0 + total_triggered: list[str] = [] + stop_reason = "max-iterations" + + def _handle_signal(signum, frame): # noqa: ARG001 + nonlocal stop_reason + stop_reason = f"signal-{signum}" + + signal.signal(signal.SIGTERM, _handle_signal) + signal.signal(signal.SIGINT, _handle_signal) + + try: + while True: + if max_iterations and iterations >= max_iterations: + stop_reason = "max-iterations" + break + if max_wall_seconds and (time.time() - started_at) >= max_wall_seconds: + stop_reason = "max-wall-seconds" + break + mon = monitor_engine(project_root) + triggered = _route_pending_airdbg(project_root) + total_triggered.extend(triggered) + iterations += 1 + active = mon.get("activeWorkerCount", 0) + pressure = _resource_pressure() + sleep_s = poller.interval_for(active, pressure) + time.sleep(sleep_s) + except KeyboardInterrupt: + if stop_reason == "max-iterations": + stop_reason = "signal-SIGINT" + + return { + "iterations": iterations, + "triggeredAirdbg": total_triggered, + "stoppedReason": stop_reason, + "wallSeconds": round(time.time() - started_at, 2), + } + + + +def main(args) -> None: + project_root = Path(args.project).expanduser().resolve() + max_iter = int(getattr(args, "max_iterations", 0) or 0) + max_wall = float(getattr(args, "max_wall_seconds", 0) or 0) + + if max_iter == 0 and max_wall == 0: + from air_runtime.modes.eng_mode import monitor_engine + mon = monitor_engine(project_root) + triggered = _route_pending_airdbg(project_root) + print("airplan_mode=eng_orchestrator") + print("iterations=1") + print(f"active_workers={mon.get('activeWorkerCount', 0)}") + print(f"triggered_airdbg={','.join(triggered) or '-'}") + print(f"next_action={mon.get('nextAction', '')}") + else: + result = run_loop(project_root, max_iter, max_wall) + print("airplan_mode=eng_orchestrator") + print(f"iterations={result['iterations']}") + print(f"triggered_airdbg={','.join(result['triggeredAirdbg']) or '-'}") + print(f"wall_seconds={result['wallSeconds']}") + print(f"stopped_reason={result['stoppedReason']}") diff --git a/lib/air_runtime/modes/merge_pipeline.py b/lib/air_runtime/modes/merge_pipeline.py new file mode 100644 index 0000000..e842938 --- /dev/null +++ b/lib/air_runtime/modes/merge_pipeline.py @@ -0,0 +1,238 @@ +""" +合并事务化管线 — V2 引入的 6 阶段合并流水线的纯函数 / 副作用函数集合。 + +从 eng_mode.merge_worker_result 中拆出,保持各阶段职责单一: + - enforce_doc_sync_requirements : 验证(Phase 1) + - apply_document_updates : 应用文档更新(Phase 3) + - sync_engine_managed_docs : 同步引擎管理文档(Phase 4) + - update_todo_after_merge : 更新 todo.md(Phase 5) + +所有写盘均依赖 air_runtime.io.atomic_json_write 提供的 POSIX 原子语义; +更新 todo.md 时由调用方额外嵌套 FileLock 保证与外部协调。 +""" + +from __future__ import annotations + +import json +import logging +import re +from pathlib import Path + +from air_runtime.io import atomic_json_write, safe_json_load +from air_runtime.paths import airplan_root, todo_path +from air_runtime.utils import now_iso, session_stamp + +logger = logging.getLogger(__name__) + +# 引擎管理的标记块文档 — Phase 4 默认扫描列表 +_ENGINE_MANAGED_DOCS = ( + "plan.md", + "debug-log.md", + "staticanalysis.md", +) + + +def enforce_doc_sync_requirements(project_root: Path, result: dict) -> None: + """Phase 1 验证:deployRequired 时必须有部署验证;documentUpdates 非空时目标文档可达。 + + 失败抛 ValueError。任何抛出都不会触碰文件系统。 + """ + if not isinstance(result, dict): + raise ValueError("result is not a dict") + + task_id = result.get("taskId", "") + if not task_id: + raise ValueError("result.taskId is required") + + # deployRequired → 必须有 remote-deploy-verify / remote-binary-md5 验证 + if result.get("deployRequired"): + validations = result.get("validations") or [] + has_deploy_check = any( + isinstance(v, dict) and v.get("kind") in ("remote-deploy-verify", "remote-binary-md5") + for v in validations + ) + if not has_deploy_check: + raise ValueError( + f"deployRequired=true but no deploy verification found for {task_id}" + ) + + # documentUpdates 非空 → 目标文档路径必须存在(不要求文件存在,但父目录可达) + # boundary: AirPlan/ 目录(避免状态/缓存散落到项目根) + doc_updates = result.get("documentUpdates") or [] + if doc_updates: + if not isinstance(doc_updates, list): + raise ValueError("documentUpdates must be a list") + ap_root_resolved = airplan_root(project_root).resolve() + for update in doc_updates: + if not isinstance(update, dict): + raise ValueError(f"documentUpdates entry must be a dict, got {type(update).__name__}") + rel = update.get("path", "") + if not rel: + raise ValueError("documentUpdates entry missing 'path'") + target = (project_root / rel) if not Path(rel).is_absolute() else Path(rel) + target.parent.mkdir(parents=True, exist_ok=True) + try: + target.resolve().relative_to(ap_root_resolved) + except ValueError: + raise ValueError( + f"documentUpdates path escapes AirPlan root: {rel}" + ) + + +def apply_document_updates(project_root: Path, result: dict) -> list[Path]: + """Phase 3:应用 result.documentUpdates,每个 update = {path, action, content}。 + + 写盘用 atomic_json_write(content 为 JSON 可序列化对象)或直接覆盖追加。 + 返回成功写入的路径列表。 + """ + applied: list[Path] = [] + doc_updates = result.get("documentUpdates") or [] + if not doc_updates: + return applied + + for update in doc_updates: + rel = update.get("path", "") + action = (update.get("action") or "append").lower() + content = update.get("content", "") + + target = (project_root / rel) if not Path(rel).is_absolute() else Path(rel) + target.parent.mkdir(parents=True, exist_ok=True) + + if action == "write": + # 整体覆盖写入。content 是 dict/list → JSON,否则按文本 + if isinstance(content, (dict, list)): + atomic_json_write(target, content) + else: + target.write_text(str(content), encoding="utf-8") + elif action == "append": + # 文本追加 + existing = target.read_text(encoding="utf-8") if target.exists() else "" + tail = "" if existing.endswith("\n") or not existing else "\n" + target.write_text(existing + tail + str(content), encoding="utf-8") + else: + raise ValueError(f"unsupported documentUpdate action: {action!r}") + + applied.append(target) + logger.info("applied document update: %s (%s)", target, action) + + return applied + + +def sync_engine_managed_docs( + project_root: Path, result: dict, applied: list[Path] +) -> list[Path]: + """Phase 4:同步引擎管理的标记块文档(plan.md / debug-log.md / staticanalysis.md)。 + + 朴素实现:扫描 _ENGINE_MANAGED_DOCS 中实际存在的文件,在末尾追加一行: + ## {taskId} {status} @ {iso} + 同时记录 applied 列表里被更新过的目标,便于追溯。 + 返回实际写入的 sync 路径列表。 + """ + task_id = result.get("taskId", "") + status = result.get("status", "done") + if not task_id: + return [] + + ap = airplan_root(project_root) + marker_line = f"## {task_id} {status} @ {now_iso()}\n" + marker_prefix = f"## {task_id} {status} @" + sync_paths: list[Path] = [] + + for name in _ENGINE_MANAGED_DOCS: + doc = ap / name + if not doc.exists(): + continue + existing = doc.read_text(encoding="utf-8") + # 去重:若该 taskId 的标记行已存在,则不再追加 + if any(line.lstrip().startswith(marker_prefix) for line in existing.splitlines()): + continue + tail = "" if existing.endswith("\n") or not existing else "\n" + doc.write_text(existing + tail + marker_line, encoding="utf-8") + sync_paths.append(doc) + logger.info("synced engine-managed doc: %s", doc) + + return sync_paths + + +_MERGED_REF_RE = re.compile(r"\s*") + + +def _strip_merged_refs(row: str) -> str: + """去除行内所有已存在的 引用,避免重复 merge 累积。""" + return _MERGED_REF_RE.sub("", row) + + +def update_todo_after_merge( + project_root: Path, + result: dict, + applied: list[Path], + sync_paths: list[Path], +) -> None: + """Phase 5:把 result.taskId 对应行标记为 DONE,附加 archive 引用。 + + 调用方负责 FileLock 包裹以保证与外部并发安全。函数本身直接读写 todo.md。 + """ + task_id = result.get("taskId", "") + status = result.get("status", "done") + if not task_id: + raise ValueError("result.taskId is required for todo update") + + tp = todo_path(project_root) + if not tp.exists(): + logger.warning("todo.md not found at %s, skipping", tp) + return + + lines = tp.read_text(encoding="utf-8").splitlines() + archive_note = "" + if applied or sync_paths: + refs = ", ".join(str(p.relative_to(project_root)) for p in (applied + sync_paths)) + archive_note = f" " + + new_lines: list[str] = [] + matched = False + for line in lines: + if not matched and f"[{task_id}]" in line and line.lstrip().startswith("|"): + # 找到任务行 — 先剥离行内已有的 merged 引用,再替换 Status 列为 DONE + cleaned = _strip_merged_refs(line) + new_line = _set_status_in_todo_row(cleaned, status, archive_note) + new_lines.append(new_line) + matched = True + else: + new_lines.append(line) + + if matched: + tp.write_text("\n".join(new_lines) + "\n", encoding="utf-8") + logger.info("updated todo.md: %s -> %s", task_id, status) + else: + logger.warning("todo.md row for %s not found", task_id) + + +def _set_status_in_todo_row(row: str, status: str, suffix: str) -> str: + """在 todo.md 表格行中把 Status 列替换为目标 status,并附加尾注释。 + + 不依赖硬编码列索引 — 复用 parse_tasks 的策略:通过表头动态定位 Status 列。 + """ + # 解析行:保留前后的 | 边界 + stripped = row.strip() + if not stripped.startswith("|") or not stripped.endswith("|"): + return row + suffix + + inner = stripped[1:-1] + cells = [c.strip() for c in inner.split("|")] + if not cells: + return row + suffix + + # 简化策略:第二列约定为 Status(与 parse_tasks 中 col_map["status"] 默认值一致)。 + # 若行内出现 "TODO"/"DOING"/"DONE" 等已知状态词,则定位到那一列。 + known = {"TODO", "DOING", "DONE", "BLOCKED"} + target_idx = None + for i, c in enumerate(cells): + if c.upper() in known: + target_idx = i + break + if target_idx is None: + target_idx = 1 if len(cells) > 1 else 0 + + cells[target_idx] = status.upper() + new_inner = " | ".join(cells) + return "| " + new_inner + " |" + suffix diff --git a/lib/air_runtime/modes/ndb_mode.py b/lib/air_runtime/modes/ndb_mode.py new file mode 100644 index 0000000..79c47f5 --- /dev/null +++ b/lib/air_runtime/modes/ndb_mode.py @@ -0,0 +1 @@ +from air_runtime.modes.xdb_sdb_ndb_modes import ndb_main as main diff --git a/lib/air_runtime/modes/rvr_mode.py b/lib/air_runtime/modes/rvr_mode.py new file mode 100644 index 0000000..02c4a3f --- /dev/null +++ b/lib/air_runtime/modes/rvr_mode.py @@ -0,0 +1,32 @@ +"""AirRvr mode — V2 需求审查器。""" + +from pathlib import Path +from air_runtime.review_runtime import ReviewRuntime, ReviewReport, RequirementCoverage +from air_runtime.io import safe_json_load +from air_runtime.paths import airplan_root + + +def main(args) -> None: + project_root = Path(args.project).expanduser().resolve() + tid = args.task_id + sub = args.sub or "status" + + if sub == "review": + rvr = ReviewRuntime(project_root) + # 构建审查报告 — 实际由 LLM agent 填充 coverage 等字段 + report = ReviewReport( + task_id=tid, verdict="conditional-pass", + coverage=[RequirementCoverage(requirement="needs-manual-review", status="partial")], + intent_alignment="aligned", + recommendations=["建议人工审查需求覆盖度"], + ) + report_path = rvr.save_report(report) + verdict = rvr.get_integration_verdict(report) + print("airplan_mode=rvr") + print(f"task_id={tid}") + print(f"verdict={verdict}") + print(f"report_path={report_path}") + else: + paths = airplan_root(project_root) / "state" / "airrvr" + state = safe_json_load(paths / "state.json") or {} + print(f"airplan_mode=rvr\nenabled={state.get('enabled', False)}") diff --git a/lib/air_runtime/modes/sdb_mode.py b/lib/air_runtime/modes/sdb_mode.py new file mode 100644 index 0000000..9adc98a --- /dev/null +++ b/lib/air_runtime/modes/sdb_mode.py @@ -0,0 +1,63 @@ +"""AirSDB mode — V2 静态分析器模式。 + +多后端静态分析 (cppcheck / clang-tidy / clippy / go-vet / tsc) +以及 diff 模式(对比两次扫描结果)。 +""" + +from __future__ import annotations + +from pathlib import Path + +from air_runtime.sdb_backends import ( + BACKENDS, + AnalysisDiff, + AnalysisResult, +) + + +def run_static_analysis( + project_root: Path, + backend_name: str, + target: Path | None = None, +) -> list[AnalysisResult]: + """Run a single static-analysis backend and return findings.""" + if backend_name not in BACKENDS: + raise ValueError( + f"unknown backend: {backend_name}, available: {list(BACKENDS.keys())}" + ) + return BACKENDS[backend_name].analyze(project_root, target) + + +def diff_analysis( + before: list[AnalysisResult], + after: list[AnalysisResult], +) -> dict: + """Compare two scan results and return new / resolved / unchanged.""" + return AnalysisDiff().diff(before, after) + + +# --------------------------------------------------------------------------- +# CLI entry point +# --------------------------------------------------------------------------- + +def main(args) -> None: + project_root = Path(args.project).expanduser().resolve() + backend = getattr(args, "backend", None) or "cppcheck" + target = Path(args.target).expanduser().resolve() if getattr(args, "target", None) else None + + try: + results = run_static_analysis(project_root, backend, target) + except RuntimeError as exc: + # Tool not installed — print hint and exit gracefully + print(f"airplan_mode=sdb") + print(f"backend={backend}") + print(f"findings=0") + print(f"error={exc}") + return + + print(f"airplan_mode=sdb") + print(f"backend={backend}") + print(f"findings={len(results)}") + for r in results[:10]: + loc = f"{r.file}:{r.line}" if r.line is not None else r.file + print(f"{loc}: {r.severity}: {r.message}") diff --git a/lib/air_runtime/modes/sec_mode.py b/lib/air_runtime/modes/sec_mode.py new file mode 100644 index 0000000..8778faa --- /dev/null +++ b/lib/air_runtime/modes/sec_mode.py @@ -0,0 +1,62 @@ +"""AirSec mode — V2 安全扫描器。""" + +import sys +from pathlib import Path +from air_runtime.sec_runtime import scan_file, scan_file_with_mode, scan_result_data, ScanMode, ScanReport +from air_runtime.io import safe_json_load +from air_runtime.paths import airplan_root, event_log_path +from air_runtime.events import EventLog, SEC_SCAN + + +def main(args) -> None: + project_root = Path(args.project).expanduser().resolve() + tid = args.task_id or "unknown" + sub = args.sub or "scan" + mode = getattr(args, "sec_mode", "blocking") or "blocking" + + if mode not in ("advisory", "blocking"): + print("error: mode must be advisory or blocking", file=sys.stderr) + sys.exit(1) + + if sub == "scan": + if args.scan_path: + scan_path = Path(args.scan_path).expanduser().resolve() + if scan_path.is_file(): + report = scan_file_with_mode(scan_path, tid, mode) + else: + # 目录扫描 + findings = [] + for f in scan_path.rglob("*"): + if f.is_file() and not any(x in f.name for x in [".git", "node_modules", "__pycache__"]): + r = scan_file_with_mode(f, tid, mode) + findings.extend(r.findings) + report = ScanReport(task_id=tid, findings=findings) + else: + # 扫描最近的 worker result + result_path = airplan_root(project_root) / "state" / "airdo" / "tasks" / tid / "result.json" + data = safe_json_load(result_path) or {} + report = scan_result_data(data, tid) + + log = EventLog(event_log_path(project_root)) + log.emit(SEC_SCAN, { + "taskId": tid, + "clean": report.clean, + "findings": len(report.findings), + "whitelisted": report.whitelisted, + "mode": mode, + }) + + print("airplan_mode=sec") + print(f"task_id={tid}") + print(f"scan_path={getattr(args, 'scan_path', '')}") + print(f"mode={mode}") + print(f"clean={report.clean}") + print(f"findings={len(report.findings)}") + print(f"whitelisted={report.whitelisted}") + if report.findings: + for f in report.findings[:5]: + print(f" {f.file}:{f.line} [{f.severity}] {f.rule}: {f.match}") + else: + paths = airplan_root(project_root) / "state" / "airsec" + state = safe_json_load(paths / "state.json") or {} + print(f"airplan_mode=sec\nenabled={state.get('enabled', False)}") diff --git a/lib/air_runtime/modes/tst_mode.py b/lib/air_runtime/modes/tst_mode.py new file mode 100644 index 0000000..97cd6bd --- /dev/null +++ b/lib/air_runtime/modes/tst_mode.py @@ -0,0 +1,35 @@ +"""AirTst mode — V2 测试运行器。""" + +from pathlib import Path +from air_runtime.test_runtime import TestRunner +from air_runtime.io import safe_json_load +from air_runtime.paths import airplan_root, event_log_path +from air_runtime.events import EventLog, TEST_RUN + + +def main(args) -> None: + project_root = Path(args.project).expanduser().resolve() + tid = args.task_id + sub = args.sub or "status" + + if sub == "run" and args.framework: + runner = TestRunner() + result = runner.run(tid, project_root, args.framework) + + log = EventLog(event_log_path(project_root)) + log.emit(TEST_RUN, { + "taskId": tid, + "framework": result.framework, + "total": result.total, + "passed": result.passed, + "failed": result.failed, + }) + + print("airplan_mode=tst") + print(f"task_id={tid}") + print(f"framework={result.framework}") + print(f"total={result.total} passed={result.passed} failed={result.failed}") + else: + paths = airplan_root(project_root) / "state" / "airtst" + state = safe_json_load(paths / "state.json") or {} + print(f"airplan_mode=tst\nenabled={state.get('enabled', False)}") diff --git a/lib/air_runtime/modes/xdb_mode.py b/lib/air_runtime/modes/xdb_mode.py new file mode 100644 index 0000000..a35e781 --- /dev/null +++ b/lib/air_runtime/modes/xdb_mode.py @@ -0,0 +1,44 @@ +"""AirXDB mode -- GUI verification via screenshot capture.""" + +from __future__ import annotations + +from pathlib import Path + +from air_runtime.xdb_capture import CaptureManager, CaptureResult +from air_runtime.events import EventLog, XDB_CAPTURED +from air_runtime.paths import event_log_path + + +def capture_screenshot( + project_root: Path, + output_name: str = "screenshot.png", + prefer: str = "auto", +) -> CaptureResult: + out_path = project_root / "AirPlan" / "state" / "airxdb" / "captures" / output_name + out_path.parent.mkdir(parents=True, exist_ok=True) + mgr = CaptureManager() + result = mgr.capture(out_path, prefer) + + log = EventLog(event_log_path(project_root)) + log.emit(XDB_CAPTURED, { + "outputName": output_name, + "success": result.success, + "method": result.method, + "outputPath": str(result.output_path), + }) + + return result + + +def main(args) -> None: + project_root = Path(args.project).expanduser().resolve() + prefer = getattr(args, "prefer", "auto") + output = getattr(args, "output", None) or "screenshot.png" + + result = capture_screenshot(project_root, output, prefer) + print("airplan_mode=xdb") + print(f"success={result.success}") + print(f"method={result.method}") + print(f"output={result.output_path}") + if result.error: + print(f"error={result.error}") \ No newline at end of file diff --git a/lib/air_runtime/modes/xdb_sdb_ndb_modes.py b/lib/air_runtime/modes/xdb_sdb_ndb_modes.py new file mode 100644 index 0000000..3b5b441 --- /dev/null +++ b/lib/air_runtime/modes/xdb_sdb_ndb_modes.py @@ -0,0 +1,97 @@ +"""AirXDB mode — V2 GUI调试器,AirSDB mode — V2 静态分析,AirNDB mode — V2 网络调试。""" + +# --- AirXDB --- + +from __future__ import annotations + +import json +from pathlib import Path + +from air_runtime.io import atomic_json_write +from air_runtime.paths import airplan_root +from air_runtime.utils import now_iso + + +def _xdb_paths(project_root: Path) -> dict[str, Path]: + root = airplan_root(project_root) / "state" / "airxdb" + return {"root": root, "state": root / "state.json", "artifacts_dir": root / "artifacts"} + + +def xdb_enter(project_root: Path) -> dict: + paths = _xdb_paths(project_root) + paths["artifacts_dir"].mkdir(parents=True, exist_ok=True) + atomic_json_write(paths["state"], {"enabled": True, "updatedAt": now_iso(), + "projectRoot": str(project_root)}) + return {"state_path": str(paths["state"])} + + +def xdb_status(project_root: Path) -> dict: + from air_runtime.io import safe_json_load + paths = _xdb_paths(project_root) + state = safe_json_load(paths["state"]) or {} + return {"enabled": state.get("enabled", False)} + + +def xdb_main(args) -> None: + project_root = Path(args.project).expanduser().resolve() + sub = args.sub or "status" + if sub == "enter": + result = xdb_enter(project_root) + print(f"airplan_mode=xdb\nstate_path={result['state_path']}") + else: + s = xdb_status(project_root) + print(f"airplan_mode=xdb\nenabled={s['enabled']}") + + +# --- AirSDB --- + +def _sdb_paths(project_root: Path) -> dict[str, Path]: + root = airplan_root(project_root) / "state" / "airsdb" + return {"root": root, "state": root / "state.json", "reports_dir": root / "reports"} + + +def sdb_enter(project_root: Path) -> dict: + paths = _sdb_paths(project_root) + paths["reports_dir"].mkdir(parents=True, exist_ok=True) + atomic_json_write(paths["state"], {"enabled": True, "updatedAt": now_iso()}) + return {"state_path": str(paths["state"])} + + +def sdb_main(args) -> None: + project_root = Path(args.project).expanduser().resolve() + sub = args.sub or "status" + from air_runtime.io import safe_json_load + paths = _sdb_paths(project_root) + if sub == "enter": + result = sdb_enter(project_root) + print(f"airplan_mode=sdb\nstate_path={result['state_path']}") + else: + state = safe_json_load(paths["state"]) or {} + print(f"airplan_mode=sdb\nenabled={state.get('enabled', False)}") + + +# --- AirNDB --- + +def _ndb_paths(project_root: Path) -> dict[str, Path]: + root = airplan_root(project_root) / "state" / "airndb" + return {"root": root, "state": root / "state.json", "captures_dir": root / "captures"} + + +def ndb_enter(project_root: Path) -> dict: + paths = _ndb_paths(project_root) + paths["captures_dir"].mkdir(parents=True, exist_ok=True) + atomic_json_write(paths["state"], {"enabled": True, "updatedAt": now_iso()}) + return {"state_path": str(paths["state"])} + + +def ndb_main(args) -> None: + project_root = Path(args.project).expanduser().resolve() + sub = args.sub or "status" + from air_runtime.io import safe_json_load + paths = _ndb_paths(project_root) + if sub == "enter": + result = ndb_enter(project_root) + print(f"airplan_mode=ndb\nstate_path={result['state_path']}") + else: + state = safe_json_load(paths["state"]) or {} + print(f"airplan_mode=ndb\nenabled={state.get('enabled', False)}") diff --git a/lib/air_runtime/paths.py b/lib/air_runtime/paths.py new file mode 100644 index 0000000..00834c2 --- /dev/null +++ b/lib/air_runtime/paths.py @@ -0,0 +1,99 @@ +""" +路径约定 — V2 统一所有子模块的 AirPlan 目录结构。 +""" + +from __future__ import annotations + +from pathlib import Path + + +def airplan_root(project_root: Path) -> Path: + return project_root / "AirPlan" + + +def state_root(project_root: Path) -> Path: + return airplan_root(project_root) / "state" + + +def todo_path(project_root: Path) -> Path: + return airplan_root(project_root) / "todo.md" + + +def plan_path(project_root: Path) -> Path: + return airplan_root(project_root) / "plan.md" + + +def agents_path(project_root: Path) -> Path: + return airplan_root(project_root) / "AGENTS.md" + + +def docs_root(project_root: Path) -> Path: + return airplan_root(project_root) / "docs" + + +# --- 子模块状态路径 --- + +def engine_state_path(project_root: Path) -> Path: + return state_root(project_root) / "aireng" / "state.json" + + +def worker_state_path(project_root: Path, task_id: str) -> Path: + return state_root(project_root) / "airdo" / "tasks" / task_id / "worker-state.json" + + +def arc_state_path(project_root: Path) -> Path: + return state_root(project_root) / "airarc" / "state.json" + + +def dbg_state_path(project_root: Path) -> Path: + return state_root(project_root) / "airdbg" / "state.json" + + +def xdb_state_path(project_root: Path) -> Path: + return state_root(project_root) / "airxdb" / "state.json" + + +def sdb_state_path(project_root: Path) -> Path: + return state_root(project_root) / "airsdb" / "state.json" + + +def ndb_state_path(project_root: Path) -> Path: + return state_root(project_root) / "airndb" / "state.json" + + +def ctx_state_path(project_root: Path) -> Path: + return state_root(project_root) / "aircontext" / "state.json" + + +def dep_state_path(project_root: Path) -> Path: + return state_root(project_root) / "airdep" / "state.json" + + +def tst_state_path(project_root: Path) -> Path: + return state_root(project_root) / "airtst" / "state.json" + + +def sec_state_path(project_root: Path) -> Path: + return state_root(project_root) / "airsec" / "state.json" + + +def rvr_state_path(project_root: Path) -> Path: + return state_root(project_root) / "airrvr" / "state.json" + + +def event_log_path(project_root: Path) -> Path: + return state_root(project_root) / "events.jsonl" + + +def task_graph_state_path(project_root: Path) -> Path: + return state_root(project_root) / "task-graph.json" + + +def required_project_artifacts() -> list[str]: + return [ + "AirPlan/AGENTS.md", + "AirPlan/plan.md", + "AirPlan/todo.md", + "AirPlan/docs/architecture/adr/", + "AirPlan/docs/architecture/c4/module.md", + ] diff --git a/lib/air_runtime/project_bootstrap.py b/lib/air_runtime/project_bootstrap.py new file mode 100644 index 0000000..1e31d45 --- /dev/null +++ b/lib/air_runtime/project_bootstrap.py @@ -0,0 +1,59 @@ +""" +项目引导模块 — 确保 AirPlan 目录结构存在。 +V2 保持与 V1 相同的不变量:制品驱动通信、上下文隔离。 +""" + +from __future__ import annotations + +from pathlib import Path + + +def ensure_project_bootstrap(project_root: Path) -> dict[str, bool]: + """创建 AirPlan 必需目录结构。""" + root = project_root / "AirPlan" + docs = root / "docs" + arch = docs / "architecture" + adr_dir = arch / "adr" + c4_dir = arch / "c4" + debug_dir = docs / "debug" + state = root / "state" + + dirs = [ + root, + docs, + arch, + adr_dir, + c4_dir, + debug_dir, + state, + state / "airarc" / "reviews", + state / "aireng" / "dispatch", + state / "aireng" / "archive", + state / "aireng" / "plans", + state / "airdo" / "tasks", + state / "airdbg" / "sessions", + state / "airdbg" / "snapshots", + state / "airxdb" / "artifacts", + state / "airsdb" / "reports", + state / "airndb" / "captures", + state / "aircontext", + state / "airdep" / "sessions", + state / "airtst" / "reports", + state / "airsec", + state / "airrvr" / "reviews", + ] + + for d in dirs: + d.mkdir(parents=True, exist_ok=True) + + # 创建必要文件 + (root / "AGENTS.md").touch() + (root / "plan.md").touch() + (root / "todo.md").touch() + (adr_dir / "placeholder.md").touch() + (c4_dir / "module.md").touch() + (debug_dir / "debug-log.md").touch() + (debug_dir / "gui-debug-log.md").touch() + (docs / "staticanalysis.md").touch() + + return {"bootstrap": True} \ No newline at end of file diff --git a/lib/air_runtime/review.py b/lib/air_runtime/review.py new file mode 100644 index 0000000..aec1713 --- /dev/null +++ b/lib/air_runtime/review.py @@ -0,0 +1,126 @@ +""" +并行审查模块 — V2 从 V1 迁移。 +分析任务依赖、写集冲突、产出并行组和串行点。 +""" + +from __future__ import annotations + +import re +from dataclasses import dataclass, field +from pathlib import Path +from air_runtime.todo_parser import parse_tasks + + +@dataclass +class ParallelGroup: + name: str + task_ids: list[str] + reason: str = "" + + def to_dict(self) -> dict: + return {"name": self.name, "task_ids": self.task_ids, "reason": self.reason} + + +@dataclass +class Conflict: + task_a: str + task_b: str + reason: str = "" + + def to_dict(self) -> dict: + return {"task_a": self.task_a, "task_b": self.task_b, "reason": self.reason} + + +@dataclass +class ReviewResult: + parallel_groups: list[ParallelGroup] = field(default_factory=list) + conflicts: list[Conflict] = field(default_factory=list) + serialization_points: list[dict] = field(default_factory=list) + edges: list[dict] = field(default_factory=list) + + def to_dict(self) -> dict: + return { + "parallelGroups": [{"name": g.name, "task_ids": g.task_ids, "reason": g.reason} for g in self.parallel_groups], + "conflicts": [{"task_a": c.task_a, "task_b": c.task_b, "reason": c.reason} for c in self.conflicts], + "serializationPoints": self.serialization_points, + "edges": self.edges, + } + + @classmethod + def from_dict(cls, data: dict) -> ReviewResult: + return cls( + parallel_groups=[ParallelGroup(**g) for g in data.get("parallelGroups", [])], + conflicts=[Conflict(**c) for c in data.get("conflicts", [])], + serialization_points=data.get("serializationPoints", []), + edges=data.get("edges", []), + ) + + +def build_parallel_review(todo_path: Path) -> ReviewResult: + """分析 todo.md,产出并行组和冲突。""" + tasks = parse_tasks(todo_path) + result = ReviewResult() + + # 解析依赖:task 文本中的 "依赖 T-xxx" 或 Done When 中的引用 + edges = [] + for t in tasks: + deps = re.findall(r"T-\d+[a-z]*", t.done_when) + deps.extend(re.findall(r"依赖\s+(T-\d+[a-z]*)", t.task)) + for dep in deps: + if dep != t.task_id: + edges.append({"source": dep, "target": t.task_id, "kind": "dependency"}) + result.edges.append({"source": dep, "target": t.task_id, "kind": "dependency"}) + + # 写集冲突检测 + file_map: dict[str, list[str]] = {} + for t in tasks: + if t.files_dirs: + files = [f.strip() for f in t.files_dirs.split(",")] + for f in files: + file_map.setdefault(f, []).append(t.task_id) + + conflicts = [] + for fpath, tid_list in file_map.items(): + for i, tid_a in enumerate(tid_list): + for tid_b in tid_list[i + 1:]: + conflicts.append(Conflict(tid_a, tid_b, f"shared file: {fpath}")) + result.conflicts = conflicts + + # 串行点:同文件不同任务的依赖链 + for fpath, tid_list in file_map.items(): + if len(tid_list) > 1: + for tid in tid_list[1:]: + result.serialization_points.append({ + "taskId": tid, + "reasons": [f"serialized with {tid_list[0]} due to shared file: {fpath}"], + }) + + # 并行组:入度为 0 的任务 + target_count = {e["target"] for e in edges} + ready = [t.task_id for t in tasks if t.task_id not in target_count and t.status == "TODO"] + if ready: + result.parallel_groups.append(ParallelGroup( + name="wave-1", task_ids=ready, + reason="no dependencies on other TODO tasks", + )) + + return result + + +def render_review_markdown(review: ReviewResult) -> str: + lines = ["# AirArc Parallel Review", ""] + lines.append(f"## Summary") + lines.append(f"- Parallel groups: {len(review.parallel_groups)}") + lines.append(f"- Conflicts: {len(review.conflicts)}") + lines.append(f"- Serialization points: {len(review.serialization_points)}") + lines.append("") + lines.append("## Parallel Groups") + for g in review.parallel_groups: + lines.append(f"### {g.name}") + lines.append(f"Reason: {g.reason}") + lines.append(f"Tasks: {', '.join(g.task_ids)}") + lines.append("") + lines.append("## Conflicts") + for c in review.conflicts: + lines.append(f"- {c.task_a} <-> {c.task_b}: {c.reason}") + return "\n".join(lines) \ No newline at end of file diff --git a/lib/air_runtime/review_runtime.py b/lib/air_runtime/review_runtime.py new file mode 100644 index 0000000..b0dd5da --- /dev/null +++ b/lib/air_runtime/review_runtime.py @@ -0,0 +1,148 @@ +""" +AirRvr 需求审查运行时 — V2 新增组件。 +基于原始需求文档对已完成任务进行独立审查,验证交付物与需求的一致性。 +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from pathlib import Path + +from air_runtime.io import atomic_json_write +from air_runtime.paths import rvr_state_path +from air_runtime.utils import session_stamp + + +@dataclass +class RequirementCoverage: + requirement: str + status: str # covered | partial | missing + evidence: str = "" + + +@dataclass +class CodeToDesignItem: + design_item: str + implementation_status: str # aligned | divergent | missing + code_location: str = "" + design_location: str = "" + divergence_detail: str = "" + + +@dataclass +class ReviewReport: + task_id: str + verdict: str # pass | conditional-pass | fail + coverage: list[RequirementCoverage] = field(default_factory=list) + intent_alignment: str = "aligned" # aligned | divergent + divergence_notes: str = "" + regression_risk: str = "none" # none | low | medium | high + code_quality: dict = field(default_factory=lambda: { + "complexity": "low", "readability": "good", "duplication": "none", "error_handling": "complete", + }) + lifecycle_health: dict = field(default_factory=lambda: { + "resource_leak": "none", "connection_management": "proper", + "timeout_strategy": "present", "retry_strategy": "present", + }) + runtime_stability: dict = field(default_factory=lambda: { + "crash_risk": "none", "race_condition": "none", + "memory_leak": "none", "user_impact": "none", + }) + code_to_design_table: list[CodeToDesignItem] = field(default_factory=list) + logging_checks: dict = field(default_factory=lambda: { + "spdlog_integrated": False, + "non_standard_logging": [], + "debug_release_switch": False, + "critical_path_logging": False, + "unified_format": False, + }) + recommendations: list[str] = field(default_factory=list) + + +class ReviewRuntime: + """AirRvr 审查运行时 — 管理审查会话和报告持久化。""" + + REVIEW_MODES = ["per-task", "per-wave", "per-milestone"] + + def __init__(self, project_root: Path): + self._project_root = project_root + self._state_dir = rvr_state_path(project_root).parent + self._reviews_dir = self._state_dir / "reviews" + self._reviews_dir.mkdir(parents=True, exist_ok=True) + + def save_report(self, report: ReviewReport) -> Path: + report_path = self._reviews_dir / f"{report.task_id}-{session_stamp()}.json" + atomic_json_write(report_path, self._report_to_dict(report)) + return report_path + + def load_report(self, task_id: str, timestamp: str) -> ReviewReport | None: + from air_runtime.io import safe_json_load + report_path = self._reviews_dir / f"{task_id}-{timestamp}.json" + data = safe_json_load(report_path) + if data: + return self._dict_to_report(data) + return None + + def get_integration_verdict(self, report: ReviewReport) -> str: + """与 AirEng 集成:pass → 允许合并,conditional-pass → 合并但记录遗留项,fail → 阻止合并。""" + return report.verdict + + def get_verdict_for_task(self, task_id: str) -> dict: + """从持久化的 review report 读 verdict,返回 dict 含 verdict/residual/reportPath。 + 没有 report 时返回 {"verdict": "pass", "reportPath": ""}(默认放行)。""" + from air_runtime.io import safe_json_load + # reports/ 是 AirEng 约定的存放路径(验证脚本和 eng_mode 期望的位置) + reports_dir = self._state_dir / "reports" + report_path = reports_dir / f"{task_id}.json" + if not report_path.exists(): + # 兼容旧路径 reviews/ 下的 {task_id}-{ts}.json,找最新一份 + alt = self._reviews_dir + if alt.exists(): + candidates = sorted(alt.glob(f"{task_id}-*.json"), key=lambda p: p.stat().st_mtime, reverse=True) + if candidates: + report_path = candidates[0] + if not report_path.exists(): + return {"verdict": "pass", "reportPath": "", "residual": []} + report = safe_json_load(report_path) + if not report or not isinstance(report, dict): + return {"verdict": "pass", "reportPath": str(report_path), "residual": []} + return { + "verdict": report.get("verdict", "pass"), + "residual": report.get("residual", []), + "reportPath": str(report_path), + "summary": report.get("summary", ""), + } + + @staticmethod + def _report_to_dict(report: ReviewReport) -> dict: + return { + "taskId": report.task_id, + "verdict": report.verdict, + "coverage": [c.__dict__ for c in report.coverage], + "intentAlignment": report.intent_alignment, + "divergenceNotes": report.divergence_notes, + "regressionRisk": report.regression_risk, + "codeQuality": report.code_quality, + "lifecycleHealth": report.lifecycle_health, + "runtimeStability": report.runtime_stability, + "codeToDesignTable": [c.__dict__ for c in report.code_to_design_table], + "loggingChecks": report.logging_checks, + "recommendations": report.recommendations, + } + + @staticmethod + def _dict_to_report(data: dict) -> ReviewReport: + return ReviewReport( + task_id=data.get("taskId", ""), + verdict=data.get("verdict", "fail"), + coverage=[RequirementCoverage(**c) for c in data.get("coverage", [])], + intent_alignment=data.get("intentAlignment", "aligned"), + divergence_notes=data.get("divergenceNotes", ""), + regression_risk=data.get("regressionRisk", "none"), + code_quality=data.get("codeQuality", {}), + lifecycle_health=data.get("lifecycleHealth", {}), + runtime_stability=data.get("runtimeStability", {}), + code_to_design_table=[CodeToDesignItem(**c) for c in data.get("codeToDesignTable", [])], + logging_checks=data.get("loggingChecks", {}), + recommendations=data.get("recommendations", []), + ) diff --git a/lib/air_runtime/sdb_backends.py b/lib/air_runtime/sdb_backends.py new file mode 100644 index 0000000..f169428 --- /dev/null +++ b/lib/air_runtime/sdb_backends.py @@ -0,0 +1,527 @@ +"""AirSDB backends — 5 static analyzer backends + AnalysisDiff. + +Backends: + CppcheckBackend — C/C++ via cppcheck + ClangTidyBackend — C/C++ via clang-tidy + RustClippyBackend — Rust via cargo clippy + GoVetBackend — Go via go vet + staticcheck + TypeScriptBackend — TypeScript via tsc --noEmit +""" + +from __future__ import annotations + +import json +import shutil +import subprocess +from abc import ABC, abstractmethod +from dataclasses import dataclass, field +from pathlib import Path + + +# --------------------------------------------------------------------------- +# Unified result type +# --------------------------------------------------------------------------- + +@dataclass(frozen=True) +class AnalysisResult: + tool: str + file: str + line: int | None + column: int | None + severity: str # error | warning | info + message: str + rule_id: str | None = None + + +# --------------------------------------------------------------------------- +# Abstract base +# --------------------------------------------------------------------------- + +class StaticAnalyzerBackend(ABC): + """Abstract base for every static-analysis backend.""" + + @abstractmethod + def analyze(self, project_root: Path, target: Path | None = None) -> list[AnalysisResult]: + """Run the analyzer and return structured findings.""" + + @property + @abstractmethod + def name(self) -> str: + """Short identifier for this backend (e.g. 'cppcheck').""" + + @property + @abstractmethod + def install_hint(self) -> str: + """Human-readable hint shown when the tool is not installed.""" + + # -- helpers available to all backends --------------------------------- + + def _check_tool(self, tool_cmd: str) -> None: + """Raise RuntimeError if *tool_cmd* is not on PATH.""" + if not shutil.which(tool_cmd): + raise RuntimeError(self.install_hint) + + @staticmethod + def _run(cmd: list[str], *, cwd: Path | None = None) -> subprocess.CompletedProcess: + """Run *cmd* and capture stdout/stderr. Returns CompletedProcess.""" + return subprocess.run( + cmd, + cwd=cwd, + capture_output=True, + text=True, + timeout=300, + ) + + +# --------------------------------------------------------------------------- +# CppcheckBackend +# --------------------------------------------------------------------------- + +class CppcheckBackend(StaticAnalyzerBackend): + """C/C++ static analysis via cppcheck.""" + + name = "cppcheck" + install_hint = ( + "cppcheck is not installed. " + "Install it with: sudo apt install cppcheck (Debian/Ubuntu) " + "or: brew install cppcheck (macOS)" + ) + + # Template: file:line:column:severity:id:message + _TEMPLATE = "{file}:{line}:{column}:{severity}:{id}:{message}" + + def analyze(self, project_root: Path, target: Path | None = None) -> list[AnalysisResult]: + self._check_tool("cppcheck") + + src = str(target) if target else str(project_root) + cmd = [ + "cppcheck", + "--quiet", + f"--template={self._TEMPLATE}", + "--force", + src, + ] + proc = self._run(cmd, cwd=project_root) + + results: list[AnalysisResult] = [] + for line in proc.stderr.splitlines(): + line = line.strip() + if not line: + continue + parts = line.split(":", 5) + if len(parts) < 6: + continue + try: + ln = int(parts[1]) if parts[1].strip() else None + except ValueError: + ln = None + try: + col = int(parts[2]) if parts[2].strip() else None + except ValueError: + col = None + + severity = parts[3].strip() + # Map cppcheck severities to our unified set + if severity not in ("error", "warning", "info"): + if severity in ("performance", "portability", "style"): + severity = "warning" + else: + severity = "info" + + results.append(AnalysisResult( + tool=self.name, + file=parts[0].strip(), + line=ln, + column=col, + severity=severity, + message=parts[5].strip(), + rule_id=parts[4].strip() or None, + )) + return results + + +# --------------------------------------------------------------------------- +# ClangTidyBackend +# --------------------------------------------------------------------------- + +class ClangTidyBackend(StaticAnalyzerBackend): + """C/C++ static analysis via clang-tidy.""" + + name = "clang-tidy" + install_hint = ( + "clang-tidy is not installed. " + "Install it with: sudo apt install clang-tidy (Debian/Ubuntu) " + "or: brew install llvm (macOS, then use llvm/bin/clang-tidy)" + ) + + def analyze(self, project_root: Path, target: Path | None = None) -> list[AnalysisResult]: + self._check_tool("clang-tidy") + + src = str(target) if target else str(project_root) + cmd = [ + "clang-tidy", + "--quiet", + src, + ] + # Use compile_commands.json if present + comp_db = project_root / "compile_commands.json" + if comp_db.exists(): + cmd.append(f"-p={comp_db.parent}") + + proc = self._run(cmd, cwd=project_root) + + results: list[AnalysisResult] = [] + # clang-tidy output format: ::: warning: [check-name] + for line in proc.stderr.splitlines(): + line = line.strip() + if not line: + continue + parts = line.split(":", 3) + if len(parts) < 4: + continue + try: + ln = int(parts[1].strip()) if parts[1].strip() else None + except ValueError: + ln = None + try: + col = int(parts[2].strip()) if parts[2].strip() else None + except ValueError: + col = None + + msg_part = parts[3].strip() + severity = "warning" + # Detect "error:" prefix + if msg_part.startswith("error:"): + severity = "error" + msg_part = msg_part[len("error:"):].strip() + elif msg_part.startswith("warning:"): + msg_part = msg_part[len("warning:"):].strip() + elif msg_part.startswith("note:"): + severity = "info" + msg_part = msg_part[len("note:"):].strip() + + # Extract [check-name] at the end + rule_id = None + if msg_part.endswith("]"): + bracket = msg_part.rfind("[") + if bracket != -1: + rule_id = msg_part[bracket + 1:-1].strip() + msg_part = msg_part[:bracket].strip() + + results.append(AnalysisResult( + tool=self.name, + file=parts[0].strip(), + line=ln, + column=col, + severity=severity, + message=msg_part, + rule_id=rule_id, + )) + return results + + +# --------------------------------------------------------------------------- +# RustClippyBackend +# --------------------------------------------------------------------------- + +class RustClippyBackend(StaticAnalyzerBackend): + """Rust static analysis via cargo clippy.""" + + name = "clippy" + install_hint = ( + "cargo clippy is not available. " + "Install Rust toolchain: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh " + "then: rustup component add clippy" + ) + + def analyze(self, project_root: Path, target: Path | None = None) -> list[AnalysisResult]: + self._check_tool("cargo") + + cmd = [ + "cargo", "clippy", + "--message-format=json", + ] + # If a specific target file/dir is given, we still run cargo clippy + # on the whole crate (cargo does not support single-file analysis). + proc = self._run(cmd, cwd=project_root) + + results: list[AnalysisResult] = [] + for line in proc.stdout.splitlines(): + line = line.strip() + if not line: + continue + try: + obj = json.loads(line) + except json.JSONDecodeError: + continue + if obj.get("reason") != "compiler-message": + continue + msg = obj.get("message", {}) + level = msg.get("level", "") + if level == "error": + severity = "error" + elif level in ("warning",): + severity = "warning" + else: + severity = "info" + + for span in msg.get("spans", []): + results.append(AnalysisResult( + tool=self.name, + file=span.get("file_name", ""), + line=span.get("line_start"), + column=span.get("column_start"), + severity=severity, + message=msg.get("message", ""), + rule_id=msg.get("code", {}).get("code") or None, + )) + + # If no JSON output (e.g. compile error), also parse stderr + if not results and proc.stderr: + for line in proc.stderr.splitlines(): + line = line.strip() + if "error" in line.lower() and ":" in line: + results.append(AnalysisResult( + tool=self.name, + file=str(project_root), + line=None, + column=None, + severity="error", + message=line, + rule_id=None, + )) + return results + + +# --------------------------------------------------------------------------- +# GoVetBackend +# --------------------------------------------------------------------------- + +class GoVetBackend(StaticAnalyzerBackend): + """Go static analysis via go vet + staticcheck.""" + + name = "go-vet" + install_hint = ( + "go is not installed. " + "Install Go: https://go.dev/dl/ " + "For staticcheck: go install honnef.co/go/tools/cmd/staticcheck@latest" + ) + + def analyze(self, project_root: Path, target: Path | None = None) -> list[AnalysisResult]: + self._check_tool("go") + + results: list[AnalysisResult] = [] + + # 1) go vet — JSON output + vet_cmd = ["go", "vet", "./..."] + proc = self._run(vet_cmd, cwd=project_root) + if proc.stderr: + results.extend(self._parse_go_vet_output(proc.stderr)) + + # 2) staticcheck (optional — don't fail if not installed) + if shutil.which("staticcheck"): + sc_cmd = ["staticcheck", "-f=json", "./..."] + sc_proc = self._run(sc_cmd, cwd=project_root) + for line in sc_proc.stdout.splitlines(): + line = line.strip() + if not line: + continue + try: + obj = json.loads(line) + except json.JSONDecodeError: + continue + severity = "warning" + if obj.get("severity", "") == "error": + severity = "error" + results.append(AnalysisResult( + tool="staticcheck", + file=obj.get("location", {}).get("file", ""), + line=obj.get("location", {}).get("line"), + column=obj.get("location", {}).get("column"), + severity=severity, + message=obj.get("message", ""), + rule_id=obj.get("code", ""), + )) + + return results + + @staticmethod + def _parse_go_vet_output(text: str) -> list[AnalysisResult]: + """Parse go vet stderr output. + + go vet output format (non-JSON): + :: + """ + results: list[AnalysisResult] = [] + for line in text.splitlines(): + line = line.strip() + if not line: + continue + parts = line.split(":", 2) + if len(parts) < 3: + continue + try: + ln = int(parts[1].strip()) if parts[1].strip() else None + except ValueError: + ln = None + results.append(AnalysisResult( + tool="go vet", + file=parts[0].strip(), + line=ln, + column=None, + severity="warning", + message=parts[2].strip(), + rule_id=None, + )) + return results + + +# --------------------------------------------------------------------------- +# TypeScriptBackend +# --------------------------------------------------------------------------- + +class TypeScriptBackend(StaticAnalyzerBackend): + """TypeScript static analysis via tsc --noEmit.""" + + name = "tsc" + install_hint = ( + "tsc (TypeScript compiler) is not installed. " + "Install it with: npm install -g typescript " + "or add it to your project: npm install --save-dev typescript" + ) + + def analyze(self, project_root: Path, target: Path | None = None) -> list[AnalysisResult]: + # tsc can be installed locally (npx) or globally + tsc_cmd = self._find_tsc() + if tsc_cmd is None: + raise RuntimeError(self.install_hint) + + cmd = tsc_cmd + ["--noEmit", "--pretty", "false"] + proc = self._run(cmd, cwd=project_root) + + results: list[AnalysisResult] = [] + # tsc output format: (,): error TS: + for line in proc.stdout.splitlines(): + line = line.strip() + if not line: + continue + results.append(self._parse_tsc_line(line)) + return results + + def _find_tsc(self) -> list[str] | None: + """Return the tsc command as a list, or None if not found.""" + if shutil.which("tsc"): + return ["tsc"] + if shutil.which("npx"): + return ["npx", "tsc"] + return None + + @staticmethod + def _parse_tsc_line(line: str) -> AnalysisResult: + """Parse a single tsc diagnostic line. + + Format: (,): error TS1234: + """ + severity = "error" + rule_id = None + + # Split on the first colon-space after the position paren + # e.g. "src/foo.ts(10,5): error TS2322: Type 'string' ..." + main_parts = line.split(": ", 1) + location_part = main_parts[0] if main_parts else line + message = main_parts[1].strip() if len(main_parts) > 1 else "" + + # Extract file, line, column from "file(line,col)" + file_part = location_part + ln = None + col = None + paren = location_part.rfind("(") + if paren != -1 and location_part.endswith(")"): + file_part = location_part[:paren] + pos_str = location_part[paren + 1:-1] + pos_parts = pos_str.split(",", 1) + try: + ln = int(pos_parts[0].strip()) if pos_parts[0].strip() else None + except ValueError: + pass + if len(pos_parts) > 1: + try: + col = int(pos_parts[1].strip()) if pos_parts[1].strip() else None + except ValueError: + pass + + # Extract severity + rule from " error TS2322" in the remainder + if len(main_parts) > 1: + # The part between the first colon-space and the message + # is in the original line — re-parse + rest = line[len(location_part) + 2:] # after ": " + if rest.startswith("error "): + severity = "error" + rest = rest[len("error "):] + elif rest.startswith("warning "): + severity = "warning" + rest = rest[len("warning "):] + # rest now starts with "TS1234: message" + ts_parts = rest.split(": ", 1) + if ts_parts: + rule_id = ts_parts[0].strip() or None + if len(ts_parts) > 1: + message = ts_parts[1].strip() + + return AnalysisResult( + tool="tsc", + file=file_part, + line=ln, + column=col, + severity=severity, + message=message, + rule_id=rule_id, + ) + + +# --------------------------------------------------------------------------- +# AnalysisDiff +# --------------------------------------------------------------------------- + +class AnalysisDiff: + """Compare two lists of AnalysisResult and classify findings as + new, resolved, or unchanged.""" + + @staticmethod + def _key(r: AnalysisResult) -> tuple[str, int | None, str | None]: + """Dedup key: (file, line, rule_id).""" + return (r.file, r.line, r.rule_id) + + def diff( + self, + before: list[AnalysisResult], + after: list[AnalysisResult], + ) -> dict: + before_keys = {self._key(r): r for r in before} + after_keys = {self._key(r): r for r in after} + + before_set = set(before_keys.keys()) + after_set = set(after_keys.keys()) + + new_keys = after_set - before_set + resolved_keys = before_set - after_set + unchanged_keys = before_set & after_set + + return { + "new": [after_keys[k] for k in new_keys], + "resolved": [before_keys[k] for k in resolved_keys], + "unchanged": [before_keys[k] for k in unchanged_keys], + } + + +# --------------------------------------------------------------------------- +# Convenience registry +# --------------------------------------------------------------------------- + +BACKENDS: dict[str, StaticAnalyzerBackend] = { + "cppcheck": CppcheckBackend(), + "clang-tidy": ClangTidyBackend(), + "clippy": RustClippyBackend(), + "go-vet": GoVetBackend(), + "tsc": TypeScriptBackend(), +} diff --git a/lib/air_runtime/sec_runtime.py b/lib/air_runtime/sec_runtime.py new file mode 100644 index 0000000..b3ad2a8 --- /dev/null +++ b/lib/air_runtime/sec_runtime.py @@ -0,0 +1,175 @@ +""" +AirSec 安全扫描运行时 — V2 新增组件。 +制品敏感数据扫描 + 自动脱敏 + 误报白名单 + 确认流程 + advisory/blocking 模式。 +""" + +from __future__ import annotations + +import re +from dataclasses import dataclass, field +from pathlib import Path + +SECRET_PATTERNS: list[tuple[str, str]] = [ + ("api_key", r'(?:api[_-]?key|apikey)\s*[:=]\s*["\']?([A-Za-z0-9_\-]{16,})["\']?'), + ("aws_key", r'AKIA[0-9A-Z]{16}'), + ("private_key", r'-----BEGIN (?:RSA|EC|DSA|OPENSSH) PRIVATE KEY-----'), + ("token", r'(?:token|secret|password)\s*[:=]\s*["\']?([^\s"\']{8,})["\']?'), + ("jwt", r'eyJ[A-Za-z0-9_\-]+\.eyJ[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+'), + ("url_credential", r'https?://[^:@]+:([^@]+)@'), +] + +ALLOWLIST_PATTERNS: list[str] = [ + r'EXAMPLE', + r'example', + r'YOUR_API_KEY', + r'TODO', + r' ScanReport: + findings: list[ScanFinding] = [] + whitelisted = 0 + + try: + content = file_path.read_text(encoding="utf-8", errors="replace") + except Exception: + return ScanReport(task_id=task_id, clean=True) + + for rule, pattern in SECRET_PATTERNS: + for match in re.finditer(pattern, content, re.IGNORECASE): + matched_text = match.group(0) + if any(re.search(ap, matched_text) for ap in ALLOWLIST_PATTERNS): + whitelisted += 1 + continue + + line_no = content[:match.start()].count("\n") + 1 + display = matched_text[:60] + "..." if len(matched_text) > 60 else matched_text + findings.append(ScanFinding( + rule=rule, file=str(file_path), line=line_no, match=display, + )) + + return ScanReport( + task_id=task_id, + findings=findings, + whitelisted=whitelisted, + clean=len(findings) == 0, + ) + + +def scan_result_data(result: dict, task_id: str = "") -> ScanReport: + """扫描 Worker result.json 中的敏感数据。""" + import json + text = json.dumps(result, ensure_ascii=False) + findings: list[ScanFinding] = [] + whitelisted = 0 + + for rule, pattern in SECRET_PATTERNS: + for match in re.finditer(pattern, text, re.IGNORECASE): + matched_text = match.group(0) + if any(re.search(ap, matched_text) for ap in ALLOWLIST_PATTERNS): + whitelisted += 1 + continue + display = matched_text[:60] + "..." if len(matched_text) > 60 else matched_text + findings.append(ScanFinding( + rule=rule, file="result.json", line=0, match=display, + )) + + return ScanReport( + task_id=task_id, + findings=findings, + whitelisted=whitelisted, + clean=len(findings) == 0, + ) + + +def scan_file_with_mode( + file_path: Path, + task_id: str = "", + mode: str = ScanMode.BLOCKING, + confirm_callback=None, # 可选:首次发现时调用此回调询问用户 +) -> ScanReport: + """ + 增强版扫描: + 1. 基础扫描(已有逻辑) + 2. 文件名白名单过滤 + 3. 模式判断(advisory vs blocking) + """ + report = scan_file(file_path, task_id) # 原有逻辑 + + # 文件名白名单过滤 + filtered_findings = [] + for f in report.findings: + filename = file_path.name + if any(re.search(p, filename) for p in WHITELIST_FILE_PATTERNS): + report.whitelisted += 1 + continue + filtered_findings.append(f) + + report.findings = filtered_findings + report.clean = len(filtered_findings) == 0 + + # 模式处理 + if not report.clean and mode == ScanMode.ADVISORY: + # advisory 模式:只记录,不阻止 + report.advisory_blocked = False + elif not report.clean and mode == ScanMode.BLOCKING: + # blocking 模式:默认阻止 + report.advisory_blocked = True + + return report + + +def confirm_pattern(task_id: str, pattern: str, user: str = "unknown") -> None: + """用户确认某模式为安全后,记录下来""" + from air_runtime.utils import now_iso + fingerprint = f"{task_id}:{pattern}" + USER_CONFIRMATIONS[fingerprint] = { + "pattern": pattern, + "confirmed_at": now_iso(), + "user": user, + } + + +def is_confirmed(task_id: str, pattern: str) -> bool: + """检查某模式是否已被用户确认""" + fingerprint = f"{task_id}:{pattern}" + return fingerprint in USER_CONFIRMATIONS diff --git a/lib/air_runtime/task_graph.py b/lib/air_runtime/task_graph.py new file mode 100644 index 0000000..e93d15e --- /dev/null +++ b/lib/air_runtime/task_graph.py @@ -0,0 +1,220 @@ +""" +动态任务依赖图(DAG)— V2 P1-14 修复。 +替代 V1 静态 todo.md 表格,支持 Arc 增量重规划,Eng 增量吸收。 +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any + + +@dataclass +class TaskNode: + id: str + status: str = "TODO" # TODO | DISPATCHED | DONE | BLOCKED + task: str = "" + files_dirs: str = "" + done_when: str = "" + in_degree: int = 0 + out_edges: list[str] = field(default_factory=list) + write_set: list[str] = field(default_factory=list) + meta: dict[str, Any] = field(default_factory=dict) + + +@dataclass +class Edge: + source: str + target: str + kind: str = "dependency" # dependency | conflict | sync + + +@dataclass +class EdgeChange: + added: list[Edge] = field(default_factory=list) + removed: list[Edge] = field(default_factory=list) + + +@dataclass +class PlanDelta: + """Arc 重规划产出的增量差异,替代全量覆盖 todo.md。""" + removed_tasks: list[str] = field(default_factory=list) + added_tasks: list[TaskNode] = field(default_factory=list) + modified_tasks: list[TaskNode] = field(default_factory=list) + edge_changes: EdgeChange = field(default_factory=EdgeChange) + + +class TaskGraph: + """动态任务依赖图,支持增量更新和全量替换。""" + + def __init__(self): + self.nodes: dict[str, TaskNode] = {} + self.edges: list[Edge] = [] + + def add_node(self, node: TaskNode) -> None: + self.nodes[node.id] = node + + def add_edge(self, edge: Edge) -> None: + self.edges.append(edge) + if edge.target in self.nodes: + self.nodes[edge.target].in_degree += 1 + if edge.source in self.nodes: + self.nodes[edge.source].out_edges.append(edge.target) + + def apply_delta(self, delta: PlanDelta) -> None: + """增量吸收 Arc 的重规划结果,保留已调度任务不受影响。""" + for task_id in delta.removed_tasks: + self._remove_node(task_id) + for node in delta.added_tasks: + self._add_node(node) + for node in delta.modified_tasks: + self._update_node(node) + for edge in delta.edge_changes.removed: + self._remove_edge(edge) + for edge in delta.edge_changes.added: + self._add_edge(edge) + + def apply_full_replace(self, nodes: list[TaskNode], edges: list[Edge]) -> None: + """全量替换模式:Arc 产出完整 DAG,保留已完成任务状态。""" + done_status = {tid: n.status for tid, n in self.nodes.items() if n.status == "DONE"} + self.nodes = {n.id: n for n in nodes} + self.edges = list(edges) + for tid, status in done_status.items(): + if tid in self.nodes: + self.nodes[tid].status = status + for edge in self.edges: + if edge.target in self.nodes: + self.nodes[edge.target].in_degree += 1 + if edge.source in self.nodes: + self.nodes[edge.source].out_edges.append(edge.target) + + def ready_tasks(self) -> list[str]: + """返回当前入度为 0 且状态为 TODO 的任务。""" + return [nid for nid, n in self.nodes.items() if n.in_degree == 0 and n.status == "TODO"] + + def diff(self, other: TaskGraph) -> PlanDelta: + """对比自身与 other,产出 PlanDelta(add/remove/modify node + edge changes)。 + + self = 新图, other = 旧图(before replan)。 + """ + delta = PlanDelta() + old_ids = set(other.nodes.keys()) + new_ids = set(self.nodes.keys()) + + # 移除 + delta.removed_tasks = list(old_ids - new_ids) + + # 新增 + delta.added_tasks = [self.nodes[tid] for tid in (new_ids - old_ids)] + + # 修改 + for tid in old_ids & new_ids: + old_n = other.nodes[tid] + new_n = self.nodes[tid] + if (old_n.task != new_n.task + or old_n.files_dirs != new_n.files_dirs + or old_n.done_when != new_n.done_when + or old_n.write_set != new_n.write_set): + delta.modified_tasks.append(new_n) + + # Edge 差异 + old_edges = {(e.source, e.target, e.kind) for e in other.edges} + new_edges = {(e.source, e.target, e.kind) for e in self.edges} + for s, t, k in (new_edges - old_edges): + delta.edge_changes.added.append(Edge(source=s, target=t, kind=k)) + for s, t, k in (old_edges - new_edges): + delta.edge_changes.removed.append(Edge(source=s, target=t, kind=k)) + + return delta + + @classmethod + def load(cls, path) -> TaskGraph: + """从 _export_task_graph_json 写的格式还原 TaskGraph。""" + from pathlib import Path + from air_runtime.io import safe_json_load + p = Path(path) + data = safe_json_load(p) + graph = cls() + if not data or not isinstance(data, dict): + return graph + for nid, nd in data.get("nodes", {}).items(): + graph.nodes[nid] = TaskNode( + id=nd.get("id", nid), + status=nd.get("status", "TODO"), + task=nd.get("task", ""), + files_dirs=nd.get("filesDirs", ""), + done_when=nd.get("doneWhen", ""), + in_degree=nd.get("inDegree", 0), + out_edges=list(nd.get("outEdges", [])), + write_set=list(nd.get("writeSet", [])), + ) + for ed in data.get("edges", []): + graph.edges.append(Edge( + source=ed["source"], target=ed["target"], + kind=ed.get("kind", "dependency"), + )) + return graph + + def task_ids_by_status(self, status: str) -> list[str]: + return [nid for nid, n in self.nodes.items() if n.status == status] + + def find_cycles(self) -> list[list[str]]: + """检测依赖环(DFS)。""" + visited: set[str] = set() + rec_stack: set[str] = set() + cycles: list[list[str]] = [] + + def dfs(node_id: str, path: list[str]) -> None: + visited.add(node_id) + rec_stack.add(node_id) + path.append(node_id) + for target in self.nodes.get(node_id, TaskNode(id=node_id)).out_edges: + if target not in visited: + dfs(target, path.copy()) + elif target in rec_stack: + cycle_start = path.index(target) + cycles.append(path[cycle_start:]) + rec_stack.discard(node_id) + + for nid in self.nodes: + if nid not in visited: + dfs(nid, []) + + return cycles + + def export_todo_md(self) -> str: + """导出为人可读的 todo.md 表格,保留 V1 的可见性优势。""" + lines = ["| Task | Status | Files/Dirs | Done When | Validation | ADR |", + "|------|--------|------------|-----------|------------|-----|"] + for nid, node in self.nodes.items(): + lines.append(f"| {node.task} | {node.status} | {node.files_dirs} | " + f"{node.done_when} | | |") + return "\n".join(lines) + "\n" + + def _remove_node(self, task_id: str) -> None: + if task_id in self.nodes: + del self.nodes[task_id] + self.edges = [e for e in self.edges if e.source != task_id and e.target != task_id] + + def _add_node(self, node: TaskNode) -> None: + self.nodes[node.id] = node + + def _update_node(self, node: TaskNode) -> None: + if node.id in self.nodes: + existing_status = self.nodes[node.id].status + self.nodes[node.id] = node + if existing_status in ("DISPATCHED", "DONE"): + self.nodes[node.id].status = existing_status + + def _remove_edge(self, edge: Edge) -> None: + self.edges = [e for e in self.edges + if not (e.source == edge.source and e.target == edge.target)] + if edge.target in self.nodes: + self.nodes[edge.target].in_degree = max(0, self.nodes[edge.target].in_degree - 1) + + def _add_edge(self, edge: Edge) -> None: + self.edges.append(edge) + if edge.target in self.nodes: + self.nodes[edge.target].in_degree += 1 + if edge.source in self.nodes: + self.nodes[edge.source].out_edges.append(edge.target) diff --git a/lib/air_runtime/test_runtime.py b/lib/air_runtime/test_runtime.py new file mode 100644 index 0000000..130ba3b --- /dev/null +++ b/lib/air_runtime/test_runtime.py @@ -0,0 +1,183 @@ +""" +AirTst 测试运行器运行时 — V2 新增组件。 +统一测试执行接口,支持多框架,产出结构化结果。 +""" + +from __future__ import annotations + +import json +import subprocess +from dataclasses import dataclass, field +from pathlib import Path + +from air_runtime.io import atomic_json_write +from air_runtime.paths import tst_state_path + + +@dataclass +class TestCase: + name: str + status: str # passed | failed | skipped + duration: str = "" + message: str = "" + + +@dataclass +class TestSuite: + name: str + total: int = 0 + passed: int = 0 + failed: int = 0 + skipped: int = 0 + cases: list[TestCase] = field(default_factory=list) + + +@dataclass +class TestRunResult: + framework: str + total: int = 0 + passed: int = 0 + failed: int = 0 + disabled: int = 0 + duration: str = "" + suites: list[TestSuite] = field(default_factory=list) + failures: list[dict] = field(default_factory=list) + + +class TestRunner: + """统一测试执行器。""" + + FRAMEWORKS = { + "pytest": ["python", "-m", "pytest", "--json-report", "-q"], + "googletest": ["ctest", "--output-on-failure"], + "jest": ["npx", "jest", "--json"], + "vitest": ["npx", "vitest", "run", "--reporter=json"], + "go": ["go", "test", "-json", "./..."], + "cargo": ["cargo", "test", "--", "--format=json"], + } + + def run(self, task_id: str, project_root: Path, framework: str, + target_path: Path | None = None, + extra_args: list[str] | None = None) -> TestRunResult: + if framework not in self.FRAMEWORKS: + return TestRunResult(framework=framework, failures=[{"error": f"unsupported framework: {framework}"}]) + + cmd = list(self.FRAMEWORKS[framework]) + if target_path: + cmd.append(str(target_path)) + if extra_args: + cmd.extend(extra_args) + + try: + result = subprocess.run(cmd, capture_output=True, text=True, + timeout=600, cwd=str(project_root)) + except subprocess.TimeoutExpired: + return TestRunResult(framework=framework, failures=[{"error": "timeout"}]) + + run_result = self._parse_result(framework, result.stdout) + self._save_report(task_id, project_root, run_result) + return run_result + + def _parse_result(self, framework: str, stdout: str) -> TestRunResult: + if framework == "pytest": + return self._parse_pytest(stdout) + if framework in ("jest", "vitest"): + return self._parse_jest(stdout) + if framework == "googletest": + return self._parse_googletest(stdout) + if framework == "go": + return self._parse_go(stdout) + if framework == "cargo": + return self._parse_cargo(stdout) + return TestRunResult(framework=framework, total=0) + + def _parse_pytest(self, stdout: str) -> TestRunResult: + try: + data = json.loads(stdout) + except json.JSONDecodeError: + return TestRunResult(framework="pytest", failures=[{"error": "json parse failed"}]) + return TestRunResult( + framework="pytest", + total=data.get("summary", {}).get("total", 0), + passed=data.get("summary", {}).get("passed", 0), + failed=data.get("summary", {}).get("failed", 0), + duration=str(data.get("duration", "")), + ) + + def _parse_jest(self, stdout: str) -> TestRunResult: + try: + data = json.loads(stdout) + except json.JSONDecodeError: + return TestRunResult(framework="jest", failures=[{"error": "json parse failed"}]) + return TestRunResult( + framework="jest", + total=data.get("numTotalTests", 0), + passed=data.get("numPassedTests", 0), + failed=data.get("numFailedTests", 0), + ) + + def _parse_googletest(self, stdout: str) -> TestRunResult: + """解析 ctest 输出。ctest 不输出 JSON,从文本提取统计。""" + import re + total = passed = failed = disabled = 0 + for line in stdout.splitlines(): + m = re.match(r"(\d+)% tests passed, (\d+) tests failed out of (\d+)", line) + if m: + failed = int(m.group(2)) + total = int(m.group(3)) + passed = total - failed + # GoogleTest 也支持 --gtest_output=json + try: + data = json.loads(stdout) + if isinstance(data, dict): + total = sum(s.get("tests", 0) for s in data.get("testsuites", [])) + failed = sum(s.get("failures", 0) for s in data.get("testsuites", [])) + passed = total - failed + disabled = sum(s.get("disabled", 0) for s in data.get("testsuites", [])) + except json.JSONDecodeError: + pass + return TestRunResult( + framework="googletest", total=total, passed=passed, failed=failed, disabled=disabled, + ) + + def _parse_go(self, stdout: str) -> TestRunResult: + """解析 go test -json 输出(JSONL 格式,每行一个事件)。""" + total = passed = failed = 0 + for line in stdout.splitlines(): + try: + ev = json.loads(line) + except json.JSONDecodeError: + continue + action = ev.get("Action", "") + if action == "pass": + passed += 1 + total += 1 + elif action == "fail": + failed += 1 + total += 1 + elif action == "skip": + total += 1 + return TestRunResult(framework="go", total=total, passed=passed, failed=failed) + + def _parse_cargo(self, stdout: str) -> TestRunResult: + """解析 cargo test --format=json 输出(JSONL 格式)。""" + total = passed = failed = 0 + for line in stdout.splitlines(): + try: + ev = json.loads(line) + except json.JSONDecodeError: + continue + if ev.get("type") == "test": + total += 1 + if ev.get("event") == "ok": + passed += 1 + elif ev.get("event") == "failed": + failed += 1 + return TestRunResult(framework="cargo", total=total, passed=passed, failed=failed) + + def _save_report(self, task_id: str, project_root: Path, result: TestRunResult) -> None: + report_dir = tst_state_path(project_root).parent / "reports" + report_dir.mkdir(parents=True, exist_ok=True) + from air_runtime.utils import session_stamp + report_path = report_dir / f"{task_id}-{session_stamp()}.json" + atomic_json_write(report_path, result.__dict__) diff --git a/lib/air_runtime/todo_parser.py b/lib/air_runtime/todo_parser.py new file mode 100644 index 0000000..c04c1ec --- /dev/null +++ b/lib/air_runtime/todo_parser.py @@ -0,0 +1,96 @@ +""" +TODO 解析器 — V2 修复 P1-8:列索引从表头推导,不再硬编码 cells[1]/cells[6]/cells[7]。 +""" + +from __future__ import annotations + +import re +from dataclasses import dataclass +from pathlib import Path + + +@dataclass +class TodoTask: + task_id: str + task: str + files_dirs: str = "" + status: str = "TODO" + done_when: str = "" + validations: str = "" + adr: str = "" + + +def parse_tasks(todo_path: Path) -> list[TodoTask]: + """从 todo.md 解析任务列表,动态检测列索引。""" + if not todo_path.exists(): + return [] + + content = todo_path.read_text(encoding="utf-8") + lines = [l.strip() for l in content.splitlines() if l.strip()] + + # 找到 Markdown 表格头 + header_idx = -1 + for i, line in enumerate(lines): + if line.startswith("|") and "Task" in line and "Status" in line: + header_idx = i + break + + if header_idx < 0: + return [] + + # 解析列名 + header_line = lines[header_idx] + header_cols = [c.strip() for c in header_line.split("|") if c.strip()] + + # 建立列名 → 索引映射 + col_map = {} + for idx, col_name in enumerate(header_cols): + col_name_lower = col_name.lower() + if "task" in col_name_lower: + col_map["task"] = idx + elif "status" in col_name_lower: + col_map["status"] = idx + elif "files" in col_name_lower or "dir" in col_name_lower: + col_map["files_dirs"] = idx + elif "done" in col_name_lower or "when" in col_name_lower: + col_map["done_when"] = idx + elif "valid" in col_name_lower: + col_map["validations"] = idx + elif "adr" in col_name_lower: + col_map["adr"] = idx + + # 跳过表头和分隔符 + tasks = [] + for line in lines[header_idx + 2:]: + if not line.startswith("|"): + continue + cells = [c.strip() for c in line.split("|") if len(c.strip()) > 0] + if not cells: + continue + + task_cell = cells[col_map.get("task", 0)] if col_map.get("task", 0) < len(cells) else "" + # 提取 [T-xxx] 格式的任务 ID + tid_match = re.match(r"\[([A-Za-z0-9_\-\.]+)\]", task_cell) + task_id = tid_match.group(1) if tid_match else task_cell + task = task_cell + status = cells[col_map.get("status", 1)] if col_map.get("status", 1) < len(cells) else "TODO" + files_dirs = cells[col_map.get("files_dirs", 2)] if col_map.get("files_dirs", 2) < len(cells) else "" + done_when = cells[col_map.get("done_when", 3)] if col_map.get("done_when", 3) < len(cells) else "" + validations = cells[col_map.get("validations", 4)] if col_map.get("validations", 4) < len(cells) else "" + adr = cells[col_map.get("adr", 5)] if col_map.get("adr", 5) < len(cells) else "" + + # 清理标记 + task_id = re.sub(r"^\[|\]$", "", task_id).strip() + + if task_id and task_id != "---": + tasks.append(TodoTask( + task_id=task_id, + task=task, + files_dirs=files_dirs, + status=status.upper() if status else "TODO", + done_when=done_when, + validations=validations, + adr=adr, + )) + + return tasks \ No newline at end of file diff --git a/lib/air_runtime/utils.py b/lib/air_runtime/utils.py new file mode 100644 index 0000000..a42ad9b --- /dev/null +++ b/lib/air_runtime/utils.py @@ -0,0 +1,65 @@ +""" +公共工具函数 — 消除 V1 中 _ordered_unique、_session_stamp、policy normalization 等 +在各模块中 3~5 份重复定义的代码。 +""" + +from __future__ import annotations + +import re +from datetime import datetime, timezone +from typing import Any + + +def ordered_unique(items: list) -> list: + """保序去重。支持字符串列表和带 id 字段的字典列表。""" + seen: set[str] = 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 now_iso() -> str: + """ISO 格式 UTC 时间戳,用于 JSON state 文件。""" + return datetime.now(timezone.utc).isoformat() + + +def normalize_policy(defaults: dict[str, Any], overrides: dict[str, Any] | None) -> dict[str, Any]: + """通用的策略合并:overrides 覆盖 defaults,类型自动转换。""" + merged = {**defaults} + if overrides: + for k, v in overrides.items(): + if k in merged: + expected_type = type(defaults[k]) + try: + merged[k] = expected_type(v) if not isinstance(v, expected_type) else v + except (ValueError, TypeError): + merged[k] = 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 "