Commit 762ba779ae935e68f59f5485339776c81c7e2850
1 parent
acfca5bf
B: unknown-scope-defect-sweep Skill for Validator loop-until-dry
Implements docs/todo/validator-loop-until-dry-skill-design.md. Pure Skill + Agent-definition wiring, no execution-engine changes -- the convergence loop runs inside a single Work Node's one claude -p session (multi-turn tool use), not a graph_manifest-level cycle, so topological_layers()/ run_graph() are untouched. New Skill skills/unknown-scope-defect-sweep/SKILL.md: K=2 consecutive no-new-finding rounds to stop, 20-round hard cap, dedup key must be a structural identifier (file path + line, route path, package name) not a natural-language description (the same finding gets reworded call to call, so text-keyed dedup never converges), dedup set is "everything ever seen" not "everything confirmed" (the common bug the 14-step reference names: a rejected candidate reappearing as a false "new finding" every round). Each candidate is verified via qa-task-validation's existing PASS/FAIL/INCONCLUSIVE framework -- deliberately NOT root_cause_trace's three-perspective jury (F), since that answers a different question (root cause AFTER a failure) from this one (is this candidate real at all) -- documented in the design doc's own tradeoff note. Wired into agent_profile.py's FRONTMATTER["validator"]["skills"] and agents/validator/AGENTS.md's "必须加载" section with a concrete trigger signal (task description can't state an exact expected count/boundary up front). Only FR-4 (Skill wired into the rendered .claude/agents/validator.md) has deterministic, mockable behavior -- new test test_validator_references_unknown_scope_defect_sweep_skill. Acceptance criteria 1-3 (actually finds everything, actually dedups correctly, actually stops at the hard cap) need real multi-turn claude -p behavior, out of scope for mocked tests (RUN_REAL_AGENT_E2E=1 territory), same category of limitation as F's own real-concurrency-timing note. Full src suite (587 tests) passes with zero regressions. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Showing
6 changed files
with
89 additions
and
4 deletions
agents/validator/AGENTS.md
| ... | ... | @@ -18,6 +18,7 @@ |
| 18 | 18 | - 每次正式验收先加载 `qa-task-validation`。 |
| 19 | 19 | - 不加载 `feishu-lark`——graphlemon 不使用飞书 Base,已由 `c_org_node_trigger`/`task` 表替代(05 节 §5.2)。 |
| 20 | 20 | - Web/UI 验收按需加载 `software-quality`;通用验收原则可参考 `software-test-acceptance`,冲突时以 `qa-task-validation` 和本项目状态机(04-P)为准。 |
| 21 | +- **任务描述里说不清楚验收对象的确切数量或边界时**(出现"扫描/审计/全部/所有/系统性/未知规模"这类无边界表述,或者开始前答不出"这轮应该恰好找到几个"),额外加载 `unknown-scope-defect-sweep`(2026-08-06,条目 B)——持续搜索、按结构性标识去重、连续两轮无新发现才停止,不要对这类任务只扫一遍就当扫完了。边界清晰的常规任务(一个 Development Unit、一个已知文件列表)不需要这个 Skill,继续走 `qa-task-validation` 的单轮验收流程。 | |
| 21 | 22 | |
| 22 | 23 | ## 唯一事实源(migrated 2026-07-31:飞书 Base → graphlemon 结构化存储) |
| 23 | 24 | ... | ... |
docs/todo/graph-engineering-open-items.md
| ... | ... | @@ -36,7 +36,7 @@ |
| 36 | 36 | |
| 37 | 37 | ## B. loop-until-dry 要不要在 Validator 落地 |
| 38 | 38 | |
| 39 | -**状态**:✅ 已确认方向——做成 Validator 的一个 Skill | |
| 39 | +**状态**:✅ 已实现(2026-08-06)→ 需求文档 `docs/todo/validator-loop-until-dry-skill-requirements.md` + 设计文档 `docs/todo/validator-loop-until-dry-skill-design.md` | |
| 40 | 40 | |
| 41 | 41 | **来源**:5 步版对照 §4 开放问题 1 |
| 42 | 42 | |
| ... | ... | @@ -44,7 +44,7 @@ |
| 44 | 44 | |
| 45 | 45 | **确认结果(2026-08-05)**:不改 Work Graph 执行模型本身(不引入图内回路)。做成一个独立的 Skill(例如 `validate-until-dry` 之类),把"连续 K 轮无新发现才停止、去重比对全部见过的而非仅已确认的"这套收敛逻辑封装成 Validator 可调用的方法论/工具,并且要求 `.claude/agents/validator.md` 在跑到"未知规模缺陷扫描"这类场景时记得调用它——落地方式是 Skill + Agent 定义引用,不是改 Graph Engine 底层。 |
| 46 | 46 | |
| 47 | -**待办**:✅ 需求文档已产出 → `docs/validator-loop-until-dry-skill-requirements.md`。关键设计澄清:loop-until-dry 不需要图内回路——收敛循环发生在**单个 Work Node 一次 `claude -p` 会话内部**(多轮工具调用),`graph_manifest` 本身仍是无环的,不需要碰 `topological_order`。 | |
| 47 | +**待办**:✅ 已实现(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)全绿,零回归。 | |
| 48 | 48 | |
| 49 | 49 | --- |
| 50 | 50 | |
| ... | ... | @@ -140,7 +140,7 @@ |
| 140 | 140 | | **G** 并行执行 | Work Graph 层内并发、层间屏障 | ✅ **已实现**(`bc0eaa5`,575 测试全绿) | M | 已完成 | 无 | |
| 141 | 141 | | **E** 动态 Work Graph 设计能力 | 让 Coder/Validator 也能规划自己的图,Planning 提示词识别可并行节点 | ✅ **已实现**(2026-08-06,580 测试全绿) | L | 已完成 | G(已满足) | |
| 142 | 142 | | **F** 三视角评审团 fan-out | `judge_root_cause` 从 Python 函数改成 Work Graph 菱形拓扑 | ✅ **已实现**(2026-08-06,586 测试全绿) | M | 已完成 | 无(G 已满足) | |
| 143 | -| **B** loop-until-dry Skill | Validator 未知规模扫描收敛能力 | 实现中 | S/M | P0(正在推进) | 无 | | |
| 143 | +| **B** loop-until-dry Skill | Validator 未知规模扫描收敛能力 | ✅ **已实现**(2026-08-06,587 测试全绿) | S/M | 已完成 | 无 | | |
| 144 | 144 | | **H** Role Package 抽象层 | 统一 role/context/skills/applications/workflows/permissions/acceptance 打包格式 | ✅ 需求评审通过(2026-08-06),待设计 | XL | P2(战略级,不阻塞其他项) | workflows 字段依赖 E 的产出(已满足) | |
| 145 | 145 | | **D** Time-travel 调试 | 回到历史节点分叉重跑 | 仅需求 | L | P3 | 无强依赖,但价值随图变复杂(E 落地后)才会显现 | |
| 146 | 146 | | C 按节点选模型 | — | ⛔ 已确认不做 | — | — | — | | ... | ... |
docs/todo/validator-loop-until-dry-skill-design.md
0 → 100644
| 1 | +# Validator "未知规模扫描收敛" Skill——详细设计(SDD) | |
| 2 | + | |
| 3 | +**日期**: 2026-08-06 | |
| 4 | +**性质**: 详细设计,对应需求文档 `docs/todo/validator-loop-until-dry-skill-requirements.md`(条目 B,已确认)。回答该需求文档第 8 节的开放问题。 | |
| 5 | +**状态**: 设计草案,实施中。 | |
| 6 | + | |
| 7 | +## 1. 范围确认:纯 Skill,不动执行引擎 | |
| 8 | + | |
| 9 | +跟 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` 加一段触发指引。 | |
| 10 | + | |
| 11 | +## 2. 开放问题的最终决定 | |
| 12 | + | |
| 13 | +- **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` 结构化存储来写。 | |
| 14 | +- **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 哪一类",是验证**之后**的归因步骤;这里要解决的是"扫描出的候选项本身是不是真缺陷",是验证**本身**,两者是流水线上不同阶段,不应该混用。 | |
| 15 | +- **K 值/硬上限**:K=2(连续 2 轮无新发现即停止,需求文档背景部分引用的 14 步版建议值,也是 FR-1 自己写的参考值)。硬上限:**20 轮**——这完全是提示词层面的自我计数(一次 `claude -p` 会话内部没有 graphlemon 代码能从外部数轮次),保守值,风险第 2 条已经承认第一版数字要靠实测校准,不是理论推导出来的精确值。触达硬上限时 Skill 要求如实报告"可能未收敛",不能假装已经扫完。 | |
| 16 | +- **AGENTS.md 触发条件**:关键词信号(任务描述含"扫描/审计/全部/所有/系统性/未知规模"这类无边界表述)+ 一条更本质的判断标准("这个任务在开始前能不能说清楚验收对象的确切数量或边界"——数不清就该用这个 Skill,能数清就走默认单轮验证),写进 `AGENTS.md`"必须加载"一节。 | |
| 17 | + | |
| 18 | +## 3. 去重键(FR-2)——不绑定具体场景 | |
| 19 | + | |
| 20 | +Skill 不规定"用文件路径"还是"用函数名"这类具体键,而是给一条通用原则:去重键必须是候选项的**结构性标识**(能唯一定位这个候选项在代码/系统里的位置——文件路径+行号、路由路径、依赖包名之类),不能用候选项的**自然语言描述**做键(同一个真实缺陷,模型两次描述的措辞很可能不完全一样,用文本做键会导致同一个候选项被误判成"新发现")。 | |
| 21 | + | |
| 22 | +## 4. 唯一可测的代码触点 | |
| 23 | + | |
| 24 | +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 测试能替代它。 | ... | ... |
skills/unknown-scope-defect-sweep/SKILL.md
0 → 100644
| 1 | +--- | |
| 2 | +name: unknown-scope-defect-sweep | |
| 3 | +description: | | |
| 4 | + QA Agent 面对"规模未知"的扫描/审计类任务(没有预先划定范围,比如"找出所有未处理的 | |
| 5 | + Promise""审计所有路由文件的鉴权缺失")时使用:持续搜索、按结构性标识去重、连续两轮 | |
| 6 | + 无新发现才停止,避免"扫一遍就当扫完了"或反过来无限扫描下去。 | |
| 7 | +version: 1.0.0 | |
| 8 | +tags: | |
| 9 | + - qa | |
| 10 | + - convergence | |
| 11 | + - sweep | |
| 12 | +metadata: | |
| 13 | + origin: graphlemon-native | |
| 14 | + related: [qa-task-validation, docs/todo/validator-loop-until-dry-skill-design.md] | |
| 15 | +platforms: [linux, macos, windows] | |
| 16 | +--- | |
| 17 | + | |
| 18 | +# SKILL:unknown-scope-defect-sweep | |
| 19 | + | |
| 20 | +## 适用场景 | |
| 21 | + | |
| 22 | +- 任务描述里没有给出确切的验收对象数量或边界("扫描整个代码库找 X""审计所有 Y"),开始前说不清楚"验收完了应该恰好找到几个"。 | |
| 23 | +- **不适用**:验收对象边界清晰(一个 Development Unit、一个已知文件列表)的常规任务——那种场景直接走 `qa-task-validation` 的单轮验收流程,不要为了"用上这个 Skill"强行套上多轮扫描的外壳。 | |
| 24 | + | |
| 25 | +## 核心循环 | |
| 26 | + | |
| 27 | +1. **搜索一轮**:用当前已知的搜索策略(grep 模式、目录遍历、依赖图遍历……)找出这一轮能发现的候选项。 | |
| 28 | +2. **去重**:把这一轮的候选项跟"全部已见过的候选项集合"比对(不是跟"已确认为真缺陷的集合"比对——见下面"去重键"一节,这是本 Skill 最容易踩错的一步)。 | |
| 29 | +3. **验证**:对这一轮**真正新增**的候选项,套用 `qa-task-validation` 已有的 PASS/FAIL/INCONCLUSIVE 判定框架逐个判断是不是真缺陷。不接根因归属机制(那是验证失败之后才需要的另一个步骤,不是这里的事)。 | |
| 30 | +4. **计数**:如果这一轮没有产生任何新增候选项(不管是因为没搜到,还是搜到了但已经在"全部已见过"集合里),连续无新发现轮次 +1;否则清零。 | |
| 31 | +5. **停止判定**:连续无新发现轮次达到 2(K=2)→ 停止,汇总全部经验证为真的候选项。或者总轮次达到 20(硬上限)→ 强制停止,如实报告"已达硬上限,可能未完全收敛",不能假装已经扫完。 | |
| 32 | + | |
| 33 | +## 去重键——本 Skill 最容易踩错的一步 | |
| 34 | + | |
| 35 | +去重键必须是候选项的**结构性标识**(文件路径+行号、路由路径、包名/版本这类能唯一定位这个候选项在系统里的位置),**不能**用候选项的自然语言描述做键。 | |
| 36 | + | |
| 37 | +**为什么这条是硬性规则**:同一个真实缺陷,模型两次描述的措辞几乎不会逐字相同("第 42 行缺少空值检查" vs "line 42 doesn't check for null")——如果用描述文本做去重键,同一个候选项会在每一轮都被判定成"新发现",收敛条件永远不会触发,循环会一直跑到硬上限才停,而不是真正找完就停。 | |
| 38 | + | |
| 39 | +同样重要:去重比对的是"全部已见过的候选项",**不是**"已确认通过验证的候选项"。一个候选项如果在验证阶段被判定为误报(FAIL/不是真缺陷),它依然要留在"已见过"集合里——如果只把"已确认"的存进去重集合,下一轮同一个搜索策略会再次挖出这个已经被否决过的候选项,当成"新发现"重新验证一遍,循环同样永远不收敛。这是 14 步版参考资料点名的"几乎所有人第一次实现都会踩的坑"。 | |
| 40 | + | |
| 41 | +## 硬上限——防止收敛条件本身失效 | |
| 42 | + | |
| 43 | +20 轮是保守默认值(还没有真实运行数据校准,未来观察真实场景后可以调整)。触达硬上限本身不是失败,而是一个诚实的"未收敛"信号:汇总目前已验证为真的候选项,同时明确报告"扫描在达到硬上限时被强制终止,不保证已覆盖全部范围",不要在报告里暗示扫描是完整的。 | |
| 44 | + | |
| 45 | +## 自审清单 | |
| 46 | + | |
| 47 | +- [ ] 去重键是结构性标识,不是自然语言描述。 | |
| 48 | +- [ ] 去重集合包含"全部见过的",不是只有"已确认的"。 | |
| 49 | +- [ ] 每个新增候选项都经过 PASS/FAIL/INCONCLUSIVE 判断,不是"出现过就算数"。 | |
| 50 | +- [ ] 连续无新发现轮次的计数逻辑正确(新一轮只要有任何新增候选项就清零,不是永久递增)。 | |
| 51 | +- [ ] 达到硬上限时如实报告"可能未收敛",没有假装扫完。 | ... | ... |
src/agent_profile.py
| ... | ... | @@ -72,7 +72,10 @@ FRONTMATTER = { |
| 72 | 72 | "validator": { |
| 73 | 73 | "memory": "project", |
| 74 | 74 | "model": "claude-sonnet-5", |
| 75 | - "skills": ["qa-task-validation", "unattended-agent-operations", "design-dynamic-work-graph"], | |
| 75 | + "skills": [ | |
| 76 | + "qa-task-validation", "unattended-agent-operations", "design-dynamic-work-graph", | |
| 77 | + "unknown-scope-defect-sweep", | |
| 78 | + ], | |
| 76 | 79 | "tools": ["Read", "Bash", "Grep", "Glob"], |
| 77 | 80 | # 2026-07-31 fix, found for real running U-014's full closed loop: |
| 78 | 81 | # scoped Bash patterns here ("Bash(git commit:*)") don't just block | ... | ... |
src/test_agent_profile.py
| ... | ... | @@ -30,6 +30,12 @@ class RenderAgentDefinitionTest(unittest.TestCase): |
| 30 | 30 | self.assertIn("- Edit", content) |
| 31 | 31 | self.assertIn("- Write", content) |
| 32 | 32 | |
| 33 | + def test_validator_references_unknown_scope_defect_sweep_skill(self): | |
| 34 | + # B (2026-08-06): unknown-scope-defect-sweep must actually be wired | |
| 35 | + # into the deployed skills: frontmatter, not just exist on disk. | |
| 36 | + content = agent_profile.render_agent_definition("validator") | |
| 37 | + self.assertIn("- unknown-scope-defect-sweep", content) | |
| 38 | + | |
| 33 | 39 | def test_validator_disallowed_tools_has_no_scoped_bash_pattern(self): |
| 34 | 40 | # 2026-07-31: confirmed for real that any "Bash(...)" entry in a |
| 35 | 41 | # subagent's disallowedTools silently disables Bash *entirely*, not | ... | ... |