From 0e41c8c6f61558435c7bbe97269395c1897f66d5 Mon Sep 17 00:00:00 2001 From: XiuChengWu <732857315@qq.com> Date: Wed, 27 May 2026 18:19:37 +0800 Subject: [PATCH] =?UTF-8?q?W15:=20workflow=20improvements=20=E2=80=94=20EX?= =?UTF-8?q?PRESS=20fast-path,=20audit=E2=86=92fix=20closed=20loop,=20metad?= =?UTF-8?q?ata=20self-check=20(W15.1-W15.3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - W15.1 (杨帆): Add EXPRESS fast-path to §11 state machine (T17/T18, E1-E6 conditions, escalation safety valve) - W15.2 (王测): Add §14 audit→fix closed loop — findings-registry.md, severity-driven auto-triage, CRITICAL blocking rule - W15.3 (胡桐): Create scripts/check_agents_metadata.py (5-check: YAML parse, rating range, group/member refs, duplicate IDs) - Fix YAML orphan bugs in 3 profiles: devops-hu, engineer-sun, security-cao (perf_log entries outside array) - Pre-fill findings-registry.md with 10 historical findings from W11.1/W11.7 audits Co-Authored-By: Claude Opus 4.7 --- agents/WORKFLOW.md | 142 +++++++++- agents/architect-yang/profile.md | 3 + agents/audits/findings-registry.md | 38 +++ agents/devops-hu/profile.md | 11 +- agents/engineer-sun/profile.md | 2 +- agents/qa-wang/profile.md | 3 + agents/security-cao/profile.md | 26 +- scripts/check_agents_metadata.py | 421 +++++++++++++++++++++++++++++ 8 files changed, 629 insertions(+), 17 deletions(-) create mode 100644 agents/audits/findings-registry.md create mode 100644 scripts/check_agents_metadata.py diff --git a/agents/WORKFLOW.md b/agents/WORKFLOW.md index 9a9fbb6..70791a5 100644 --- a/agents/WORKFLOW.md +++ b/agents/WORKFLOW.md @@ -144,7 +144,7 @@ | PROPOSE | +----+-----+ | - simple | complex + simple / EXPRESS | complex +-------------+--------------+ | | | v @@ -183,6 +183,7 @@ - OPTIMIZE --re-vote--> VOTE - OPTIMIZE --fundamental rewrite--> PROPOSE +- EXECUTE --EXPRESS escalation--> VOTE - INSPECT fail --fixable--> EXECUTE - INSPECT fail --design--> OPTIMIZE - INSPECT fail --fatal--> ROLLBACK @@ -192,6 +193,23 @@ - ANY --CEO abort--> ABORT +### 11.2.1 EXPRESS 快跳路径 + +快跳(EXPRESS)是将 PROPOSE→VOTE→OPTIMIZE→INTEGRATE 压缩为 PROPOSE→EXECUTE 的合法短路径。快跳适用条件为以下 **全部 6 条** 同时满足: + +| # | 条件 | 验证方式 | +|---|------|----------| +| E1 | 改动源文件 ≤ 2 个(不含 profile.md) | `git diff --stat HEAD` 统计 changed files | +| E2 | 不改动 `dstalk-core/include/` 下的任何公共头文件 | `git diff --name-only HEAD` 与 include/ 交集为空 | +| E3 | 不改动 CMakeLists.txt / cmake/ 目录 / CMakePresets.json | `git diff --name-only HEAD` 与构建文件交集为空 | +| E4 | 不新增公共 API 面:无新 `dstalk_` 前缀函数声明、无新插件接口结构体 | diff 中公共头文件无新增函数声明 | +| E5 | 不涉及跨模块依赖变更:改动文件涉及 ≤ 2 个顶层目录(如 `dstalk-core/`、单个 `plugins//`) | `git diff --dirstat HEAD` 目录数 ≤ 2 | +| E6 | CEO 在 WORKFLOW.md §7 任务条目中显式标注 `[EXPRESS]` | 人工核对 §7 | + +满足全部 E1-E6 → CEO 可声明 EXPRESS 快跳,任务直接进入 EXECUTE(跳过 VOTE / OPTIMIZE / INTEGRATE),对应转换规则 **T17**。 + +**EXPRESS 升级**:若执行者在 EXECUTE 阶段发现任务实际超出 EXPRESS 条件(E1-E5 任一条不再成立),须立即报告 CEO。CEO 核实后移除 `[EXPRESS]` 标签并替换为 `[ESCALATED]`,任务从 EXECUTE 退回 VOTE 走完整治理路径,对应转换规则 **T18**。 + ### 11.3 转换条件表 | # | 从 | 到 | 触发条件 | 决策者 | @@ -212,6 +230,8 @@ | T14 | INSPECT | ROLLBACK | 验收发现不可逆副作用:文件错误删除或覆盖 / 二进制损坏 / .git 目录状态异常 | CEO | | T15 | INSPECT | ABORT | CEO 判定继续修复成本 > 重新执行成本(需改 >5 个文件且涉及多个执行者重新协调) | CEO | | T16 | ANY | ABORT | 用户明确指令中止 OR 触发安全红线(凭证泄露、未加密敏感数据落盘) | CEO | +| T17 | PROPOSE | EXECUTE | EXPRESS 快跳:同时满足 E1-E6(见 §11.2.1 EXPRESS 条件表)AND CEO 在 §7 任务条目标注 `[EXPRESS]` | CEO | +| T18 | EXECUTE | VOTE | EXPRESS 升级:执行者报告任务实际范围超出 EXPRESS 条件(E1-E5 任一条不再成立)AND CEO 核实后将 `[EXPRESS]` 改为 `[ESCALATED]` | CEO | ### 11.4 状态进入/退出动作 @@ -344,4 +364,122 @@ 1. 在 WORKFLOW.md §7 记录:中止原因、时间、影响范围 2. 决定改动处置:保留(`git stash`)或丢弃(`git checkout`) 3. 本波 W 编号标记为 ABORTED,下一波使用新编号 -4. 相关执行者 profile.md performance_log 仍追加条目(rating: aborted),保留参与记录 \ No newline at end of file +4. 相关执行者 profile.md performance_log 仍追加条目(rating: aborted),保留参与记录 + +## 14. 审计→修复闭环机制 + +审计(audit)产出的发现必须转化为可跟踪、可执行、可验收的修复任务。不允许审计报告写完即归档、发现问题无人跟进。 + +核心闭环:**Audit report → Finding registration → Severity triage → Fix task (Wave) → CEO verify → Close** + +### 14.1 发现登记格式 + +所有审计发现统一登记在 `agents/audits/findings-registry.md`,分为 Open Findings 和 Closed Findings 两个分区。每条发现包含以下字段: + +| 字段 | 说明 | 示例 | +|------|------|------| +| ID | `F-<源Wave>-<序号>`,全局唯一 | F-11.7-1 | +| Severity | CRITICAL / HIGH / MEDIUM / LOW | CRITICAL | +| Source | 审计报告文件名(相对于 agents/audits/) | W11.7-destructive-test.md | +| Title | 一句话描述,含关键行号和症状 | `/clear` reports [OK] even when session unavailable — main.cpp:168-172 | +| Status | 见 §14.2 状态定义 | OPEN | +| Assigned To | 负责修复的员工 agent-id(OPEN 状态为空) | architect-huang | +| Fix Wave | 修复所在的 Wave 编号(FIXED 后填写) | W16.1 | +| Verified By | 验收人 agent-id(VERIFIED 后填写) | qa-wang | + +发现数量超过 20 条时,qa-wang 负责将 Closed 分区中超过 30 天的条目归档到 `agents/audits/findings-archive.md`。 + +### 14.2 发现状态生命周期 + +| 状态 | 类型 | 含义 | 进入条件 | 退出条件 | +|------|------|------|----------|----------| +| OPEN | 活跃 | 发现已登记,待 triage | 审计报告提交后,由审计人或 QA 组长录入 registry | CEO/QA 组长完成 triage 并指定执行者 | +| ASSIGNED | 活跃 | 已指派修复人,等待执行 | Triage 完成 + CEO 在 PROPOSE 阶段创建对应修复 W 任务 | 执行者提交修复 + 自述 cmake 0 error + ctest 100% pass | +| FIXED | 活跃 | 修复已提交,等待验证 | 执行者完成修复并更新 registry 状态 | CEO INSPECT 通过(§12)OR QA 组长验证通过 | +| VERIFIED | 活跃 | 修复已验证,即将关闭 | INSPECT 全部通过 OR 回归测试覆盖通过 | 自动进入 CLOSED | +| CLOSED | 终止 | 已关闭 | VERIFIED 后自动关闭 | — | +| WONTFIX | 终止 | 决定不修复 | CEO 明确判定不修复(附理由) | — | +| BLOCKED | 活跃 | 被阻塞 | 依赖的其他发现未修复 OR 外部条件不满足 | 阻塞解除后回到 ASSIGNED | + +状态转换图: + +``` +OPEN ──→ ASSIGNED ──→ FIXED ──→ VERIFIED ──→ CLOSED + │ │ │ │ + │ │ │ │ + ↓ ↓ ↓ ↓ +WONTFIX BLOCKED REOPEN REOPEN + (回到 (回到 + ASSIGNED) ASSIGNED) +``` + +回边定义: + +- FIXED → ASSIGNED (REOPEN):CEO INSPECT 发现修复不完整或引入新回归,退回执行者 +- VERIFIED → ASSIGNED (REOPEN):后续回归测试暴露本发现的修复引入了新问题 +- ASSIGNED → BLOCKED → ASSIGNED:执行者发现依赖未满足,申请阻塞;依赖解除后恢复 +- OPEN → WONTFIX:CEO 判定(如:修复成本远超收益 / 已被后续重构覆盖 / 非 bug 是设计取舍) + +全局出口: + +- ANY → WONTFIX:CEO 在任意状态可强制关闭(需附理由写入 registry Change Log) + +### 14.3 自动转化规则 + +从审计发现到修复任务的转化由严重级别驱动: + +| Severity | 转化规则 | 时限 | 触发者 | +|----------|----------|------|--------| +| CRITICAL | 下一波 PROPOSE 阶段 MUST 创建对应修复任务(W 编号),优先级最高,阻塞其他任务排期 | 当前 Wave + 1 | CEO | +| HIGH | 2 个 Wave 内 MUST 安排修复任务 | 当前 Wave + 2 | CEO / QA 组长 | +| MEDIUM | 每波 PROPOSE 阶段评估,可合并到其他同文件/同模块任务中附带修复 | 不限,但每 5 个 Wave 至少回顾一次 backlog | QA 组长 triage | +| LOW | 进入 backlog,在相关源文件被其他任务改动时附带修复(opportunistic fix) | 不限 | 执行者自行判断 | + +CRITICAL 阻塞规则: +- 如果进入 EXECUTE 阶段时仍有 OPEN 状态的 CRITICAL 发现,CEO 必须明确决策:(a) 本波优先修 CRITICAL,或 (b) 标记 WONTFIX(附理由),或 (c) 降级为 HIGH(附降级理由) +- 不允许带着 OPEN CRITICAL 发现进入 SUCCESS + +### 14.4 CEO 审查协议(新增验收项) + +在 §12 验收清单基础上,INSPECT 阶段追加以下检查项: + +| # | 检查项 | 命令/方法 | 通过标准 | +|---|--------|-----------|----------| +| A1 | 发现登记完整性 | 检查本波新增审计报告:逐一核对 severity ≥ MEDIUM 的发现是否已录入 findings-registry.md | 无遗漏 | +| A2 | CRITICAL 发现清零 | `grep "CRITICAL.*OPEN" agents/audits/findings-registry.md` | 输出为空(所有 CRITICAL 已修复或 WONTFIX) | +| A3 | 修复关联标注 | 检查本波 EXECUTE 子代理报告 | 每个修复任务标注了对应的 Finding ID(格式:`Fixes: F--`) | +| A4 | 状态同步 | 逐一核对 registry 中本波涉及的发现状态与实际修复结果一致 | FIXED 状态发现的 cmake + ctest 已通过 | + +验收结论扩展: + +| 失败项 | 处理 | +|--------|------| +| A1 失败 | 补充录入 → 重新检查 | +| A2 失败 | INSPECT → EXECUTE(优先修复 CRITICAL) | +| A3 失败 | 补标注 → 重新检查 | +| A4 失败 | 状态回退到 ASSIGNED → EXECUTE | + +### 14.5 与 §11 状态机的集成点 + +| §11 状态 | §14 动作 | 责任人 | +|----------|----------|--------| +| PROPOSE | 1. 读取 findings-registry.md Open 分区 2. 将 CRITICAL/HIGH OPEN 发现转为候选 W 任务 3. 评估 MEDIUM backlog | CEO | +| EXECUTE | 1. 子代理 prompt 中标注 `Fixes: F--` 2. 修复完成后更新 registry 中该发现状态为 FIXED | 执行者 | +| INSPECT | 1. 执行 §14.4 A1-A4 检查 2. 通过的发现 FIXED → VERIFIED → CLOSED 3. 失败的发现退回 ASSIGNED(REOPEN) | CEO | +| SUCCESS | 1. 本波 CLOSED 的发现从 Open 分区移到 Closed 分区 2. 记录关闭日期和 Fix Wave | CEO | +| ABORT | 本波 ASSIGNED 的发现回退到 OPEN(修复未发生) | CEO | + +### 14.6 审计人职责 + +提交审计报告时,审计人必须同时完成以下动作: + +1. 在审计报告末尾新增 "## Findings Summary" 小节,列出所有发现的 ID、Severity、Title(与 registry 格式一致) +2. 将 severity ≥ MEDIUM 的发现录入 `findings-registry.md` Open 分区,状态 OPEN +3. LOW 发现同样录入(保持完整记录),但可在 triage 时直接标记为 backlog + +审计人完成登记后通知 CEO 或 QA 组长进行 triage。 + +### 14.7 关联文档 + +- [findings-registry.md](audits/findings-registry.md) — 发现注册表(单一事实来源) +- [PROMPT_TEMPLATE.md](PROMPT_TEMPLATE.md) — 子代理 prompt 模板(修复任务使用标准模板,Fixes 行添加到交付清单) \ No newline at end of file diff --git a/agents/architect-yang/profile.md b/agents/architect-yang/profile.md index 589f814..5673240 100644 --- a/agents/architect-yang/profile.md +++ b/agents/architect-yang/profile.md @@ -26,5 +26,8 @@ performance_log: - date: 2026-05-27 event: "W13.1: 深度审计 anthropic_plugin.cpp (497行),6个C ABI函数零try/catch (§8违反),response_body泄漏 + 全局指针竞态,tool_use静默丢弃。综合评级C。报告写入 agents/audits/W13.1-anthropic-audit.md" rating: completed + - date: 2026-05-27 + event: "W15.1: 为 WORKFLOW.md §11 协作状态机设计 EXPRESS 快跳路径。定义 E1-E6 六项客观准入条件,新增 T17(快跳入口) + T18(升级回退) 两条转换规则,§11.2 图增加快跳边标注,新增 §11.2.1 完整说明。建议将 EXPRESS 作为正式快跳标签(非新增状态,避免状态爆炸)" + rating: completed current_groups: [] --- diff --git a/agents/audits/findings-registry.md b/agents/audits/findings-registry.md new file mode 100644 index 0000000..1769679 --- /dev/null +++ b/agents/audits/findings-registry.md @@ -0,0 +1,38 @@ +# Audit Findings Registry + +> **维护人**: grp-quality-core (王测) +> **格式定义**: 见 `agents/WORKFLOW.md` §14.2 +> **最后更新**: 2026-05-27 (W15.2 初始化,从 W11.1/W11.7 审计报告提取) + +--- + +## Open Findings + +| ID | Severity | Source | Title | Status | Assigned To | Fix Wave | Verified By | +|----|----------|--------|-------|--------|-------------|----------|-------------| +| F-11.7-1 | CRITICAL | [W11.7-destructive-test.md](W11.7-destructive-test.md) | `build/bin/dstalk-cli.exe` corrupt copy (MD5 d8e8c92b vs 803ca2ea); all commands treated as AI prompt, exit code always 3 | OPEN | — | — | — | +| F-11.7-2 | MEDIUM | [W11.7-destructive-test.md](W11.7-destructive-test.md) | `/clear` reports [OK] even when session unavailable (g_session==null) — main.cpp:168-172 | OPEN | — | — | — | +| F-11.7-3 | LOW | [W11.7-destructive-test.md](W11.7-destructive-test.md) | `/context` silent no-output when session unavailable; no else branch — main.cpp:175-185 | OPEN | — | — | — | +| F-11.7-4 | LOW | [W11.7-destructive-test.md](W11.7-destructive-test.md) | `/file write` (no args) matched as unknown command instead of usage hint | OPEN | — | — | — | +| F-11.1-1 | HIGH | [W11.1-context-audit.md](W11.1-context-audit.md) | C++ exception (`std::bad_alloc`)穿越ABI边界,违反plugin-abi §5.3;trim_impl (L114-226) 无try/catch → std::terminate() | OPEN | — | — | — | +| F-11.1-2 | HIGH | [W11.1-context-audit.md](W11.1-context-audit.md) | strdup返回值未检查,OOM时静默失败+泄漏;L138-141/L219-222 循环内4次strdup无nullptr检查 | OPEN | — | — | — | +| F-11.1-3 | MEDIUM | [W11.1-context-audit.md](W11.1-context-audit.md) | context_set_max_tokens死API,g_max_tokens从未被读取(L21/L243-244) | OPEN | — | — | — | +| F-11.1-4 | LOW | [W11.1-context-audit.md](W11.1-context-audit.md) | UTF-8解码无越界保护(L42-64, L96-104),多字节序列假设后续字节有效 | OPEN | — | — | — | +| F-11.1-5 | LOW | [W11.1-context-audit.md](W11.1-context-audit.md) | token计数逻辑重复(L34-68 vs L91-106 ~90%重复) | OPEN | — | — | — | +| F-11.1-6 | LOW | [W11.1-context-audit.md](W11.1-context-audit.md) | 0xC0/0xC1过短编码未识别(L52, L100),仅影响token估算计数 | OPEN | — | — | — | + +--- + +## Closed Findings + +| ID | Severity | Source | Title | Closed Date | Fix Wave | Verified By | +|----|----------|--------|-------|-------------|----------|-------------| +| — | — | — | 暂无已关闭发现 | — | — | — | + +--- + +## Change Log + +| Date | Change | Author | +|------|--------|--------| +| 2026-05-27 | W15.2 初始化,从 W11.1/W11.7 提取 10 条发现 | 王测 (qa-wang) | diff --git a/agents/devops-hu/profile.md b/agents/devops-hu/profile.md index 125495d..0cbd4b9 100644 --- a/agents/devops-hu/profile.md +++ b/agents/devops-hu/profile.md @@ -32,7 +32,6 @@ performance_log: 顺带修复: tools_plugin.cpp 缺少前向声明、lsp_plugin.cpp 函数签名 mismatch、 5 个插件缺少 #include (Boost 1.86 不再识别 HEADER_ONLY)。 rating: done -current_groups: [] - date: 2026-05-27 event: "W12.4 修复 build 产物路径不一致 (BUG-1)" detail: > @@ -43,4 +42,14 @@ current_groups: [] ${CMAKE_BINARY_DIR}/bin 作为防御性显式声明;删除陈旧 build/dstalk-cli/dstalk-cli.exe。 验证: clean rebuild 后仅 build/bin/dstalk-cli.exe 存在,ctest 4/4 pass。 rating: done + - date: 2026-05-27 + event: "W15.3: 设计 agents/ 目录元数据自检机制 (scripts/check_agents_metadata.py)" + detail: > + 修复自身 profile.md YAML 格式错误 (perf_log 条目被误放在 current_groups: [] 之后)。 + 创建 5 项自检: C1 YAML 解析合法性、C2 rating 值范围、C3 current_groups -> group 引用完整性、 + C4 group members -> agent 引用完整性、C5 重复 ID 检测 + 目录名一致性。 + 首轮运行发现 engineer-sun + security-cao 的 profile.md 存在同类 YAML 错误 (各 2 条目 orphan)。 + 建议集成到 refresh_status.py 作为前置检查,并加入 WORKFLOW.md §5 CEO 自查清单。 + rating: done +current_groups: [] --- diff --git a/agents/engineer-sun/profile.md b/agents/engineer-sun/profile.md index 6c16c1e..845a251 100644 --- a/agents/engineer-sun/profile.md +++ b/agents/engineer-sun/profile.md @@ -29,7 +29,6 @@ performance_log: 修复前:第一行非 Content-Length 时 continue 丢弃该行,导致 header 解析偏移错位。 修复后:正确遍历所有 header 行,空行后若仍未找到 Content-Length 则记录错误并跳过帧。 编译通过,smoke test 通过。 -current_groups: [] - date: 2026-05-27 event: "W13.2: 深度审计 deepseek_plugin.cpp (486 行) — SSE 解析/ABI 异常安全/堆纪律/重复度" rating: completed @@ -57,4 +56,5 @@ current_groups: [] 构建验证: cmake --build Release 0 error; ctest 4/4 pass。 L420-471 reader_loop, L481-559 start, L561-603 stop 三件套, L605-630 open, L632-655 close, L657-683 diagnostics, L685-730 hover, L730-780 completion, L807-821 on_shutdown. +current_groups: [] --- diff --git a/agents/qa-wang/profile.md b/agents/qa-wang/profile.md index d8f5ab0..1a5389b 100644 --- a/agents/qa-wang/profile.md +++ b/agents/qa-wang/profile.md @@ -36,6 +36,9 @@ performance_log: - date: 2026-05-27 event: "W13.3: network_plugin.cpp 深度审计 (322行, 9维度)。发现 TLS 证书验证完全禁用 (F, CVSS 7.4) + DNS 解析无超时 (永久hang) + 缺 catch(...)。RAII/堆纪律/并发 A 级。综合 C 级" rating: A + - date: 2026-05-27 + event: "W15.2: 设计审计→修复闭环机制。定义 findings-registry.md 格式 + OPEN→ASSIGNED→FIXED→VERIFIED→CLOSED 状态生命周期 + 4级严重度自动转化规则 + WORKFLOW.md §14 完整草案。从 W11.1/W11.7 提取 10 条历史发现初始化注册表" + rating: A current_groups: - grp-quality-core (组长) --- diff --git a/agents/security-cao/profile.md b/agents/security-cao/profile.md index 0a149a4..9ef8ea6 100644 --- a/agents/security-cao/profile.md +++ b/agents/security-cao/profile.md @@ -50,18 +50,18 @@ performance_log: 命令注入: 未发现。路径遍历: tools 确认。 评级 session:D+ / tools:D。 报告: agents/audits/W13.5-session-tools-audit.md - - date: 2026-05-27 - event: "W14.3: 修复 W13.5 审计发现 — 路径遍历 + 全局状态加锁 + 9 vtable try/catch" - rating: done - detail: | - 修改 session_plugin.cpp (294行) + tools_plugin.cpp (292行)。 - (1) is_safe_path() 拒绝空路径、绝对路径(/或盘符)、含..段,lexically_normal二次校验; - builtin_file_read(L50) 和 builtin_file_write(L85) 入口调用,不安全→log ERROR + 返回错误JSON。 - (2) 加锁: session g_history/g_cached_history→g_session_mutex; tools g_tools→g_tools_mutex; - g_host/g_file_io→std::atomic load(acquire)/store(release)。 - (3) 9 vtable try/catch 覆盖: session_add/save/load/history (session) + - tools_register_tool/unregister_tool/get_tools_json/execute/on_init (tools)。 - 编译: cmake --build build --config Release → 0 error 0 warning。 - ctest -C Release → 4/4 pass。 + - date: 2026-05-27 + event: "W14.3: 修复 W13.5 审计发现 — 路径遍历 + 全局状态加锁 + 9 vtable try/catch" + rating: done + detail: | + 修改 session_plugin.cpp (294行) + tools_plugin.cpp (292行)。 + (1) is_safe_path() 拒绝空路径、绝对路径(/或盘符)、含..段,lexically_normal二次校验; + builtin_file_read(L50) 和 builtin_file_write(L85) 入口调用,不安全→log ERROR + 返回错误JSON。 + (2) 加锁: session g_history/g_cached_history→g_session_mutex; tools g_tools→g_tools_mutex; + g_host/g_file_io→std::atomic load(acquire)/store(release)。 + (3) 9 vtable try/catch 覆盖: session_add/save/load/history (session) + + tools_register_tool/unregister_tool/get_tools_json/execute/on_init (tools)。 + 编译: cmake --build build --config Release → 0 error 0 warning。 + ctest -C Release → 4/4 pass。 current_groups: [] --- diff --git a/scripts/check_agents_metadata.py b/scripts/check_agents_metadata.py new file mode 100644 index 0000000..e95406f --- /dev/null +++ b/scripts/check_agents_metadata.py @@ -0,0 +1,421 @@ +#!/usr/bin/env python3 +""" +agents/ metadata self-check: profile.md YAML validity, rating range, +group cross-references, member cross-references. + +Usage: + python scripts/check_agents_metadata.py + python scripts/check_agents_metadata.py --strict # treat warnings as errors + python scripts/check_agents_metadata.py --json # machine-readable output + +Exit code: 0 = all checks pass, 1 = errors found, 2 = warnings only (--strict). + +Checks: + C1 YAML parse - every profile.md + grp-*.md front matter parses legally + C2 rating range - every performance_log entry uses a known rating token + C3 group ref - every current_groups entry points to an existing grp-*.md + C4 member ref - every group members entry points to an existing agent dir + +Requirements: Python 3.8+, PyYAML (pip install pyyaml). +""" + +import sys +import re +import argparse +import json +from pathlib import Path + +# Enforce UTF-8 I/O on Windows +for _stream in (sys.stdout, sys.stderr): + try: + _stream.reconfigure(encoding='utf-8') + except Exception: + pass + +try: + import yaml +except ImportError: + print("FATAL: PyYAML not installed. Run: pip install pyyaml", file=sys.stderr) + sys.exit(1) + + +# ============================================================================= +# Constants +# ============================================================================= + +# Allowed rating tokens (union of PROMPT_TEMPLATE.md spec + observed usage) +ALLOWED_RATINGS = frozenset({ + 'ongoing', # task in progress + 'done', # DevOps shorthand + 'completed', # standard completion + 'success', # engineer-chen style + 'good', # engineer-zhou / qa-xu style + 'A', 'A+', 'A-', # top grade + 'B', 'B+', 'B-', # mid grade + 'C', 'C+', 'C-', # low grade (spec says up to C) + 'aborted', # WORKFLOW.md §13.7 +}) + +# Valid roles (for optional C5 check, not enforced by default) +KNOWN_ROLES = frozenset({ + '架构师', '工程师', '质量工程师', 'DevOps 工程师', + 'UX/CLI 设计师', '安全工程师', '技术作家', +}) + + +# ============================================================================= +# Path helpers +# ============================================================================= + +def _repo_root(): + return Path(__file__).resolve().parent.parent + + +def _agents_dir(): + return _repo_root() / 'agents' + + +# ============================================================================= +# YAML front matter extraction +# ============================================================================= + +def _extract_front_matter(filepath): + """Return (parsed_dict, error_string). + On success: (dict, None). On failure: (None, 'reason string').""" + try: + text = filepath.read_text(encoding='utf-8') + except (OSError, UnicodeDecodeError) as e: + return None, f"read error: {e}" + + m = re.match(r'^---\s*\n(.*?)\n---', text, re.DOTALL) + if not m: + return None, "no YAML front matter (missing --- delimiters)" + + raw = m.group(1) + try: + parsed = yaml.safe_load(raw) + except yaml.YAMLError as e: + return None, f"YAML parse error: {e}" + + if parsed is None: + return None, "YAML front matter is empty" + + if not isinstance(parsed, dict): + return None, f"YAML front matter is not a mapping (got {type(parsed).__name__})" + + return parsed, None + + +# ============================================================================= +# Check C1: YAML parse +# ============================================================================= + +def check_yaml_parse(agents_dir): + """Return list of (severity, file, msg) tuples.""" + findings = [] + + # Profile files + for child in sorted(agents_dir.iterdir()): + if not child.is_dir() or child.name.startswith('.') or child.name == 'groups': + continue + pf = child / 'profile.md' + if not pf.is_file(): + findings.append(('warn', str(pf), 'profile.md not found')) + continue + result, err = _extract_front_matter(pf) + if result is None: + findings.append(('error', str(pf), err)) + else: + required = ['agent_id', 'name', 'role'] + for key in required: + if key not in result: + findings.append(('error', str(pf), f"missing required field '{key}'")) + if 'performance_log' not in result or result['performance_log'] is None: + findings.append(('warn', str(pf), "missing performance_log")) + + # Group files + groups_dir = agents_dir / 'groups' + if groups_dir.is_dir(): + for gf in sorted(groups_dir.glob('grp-*.md')): + result, err = _extract_front_matter(gf) + if result is None: + findings.append(('error', str(gf), err)) + else: + required = ['group_id', 'name', 'lead', 'mission'] + for key in required: + if key not in result or result[key] is None: + findings.append(('error', str(gf), f"missing required field '{key}'")) + + return findings + + +# ============================================================================= +# Check C2: rating range +# ============================================================================= + +def check_rating_range(agents_dir): + """Return list of (severity, file, msg) tuples.""" + findings = [] + + for child in sorted(agents_dir.iterdir()): + if not child.is_dir() or child.name.startswith('.') or child.name == 'groups': + continue + pf = child / 'profile.md' + if not pf.is_file(): + continue + result, err = _extract_front_matter(pf) + if result is None or not isinstance(result, dict): + continue + + perf_log = result.get('performance_log', []) + if not perf_log: + continue + + for i, entry in enumerate(perf_log): + if not isinstance(entry, dict): + findings.append(('error', str(pf), f'perf_log[{i}] is not a mapping')) + continue + rating = entry.get('rating') + if rating is None: + findings.append(('error', str(pf), f'perf_log[{i}] missing rating')) + elif str(rating).strip() not in ALLOWED_RATINGS: + findings.append( + ('warn', str(pf), + f'perf_log[{i}] rating="{rating}" not in allowed set')) + + return findings + + +# ============================================================================= +# Check C3: current_groups -> groups/*.md +# ============================================================================= + +def check_group_refs(agents_dir): + """Return list of (severity, file, msg) tuples.""" + findings = [] + groups_dir = agents_dir / 'groups' + + # Collect valid group_ids + valid_groups = set() + if groups_dir.is_dir(): + for gf in sorted(groups_dir.glob('grp-*.md')): + result, err = _extract_front_matter(gf) + if result is not None and isinstance(result, dict): + gid = result.get('group_id') + if gid: + valid_groups.add(str(gid).strip()) + + for child in sorted(agents_dir.iterdir()): + if not child.is_dir() or child.name.startswith('.') or child.name == 'groups': + continue + pf = child / 'profile.md' + if not pf.is_file(): + continue + result, err = _extract_front_matter(pf) + if result is None or not isinstance(result, dict): + continue + + current_groups = result.get('current_groups', []) + if not current_groups: + continue + + for g in current_groups: + gid = str(g).strip() + # Strip parenthetical annotations like "grp-xxx (inactive)" + gid_clean = re.sub(r'\s*\(.*\)', '', gid).strip() + if gid_clean and gid_clean not in valid_groups: + findings.append( + ('error', str(pf), + f'current_groups references unknown group "{gid_clean}"')) + + return findings + + +# ============================================================================= +# Check C4: group members -> agents/*/ +# ============================================================================= + +def check_member_refs(agents_dir): + """Return list of (severity, file, msg) tuples.""" + findings = [] + groups_dir = agents_dir / 'groups' + + # Collect valid agent_ids + valid_agents = set() + for child in sorted(agents_dir.iterdir()): + if not child.is_dir() or child.name.startswith('.') or child.name == 'groups': + continue + if (child / 'profile.md').is_file(): + valid_agents.add(child.name) + + if not groups_dir.is_dir(): + return findings + + for gf in sorted(groups_dir.glob('grp-*.md')): + result, err = _extract_front_matter(gf) + if result is None or not isinstance(result, dict): + continue + + members = result.get('members', []) + lead = result.get('lead') + + # Check lead + if lead and str(lead).strip() not in valid_agents: + findings.append( + ('error', str(gf), + f'lead "{lead}" is not a valid agent_id')) + + # Check members + for m in (members or []): + mid = str(m).strip() + if mid and mid not in valid_agents: + findings.append( + ('error', str(gf), + f'member "{mid}" is not a valid agent_id')) + + return findings + + +# ============================================================================= +# Check C5: duplicate IDs (bonus safety net) +# ============================================================================= + +def check_duplicate_ids(agents_dir): + """Check for duplicate agent_id / group_id across files.""" + findings = [] + + agent_ids = {} + for child in sorted(agents_dir.iterdir()): + if not child.is_dir() or child.name.startswith('.') or child.name == 'groups': + continue + pf = child / 'profile.md' + if not pf.is_file(): + continue + result, err = _extract_front_matter(pf) + if result is None or not isinstance(result, dict): + continue + aid = result.get('agent_id') + if aid: + aid = str(aid).strip() + if aid in agent_ids: + findings.append( + ('error', str(pf), + f'duplicate agent_id "{aid}" (also in {agent_ids[aid]})')) + else: + agent_ids[aid] = str(pf) + + # Also verify dir name matches agent_id + for child in sorted(agents_dir.iterdir()): + if not child.is_dir() or child.name.startswith('.') or child.name == 'groups': + continue + pf = child / 'profile.md' + if not pf.is_file(): + continue + result, err = _extract_front_matter(pf) + if result is None or not isinstance(result, dict): + continue + aid = result.get('agent_id') + if aid and str(aid).strip() != child.name: + findings.append( + ('warn', str(pf), + f'directory name "{child.name}" != agent_id "{str(aid).strip()}"')) + + # Group ID duplicates + groups_dir = agents_dir / 'groups' + group_ids = {} + if groups_dir.is_dir(): + for gf in sorted(groups_dir.glob('grp-*.md')): + result, err = _extract_front_matter(gf) + if result is None or not isinstance(result, dict): + continue + gid = result.get('group_id') + if gid: + gid = str(gid).strip() + if gid in group_ids: + findings.append( + ('error', str(gf), + f'duplicate group_id "{gid}" (also in {group_ids[gid]})')) + else: + group_ids[gid] = str(gf) + + return findings + + +# ============================================================================= +# Main +# ============================================================================= + +def main(): + parser = argparse.ArgumentParser( + description='Check agents/ metadata integrity (profile.md + groups/*.md).' + ) + parser.add_argument( + '--strict', action='store_true', + help='Treat warnings as errors (exit 2 -> exit 1).' + ) + parser.add_argument( + '--json', action='store_true', + help='Machine-readable JSON output.' + ) + args = parser.parse_args() + + agents_dir = _agents_dir() + if not agents_dir.is_dir(): + print(f'ERROR: agents/ not found at {agents_dir}', file=sys.stderr) + sys.exit(1) + + check_suites = [ + ('C1', 'YAML parse', check_yaml_parse), + ('C2', 'rating range', check_rating_range), + ('C3', 'group refs', check_group_refs), + ('C4', 'member refs', check_member_refs), + ('C5', 'duplicate IDs', check_duplicate_ids), + ] + + all_findings = [] + for code, label, fn in check_suites: + findings = fn(agents_dir) + all_findings.extend((code, label, f) for f in findings) + + errors = [f for f in all_findings if f[2][0] == 'error'] + warnings = [f for f in all_findings if f[2][0] == 'warn'] + + if args.json: + output = { + 'passed': len(errors) == 0 and (not args.strict or len(warnings) == 0), + 'errors': [ + {'check': f[0], 'suite': f[1], 'file': f[2][1], 'message': f[2][2]} + for f in errors + ], + 'warnings': [ + {'check': f[0], 'suite': f[1], 'file': f[2][1], 'message': f[2][2]} + for f in warnings + ], + 'summary': { + 'total_errors': len(errors), + 'total_warnings': len(warnings), + 'checks_ran': 5, + } + } + print(json.dumps(output, ensure_ascii=False, indent=2)) + else: + if not all_findings: + print('OK: All 5 metadata checks passed.', file=sys.stderr) + else: + for code, label, (sev, filepath, msg) in all_findings: + tag = 'ERROR' if sev == 'error' else 'WARN' + print(f'[{code}] {tag}: {filepath}: {msg}', file=sys.stderr) + print( + f'\nSummary: {len(errors)} errors, {len(warnings)} warnings', + file=sys.stderr + ) + + if errors: + sys.exit(1) + if args.strict and warnings: + sys.exit(2) + sys.exit(0) + + +if __name__ == '__main__': + main()