Commit 762ba779ae935e68f59f5485339776c81c7e2850

Authored by bboonnee
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>
agents/validator/AGENTS.md
@@ -18,6 +18,7 @@ @@ -18,6 +18,7 @@
18 - 每次正式验收先加载 `qa-task-validation`。 18 - 每次正式验收先加载 `qa-task-validation`。
19 - 不加载 `feishu-lark`——graphlemon 不使用飞书 Base,已由 `c_org_node_trigger`/`task` 表替代(05 节 §5.2)。 19 - 不加载 `feishu-lark`——graphlemon 不使用飞书 Base,已由 `c_org_node_trigger`/`task` 表替代(05 节 §5.2)。
20 - Web/UI 验收按需加载 `software-quality`;通用验收原则可参考 `software-test-acceptance`,冲突时以 `qa-task-validation` 和本项目状态机(04-P)为准。 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 ## 唯一事实源(migrated 2026-07-31:飞书 Base → graphlemon 结构化存储) 23 ## 唯一事实源(migrated 2026-07-31:飞书 Base → graphlemon 结构化存储)
23 24
docs/todo/graph-engineering-open-items.md
@@ -36,7 +36,7 @@ @@ -36,7 +36,7 @@
36 36
37 ## B. loop-until-dry 要不要在 Validator 落地 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 **来源**:5 步版对照 §4 开放问题 1 41 **来源**:5 步版对照 §4 开放问题 1
42 42
@@ -44,7 +44,7 @@ @@ -44,7 +44,7 @@
44 44
45 **确认结果(2026-08-05)**:不改 Work Graph 执行模型本身(不引入图内回路)。做成一个独立的 Skill(例如 `validate-until-dry` 之类),把"连续 K 轮无新发现才停止、去重比对全部见过的而非仅已确认的"这套收敛逻辑封装成 Validator 可调用的方法论/工具,并且要求 `.claude/agents/validator.md` 在跑到"未知规模缺陷扫描"这类场景时记得调用它——落地方式是 Skill + Agent 定义引用,不是改 Graph Engine 底层。 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,7 +140,7 @@
140 | **G** 并行执行 | Work Graph 层内并发、层间屏障 | ✅ **已实现**(`bc0eaa5`,575 测试全绿) | M | 已完成 | 无 | 140 | **G** 并行执行 | Work Graph 层内并发、层间屏障 | ✅ **已实现**(`bc0eaa5`,575 测试全绿) | M | 已完成 | 无 |
141 | **E** 动态 Work Graph 设计能力 | 让 Coder/Validator 也能规划自己的图,Planning 提示词识别可并行节点 | ✅ **已实现**(2026-08-06,580 测试全绿) | L | 已完成 | G(已满足) | 141 | **E** 动态 Work Graph 设计能力 | 让 Coder/Validator 也能规划自己的图,Planning 提示词识别可并行节点 | ✅ **已实现**(2026-08-06,580 测试全绿) | L | 已完成 | G(已满足) |
142 | **F** 三视角评审团 fan-out | `judge_root_cause` 从 Python 函数改成 Work Graph 菱形拓扑 | ✅ **已实现**(2026-08-06,586 测试全绿) | M | 已完成 | 无(G 已满足) | 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 | **H** Role Package 抽象层 | 统一 role/context/skills/applications/workflows/permissions/acceptance 打包格式 | ✅ 需求评审通过(2026-08-06),待设计 | XL | P2(战略级,不阻塞其他项) | workflows 字段依赖 E 的产出(已满足) | 144 | **H** Role Package 抽象层 | 统一 role/context/skills/applications/workflows/permissions/acceptance 打包格式 | ✅ 需求评审通过(2026-08-06),待设计 | XL | P2(战略级,不阻塞其他项) | workflows 字段依赖 E 的产出(已满足) |
145 | **D** Time-travel 调试 | 回到历史节点分叉重跑 | 仅需求 | L | P3 | 无强依赖,但价值随图变复杂(E 落地后)才会显现 | 145 | **D** Time-travel 调试 | 回到历史节点分叉重跑 | 仅需求 | L | P3 | 无强依赖,但价值随图变复杂(E 落地后)才会显现 |
146 | C 按节点选模型 | — | ⛔ 已确认不做 | — | — | — | 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,7 +72,10 @@ FRONTMATTER = {
72 "validator": { 72 "validator": {
73 "memory": "project", 73 "memory": "project",
74 "model": "claude-sonnet-5", 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 "tools": ["Read", "Bash", "Grep", "Glob"], 79 "tools": ["Read", "Bash", "Grep", "Glob"],
77 # 2026-07-31 fix, found for real running U-014's full closed loop: 80 # 2026-07-31 fix, found for real running U-014's full closed loop:
78 # scoped Bash patterns here ("Bash(git commit:*)") don't just block 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,6 +30,12 @@ class RenderAgentDefinitionTest(unittest.TestCase):
30 self.assertIn("- Edit", content) 30 self.assertIn("- Edit", content)
31 self.assertIn("- Write", content) 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 def test_validator_disallowed_tools_has_no_scoped_bash_pattern(self): 39 def test_validator_disallowed_tools_has_no_scoped_bash_pattern(self):
34 # 2026-07-31: confirmed for real that any "Bash(...)" entry in a 40 # 2026-07-31: confirmed for real that any "Bash(...)" entry in a
35 # subagent's disallowedTools silently disables Bash *entirely*, not 41 # subagent's disallowedTools silently disables Bash *entirely*, not