diff --git a/agents/validator/AGENTS.md b/agents/validator/AGENTS.md index 2e1867d..4c77209 100644 --- a/agents/validator/AGENTS.md +++ b/agents/validator/AGENTS.md @@ -18,6 +18,7 @@ - 每次正式验收先加载 `qa-task-validation`。 - 不加载 `feishu-lark`——graphlemon 不使用飞书 Base,已由 `c_org_node_trigger`/`task` 表替代(05 节 §5.2)。 - Web/UI 验收按需加载 `software-quality`;通用验收原则可参考 `software-test-acceptance`,冲突时以 `qa-task-validation` 和本项目状态机(04-P)为准。 +- **任务描述里说不清楚验收对象的确切数量或边界时**(出现"扫描/审计/全部/所有/系统性/未知规模"这类无边界表述,或者开始前答不出"这轮应该恰好找到几个"),额外加载 `unknown-scope-defect-sweep`(2026-08-06,条目 B)——持续搜索、按结构性标识去重、连续两轮无新发现才停止,不要对这类任务只扫一遍就当扫完了。边界清晰的常规任务(一个 Development Unit、一个已知文件列表)不需要这个 Skill,继续走 `qa-task-validation` 的单轮验收流程。 ## 唯一事实源(migrated 2026-07-31:飞书 Base → graphlemon 结构化存储) diff --git a/docs/todo/graph-engineering-open-items.md b/docs/todo/graph-engineering-open-items.md index f4c384a..97aa8ce 100644 --- a/docs/todo/graph-engineering-open-items.md +++ b/docs/todo/graph-engineering-open-items.md @@ -36,7 +36,7 @@ ## B. loop-until-dry 要不要在 Validator 落地 -**状态**:✅ 已确认方向——做成 Validator 的一个 Skill +**状态**:✅ 已实现(2026-08-06)→ 需求文档 `docs/todo/validator-loop-until-dry-skill-requirements.md` + 设计文档 `docs/todo/validator-loop-until-dry-skill-design.md` **来源**:5 步版对照 §4 开放问题 1 @@ -44,7 +44,7 @@ **确认结果(2026-08-05)**:不改 Work Graph 执行模型本身(不引入图内回路)。做成一个独立的 Skill(例如 `validate-until-dry` 之类),把"连续 K 轮无新发现才停止、去重比对全部见过的而非仅已确认的"这套收敛逻辑封装成 Validator 可调用的方法论/工具,并且要求 `.claude/agents/validator.md` 在跑到"未知规模缺陷扫描"这类场景时记得调用它——落地方式是 Skill + Agent 定义引用,不是改 Graph Engine 底层。 -**待办**:✅ 需求文档已产出 → `docs/validator-loop-until-dry-skill-requirements.md`。关键设计澄清:loop-until-dry 不需要图内回路——收敛循环发生在**单个 Work Node 一次 `claude -p` 会话内部**(多轮工具调用),`graph_manifest` 本身仍是无环的,不需要碰 `topological_order`。 +**待办**:✅ 已实现(2026-08-06)。关键设计澄清:loop-until-dry 不需要图内回路——收敛循环发生在**单个 Work Node 一次 `claude -p` 会话内部**(多轮工具调用),`graph_manifest` 本身仍是无环的,不需要碰 `topological_order`。实现内容:新 Skill `skills/unknown-scope-defect-sweep/SKILL.md`(K=2 连续无新发现停止、20 轮硬上限、去重键必须是结构性标识而非自然语言描述、去重集合是"全部见过的"而非"已确认的"——14 步版点名的常见坑),接入 `agent_profile.py::FRONTMATTER["validator"]["skills"]`;`agents/validator/AGENTS.md`"必须加载"一节新增触发指引(任务描述说不清验收对象确切数量/边界时加载)。FR-3 的判定框架接线选择了复用 `qa-task-validation` 已有的 PASS/FAIL/INCONCLUSIVE 判断,**没有**接 F 的 `root_cause_trace`/三视角评审团——两者是流水线上不同阶段(验证本身 vs 验证失败后的根因归属),不应该混用,设计文档第 2 节记录了这个取舍。本轮唯一有确定性代码行为、可测试的部分是 FR-4(Skill 接入 `agent_profile.py` 渲染进部署产物),新增 `test_agent_profile.py::test_validator_references_unknown_scope_defect_sweep_skill`;验收标准 1-3(真的能找全、真的能正确去重、真的能在硬上限停止)依赖真实 `claude -p` 多轮工具调用行为,只能用 `RUN_REAL_AGENT_E2E=1` 验证,不在本轮 mock 测试范围内,如实记录(跟 F 设计文档"视角节点并发时间收益的实测"是同一类限制)。`python3 -m unittest discover -s src`(587 tests)全绿,零回归。 --- @@ -140,7 +140,7 @@ | **G** 并行执行 | Work Graph 层内并发、层间屏障 | ✅ **已实现**(`bc0eaa5`,575 测试全绿) | M | 已完成 | 无 | | **E** 动态 Work Graph 设计能力 | 让 Coder/Validator 也能规划自己的图,Planning 提示词识别可并行节点 | ✅ **已实现**(2026-08-06,580 测试全绿) | L | 已完成 | G(已满足) | | **F** 三视角评审团 fan-out | `judge_root_cause` 从 Python 函数改成 Work Graph 菱形拓扑 | ✅ **已实现**(2026-08-06,586 测试全绿) | M | 已完成 | 无(G 已满足) | -| **B** loop-until-dry Skill | Validator 未知规模扫描收敛能力 | 实现中 | S/M | P0(正在推进) | 无 | +| **B** loop-until-dry Skill | Validator 未知规模扫描收敛能力 | ✅ **已实现**(2026-08-06,587 测试全绿) | S/M | 已完成 | 无 | | **H** Role Package 抽象层 | 统一 role/context/skills/applications/workflows/permissions/acceptance 打包格式 | ✅ 需求评审通过(2026-08-06),待设计 | XL | P2(战略级,不阻塞其他项) | workflows 字段依赖 E 的产出(已满足) | | **D** Time-travel 调试 | 回到历史节点分叉重跑 | 仅需求 | L | P3 | 无强依赖,但价值随图变复杂(E 落地后)才会显现 | | C 按节点选模型 | — | ⛔ 已确认不做 | — | — | — | diff --git a/docs/todo/validator-loop-until-dry-skill-design.md b/docs/todo/validator-loop-until-dry-skill-design.md new file mode 100644 index 0000000..ce26137 --- /dev/null +++ b/docs/todo/validator-loop-until-dry-skill-design.md @@ -0,0 +1,24 @@ +# Validator "未知规模扫描收敛" Skill——详细设计(SDD) + +**日期**: 2026-08-06 +**性质**: 详细设计,对应需求文档 `docs/todo/validator-loop-until-dry-skill-requirements.md`(条目 B,已确认)。回答该需求文档第 8 节的开放问题。 +**状态**: 设计草案,实施中。 + +## 1. 范围确认:纯 Skill,不动执行引擎 + +跟 E/F/G 不同,这条不涉及 `work_graph_orchestrate.py`/`work_node_run.py` 任何一行代码——需求文档第 1 节的"关键澄清"已经说清楚:收敛循环发生在**一次 Work Node 执行内部**(一次 `claude -p` 会话自己的多轮工具调用),不是 `graph_manifest` 层面的图内回路。所以本轮改动只有三处:新增一个 Skill 文件、`agent_profile.py::FRONTMATTER["validator"]["skills"]` 加一行、`agents/validator/AGENTS.md` 加一段触发指引。 + +## 2. 开放问题的最终决定 + +- **Skill 命名**:`unknown-scope-defect-sweep`(需求文档 FR-1 自己给出的暂定名,评审后确认沿用,语义准确,不需要改)。目录 `skills/unknown-scope-defect-sweep/SKILL.md`,与 `qa-task-validation` 平级。**不**照抄 `qa-task-validation/SKILL.md` 里遗留的飞书 Base 内容(约束第 1 条已经点名),按 graphlemon 现有的 `task`/`bug_record` 结构化存储来写。 +- **FR-3 接线方式**:每个候选项复用 `qa-task-validation` 已经定义的 PASS/FAIL/INCONCLUSIVE 判定框架(同一次会话里 Validator 已经加载了这个 Skill,直接引用它的判定原则,不重新发明一套),**不**接 `root_cause_trace` 的三视角评审团机制——`root_cause_trace`/`judge_root_cause_via_work_graph`(F)解决的是"验证失败后,根因属于 planning/coder/skill_defect 哪一类",是验证**之后**的归因步骤;这里要解决的是"扫描出的候选项本身是不是真缺陷",是验证**本身**,两者是流水线上不同阶段,不应该混用。 +- **K 值/硬上限**:K=2(连续 2 轮无新发现即停止,需求文档背景部分引用的 14 步版建议值,也是 FR-1 自己写的参考值)。硬上限:**20 轮**——这完全是提示词层面的自我计数(一次 `claude -p` 会话内部没有 graphlemon 代码能从外部数轮次),保守值,风险第 2 条已经承认第一版数字要靠实测校准,不是理论推导出来的精确值。触达硬上限时 Skill 要求如实报告"可能未收敛",不能假装已经扫完。 +- **AGENTS.md 触发条件**:关键词信号(任务描述含"扫描/审计/全部/所有/系统性/未知规模"这类无边界表述)+ 一条更本质的判断标准("这个任务在开始前能不能说清楚验收对象的确切数量或边界"——数不清就该用这个 Skill,能数清就走默认单轮验证),写进 `AGENTS.md`"必须加载"一节。 + +## 3. 去重键(FR-2)——不绑定具体场景 + +Skill 不规定"用文件路径"还是"用函数名"这类具体键,而是给一条通用原则:去重键必须是候选项的**结构性标识**(能唯一定位这个候选项在代码/系统里的位置——文件路径+行号、路由路径、依赖包名之类),不能用候选项的**自然语言描述**做键(同一个真实缺陷,模型两次描述的措辞很可能不完全一样,用文本做键会导致同一个候选项被误判成"新发现")。 + +## 4. 唯一可测的代码触点 + +FR-4 的 `agent_profile.py::FRONTMATTER["validator"]["skills"]` 追加 + `deploy_agent_definitions()` 渲染进 `.claude/agents/validator.md` 的 `skills:` frontmatter——这是本轮唯一有确定性代码行为、可以用现有 `test_agent_profile.py` 模式写单测的部分(验收标准 4)。验收标准 1-3(真的能找全 5 处缺陷、真的能正确去重收敛、真的能在硬上限强制停止)依赖真实 `claude -p` 多轮工具调用行为,只能用 `RUN_REAL_AGENT_E2E=1` 真实验证,不在本轮 mock 测试范围内——这跟 F 设计文档第 6 节"视角节点并发时间收益的实测"是同一类限制,如实记录,不假装 mock 测试能替代它。 diff --git a/skills/unknown-scope-defect-sweep/SKILL.md b/skills/unknown-scope-defect-sweep/SKILL.md new file mode 100644 index 0000000..a321ec2 --- /dev/null +++ b/skills/unknown-scope-defect-sweep/SKILL.md @@ -0,0 +1,51 @@ +--- +name: unknown-scope-defect-sweep +description: | + QA Agent 面对"规模未知"的扫描/审计类任务(没有预先划定范围,比如"找出所有未处理的 + Promise""审计所有路由文件的鉴权缺失")时使用:持续搜索、按结构性标识去重、连续两轮 + 无新发现才停止,避免"扫一遍就当扫完了"或反过来无限扫描下去。 +version: 1.0.0 +tags: + - qa + - convergence + - sweep +metadata: + origin: graphlemon-native + related: [qa-task-validation, docs/todo/validator-loop-until-dry-skill-design.md] +platforms: [linux, macos, windows] +--- + +# SKILL:unknown-scope-defect-sweep + +## 适用场景 + +- 任务描述里没有给出确切的验收对象数量或边界("扫描整个代码库找 X""审计所有 Y"),开始前说不清楚"验收完了应该恰好找到几个"。 +- **不适用**:验收对象边界清晰(一个 Development Unit、一个已知文件列表)的常规任务——那种场景直接走 `qa-task-validation` 的单轮验收流程,不要为了"用上这个 Skill"强行套上多轮扫描的外壳。 + +## 核心循环 + +1. **搜索一轮**:用当前已知的搜索策略(grep 模式、目录遍历、依赖图遍历……)找出这一轮能发现的候选项。 +2. **去重**:把这一轮的候选项跟"全部已见过的候选项集合"比对(不是跟"已确认为真缺陷的集合"比对——见下面"去重键"一节,这是本 Skill 最容易踩错的一步)。 +3. **验证**:对这一轮**真正新增**的候选项,套用 `qa-task-validation` 已有的 PASS/FAIL/INCONCLUSIVE 判定框架逐个判断是不是真缺陷。不接根因归属机制(那是验证失败之后才需要的另一个步骤,不是这里的事)。 +4. **计数**:如果这一轮没有产生任何新增候选项(不管是因为没搜到,还是搜到了但已经在"全部已见过"集合里),连续无新发现轮次 +1;否则清零。 +5. **停止判定**:连续无新发现轮次达到 2(K=2)→ 停止,汇总全部经验证为真的候选项。或者总轮次达到 20(硬上限)→ 强制停止,如实报告"已达硬上限,可能未完全收敛",不能假装已经扫完。 + +## 去重键——本 Skill 最容易踩错的一步 + +去重键必须是候选项的**结构性标识**(文件路径+行号、路由路径、包名/版本这类能唯一定位这个候选项在系统里的位置),**不能**用候选项的自然语言描述做键。 + +**为什么这条是硬性规则**:同一个真实缺陷,模型两次描述的措辞几乎不会逐字相同("第 42 行缺少空值检查" vs "line 42 doesn't check for null")——如果用描述文本做去重键,同一个候选项会在每一轮都被判定成"新发现",收敛条件永远不会触发,循环会一直跑到硬上限才停,而不是真正找完就停。 + +同样重要:去重比对的是"全部已见过的候选项",**不是**"已确认通过验证的候选项"。一个候选项如果在验证阶段被判定为误报(FAIL/不是真缺陷),它依然要留在"已见过"集合里——如果只把"已确认"的存进去重集合,下一轮同一个搜索策略会再次挖出这个已经被否决过的候选项,当成"新发现"重新验证一遍,循环同样永远不收敛。这是 14 步版参考资料点名的"几乎所有人第一次实现都会踩的坑"。 + +## 硬上限——防止收敛条件本身失效 + +20 轮是保守默认值(还没有真实运行数据校准,未来观察真实场景后可以调整)。触达硬上限本身不是失败,而是一个诚实的"未收敛"信号:汇总目前已验证为真的候选项,同时明确报告"扫描在达到硬上限时被强制终止,不保证已覆盖全部范围",不要在报告里暗示扫描是完整的。 + +## 自审清单 + +- [ ] 去重键是结构性标识,不是自然语言描述。 +- [ ] 去重集合包含"全部见过的",不是只有"已确认的"。 +- [ ] 每个新增候选项都经过 PASS/FAIL/INCONCLUSIVE 判断,不是"出现过就算数"。 +- [ ] 连续无新发现轮次的计数逻辑正确(新一轮只要有任何新增候选项就清零,不是永久递增)。 +- [ ] 达到硬上限时如实报告"可能未收敛",没有假装扫完。 diff --git a/src/agent_profile.py b/src/agent_profile.py index 80d0da9..d7c5e86 100644 --- a/src/agent_profile.py +++ b/src/agent_profile.py @@ -72,7 +72,10 @@ FRONTMATTER = { "validator": { "memory": "project", "model": "claude-sonnet-5", - "skills": ["qa-task-validation", "unattended-agent-operations", "design-dynamic-work-graph"], + "skills": [ + "qa-task-validation", "unattended-agent-operations", "design-dynamic-work-graph", + "unknown-scope-defect-sweep", + ], "tools": ["Read", "Bash", "Grep", "Glob"], # 2026-07-31 fix, found for real running U-014's full closed loop: # scoped Bash patterns here ("Bash(git commit:*)") don't just block diff --git a/src/test_agent_profile.py b/src/test_agent_profile.py index a8cf5da..0846ced 100644 --- a/src/test_agent_profile.py +++ b/src/test_agent_profile.py @@ -30,6 +30,12 @@ class RenderAgentDefinitionTest(unittest.TestCase): self.assertIn("- Edit", content) self.assertIn("- Write", content) + def test_validator_references_unknown_scope_defect_sweep_skill(self): + # B (2026-08-06): unknown-scope-defect-sweep must actually be wired + # into the deployed skills: frontmatter, not just exist on disk. + content = agent_profile.render_agent_definition("validator") + self.assertIn("- unknown-scope-defect-sweep", content) + def test_validator_disallowed_tools_has_no_scoped_bash_pattern(self): # 2026-07-31: confirmed for real that any "Bash(...)" entry in a # subagent's disallowedTools silently disables Bash *entirely*, not