diff --git a/docs/todo/org-graph-unification-and-universal-self-planning-requirements.md b/docs/todo/org-graph-unification-and-universal-self-planning-requirements.md index 92624f5..86c81a3 100644 --- a/docs/todo/org-graph-unification-and-universal-self-planning-requirements.md +++ b/docs/todo/org-graph-unification-and-universal-self-planning-requirements.md @@ -76,7 +76,7 @@ 3. **Fix-skill 节点**:不是修这一次的输出,是**修 Skill 本身**——真实调用已经存在的 Skill 迭代机制(`src/skill_update.py::update_skill()` + `src/bug_record.py::create_bug_record(root_cause_category='skill_defect')`),产出一次真实的、Git 版本化、可回滚的 Skill 内容更新(`skill_iteration` 表,`before_git_commit_sha`/`after_git_commit_sha` 都是真实 commit,不是模拟)。**这套机制在代码库里早就存在**(U-015/U-016 时期就建好了),但至今只在 `org_level_loop.py` 的 Org 级粗粒度路径上被触发过——FR-4 要把它真正接进 Work Graph 内部的判断/回路循环,让它成为节点级失败恢复的常规路径之一,不是只有跨 Org Agent 的大失败才配触发。 4. **跳回执行**:Skill 更新落地后,跳回"用这个 Skill 的节点"重新执行——这次用的是刚修好的新 Skill,不是拿旧 Skill 再赌一次运气。跟 `_loop_history_prompt_suffix`(现有机制,把历史反馈拼进 prompt)的本质区别:prompt 里的反馈是**这一次任务专属、跑完就忘**的临时纠偏;Skill 更新是**持久化、跨任务复用**的真正修复——两者不互斥(可以同时生效:这次重试既带着历史反馈,也带着已经更新的 Skill),但 FR-4 的核心价值在后者。 -**跟 Phase 2 SDD 的关系**:`docs/todo/org-graph-unification-phase2-universal-self-planning-design.md` §5 设计的 `on_fail.action="dynamic_loop_back"` 是这个方法论第 1/2/4 步(判断失败 → 动态诊断根因节点 → 跳回执行)的**已完成设计**,但**第 3 步(Fix-skill)还没有设计**——目前的 `dynamic_loop_back` 只处理"一次性内容错误"这一种诊断结果,没有覆盖"系统性 skill 缺陷"这条分支。这是 Phase 2 设计文档需要补齐的部分,不是另开一个 Phase——具体设计(Reflection-review 节点判断"一次性 vs 系统性"的标准、Fix-skill 节点的 graph_manifest 形状、"用哪个 Skill 文件"跟"节点声明的 skill_ref"字段(FR-3)怎么对应上)留给该文档的下一次修订。 +**跟 Phase 2 SDD 的关系**:`docs/todo/org-graph-unification-phase2-universal-self-planning-design.md` §5 已经是四步的完整设计(2026-08-12 补齐)——`on_fail.action="dynamic_loop_back"` 触发时,Reflection-review(§5.4)同时判断"跳回哪个节点"和"这是一次性错误还是系统性 Skill 缺陷";系统性缺陷时先经过 Fix-Skill(§5.5,真实调用既有但从未真正投入生产使用的 `skill_update.py::update_skill()`,产出 Git 版本化的 Skill 迭代)再跳回执行。由哪个身份执笔改 Skill,已确认(2026-08-12)统一用 Validator(该文档 §5.5)。 --- diff --git a/docs/todo/org-graph-unification-phase2-universal-self-planning-design.md b/docs/todo/org-graph-unification-phase2-universal-self-planning-design.md index 8331e32..8606727 100644 --- a/docs/todo/org-graph-unification-phase2-universal-self-planning-design.md +++ b/docs/todo/org-graph-unification-phase2-universal-self-planning-design.md @@ -3,7 +3,7 @@ **日期**: 2026-08-08 **性质**: 详细设计,对应 `docs/todo/org-graph-unification-and-universal-self-planning-requirements.md` 的 Phase 2(FR-2/FR-3/FR-4),按 §4/§4.1 已拍板的合图(方案 A)路径设计。 **依赖**: 硬依赖 Phase 1(`docs/done/org-graph-unification-phase1-review-gate-design.md`)——已实现(commit `ea19c7d`,2026-08-08)。§9 接口核对清单已于 2026-08-12 逐条核对完成:三项全部吻合(`plan_and_review_documents()` 经 `_review_planner_output_with_retries()` 透传 `plan_documents()` 的 `{"run_id", "artifact_ids"}` 返回形状,含 `run_id`;`task.status` 新值确认是 `reviewing_planner_output`;`roles/planner_reviewer/role.json` 目前 `skills: ["planner-output-review", "unattended-agent-operations"]`,未预留 §7.1 新 skill 位置,Phase 2 实施时新增一行即可,不是不兼容)。 -**状态**: 设计草案,Phase 1 前置条件已满足,可以开始实施。**2026-08-08 修订**:写作过程中,需求文档 §7 的四个开放问题已被用户直接拍板(详见该文档 §7 对应条目),其中两条推翻了本文档最初的设计判断——Coder/Validator 自规划改为"建议节点清单"而非完全自由(§3),`work_graph_run.org_node` 改为彻底移除而非保留(§2.1)——本版本已按拍板结果改写,不是原始草稿。**2026-08-08 追加**(同日,见需求文档新增 §3.1):FR-4 的目标被明确为"失败即进化"完整方法论(判断失败 → Reflection-review 动态诊断 → 系统性缺陷则先 Fix-Skill → 跳回执行),本文档 §5 设计的 `dynamic_loop_back` 只覆盖其中第 1/2/4 步,第 3 步(Fix-Skill)是已知缺口,见 §5 开头说明和 §8 第 5 条。 +**状态**: 设计草案,Phase 1 前置条件已满足,可以开始实施。**2026-08-08 修订**:写作过程中,需求文档 §7 的四个开放问题已被用户直接拍板(详见该文档 §7 对应条目),其中两条推翻了本文档最初的设计判断——Coder/Validator 自规划改为"建议节点清单"而非完全自由(§3),`work_graph_run.org_node` 改为彻底移除而非保留(§2.1)——本版本已按拍板结果改写,不是原始草稿。**2026-08-08 追加**(同日,见需求文档新增 §3.1):FR-4 的目标被明确为"失败即进化"完整方法论(判断失败 → Reflection-review 动态诊断 → 系统性缺陷则先 Fix-Skill → 跳回执行),当时 §5 设计的 `dynamic_loop_back` 只覆盖第 1/2/4 步,第 3 步(Fix-Skill)是已知缺口。**2026-08-12 补齐**:§5 已扩写为完整四步设计(§5.4 Reflection-review 现在同时判断目标节点和缺陷性质,§5.5 新增 Fix-Skill,真实调用既有但从未真正投入使用的 `skill_update.py::update_skill()`),过程中发现两个既有事实(`skills:` frontmatter 只按名字动态加载内容、不需要重新部署;`org_level_loop.py` 的 `_ROUTE_FOR_CATEGORY` 从来没有 `skill_defect` 这个 key,`update_skill()` 在这个代码库里投产以来从未被真正调用过)——详见 §5 开头。由哪个身份执笔改 Skill 已通过 `AskUserQuestion` 确认(2026-08-12):统一用 Validator,见 §5.5。 --- @@ -87,6 +87,10 @@ if org_node: 3. **`INSERT INTO work_graph_run (...)`(第 164-168 行)**、**`fork_info`/`execute_fork()` 里的 `org_node`(第 189/208 行)**:INSERT 语句去掉 `org_node` 列;`execute_fork()` 调用 `run_graph()` 时原来传 `fork_info["org_node"]` 的位置,改传 `fork_info["subagent_type"]`(源节点自己的执行身份,作为 `run_graph()` 新签名里"没有节点级覆盖时的默认值",见 §4.1)。 +#### 2.1.3 `root_cause_trace.py::judge_root_cause_via_work_graph`(第三处,设计 §5 Fix-Skill 时新发现,§2.1 原稿遗漏的一处) + +`judge_root_cause_via_work_graph()`(`root_cause_trace.py:222-226`)自己也有一次 `INSERT INTO work_graph_run (run_id, task_id, org_node, graph_manifest, status) VALUES (...)`——跟 Planner/Coder/Validator 一样直接写了 `org_node` 列,同样要删掉这个列。这个函数本身不受合图影响(它总是为陪审团判断新建一张独立的小图,不追加进跨阶段大图),但既然 `work_graph_run` 表结构变了,这里的 INSERT 语句必须跟着改,不能漏掉——本文档 §5.4 的 `_judge_root_cause_node` 直接复用这个函数的骨架,实施时会经过这段代码,顺手一起改。 + ### 2.2 `ux_work_graph_run_active` 语义变化(需要迁移) `schema.py:151-152`:`CREATE UNIQUE INDEX ux_work_graph_run_active ON work_graph_run(task_id, org_node)`(覆盖非终态 status)——语义是"同一 task 同一 org_node 同一时刻只有一个活跃 run"。合图后一个 task 全程只有一个活跃 run(不再按 org_node 切分),这条唯一索引的粒度要从 `(task_id, org_node)` 收紧为 `(task_id)`: @@ -220,15 +224,19 @@ def _run_one_node(conn, run_id, org_node, skill_id, target_folder_path, node, ou --- -## 5. 新增 `on_fail.action = "dynamic_loop_back"`(对应 §1.3,FR-4 的核心) +## 5. 新增 `on_fail.action = "dynamic_loop_back"` + 完整四步链路(Reflection-review → Fix-Skill → Re-loop) + +需求文档 §3.1("失败即进化")把 FR-4 定为四步:①判断失败 ②Reflection-review 动态诊断(一次性内容错误 vs 系统性 Skill 缺陷)③如果是系统性缺陷,先修 Skill 本身 ④跳回执行。本节 2026-08-08 首版只设计了①②④,第 3 步(Fix-Skill)本次补齐——本次设计过程中读代码发现了两个直接决定 Fix-Skill 该怎么接的既有事实,先说明: -**2026-08-08 范围说明**:需求文档新增 §3.1("失败即进化:Reflection → Fix-Skill → Re-loop")把 FR-4 的完整方法论定为四步——判断失败→Reflection-review 动态诊断根因→**如果根因是系统性 Skill 缺陷,先修 Skill 本身(真实调用 `skill_update.py::update_skill()`,产出 Git 版本化的 Skill 迭代)**→跳回执行。本节设计的 `dynamic_loop_back` 只覆盖第 1/2/4 步(诊断根因节点 + 跳回执行),对应"一次性内容错误"这一种诊断结果;**"系统性 skill 缺陷"分支(第 3 步 Fix-skill 节点)还没有设计**,是本文档已知的缺口,不是遗漏——留给下一次修订,具体要解决:Reflection-review 怎么判断"一次性 vs 系统性"、Fix-skill 节点的 graph_manifest 形状、`skill_update.update_skill()` 需要的 `skill_id`/`repo_path` 跟 §4 节点级 `skill_ref` 字段怎么对应。 +**既有事实 A(关键,决定了"改 Skill 会不会真的生效")**:`.claude/agents/.md` 的 `skills:` frontmatter 只引用 Skill **名字**(`agent_profile.py::_frontmatter_yaml`),真正的 `skills//SKILL.md` 内容是 Claude Code 自己在每次 `claude --agent ` 调用时按名字动态加载的——**改完 Skill 文件、提交,下一次调用同一个 org_node 就会用上新内容,不需要重新部署 `.claude/agents/*.md`**。这意味着"改 Skill → 跳回执行"这个链路不需要额外一步"重新部署",直接可用。 + +**既有事实 B(说明这条能力在这个代码库里目前完全是摆设)**:`org_level_loop.py::run_fix_loop()` 的 `_ROUTE_FOR_CATEGORY = {"coder": "fast_lane", "planning": "full_replan"}`(`org_level_loop.py:10`)**没有 `skill_defect` 这个 key**——`root_cause_trace.py` 的三视角判断早就能产出 `skill_defect` 这个类别(`_PERSPECTIVE_FRAMINGS["skill_defect"]`,`root_cause_trace.py:112-116`),`bug_record.create_bug_record()` 也接受这个类别,但 `route = _ROUTE_FOR_CATEGORY.get(category, "full_replan")` 对 `skill_defect` 直接落到 `full_replan` 这个兜底值——**`skill_update.py::update_skill()` 在这个代码库里投产以来从未被任何生产代码路径真正调用过**,只有测试在用。Fix-Skill 是第一次把这条早就设计好、但从没接线的能力真正用起来。(这个 dead-code 缺口本身跟 Phase 2 无关,独立可修,见 §8 第 6 条——但不是本次设计的阻塞项,Fix-Skill 走的是全新的合图内部路径,不依赖修好这个旧 bug。) ### 5.1 语义 -跟现有 `loop_back`(跳回一个规划阶段就写死的固定目标)不同,`dynamic_loop_back` 表示"这个节点失败时,由一次专门的根因判断调用,从这个节点的全部上游祖先节点里动态选出真正该回退的目标"——只用于合适放在**图末端的终审性判断节点**(比如 Phase 1 引入的终审 Validator 节点),不要求每个节点都用这个更贵的机制。 +跟现有 `loop_back`(跳回一个规划阶段就写死的固定目标)不同,`dynamic_loop_back` 表示"这个节点失败时,由一次专门的根因判断调用(Reflection-review),从这个节点的全部上游祖先节点里动态选出真正该回退的目标,并判断这是一次性内容错误还是系统性 Skill 缺陷;后者先修 Skill(Fix-Skill)再回退"——只用于合适放在**图末端的终审性判断节点**(比如 Phase 1 引入的终审 Validator 节点),不要求每个节点都用这个更贵的机制。 -### 5.2 graph_manifest 字段 +### 5.2 graph_manifest 字段(不变) ```jsonc { @@ -239,49 +247,137 @@ def _run_one_node(conn, run_id, org_node, skill_id, target_folder_path, node, ou } ``` -### 5.3 执行逻辑(`work_graph_orchestrate.py` 新增分支) - -`_run_one_node()` 里 `action = (node.get("on_fail") or {}).get("action", "retry_self")` 判断分支新增一支: +### 5.3 执行逻辑总览(`work_graph_orchestrate.py` 新增分支) ```python if action == "dynamic_loop_back": ancestors = sorted(_transitive_ancestors(graph_manifest, node_id) - {node_id}) - diagnosis = _judge_root_cause_node( - conn, node_id, feedback, ancestors, org_node, target_folder_path - ) # 见 §5.4,新函数 + diagnosis = _judge_root_cause_node( # 见 §5.4,Reflection-review + conn, run_id, node_id, feedback, ancestors, graph_manifest, build_prompt, outputs, + org_node, target_folder_path, + ) target = diagnosis["target_node_id"] - if target not in ancestors: # 运行时防御,见下方"安全校验" + if target not in ancestors: # 运行时防御,见 §5.7 raise ValueError( "dynamic_loop_back judge picked {!r}, not a real ancestor of {}".format(target, node_id) ) + if diagnosis["defect_kind"] == "skill_defect": + _fix_skill_for_node( # 见 §5.5,Fix-Skill——先修完 Skill 再触发回退 + conn, run_id, target, graph_manifest, build_prompt, outputs, feedback, target_folder_path, + ) raise _LoopBackRequested(source_node_id=node_id, target_node_id=target, feedback=feedback) ``` -`_LoopBackRequested`/`run_graph()` 主循环(`_run_layers` 里 `loop_back_signal` 的处理,`work_graph_orchestrate.py:811-830`)**完全不需要改**——它已经是"给一个 `(source_node_id, target_node_id)` 就重新执行 target 及其全部下游"的通用机制,不关心这个 target 是静态声明来的还是动态判断来的,`_transitive_dependents`/`_loop_history_prompt_suffix`/`_resolve_loop_limit` 全部原样复用。**这印证了这确实是"新增一个 action 分支"而不是"重写引擎"**——回路的执行部分完全通用,只有"怎么决定 target 是谁"这一步是新的。 +`_LoopBackRequested`/`run_graph()` 主循环(`_run_layers` 里 `loop_back_signal` 的处理,`work_graph_orchestrate.py:811-830`)**完全不需要改**——它已经是"给一个 `(source_node_id, target_node_id)` 就重新执行 target 及其全部下游"的通用机制,不关心这个 target 是静态声明来的还是动态判断来的,也不关心跳回之前有没有发生过 Fix-Skill 这一步。`target` 节点重新执行时会真的用上刚提交的新 Skill 内容(既有事实 A)——**这就是第 4 步"跳回执行、用新 Skill"的完整实现,不需要额外的"通知节点使用新 Skill"机制**,因为 Skill 加载本来就是运行时按名字动态发生的。 -### 5.4 `_judge_root_cause_node`:复用 `root_cause_trace.py` 的陪审团结构,换判断目标 +### 5.4 Reflection-review:`_judge_root_cause_node`——判断目标节点 + 判断缺陷性质 -`root_cause_trace.judge_root_cause_via_work_graph`(`docs/done/verifier-fan-out-jury-design.md`)已经是"多视角 Work Graph fan-out + `_tally_perspectives` 多数票仲裁"的现成机制,只是今天判断的是三选一的粗粒度类别(`planning`/`coder`/`skill_defect`)。新函数复用同一个骨架,把"判断类别"换成"判断具体节点": +复用 `root_cause_trace.judge_root_cause_via_work_graph` 的"多视角 Work Graph fan-out + `_tally_perspectives` 多数票仲裁"骨架,但这次要求陪审团在给出目标节点的同时,**再判断一次这是一次性错误还是系统性缺陷**——这是 Reflection-review 全链路复盘的核心职责,不能只做"选节点"这一半。 ```python -def _judge_root_cause_node(conn, failed_node_id, feedback, candidate_node_ids, org_node, target_folder_path, timeout=180): - """跟 root_cause_trace.judge_root_cause_via_work_graph 同构:每个视角 - 是一次独立的 Work Node fan-out 判断调用,答案从 candidate_node_ids - 里选一个(不是自由文本),_tally_perspectives 同款 >=2/3 多数票仲裁; - < 2/3 一致时不像 root_cause_trace 那样抛 InconclusiveEscalateError 升级 - 人工——见 §5.5,这里退化成"回到最近的上游节点"而不是直接升级人工, - 因为这个判断本身就是可以再来一轮的(下一次 dynamic_loop_back 还有机会 - 重新判断),不像根因分类升级人工那样是终局判断。 +_REFLECTION_OUTPUT_SCHEMA = { + "type": "object", "required": ["target_node_id", "defect_kind", "detail"], + "properties": { + "target_node_id": {"type": "string"}, + "defect_kind": {"type": "string", "enum": ["content_error", "skill_defect"]}, + "detail": {"type": "string"}, + }, +} + +def _judge_root_cause_node( + conn, run_id, failed_node_id, feedback, candidate_node_ids, graph_manifest, build_prompt, outputs, + org_node, target_folder_path, timeout=180, +): + """三个视角,每个是一次独立 Work Node fan-out 调用(跟 + judge_root_cause_via_work_graph 同一套机制),但答案是 + {target_node_id, defect_kind, detail} 而不是自由文本;target_node_id + 必须是 candidate_node_ids 之一。多数票仲裁维度是 target_node_id(哪个 + 节点最多票就是哪个);defect_kind 取该目标节点获票的判断里的多数值 + (同一票里 target 和 defect_kind 是绑定的一对答案,不是分开投票)。 + < 2/3 一致时不升级人工(跟 root_cause_trace 不同,见 §5.7 第 2 点)。 """ ``` -三个视角建议:①"哪个节点的产出内容本身有实质缺陷"(对着候选节点逐个看 output_payload)、②"如果都没有实质缺陷,是不是节点之间的依赖/衔接有问题"、③固定复用一次原始 `feedback`(终审判断本身给出的失败原因)里能不能直接抽取出候选节点名字。三个视角都在同一批 `candidate_node_ids` 里选一个,用 `_tally_perspectives` 的既有多数票逻辑仲裁。 +三个视角的具体提问: + +1. **内容视角**:"逐个看 `candidate_node_ids` 每个节点在这次 `outputs` 里的真实产出内容(`build_prompt(candidate_node, outputs)` 重建它当初被要求做什么 + `outputs[candidate_node_id]` 是它实际给出的什么),哪一个的内容本身站不住脚?是这次内容恰好没写好(`content_error`),还是这类节点反复容易出这类问题、看起来是方法论/prompt 本身有系统性偏差(`skill_defect`)?" —— 直接把 `validator-test-script-narrative-output-issue.md` §6 的根因分析("先探索再产出"这类节点收尾话术天然倾向叙述文字)作为 `skill_defect` 的具体识别范式喂给这个视角,不是让它凭空判断。 +2. **衔接视角**:"如果每个候选节点单独看都没有实质内容缺陷,是不是节点之间的依赖关系/输入衔接本身有问题(比如一个节点该依赖另一个节点的输出却没声明 `depends_on`)?" —— 这种情况通常判 `content_error`(属于这次规划的偶然疏漏,不是 Skill 方法论错误)。 +3. **历史视角**:"这个候选节点对应的 org_node/skill_ref 组合,在 `execution_event`(`loop_back_triggered`/`skill_iterated` 两种类型)里过去是否已经因为同一类问题被跳回或修过 Skill?如果是重复出现的同一个坑,倾向 `skill_defect`;如果是第一次见,倾向 `content_error`。" —— 这是三个视角里唯一利用跨任务/跨轮次历史数据的一个,需要真的查一次 `execution_event`,不是只看这一次的 `outputs`。 + +### 5.5 Fix-Skill:真实修 Skill,不是修这一次的输出 + +**触发条件**:`_judge_root_cause_node` 判定 `defect_kind == "skill_defect"`。**要修哪个 Skill**:直接查 `graph_manifest` 里 `target_node_id` 节点自己声明的 `skill_ref`(§4 节点级绑定字段,缺省退回图级默认 `skill_id`)——不查 `work_node_execution` 表(那张表的 `node_id` 可能带 `-attemptN` 后缀,从 `graph_manifest` 原始节点定义读更直接、无歧义)。 + +**由谁来修——已用户拍板(2026-08-12):统一用 Validator 身份**(`org_node="validator"`),不管出问题的节点自己是哪个 org_node,都不让它自己改自己的 Skill。理由:`skill_update.py` 模块自己的 docstring 原话是"a **Validator**-triggered rewrite"——这是 03-C 时期就定下的既有设计意图,本设计只是第一次真正把它接起来,不是新发明;更本质的理由是,如果一个 org_node 的 Skill 本身有系统性偏差,让它用同一个(有偏差的)判断力去修自己的 Skill,大概率修不到点子上(这次踩的坑,恰恰是它自己看不出来的盲区)——独立的 Validator 身份更接近"找另一双眼睛"这个 Reflection-review 一直在做的事。 + +**真实调用链**: + +```python +def _fix_skill_for_node(conn, run_id, target_node_id, graph_manifest, build_prompt, outputs, feedback, target_folder_path, timeout=300): + target_node = next(n for n in graph_manifest["nodes"] if n["node_id"] == target_node_id) + skill_id = target_node.get("skill_ref", "unknown") # 图级默认由调用方 run_graph() 的 skill_id 参数兜底 + + current_content = _read_skill_content(skill_id) # skills//SKILL.md, skill_query._REPO_ROOT + task_id = conn.execute("SELECT task_id FROM work_graph_run WHERE run_id = ?", (run_id,)).fetchone()[0] + goal_text = conn.execute("SELECT goal_text FROM task WHERE task_id = ?", (task_id,)).fetchone()[0] + + rewrite_result = org_node_trigger.trigger_org_node_structured( # 见 work-graph-node-execution-deblackbox-requirements.md + "validator", _SKILL_REWRITE_PROMPT.format( + goal_text=goal_text, skill_id=skill_id, current_content=current_content, + what_was_asked=build_prompt(target_node, outputs), + what_it_produced=outputs[target_node_id].get("content", ""), + failure_feedback="; ".join(feedback or []), + loop_history=_loop_history_prompt_suffix(conn, run_id, target_node_id), # 复用既有函数 + ), + target_folder_path, _SKILL_REWRITE_SCHEMA, timeout=timeout, + ) + + bug_result = bug_record.create_bug_record( + conn, task_id=task_id, + description="Skill {} produced repeated defective output at node {}".format(skill_id, target_node_id), + root_cause_category="skill_defect", root_cause_detail=rewrite_result["change_summary"], + suggested_route="full_replan", # 沿用既有枚举值填充要求,本路径不真的按它路由 + ) + iteration = skill_update.update_skill( + conn, skill_id, bug_result["bug_id"], rewrite_result["new_content"], + rewrite_result["change_summary"], repo_path=skill_query._REPO_ROOT, # 见既有事实 A 上方的先例 + ) + _log_skill_iterated_event(conn, run_id, target_node_id, skill_id, iteration["iteration_id"]) # 见下方 + return iteration +``` + +`_SKILL_REWRITE_PROMPT`/`_SKILL_REWRITE_SCHEMA`(`{"new_content": str, "change_summary": str}`)是新增内容,方法论("怎么在不破坏这个 Skill 现有正确行为的前提下,精确修正导致这次失败的那一小部分")沉淀成新 skill `skills/skill-defect-remediation/SKILL.md`(见 §7.1 更新),挂到 `roles/validator/role.json`——这是 Validator 身份专属的新能力,不是 §7.1 `work-graph-node-review` 的职责范围(那个判断"一个节点的输出好不好",这个是"怎么改一份 Skill 文档本身",性质不同)。 -### 5.5 安全边界 +**记录一次 `skill_iterated` execution_event(发现:这个 event_type 已经在 CHECK 枚举里存在,从 U-001 起就有,但从未被任何代码写过——不需要新 schema 迁移)**: + +```python +def _log_skill_iterated_event(conn, run_id, target_node_id, skill_id, iteration_id): + row = conn.execute("SELECT wgr.task_id, t.project_id FROM work_graph_run wgr " + "JOIN task t ON wgr.task_id = t.task_id WHERE wgr.run_id = ?", (run_id,)).fetchone() + conn.execute( + "INSERT INTO execution_event (event_id, project_id, task_id, run_id, event_type, payload) " + "VALUES (?, ?, ?, ?, 'skill_iterated', ?)", + (str(uuid.uuid4()), row[1], row[0], run_id, json.dumps({ + "node_id": target_node_id, "skill_id": skill_id, "iteration_id": iteration_id, + })), + ) + conn.commit() +``` + +这条 event 是 Phase 3 前端(FR-5b 历史回放)能把"这一轮为什么跟上一轮不一样"讲清楚的关键数据——不只是"跳回了",而是"跳回之前,Skill 被真的改过一次,改了什么"。 + +### 5.6 Re-loop:不需要新机制 + +`_fix_skill_for_node` 返回后,§5.3 紧接着照常 `raise _LoopBackRequested(...)`——跟"一次性内容错误"分支走的是同一行代码。两条路径唯一的区别就是要不要先经过 §5.5,跳回执行本身完全复用现有机制(§5.3 已经说明为什么不需要改 `run_graph()` 主循环)。 + +### 5.7 安全边界(含新增的 Fix-Skill 专属风险) 1. **运行时校验目标必须是真实祖先**(§5.3 已展示)——即使判断调用本身犯错选了一个不存在或非祖先的 node_id,直接报错而不是静默产生一次无意义的回路。 -2. **判断未达成 2/3 一致时的退化策略**:不像 `root_cause_trace` 那样升级人工(那是跨 Org Agent 边界的终局判断,这里只是图内部一次重试的一部分),退化为"回退到 `candidate_node_ids` 里离失败节点最近的直接上游"(`_transitive_ancestors` 结果里 `depends_on` 直接列出的那些,不含更远的间接祖先)——一个保守但总能往前推进的默认值,不会卡死整个循环。 -3. **`_resolve_loop_limit` 的计数维度不变**:`_loop_attempt_count_from_events` 是按 `(target_node_id, source_node_id)` 这一对计数的(`work_graph_orchestrate.py:560-567`),`dynamic_loop_back` 每次可能选出不同的 `target_node_id`,意味着"同一个 source 连续 3 次动态判断到 3 个不同的 target"不会被现有计数逻辑拦住(各自计数都是 1)——**这是一个需要显式接受的简化**:额外加一个"同一个 source_node_id 累计触发 dynamic_loop_back 的总次数"上限(不分 target,配置沿用 `work_graph_loop_limit_config` 同一张表,`config_id` 用 `"{org_node}:{source_node_id}:total"` 这个新的 key 形状),防止"每次判断出不同目标"绕过限流、无限循环。 +2. **判断未达成 2/3 一致时的退化策略**:不像 `root_cause_trace` 那样升级人工(那是跨 Org Agent 边界的终局判断,这里只是图内部一次重试的一部分),退化为"回退到 `candidate_node_ids` 里离失败节点最近的直接上游 + `defect_kind` 视为 `content_error`"(保守默认——不确定是不是系统性问题时,不touch Skill,只是普通回退)。 +3. **`_resolve_loop_limit` 的计数维度不变**:`_loop_attempt_count_from_events` 是按 `(target_node_id, source_node_id)` 这一对计数的(`work_graph_orchestrate.py:560-567`),`dynamic_loop_back` 每次可能选出不同的 `target_node_id`,意味着"同一个 source 连续 3 次动态判断到 3 个不同的 target"不会被现有计数逻辑拦住——额外加一个"同一个 source_node_id 累计触发 dynamic_loop_back 的总次数"上限(配置沿用 `work_graph_loop_limit_config` 同一张表,`config_id` 用 `"{org_node}:{source_node_id}:total"` 这个新的 key 形状),防止绕过限流、无限循环。 +4. ~~由 Validator 身份执笔改 Skill 这个设计判断需要用户确认~~——已通过 `AskUserQuestion` 确认(2026-08-12),采用 Validator 身份,见 §5.5。 +5. **同一个 Skill 在一次 run 内被反复重写的风险**:`_fix_skill_for_node` 本身没有"这个 skill_id 这次 run 已经改过一次"的去重检查——如果 Reflection-review 连续几轮都判定同一个 `skill_ref` 是系统性缺陷(比如第一次改得不够准,问题仍然复现),会对同一个 Skill 文件连续产生多次真实 Git commit。这不算错误(每次改动理论上都在收敛),但值得在 §5.3 的分支里加一条"同一个 `(run_id, skill_id)` 组合本次 run 内的 Fix-Skill 触发次数"计数,超过一个小上限(比如 2 次)后即使判定是 `skill_defect` 也退化成普通 `content_error` 处理,避免一次任务反复重写同一份 Skill——具体阈值留给实施阶段。 +6. **self-target 场景:Skill 库commit 和任务自己的代码改动共享同一个物理仓库**——`skill_query._REPO_ROOT` 默认等于 `paths.repo_root()`;当 `target_folder_path` 恰好也是 graphlemon 自己(`paths.is_self_target(target_folder_path)` 为真)时,两者是**同一个仓库**。`update_skill()` 直接对 `skill_query._REPO_ROOT` 做 `git add`+`commit`,不经过 05 §5.8 那套 worktree 隔离——这是**有意为之,不是遗漏**:`skill_update.py` 模块 docstring 早就说明"a Validator-triggered rewrite takes effect immediately",Skill 库改动本来就设计成直接落主仓库,不像任务自己的代码改动需要隔离后再合并。两者物理上不冲突(分别只碰 `skills//` 和任务自己改的路径,如 `src/`),但没有专门测试过"self-target 任务执行期间,Fix-Skill 的直接 commit 和该任务自己的 worktree 合并操作,在 git 层面是否有任何可观测的交互"——建议 Phase 2c 实施验收时至少构造一次这个组合场景跑一遍,不要假设"理论上不冲突"就等于"实测也没事"(这个代码库里 worktree 相关的坑,历史上从来都是实测才发现的,见 CLAUDE.md 的 Isolation 章节)。 --- @@ -379,6 +475,10 @@ Phase 1 SDD 设计的 `plan_and_review_documents()`(新增函数,Planner 阶 不改造 `qa-task-validation`——那个 skill 判断的是"整体实现是否验收通过"(任务级),职责不同,两者并存,不合并。 +### 7.1.1 新 skill:`skill-defect-remediation`(Fix-Skill 专属,§5.5 新增) + +新增 `skills/skill-defect-remediation/SKILL.md`,只挂到 `roles/validator/role.json`(不是 org-agent-无关的,只有执笔改 Skill 的身份需要它——见 §5.5 的"由谁来修"决策)。方法论核心:怎么在不破坏一份 Skill 现有正确行为的前提下,精确定位并只修正导致这次真实失败的那一小部分——跟 `work-graph-node-review`(判断"一个节点的输出好不好")职责完全不同,这个 skill 判断的是"怎么改一份方法论文档本身",不合并进同一份 skill。 + ### 7.2 `org_level_loop.py`/`org_loop_route.py` 的粗粒度路由退休 Phase 1 阶段"复核失败先用现有 `org_level_loop.py` 粗粒度路由兜底"(Phase 1 SDD 的过渡方案)在 Phase 2 落地后被替换:终审 Validator 节点声明 `on_fail.action="dynamic_loop_back"`(§5),失败时直接在**同一个** `work_graph_run` 内精确回退,不再需要 `route_org_loop()` 那种"改 `task.status`、换 `assigned_org_node`、期待调用方重新触发另一个函数"的跨阶段路由。 @@ -389,11 +489,12 @@ Phase 1 阶段"复核失败先用现有 `org_level_loop.py` 粗粒度路由兜åº ## 8. 明确的不确定性(比 Phase 1 更大,如实标注) -1. **`_judge_root_cause_node`(§5.4)的判断质量未经真实验证**——三视角判断"哪个上游节点该负责"是全新设计,没有 A/B 数据支撑,上线后大概率需要根据真实误判案例调整 prompt/视角设计,不是一次写对。 +1. **`_judge_root_cause_node`(§5.4)的判断质量未经真实验证**——三视角判断"哪个上游节点该负责 + 是不是系统性缺陷"是全新设计,没有 A/B 数据支撑,上线后大概率需要根据真实误判案例调整 prompt/视角设计,不是一次写对。`defect_kind` 这个二选一判断尤其新——误判方向有两种,且代价不对称:把系统性问题误判成一次性(漏改 Skill,同类错误还会再犯,但代价只是"没有更好")比把一次性问题误判成系统性(多余地真实重写了一次 Skill 文件,代价是留下一次不必要但无害的 Git 提交)风险更值得关注,值得在真实运行中专门跟踪这两类误判各自的发生率。 2. **`promotes_to_artifact`(§2.3)依赖 Coder/Validator 老实遵守"只有最终交付物才声明"这条约束**——是 prompt 层面的软约束,不是机械强制,理论上 LLM 可能给每个节点都声明 `promotes_to_artifact`(虽然 artifact_type 枚举本身会挡住不合法值,但不会挡住"过度声明"这种滥用),需要在真实运行中观察是否需要加机械检查。 -3. **`dynamic_loop_back` 的"同一 source 总次数上限"(§5.5 第 3 点)需要新的 `work_graph_loop_limit_config` key 形状**——`"{org_node}:{source_node_id}:total"` 这个具体格式没有先例,实施时需要重新过一遍 `settings_config.py` 的查询/写入接口是否兼容这种三段式 key,还是需要专门扩展。 +3. **`dynamic_loop_back` 的"同一 source 总次数上限"(§5.7 第 3 点)需要新的 `work_graph_loop_limit_config` key 形状**——`"{org_node}:{source_node_id}:total"` 这个具体格式没有先例,实施时需要重新过一遍 `settings_config.py` 的查询/写入接口是否兼容这种三段式 key,还是需要专门扩展。 4. **`org_level_loop.py` 新旧流程共存期的判断依据**(§7.2 结尾)还没有定论,是留给实施阶段解决的开放问题,不是本文档的疏漏——但这个问题不解决,Phase 2 上线当天就会有"旧任务卡在新代码路径里"的真实风险,实施顺序上应该早排。 -5. **需求文档 §3.1 方法论的第 3 步(Fix-skill)完全没有设计**(本节 §5 开头已标注)——`dynamic_loop_back` 目前只实现"诊断根因节点 + 跳回","根因是系统性 Skill 缺陷时先修 Skill 本身"这条分支是 FR-4 方法论里明确要求、但本文档还没给出具体设计的部分,是当前最大的范围缺口,不是细节问题——建议作为 Phase 2c(或独立的 Phase 2d)优先设计,而不是留到实施中间即兴决定。 +5. ~~Fix-Skill 由谁执笔改 Skill 需要用户确认~~——已确认(2026-08-12),统一用 Validator 身份,理由见 §5.5。 +6. **发现的独立 dead-code 缺口,不阻塞 Phase 2 但值得单独一提**:`org_level_loop.py::_ROUTE_FOR_CATEGORY` 从来没有 `skill_defect` 这个 key(§5 开头"既有事实 B"),意味着这条旧的粗粒度路径下 `skill_update.update_skill()` 从来没被真正调用过。Phase 2 的 Fix-Skill 走的是全新的合图内部路径,不依赖修好这个旧 bug,但如果 §7.2 决定新旧流程要共存一段时间,这个旧路径本身的这个缺口在共存期内仍然是活的——是否值得顺手在 `_ROUTE_FOR_CATEGORY` 里补一条 `"skill_defect": "full_replan"`(不真的接 `update_skill()`,只是让路由不再是意外的默认值 fallback,语义更诚实),是一个独立、廉价、可以随时做的小修复,不必等 Phase 2c。 ---