From fa86f3638388aaad1d4b2a3ff904ff4bc36cbe05 Mon Sep 17 00:00:00 2001 From: bboonnee Date: Thu, 6 Aug 2026 17:14:59 +0800 Subject: [PATCH] docs: move A/B/D/E/F/G/H's requirements+design docs into docs/done/ --- docs/done/dynamic-work-graph-design-skill-design.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ docs/done/dynamic-work-graph-design-skill-requirements.md | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/done/role-package-abstraction-design.md | 111 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/done/role-package-abstraction-requirements.md | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/done/validator-loop-until-dry-skill-design.md | 24 ++++++++++++++++++++++++ docs/done/validator-loop-until-dry-skill-requirements.md | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/done/verifier-fan-out-jury-design.md | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/done/verifier-fan-out-jury-requirements.md | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/done/work-graph-parallel-execution-design.md | 277 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/done/work-graph-parallel-execution-requirements.md | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/done/work-graph-time-travel-debugging-design.md | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/done/work-graph-time-travel-debugging-requirements.md | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/todo/dynamic-work-graph-design-skill-design.md | 51 --------------------------------------------------- docs/todo/dynamic-work-graph-design-skill-requirements.md | 68 -------------------------------------------------------------------- docs/todo/graph-engineering-open-items.md | 16 ++++++++-------- docs/todo/role-package-abstraction-design.md | 111 --------------------------------------------------------------------------------------------------------------- docs/todo/role-package-abstraction-requirements.md | 76 ---------------------------------------------------------------------------- docs/todo/validator-loop-until-dry-skill-design.md | 24 ------------------------ docs/todo/validator-loop-until-dry-skill-requirements.md | 59 ----------------------------------------------------------- docs/todo/verifier-fan-out-jury-design.md | 70 ---------------------------------------------------------------------- docs/todo/verifier-fan-out-jury-requirements.md | 69 --------------------------------------------------------------------- docs/todo/work-graph-parallel-execution-design.md | 277 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- docs/todo/work-graph-parallel-execution-requirements.md | 76 ---------------------------------------------------------------------------- docs/todo/work-graph-time-travel-debugging-design.md | 61 ------------------------------------------------------------- docs/todo/work-graph-time-travel-debugging-requirements.md | 73 ------------------------------------------------------------------------- skills/design-dynamic-work-graph/SKILL.md | 2 +- skills/unknown-scope-defect-sweep/SKILL.md | 2 +- src/agent_profile.py | 2 +- src/org_level_loop.py | 2 +- src/root_cause_trace.py | 2 +- src/schema.py | 2 +- src/test_org_level_loop.py | 2 +- src/test_work_graph_fork.py | 2 +- src/work_graph_fork.py | 2 +- src/work_graph_orchestrate.py | 2 +- src/work_graph_plan.py | 2 +- 36 files changed, 1034 insertions(+), 1034 deletions(-) create mode 100644 docs/done/dynamic-work-graph-design-skill-design.md create mode 100644 docs/done/dynamic-work-graph-design-skill-requirements.md create mode 100644 docs/done/role-package-abstraction-design.md create mode 100644 docs/done/role-package-abstraction-requirements.md create mode 100644 docs/done/validator-loop-until-dry-skill-design.md create mode 100644 docs/done/validator-loop-until-dry-skill-requirements.md create mode 100644 docs/done/verifier-fan-out-jury-design.md create mode 100644 docs/done/verifier-fan-out-jury-requirements.md create mode 100644 docs/done/work-graph-parallel-execution-design.md create mode 100644 docs/done/work-graph-parallel-execution-requirements.md create mode 100644 docs/done/work-graph-time-travel-debugging-design.md create mode 100644 docs/done/work-graph-time-travel-debugging-requirements.md delete mode 100644 docs/todo/dynamic-work-graph-design-skill-design.md delete mode 100644 docs/todo/dynamic-work-graph-design-skill-requirements.md delete mode 100644 docs/todo/role-package-abstraction-design.md delete mode 100644 docs/todo/role-package-abstraction-requirements.md delete mode 100644 docs/todo/validator-loop-until-dry-skill-design.md delete mode 100644 docs/todo/validator-loop-until-dry-skill-requirements.md delete mode 100644 docs/todo/verifier-fan-out-jury-design.md delete mode 100644 docs/todo/verifier-fan-out-jury-requirements.md delete mode 100644 docs/todo/work-graph-parallel-execution-design.md delete mode 100644 docs/todo/work-graph-parallel-execution-requirements.md delete mode 100644 docs/todo/work-graph-time-travel-debugging-design.md delete mode 100644 docs/todo/work-graph-time-travel-debugging-requirements.md diff --git a/docs/done/dynamic-work-graph-design-skill-design.md b/docs/done/dynamic-work-graph-design-skill-design.md new file mode 100644 index 0000000..d763fd6 --- /dev/null +++ b/docs/done/dynamic-work-graph-design-skill-design.md @@ -0,0 +1,51 @@ +# 跨 Org Agent 的动态 Work Graph 设计能力——详细设计(SDD) + +**日期**: 2026-08-06 +**性质**: 详细设计,对应需求文档 `docs/done/dynamic-work-graph-design-skill-requirements.md`(条目 E,已确认)。回答该需求文档第 8 节的开放问题。 +**依赖**:条目 G(`docs/work-graph-parallel-execution-design.md`)已实现——`topological_layers()`/`run_graph()` 已经在生产代码里。 +**状态**: 设计草案,待评审后实施。 + +## 1. G 已经回答了 FR-2(并行分组字段格式) + +需求文档 FR-2 留空的"哪些节点属于同一个可并行分组"字段格式,G 的实际实现给出了确定答案:**不需要新字段**。`topological_layers()` 完全靠 `depends_on`/`edges` 结构本身推导独立性——没有相互依赖的节点自动落入同一层,`run_graph()` 自动并发执行同层节点。Planning Skill 的方法论职责因此简化为:"如实表达真正的依赖关系,不要为了图好看而人为加上并不存在的 depends_on"(14 步版对照笔记"边是数据契约"这条原则的直接应用),不需要额外发明一个"parallel_group"标记。 + +## 2. 新 Skill:`design-dynamic-work-graph` + +`skills/design-dynamic-work-graph/SKILL.md`,与 `planning-requirement-expansion`、`code-task-development` 等现有 Skill 平级存放。内容方法论(不含代码): + +1. 节点边界怎么划:一个节点 = 一次独立的、有明确输入输出契约的 Work Node 执行(一次 `claude -p` 调用)。任务小到一次调用就能完成时不需要拆图(呼应需求文档第 6 节约束)。 +2. `edge_type` 怎么选:`zero_token`(纯聚合/规约,不需要 Agent 推理,比如"把三份判断投票汇总")vs `needs_judgment`(下游需要真正读懂上游内容才能继续,比如"PRD 依赖 BRD 的实际内容")。 +3. 如何识别可并行的候选点:如实列出每对节点之间是否存在真实数据流动——没有数据流动就不要加 `depends_on`,G 会自动把它们并发调度。 +4. 每个节点必须自带 `task_prompt`(该节点执行时使用的完整提示词,自包含,不依赖调用方另外拼接)——这是 Coder/Validator 场景与 Planner 场景的关键区别:Planner 的四个节点类型固定(brd/prd/sdd/benchmark...),下游代码用一个查表 `_DOC_PROMPTS[artifact_type]` 就能拿到提示词;Coder/Validator 的子任务没有固定的小词表,必须由 Claude 自己在规划阶段把每个节点的提示词写出来。 + +## 3. `work_graph_plan.py` 改造(FR-3) + +**不改动 Planner 现有路径**:`_PROMPT_TEMPLATE`(brd/prd/sdd/benchmark 固定链)与 `_normalize_graph_manifest` 的 canonical-chain 强制逻辑完全保留,`org_node == "planner"` 时走原有分支,零回归风险。 + +新增: + +- `_GENERIC_PROMPT_TEMPLATE`:当 `org_node != "planner"` 时使用,要求 Claude 依据 `design-dynamic-work-graph` Skill 的方法论,把 `goal_text` 拆成一个 `graph_manifest`,每个节点除 `node_id`/`depends_on` 外必须包含 `task_prompt`(自包含提示词);输出格式(fenced ```json 块)跟现有约定一致,复用同一套 `_extract_graph_manifest` 解析逻辑。 +- `plan_work_graph()` 内部按 `org_node == "planner"` 二选一模板,函数签名/返回值不变(需求文档非目标 #2)。 +- `generic_node_prompt_builder(graph_manifest)`:新增的公开辅助函数,返回一个 `build_prompt(node, outputs)` 闭包,直接读取 `node["task_prompt"]`(`run_graph()` 的 `build_prompt` 约定不变,仍然是"给定 node + 已完成 outputs,返回这次调用的提示词")——之所以放在 `work_graph_plan.py` 而不是 `e2e_pipeline.py`,是因为它跟生成侧的字段约定(`task_prompt` 这个 key 名)强耦合,两者应该在同一个模块里维护,避免字段改名时两处不同步。 + +`_normalize_graph_manifest` 不需要改动——canonical-chain 强制分支本来就有 `all(t in by_artifact_type for t in _CANONICAL_DOC_CHAIN)` 前置判断,Coder/Validator 的动态图没有 brd/prd/sdd/benchmark 节点,天然不触发,其余的 `id`→`node_id`/`depends_on` 兜底逻辑对新路径同样适用,不需要区分。 + +## 4. Skill 接入 Coder/Validator + +`src/agent_profile.py::FRONTMATTER["coder"]["skills"]`/`["validator"]["skills"]` 各自追加 `"design-dynamic-work-graph"`。`.claude/agents/{coder,validator}.md` 是从这里渲染出来的(`render_agent_definition`),不需要手改部署产物。 + +## 5. 验证范围(对应需求文档验收标准,按非目标 #1 收窄) + +需求文档 FR-5/验收标准 1 优先级是"应该",且非目标 #1 明确"不要求第一版就覆盖 Coder/Validator 的全部真实工作场景,只需要证明机制本身走得通"。本轮实现范围: + +- 证明 `plan_work_graph(conn, "coder", ...)` 能生成一个正确带 `task_prompt`、互不依赖节点的 `graph_manifest`(mock `claude` 子进程返回一个"给三个独立模块各自补单元测试"的真实场景响应)。 +- 证明该 `graph_manifest` 经 `topological_layers()` 分层后,三个节点确实落在同一层(跟 G 的实现直接挂钩,不是空谈)。 +- 证明 `generic_node_prompt_builder` 生成的 `build_prompt` 能驱动 `run_graph()` 端到端跑完(mock `execute_work_graph`)。 +- **不改动** `e2e_pipeline.py::implement_code()` 的现有单节点生产路径——那是更大的、需要独立评估的改动(如何决定"这个 Coder 任务复杂到该规划成图"本身就是一个开放问题,需求文档风险第 3 条已经点出),留给后续任务。 + +## 6. 回答需求文档第 8 节开放问题 + +- 新 Skill 命名:`design-dynamic-work-graph`,目录 `skills/design-dynamic-work-graph/`。 +- `_PROMPT_TEMPLATE` 改造方式:按 `org_node` 二选一模板(Planner 固定链 vs 通用动态规划),不是让 Claude 完全自由决定 Planner 要生成哪些节点——Planner 那条链是"架构常量",如 `_normalize_graph_manifest` 自己的注释所说,不应该变成每次都不同。 +- FR-2 并行分组字段格式:不需要,G 靠 `depends_on`/`edges` 结构自动推导(见第 1 节)。 +- Coder/Validator 第一个接入场景:mock 场景"给三个独立模块各自补一个单元测试",用于验证机制可用性,不接入真实生产路径。 diff --git a/docs/done/dynamic-work-graph-design-skill-requirements.md b/docs/done/dynamic-work-graph-design-skill-requirements.md new file mode 100644 index 0000000..a64ce76 --- /dev/null +++ b/docs/done/dynamic-work-graph-design-skill-requirements.md @@ -0,0 +1,68 @@ +# 跨 Org Agent 的动态 Work Graph 设计能力——需求文档 + +**日期**: 2026-08-05 +**性质**: 需求文档(BRD/PRD 层面),不含详细技术设计。回应 `docs/todo/graph-engineering-open-items.md` 条目 E 的确认结果——"Planner agent 里面要有多个 skill,不仅仅是 brd/prd/sdd/benchmark,还有给其他 Org Graph 节点的 agent 做各种动态 Work Graph 设计,提示词强化肯定要做"。 +**依赖**:本文档定义的"节点分组可并行"标记方式需要跟 `docs/work-graph-parallel-execution-requirements.md`(条目 G)的详细设计对齐,建议 G 的 `graph_manifest` 数据结构定下来之后再最终敲定本文档的 FR-2。 +**状态**: 草案,待评审。 + +## 1. 背景——一个比原开放问题更大的真实缺口 + +综述版/14 步版对照笔记原本提出的问题是"要不要把识别并行节点的判断标准写进 Planning Skill 的 prompt"。深入代码后发现实际缺口比这更大: + +**当前生产代码里,只有 Planner 一个 Org Agent 真正用上了"动态生成 `graph_manifest` 再执行"这套机制**: + +- `src/work_graph_plan.py::plan_work_graph()` 的 `_PROMPT_TEMPLATE`(`work_graph_plan.py:64-74`)是**硬编码**的:不管调用方传入的 `org_node` 是什么,提示词永远要求"Output a graph_manifest with one node per artifact_type (brd, prd, sdd, benchmark)"——这个函数虽然签名上接受任意 `org_node`,但实质上只能用来生成 Planner 的文档写作链。 +- 唯一真实调用它的地方是 `src/e2e_pipeline.py::plan_documents()`,而且写死传入 `"planner"`(`e2e_pipeline.py:220`)。 +- **Coder 和 Validator 目前完全不走这条 plan→execute 的路径**。`work_graph_orchestrate.py` 模块 docstring 明确记录:真正跑通的闭环(`e2e_pipeline.py`,U-014)里,Coder/Validator 用的是"一个更简单、独立的机制(flat `org_node_trigger.trigger_org_node()` calls)"——一次性的单个 Agent 调用,不经过任何"先规划出一张图、再按图执行"的过程。 + +也就是说,架构文档 §5"每个 Org Agent 拥有一个 Planning Skill,在接到任务时先生成本轮的 Work Graph"这句话,目前只对 Planner 成立。Coder 的 to-do 拆解+并行分派(架构文档 §6.2)、Validator 的功能/性能/边界测试并行执行(架构文档 §6.3)都还停留在设计意图,没有对应的动态规划实现——这正是用户说"还有给其他 Org Graph 节点的 agent 做各种动态 Work Graph 的设计"这句话所指的真实缺口,不是一个小的提示词优化。 + +**一个已经存在、值得保留的细节**:`_PROMPT_TEMPLATE` 里已经有 `edge_type: 'zero_token' | 'needs_judgment'` 的区分(`work_graph_plan.py:69-70`),这正好对应 14 步版"边是数据契约,简单聚合应该是零 token 的代码边,只有真正需要判断力的合并才值得用 Agent"这条原则——只是目前这个字段只用在 Planner 的纯串行链上,从未真正驱动过任何"这条边要不要派生一个 Agent"的执行决策。新方案应该保留并真正用起来这个字段,而不是重新发明一套。 + +## 2. 目标 + +1. 把"如何设计一张动态 Work Graph"(识别节点边界、识别哪些节点可以 fan-out、区分 `zero_token`/`needs_judgment` 边、给 fan-out 出的并行节点分组打标记)这套方法论,从 Planner 专属的硬编码提示词里抽出来,做成一个**跨 Org Agent 通用**的能力。 +2. 让 Coder、Validator 也能各自调用这套能力,生成自己的 `graph_manifest`,走上跟 Planner 一样的 plan→execute 路径,而不是继续停留在"一次性单个 Agent 调用"的简化实现。 +3. 在生成 `graph_manifest` 阶段就识别并标注"哪些节点互不依赖、可以并行执行"——即使执行层的并行能力(条目 G)还没上线,至少先把这个信息在数据模型里留出来,不需要等 G 上线后再回头改 Planning 阶段的产出格式。 + +## 3. 非目标 + +1. **不要求第一版就覆盖 Coder/Validator 的全部真实工作场景**。架构文档 §6.2/§6.3 描述的 Coder to-do 拆解、Validator 多类型测试并行是长期目标,第一版只需要证明"Coder/Validator 也能用同一套通用方法论生成一个可执行的 `graph_manifest`"这件事本身走得通,不需要一次性把每种真实业务场景都覆盖。 +2. **不改变 `plan_work_graph`/`run_graph` 的既有函数签名和执行机制**。这是提示词/方法论层面的扩展("用什么内容去问 Claude"),不是执行引擎的改动——`_PROMPT_TEMPLATE` 硬编码这件事本身要改,但改的方式是让它按 `org_node`/任务类型选择不同的提示词模板,不是重写 `plan_work_graph` 的调用方式。 +3. **不在本文档里定案"节点分组可并行"的具体字段格式**。这个格式需要跟条目 G 的详细设计对齐(G 需要知道 `graph_manifest` 里怎么表达"这组节点可以并发触发",才能实现相应的调度逻辑),本文档只提出需求,不抢先定字段名。 + +## 4. 功能需求 + +| # | 需求描述 | 优先级 | +|---|---|---| +| FR-1 | 把"如何设计一张 Work Graph"的通用方法论(节点边界怎么划、`zero_token`/`needs_judgment` 边怎么区分、fan-out 候选点怎么识别)抽成一个独立的 Skill(暂定名 `design-dynamic-work-graph`,具体命名留待设计阶段确定),跟现有 `skills/write-brd` 一类专属技能平级存放。 | 必须 | +| FR-2 | 这个 Skill 的产出需要能标注"哪些节点属于同一个可并行分组"——具体字段格式待 G 的设计确定后再补全,本需求只要求"设计阶段必须把这个信息在 `graph_manifest` 层面表达出来"这件事本身。 | 必须 | +| FR-3 | `work_graph_plan.py::plan_work_graph()` 的提示词生成逻辑需要从"永远硬编码 brd/prd/sdd/benchmark"改造成"按调用方传入的任务描述/`org_node` 动态生成",具体改造方式(拆成多个模板、还是让 Skill 内容本身驱动 Claude 自主判断要生成哪些节点)留待设计阶段决定。 | 必须 | +| FR-4 | Coder、Validator 各自的 `.claude/agents/{coder,validator}.md`(及其 canonical 来源 `src/agent_profile.py::FRONTMATTER`)需要引用这个新 Skill。 | 必须 | +| FR-5 | 需要至少一条真实的 Coder 或 Validator 场景,验证"调用新 Skill 生成 `graph_manifest` → 走 `plan_work_graph`/`run_graph` 路径执行 → 产出正确结果"这条链路真的跑得通,不是只停留在 Planner 一个场景上验证。 | 应该 | + +## 5. 验收标准 + +1. 给 Coder 一个可以自然拆成 2-3 个互不依赖子任务的真实场景(比如"给三个独立的模块各自补一个单元测试"),验证 Coder 能通过新 Skill 生成一个 `graph_manifest`(而不是走现在的单次 `trigger_org_node` 调用),且该 `graph_manifest` 正确标注了这几个节点互不依赖。 +2. 验证 `plan_work_graph()` 面对 Coder/Validator 的调用时,不再原样返回"one node per artifact_type (brd, prd, sdd, benchmark)"这个 Planner 专属结构。 +3. 验证 Planner 现有的 BRD→PRD→SDD→Benchmark 场景在改造后行为不受影响(回归测试,`src/test_e2e_pipeline.py` 等既有用例应继续通过)。 +4. 部署后能在 Coder/Validator 的 `.claude/agents/*.md` 里看到新 Skill 出现在 `skills:` frontmatter 列表。 + +## 6. 约束与假设 + +- 复用现有的 `plan_work_graph()`/`run_graph()`/`work_graph_execute.execute_work_graph()` 执行机制,不新建一套并行的规划-执行引擎。 +- `_PROMPT_TEMPLATE` 里已有的 `zero_token`/`needs_judgment` 边类型区分要保留,新 Skill 的方法论要明确指导 Claude 在生成边时如何选择这个字段,而不是留空/随意填。 +- Coder/Validator 引入 plan→execute 路径后,原来的 `org_node_trigger.trigger_org_node()` 单次调用机制不需要立刻废弃——对于确实只需要单个节点就能完成的简单任务,继续走这条更轻量的路径是合理的(呼应架构文档和 14 步版共同的原则:"任务很小或孤立时不需要上图")。新 Skill 解决的是"任务复杂到值得规划成图时,Coder/Validator 有没有能力做这件事",不是要求所有任务都必须走图。 + +## 7. 风险 + +- **`_PROMPT_TEMPLATE` 硬编码改造有回归风险**:这是唯一一条已经在生产里真实跑通、支撑着 Planner BRD→PRD→SDD→Benchmark 全链路的代码路径,改造时必须有充分的回归测试覆盖,不能因为扩展到 Coder/Validator 而破坏 Planner 现有场景。 +- **FR-2 的具体字段格式依赖 G 的设计**,如果 G 的设计阶段推迟或方向调整,本条目的 FR-2/FR-3 也需要跟着调整,两者需要保持沟通同步,不要各自独立往前推进导致字段对不上。 +- **Coder/Validator 引入图规划后,"什么时候该规划成图、什么时候该走简单单次调用"这个判断本身也需要方法论指导**——如果不加约束,可能出现"任务很小也被规划成图"的过度设计,第 6 节约束里提到的原则需要在 Skill 提示词里显式写出来,不能只是隐式假设 Claude 会自己把握分寸。 + +## 8. 开放问题(留给详细设计阶段回答) + +- [ ] 新 Skill 的具体命名和目录结构。 +- [ ] `_PROMPT_TEMPLATE` 具体怎么改造——是给每个 `org_node` 各自一份模板,还是设计成让 Claude 根据任务描述自主决定要生成哪些节点(更灵活但更难约束输出格式)? +- [ ] FR-2 的并行分组字段具体格式(待 G 确定)。 +- [ ] Coder/Validator 第一个接入的真实业务场景选哪个,作为验收标准 1 的具体测试用例? diff --git a/docs/done/role-package-abstraction-design.md b/docs/done/role-package-abstraction-design.md new file mode 100644 index 0000000..650685b --- /dev/null +++ b/docs/done/role-package-abstraction-design.md @@ -0,0 +1,111 @@ +# 统一 Role Package 抽象层——详细设计(SDD) + +**日期**: 2026-08-06 +**性质**: 详细设计,对应需求文档 `docs/done/role-package-abstraction-requirements.md`(条目 H,需求评审已通过,见 `docs/todo/graph-engineering-open-items.md` H 节的评审记录)。回答该需求文档第 8 节的开放问题。 +**范围确认(用户 2026-08-06)**:FR-3 的 4 张 CHECK 约束表一次性全改,不分阶段。 +**状态**: 设计草案,实施中。 + +## 1. Role Package 载体格式:JSON,不是 YAML + +需求文档 FR-2 的开放问题是"单文件 vs 目录约定",但更根本的约束是 05 §5.7 的纯 stdlib 限制——graphlemon 自己的代码不能依赖 PyYAML。`skills//SKILL.md` 现有的 YAML frontmatter 之所以能用,是因为**graphlemon 自己的代码从来没有解析过它**(`skill_query.py`/`skill_update.py` 只把 SKILL.md 当不透明文本读写,真正解析 `skills:`/`tools:` frontmatter 的是 Claude Code 自己,不是这个仓库的 Python 代码)。Role Package 不一样:`agent_profile.py` 需要真正**读懂**它的结构化字段(哪些 skills、哪些 tools、memory 配置……),手写一个 YAML 子集解析器风险高、容易在多行字符串/嵌套列表这类边界情况上出 bug——纯 stdlib 的 `json` 模块能零风险做到同样的事。 + +**决定**:`roles//role.json`,纯 JSON,`json.load` 直接解析。 + +## 2. role.json 是"引用清单",不是内容复制 + +需求文档 FR-1 自己的措辞已经暗示了这一点——"context(memory 配置 + 运行时 context 组装规则的**引用**)"、"skills(**对应**现有 `skills:` frontmatter)"、"acceptance criteria(**引用**)"——七个维度里大多数本来就该是指针,不是把内容整个复制一份。 + +具体到 role/identity 维度:`agents//SOUL.md`(+ validator 的 `AGENTS.md`)**原样保留,不删除、不合并、不改名**——role.json 只存这两个文件的相对路径引用(`identity.soul_md_path`/`identity.agents_md_path`)。这个决定直接让验收标准 2("迁移后部署产物逐字节一致")几乎自动成立:`render_agent_definition()` 的 body 拼接逻辑(`SOUL.md` 内容 + `AGENTS.md` 内容)完全不变,唯一变化的是 frontmatter 字段的**来源**从硬编码 `FRONTMATTER` 字典变成解析 `role.json`。 + +```json +{ + "role_id": "validator", + "description": "graphlemon Validator: independently verifies a task, PASS/FAIL/INCONCLUSIVE, read-only on source.", + "migrated_from_profile": "qa", + "identity": { + "soul_md_path": "agents/validator/SOUL.md", + "agents_md_path": "agents/validator/AGENTS.md" + }, + "context": { "memory": "project" }, + "model": "claude-sonnet-5", + "skills": ["qa-task-validation", "unattended-agent-operations", "design-dynamic-work-graph", "unknown-scope-defect-sweep"], + "applications": { "mcp_servers": [] }, + "workflows": { "dynamic_planning": "generic" }, + "permissions": { + "tools": ["Read", "Bash", "Grep", "Glob"], + "disallowed_tools": ["Edit", "Write"], + "notes": [ + { "constraint": "不提交、推送、reset、清理或覆盖用户工作树", "enforced": "prose_only" } + ] + }, + "acceptance_criteria": { "ref": "docs/sdd/06-验收与测试锚点.md" } +} +``` + +- **applications**(FR-6,"应该"):`mcp_servers` 列表,允许为空——满足"这个维度存在、字段长什么样、允许为空",不要求真实集成(非目标 #4)。 +- **workflows**:接 E 的产出(非目标 #2 明确不重复设计 E 已经做的事,这里只是一个指向枚举)。`planner` 是 `"canonical_doc_chain"`(`work_graph_plan.py` 里 `org_node == "planner"` 走的固定链),`coder`/`validator` 是 `"generic"`(E 的 `_GENERIC_PROMPT_TEMPLATE`/`generic_node_prompt_builder` 路径)。 +- **permissions.notes**(验收标准 4):Validator 的 Bash 子命令限定失效那条已知漏洞,诚实标注 `"enforced": "prose_only"`,不假装是工具层强制——非目标 #3 明确不在本文档修复这个漏洞本身,只要求如实反映现状。 + +## 3. `agent_profile.py` 改造:`ORG_NODES` 从硬编码元组变成目录发现 + +```python +_ROLES_SOURCE_DIR = os.path.join(_REPO_ROOT, "roles") + +def _discover_role_ids(roles_dir=None): + """扫描 roles//role.json 是否存在,role_id 就是目录名。 + roles_dir 可覆盖(测试用真实文件系统 fixture 验证'新增角色不用改代码' + 这条验收标准,不需要真的往这个仓库的 roles/ 目录里塞测试数据)。""" + roles_dir = roles_dir or _ROLES_SOURCE_DIR + if not os.path.isdir(roles_dir): + return () + return tuple(sorted( + name for name in os.listdir(roles_dir) + if os.path.isfile(os.path.join(roles_dir, name, "role.json")) + )) + +ORG_NODES = _discover_role_ids() +``` + +`FRONTMATTER`/`_DESCRIPTIONS`/`_MIGRATED_FROM_PROFILE` 三个硬编码字典全部删除,替换成 `_load_role(role_id, roles_dir=None)`(`json.load` 解析对应 `role.json`)。`render_agent_definition()`/`deploy_agent_definitions()`/`record_agent_profiles()` 都接受一个可选 `roles_dir` 参数(默认 `None` 即用真实目录),这是验收标准 1 能被自动化测试验证的关键——测试往一个临时目录写一个新角色的 `role.json`,不改任何 Python 代码或 `roles_dir` 之外的任何东西,直接调用这三个未经修改的函数,确认新角色能被发现、渲染、部署。 + +`ORG_NODES` 这个名字保留不改(`org_node_trigger.py` 等外部模块已经在用 `from src.agent_profile import ORG_NODES`,改名会不必要地扩大改动面),但注释说明它现在是数据驱动的,不再是硬编码元组。 + +## 4. Schema:4 张表的 CHECK 约束怎么改 + +**决定的形状**:`CHECK (x IN ('planner', 'coder', 'validator'))` → `CHECK (x != '')`——仍然是数据库层面的最小完整性保证(非空字符串),但不再枚举具体值。不是完全去掉 CHECK(那样连"总不能是空字符串"这点基本保证都没有了),也不是新建一张 `role` 表做外键(需求文档 §8 留的另一个选项)——本设计选择更简单的那条路径:4 张表全改,但改的目标是"放开枚举"而不是"建立引用完整性",避免这次改动同时背上"外键完整性设计"这个新的复杂度。 + +**真实的改动面**(这是这次评审最重要的发现,比需求文档原始风险描述更细):`task`/`work_graph_run`/`skill` 三张表被 `work_node_execution`/`artifact`/`bug_record`/`execution_event` 等多张表通过外键引用;`agent_profile` 没有任何表引用它。SQLite 不支持 `ALTER TABLE ... DROP CONSTRAINT`,改 CHECK 约束必须用"建新表复制数据删旧表改名"的标准操作——但因为改的是**父表**、外键定义本身(`REFERENCES task(task_id)` 这类文本)**不需要跟着变**,所以只需要重建这 4 张父表本身,不需要动任何一张子表。SQLite 官方文档给的正确流程("Making Other Kinds Of Table Schema Changes")是: + +1. `PRAGMA foreign_keys = OFF`(关闭本连接的外键校验,只在这次迁移期间) +2. 开事务 +3. 对每张表:建同结构、新 CHECK 的 `_new` → `INSERT INTO
_new SELECT * FROM
` → `DROP TABLE
` → `ALTER TABLE
_new RENAME TO
` → 重建这张表原有的索引 +4. `PRAGMA foreign_key_check` 确认没有产生悬空外键 +5. 提交事务 +6. `PRAGMA foreign_keys = ON` + +`migrate_relax_org_node_constraints(conn)` 一个函数按这个流程处理全部 4 张表(一次性开一个事务,不是 4 个独立迁移各自开关 `foreign_keys`),每张表重建前用 `sqlite_master` 里的 CREATE TABLE 文本判断是否已经是新约束,已经是就跳过(幂等,`migrate_expand_artifact_types` 的同款前置检查模式)。`SCHEMA_SQL` 里的 4 处 `CHECK (... IN ('planner', 'coder', 'validator'))` 直接改成 `CHECK (... != '')`,新数据库不需要迁移。 + +**索引重建清单**(重建表之后必须重新建的,`DROP TABLE` 会连带丢失表自己的索引): +- `skill`:无额外索引。 +- `task`:`idx_task_status`、`idx_task_assigned_org_node`、`idx_task_project_id`。 +- `work_graph_run`:`ux_work_graph_run_active`(分区唯一索引,D 已经依赖这条索引的语义,重建后必须原样保留)。 +- `agent_profile`:PK 本身即索引,无需额外重建。 + +## 5. `work_graph_run_query.py::_VALID_ORG_NODES`(评审时发现的遗漏依赖) + +改成直接引用 `agent_profile.ORG_NODES`,不再自己维护一份硬编码三元组——这条不在需求文档原始 FR 列表里,是评审阶段核实代码时发现的真实遗漏(`docs/todo/graph-engineering-open-items.md` H 节评审记录已经点名),顺手一起改掉。 + +## 6. 明确不做的部分(如实记录,呼应评审阶段的结论) + +- **`frontend/app.js` 的 4 处硬编码三角色枚举**(路由正则、`ORG_NODE_ORDER` 排序数组、任务创建表单下拉、`byOrgNode` 查找对象)——评审阶段已经确认这些不在验收标准 1 的范围内(验收标准 1 明确"这个新角色不需要真的接入 Org Graph 执行"),本轮不改。新增一个角色之后,PH3 控制台依然认不出它,这是本轮已知、刻意保留的缺口,不是遗漏。 +- **`role` 表 + 外键完整性**(§4 提到的另一个选项)——本轮用更简单的"放开枚举"实现,不建立真正的引用完整性约束。 +- **`org_node_trigger.py` 校验逻辑本身**——它已经用 `from src.agent_profile import ORG_NODES` 做校验,`ORG_NODES` 变成数据驱动之后这条校验自动跟着数据驱动,不需要改这个文件。 +- **permissions 维度的 Bash 子命令限定漏洞本身**——诚实标注(§2),不修复(需求文档非目标 #3)。 + +## 7. 回答需求文档第 8 节开放问题 + +- 载体格式:单文件 `role.json`(不是目录多文件),JSON 不是 YAML(§1)。 +- CHECK 约束改造方式:放开成 `!= ''`,不建 `role` 表外键(§4)。 +- applications 最小定义:`{"mcp_servers": []}`(§2)。 +- 迁移步骤:三个角色各自的 `role.json` 直接引用现有 `agents//SOUL.md`/`AGENTS.md`,不复制内容(§2),验证方式是 `test_agent_profile.py` 现有测试断言部署产物不变。 +- Role Package 跟 E 的接口:`workflows.dynamic_planning` 字段,`"canonical_doc_chain"` / `"generic"` 二选一(§2)。 diff --git a/docs/done/role-package-abstraction-requirements.md b/docs/done/role-package-abstraction-requirements.md new file mode 100644 index 0000000..0704a5a --- /dev/null +++ b/docs/done/role-package-abstraction-requirements.md @@ -0,0 +1,76 @@ +# 统一 Role Package 抽象层——需求文档 + +**日期**: 2026-08-06 +**性质**: 需求文档(BRD/PRD 层面),不含详细技术设计。回应用户对 Agentic Enterprise 产品"可复用打包单元"目标的对照检查——"role + context + skills + applications + workflows + permissions + acceptance criteria;all essentially are context for intelligent computing"。 +**状态**: 草案,待评审。 + +## 1. 背景 + +用户提出的目标判断是:Agentic Enterprise 产品的可复用打包单元,本质上应该是 role / context / skills / applications / workflows / permissions / acceptance criteria 这七个维度,而且这七个维度**本质上是同一种东西**——喂给智能计算的 context,应该被统一表达成一个可序列化、可实例化、可复制的对象。 + +对照 graphlemon 现状(逐维度核对代码得出): + +| 维度 | 现状 | 成熟度 | +|---|---|---| +| **role** | `.claude/agents/{planner,coder,validator}.md` + `agents/*/SOUL.md`,`src/agent_profile.py::FRONTMATTER` 是权威定义 | 有清晰打包格式,但**不可重复实例化**——见第 2 节 | +| **context** | `memory: project` → `.claude/agent-memory/{role}/`,git 版本化;但 Work Node 执行时塞进 prompt 的内容是每个调用点临时拼的(`build_prompt(node, outputs)`),没有独立的"context 组装层"抽象 | 局部实现,无统一抽象 | +| **skills** | `skills//SKILL.md`,按名字挂进角色的 `skills:` frontmatter | 七项里最成熟、最像"可复用打包单元"的一个 | +| **applications** | 没有对应的一等概念。架构文档 §7.2 提到 `mcpServers` 字段可以挂专属 MCP server,但 `agent_profile.py::FRONTMATTER` 里三个角色**一个都没配** | 基本空白 | +| **workflows** | Claude Code 原生 Dynamic Workflows 机制在,但只有 Planner 真正用——`work_graph_plan.py::plan_work_graph()` 的提示词硬编码永远只生成 brd/prd/sdd/benchmark 四个节点,Coder/Validator 走的是更简化的单次 `trigger_org_node()` 调用,没有"先规划图再执行"这条路径(见 `docs/done/dynamic-work-graph-design-skill-requirements.md`,即条目 E,已确认待做) | 机制存在,不是每个角色都能复用的打包能力 | +| **permissions** | `tools`/`disallowedTools` per role(如 Validator `disallowedTools: [Edit, Write]`) | 有格式,但有记录在案的真实漏洞:`agent_profile.py` 注释明确写了"scoped Bash patterns(如 `Bash(git commit:*)`)会把 Bash 整个禁掉",Validator 的 git-mutation 限制实际上退化成了 SOUL.md 里的文字约定,不是工具层强制 | +| **acceptance criteria** | Benchmark、PASS/FAIL/INCONCLUSIVE、SDD REQ-01~15、`root_cause_trace` 根因归属 | 七项里最成熟的一环 | + +**核心判断**:这不是功能缺口,是抽象层缺口。七个维度里六个都有对应实现(applications 是真空白),但没有被收敛成一个统一的"role package"格式——要理解"Validator 这个角色完整定义是什么",现在得同时读 `agent_profile.py` + `skills/qa-task-validation/SKILL.md` + `.claude/agent-memory/validator/` + `docs/sdd/06-验收与测试锚点.md` 四个地方拼出来。而且 role 本身不是数据驱动的——`src/agent_profile.py::ORG_NODES = ("planner", "coder", "validator")` 是硬编码元组,`src/schema.py` 里四张表(`skill.owner_org_node`、`task.assigned_org_node`、`work_graph_run.org_node`、`agent_profile.org_node`)全部是 `CHECK (... IN ('planner', 'coder', 'validator'))` 的枚举约束——新增一个角色需要改代码和 schema,不是"包一个新角色进来"。 + +## 2. 目标 + +1. 定义一个统一的、可序列化的"Role Package"抽象——把 role/context/skills/applications/workflows/permissions/acceptance criteria 这七个维度表达成同一种对象的不同字段/引用,而不是分散在 frontmatter、独立目录、运行时生成脚本、SDD 文档四个不相关的地方。 +2. 让"角色"从硬编码的 3 个枚举值变成数据驱动的、可新增的对象——**具体的可验证标准**:能不能不改 `schema.py` 的 CHECK 约束、不改 `agent_profile.py` 的硬编码字典,只靠"写一份新的 role package"就让系统认识一个新角色。 +3. 把 applications 这个目前空白的维度定义清楚——至少要回答"一个角色能调用哪些外部工具/集成/MCP server"这件事该怎么表达,不要求第一版就有完整实现。 +4. 现有 Planner/Coder/Validator 三个角色要能**用新格式重新表达**(迁移,不是重写行为),验证这套抽象本身是自洽的,不是只停留在文档设计层面。 + +## 3. 非目标 + +1. **不要求第一版就实现"运行时自主创建新角色"**。架构文档 §3"关键设计原则 1"明确 Org Graph 结构是人工定义、代码级固定,改动属于架构变更需要人工评审——这条决策不动。本需求要解决的是"人工新增一个角色时,需要改多少处代码/schema"这个摩擦,而不是让系统自己决定要不要新增角色。新增角色依然是人工发起的动作,只是发起方式从"改代码"变成"写一份声明式的 package"。 +2. **不在本文档里解决 workflows 维度的具体落地**——Coder/Validator 缺少动态 Work Graph 规划能力这件事,已经是条目 E(`docs/done/dynamic-work-graph-design-skill-requirements.md`)的范围,本文档只要求"Role Package 里要有一个字段/引用指向这个角色的 workflow 规划能力",不重复设计 E 已经在做的事。 +3. **不在本文档里重新解决 permissions 维度的已知漏洞**(scoped Bash disallowedTools 不生效)——这是 Claude Code 平台本身的行为限制,本文档只要求 Role Package 的 permissions 字段要能诚实反映"这条限制实际上是工具层强制还是仅靠文字约定",不假装这个漏洞已经修复。 +4. **不要求一次性覆盖全部七个维度到同等成熟度**。applications 维度目前是空白,第一版只需要定义清楚"这个维度存在、字段长什么样、允许为空",不要求马上有真实的 MCP server 集成可用。 +5. **不做通用的"角色市场/角色商店"之类的产品化能力**。这是纯粹的内部抽象层重构,不涉及把角色包装成可分发、可交易的产品单元。 + +## 4. 功能需求 + +| # | 需求描述 | 优先级 | +|---|---|---| +| FR-1 | 定义 Role Package 的字段结构,覆盖七个维度:role(身份/人设,对应现有 SOUL.md 正文)、context(memory 配置 + 运行时 context 组装规则的引用)、skills(技能列表,对应现有 `skills:` frontmatter)、applications(可调用的外部工具/集成/MCP server 列表,当前允许为空)、workflows(该角色的 Work Graph 规划能力引用,对接条目 E 的产出)、permissions(tools/disallowedTools,并诚实标注每条限制是工具层强制还是仅文字约定)、acceptance criteria(该角色产出的验收标准引用,比如 Validator 对接 SDD REQ 锚点)。 | 必须 | +| FR-2 | Role Package 需要是**声明式**的(数据,不是代码)——具体载体形式(单个 YAML/JSON 文件、还是延续现在"一个目录 + 几个子文件"的约定)留待设计阶段决定,但判断标准是"新增一个角色不需要写 Python 代码"。 | 必须 | +| FR-3 | `src/schema.py` 里 `org_node` 相关的四处 `CHECK (org_node IN ('planner','coder','validator'))` 约束需要改造成不再硬编码这三个值——具体改造方式(改成引用一张新的 `role` 表做外键约束,还是放开成自由文本 + 应用层校验)留待设计阶段决定。 | 必须 | +| FR-4 | `src/agent_profile.py::ORG_NODES`/`FRONTMATTER` 这两个硬编码常量需要改造成从 Role Package 读取,而不是 Python 字典里硬写三个角色的定义——但 `deploy_agent_definitions()` 对外的部署行为(生成 `.claude/agents/*.md`)保持不变,只是数据来源变了。 | 必须 | +| FR-5 | 现有 Planner/Coder/Validator 三个角色需要各自被表达成一份 Role Package,作为这套抽象的第一批真实用例,且迁移后系统对外行为(部署出的 `.claude/agents/*.md` 内容、实际执行结果)不应该有可观察的变化——这是一次纯粹的内部表达方式重构,不是功能变更。 | 必须 | +| FR-6 | applications 维度需要有一个明确的最小定义(哪怕第一版只是"MCP server 列表,允许为空"),不能继续保持"架构文档提过但从未真正定义"的状态。 | 应该 | + +## 5. 验收标准 + +1. **核心验收(对应目标 2 的可验证标准)**:能够只通过"新增一份 Role Package"(不改 `schema.py`、不改 `agent_profile.py` 的硬编码逻辑)让系统认识一个新角色,并成功为它部署出一份 `.claude/agents/<新角色>.md`。这个新角色不需要真的接入 Org Graph 执行(Org Graph 本身仍是人工评审后才能改的架构决策,见非目标第 1 条),只需要验证"定义一个角色"这件事本身不再需要写 Python 代码或改 schema 约束。 +2. 迁移后,现有 Planner/Coder/Validator 三个角色部署出的 `.claude/agents/*.md` 内容(frontmatter 字段、正文)跟迁移前逐字节一致(或差异只是格式规范化,不影响语义),既有测试套件(`src/test_agent_profile.py` 一类)全部通过,不需要修改断言。 +3. 给定一份 Role Package,能够看清楚它在七个维度上分别声明了什么——不需要再像现状一样,为了理解"这个角色能干什么"而去翻四个不同的地方。 +4. permissions 维度里,Validator 的 git-mutation 限制在新格式里被诚实标注为"仅文字约定,非工具层强制"(不是假装它已经被强制执行)。 + +## 6. 约束与假设 + +- 这次重构不改变 Org Graph 本身"人工设计、变更需人工评审"的既定原则(架构文档 §3 决策 1),只改变"定义一个角色的具体成本"。 +- Role Package 的 workflows 字段依赖条目 E 的产出(跨 Org Agent 通用的动态 Work Graph 设计能力)——如果 E 还没落地,Role Package 的 workflows 字段可以先指向"占位/尚未实现",不阻塞本需求其余六个维度的推进。 +- 复用现有的 Skill 目录约定(`skills//SKILL.md`)和 memory 约定(`memory: project` → `.claude/agent-memory/{role}/`),不重新发明这两块已经工作良好的机制,只是让 Role Package 引用它们,而不是重新定义。 + +## 7. 风险 + +- **改动面比 B/D/E/F/G 任何一条都大**:`org_node` 的 CHECK 约束出现在 4 张表上,`ORG_NODES`/`FRONTMATTER` 被 `deploy_agent_definitions`、`org_node_trigger`、`work_graph_orchestrate` 等多处代码直接依赖,改造前需要在设计阶段完整梳理所有依赖点,不能只看 schema 定义本身。 +- **applications 维度定义不清可能导致设计阶段卡壳**——这是七个维度里唯一"从零开始"的,没有现成机制可以对齐(不像 skills/permissions/acceptance criteria 那样已经有半成品可以收编),需要额外的探索性设计工作,不能假设跟其他六个维度一样容易定义。 +- **"声明式、不用写代码就能新增角色"这个目标可能跟 Claude Code 本身的能力边界冲突**——`.claude/agents/*.md` 的部署机制、`skills:`/`tools:` frontmatter 本身已经是 Claude Code 原生支持的声明式格式,Role Package 更多是在这之上再包一层统一抽象,需要在设计阶段验证这层抽象不会变成"重新发明 Claude Code 已经提供的能力"的冗余包装。 + +## 8. 开放问题(留给详细设计阶段回答) + +- [ ] Role Package 的具体载体格式(单文件 vs 目录约定)。 +- [ ] `org_node` CHECK 约束的改造方式——新增 `role` 表做外键,还是放开成自由文本 + 应用层校验。 +- [ ] applications 维度的最小可行定义——只是 MCP server 列表,还是要覆盖更广的"角色能操作哪些外部系统"。 +- [ ] 迁移 Planner/Coder/Validator 三个现有角色到新格式的具体步骤和验证方式。 +- [ ] 这套抽象跟条目 E(动态 Work Graph 设计能力)的接口——Role Package 的 workflows 字段具体怎么引用 E 的产出。 diff --git a/docs/done/validator-loop-until-dry-skill-design.md b/docs/done/validator-loop-until-dry-skill-design.md new file mode 100644 index 0000000..4e93ec0 --- /dev/null +++ b/docs/done/validator-loop-until-dry-skill-design.md @@ -0,0 +1,24 @@ +# Validator "未知规模扫描收敛" Skill——详细设计(SDD) + +**日期**: 2026-08-06 +**性质**: 详细设计,对应需求文档 `docs/done/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/docs/done/validator-loop-until-dry-skill-requirements.md b/docs/done/validator-loop-until-dry-skill-requirements.md new file mode 100644 index 0000000..fa295d1 --- /dev/null +++ b/docs/done/validator-loop-until-dry-skill-requirements.md @@ -0,0 +1,59 @@ +# Validator "未知规模扫描收敛" Skill——需求文档 + +**日期**: 2026-08-05 +**性质**: 需求文档(BRD/PRD 层面),不含详细技术设计。回应 `docs/todo/graph-engineering-open-items.md` 条目 B 的确认结果——"loop-until-dry 落地在 Validator,做成一个 Skill,`validator.md` 运行时记得调用"。 +**状态**: 草案,待评审。 + +## 1. 背景 + +14 步版对照笔记(`docs/reference/Graph Engineering with Claude_ 14-Step roadmap from 0 to graph architect (Full Course).diff-with-graphlemon.md` §1 第 11 行、§2)指出:graphlemon 现有的收敛机制只有 Org-level Loop 的固定 3 轮上限(`docs/sdd/04-P-流程与状态流转.md` 已标注"未来优化候选"),而 14 步版描述的 loop-until-dry(连续 K 轮无新发现才停止,去重要对比"全部见过的"而非"已确认的")没有对应实现。 + +同一份笔记也指出一个结构性事实:`work_graph_orchestrate.py::topological_order` 对有环的 `graph_manifest` 直接报错("graph_manifest has a dependency cycle")——**Work Graph 被设计成严格 DAG,图内不支持回路**。这意味着 loop-until-dry **不能**实现成"`graph_manifest` 里画一条指回自己的边",那需要改动整个执行模型(`run_graph()`/`topological_order()`),代价远大于收益。 + +**关键澄清(本需求的立论基础)**:loop-until-dry 不需要是 Work Graph 层面的图内回路。它可以是**一个 Work Node 内部、单次 `agent()` 调用会话自己的行为**——一次 `claude -p` 调用本身就是一个多轮工具调用的会话(读文件、搜索、判断、再读文件……),只要 Skill 的提示词要求 Agent"持续搜索直到连续两轮没有新发现才停止、去重比对全部见过而非仅已确认的结果",这个收敛循环完全发生在**一次 Work Node 执行内部**,`graph_manifest` 本身仍然是一条无环的边(该节点的输入是任务描述,输出是收敛后的结构化 bug list),不需要改动 `topological_order`/`run_graph` 的任何一行代码。这跟"B. loop-until-dry 要不要在 Validator 落地"的确认结果——"做成一个 Skill"——是同一个思路:Skill 是提示词层面的方法论,不是图结构。 + +## 2. 目标 + +1. 给 Validator 一个新的 Skill,让它在面对"未知规模缺陷扫描"类任务时,能够自主判断收敛(连续 K 轮无新发现才停止),而不是像现在这样只能做"扫一遍就结束"的单轮验证。 +2. 这个 Skill 的去重逻辑必须对比"全部已见过的候选项",而不是只对比"已确认通过的结果"——避免被验证器打回的候选项在下一轮又被当成新发现重新提出,导致循环永远不收敛(14 步版原文点名的"几乎所有人第一次实现都会踩的坑")。 +3. `.claude/agents/validator.md`(及其 canonical 来源 `src/agent_profile.py::FRONTMATTER["validator"]`)需要引用这个新 Skill,并且 Validator 的人设文档(`agents/validator/SOUL.md` 迁移版)需要有明确指引:什么场景下应该主动调用这个 Skill,而不是走默认的单轮验证路径。 + +## 3. 非目标 + +1. **不改 Work Graph 执行模型**。不给 `graph_manifest`/`topological_order`/`run_graph` 引入图内回路支持,见第 1 节的关键澄清。 +2. **不改 Org-level Loop 的 3 轮上限**。Org-level Loop 管的是"Validator 判定不通过、打回 Planner/Coder、再来一轮"这个更粗粒度的循环,跟这个 Skill 管的"Validator 一次验证任务内部要扫多少轮才算扫完"是两层不同的收敛机制,互不冲突、互不替代。 +3. **不要求这个 Skill 覆盖 Validator 的所有验证场景**。大多数验证任务(比如验收一个边界清晰的 Development Unit)本来就是有限规模的,不需要 loop-until-dry;这个 Skill 只服务于"规模未知"这一类场景(比如"扫描整个代码库找所有未处理的 Promise""审计所有路由文件的鉴权缺失"这种没有预先划定范围的任务)。 + +## 4. 功能需求 + +| # | 需求描述 | 优先级 | +|---|---|---| +| FR-1 | 新增一个 Skill(暂定名 `unknown-scope-defect-sweep`,具体命名留待设计阶段确定),描述 loop-until-dry 的方法论:持续搜索/扫描 → 每轮结果与"全部已见过的候选项"去重 → 连续 K 轮(默认值留待设计阶段确定,参考 14 步版建议的 K=2)无新发现 → 停止并汇总。 | 必须 | +| FR-2 | Skill 需要明确定义"候选项"的去重键(key)该怎么取——不同任务场景(bug 扫描 vs 路由审计 vs 依赖审计)候选项的自然标识不同,Skill 需要给出通用指导原则,而不是绑定某一种具体场景。 | 必须 | +| FR-3 | Skill 执行过程中每一轮的候选发现,在最终汇总前应当经过验证(复用 Validator 已有的 PASS/FAIL/INCONCLUSIVE 判定框架或 `root_cause_trace` 机制,具体复用方式留待设计阶段确定)——不能是"只要出现过就算数",避免把误报也计入最终结果。 | 应该 | +| FR-4 | `src/agent_profile.py::FRONTMATTER["validator"]["skills"]` 需要加入这个新 Skill 的目录名,并且 `agents/validator/SOUL.md`(canonical 人设源文件)需要新增一段指引:什么信号(比如任务描述里出现"扫描/审计/全部/所有"这类无边界表述)应该触发这个 Skill,而不是走默认单轮验证路径。 | 必须 | +| FR-5 | Skill 需要有一个硬性上限(最大轮次或最大 token/时间预算),防止"连续 K 轮无新发现才停"这个收敛条件本身因为某种原因(比如候选项生成逻辑有 bug,每轮都能生成一个"看起来新"但实际重复的假发现)永远不触发,导致单次 Work Node 执行无限跑下去。 | 必须 | + +## 5. 验收标准 + +1. 给 Validator 一个人为构造的"扫描规模未知"任务(例如一个测试仓库里散落 5 处同一类缺陷),验证 Validator 使用新 Skill 后能够找全全部 5 处,并在连续两轮无新发现后主动停止,不需要人工告诉它"已经找完了"。 +2. 验证去重逻辑:人为构造一个"候选项会被验证器打回、但下一轮扫描逻辑本身还会再次生成同一个候选项"的场景,确认该候选项不会被无限重复计入"新发现",循环能够正常收敛(不是因为侥幸没再被搜到,而是因为去重键命中了"已见过"记录)。 +3. 验证 FR-5 的硬上限:人为构造一个"永远能生成看似新其实重复"的极端场景,确认 Skill 会在触达硬上限后强制停止并如实报告"可能未收敛",而不是无限执行下去耗尽预算。 +4. `.claude/agents/validator.md` 部署后(`deploy_agent_definitions`),能看到新 Skill 出现在 `skills:` frontmatter 列表里。 + +## 6. 约束与假设 + +- 复用 Validator 现有的技能目录约定(`skills//SKILL.md`,参考 `skills/qa-task-validation/SKILL.md` 的文件结构),但**不复用**该文件里遗留的飞书 Base 相关内容(`tblnlIVgHNaQXLAL` 等)——那是尚未清理的 Hermes profile 遗留细节,新 Skill 要按 graphlemon 当前的实际架构(无飞书 Base 依赖)编写,不能照抄旧格式。 +- 假设这个 Skill 在**单次 Work Node 执行内部**完成收敛循环(一次 `claude -p` 调用内的多轮工具使用),不依赖 Work Graph 层面的任何调度改动,因此和「G. Work Graph 节点并行执行」没有直接依赖关系,可以独立排期。 + +## 7. 风险 + +- **收敛判定本身依赖模型自我报告"这轮有没有新发现"**,如果模型判断不准确(漏报"这是新发现"或误报"这不是新发现"),收敛条件可能提前/延迟触发。FR-3 的"每轮发现经过验证"部分程度上缓解这个风险,但无法完全消除——这是提示词工程层面的固有局限,不是可以被架构设计完全解决的问题。 +- **K 值和硬上限的默认值需要实测校准**,不同任务规模下合适的 K 值可能不同,第一版先给一个保守默认值,观察真实运行数据后再调整。 + +## 8. 开放问题(留给详细设计阶段回答) + +- [ ] Skill 的具体命名和目录结构。 +- [ ] FR-3"复用已有验证框架"的具体接线方式——是在 Skill 提示词里直接要求"每个候选项过一遍 PASS/FAIL/INCONCLUSIVE 判断",还是调用 `root_cause_trace` 现成的多视角判断机制? +- [ ] K 值、硬上限的默认数字。 +- [ ] `agents/validator/SOUL.md` 里新增指引段落的具体触发条件描述(关键词匹配?任务类型标注?)。 diff --git a/docs/done/verifier-fan-out-jury-design.md b/docs/done/verifier-fan-out-jury-design.md new file mode 100644 index 0000000..0be4e4e --- /dev/null +++ b/docs/done/verifier-fan-out-jury-design.md @@ -0,0 +1,70 @@ +# 三视角评审团——Work Graph Fan-out 实现——详细设计(SDD) + +**日期**: 2026-08-06 +**性质**: 详细设计,对应需求文档 `docs/done/verifier-fan-out-jury-requirements.md`(条目 F,已确认)。回答该需求文档第 8 节的开放问题。 +**依赖代码**:`src/root_cause_trace.py`(`judge_root_cause`/`_PERSPECTIVE_FRAMINGS`)、`src/org_level_loop.py`(`run_fix_loop`,唯一真实生产调用方)、`src/work_graph_orchestrate.py`(G 已实现的 `run_graph()`/`topological_layers()`)。 +**状态**: 设计草案,待评审后实施。 + +## 1. 汇总节点不是 Work Node——这是"zero_token 边"原则的直接应用 + +需求文档背景里"1 个汇总节点"的表述容易被误读成"汇总也要是一次 `claude -p` 调用"。实际不需要:`>=2/3` 投票本身是纯粹的计数聚合,不需要 Agent 推理——这正是 E 设计文档里强调的 `edge_type: zero_token` 场景本身。汇总逻辑保持为一个普通 Python 函数,在 `run_graph()` 返回之后,在调用方所在的进程里直接对 `outputs` 做计数,不产生额外的 `claude -p` 调用、不产生额外 token 成本(呼应需求文档第 7 节风险"token 成本上升"的顾虑,此处直接不引入这块新增成本)。 + +验收标准 1 要求"能在 execution_event/日志里看到三次独立的节点执行记录"——这只要求**三个视角节点**是真 Work Node(`run_graph()`/`run_work_node()` 本来就会为每个节点写 `node_start`/`node_completed` 日志),不要求汇总也是。 + +## 2. 判定规则复用,不重写(FR-3) + +`root_cause_trace.judge_root_cause` 现有的 `>=2/3` 计数逻辑被抽成一个共享私有函数 `_tally_perspectives(judgments)`,`judge_root_cause`(保留,闭包版本)和新的 `judge_root_cause_via_work_graph`(Work Graph 版本)都调用它——两套判定规则物理上是同一份代码,不是两份逻辑各自实现、容易失配。`judgments` 的 dict 形状(`{"category", "detail"}`)两个版本完全一致,`_tally_perspectives` 不需要知道调用方是闭包还是 Work Node 输出。 + +## 3. `judge_root_cause_via_work_graph`:新的 Work Graph 版本 + +```python +def judge_root_cause_via_work_graph( + conn, task_id, target_folder_path, bug_description, context, + org_node="validator", skill_id="qa-task-validation", timeout=300, +): +``` + +内部结构: + +1. `_PERSPECTIVE_FRAMINGS`(已存在,三个视角的 framing 文本)驱动构造每个视角的完整提示词(复用现有 framing 文本,不改视角内容本身)。 +2. 构造一个菱形(严格说是"N 节点单层,无边")`graph_manifest`:`nodes = [{"node_id": "perspective-" + name, "depends_on": []} for name in perspectives]`,`edges = []`——三个节点互不依赖,`topological_layers()` 天然把它们分进同一层,G 自动并发调度,不需要额外的调度代码。 +3. 像 `e2e_pipeline.py::implement_code`/`validate_implementation_with_own_graph` 一样,手动 `INSERT INTO work_graph_run (..., status='running')`(不经过 `plan_work_graph()`——这个图的形状是固定的、代码已知的,不需要一次额外的 LLM 规划调用,同样是"机制用代码"原则的应用)。 +4. `run_graph()` 执行:每个视角节点用**真实 schema 约束的结构化输出**(`{"type": "object", "required": ["category", "detail"]}`),不再像现有 `real_validator_perspective` 那样手工解析"CATEGORY:/DETAIL:"文本行——`work_graph_orchestrate._make_run_fn` 走的是单节点 dynamic-workflow 脚本机制(`agent(prompt, {{ agentType, schema }})`),这条路径已经被 2026-08-01 的真实调用验证过 `structured_output` 真的能拿到(`work_graph_orchestrate.py` 模块 docstring 有记录),跟 `work_graph_plan.py` 那条"`--json-schema` 对 `--agent` 无效"的已知限制是两回事,不受它影响。 +5. `_tally_perspectives([outputs["perspective-" + name] for name in perspectives])`,返回值形状跟现有 `judge_root_cause` 完全一致(`{"root_cause_category", "root_cause_detail", "judgments"}`),调用方不需要区分。 + +每个视角节点获得独立、干净的上下文(验收标准 3):`_ensure_node_row`/`run_work_node` 本身就是每个 node_exec_id 一次全新的 workflow 脚本渲染 + 独立子进程调用,`input_payload` 只含 `{"node_id": ...}`,视角提示词通过 `build_prompt` 的闭包参数传入,节点之间不共享对话历史——这是 `run_graph()` 现有机制的天然属性,不需要额外设计。 + +## 4. `org_level_loop.run_fix_loop` 迁移(FR-5,唯一真实生产场景) + +现状: + +```python +perspective_fns = [root_cause_trace.real_validator_perspective(...) for name in (...)] +judgment = root_cause_trace.judge_root_cause(perspective_fns) +``` + +改为: + +```python +judgment = root_cause_trace.judge_root_cause_via_work_graph( + conn, task_id, target_folder_path, bug_description, context, +) +``` + +`real_validator_perspective`/闭包版 `judge_root_cause` **不删除**——需求文档第 8 节开放问题已问过"要不要保留作为轻量备选",答案是保留:`src/test_root_cause_trace.py` 现有单测(`JudgeRootCauseTest`/`RealValidatorPerspectiveParsingTest`)覆盖的是判定规则本身的正确性,这些测试不需要真实 `claude` 子进程调用就能跑(用纯 Python 闭包 mock),继续作为规则正确性的快速回归保障(`_tally_perspectives` 被两边共用,这些测试事实上也在验证 Work Graph 版本的判定逻辑);闭包版本本身作为"不需要 worktree/Work Graph 开销的场景"的备选实现继续存在,不强制废弃。 + +## 5. 测试改造范围(真实影响面,如实记录) + +`src/test_org_level_loop.py` 现有 3 个测试全部通过 mock `org_node_trigger.trigger_org_node`(拦截"CATEGORY:"提示词)来伪造三视角判断——这个 mock 边界在迁移后失效(新实现走 `work_graph_execute.execute_work_graph`,不再调用 `trigger_org_node` 做视角判断)。必须同步把这三个测试的 mock 边界改成 `work_graph_execute.write_workflow_script`/`execute_work_graph`(跟 `test_work_graph_orchestrate.py` 现有模式一致),`org_node_trigger.trigger_org_node` 的 mock 保留但只用于 fixer/verify 两次调用(迁移范围明确不包括这两处)。这是本次改动风险最高的一步——`run_fix_loop` 是真实生产闭环的一部分,测试改造必须逐条比对新旧断言等价,不能只是"能跑通"就算数。 + +## 6. 不实现的部分(诚实记录,对应需求文档非目标 + FR-4 的"应该"优先级) + +- **FR-4(`verify_fn` 通用对接)不在本轮实现**:把评审团机制包装成一个可以传给 `run_graph(verify_fn=...)` 的工厂函数,用于升级 `_check_prd_consistency` 这类现有单一验证器场景——需求文档标注"应该"而非"必须",且"哪些验证点该升级成评审团"本身是待定的判定标准(需求文档第 8 节开放问题未回答)。本轮只交付 FR-5(根因溯源场景本身)这一个"必须"级别的真实落地点,`judge_root_cause_via_work_graph` 的函数签名设计上保持通用(视角 framing/数量可参数化),为未来接 FR-4 留出空间,但不在本轮消费它。 +- **视角节点并发时间收益的实测**(验收标准 4):G 已实现,`topological_layers()` 会自动把三个视角节点分进同一层走 `ThreadPoolExecutor`,机制上必然并发;但"在真实 `claude -p` 调用下测出时间重叠"需要 `RUN_REAL_AGENT_E2E=1` 真实调用,不在 mock 测试范围内验证,留给后续真实运行观察确认(跟 G 自己的 `RunGraphConcurrencyTest` 用 mock+`time.sleep` 验证并发的方式已经覆盖了"机制确实并发"这件事,不需要为 F 重新证明一遍 G 的能力)。 + +## 7. 回答需求文档第 8 节开放问题 + +- `judge_root_cause`(闭包版)保留作为轻量备选,不废弃(第 4 节)。 +- FR-4 接口设计:本轮不实现,见第 6 节。 +- 除根因溯源外还有哪些验证点该升级:本轮不判定,留待 FR-4 一起决定。 +- 视角数量 N 是否可配置:`judge_root_cause_via_work_graph`/`_tally_perspectives` 设计上已经是对任意数量视角通用的(不硬编码 3),根因溯源场景固定传 3 个,未来场景可以传不同数量而不需要改动这两个函数本身。 diff --git a/docs/done/verifier-fan-out-jury-requirements.md b/docs/done/verifier-fan-out-jury-requirements.md new file mode 100644 index 0000000..39eba7a --- /dev/null +++ b/docs/done/verifier-fan-out-jury-requirements.md @@ -0,0 +1,69 @@ +# 三视角评审团——Work Graph Fan-out 实现——需求文档 + +**日期**: 2026-08-05 +**性质**: 需求文档(BRD/PRD 层面),不含详细技术设计。回应 `docs/todo/graph-engineering-open-items.md` 条目 F 的确认结果——"三视角评审团可以做,做成 Work Graph 的 fan-out 图来实现"。 +**依赖**:本文档描述的 fan-out 验证节点若要真正获得"多视角同时判断"的时间收益,强依赖 `docs/work-graph-parallel-execution-requirements.md`(条目 G)的并行执行能力——没有 G,把 `judge_root_cause` 现有的顺序调用改成 Work Graph fan-out 反而更慢(多了 Work Node 调度开销,却没有并行收益)。建议 G 先进入实现或至少详细设计定型后,再排期本文档。 +**状态**: 草案,待评审。 + +## 1. 背景 + +`src/root_cause_trace.py::judge_root_cause()` 已经实现了 14 步版"多视角差异化校验"模式的核心逻辑: + +```python +def judge_root_cause(perspective_fns): + """要求 >= 2/3 视角一致才定案 ... 否则 raise InconclusiveEscalateError""" + judgments = [fn() for fn in perspective_fns] + ... +``` + +三个视角(正确性/需求完整性/Skill 系统性缺陷)目前只用在 Validator 的 `test_failed` 根因溯源场景,且是**纯 Python 函数式实现**——`perspective_fns` 是一组闭包,`judge_root_cause` 一个一个顺序调用它们(`judgments = [fn() for fn in perspective_fns]` 是一个普通的列表推导,不是并发调用),每个 `fn()` 内部大概率是一次独立的 `claude -p` 调用(`real_validator_perspective` 函数名暗示了这一点)。 + +这次确认的方向:把这套机制从"Python 函数顺序调用三个闭包"升级为"Work Graph 里一张真正的菱形拓扑"——fan-out 出 N 个独立的验证 Work Node(每个节点携带自己的视角提示词,是一次独立的 Work Node 执行,而不是一个 Python 闭包),fan-in 节点做投票/汇总。这跟综述版对照笔记(`docs/reference/graph-engineering-vs-loop-engineering-overview.diff-with-graphlemon.md` §2)指出的"这正好对应文章第 08 章'评审团'式多视角交叉审"是同一个模式,只是要把它从函数调用升格为图结构。 + +## 2. 目标 + +1. 把三视角(或未来可能扩展到更多视角)评审团实现成一张真正的 Work Graph fan-out/fan-in 菱形:一个 split 节点(或直接由调用方给出待评审对象)→ N 个并行的独立验证 Work Node(各自视角、各自独立的 `claude -p` 调用、各自独立的持久化状态) → 一个 fan-in 节点汇总投票结果。 +2. 这套 fan-out 评审团机制要能被**不同的验证场景复用**,不只是 Validator 的根因溯源——`run_graph()` 现有的 `verify_fn`(单一验证器 + 失败重试一次)应该能够选择性地升级为这套多视角 fan-out 验证,用于高风险节点(比如 SDD 审核、代码变更审核)。 +3. 每个视角的验证节点获得**独立、干净的上下文**(呼应 5 步版对照笔记已确认的"Verifier 需要干净上下文"这条原则)——不是同一个对话里问三次不同角度的问题,而是三次完全独立的 Work Node 执行。 + +## 3. 非目标 + +1. **不要求所有验证点都升级成 fan-out 评审团**。这是一种"重打法",只用于高风险节点,大多数验证场景继续用现有的单一 `verify_fn`(一个验证器 + 失败重试一次)就够了——具体"高风险"的判定标准留待设计阶段确定,但原则上默认走轻量路径,评审团是可选升级,不是新的默认行为。 +2. **不要求第一版就废弃 `root_cause_trace.judge_root_cause` 的现有 Python 函数式实现**。是否完全替换、还是保留作为"不需要 worktree/轻量场景"的备选实现,留待设计阶段确定(见第 8 节开放问题)。 +3. **不在本文档定案 fan-out 验证节点的并发触发机制细节**。具体怎么并行触发这几个验证节点,是条目 G 的职责范围,本文档只提出"这几个节点需要能并行执行"这个需求,不重复 G 已经在做的设计工作。 + +## 4. 功能需求 + +| # | 需求描述 | 优先级 | +|---|---|---| +| FR-1 | 设计一种可复用的 `graph_manifest` 模式(菱形拓扑:1 个输入节点或直接的调用方输入 → N 个独立视角验证节点 → 1 个汇总节点),可以被任何需要"多视角评审团"式验证的调用方复用,不是针对根因溯源场景写死的一次性结构。 | 必须 | +| FR-2 | N 个视角验证节点的具体视角内容(提示词)需要能按调用场景参数化——根因溯源场景用"正确性/需求完整性/Skill 缺陷"三视角,未来其他场景(比如代码审查)可能需要"正确性/安全/性能"这类不同的视角组合,机制本身要跟具体视角内容解耦。 | 必须 | +| FR-3 | 汇总节点需要保留现有 `judge_root_cause` 已经验证过的判定规则——`>= 2/3` 视角一致才定案,不足则升级(`InconclusiveEscalateError` 的等价行为),这条规则本身经过实战验证,不需要重新设计,只需要把它从"Python 函数内的逻辑"迁移成"fan-in 节点的判定逻辑"。 | 必须 | +| FR-4 | `run_graph()` 的 `verify_fn` 接口需要能够对接这种 fan-out 评审团实现——具体是扩展 `verify_fn` 的调用约定,还是提供一个新的 `verify_fn` 工厂函数(生成一个内部会触发 fan-out 评审团的 `verify_fn`),留待设计阶段确定,但调用方(比如 `e2e_pipeline.py::plan_documents` 的 `_prd_consistency_verify_fn`)不应该需要感知内部是单一验证器还是评审团。 | 应该 | +| FR-5 | Validator 现有的 `test_failed` 根因溯源场景要能迁移到这套新机制上,作为第一个真实落地场景,验证机制本身可用,而不是只停留在通用接口设计层面。 | 必须 | + +## 5. 验收标准 + +1. 给定一个需要根因溯源的模拟场景,用新的 fan-out 评审团机制跑一遍,验证三个视角节点各自作为独立的 Work Node 执行(能在 `execution_event`/日志里看到三次独立的节点执行记录,而不是一次函数调用内部的三次闭包调用)。 +2. 验证判定规则等价性:构造"2/3 一致""不足 2/3"两种场景,确认新机制的判定结果跟现有 `judge_root_cause` 单测覆盖的场景给出相同的结论。 +3. 验证视角节点确实使用独立、干净的上下文——每个视角节点的 `input_payload` 只包含它需要判断的内容,不包含其他视角节点的推理过程。 +4. 在 G(并行执行)就绪后补充验证:三个视角节点的执行时间有重叠(不是排队执行)。 + +## 6. 约束与假设 + +- 复用现有的 Work Node 执行机制(`work_node_run.run_work_node`/`work_graph_execute.execute_work_graph`),每个视角节点是一次独立的单节点 workflow 脚本调用,跟现有其他 Work Node 没有本质区别。 +- 复用现有的 `graph_manifest`/`run_graph()` 执行入口,不新建专门的"评审团执行器"——评审团只是 `graph_manifest` 的一种特定拓扑形状(菱形),不需要新的执行引擎能力(依赖 G 提供的并行触发能力,但执行入口本身还是 `run_graph()`)。 +- `root_cause_trace.judge_root_cause` 现有的单测(如果存在)应该被保留或迁移,作为判定规则本身正确性的回归保障,不因为改成图结构就失去测试覆盖。 + +## 7. 风险 + +- **强依赖 G,排期上不能先于 G**:如果 G 推迟,这条也要跟着推迟,或者先落地"fan-out 但暂时串行执行"的过渡版本(功能正确但没有时间收益),需要跟用户确认是否接受这种过渡状态,还是宁可等 G 一起上。 +- **每个视角节点变成独立 Work Node 后,token 成本会上升**:现有 Python 函数式实现里,"三次独立调用"这件事本身没有变化,但包装成 Work Node 后每个节点会多一层 schema 校验往返、多一次单节点 workflow 脚本渲染的开销——这个开销预计很小,但需要在设计阶段实测确认,不是纯粹的免费升级。 +- **"高风险"判定标准如果定得太宽,会导致大量场景都触发评审团**,造成不必要的成本增长——需要在设计阶段给出明确、克制的判定标准,而不是默认"能用评审团就用评审团"。 + +## 8. 开放问题(留给详细设计阶段回答) + +- [ ] `root_cause_trace.judge_root_cause` 的 Python 函数式实现要不要保留作为轻量级备选(比如单元测试场景、不需要真实 `claude` 子进程调用的场景),还是完全被 Work Graph 版本取代? +- [ ] FR-4 的具体接口设计——`verify_fn` 扩展方式。 +- [ ] 除了 Validator 根因溯源,哪些验证点要升级成评审团(SDD 审核?代码变更审核?)——"高风险"判定标准怎么定,是节点类型固定标注还是运行时动态判断? +- [ ] 视角数量 N 是否需要可配置(不同场景可能需要 2 个视角或 5 个视角,不一定总是 3 个)? diff --git a/docs/done/work-graph-parallel-execution-design.md b/docs/done/work-graph-parallel-execution-design.md new file mode 100644 index 0000000..ed6fe2e --- /dev/null +++ b/docs/done/work-graph-parallel-execution-design.md @@ -0,0 +1,277 @@ +# Work Graph 节点并行执行——详细设计(SDD) + +**日期**: 2026-08-05 +**性质**: 详细设计,对应需求文档 `docs/work-graph-parallel-execution-requirements.md`(已确认,核心优先级)。本设计逐条回答该需求文档第 8 节的开放问题,并给出可直接实施的接口/算法/测试方案。 +**依赖代码**:`src/work_graph_orchestrate.py`(`run_graph`/`topological_order`/`_ensure_node_row`/`_make_run_fn`/`_verify_and_maybe_regenerate`)、`src/work_node_run.py`(`run_work_node`/`create_worktree`)、`src/backend_server.py`(`conn_lock` 先例)。 +**状态**: 设计草案,待评审后实施。 + +--- + +## 1. 设计概览 + +现状:`run_graph()`(`work_graph_orchestrate.py:196`)用 `topological_order()` 把 `graph_manifest` 拍平成一条扁平列表,纯 Python `for` 循环逐节点执行。 + +改造后:新增 `topological_layers()`,把 Kahn 算法的中间产物(每一轮 in-degree 归零的节点集合)保留为"层"而不是拍平;`run_graph()` 改成"层间顺序、层内并发"——同一层内的节点用 `ThreadPoolExecutor` 并发触发各自的 `run_work_node()`,层与层之间保持屏障(下一层要等上一层全部完成)。 + +**不改变的部分**(刻意保持稳定,降低改动面): +- 每个节点仍然是"渲染成一个单节点 workflow 脚本 + 独立 `claude -p` 子进程调用"(`_make_run_fn`/`_render_node_script` 不变)。 +- `run_work_node()` 的状态机、断点续跑逻辑、重试/配额等待逻辑完全不变。 +- `run_graph()` 的对外签名和返回值格式(`{node_id: output_payload}`)不变(需求文档 FR-9)。 +- `verify_fn` 的语义不变(需求文档 FR-8)。 + +## 2. `topological_layers()`:新增分层拓扑排序 + +```python +def topological_layers(graph_manifest): + """Kahn 算法的分层版本:返回 List[List[node]],每个内层 list 是同一轮 + in-degree 归零的节点集合(互相之间保证没有依赖,可以并发执行)。 + 层内按 node_id 排序(保持确定性,便于测试断言)。 + 依赖来源、报错行为跟现有 topological_order() 完全一致(合并 + depends_on 和 edges 两个来源;有环或引用未知 node_id 时 raise ValueError)。 + """ + nodes = {n["node_id"]: n for n in graph_manifest["nodes"]} + deps = {node_id: set(n.get("depends_on") or []) for node_id, n in nodes.items()} + for edge in graph_manifest.get("edges", []): + to_id, from_id = edge.get("to"), edge.get("from") + if to_id in deps and from_id in nodes: + deps[to_id].add(from_id) + + in_degree = {node_id: len(d) for node_id, d in deps.items()} + dependents = {node_id: [] for node_id in nodes} + for node_id, d in deps.items(): + for dep in d: + if dep in dependents: + dependents[dep].append(node_id) + + layers = [] + current = sorted(node_id for node_id, degree in in_degree.items() if degree == 0) + seen_count = 0 + while current: + layers.append([nodes[node_id] for node_id in current]) + seen_count += len(current) + next_layer = set() + for node_id in current: + for dependent in dependents[node_id]: + in_degree[dependent] -= 1 + if in_degree[dependent] == 0: + next_layer.add(dependent) + current = sorted(next_layer) + if seen_count != len(nodes): + raise ValueError( + "graph_manifest has a dependency cycle or a depends_on/edge references an unknown node_id" + ) + return layers +``` + +`topological_order()` **保留**(其他调用方,如 `e2e_pipeline.py:233` 直接用它拿扁平顺序写 artifact),但改成对 `topological_layers()` 的薄封装,保证行为/顺序完全不变: + +```python +def topological_order(graph_manifest): + return [node for layer in topological_layers(graph_manifest) for node in layer] +``` + +(需要跑一次现有 `test_work_graph_orchestrate.py` 里 `topological_order` 的既有单测,确认拍平后的顺序逐字节相同——这两版算法的排序规则一致,理论上不会有差异,但改动了实现路径,必须实测验证不是"理论上一致"就够。) + +## 3. SQLite 并发写入:`_LockedConn` 代理 + +**问题**:`run_work_node()`/`_ensure_node_row()`/`execution_log.append_log()` 等函数内部有大量分散的 `conn.execute(...)` + `conn.commit()` 调用点。层内并发后,这些调用可能从多个线程同时打到同一个 `sqlite3.Connection` 对象上。 + +**关键判断**:SQLite 本身同一时刻只允许一个写者(这是 SQLite 的架构限制,不是 Python 线程模型能绕开的),所以不需要设计"细粒度多把锁",**一把锁保护整个连接的读写足够**——真正的并发收益来自于"锁只在单条 SQL 语句执行期间持有,`claude -p` 子进程调用(决定墙钟时间的部分)完全不持锁",而不是来自"多个线程能同时写 SQLite"(这本来就不可能)。 + +**方案**:不去逐个包裹现有几十处 `conn.execute()` 调用点(改动面大、容易漏改),而是引入一个跟 `sqlite3.Connection` 接口兼容的轻量代理对象,在并发路径上把 `conn` 参数替换成这个代理——调用方代码(`work_node_run.py`/`work_graph_orchestrate.py`/`execution_log.py`)完全不用改一行: + +```python +class _LockedConn: + """sqlite3.Connection 的线程安全代理:execute/commit/executemany 各自 + 持锁执行,可以直接替代 conn 参数传给现有代码,调用方无需感知。复用 + backend_server.py::_Server 已经验证过的"共享连接 + threading.Lock"模式, + 只是把锁的粒度做到了每条语句级别(backend_server 现有的 conn_lock 只 + 保护 server_close() 一处,不是通用查询锁——本类是把同一个思路做成可 + 在任意并发场景复用的通用工具,两者不冲突,backend_server 未来可以直 + 接换成这个类)。 + """ + def __init__(self, conn, lock=None): + self._conn = conn + self._lock = lock or threading.Lock() + + def execute(self, *args, **kwargs): + with self._lock: + return self._conn.execute(*args, **kwargs) + + def executemany(self, *args, **kwargs): + with self._lock: + return self._conn.executemany(*args, **kwargs) + + def commit(self): + with self._lock: + return self._conn.commit() + + def __getattr__(self, name): + # cursor()/rollback()/close() 等未显式包装的方法透传给底层连接, + # 调用方目前不会从多线程并发调用这些(没有并发路径用到它们)。 + return getattr(self._conn, name) +``` + +`run_graph()` 在进入并发执行前,用一把 `threading.Lock()` 包出一个 `_LockedConn` 实例,传给每个并发任务内部实际调用的 `_ensure_node_row`/`run_work_node`/`_make_run_fn`(后者内部会调用 `execution_log.append_log`)——注意:**只有并发的那一层用 `_LockedConn`,层外(比如 `run_graph()` 自己开头调用 `ensure_placeholder_skill`)继续用原始 `conn`,因为那部分本来就不在并发路径上,没必要多一层锁的开销**。 + +## 4. Worktree 创建竞态 + +`work_node_run.py::create_worktree()`(`git worktree add -b `)在并发场景下可能对同一个 repo 的 `.git/worktrees/` 元数据产生竞态——这是**推测**,需求文档已标注"未经验证"。设计上采用保守做法:给 `create_worktree()` 包一把**进程级模块锁**(跟 SQLite 锁分开,因为这个锁保护的是文件系统/git 内部状态,不是数据库): + +```python +_worktree_creation_lock = threading.Lock() + +def create_worktree(repo_path, node_exec_id): + with _worktree_creation_lock: + ... # 原逻辑不变 +``` + +这个锁只包住 `git worktree add` 这一条命令本身(毫秒级),不包住节点其余的执行时间,所以即使锁確实是必要的,对整体并行收益的影响可以忽略——`claude -p` 调用(秒级到分钟级)才是墙钟时间的主体。**验证方式**:写一个真实的并发 `create_worktree` 调用测试(多线程同时对同一个临时 git 仓库调用),先不加锁跑一遍看是否真的出错,出错则保留锁、不出错也保留锁(保守优先,反正代价极小)。 + +## 5. 并发调度器:`run_graph()` 改造 + +```python +import concurrent.futures + +_DEFAULT_MAX_WORKERS = 4 # 保守默认值,见第 9 节 + +def run_graph( + conn, run_id, org_node, skill_id, target_folder_path, graph_manifest, + build_prompt, input_schema, output_schema, timeout=600, needs_worktree=False, + verify_fn=None, sleep=time.sleep, now=time.time, max_workers=_DEFAULT_MAX_WORKERS, +): + ensure_placeholder_skill(conn, skill_id, org_node) + effective_needs_worktree = needs_worktree or paths.is_self_target(target_folder_path) + layers = topological_layers(graph_manifest) + outputs = {} + db_lock = threading.Lock() + locked_conn = _LockedConn(conn, db_lock) + + for layer in layers: + if len(layer) == 1: + # 单节点层:不走线程池,直接同步执行——省掉线程池调度开销, + # 也让现有单节点场景(目前观察到的真实 graph_manifest 全都是 + # 单节点链)行为路径尽量贴近改造前,降低回归风险。 + node = layer[0] + outputs[node["node_id"]] = _run_one_node( + conn, run_id, org_node, skill_id, target_folder_path, node, outputs, + build_prompt, input_schema, output_schema, timeout, + effective_needs_worktree, verify_fn, sleep, now, + ) + continue + + worker_count = min(max_workers, len(layer)) + errors = [] + with concurrent.futures.ThreadPoolExecutor(max_workers=worker_count) as pool: + futures = { + pool.submit( + _run_one_node, locked_conn, run_id, org_node, skill_id, target_folder_path, + node, outputs, build_prompt, input_schema, output_schema, timeout, + effective_needs_worktree, verify_fn, sleep, now, + ): node + for node in layer + } + # 等整层全部完成(成功或失败)再继续——层内节点互不依赖, + # 一个节点的失败不该取消已经在跑的兄弟节点(需求文档 FR-5)。 + done, _ = concurrent.futures.wait(futures, return_when=concurrent.futures.ALL_COMPLETED) + for future in done: + node = futures[future] + try: + outputs[node["node_id"]] = future.result() + except Exception as e: + errors.append((node["node_id"], e)) + + if errors: + # 聚合报错:全部记录,抛出的异常携带完整清单,不只是"第一个", + # 方便排查同一层里到底几个节点失败、分别是什么原因。 + raise WorkGraphLayerError(errors) + + return outputs + + +def _run_one_node( + conn, run_id, org_node, skill_id, target_folder_path, node, outputs, + build_prompt, input_schema, output_schema, timeout, needs_worktree, verify_fn, sleep, now, +): + """原 run_graph() 循环体单节点逻辑原样保留,只是抽成一个独立函数, + 好让同步路径(单节点层)和并发路径(多节点层)共用同一份实现, + 不重复维护两份"跑一个节点+可选 verify_fn"的逻辑。""" + node_id = node["node_id"] + node_exec_id = _ensure_node_row( + conn, run_id, node_id, org_node, skill_id, + "work_graph_node_input.schema.json", "work_graph_node_output.schema.json", + {"node_id": node_id}, + ) + prompt = build_prompt(node, outputs) + run_fn = _make_run_fn(conn, target_folder_path, node_exec_id, org_node, prompt, output_schema, timeout) + output = work_node_run.run_work_node( + conn, node_exec_id, {"node_id": node_id}, input_schema, output_schema, run_fn, + needs_worktree=needs_worktree, repo_path=target_folder_path, sleep=sleep, now=now, + ) + if verify_fn is not None: + output = _verify_and_maybe_regenerate( + conn, run_id, org_node, skill_id, target_folder_path, node, node_id, prompt, + output, input_schema, output_schema, timeout, needs_worktree, verify_fn, sleep, now, + ) + return output + + +class WorkGraphLayerError(Exception): + """一层内一个或多个节点执行失败——携带 [(node_id,原始异常), ...] 完整 + 清单,调用方可以按需检查是不是全是同一种可恢复错误。""" + def __init__(self, errors): + self.errors = errors + summary = "; ".join("{}: {}".format(node_id, e) for node_id, e in errors) + super().__init__("{} node(s) failed in one layer: {}".format(len(errors), summary)) +``` + +**关键设计取舍——`build_prompt(node, outputs)` 里的 `outputs` 读取时机**:层内并发时,`outputs` 字典在整层的所有 `_run_one_node` 调用里都是**同一个共享字典**,但层内节点互相没有依赖,`build_prompt` 不会读到"同层兄弟节点"的输出(拓扑保证),只会读到"更早的层"已经写完的输出——这些更早层的写入在当前层开始前已经全部完成(层屏障保证),所以层内并发读 `outputs` 是安全的,不需要给 `outputs` 字典本身加锁(Python 字典的读操作本身也是线程安全的,只要没有并发写同一个 key,而层内不同节点写的是不同的 `node_id` key,不冲突)。**每个节点各自的 `outputs[node_id] = ...` 写入统一放到层执行完、`concurrent.futures.wait` 返回之后、在主线程里做**(上面伪代码里 `for future in done: outputs[node["node_id"]] = future.result()`),不在子线程里直接写共享字典,进一步避免任何并发写字典的疑虑,即使实际上没有真正的 key 冲突。 + +## 6. 错误/配额处理的真实限制(诚实记录,不回避) + +`work_node_run.run_work_node()` 内部对配额限制(`UsageLimitReachedError`→`_UsageLimitSignal`)的处理是**在函数内部原地 sleep-retry**,退避时间可以长达 30 分钟一次、session 级超时 8 小时、weekly 级超时 9 天(`work_node_run.py:42-43`)。这意味着:如果同一层里节点 A 触发了配额等待、节点 B 几秒内就正常完成,`concurrent.futures.wait(..., return_when=ALL_COMPLETED)` 会让整层卡在等 A——B 的结果虽然已经算出来了,但因为层屏障语义(下一层要等这一层全部完成),实际上整个 `run_graph()` 调用也会被拖住。 + +**这是本设计接受的限制,不是本轮要解决的问题**:Python 线程一旦启动就没有干净的"取消并回收"机制(`Future.cancel()` 只能取消还没开始跑的任务),要让 B 提前继续、A 在后台继续等配额而不阻塞整层,需要给 `run_work_node()` 引入可中断/可分离的配额等待机制——这是比本次改造大得多的另一个课题,不在本次范围内(需求文档"非目标"第 1 条已经声明"不做完整的数据流式调度器",这个限制是同一类取舍的自然延伸)。如果未来这个限制成为真实瓶颈,需要单独立项评估。 + +## 7. 断点续跑兼容性——不需要新逻辑 + +`run_work_node()` 已有的"已 `done` 节点短路返回持久化输出"逻辑对并发路径天然兼容,不需要改动:进程重启后重新调用 `run_graph()`,`topological_layers()` 重新计算分层,对每一层内的节点,`_run_one_node()` 里的 `run_work_node()` 调用会对"已完成"的节点立即返回持久化结果(几乎不占用线程池工作时间),对"未完成"的节点正常重新执行——同一层里有的节点秒回、有的节点真正重跑,线程池天然处理这种混合情况,不需要额外判断"这层要不要重跑"。 + +## 8. 可观测性 + +`_log_event`(`work_graph_orchestrate.py:74`)现有的 `node_start`/`node_completed`/`node_failed`/`quota_limit` 四类事件保持不变,每条事件本身已经带 `node_exec_id`,事件的时间戳(`execution_log.append_log` 底层写入时机)已经足够回答"这次运行到底并发了多少节点"——只要看同一个 `run_id` 下多个 `node_start` 事件的时间戳有没有重叠,不需要新增专门的"分组开始/分组结束"事件类型(呼应需求文档开放问题第 4 条:复用现有事件、靠时间戳重叠反推,不新增事件类型,保持改动面最小)。 + +## 9. 并发度默认值 + +`_DEFAULT_MAX_WORKERS = 4`。理由:现在观察到的真实 `graph_manifest` 规模是个位数节点,4 是一个不会让"一次任务同时拉起过多 `claude` 子进程"的保守值,同时足够覆盖当前能想到的 fan-out 场景(比如 Planner 未来 PRD 阶段 3-5 个场景并行)。作为 `run_graph()` 的一个具名参数(而不是硬编码常量或环境变量),调用方可以按场景覆盖,但不新增管理后台配置项(需求文档非目标已声明不做这类配置暴露,YAGNI——等真的有需要按项目/按 Org Agent 类型区分并发度时再加)。 + +## 10. 测试计划(对应需求文档验收标准 1-7) + +| 验收标准 | 测试方式 | +|---|---| +| 1. 同层节点确实并发触发 | mock `run_fn`,每次调用记录 `(node_id, 开始时间, 结束时间)` 到共享列表(用锁保护这个记录列表本身,测试代码自己的锁,不是被测代码的),断言同层两个节点的时间区间有重叠 | +| 2. 单测断言并发+层间顺序 | 同上,另外断言"下一层任意节点的开始时间 >= 上一层全部节点的结束时间"(层屏障) | +| 3. 断点续跑:已完成节点不重跑 | 第一次调用 mock 让某层其中一个节点抛异常模拟中断(`WorkGraphLayerError` 传播出来),检查 DB 里其余节点确实落库为 `done`;第二次调用同一个 `run_graph()`,断言 `run_fn` 没有被再次调用去跑那些已 `done` 的节点(用调用计数 mock 断言) | +| 4. SQLite 并发写入无异常 | 用真实 `:memory:`(或临时文件)SQLite 库跑一个多节点并发层,不 mock `conn`,跑多轮(并发问题往往概率复现)确认不抛 `database is locked` | +| 5. worktree 并发创建互不冲突 | 真实临时 git 仓库,多线程调用 `create_worktree`,断言 `git worktree list` 输出的路径数量和唯一性符合预期 | +| 6. 配额限制 + 正常完成共存 | mock 一个节点的 `run_fn` 触发 `_UsageLimitSignal` 且配置极短的超时(依赖注入的 `sleep`/`now`,不真的等待),另一个节点正常返回,断言正常节点的输出被保留在最终 `outputs` 里,且整体调用最终正确抛出/传播配额相关信号 | +| 7. 既有测试兼容性 | 跑 `src/test_work_graph_orchestrate.py`、`src/test_work_graph_execute.py`、`src/test_e2e_pipeline.py` 全部既有用例,逐个确认通过;对断言"严格串行"这一实现细节的用例(如果存在)需要单独识别并更新断言方式(改成断言"层间顺序"而不是"节点间顺序") | + +## 11. 实施步骤建议(拆成可独立验证的小步) + +1. `topological_layers()` 新增 + `topological_order()` 改写成基于它的薄封装,跑既有测试确认零回归(不改 `run_graph()`,先证明分层算法本身正确)。 +2. 引入 `_LockedConn`,先只在新代码路径里用,不碰现有串行路径。 +3. `create_worktree()` 加锁,写第 10 节测试用例 5 验证是否真的需要(哪怕结论是"其实不需要",锁本身开销极小,可以保留作为防御性设计)。 +4. `run_graph()` 改造成层级调度(第 5 节的完整实现),单节点层走同步路径、多节点层走线程池路径。 +5. 补全第 10 节全部测试用例。 +6. 找一个真实存在多节点并行结构的 `graph_manifest` 场景做一次端到端验证(如果当前还没有这样的真实场景,构造一个人工测试场景即可,不强求等 E 落地后才能验证 G 本身)。 + +## 12. 开放问题的最终决定 + +回答需求文档 `docs/work-graph-parallel-execution-requirements.md` §8 列的开放问题: + +- **`topological_layers()` 实现方式**:新增函数,`topological_order()` 改写成基于它的薄封装(第 2 节已给出实现)。 +- **`max_workers` 默认值**:4,作为 `run_graph()` 的具名参数,不做管理后台配置项(第 9 节)。 +- **FR-5 异常聚合规则**:层内全部完成后统一收集,抛出携带完整 `[(node_id, exception), ...]` 清单的 `WorkGraphLayerError`(第 5 节),调用方可以按需检查列表内容做进一步判断(比如是否全是可重试的配额问题)。 +- **`execution_event` 是否需要新事件类型**:不需要,复用现有事件+时间戳重叠反推(第 8 节)。 +- **worktree 加锁粒度**:只锁 `create_worktree()` 内部 `git worktree add` 这一条命令,不锁整个节点执行(第 4 节)。 diff --git a/docs/done/work-graph-parallel-execution-requirements.md b/docs/done/work-graph-parallel-execution-requirements.md new file mode 100644 index 0000000..6f70eba --- /dev/null +++ b/docs/done/work-graph-parallel-execution-requirements.md @@ -0,0 +1,76 @@ +# Work Graph 节点并行执行——需求文档 + +**日期**: 2026-08-04(2026-08-05 更新:已确认,标记为核心优先级,进入详细设计阶段) +**性质**: 需求文档(BRD/PRD 层面),不含详细技术设计。回应对 `docs/reference/Graph Engineering with Claude_ 14-Step roadmap from 0 to graph architect (Full Course).diff-with-graphlemon.md` 开放问题第 1 条的讨论——"现在的每节点独立子进程设计,断点续跑收益是否值得放弃 fan-out 并行?能否两者兼得?"。详细设计(SDD 层面:`topological_layers()` 的具体实现、线程池调度器接口、锁的粒度)留到需求确认后单独产出。 +**状态**: ✅ 已确认。验收口径固化为一句话(2026-08-05 用户原话):"只要 Work Graph 规划了 fan-out、fan-in 的并行节点,就可以在节点触发的时候并行执行"——这与本文档 FR-1/FR-2 一致,无需改动功能需求本身。详细设计已产出,见 `docs/work-graph-parallel-execution-design.md`。 + +## 1. 背景 + +`src/work_graph_orchestrate.py::run_graph()` 目前用 Kahn 算法把 `graph_manifest` 拍平成一条扁平的拓扑序,用纯 Python `for` 循环逐个节点执行——即使两个节点在 `graph_manifest` 里互相没有 `depends_on` 关系(本可以并行),现在也是严格排队执行,跑完一个才开始下一个。 + +这是一个记录在案的、有意为之的权衡(见该模块 docstring):早先方案(`docs/graphlemon_architecture.md` §10.1)设想直接复用 Claude Code 原生动态工作流的 `parallel()`/`pipeline()` 来跑整个 Work Graph,但那要求把整张图塞进一次不透明的 `claude` 子进程调用,Python 侧在执行过程中对每个节点的完成状态完全不可见——这跟 REQ-06"进程重启后从已完成节点继续、不重跑"的断点续跑硬需求根本冲突。所以最终选择了"每个节点一次独立的 `claude -p` 子进程调用 + Python 层持久化每个节点的状态",代价是放弃了图内并行。 + +随着 Work Graph 规模变大(例如 Planner 阶段 PRD/SDD 的多场景/多方案 fan-out 候选,`docs/graphlemon_architecture.md` §6.1 已经设想过、但目前从未真正在生成的 `graph_manifest` 里出现过),纯串行执行会让原本可以同时进行的工作被迫排队,拖长任务的墙钟时间。本需求要解决的问题:**在不牺牲现有断点续跑能力的前提下,让 `graph_manifest` 里互不依赖的节点能够真正并发执行**。 + +## 2. 目标 + +1. `graph_manifest` 中没有依赖关系(无论是直接 `depends_on` 还是通过其他节点间接依赖)的节点集合,能够在同一时间窗口内并发执行,而不是被迫排队。 +2. 现有的断点续跑语义(进程重启后,已经落库为 `done` 的节点不重跑,从未完成的节点继续)在并行改造后必须继续成立,不能因为改成并发就丢失这个能力。 +3. 现有的每节点独立 worktree 隔离(self-target 场景无条件强制)在并发场景下继续正确工作,不出现多个节点互相踩写文件。 +4. 现有的单节点验证-重试机制(`verify_fn`:验证失败则原地重跑一次,验证通过才允许下游节点开始)语义保持不变。 +5. 并发规模可控,不因为一次任务的 `graph_manifest` 节点数变多就无限制地同时拉起等量的 `claude` 子进程,导致失控的 token/账单开销或系统资源耗尽。 + +## 3. 非目标(本轮明确不做) + +1. **不做完整的"随时有依赖满足就调度"的数据流式调度器**。本轮只做"按拓扑层分批 + 层内并发、层间屏障"的粗粒度并行——层边界由 Kahn 算法的天然分层给出,不追求"某个节点的依赖一满足就立刻插队执行,不必等同层其他节点跑完"这种更细粒度的最优调度。理由:现在的 Work Graph 规模是个位数到十几个节点,层级屏障和真正的最优调度之间的墙钟差异预计很小,不值得为此引入更复杂的调度器和更难验证的正确性。 +2. **不改变"每个节点是一次独立 `claude -p` 子进程调用"这个既有设计**。本需求只改变"什么时候触发这次调用"(从严格串行改成同层并发触发),不改变每次调用本身的机制(仍然是单节点 workflow 脚本 + `--json-schema` 校验),也不重新尝试把整图塞进一次 Claude 原生 `parallel()`/`pipeline()` 编排脚本。 +3. **不引入按节点选模型的策略**。`_render_node_script` 已经预留的 `model` 参数、"哪个节点该用哪个模型"这层策略是独立的开放问题(见另一份对照笔记开放问题第 3 条),本需求不涉及。 +4. **不改变 Org-level Loop 的三轮上限、loop-until-dry 机制**。本需求只影响单次 `run_graph()` 调用内部的节点调度,不涉及跨 Org Graph 的重试/收敛逻辑,也不试图让 `graph_manifest` 支持图内有环(`topological_order` 目前对有环图直接报错,这个限制维持不变)。 +5. **不承诺降低 token 成本**。并行改造换来的是墙钟时间缩短,节点总数、每个节点的调用量不变,token 总消耗预计基本持平(详见「7. 风险与已知权衡」)。 + +## 4. 功能需求 + +| # | 需求描述 | 优先级 | +|---|---|---| +| FR-1 | `run_graph()` 在拿到 `graph_manifest` 后,应能识别出可以并发执行的节点分组(同一拓扑层内、彼此无依赖关系的节点集合),而不是像现在这样拍平成一条无结构信息的顺序列表。 | 必须 | +| FR-2 | 同一分组内的多个节点应能真正并发触发各自的 `claude -p` 子进程调用,而不是等前一个节点完全结束才开始下一个。 | 必须 | +| FR-3 | 并发执行期间,每个节点各自的持久化状态写入(节点行创建、状态流转、执行事件日志)必须保持数据完整性——不能因为多线程同时写同一个 SQLite 连接而出现"database is locked"报错、脏写、或状态丢失。 | 必须 | +| FR-4 | 并发执行期间,多个节点各自需要创建 git worktree 时(`needs_worktree=True` 场景)不能相互冲突,每个节点仍然拿到一个独立、干净的工作区。 | 必须 | +| FR-5 | 若同一分组内某个节点执行失败(含配额限制 `UsageLimitReachedError`、一般执行错误、`verify_fn` 验证失败且重试后仍不通过),已经在并发执行中的其他兄弟节点应当被允许自然跑完,不因为一个节点失败就中途取消其他正在消耗资源的调用;该分组内全部节点结束后再统一判定整个 `run_graph()` 调用的最终结果(成功/失败/需要升级)。 | 必须 | +| FR-6 | 并发度必须可配置且有合理的默认上限,防止一次任务的 `graph_manifest` 节点数暴涨时同时拉起等量的 `claude` 子进程。 | 必须 | +| FR-7 | 进程在某个分组执行到一半时中断重启后,恢复执行应能正确识别"这个分组里哪些节点已经 `done`、哪些还没跑完",只重新执行未完成的部分,不重跑已完成节点——这个语义必须和现在的单节点断点续跑完全等价,只是作用范围从"单个节点"扩展到了"一个分组内的多个节点"。 | 必须 | +| FR-8 | `verify_fn` 的语义保持不变:一个节点的输出必须先通过验证(或验证失败重试后通过),该节点的结果才能进入下游节点可读取的 `outputs`;依赖该节点的下游节点所在的分组,必须等到这一验证真正完成后才能开始执行,不能提前读取未经验证的中间结果。 | 必须 | +| FR-9 | 现有调用方(`e2e_pipeline.py` 等)对 `run_graph()` 的调用方式和返回值格式(`{node_id: output_payload}`)应保持兼容,不需要因为内部改成并行调度而修改调用方代码。 | 应该 | +| FR-10 | 并行执行的分组信息(哪些节点被并发调度、每组的起止时间)应当可观测——至少体现在现有的 `execution_event`/日志机制里,方便事后排查"某次运行到底并发了多少节点、并发是否生效"。 | 应该 | + +## 5. 验收标准 + +1. 给定一个 `graph_manifest`,其中包含至少一组"两个节点互不依赖、都依赖同一个上游节点"的结构(例如 A→{B, C}→D),跑一次 `run_graph()`,能观测到 B 和 C 的 `claude -p` 子进程调用在时间上有重叠(不是 B 完全结束后 C 才开始)。 +2. 用 mock 的 `run_fn`(不真实调用 `claude`,返回固定输出,可注入人为延迟)写单元测试:断言同一分组内的节点确实并发触发(例如用一个共享计数器/时间戳断言重叠),且分组之间保持严格先后顺序(下一层的节点不会在上一层未全部完成前开始)。 +3. 模拟"进程在某个分组执行到一半时中断"(例如分组内两个节点,mock 让其中一个成功落库为 `done`、另一个抛异常模拟中断),重新调用 `run_graph()`,断言已完成的节点不会被重新执行(`run_fn` 没有被再次调用),只有未完成的节点被重新调度。 +4. 并发场景下跑一轮包含多个节点写入 SQLite 的测试,不出现 `sqlite3.OperationalError: database is locked` 或类似的并发写入异常。 +5. 模拟 `needs_worktree=True` 场景下同一分组内多个节点并发创建 worktree,断言每个节点拿到的 worktree 路径互不相同、互不冲突,且 `git worktree list` 能看到全部预期数量的 worktree。 +6. 模拟同一分组内一个节点抛 `UsageLimitReachedError`、另一个节点正常完成,断言正常完成的节点的输出被正确保留(不因为兄弟节点失败而丢失),同时整体调用仍然正确地向上抛出/传播配额限制信号。 +7. 现有 `src/test_work_graph_orchestrate.py`、`src/test_work_graph_execute.py`、`src/test_e2e_pipeline.py` 中的既有测试用例在改造后无需修改断言即可继续通过(验证行为兼容性),除非该用例本身就是在断言"严格串行"这一将被改变的实现细节(这类用例需要明确识别出来,逐一确认是否要更新)。 + +## 6. 约束与假设 + +- 复用 `src/backend_server.py::_Server` 里已经验证过的模式:共享一个 `sqlite3.connect(..., check_same_thread=False)` 连接 + `threading.Lock()` 保护并发写入,不引入连接池或每线程独立连接这类更重的方案。 +- 复用现有的 `work_node_run.py` 断点续跑机制("已 `done` 的节点短路,直接返回持久化输出"),并行改造不重新设计这一层,只是让触发多个节点执行这件事本身从串行变成并发。 +- 复用现有的每节点 worktree 隔离机制(`work_node_run.py::create_worktree`),如果发现并发创建 worktree 本身有 git 层面的竞态,允许在"创建 worktree"这一步单独加锁串行化,不要求整个节点执行都串行——真正耗时的 `claude -p` 调用部分仍然并发。 +- 默认并发上限取一个保守值(具体数字留给设计阶段结合实测确定,不在需求阶段写死),避免一次运行同时拉起过多 `claude` 子进程。 + +## 7. 风险与已知权衡 + +- **不降低 token 成本,只缩短墙钟时间**:并行改造让节点调用"同时发生"而不是排队,但总的节点数量、每个节点的 token 消耗不变。如果验收标准或后续讨论里出现"并行后应该更省钱"的预期,需要提前澄清纠正。 +- **层级屏障不是最优调度**:如果某一层里有一个节点明显比同层其他节点慢很多,下一层要等这个最慢的节点跑完才能开始,即使下一层里有些节点其实只依赖同层里已经跑完的那些节点。本需求接受这个次优性(见「3. 非目标」第 1 条),如果未来图规模变大、这个次优性变成真实瓶颈,需要另立需求评估数据流式调度器。 +- **SQLite 并发写入的锁粒度需要在设计阶段仔细确定**:过粗的锁(整个 `run_graph()` 调用期间持锁)会退化成事实上的串行,失去并行的意义;过细的锁需要更仔细地审视哪些写入操作之间存在真正的竞态。这是详细设计阶段需要重点验证的部分,不是需求阶段能定论的。 +- **worktree 创建的 git 层面竞态目前只是推测,未经验证**:需要在设计/实现阶段用真实的 `git worktree add` 并发调用做实测,确认是否真的需要额外加锁,还是 git 本身已经能安全处理并发创建。 + +## 8. 开放问题(留给详细设计阶段回答) + +- [ ] `topological_layers()` 的具体实现:是在现有 `topological_order()` 旁边新增一个函数,还是重构 `topological_order()` 本身让它返回分层结构,由调用方按需拍平? +- [ ] 并发上限(`max_workers`)默认值定多少?是否需要按 Org Agent 类型(Planner 偏文档生成、Coder 偏代码修改)给不同的默认上限?是否需要暴露成可配置项(管理后台/环境变量)? +- [ ] FR-5 的"该分组内全部节点结束后再统一判定整体结果"具体的异常聚合/优先级规则是什么(比如同组里既有配额限制又有一般执行错误,该抛出哪一个、还是都记录?)? +- [ ] 是否需要在 `execution_event` 里新增一种事件类型来标记"分组开始/分组结束",还是复用现有的 `node_start`/`node_completed` 事件、靠时间戳重叠反推并发情况就够用? +- [ ] worktree 创建竞态如果实测确认存在,加锁的粒度是"整个 `create_worktree` 函数"还是可以更细(比如只锁 `git worktree add` 这一条命令的调用)? diff --git a/docs/done/work-graph-time-travel-debugging-design.md b/docs/done/work-graph-time-travel-debugging-design.md new file mode 100644 index 0000000..4b31a09 --- /dev/null +++ b/docs/done/work-graph-time-travel-debugging-design.md @@ -0,0 +1,61 @@ +# Work Graph 执行历史 Time-Travel 调试——详细设计(SDD) + +**日期**: 2026-08-06 +**性质**: 详细设计,对应需求文档 `docs/done/work-graph-time-travel-debugging-requirements.md`(条目 D,已确认)。回答该需求文档第 8 节的开放问题。 +**状态**: 设计草案,实施中,后端+控制台一起做(用户 2026-08-06 确认)。 + +## 1. `ux_work_graph_run_active` 比需求文档描述的风险更小 + +`src/schema.py:134` 那条唯一索引是**带 WHERE 条件的部分索引**:`WHERE status IN ('planning', 'running', 'merging', 'verifying')`——'done'/'failed'/'blocked' 状态的 run 完全不受它约束。这意味着"从一个已经跑完很久的 task 分叉调试"(最常见的真实场景)根本不会撞上这条约束。 + +**v1 范围裁剪(跟用户讨论后确认)**:只支持从**父 run 已经到达终态**(`done`/`failed`/`blocked`)的节点分叉。这样新 fork 出的 run 完全落在这条部分索引的约束范围之外,不需要碰这条索引本身,也不需要梳理"还有哪些代码路径隐式假设了一个 task_id+org_node 只有一个活跃 run"这个更大的排查面(需求文档风险第 1 条点名的工作)。从还在跑的父 run 分叉,留给以后有真实需求再做。 + +## 2. 关键设计决定:替换输出,不重新提示(解决 FR-1 的歧义) + +需求文档 FR-1 原文:"人工可以指定新的输入(覆盖原 `input_payload` 的部分或全部字段),触发从这个节点开始的重新执行。" 但 `input_payload` 在现有代码里几乎总是一个薄壳(`{"node_id": ...}`)——真正的提示词内容来自调用方各自不同的机制(Planner 靠 `artifact_type` 查 `_DOC_PROMPTS`,E 的通用路径靠节点自带的 `task_prompt` 字段,F 的评审团靠运行时闭包)。要让分叉机制对这些不同调用方都通用,"覆盖 input_payload 重新触发 LLM 调用"这条路走不通,会绑死在某一种调用方约定上。 + +**决定**:分叉点这个节点本身**不重新执行**——它的 `output_payload` 被人工提供的新内容直接替换(验收标准 1 的例子"换一个不同的 PRD 内容"本来就是这个意思:人工已经有了想要的新内容,不是要求 Agent 重新生成一个不一样的版本)。只有**严格下游**的节点才会真的重新跑一次 `claude -p`。这个决定让分叉机制完全不需要知道原图是哪个调用方产出的、用的是哪种 `build_prompt` 约定——只需要持久化的 `graph_manifest` 和各节点的 `output_payload`,这两样任何一条 Work Graph run 都有。 + +## 3. 实现机制:100% 复用 REQ-06 断点续跑,零改动执行引擎 + +`work_node_run.run_work_node()` 已有的逻辑:一个 `work_node_execution` 行如果 `status='done'`,直接返回持久化的 `output_payload`,不调用 `run_fn`。`work_graph_orchestrate.run_graph()`/`topological_layers()` 对此完全无感——它只是正常跑一遍拓扑分层调度,每层每个节点各自决定"是短路返回还是真的执行"是 `run_work_node()` 内部的事。 + +这意味着 `fork_work_graph_run()` 不需要裁剪 `graph_manifest`、不需要给 `run_graph()` 传一个"预填充的 outputs"参数(`run_graph()` 也没有这个参数,不需要新加)——**只需要在调用 `run_graph()` 之前,往新 `run_id` 下预先插入一批 `work_node_execution` 行**: + +- 分叉点本身:`status='done'`,`output_payload` = 人工提供的新内容。 +- 分叉点的**非下游**节点(严格下游子图之外的所有节点,包括分叉点的上游和跟分叉点无关的兄弟分支):`status='done'`,`output_payload` = 原始 run 里这个节点已经产出的内容,原样复制。 +- 分叉点的**严格下游**节点:什么都不插入,留给 `run_graph()` 正常调度、真实执行。 + +`run_graph()` 拿到的还是**完整的原始 `graph_manifest`**(不裁剪),对每个节点走到 `_ensure_node_row()` 时,发现 `(new_run_id, node_id)` 已经有行了(我们预先插的),直接复用那个 `node_exec_id`;`run_work_node()` 一看 `status='done'`,立刻短路返回——这条路径是 `RunGraphHappyPathTest.test_resumes_without_rerunning_done_nodes` 已经在测的行为,不是新写的逻辑。严格下游的节点走的是全新的 `node_exec_id`,正常执行,`build_prompt(node, outputs)` 拿到的 `outputs` 里已经有了(无论是复用来的还是刚替换的)上游内容——这跟"进程重启后 resume"时 `outputs` 字典的填充方式完全一样,天然支持 G 的并发调度(如果严格下游子图里有互不依赖的节点,一样会被分进同一层并发执行,不需要额外代码)。 + +`_downstream_node_ids(graph_manifest, source_node_id)`:从 `depends_on`/`edges` 正向遍历依赖图(复用 `topological_layers()` 已经在用的同一套 dependents 构建逻辑,抽出来共享,不重复实现一遍)。 + +## 4. Schema 改动 + +`work_graph_run` 新增两个可空字段:`parent_run_id`(指向被分叉出去的原始 run)、`fork_of_node_exec_id`(指向具体是从哪个节点分叉的)。纯新增字段,**不改** `ux_work_graph_run_active`(见第 1 节,v1 范围内不需要)。新数据库直接建在 `SCHEMA_SQL` 里;已有数据库用 `migrate_add_work_graph_fork_columns()`(跟 `migrate_expand_artifact_types` 同一个先例,但更简单——纯 `ALTER TABLE ADD COLUMN`,不需要建新表复制数据那套,因为不是改 CHECK 约束)。 + +## 5. 查询层 + 控制台(FR-4) + +`work_graph_run_query.list_work_graph_runs`/`get_work_graph_run`、`task_query.get_task` 的 `work_graph_runs` 列表,都加上 `parent_run_id`/`fork_of_node_exec_id` 两个字段——前端靠这两个字段判断"这条是原始记录还是分叉分支",以及"分叉自哪一条"。 + +控制台改动(`frontend/app.js`): +- 运行历史列表/任务详情页的每条 `work_graph_run` 卡片,`parent_run_id` 非空时显示一个"🔀 从 `` 分叉自 ``"的标记,点击可跳转到父 run。 +- 节点详情面板(已有的 `renderGraphVisualizerPanel`/节点 hover 面板)里,`status='done'` 的节点新增一个"从这里分叉"按钮,点开一个表单填新的 output 内容(JSON),提交调用新端点。 +- 不做 FR-5 提到的"设为主线"之类的合并操作——需求文档非目标 #3 已经说了第一版不做 diff/合并 UI,只读展示 + 触发分叉。 + +## 6. HTTP 端点 + +`POST /api/work-node-executions/:node_exec_id/fork`,body `{"new_output_payload": {...}}`,返回 `{"run_id", "outputs"}`。放在 `backend_server.py` 现有的路由风格里(正则匹配路径 + 方法分派,参考已有的 `/api/tasks/:id/...` 系列端点)。 + +## 7. 不做的部分(如实记录) + +- **从还在跑的父 run 分叉**(第 1 节裁剪掉的部分)。 +- **FR-5 的"设为主线"交互**——只读展示分叉分支,人工另外决定怎么处理分叉产物(需求文档非目标 #3 已经说了 diff/比较视图留给后续)。 +- **分叉分支的生命周期/清理策略**(需求文档 §8 开放问题最后一条)——先不清理,跟原始执行日志的保留策略是两个独立的问题,等真的堆积成问题再处理。 + +## 8. 回答需求文档第 8 节开放问题 + +- `parent_run_id`/`fork_of_node_exec_id` 具体 schema:见第 4 节,纯新增可空字段,不动唯一索引(因为 v1 范围裁剪掉了会冲突的场景,见第 1 节)。 +- 触发入口:控制台 + 一个后端 API(`POST /api/work-node-executions/:node_exec_id/fork`),两者复用同一个 `fork_work_graph_run()`。 +- FR-5 交互:不做"设为主线"按钮,纯只读展示(见第 5、7 节)。 +- 生命周期/清理策略:本轮不做(见第 7 节)。 diff --git a/docs/done/work-graph-time-travel-debugging-requirements.md b/docs/done/work-graph-time-travel-debugging-requirements.md new file mode 100644 index 0000000..bf7a295 --- /dev/null +++ b/docs/done/work-graph-time-travel-debugging-requirements.md @@ -0,0 +1,73 @@ +# Work Graph 执行历史 Time-Travel 调试——需求文档 + +**日期**: 2026-08-05 +**性质**: 需求文档(BRD/PRD 层面),不含详细技术设计。回应 `docs/todo/graph-engineering-open-items.md` 条目 D 的确认结果——"time travel 可以立项"。 +**状态**: 草案,待评审。 + +**命名消歧(仿照 CLAUDE.md「两个 Supervisor」的先例)**:本文档说的"Time-Travel 调试"跟 `docs/done/supervisor-auto-rollback-design.md` 描述的 Deploy Supervisor 自动回滚是两件完全不同的事——不要混淆: + +| | Time-Travel 调试(本文档) | Deploy Supervisor 自动回滚 | +|---|---|---| +| 回滚的对象 | 某一次任务的 Work Graph 执行过程(某个 `work_node_execution` 的中间状态) | graphlemon 自身的代码版本(git commit) | +| 触发者 | 人工调试意图("我想看看如果这一步换个输入会怎样") | 自动健康检查失败信号 | +| 影响范围 | 单个 task 的执行历史,不影响其他任务 | 整个 graphlemon 进程的运行版本 | +| 相关文档 | 本文档 | `docs/done/supervisor-auto-rollback-design.md` | + +## 1. 背景 + +综述版对照笔记(`docs/reference/graph-engineering-vs-loop-engineering-overview.diff-with-graphlemon.md` §6)指出:LangGraph 的 checkpointer 机制带来四个能力——人在回路、记忆、时间旅行调试、容错。逐项对照 graphlemon 现状: + +| LangGraph 能力 | graphlemon 现状 | +|---|---| +| 容错(部分节点失败保留已成功节点输出) | ✅ 已具备(`work_node_execution` 按节点独立持久化状态) | +| 人在回路(暂停/恢复) | ✅ 已具备(`src/task_execution.py` 的 Start/Pause/Stop) | +| 记忆(多轮交互间保留上下文) | ✅ 已具备(`.claude/agent-memory/`) | +| 时间旅行调试(回到任意历史检查点重放/分叉) | ❌ 未实现,现状是"用 git 历史 + 手动重跑" | + +`work_node_execution` 表(`src/schema.py:138`)目前按节点持久化 `input_payload`/`output_payload`/`status`/`retry_count` 等字段,`work_graph_run` 表(`src/schema.py:118`)持久化 `graph_manifest` 和整体 `status`——这些数据本身已经具备"回放某一次执行发生了什么"的信息基础,缺的是"从某个历史节点分叉出一条新的执行分支"这个操作本身。 + +## 2. 目标 + +1. 允许人工从某次 Work Graph 执行的某个历史节点(`work_node_execution`)出发,用新的输入(不同的 prompt、不同的上游产物)重新执行该节点及其下游依赖节点,产出一条与原执行并存、可独立查看的新分支,而不覆盖或污染原始执行记录。 +2. 分叉出来的新分支要能在控制台里被查看、区分(哪条是原始记录、哪条是分叉出来的调试分支)。 +3. 这个能力主要服务于调试/复盘场景(例如:"如果 SDD 阶段换一版方案,后续的 Coder 实现会有什么不同"),不是生产环境的正常任务执行路径的一部分。 + +## 3. 非目标 + +1. **不是自动化机制**。分叉重放由人工在控制台里主动触发,不是系统自主决定"要不要回溯重跑"的行为——这跟 Org-level Loop 那种系统自主触发的回退是两回事。 +2. **不追求支持任意粒度的时间点**(比如"回到某个节点执行到一半的中间状态")。粒度限定在 `work_node_execution` 这个已有的持久化单元——从"某个节点完成之后(它的 `output_payload` 已经落库)"这个时间点分叉,不试图捕获节点内部一次 `claude -p` 调用执行过程中的中间状态。 +3. **不在第一版里做完整的可视化 diff/比较工具**(比如"原分支和新分支的产物并排对比高亮差异")。第一版先解决"分叉出一条新分支并能分别查看"这个核心能力,比较/diff 视图留给后续迭代。 + +## 4. 功能需求 + +| # | 需求描述 | 优先级 | +|---|---|---| +| FR-1 | 给定一个已完成(`status='done'`)的 `work_node_execution`,人工可以指定新的输入(覆盖原 `input_payload` 的部分或全部字段),触发从这个节点开始的重新执行。 | 必须 | +| FR-2 | 分叉执行产生的是一次**新的** `work_graph_run`(不是修改原 `work_graph_run` 的记录),需要能追溯"这次运行是从哪个历史节点、哪个原始 run 分叉出来的"。 | 必须 | +| FR-3 | 分叉出的新 run 只重新执行"从分叉点开始,到该节点的下游依赖链",分叉点**之前**的节点(分叉点本身的上游)直接复用原始 run 已经产出的 `output_payload`,不重新执行——这跟现有断点续跑"复用已完成节点输出"的逻辑是同一个原则的延伸。 | 必须 | +| FR-4 | 控制台(架构文档 §13 的人机交互控制台)需要能列出"某个 task 下所有的分叉分支",并支持切换查看每条分支各自的节点执行详情(复用现有 v_task_detail 一类页面的节点级展示能力)。 | 应该 | +| FR-5 | 分叉出的调试分支不应该影响原始任务的正常执行状态——即使分叉分支的执行结果比原分支"更好",也不会自动替换/合并回原始任务的 `task.status`,是否采纳分叉分支的结果需要人工另外决定(比如人工把分叉分支的产物手动复制回主线,或者认可分叉分支本身就是新的正确路径)。 | 必须 | + +## 5. 验收标准 + +1. 给定一个已经跑完的简单 Work Graph(比如 Planner 的 BRD→PRD→SDD→Benchmark 四节点链),从 PRD 节点分叉、换一个不同的 PRD 内容,触发重新执行,验证 SDD、Benchmark 两个下游节点用新的 PRD 内容重新跑了一遍,而 BRD 节点没有被重新执行(直接复用原值)。 +2. 验证原始 run 的记录在分叉操作后完全不受影响——分叉前后分别查询原始 `work_graph_run`/`work_node_execution`,字段值一致。 +3. 验证同一个历史节点可以被分叉多次,产生多条互不干扰的分支,且每条分支都能独立追溯到"从哪个节点、哪个原始 run 分叉而来"。 +4. 控制台里能看到某个 task 下全部分支的列表,并能分别打开查看。 + +## 6. 约束与假设 + +- `work_graph_run` 表当前有一条唯一约束:`ux_work_graph_run_active`——同一个 `(task_id, org_node)` 组合同一时间只能有一个非终态的 run(`src/schema.py:134`)。分叉产生的新 run 如果沿用同一个 `task_id`,会撞上这条约束(分叉分支和原分支可能同时处于非终态)。需要在设计阶段决定:分叉出的 run 是否需要一个独立的标识维度(比如新增一个 `parent_run_id`/`fork_of_node_exec_id` 字段,并调整唯一约束的范围,使其只约束"同一条分支线"内部,而不是整个 `task_id`),这是本需求对现有数据模型影响最大的一处,必须在设计阶段仔细过一遍现有依赖这条约束的代码路径(比如断点续跑逻辑是否假设了"同一 task_id+org_node 只有一个活跃 run")。 +- 分叉执行复用现有的每节点独立子进程执行机制(`work_node_run.run_work_node`/`work_graph_execute.execute_work_graph`),不需要新的执行引擎——分叉本质上是"用一份新的 `graph_manifest`(去掉已复用的上游节点)+ 新的初始 `outputs` 字典(预填充复用节点的输出)调用一次 `run_graph()`",复用度很高。 + +## 7. 风险 + +- **数据模型改动的影响面需要在设计阶段仔细评估**,`ux_work_graph_run_active` 这条唯一约束背后可能还有其他代码路径隐式依赖"一个 task_id+org_node 只有一个活跃 run"这个假设(比如查询任务当前状态的逻辑、WebSocket 推送逻辑),改动前需要逐一排查,不能只看 schema 本身。 +- **控制台可视化工作量可能比后端数据模型改动更大**——FR-4 涉及新的 UI 交互(选择分叉点、对比分支列表),需要跟现有 `docs/wireframes/` 里的 `v_task_detail` 类页面设计对齐,这部分工作量需要在设计阶段单独评估,不要低估。 + +## 8. 开放问题(留给详细设计阶段回答) + +- [ ] `parent_run_id`/`fork_of_node_exec_id` 这类新字段的具体 schema 设计,以及 `ux_work_graph_run_active` 约束要怎么调整。 +- [ ] 分叉操作的触发入口——只在控制台里做,还是也要开一个后端 API 供其他方式调用? +- [ ] FR-5"是否采纳分叉分支结果"的人工操作具体是什么交互(一个"设为主线"按钮?还是纯只读展示,人工另外手动处理)? +- [ ] 是否需要给分叉分支设置生命周期/清理策略(长期堆积的调试分支会不会需要定期清理,类似原始执行日志的保留天数机制)? diff --git a/docs/todo/dynamic-work-graph-design-skill-design.md b/docs/todo/dynamic-work-graph-design-skill-design.md deleted file mode 100644 index 17fe841..0000000 --- a/docs/todo/dynamic-work-graph-design-skill-design.md +++ /dev/null @@ -1,51 +0,0 @@ -# 跨 Org Agent 的动态 Work Graph 设计能力——详细设计(SDD) - -**日期**: 2026-08-06 -**性质**: 详细设计,对应需求文档 `docs/todo/dynamic-work-graph-design-skill-requirements.md`(条目 E,已确认)。回答该需求文档第 8 节的开放问题。 -**依赖**:条目 G(`docs/work-graph-parallel-execution-design.md`)已实现——`topological_layers()`/`run_graph()` 已经在生产代码里。 -**状态**: 设计草案,待评审后实施。 - -## 1. G 已经回答了 FR-2(并行分组字段格式) - -需求文档 FR-2 留空的"哪些节点属于同一个可并行分组"字段格式,G 的实际实现给出了确定答案:**不需要新字段**。`topological_layers()` 完全靠 `depends_on`/`edges` 结构本身推导独立性——没有相互依赖的节点自动落入同一层,`run_graph()` 自动并发执行同层节点。Planning Skill 的方法论职责因此简化为:"如实表达真正的依赖关系,不要为了图好看而人为加上并不存在的 depends_on"(14 步版对照笔记"边是数据契约"这条原则的直接应用),不需要额外发明一个"parallel_group"标记。 - -## 2. 新 Skill:`design-dynamic-work-graph` - -`skills/design-dynamic-work-graph/SKILL.md`,与 `planning-requirement-expansion`、`code-task-development` 等现有 Skill 平级存放。内容方法论(不含代码): - -1. 节点边界怎么划:一个节点 = 一次独立的、有明确输入输出契约的 Work Node 执行(一次 `claude -p` 调用)。任务小到一次调用就能完成时不需要拆图(呼应需求文档第 6 节约束)。 -2. `edge_type` 怎么选:`zero_token`(纯聚合/规约,不需要 Agent 推理,比如"把三份判断投票汇总")vs `needs_judgment`(下游需要真正读懂上游内容才能继续,比如"PRD 依赖 BRD 的实际内容")。 -3. 如何识别可并行的候选点:如实列出每对节点之间是否存在真实数据流动——没有数据流动就不要加 `depends_on`,G 会自动把它们并发调度。 -4. 每个节点必须自带 `task_prompt`(该节点执行时使用的完整提示词,自包含,不依赖调用方另外拼接)——这是 Coder/Validator 场景与 Planner 场景的关键区别:Planner 的四个节点类型固定(brd/prd/sdd/benchmark...),下游代码用一个查表 `_DOC_PROMPTS[artifact_type]` 就能拿到提示词;Coder/Validator 的子任务没有固定的小词表,必须由 Claude 自己在规划阶段把每个节点的提示词写出来。 - -## 3. `work_graph_plan.py` 改造(FR-3) - -**不改动 Planner 现有路径**:`_PROMPT_TEMPLATE`(brd/prd/sdd/benchmark 固定链)与 `_normalize_graph_manifest` 的 canonical-chain 强制逻辑完全保留,`org_node == "planner"` 时走原有分支,零回归风险。 - -新增: - -- `_GENERIC_PROMPT_TEMPLATE`:当 `org_node != "planner"` 时使用,要求 Claude 依据 `design-dynamic-work-graph` Skill 的方法论,把 `goal_text` 拆成一个 `graph_manifest`,每个节点除 `node_id`/`depends_on` 外必须包含 `task_prompt`(自包含提示词);输出格式(fenced ```json 块)跟现有约定一致,复用同一套 `_extract_graph_manifest` 解析逻辑。 -- `plan_work_graph()` 内部按 `org_node == "planner"` 二选一模板,函数签名/返回值不变(需求文档非目标 #2)。 -- `generic_node_prompt_builder(graph_manifest)`:新增的公开辅助函数,返回一个 `build_prompt(node, outputs)` 闭包,直接读取 `node["task_prompt"]`(`run_graph()` 的 `build_prompt` 约定不变,仍然是"给定 node + 已完成 outputs,返回这次调用的提示词")——之所以放在 `work_graph_plan.py` 而不是 `e2e_pipeline.py`,是因为它跟生成侧的字段约定(`task_prompt` 这个 key 名)强耦合,两者应该在同一个模块里维护,避免字段改名时两处不同步。 - -`_normalize_graph_manifest` 不需要改动——canonical-chain 强制分支本来就有 `all(t in by_artifact_type for t in _CANONICAL_DOC_CHAIN)` 前置判断,Coder/Validator 的动态图没有 brd/prd/sdd/benchmark 节点,天然不触发,其余的 `id`→`node_id`/`depends_on` 兜底逻辑对新路径同样适用,不需要区分。 - -## 4. Skill 接入 Coder/Validator - -`src/agent_profile.py::FRONTMATTER["coder"]["skills"]`/`["validator"]["skills"]` 各自追加 `"design-dynamic-work-graph"`。`.claude/agents/{coder,validator}.md` 是从这里渲染出来的(`render_agent_definition`),不需要手改部署产物。 - -## 5. 验证范围(对应需求文档验收标准,按非目标 #1 收窄) - -需求文档 FR-5/验收标准 1 优先级是"应该",且非目标 #1 明确"不要求第一版就覆盖 Coder/Validator 的全部真实工作场景,只需要证明机制本身走得通"。本轮实现范围: - -- 证明 `plan_work_graph(conn, "coder", ...)` 能生成一个正确带 `task_prompt`、互不依赖节点的 `graph_manifest`(mock `claude` 子进程返回一个"给三个独立模块各自补单元测试"的真实场景响应)。 -- 证明该 `graph_manifest` 经 `topological_layers()` 分层后,三个节点确实落在同一层(跟 G 的实现直接挂钩,不是空谈)。 -- 证明 `generic_node_prompt_builder` 生成的 `build_prompt` 能驱动 `run_graph()` 端到端跑完(mock `execute_work_graph`)。 -- **不改动** `e2e_pipeline.py::implement_code()` 的现有单节点生产路径——那是更大的、需要独立评估的改动(如何决定"这个 Coder 任务复杂到该规划成图"本身就是一个开放问题,需求文档风险第 3 条已经点出),留给后续任务。 - -## 6. 回答需求文档第 8 节开放问题 - -- 新 Skill 命名:`design-dynamic-work-graph`,目录 `skills/design-dynamic-work-graph/`。 -- `_PROMPT_TEMPLATE` 改造方式:按 `org_node` 二选一模板(Planner 固定链 vs 通用动态规划),不是让 Claude 完全自由决定 Planner 要生成哪些节点——Planner 那条链是"架构常量",如 `_normalize_graph_manifest` 自己的注释所说,不应该变成每次都不同。 -- FR-2 并行分组字段格式:不需要,G 靠 `depends_on`/`edges` 结构自动推导(见第 1 节)。 -- Coder/Validator 第一个接入场景:mock 场景"给三个独立模块各自补一个单元测试",用于验证机制可用性,不接入真实生产路径。 diff --git a/docs/todo/dynamic-work-graph-design-skill-requirements.md b/docs/todo/dynamic-work-graph-design-skill-requirements.md deleted file mode 100644 index a64ce76..0000000 --- a/docs/todo/dynamic-work-graph-design-skill-requirements.md +++ /dev/null @@ -1,68 +0,0 @@ -# 跨 Org Agent 的动态 Work Graph 设计能力——需求文档 - -**日期**: 2026-08-05 -**性质**: 需求文档(BRD/PRD 层面),不含详细技术设计。回应 `docs/todo/graph-engineering-open-items.md` 条目 E 的确认结果——"Planner agent 里面要有多个 skill,不仅仅是 brd/prd/sdd/benchmark,还有给其他 Org Graph 节点的 agent 做各种动态 Work Graph 设计,提示词强化肯定要做"。 -**依赖**:本文档定义的"节点分组可并行"标记方式需要跟 `docs/work-graph-parallel-execution-requirements.md`(条目 G)的详细设计对齐,建议 G 的 `graph_manifest` 数据结构定下来之后再最终敲定本文档的 FR-2。 -**状态**: 草案,待评审。 - -## 1. 背景——一个比原开放问题更大的真实缺口 - -综述版/14 步版对照笔记原本提出的问题是"要不要把识别并行节点的判断标准写进 Planning Skill 的 prompt"。深入代码后发现实际缺口比这更大: - -**当前生产代码里,只有 Planner 一个 Org Agent 真正用上了"动态生成 `graph_manifest` 再执行"这套机制**: - -- `src/work_graph_plan.py::plan_work_graph()` 的 `_PROMPT_TEMPLATE`(`work_graph_plan.py:64-74`)是**硬编码**的:不管调用方传入的 `org_node` 是什么,提示词永远要求"Output a graph_manifest with one node per artifact_type (brd, prd, sdd, benchmark)"——这个函数虽然签名上接受任意 `org_node`,但实质上只能用来生成 Planner 的文档写作链。 -- 唯一真实调用它的地方是 `src/e2e_pipeline.py::plan_documents()`,而且写死传入 `"planner"`(`e2e_pipeline.py:220`)。 -- **Coder 和 Validator 目前完全不走这条 plan→execute 的路径**。`work_graph_orchestrate.py` 模块 docstring 明确记录:真正跑通的闭环(`e2e_pipeline.py`,U-014)里,Coder/Validator 用的是"一个更简单、独立的机制(flat `org_node_trigger.trigger_org_node()` calls)"——一次性的单个 Agent 调用,不经过任何"先规划出一张图、再按图执行"的过程。 - -也就是说,架构文档 §5"每个 Org Agent 拥有一个 Planning Skill,在接到任务时先生成本轮的 Work Graph"这句话,目前只对 Planner 成立。Coder 的 to-do 拆解+并行分派(架构文档 §6.2)、Validator 的功能/性能/边界测试并行执行(架构文档 §6.3)都还停留在设计意图,没有对应的动态规划实现——这正是用户说"还有给其他 Org Graph 节点的 agent 做各种动态 Work Graph 的设计"这句话所指的真实缺口,不是一个小的提示词优化。 - -**一个已经存在、值得保留的细节**:`_PROMPT_TEMPLATE` 里已经有 `edge_type: 'zero_token' | 'needs_judgment'` 的区分(`work_graph_plan.py:69-70`),这正好对应 14 步版"边是数据契约,简单聚合应该是零 token 的代码边,只有真正需要判断力的合并才值得用 Agent"这条原则——只是目前这个字段只用在 Planner 的纯串行链上,从未真正驱动过任何"这条边要不要派生一个 Agent"的执行决策。新方案应该保留并真正用起来这个字段,而不是重新发明一套。 - -## 2. 目标 - -1. 把"如何设计一张动态 Work Graph"(识别节点边界、识别哪些节点可以 fan-out、区分 `zero_token`/`needs_judgment` 边、给 fan-out 出的并行节点分组打标记)这套方法论,从 Planner 专属的硬编码提示词里抽出来,做成一个**跨 Org Agent 通用**的能力。 -2. 让 Coder、Validator 也能各自调用这套能力,生成自己的 `graph_manifest`,走上跟 Planner 一样的 plan→execute 路径,而不是继续停留在"一次性单个 Agent 调用"的简化实现。 -3. 在生成 `graph_manifest` 阶段就识别并标注"哪些节点互不依赖、可以并行执行"——即使执行层的并行能力(条目 G)还没上线,至少先把这个信息在数据模型里留出来,不需要等 G 上线后再回头改 Planning 阶段的产出格式。 - -## 3. 非目标 - -1. **不要求第一版就覆盖 Coder/Validator 的全部真实工作场景**。架构文档 §6.2/§6.3 描述的 Coder to-do 拆解、Validator 多类型测试并行是长期目标,第一版只需要证明"Coder/Validator 也能用同一套通用方法论生成一个可执行的 `graph_manifest`"这件事本身走得通,不需要一次性把每种真实业务场景都覆盖。 -2. **不改变 `plan_work_graph`/`run_graph` 的既有函数签名和执行机制**。这是提示词/方法论层面的扩展("用什么内容去问 Claude"),不是执行引擎的改动——`_PROMPT_TEMPLATE` 硬编码这件事本身要改,但改的方式是让它按 `org_node`/任务类型选择不同的提示词模板,不是重写 `plan_work_graph` 的调用方式。 -3. **不在本文档里定案"节点分组可并行"的具体字段格式**。这个格式需要跟条目 G 的详细设计对齐(G 需要知道 `graph_manifest` 里怎么表达"这组节点可以并发触发",才能实现相应的调度逻辑),本文档只提出需求,不抢先定字段名。 - -## 4. 功能需求 - -| # | 需求描述 | 优先级 | -|---|---|---| -| FR-1 | 把"如何设计一张 Work Graph"的通用方法论(节点边界怎么划、`zero_token`/`needs_judgment` 边怎么区分、fan-out 候选点怎么识别)抽成一个独立的 Skill(暂定名 `design-dynamic-work-graph`,具体命名留待设计阶段确定),跟现有 `skills/write-brd` 一类专属技能平级存放。 | 必须 | -| FR-2 | 这个 Skill 的产出需要能标注"哪些节点属于同一个可并行分组"——具体字段格式待 G 的设计确定后再补全,本需求只要求"设计阶段必须把这个信息在 `graph_manifest` 层面表达出来"这件事本身。 | 必须 | -| FR-3 | `work_graph_plan.py::plan_work_graph()` 的提示词生成逻辑需要从"永远硬编码 brd/prd/sdd/benchmark"改造成"按调用方传入的任务描述/`org_node` 动态生成",具体改造方式(拆成多个模板、还是让 Skill 内容本身驱动 Claude 自主判断要生成哪些节点)留待设计阶段决定。 | 必须 | -| FR-4 | Coder、Validator 各自的 `.claude/agents/{coder,validator}.md`(及其 canonical 来源 `src/agent_profile.py::FRONTMATTER`)需要引用这个新 Skill。 | 必须 | -| FR-5 | 需要至少一条真实的 Coder 或 Validator 场景,验证"调用新 Skill 生成 `graph_manifest` → 走 `plan_work_graph`/`run_graph` 路径执行 → 产出正确结果"这条链路真的跑得通,不是只停留在 Planner 一个场景上验证。 | 应该 | - -## 5. 验收标准 - -1. 给 Coder 一个可以自然拆成 2-3 个互不依赖子任务的真实场景(比如"给三个独立的模块各自补一个单元测试"),验证 Coder 能通过新 Skill 生成一个 `graph_manifest`(而不是走现在的单次 `trigger_org_node` 调用),且该 `graph_manifest` 正确标注了这几个节点互不依赖。 -2. 验证 `plan_work_graph()` 面对 Coder/Validator 的调用时,不再原样返回"one node per artifact_type (brd, prd, sdd, benchmark)"这个 Planner 专属结构。 -3. 验证 Planner 现有的 BRD→PRD→SDD→Benchmark 场景在改造后行为不受影响(回归测试,`src/test_e2e_pipeline.py` 等既有用例应继续通过)。 -4. 部署后能在 Coder/Validator 的 `.claude/agents/*.md` 里看到新 Skill 出现在 `skills:` frontmatter 列表。 - -## 6. 约束与假设 - -- 复用现有的 `plan_work_graph()`/`run_graph()`/`work_graph_execute.execute_work_graph()` 执行机制,不新建一套并行的规划-执行引擎。 -- `_PROMPT_TEMPLATE` 里已有的 `zero_token`/`needs_judgment` 边类型区分要保留,新 Skill 的方法论要明确指导 Claude 在生成边时如何选择这个字段,而不是留空/随意填。 -- Coder/Validator 引入 plan→execute 路径后,原来的 `org_node_trigger.trigger_org_node()` 单次调用机制不需要立刻废弃——对于确实只需要单个节点就能完成的简单任务,继续走这条更轻量的路径是合理的(呼应架构文档和 14 步版共同的原则:"任务很小或孤立时不需要上图")。新 Skill 解决的是"任务复杂到值得规划成图时,Coder/Validator 有没有能力做这件事",不是要求所有任务都必须走图。 - -## 7. 风险 - -- **`_PROMPT_TEMPLATE` 硬编码改造有回归风险**:这是唯一一条已经在生产里真实跑通、支撑着 Planner BRD→PRD→SDD→Benchmark 全链路的代码路径,改造时必须有充分的回归测试覆盖,不能因为扩展到 Coder/Validator 而破坏 Planner 现有场景。 -- **FR-2 的具体字段格式依赖 G 的设计**,如果 G 的设计阶段推迟或方向调整,本条目的 FR-2/FR-3 也需要跟着调整,两者需要保持沟通同步,不要各自独立往前推进导致字段对不上。 -- **Coder/Validator 引入图规划后,"什么时候该规划成图、什么时候该走简单单次调用"这个判断本身也需要方法论指导**——如果不加约束,可能出现"任务很小也被规划成图"的过度设计,第 6 节约束里提到的原则需要在 Skill 提示词里显式写出来,不能只是隐式假设 Claude 会自己把握分寸。 - -## 8. 开放问题(留给详细设计阶段回答) - -- [ ] 新 Skill 的具体命名和目录结构。 -- [ ] `_PROMPT_TEMPLATE` 具体怎么改造——是给每个 `org_node` 各自一份模板,还是设计成让 Claude 根据任务描述自主决定要生成哪些节点(更灵活但更难约束输出格式)? -- [ ] FR-2 的并行分组字段具体格式(待 G 确定)。 -- [ ] Coder/Validator 第一个接入的真实业务场景选哪个,作为验收标准 1 的具体测试用例? diff --git a/docs/todo/graph-engineering-open-items.md b/docs/todo/graph-engineering-open-items.md index a3c8276..31daa93 100644 --- a/docs/todo/graph-engineering-open-items.md +++ b/docs/todo/graph-engineering-open-items.md @@ -1,7 +1,7 @@ # Graph Engineering 系列对照笔记——待确认事项清单 -**日期**: 2026-08-04(2026-08-05 更新:A/B/D/E/F/G 已确认方向,C 已确认暂不做) -**性质**: 从三份对照笔记里提取出的全部"待讨论的开放问题",去重合并后按主题整理,供逐条确认方案。每条确认后会展开成独立的需求文档(参考 `docs/work-graph-parallel-execution-requirements.md` 的产出方式),本文件本身只做索引和现状追踪,不写详细方案。 +**日期**: 2026-08-04(2026-08-06 更新:A/B/D/E/F/G/H 全部已实现,C 已确认不做——全部条目均已resolve,本文件继续作为这条主线的活索引保留在 `docs/todo/`,不迁入 `docs/done/`,因为它会随新一批对照笔记/差距检查持续追加条目,见 H 的加入方式) +**性质**: 从三份对照笔记里提取出的全部"待讨论的开放问题",去重合并后按主题整理,供逐条确认方案。每条确认后会展开成独立的需求文档(参考 `docs/done/work-graph-parallel-execution-requirements.md` 的产出方式,条目落地后连同设计文档一并移入 `docs/done/`),本文件本身只做索引和现状追踪,不写详细方案。 **来源文档**: - `docs/reference/graph-engineering-1000-agents-5-steps.diff-with-graphlemon.md`(简称"5 步版对照") - `docs/reference/graph-engineering-vs-loop-engineering-overview.diff-with-graphlemon.md`(简称"综述版对照") @@ -36,7 +36,7 @@ ## B. loop-until-dry 要不要在 Validator 落地 -**状态**:✅ 已实现(2026-08-06)→ 需求文档 `docs/todo/validator-loop-until-dry-skill-requirements.md` + 设计文档 `docs/todo/validator-loop-until-dry-skill-design.md` +**状态**:✅ 已实现(2026-08-06)→ 需求文档 `docs/done/validator-loop-until-dry-skill-requirements.md` + 设计文档 `docs/done/validator-loop-until-dry-skill-design.md` **来源**:5 步版对照 §4 开放问题 1 @@ -62,7 +62,7 @@ ## D. Time-travel 调试要不要立项 -**状态**:✅ 已实现(2026-08-06)→ 需求文档 `docs/todo/work-graph-time-travel-debugging-requirements.md` + 设计文档 `docs/todo/work-graph-time-travel-debugging-design.md` +**状态**:✅ 已实现(2026-08-06)→ 需求文档 `docs/done/work-graph-time-travel-debugging-requirements.md` + 设计文档 `docs/done/work-graph-time-travel-debugging-design.md` **来源**:综述版对照 §6 @@ -78,7 +78,7 @@ ## E. Planning Skill 提示词强化——识别可并行节点 + 通用 Work Graph 设计能力 -**状态**:✅ 已实现(2026-08-06)→ 需求文档 `docs/todo/dynamic-work-graph-design-skill-requirements.md` + 设计文档 `docs/todo/dynamic-work-graph-design-skill-design.md` +**状态**:✅ 已实现(2026-08-06)→ 需求文档 `docs/done/dynamic-work-graph-design-skill-requirements.md` + 设计文档 `docs/done/dynamic-work-graph-design-skill-design.md` **来源**:综述版对照 §7 开放问题 3 + 14 步版对照 §3 开放问题 2(同一主题:让 Planning 阶段更早识别并行机会) @@ -92,7 +92,7 @@ ## F. 三视角评审团机制要不要推广到其他验证点 -**状态**:✅ 已实现(2026-08-06)→ 需求文档 `docs/todo/verifier-fan-out-jury-requirements.md` + 设计文档 `docs/todo/verifier-fan-out-jury-design.md` +**状态**:✅ 已实现(2026-08-06)→ 需求文档 `docs/done/verifier-fan-out-jury-requirements.md` + 设计文档 `docs/done/verifier-fan-out-jury-design.md` **来源**:综述版对照 §7 开放问题 4 + 14 步版对照 §3 开放问题 4(同一主题) @@ -106,7 +106,7 @@ ## G. Work Graph 节点并行执行(Python 线程池方案) -**状态**:✅ 已实现(2026-08-06)→ 需求文档见 `docs/work-graph-parallel-execution-requirements.md` +**状态**:✅ 已实现(2026-08-06)→ 需求文档 `docs/done/work-graph-parallel-execution-requirements.md` + 设计文档 `docs/done/work-graph-parallel-execution-design.md` **来源**:14 步版对照 §3 开放问题 1 @@ -120,7 +120,7 @@ ## H. 统一 Role Package 抽象层 -**状态**:✅ 已实现(2026-08-06)→ 需求文档 `docs/todo/role-package-abstraction-requirements.md` + 设计文档 `docs/todo/role-package-abstraction-design.md` +**状态**:✅ 已实现(2026-08-06)→ 需求文档 `docs/done/role-package-abstraction-requirements.md` + 设计文档 `docs/done/role-package-abstraction-design.md` **来源**:不属于三份 Graph Engineering 对照笔记,是用户对照 Agentic Enterprise 产品目标("role + context + skills + applications + workflows + permissions + acceptance criteria;all essentially are context for intelligent computing")单独提出的差距检查,2026-08-06。 diff --git a/docs/todo/role-package-abstraction-design.md b/docs/todo/role-package-abstraction-design.md deleted file mode 100644 index 44805be..0000000 --- a/docs/todo/role-package-abstraction-design.md +++ /dev/null @@ -1,111 +0,0 @@ -# 统一 Role Package 抽象层——详细设计(SDD) - -**日期**: 2026-08-06 -**性质**: 详细设计,对应需求文档 `docs/todo/role-package-abstraction-requirements.md`(条目 H,需求评审已通过,见 `docs/todo/graph-engineering-open-items.md` H 节的评审记录)。回答该需求文档第 8 节的开放问题。 -**范围确认(用户 2026-08-06)**:FR-3 的 4 张 CHECK 约束表一次性全改,不分阶段。 -**状态**: 设计草案,实施中。 - -## 1. Role Package 载体格式:JSON,不是 YAML - -需求文档 FR-2 的开放问题是"单文件 vs 目录约定",但更根本的约束是 05 §5.7 的纯 stdlib 限制——graphlemon 自己的代码不能依赖 PyYAML。`skills//SKILL.md` 现有的 YAML frontmatter 之所以能用,是因为**graphlemon 自己的代码从来没有解析过它**(`skill_query.py`/`skill_update.py` 只把 SKILL.md 当不透明文本读写,真正解析 `skills:`/`tools:` frontmatter 的是 Claude Code 自己,不是这个仓库的 Python 代码)。Role Package 不一样:`agent_profile.py` 需要真正**读懂**它的结构化字段(哪些 skills、哪些 tools、memory 配置……),手写一个 YAML 子集解析器风险高、容易在多行字符串/嵌套列表这类边界情况上出 bug——纯 stdlib 的 `json` 模块能零风险做到同样的事。 - -**决定**:`roles//role.json`,纯 JSON,`json.load` 直接解析。 - -## 2. role.json 是"引用清单",不是内容复制 - -需求文档 FR-1 自己的措辞已经暗示了这一点——"context(memory 配置 + 运行时 context 组装规则的**引用**)"、"skills(**对应**现有 `skills:` frontmatter)"、"acceptance criteria(**引用**)"——七个维度里大多数本来就该是指针,不是把内容整个复制一份。 - -具体到 role/identity 维度:`agents//SOUL.md`(+ validator 的 `AGENTS.md`)**原样保留,不删除、不合并、不改名**——role.json 只存这两个文件的相对路径引用(`identity.soul_md_path`/`identity.agents_md_path`)。这个决定直接让验收标准 2("迁移后部署产物逐字节一致")几乎自动成立:`render_agent_definition()` 的 body 拼接逻辑(`SOUL.md` 内容 + `AGENTS.md` 内容)完全不变,唯一变化的是 frontmatter 字段的**来源**从硬编码 `FRONTMATTER` 字典变成解析 `role.json`。 - -```json -{ - "role_id": "validator", - "description": "graphlemon Validator: independently verifies a task, PASS/FAIL/INCONCLUSIVE, read-only on source.", - "migrated_from_profile": "qa", - "identity": { - "soul_md_path": "agents/validator/SOUL.md", - "agents_md_path": "agents/validator/AGENTS.md" - }, - "context": { "memory": "project" }, - "model": "claude-sonnet-5", - "skills": ["qa-task-validation", "unattended-agent-operations", "design-dynamic-work-graph", "unknown-scope-defect-sweep"], - "applications": { "mcp_servers": [] }, - "workflows": { "dynamic_planning": "generic" }, - "permissions": { - "tools": ["Read", "Bash", "Grep", "Glob"], - "disallowed_tools": ["Edit", "Write"], - "notes": [ - { "constraint": "不提交、推送、reset、清理或覆盖用户工作树", "enforced": "prose_only" } - ] - }, - "acceptance_criteria": { "ref": "docs/sdd/06-验收与测试锚点.md" } -} -``` - -- **applications**(FR-6,"应该"):`mcp_servers` 列表,允许为空——满足"这个维度存在、字段长什么样、允许为空",不要求真实集成(非目标 #4)。 -- **workflows**:接 E 的产出(非目标 #2 明确不重复设计 E 已经做的事,这里只是一个指向枚举)。`planner` 是 `"canonical_doc_chain"`(`work_graph_plan.py` 里 `org_node == "planner"` 走的固定链),`coder`/`validator` 是 `"generic"`(E 的 `_GENERIC_PROMPT_TEMPLATE`/`generic_node_prompt_builder` 路径)。 -- **permissions.notes**(验收标准 4):Validator 的 Bash 子命令限定失效那条已知漏洞,诚实标注 `"enforced": "prose_only"`,不假装是工具层强制——非目标 #3 明确不在本文档修复这个漏洞本身,只要求如实反映现状。 - -## 3. `agent_profile.py` 改造:`ORG_NODES` 从硬编码元组变成目录发现 - -```python -_ROLES_SOURCE_DIR = os.path.join(_REPO_ROOT, "roles") - -def _discover_role_ids(roles_dir=None): - """扫描 roles//role.json 是否存在,role_id 就是目录名。 - roles_dir 可覆盖(测试用真实文件系统 fixture 验证'新增角色不用改代码' - 这条验收标准,不需要真的往这个仓库的 roles/ 目录里塞测试数据)。""" - roles_dir = roles_dir or _ROLES_SOURCE_DIR - if not os.path.isdir(roles_dir): - return () - return tuple(sorted( - name for name in os.listdir(roles_dir) - if os.path.isfile(os.path.join(roles_dir, name, "role.json")) - )) - -ORG_NODES = _discover_role_ids() -``` - -`FRONTMATTER`/`_DESCRIPTIONS`/`_MIGRATED_FROM_PROFILE` 三个硬编码字典全部删除,替换成 `_load_role(role_id, roles_dir=None)`(`json.load` 解析对应 `role.json`)。`render_agent_definition()`/`deploy_agent_definitions()`/`record_agent_profiles()` 都接受一个可选 `roles_dir` 参数(默认 `None` 即用真实目录),这是验收标准 1 能被自动化测试验证的关键——测试往一个临时目录写一个新角色的 `role.json`,不改任何 Python 代码或 `roles_dir` 之外的任何东西,直接调用这三个未经修改的函数,确认新角色能被发现、渲染、部署。 - -`ORG_NODES` 这个名字保留不改(`org_node_trigger.py` 等外部模块已经在用 `from src.agent_profile import ORG_NODES`,改名会不必要地扩大改动面),但注释说明它现在是数据驱动的,不再是硬编码元组。 - -## 4. Schema:4 张表的 CHECK 约束怎么改 - -**决定的形状**:`CHECK (x IN ('planner', 'coder', 'validator'))` → `CHECK (x != '')`——仍然是数据库层面的最小完整性保证(非空字符串),但不再枚举具体值。不是完全去掉 CHECK(那样连"总不能是空字符串"这点基本保证都没有了),也不是新建一张 `role` 表做外键(需求文档 §8 留的另一个选项)——本设计选择更简单的那条路径:4 张表全改,但改的目标是"放开枚举"而不是"建立引用完整性",避免这次改动同时背上"外键完整性设计"这个新的复杂度。 - -**真实的改动面**(这是这次评审最重要的发现,比需求文档原始风险描述更细):`task`/`work_graph_run`/`skill` 三张表被 `work_node_execution`/`artifact`/`bug_record`/`execution_event` 等多张表通过外键引用;`agent_profile` 没有任何表引用它。SQLite 不支持 `ALTER TABLE ... DROP CONSTRAINT`,改 CHECK 约束必须用"建新表复制数据删旧表改名"的标准操作——但因为改的是**父表**、外键定义本身(`REFERENCES task(task_id)` 这类文本)**不需要跟着变**,所以只需要重建这 4 张父表本身,不需要动任何一张子表。SQLite 官方文档给的正确流程("Making Other Kinds Of Table Schema Changes")是: - -1. `PRAGMA foreign_keys = OFF`(关闭本连接的外键校验,只在这次迁移期间) -2. 开事务 -3. 对每张表:建同结构、新 CHECK 的 `
_new` → `INSERT INTO
_new SELECT * FROM
` → `DROP TABLE
` → `ALTER TABLE
_new RENAME TO
` → 重建这张表原有的索引 -4. `PRAGMA foreign_key_check` 确认没有产生悬空外键 -5. 提交事务 -6. `PRAGMA foreign_keys = ON` - -`migrate_relax_org_node_constraints(conn)` 一个函数按这个流程处理全部 4 张表(一次性开一个事务,不是 4 个独立迁移各自开关 `foreign_keys`),每张表重建前用 `sqlite_master` 里的 CREATE TABLE 文本判断是否已经是新约束,已经是就跳过(幂等,`migrate_expand_artifact_types` 的同款前置检查模式)。`SCHEMA_SQL` 里的 4 处 `CHECK (... IN ('planner', 'coder', 'validator'))` 直接改成 `CHECK (... != '')`,新数据库不需要迁移。 - -**索引重建清单**(重建表之后必须重新建的,`DROP TABLE` 会连带丢失表自己的索引): -- `skill`:无额外索引。 -- `task`:`idx_task_status`、`idx_task_assigned_org_node`、`idx_task_project_id`。 -- `work_graph_run`:`ux_work_graph_run_active`(分区唯一索引,D 已经依赖这条索引的语义,重建后必须原样保留)。 -- `agent_profile`:PK 本身即索引,无需额外重建。 - -## 5. `work_graph_run_query.py::_VALID_ORG_NODES`(评审时发现的遗漏依赖) - -改成直接引用 `agent_profile.ORG_NODES`,不再自己维护一份硬编码三元组——这条不在需求文档原始 FR 列表里,是评审阶段核实代码时发现的真实遗漏(`docs/todo/graph-engineering-open-items.md` H 节评审记录已经点名),顺手一起改掉。 - -## 6. 明确不做的部分(如实记录,呼应评审阶段的结论) - -- **`frontend/app.js` 的 4 处硬编码三角色枚举**(路由正则、`ORG_NODE_ORDER` 排序数组、任务创建表单下拉、`byOrgNode` 查找对象)——评审阶段已经确认这些不在验收标准 1 的范围内(验收标准 1 明确"这个新角色不需要真的接入 Org Graph 执行"),本轮不改。新增一个角色之后,PH3 控制台依然认不出它,这是本轮已知、刻意保留的缺口,不是遗漏。 -- **`role` 表 + 外键完整性**(§4 提到的另一个选项)——本轮用更简单的"放开枚举"实现,不建立真正的引用完整性约束。 -- **`org_node_trigger.py` 校验逻辑本身**——它已经用 `from src.agent_profile import ORG_NODES` 做校验,`ORG_NODES` 变成数据驱动之后这条校验自动跟着数据驱动,不需要改这个文件。 -- **permissions 维度的 Bash 子命令限定漏洞本身**——诚实标注(§2),不修复(需求文档非目标 #3)。 - -## 7. 回答需求文档第 8 节开放问题 - -- 载体格式:单文件 `role.json`(不是目录多文件),JSON 不是 YAML(§1)。 -- CHECK 约束改造方式:放开成 `!= ''`,不建 `role` 表外键(§4)。 -- applications 最小定义:`{"mcp_servers": []}`(§2)。 -- 迁移步骤:三个角色各自的 `role.json` 直接引用现有 `agents//SOUL.md`/`AGENTS.md`,不复制内容(§2),验证方式是 `test_agent_profile.py` 现有测试断言部署产物不变。 -- Role Package 跟 E 的接口:`workflows.dynamic_planning` 字段,`"canonical_doc_chain"` / `"generic"` 二选一(§2)。 diff --git a/docs/todo/role-package-abstraction-requirements.md b/docs/todo/role-package-abstraction-requirements.md deleted file mode 100644 index 00ee4f0..0000000 --- a/docs/todo/role-package-abstraction-requirements.md +++ /dev/null @@ -1,76 +0,0 @@ -# 统一 Role Package 抽象层——需求文档 - -**日期**: 2026-08-06 -**性质**: 需求文档(BRD/PRD 层面),不含详细技术设计。回应用户对 Agentic Enterprise 产品"可复用打包单元"目标的对照检查——"role + context + skills + applications + workflows + permissions + acceptance criteria;all essentially are context for intelligent computing"。 -**状态**: 草案,待评审。 - -## 1. 背景 - -用户提出的目标判断是:Agentic Enterprise 产品的可复用打包单元,本质上应该是 role / context / skills / applications / workflows / permissions / acceptance criteria 这七个维度,而且这七个维度**本质上是同一种东西**——喂给智能计算的 context,应该被统一表达成一个可序列化、可实例化、可复制的对象。 - -对照 graphlemon 现状(逐维度核对代码得出): - -| 维度 | 现状 | 成熟度 | -|---|---|---| -| **role** | `.claude/agents/{planner,coder,validator}.md` + `agents/*/SOUL.md`,`src/agent_profile.py::FRONTMATTER` 是权威定义 | 有清晰打包格式,但**不可重复实例化**——见第 2 节 | -| **context** | `memory: project` → `.claude/agent-memory/{role}/`,git 版本化;但 Work Node 执行时塞进 prompt 的内容是每个调用点临时拼的(`build_prompt(node, outputs)`),没有独立的"context 组装层"抽象 | 局部实现,无统一抽象 | -| **skills** | `skills//SKILL.md`,按名字挂进角色的 `skills:` frontmatter | 七项里最成熟、最像"可复用打包单元"的一个 | -| **applications** | 没有对应的一等概念。架构文档 §7.2 提到 `mcpServers` 字段可以挂专属 MCP server,但 `agent_profile.py::FRONTMATTER` 里三个角色**一个都没配** | 基本空白 | -| **workflows** | Claude Code 原生 Dynamic Workflows 机制在,但只有 Planner 真正用——`work_graph_plan.py::plan_work_graph()` 的提示词硬编码永远只生成 brd/prd/sdd/benchmark 四个节点,Coder/Validator 走的是更简化的单次 `trigger_org_node()` 调用,没有"先规划图再执行"这条路径(见 `docs/todo/dynamic-work-graph-design-skill-requirements.md`,即条目 E,已确认待做) | 机制存在,不是每个角色都能复用的打包能力 | -| **permissions** | `tools`/`disallowedTools` per role(如 Validator `disallowedTools: [Edit, Write]`) | 有格式,但有记录在案的真实漏洞:`agent_profile.py` 注释明确写了"scoped Bash patterns(如 `Bash(git commit:*)`)会把 Bash 整个禁掉",Validator 的 git-mutation 限制实际上退化成了 SOUL.md 里的文字约定,不是工具层强制 | -| **acceptance criteria** | Benchmark、PASS/FAIL/INCONCLUSIVE、SDD REQ-01~15、`root_cause_trace` 根因归属 | 七项里最成熟的一环 | - -**核心判断**:这不是功能缺口,是抽象层缺口。七个维度里六个都有对应实现(applications 是真空白),但没有被收敛成一个统一的"role package"格式——要理解"Validator 这个角色完整定义是什么",现在得同时读 `agent_profile.py` + `skills/qa-task-validation/SKILL.md` + `.claude/agent-memory/validator/` + `docs/sdd/06-验收与测试锚点.md` 四个地方拼出来。而且 role 本身不是数据驱动的——`src/agent_profile.py::ORG_NODES = ("planner", "coder", "validator")` 是硬编码元组,`src/schema.py` 里四张表(`skill.owner_org_node`、`task.assigned_org_node`、`work_graph_run.org_node`、`agent_profile.org_node`)全部是 `CHECK (... IN ('planner', 'coder', 'validator'))` 的枚举约束——新增一个角色需要改代码和 schema,不是"包一个新角色进来"。 - -## 2. 目标 - -1. 定义一个统一的、可序列化的"Role Package"抽象——把 role/context/skills/applications/workflows/permissions/acceptance criteria 这七个维度表达成同一种对象的不同字段/引用,而不是分散在 frontmatter、独立目录、运行时生成脚本、SDD 文档四个不相关的地方。 -2. 让"角色"从硬编码的 3 个枚举值变成数据驱动的、可新增的对象——**具体的可验证标准**:能不能不改 `schema.py` 的 CHECK 约束、不改 `agent_profile.py` 的硬编码字典,只靠"写一份新的 role package"就让系统认识一个新角色。 -3. 把 applications 这个目前空白的维度定义清楚——至少要回答"一个角色能调用哪些外部工具/集成/MCP server"这件事该怎么表达,不要求第一版就有完整实现。 -4. 现有 Planner/Coder/Validator 三个角色要能**用新格式重新表达**(迁移,不是重写行为),验证这套抽象本身是自洽的,不是只停留在文档设计层面。 - -## 3. 非目标 - -1. **不要求第一版就实现"运行时自主创建新角色"**。架构文档 §3"关键设计原则 1"明确 Org Graph 结构是人工定义、代码级固定,改动属于架构变更需要人工评审——这条决策不动。本需求要解决的是"人工新增一个角色时,需要改多少处代码/schema"这个摩擦,而不是让系统自己决定要不要新增角色。新增角色依然是人工发起的动作,只是发起方式从"改代码"变成"写一份声明式的 package"。 -2. **不在本文档里解决 workflows 维度的具体落地**——Coder/Validator 缺少动态 Work Graph 规划能力这件事,已经是条目 E(`docs/todo/dynamic-work-graph-design-skill-requirements.md`)的范围,本文档只要求"Role Package 里要有一个字段/引用指向这个角色的 workflow 规划能力",不重复设计 E 已经在做的事。 -3. **不在本文档里重新解决 permissions 维度的已知漏洞**(scoped Bash disallowedTools 不生效)——这是 Claude Code 平台本身的行为限制,本文档只要求 Role Package 的 permissions 字段要能诚实反映"这条限制实际上是工具层强制还是仅靠文字约定",不假装这个漏洞已经修复。 -4. **不要求一次性覆盖全部七个维度到同等成熟度**。applications 维度目前是空白,第一版只需要定义清楚"这个维度存在、字段长什么样、允许为空",不要求马上有真实的 MCP server 集成可用。 -5. **不做通用的"角色市场/角色商店"之类的产品化能力**。这是纯粹的内部抽象层重构,不涉及把角色包装成可分发、可交易的产品单元。 - -## 4. 功能需求 - -| # | 需求描述 | 优先级 | -|---|---|---| -| FR-1 | 定义 Role Package 的字段结构,覆盖七个维度:role(身份/人设,对应现有 SOUL.md 正文)、context(memory 配置 + 运行时 context 组装规则的引用)、skills(技能列表,对应现有 `skills:` frontmatter)、applications(可调用的外部工具/集成/MCP server 列表,当前允许为空)、workflows(该角色的 Work Graph 规划能力引用,对接条目 E 的产出)、permissions(tools/disallowedTools,并诚实标注每条限制是工具层强制还是仅文字约定)、acceptance criteria(该角色产出的验收标准引用,比如 Validator 对接 SDD REQ 锚点)。 | 必须 | -| FR-2 | Role Package 需要是**声明式**的(数据,不是代码)——具体载体形式(单个 YAML/JSON 文件、还是延续现在"一个目录 + 几个子文件"的约定)留待设计阶段决定,但判断标准是"新增一个角色不需要写 Python 代码"。 | 必须 | -| FR-3 | `src/schema.py` 里 `org_node` 相关的四处 `CHECK (org_node IN ('planner','coder','validator'))` 约束需要改造成不再硬编码这三个值——具体改造方式(改成引用一张新的 `role` 表做外键约束,还是放开成自由文本 + 应用层校验)留待设计阶段决定。 | 必须 | -| FR-4 | `src/agent_profile.py::ORG_NODES`/`FRONTMATTER` 这两个硬编码常量需要改造成从 Role Package 读取,而不是 Python 字典里硬写三个角色的定义——但 `deploy_agent_definitions()` 对外的部署行为(生成 `.claude/agents/*.md`)保持不变,只是数据来源变了。 | 必须 | -| FR-5 | 现有 Planner/Coder/Validator 三个角色需要各自被表达成一份 Role Package,作为这套抽象的第一批真实用例,且迁移后系统对外行为(部署出的 `.claude/agents/*.md` 内容、实际执行结果)不应该有可观察的变化——这是一次纯粹的内部表达方式重构,不是功能变更。 | 必须 | -| FR-6 | applications 维度需要有一个明确的最小定义(哪怕第一版只是"MCP server 列表,允许为空"),不能继续保持"架构文档提过但从未真正定义"的状态。 | 应该 | - -## 5. 验收标准 - -1. **核心验收(对应目标 2 的可验证标准)**:能够只通过"新增一份 Role Package"(不改 `schema.py`、不改 `agent_profile.py` 的硬编码逻辑)让系统认识一个新角色,并成功为它部署出一份 `.claude/agents/<新角色>.md`。这个新角色不需要真的接入 Org Graph 执行(Org Graph 本身仍是人工评审后才能改的架构决策,见非目标第 1 条),只需要验证"定义一个角色"这件事本身不再需要写 Python 代码或改 schema 约束。 -2. 迁移后,现有 Planner/Coder/Validator 三个角色部署出的 `.claude/agents/*.md` 内容(frontmatter 字段、正文)跟迁移前逐字节一致(或差异只是格式规范化,不影响语义),既有测试套件(`src/test_agent_profile.py` 一类)全部通过,不需要修改断言。 -3. 给定一份 Role Package,能够看清楚它在七个维度上分别声明了什么——不需要再像现状一样,为了理解"这个角色能干什么"而去翻四个不同的地方。 -4. permissions 维度里,Validator 的 git-mutation 限制在新格式里被诚实标注为"仅文字约定,非工具层强制"(不是假装它已经被强制执行)。 - -## 6. 约束与假设 - -- 这次重构不改变 Org Graph 本身"人工设计、变更需人工评审"的既定原则(架构文档 §3 决策 1),只改变"定义一个角色的具体成本"。 -- Role Package 的 workflows 字段依赖条目 E 的产出(跨 Org Agent 通用的动态 Work Graph 设计能力)——如果 E 还没落地,Role Package 的 workflows 字段可以先指向"占位/尚未实现",不阻塞本需求其余六个维度的推进。 -- 复用现有的 Skill 目录约定(`skills//SKILL.md`)和 memory 约定(`memory: project` → `.claude/agent-memory/{role}/`),不重新发明这两块已经工作良好的机制,只是让 Role Package 引用它们,而不是重新定义。 - -## 7. 风险 - -- **改动面比 B/D/E/F/G 任何一条都大**:`org_node` 的 CHECK 约束出现在 4 张表上,`ORG_NODES`/`FRONTMATTER` 被 `deploy_agent_definitions`、`org_node_trigger`、`work_graph_orchestrate` 等多处代码直接依赖,改造前需要在设计阶段完整梳理所有依赖点,不能只看 schema 定义本身。 -- **applications 维度定义不清可能导致设计阶段卡壳**——这是七个维度里唯一"从零开始"的,没有现成机制可以对齐(不像 skills/permissions/acceptance criteria 那样已经有半成品可以收编),需要额外的探索性设计工作,不能假设跟其他六个维度一样容易定义。 -- **"声明式、不用写代码就能新增角色"这个目标可能跟 Claude Code 本身的能力边界冲突**——`.claude/agents/*.md` 的部署机制、`skills:`/`tools:` frontmatter 本身已经是 Claude Code 原生支持的声明式格式,Role Package 更多是在这之上再包一层统一抽象,需要在设计阶段验证这层抽象不会变成"重新发明 Claude Code 已经提供的能力"的冗余包装。 - -## 8. 开放问题(留给详细设计阶段回答) - -- [ ] Role Package 的具体载体格式(单文件 vs 目录约定)。 -- [ ] `org_node` CHECK 约束的改造方式——新增 `role` 表做外键,还是放开成自由文本 + 应用层校验。 -- [ ] applications 维度的最小可行定义——只是 MCP server 列表,还是要覆盖更广的"角色能操作哪些外部系统"。 -- [ ] 迁移 Planner/Coder/Validator 三个现有角色到新格式的具体步骤和验证方式。 -- [ ] 这套抽象跟条目 E(动态 Work Graph 设计能力)的接口——Role Package 的 workflows 字段具体怎么引用 E 的产出。 diff --git a/docs/todo/validator-loop-until-dry-skill-design.md b/docs/todo/validator-loop-until-dry-skill-design.md deleted file mode 100644 index ce26137..0000000 --- a/docs/todo/validator-loop-until-dry-skill-design.md +++ /dev/null @@ -1,24 +0,0 @@ -# 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/docs/todo/validator-loop-until-dry-skill-requirements.md b/docs/todo/validator-loop-until-dry-skill-requirements.md deleted file mode 100644 index fa295d1..0000000 --- a/docs/todo/validator-loop-until-dry-skill-requirements.md +++ /dev/null @@ -1,59 +0,0 @@ -# Validator "未知规模扫描收敛" Skill——需求文档 - -**日期**: 2026-08-05 -**性质**: 需求文档(BRD/PRD 层面),不含详细技术设计。回应 `docs/todo/graph-engineering-open-items.md` 条目 B 的确认结果——"loop-until-dry 落地在 Validator,做成一个 Skill,`validator.md` 运行时记得调用"。 -**状态**: 草案,待评审。 - -## 1. 背景 - -14 步版对照笔记(`docs/reference/Graph Engineering with Claude_ 14-Step roadmap from 0 to graph architect (Full Course).diff-with-graphlemon.md` §1 第 11 行、§2)指出:graphlemon 现有的收敛机制只有 Org-level Loop 的固定 3 轮上限(`docs/sdd/04-P-流程与状态流转.md` 已标注"未来优化候选"),而 14 步版描述的 loop-until-dry(连续 K 轮无新发现才停止,去重要对比"全部见过的"而非"已确认的")没有对应实现。 - -同一份笔记也指出一个结构性事实:`work_graph_orchestrate.py::topological_order` 对有环的 `graph_manifest` 直接报错("graph_manifest has a dependency cycle")——**Work Graph 被设计成严格 DAG,图内不支持回路**。这意味着 loop-until-dry **不能**实现成"`graph_manifest` 里画一条指回自己的边",那需要改动整个执行模型(`run_graph()`/`topological_order()`),代价远大于收益。 - -**关键澄清(本需求的立论基础)**:loop-until-dry 不需要是 Work Graph 层面的图内回路。它可以是**一个 Work Node 内部、单次 `agent()` 调用会话自己的行为**——一次 `claude -p` 调用本身就是一个多轮工具调用的会话(读文件、搜索、判断、再读文件……),只要 Skill 的提示词要求 Agent"持续搜索直到连续两轮没有新发现才停止、去重比对全部见过而非仅已确认的结果",这个收敛循环完全发生在**一次 Work Node 执行内部**,`graph_manifest` 本身仍然是一条无环的边(该节点的输入是任务描述,输出是收敛后的结构化 bug list),不需要改动 `topological_order`/`run_graph` 的任何一行代码。这跟"B. loop-until-dry 要不要在 Validator 落地"的确认结果——"做成一个 Skill"——是同一个思路:Skill 是提示词层面的方法论,不是图结构。 - -## 2. 目标 - -1. 给 Validator 一个新的 Skill,让它在面对"未知规模缺陷扫描"类任务时,能够自主判断收敛(连续 K 轮无新发现才停止),而不是像现在这样只能做"扫一遍就结束"的单轮验证。 -2. 这个 Skill 的去重逻辑必须对比"全部已见过的候选项",而不是只对比"已确认通过的结果"——避免被验证器打回的候选项在下一轮又被当成新发现重新提出,导致循环永远不收敛(14 步版原文点名的"几乎所有人第一次实现都会踩的坑")。 -3. `.claude/agents/validator.md`(及其 canonical 来源 `src/agent_profile.py::FRONTMATTER["validator"]`)需要引用这个新 Skill,并且 Validator 的人设文档(`agents/validator/SOUL.md` 迁移版)需要有明确指引:什么场景下应该主动调用这个 Skill,而不是走默认的单轮验证路径。 - -## 3. 非目标 - -1. **不改 Work Graph 执行模型**。不给 `graph_manifest`/`topological_order`/`run_graph` 引入图内回路支持,见第 1 节的关键澄清。 -2. **不改 Org-level Loop 的 3 轮上限**。Org-level Loop 管的是"Validator 判定不通过、打回 Planner/Coder、再来一轮"这个更粗粒度的循环,跟这个 Skill 管的"Validator 一次验证任务内部要扫多少轮才算扫完"是两层不同的收敛机制,互不冲突、互不替代。 -3. **不要求这个 Skill 覆盖 Validator 的所有验证场景**。大多数验证任务(比如验收一个边界清晰的 Development Unit)本来就是有限规模的,不需要 loop-until-dry;这个 Skill 只服务于"规模未知"这一类场景(比如"扫描整个代码库找所有未处理的 Promise""审计所有路由文件的鉴权缺失"这种没有预先划定范围的任务)。 - -## 4. 功能需求 - -| # | 需求描述 | 优先级 | -|---|---|---| -| FR-1 | 新增一个 Skill(暂定名 `unknown-scope-defect-sweep`,具体命名留待设计阶段确定),描述 loop-until-dry 的方法论:持续搜索/扫描 → 每轮结果与"全部已见过的候选项"去重 → 连续 K 轮(默认值留待设计阶段确定,参考 14 步版建议的 K=2)无新发现 → 停止并汇总。 | 必须 | -| FR-2 | Skill 需要明确定义"候选项"的去重键(key)该怎么取——不同任务场景(bug 扫描 vs 路由审计 vs 依赖审计)候选项的自然标识不同,Skill 需要给出通用指导原则,而不是绑定某一种具体场景。 | 必须 | -| FR-3 | Skill 执行过程中每一轮的候选发现,在最终汇总前应当经过验证(复用 Validator 已有的 PASS/FAIL/INCONCLUSIVE 判定框架或 `root_cause_trace` 机制,具体复用方式留待设计阶段确定)——不能是"只要出现过就算数",避免把误报也计入最终结果。 | 应该 | -| FR-4 | `src/agent_profile.py::FRONTMATTER["validator"]["skills"]` 需要加入这个新 Skill 的目录名,并且 `agents/validator/SOUL.md`(canonical 人设源文件)需要新增一段指引:什么信号(比如任务描述里出现"扫描/审计/全部/所有"这类无边界表述)应该触发这个 Skill,而不是走默认单轮验证路径。 | 必须 | -| FR-5 | Skill 需要有一个硬性上限(最大轮次或最大 token/时间预算),防止"连续 K 轮无新发现才停"这个收敛条件本身因为某种原因(比如候选项生成逻辑有 bug,每轮都能生成一个"看起来新"但实际重复的假发现)永远不触发,导致单次 Work Node 执行无限跑下去。 | 必须 | - -## 5. 验收标准 - -1. 给 Validator 一个人为构造的"扫描规模未知"任务(例如一个测试仓库里散落 5 处同一类缺陷),验证 Validator 使用新 Skill 后能够找全全部 5 处,并在连续两轮无新发现后主动停止,不需要人工告诉它"已经找完了"。 -2. 验证去重逻辑:人为构造一个"候选项会被验证器打回、但下一轮扫描逻辑本身还会再次生成同一个候选项"的场景,确认该候选项不会被无限重复计入"新发现",循环能够正常收敛(不是因为侥幸没再被搜到,而是因为去重键命中了"已见过"记录)。 -3. 验证 FR-5 的硬上限:人为构造一个"永远能生成看似新其实重复"的极端场景,确认 Skill 会在触达硬上限后强制停止并如实报告"可能未收敛",而不是无限执行下去耗尽预算。 -4. `.claude/agents/validator.md` 部署后(`deploy_agent_definitions`),能看到新 Skill 出现在 `skills:` frontmatter 列表里。 - -## 6. 约束与假设 - -- 复用 Validator 现有的技能目录约定(`skills//SKILL.md`,参考 `skills/qa-task-validation/SKILL.md` 的文件结构),但**不复用**该文件里遗留的飞书 Base 相关内容(`tblnlIVgHNaQXLAL` 等)——那是尚未清理的 Hermes profile 遗留细节,新 Skill 要按 graphlemon 当前的实际架构(无飞书 Base 依赖)编写,不能照抄旧格式。 -- 假设这个 Skill 在**单次 Work Node 执行内部**完成收敛循环(一次 `claude -p` 调用内的多轮工具使用),不依赖 Work Graph 层面的任何调度改动,因此和「G. Work Graph 节点并行执行」没有直接依赖关系,可以独立排期。 - -## 7. 风险 - -- **收敛判定本身依赖模型自我报告"这轮有没有新发现"**,如果模型判断不准确(漏报"这是新发现"或误报"这不是新发现"),收敛条件可能提前/延迟触发。FR-3 的"每轮发现经过验证"部分程度上缓解这个风险,但无法完全消除——这是提示词工程层面的固有局限,不是可以被架构设计完全解决的问题。 -- **K 值和硬上限的默认值需要实测校准**,不同任务规模下合适的 K 值可能不同,第一版先给一个保守默认值,观察真实运行数据后再调整。 - -## 8. 开放问题(留给详细设计阶段回答) - -- [ ] Skill 的具体命名和目录结构。 -- [ ] FR-3"复用已有验证框架"的具体接线方式——是在 Skill 提示词里直接要求"每个候选项过一遍 PASS/FAIL/INCONCLUSIVE 判断",还是调用 `root_cause_trace` 现成的多视角判断机制? -- [ ] K 值、硬上限的默认数字。 -- [ ] `agents/validator/SOUL.md` 里新增指引段落的具体触发条件描述(关键词匹配?任务类型标注?)。 diff --git a/docs/todo/verifier-fan-out-jury-design.md b/docs/todo/verifier-fan-out-jury-design.md deleted file mode 100644 index aac956f..0000000 --- a/docs/todo/verifier-fan-out-jury-design.md +++ /dev/null @@ -1,70 +0,0 @@ -# 三视角评审团——Work Graph Fan-out 实现——详细设计(SDD) - -**日期**: 2026-08-06 -**性质**: 详细设计,对应需求文档 `docs/todo/verifier-fan-out-jury-requirements.md`(条目 F,已确认)。回答该需求文档第 8 节的开放问题。 -**依赖代码**:`src/root_cause_trace.py`(`judge_root_cause`/`_PERSPECTIVE_FRAMINGS`)、`src/org_level_loop.py`(`run_fix_loop`,唯一真实生产调用方)、`src/work_graph_orchestrate.py`(G 已实现的 `run_graph()`/`topological_layers()`)。 -**状态**: 设计草案,待评审后实施。 - -## 1. 汇总节点不是 Work Node——这是"zero_token 边"原则的直接应用 - -需求文档背景里"1 个汇总节点"的表述容易被误读成"汇总也要是一次 `claude -p` 调用"。实际不需要:`>=2/3` 投票本身是纯粹的计数聚合,不需要 Agent 推理——这正是 E 设计文档里强调的 `edge_type: zero_token` 场景本身。汇总逻辑保持为一个普通 Python 函数,在 `run_graph()` 返回之后,在调用方所在的进程里直接对 `outputs` 做计数,不产生额外的 `claude -p` 调用、不产生额外 token 成本(呼应需求文档第 7 节风险"token 成本上升"的顾虑,此处直接不引入这块新增成本)。 - -验收标准 1 要求"能在 execution_event/日志里看到三次独立的节点执行记录"——这只要求**三个视角节点**是真 Work Node(`run_graph()`/`run_work_node()` 本来就会为每个节点写 `node_start`/`node_completed` 日志),不要求汇总也是。 - -## 2. 判定规则复用,不重写(FR-3) - -`root_cause_trace.judge_root_cause` 现有的 `>=2/3` 计数逻辑被抽成一个共享私有函数 `_tally_perspectives(judgments)`,`judge_root_cause`(保留,闭包版本)和新的 `judge_root_cause_via_work_graph`(Work Graph 版本)都调用它——两套判定规则物理上是同一份代码,不是两份逻辑各自实现、容易失配。`judgments` 的 dict 形状(`{"category", "detail"}`)两个版本完全一致,`_tally_perspectives` 不需要知道调用方是闭包还是 Work Node 输出。 - -## 3. `judge_root_cause_via_work_graph`:新的 Work Graph 版本 - -```python -def judge_root_cause_via_work_graph( - conn, task_id, target_folder_path, bug_description, context, - org_node="validator", skill_id="qa-task-validation", timeout=300, -): -``` - -内部结构: - -1. `_PERSPECTIVE_FRAMINGS`(已存在,三个视角的 framing 文本)驱动构造每个视角的完整提示词(复用现有 framing 文本,不改视角内容本身)。 -2. 构造一个菱形(严格说是"N 节点单层,无边")`graph_manifest`:`nodes = [{"node_id": "perspective-" + name, "depends_on": []} for name in perspectives]`,`edges = []`——三个节点互不依赖,`topological_layers()` 天然把它们分进同一层,G 自动并发调度,不需要额外的调度代码。 -3. 像 `e2e_pipeline.py::implement_code`/`validate_implementation_with_own_graph` 一样,手动 `INSERT INTO work_graph_run (..., status='running')`(不经过 `plan_work_graph()`——这个图的形状是固定的、代码已知的,不需要一次额外的 LLM 规划调用,同样是"机制用代码"原则的应用)。 -4. `run_graph()` 执行:每个视角节点用**真实 schema 约束的结构化输出**(`{"type": "object", "required": ["category", "detail"]}`),不再像现有 `real_validator_perspective` 那样手工解析"CATEGORY:/DETAIL:"文本行——`work_graph_orchestrate._make_run_fn` 走的是单节点 dynamic-workflow 脚本机制(`agent(prompt, {{ agentType, schema }})`),这条路径已经被 2026-08-01 的真实调用验证过 `structured_output` 真的能拿到(`work_graph_orchestrate.py` 模块 docstring 有记录),跟 `work_graph_plan.py` 那条"`--json-schema` 对 `--agent` 无效"的已知限制是两回事,不受它影响。 -5. `_tally_perspectives([outputs["perspective-" + name] for name in perspectives])`,返回值形状跟现有 `judge_root_cause` 完全一致(`{"root_cause_category", "root_cause_detail", "judgments"}`),调用方不需要区分。 - -每个视角节点获得独立、干净的上下文(验收标准 3):`_ensure_node_row`/`run_work_node` 本身就是每个 node_exec_id 一次全新的 workflow 脚本渲染 + 独立子进程调用,`input_payload` 只含 `{"node_id": ...}`,视角提示词通过 `build_prompt` 的闭包参数传入,节点之间不共享对话历史——这是 `run_graph()` 现有机制的天然属性,不需要额外设计。 - -## 4. `org_level_loop.run_fix_loop` 迁移(FR-5,唯一真实生产场景) - -现状: - -```python -perspective_fns = [root_cause_trace.real_validator_perspective(...) for name in (...)] -judgment = root_cause_trace.judge_root_cause(perspective_fns) -``` - -改为: - -```python -judgment = root_cause_trace.judge_root_cause_via_work_graph( - conn, task_id, target_folder_path, bug_description, context, -) -``` - -`real_validator_perspective`/闭包版 `judge_root_cause` **不删除**——需求文档第 8 节开放问题已问过"要不要保留作为轻量备选",答案是保留:`src/test_root_cause_trace.py` 现有单测(`JudgeRootCauseTest`/`RealValidatorPerspectiveParsingTest`)覆盖的是判定规则本身的正确性,这些测试不需要真实 `claude` 子进程调用就能跑(用纯 Python 闭包 mock),继续作为规则正确性的快速回归保障(`_tally_perspectives` 被两边共用,这些测试事实上也在验证 Work Graph 版本的判定逻辑);闭包版本本身作为"不需要 worktree/Work Graph 开销的场景"的备选实现继续存在,不强制废弃。 - -## 5. 测试改造范围(真实影响面,如实记录) - -`src/test_org_level_loop.py` 现有 3 个测试全部通过 mock `org_node_trigger.trigger_org_node`(拦截"CATEGORY:"提示词)来伪造三视角判断——这个 mock 边界在迁移后失效(新实现走 `work_graph_execute.execute_work_graph`,不再调用 `trigger_org_node` 做视角判断)。必须同步把这三个测试的 mock 边界改成 `work_graph_execute.write_workflow_script`/`execute_work_graph`(跟 `test_work_graph_orchestrate.py` 现有模式一致),`org_node_trigger.trigger_org_node` 的 mock 保留但只用于 fixer/verify 两次调用(迁移范围明确不包括这两处)。这是本次改动风险最高的一步——`run_fix_loop` 是真实生产闭环的一部分,测试改造必须逐条比对新旧断言等价,不能只是"能跑通"就算数。 - -## 6. 不实现的部分(诚实记录,对应需求文档非目标 + FR-4 的"应该"优先级) - -- **FR-4(`verify_fn` 通用对接)不在本轮实现**:把评审团机制包装成一个可以传给 `run_graph(verify_fn=...)` 的工厂函数,用于升级 `_check_prd_consistency` 这类现有单一验证器场景——需求文档标注"应该"而非"必须",且"哪些验证点该升级成评审团"本身是待定的判定标准(需求文档第 8 节开放问题未回答)。本轮只交付 FR-5(根因溯源场景本身)这一个"必须"级别的真实落地点,`judge_root_cause_via_work_graph` 的函数签名设计上保持通用(视角 framing/数量可参数化),为未来接 FR-4 留出空间,但不在本轮消费它。 -- **视角节点并发时间收益的实测**(验收标准 4):G 已实现,`topological_layers()` 会自动把三个视角节点分进同一层走 `ThreadPoolExecutor`,机制上必然并发;但"在真实 `claude -p` 调用下测出时间重叠"需要 `RUN_REAL_AGENT_E2E=1` 真实调用,不在 mock 测试范围内验证,留给后续真实运行观察确认(跟 G 自己的 `RunGraphConcurrencyTest` 用 mock+`time.sleep` 验证并发的方式已经覆盖了"机制确实并发"这件事,不需要为 F 重新证明一遍 G 的能力)。 - -## 7. 回答需求文档第 8 节开放问题 - -- `judge_root_cause`(闭包版)保留作为轻量备选,不废弃(第 4 节)。 -- FR-4 接口设计:本轮不实现,见第 6 节。 -- 除根因溯源外还有哪些验证点该升级:本轮不判定,留待 FR-4 一起决定。 -- 视角数量 N 是否可配置:`judge_root_cause_via_work_graph`/`_tally_perspectives` 设计上已经是对任意数量视角通用的(不硬编码 3),根因溯源场景固定传 3 个,未来场景可以传不同数量而不需要改动这两个函数本身。 diff --git a/docs/todo/verifier-fan-out-jury-requirements.md b/docs/todo/verifier-fan-out-jury-requirements.md deleted file mode 100644 index 39eba7a..0000000 --- a/docs/todo/verifier-fan-out-jury-requirements.md +++ /dev/null @@ -1,69 +0,0 @@ -# 三视角评审团——Work Graph Fan-out 实现——需求文档 - -**日期**: 2026-08-05 -**性质**: 需求文档(BRD/PRD 层面),不含详细技术设计。回应 `docs/todo/graph-engineering-open-items.md` 条目 F 的确认结果——"三视角评审团可以做,做成 Work Graph 的 fan-out 图来实现"。 -**依赖**:本文档描述的 fan-out 验证节点若要真正获得"多视角同时判断"的时间收益,强依赖 `docs/work-graph-parallel-execution-requirements.md`(条目 G)的并行执行能力——没有 G,把 `judge_root_cause` 现有的顺序调用改成 Work Graph fan-out 反而更慢(多了 Work Node 调度开销,却没有并行收益)。建议 G 先进入实现或至少详细设计定型后,再排期本文档。 -**状态**: 草案,待评审。 - -## 1. 背景 - -`src/root_cause_trace.py::judge_root_cause()` 已经实现了 14 步版"多视角差异化校验"模式的核心逻辑: - -```python -def judge_root_cause(perspective_fns): - """要求 >= 2/3 视角一致才定案 ... 否则 raise InconclusiveEscalateError""" - judgments = [fn() for fn in perspective_fns] - ... -``` - -三个视角(正确性/需求完整性/Skill 系统性缺陷)目前只用在 Validator 的 `test_failed` 根因溯源场景,且是**纯 Python 函数式实现**——`perspective_fns` 是一组闭包,`judge_root_cause` 一个一个顺序调用它们(`judgments = [fn() for fn in perspective_fns]` 是一个普通的列表推导,不是并发调用),每个 `fn()` 内部大概率是一次独立的 `claude -p` 调用(`real_validator_perspective` 函数名暗示了这一点)。 - -这次确认的方向:把这套机制从"Python 函数顺序调用三个闭包"升级为"Work Graph 里一张真正的菱形拓扑"——fan-out 出 N 个独立的验证 Work Node(每个节点携带自己的视角提示词,是一次独立的 Work Node 执行,而不是一个 Python 闭包),fan-in 节点做投票/汇总。这跟综述版对照笔记(`docs/reference/graph-engineering-vs-loop-engineering-overview.diff-with-graphlemon.md` §2)指出的"这正好对应文章第 08 章'评审团'式多视角交叉审"是同一个模式,只是要把它从函数调用升格为图结构。 - -## 2. 目标 - -1. 把三视角(或未来可能扩展到更多视角)评审团实现成一张真正的 Work Graph fan-out/fan-in 菱形:一个 split 节点(或直接由调用方给出待评审对象)→ N 个并行的独立验证 Work Node(各自视角、各自独立的 `claude -p` 调用、各自独立的持久化状态) → 一个 fan-in 节点汇总投票结果。 -2. 这套 fan-out 评审团机制要能被**不同的验证场景复用**,不只是 Validator 的根因溯源——`run_graph()` 现有的 `verify_fn`(单一验证器 + 失败重试一次)应该能够选择性地升级为这套多视角 fan-out 验证,用于高风险节点(比如 SDD 审核、代码变更审核)。 -3. 每个视角的验证节点获得**独立、干净的上下文**(呼应 5 步版对照笔记已确认的"Verifier 需要干净上下文"这条原则)——不是同一个对话里问三次不同角度的问题,而是三次完全独立的 Work Node 执行。 - -## 3. 非目标 - -1. **不要求所有验证点都升级成 fan-out 评审团**。这是一种"重打法",只用于高风险节点,大多数验证场景继续用现有的单一 `verify_fn`(一个验证器 + 失败重试一次)就够了——具体"高风险"的判定标准留待设计阶段确定,但原则上默认走轻量路径,评审团是可选升级,不是新的默认行为。 -2. **不要求第一版就废弃 `root_cause_trace.judge_root_cause` 的现有 Python 函数式实现**。是否完全替换、还是保留作为"不需要 worktree/轻量场景"的备选实现,留待设计阶段确定(见第 8 节开放问题)。 -3. **不在本文档定案 fan-out 验证节点的并发触发机制细节**。具体怎么并行触发这几个验证节点,是条目 G 的职责范围,本文档只提出"这几个节点需要能并行执行"这个需求,不重复 G 已经在做的设计工作。 - -## 4. 功能需求 - -| # | 需求描述 | 优先级 | -|---|---|---| -| FR-1 | 设计一种可复用的 `graph_manifest` 模式(菱形拓扑:1 个输入节点或直接的调用方输入 → N 个独立视角验证节点 → 1 个汇总节点),可以被任何需要"多视角评审团"式验证的调用方复用,不是针对根因溯源场景写死的一次性结构。 | 必须 | -| FR-2 | N 个视角验证节点的具体视角内容(提示词)需要能按调用场景参数化——根因溯源场景用"正确性/需求完整性/Skill 缺陷"三视角,未来其他场景(比如代码审查)可能需要"正确性/安全/性能"这类不同的视角组合,机制本身要跟具体视角内容解耦。 | 必须 | -| FR-3 | 汇总节点需要保留现有 `judge_root_cause` 已经验证过的判定规则——`>= 2/3` 视角一致才定案,不足则升级(`InconclusiveEscalateError` 的等价行为),这条规则本身经过实战验证,不需要重新设计,只需要把它从"Python 函数内的逻辑"迁移成"fan-in 节点的判定逻辑"。 | 必须 | -| FR-4 | `run_graph()` 的 `verify_fn` 接口需要能够对接这种 fan-out 评审团实现——具体是扩展 `verify_fn` 的调用约定,还是提供一个新的 `verify_fn` 工厂函数(生成一个内部会触发 fan-out 评审团的 `verify_fn`),留待设计阶段确定,但调用方(比如 `e2e_pipeline.py::plan_documents` 的 `_prd_consistency_verify_fn`)不应该需要感知内部是单一验证器还是评审团。 | 应该 | -| FR-5 | Validator 现有的 `test_failed` 根因溯源场景要能迁移到这套新机制上,作为第一个真实落地场景,验证机制本身可用,而不是只停留在通用接口设计层面。 | 必须 | - -## 5. 验收标准 - -1. 给定一个需要根因溯源的模拟场景,用新的 fan-out 评审团机制跑一遍,验证三个视角节点各自作为独立的 Work Node 执行(能在 `execution_event`/日志里看到三次独立的节点执行记录,而不是一次函数调用内部的三次闭包调用)。 -2. 验证判定规则等价性:构造"2/3 一致""不足 2/3"两种场景,确认新机制的判定结果跟现有 `judge_root_cause` 单测覆盖的场景给出相同的结论。 -3. 验证视角节点确实使用独立、干净的上下文——每个视角节点的 `input_payload` 只包含它需要判断的内容,不包含其他视角节点的推理过程。 -4. 在 G(并行执行)就绪后补充验证:三个视角节点的执行时间有重叠(不是排队执行)。 - -## 6. 约束与假设 - -- 复用现有的 Work Node 执行机制(`work_node_run.run_work_node`/`work_graph_execute.execute_work_graph`),每个视角节点是一次独立的单节点 workflow 脚本调用,跟现有其他 Work Node 没有本质区别。 -- 复用现有的 `graph_manifest`/`run_graph()` 执行入口,不新建专门的"评审团执行器"——评审团只是 `graph_manifest` 的一种特定拓扑形状(菱形),不需要新的执行引擎能力(依赖 G 提供的并行触发能力,但执行入口本身还是 `run_graph()`)。 -- `root_cause_trace.judge_root_cause` 现有的单测(如果存在)应该被保留或迁移,作为判定规则本身正确性的回归保障,不因为改成图结构就失去测试覆盖。 - -## 7. 风险 - -- **强依赖 G,排期上不能先于 G**:如果 G 推迟,这条也要跟着推迟,或者先落地"fan-out 但暂时串行执行"的过渡版本(功能正确但没有时间收益),需要跟用户确认是否接受这种过渡状态,还是宁可等 G 一起上。 -- **每个视角节点变成独立 Work Node 后,token 成本会上升**:现有 Python 函数式实现里,"三次独立调用"这件事本身没有变化,但包装成 Work Node 后每个节点会多一层 schema 校验往返、多一次单节点 workflow 脚本渲染的开销——这个开销预计很小,但需要在设计阶段实测确认,不是纯粹的免费升级。 -- **"高风险"判定标准如果定得太宽,会导致大量场景都触发评审团**,造成不必要的成本增长——需要在设计阶段给出明确、克制的判定标准,而不是默认"能用评审团就用评审团"。 - -## 8. 开放问题(留给详细设计阶段回答) - -- [ ] `root_cause_trace.judge_root_cause` 的 Python 函数式实现要不要保留作为轻量级备选(比如单元测试场景、不需要真实 `claude` 子进程调用的场景),还是完全被 Work Graph 版本取代? -- [ ] FR-4 的具体接口设计——`verify_fn` 扩展方式。 -- [ ] 除了 Validator 根因溯源,哪些验证点要升级成评审团(SDD 审核?代码变更审核?)——"高风险"判定标准怎么定,是节点类型固定标注还是运行时动态判断? -- [ ] 视角数量 N 是否需要可配置(不同场景可能需要 2 个视角或 5 个视角,不一定总是 3 个)? diff --git a/docs/todo/work-graph-parallel-execution-design.md b/docs/todo/work-graph-parallel-execution-design.md deleted file mode 100644 index ed6fe2e..0000000 --- a/docs/todo/work-graph-parallel-execution-design.md +++ /dev/null @@ -1,277 +0,0 @@ -# Work Graph 节点并行执行——详细设计(SDD) - -**日期**: 2026-08-05 -**性质**: 详细设计,对应需求文档 `docs/work-graph-parallel-execution-requirements.md`(已确认,核心优先级)。本设计逐条回答该需求文档第 8 节的开放问题,并给出可直接实施的接口/算法/测试方案。 -**依赖代码**:`src/work_graph_orchestrate.py`(`run_graph`/`topological_order`/`_ensure_node_row`/`_make_run_fn`/`_verify_and_maybe_regenerate`)、`src/work_node_run.py`(`run_work_node`/`create_worktree`)、`src/backend_server.py`(`conn_lock` 先例)。 -**状态**: 设计草案,待评审后实施。 - ---- - -## 1. 设计概览 - -现状:`run_graph()`(`work_graph_orchestrate.py:196`)用 `topological_order()` 把 `graph_manifest` 拍平成一条扁平列表,纯 Python `for` 循环逐节点执行。 - -改造后:新增 `topological_layers()`,把 Kahn 算法的中间产物(每一轮 in-degree 归零的节点集合)保留为"层"而不是拍平;`run_graph()` 改成"层间顺序、层内并发"——同一层内的节点用 `ThreadPoolExecutor` 并发触发各自的 `run_work_node()`,层与层之间保持屏障(下一层要等上一层全部完成)。 - -**不改变的部分**(刻意保持稳定,降低改动面): -- 每个节点仍然是"渲染成一个单节点 workflow 脚本 + 独立 `claude -p` 子进程调用"(`_make_run_fn`/`_render_node_script` 不变)。 -- `run_work_node()` 的状态机、断点续跑逻辑、重试/配额等待逻辑完全不变。 -- `run_graph()` 的对外签名和返回值格式(`{node_id: output_payload}`)不变(需求文档 FR-9)。 -- `verify_fn` 的语义不变(需求文档 FR-8)。 - -## 2. `topological_layers()`:新增分层拓扑排序 - -```python -def topological_layers(graph_manifest): - """Kahn 算法的分层版本:返回 List[List[node]],每个内层 list 是同一轮 - in-degree 归零的节点集合(互相之间保证没有依赖,可以并发执行)。 - 层内按 node_id 排序(保持确定性,便于测试断言)。 - 依赖来源、报错行为跟现有 topological_order() 完全一致(合并 - depends_on 和 edges 两个来源;有环或引用未知 node_id 时 raise ValueError)。 - """ - nodes = {n["node_id"]: n for n in graph_manifest["nodes"]} - deps = {node_id: set(n.get("depends_on") or []) for node_id, n in nodes.items()} - for edge in graph_manifest.get("edges", []): - to_id, from_id = edge.get("to"), edge.get("from") - if to_id in deps and from_id in nodes: - deps[to_id].add(from_id) - - in_degree = {node_id: len(d) for node_id, d in deps.items()} - dependents = {node_id: [] for node_id in nodes} - for node_id, d in deps.items(): - for dep in d: - if dep in dependents: - dependents[dep].append(node_id) - - layers = [] - current = sorted(node_id for node_id, degree in in_degree.items() if degree == 0) - seen_count = 0 - while current: - layers.append([nodes[node_id] for node_id in current]) - seen_count += len(current) - next_layer = set() - for node_id in current: - for dependent in dependents[node_id]: - in_degree[dependent] -= 1 - if in_degree[dependent] == 0: - next_layer.add(dependent) - current = sorted(next_layer) - if seen_count != len(nodes): - raise ValueError( - "graph_manifest has a dependency cycle or a depends_on/edge references an unknown node_id" - ) - return layers -``` - -`topological_order()` **保留**(其他调用方,如 `e2e_pipeline.py:233` 直接用它拿扁平顺序写 artifact),但改成对 `topological_layers()` 的薄封装,保证行为/顺序完全不变: - -```python -def topological_order(graph_manifest): - return [node for layer in topological_layers(graph_manifest) for node in layer] -``` - -(需要跑一次现有 `test_work_graph_orchestrate.py` 里 `topological_order` 的既有单测,确认拍平后的顺序逐字节相同——这两版算法的排序规则一致,理论上不会有差异,但改动了实现路径,必须实测验证不是"理论上一致"就够。) - -## 3. SQLite 并发写入:`_LockedConn` 代理 - -**问题**:`run_work_node()`/`_ensure_node_row()`/`execution_log.append_log()` 等函数内部有大量分散的 `conn.execute(...)` + `conn.commit()` 调用点。层内并发后,这些调用可能从多个线程同时打到同一个 `sqlite3.Connection` 对象上。 - -**关键判断**:SQLite 本身同一时刻只允许一个写者(这是 SQLite 的架构限制,不是 Python 线程模型能绕开的),所以不需要设计"细粒度多把锁",**一把锁保护整个连接的读写足够**——真正的并发收益来自于"锁只在单条 SQL 语句执行期间持有,`claude -p` 子进程调用(决定墙钟时间的部分)完全不持锁",而不是来自"多个线程能同时写 SQLite"(这本来就不可能)。 - -**方案**:不去逐个包裹现有几十处 `conn.execute()` 调用点(改动面大、容易漏改),而是引入一个跟 `sqlite3.Connection` 接口兼容的轻量代理对象,在并发路径上把 `conn` 参数替换成这个代理——调用方代码(`work_node_run.py`/`work_graph_orchestrate.py`/`execution_log.py`)完全不用改一行: - -```python -class _LockedConn: - """sqlite3.Connection 的线程安全代理:execute/commit/executemany 各自 - 持锁执行,可以直接替代 conn 参数传给现有代码,调用方无需感知。复用 - backend_server.py::_Server 已经验证过的"共享连接 + threading.Lock"模式, - 只是把锁的粒度做到了每条语句级别(backend_server 现有的 conn_lock 只 - 保护 server_close() 一处,不是通用查询锁——本类是把同一个思路做成可 - 在任意并发场景复用的通用工具,两者不冲突,backend_server 未来可以直 - 接换成这个类)。 - """ - def __init__(self, conn, lock=None): - self._conn = conn - self._lock = lock or threading.Lock() - - def execute(self, *args, **kwargs): - with self._lock: - return self._conn.execute(*args, **kwargs) - - def executemany(self, *args, **kwargs): - with self._lock: - return self._conn.executemany(*args, **kwargs) - - def commit(self): - with self._lock: - return self._conn.commit() - - def __getattr__(self, name): - # cursor()/rollback()/close() 等未显式包装的方法透传给底层连接, - # 调用方目前不会从多线程并发调用这些(没有并发路径用到它们)。 - return getattr(self._conn, name) -``` - -`run_graph()` 在进入并发执行前,用一把 `threading.Lock()` 包出一个 `_LockedConn` 实例,传给每个并发任务内部实际调用的 `_ensure_node_row`/`run_work_node`/`_make_run_fn`(后者内部会调用 `execution_log.append_log`)——注意:**只有并发的那一层用 `_LockedConn`,层外(比如 `run_graph()` 自己开头调用 `ensure_placeholder_skill`)继续用原始 `conn`,因为那部分本来就不在并发路径上,没必要多一层锁的开销**。 - -## 4. Worktree 创建竞态 - -`work_node_run.py::create_worktree()`(`git worktree add -b `)在并发场景下可能对同一个 repo 的 `.git/worktrees/` 元数据产生竞态——这是**推测**,需求文档已标注"未经验证"。设计上采用保守做法:给 `create_worktree()` 包一把**进程级模块锁**(跟 SQLite 锁分开,因为这个锁保护的是文件系统/git 内部状态,不是数据库): - -```python -_worktree_creation_lock = threading.Lock() - -def create_worktree(repo_path, node_exec_id): - with _worktree_creation_lock: - ... # 原逻辑不变 -``` - -这个锁只包住 `git worktree add` 这一条命令本身(毫秒级),不包住节点其余的执行时间,所以即使锁確实是必要的,对整体并行收益的影响可以忽略——`claude -p` 调用(秒级到分钟级)才是墙钟时间的主体。**验证方式**:写一个真实的并发 `create_worktree` 调用测试(多线程同时对同一个临时 git 仓库调用),先不加锁跑一遍看是否真的出错,出错则保留锁、不出错也保留锁(保守优先,反正代价极小)。 - -## 5. 并发调度器:`run_graph()` 改造 - -```python -import concurrent.futures - -_DEFAULT_MAX_WORKERS = 4 # 保守默认值,见第 9 节 - -def run_graph( - conn, run_id, org_node, skill_id, target_folder_path, graph_manifest, - build_prompt, input_schema, output_schema, timeout=600, needs_worktree=False, - verify_fn=None, sleep=time.sleep, now=time.time, max_workers=_DEFAULT_MAX_WORKERS, -): - ensure_placeholder_skill(conn, skill_id, org_node) - effective_needs_worktree = needs_worktree or paths.is_self_target(target_folder_path) - layers = topological_layers(graph_manifest) - outputs = {} - db_lock = threading.Lock() - locked_conn = _LockedConn(conn, db_lock) - - for layer in layers: - if len(layer) == 1: - # 单节点层:不走线程池,直接同步执行——省掉线程池调度开销, - # 也让现有单节点场景(目前观察到的真实 graph_manifest 全都是 - # 单节点链)行为路径尽量贴近改造前,降低回归风险。 - node = layer[0] - outputs[node["node_id"]] = _run_one_node( - conn, run_id, org_node, skill_id, target_folder_path, node, outputs, - build_prompt, input_schema, output_schema, timeout, - effective_needs_worktree, verify_fn, sleep, now, - ) - continue - - worker_count = min(max_workers, len(layer)) - errors = [] - with concurrent.futures.ThreadPoolExecutor(max_workers=worker_count) as pool: - futures = { - pool.submit( - _run_one_node, locked_conn, run_id, org_node, skill_id, target_folder_path, - node, outputs, build_prompt, input_schema, output_schema, timeout, - effective_needs_worktree, verify_fn, sleep, now, - ): node - for node in layer - } - # 等整层全部完成(成功或失败)再继续——层内节点互不依赖, - # 一个节点的失败不该取消已经在跑的兄弟节点(需求文档 FR-5)。 - done, _ = concurrent.futures.wait(futures, return_when=concurrent.futures.ALL_COMPLETED) - for future in done: - node = futures[future] - try: - outputs[node["node_id"]] = future.result() - except Exception as e: - errors.append((node["node_id"], e)) - - if errors: - # 聚合报错:全部记录,抛出的异常携带完整清单,不只是"第一个", - # 方便排查同一层里到底几个节点失败、分别是什么原因。 - raise WorkGraphLayerError(errors) - - return outputs - - -def _run_one_node( - conn, run_id, org_node, skill_id, target_folder_path, node, outputs, - build_prompt, input_schema, output_schema, timeout, needs_worktree, verify_fn, sleep, now, -): - """原 run_graph() 循环体单节点逻辑原样保留,只是抽成一个独立函数, - 好让同步路径(单节点层)和并发路径(多节点层)共用同一份实现, - 不重复维护两份"跑一个节点+可选 verify_fn"的逻辑。""" - node_id = node["node_id"] - node_exec_id = _ensure_node_row( - conn, run_id, node_id, org_node, skill_id, - "work_graph_node_input.schema.json", "work_graph_node_output.schema.json", - {"node_id": node_id}, - ) - prompt = build_prompt(node, outputs) - run_fn = _make_run_fn(conn, target_folder_path, node_exec_id, org_node, prompt, output_schema, timeout) - output = work_node_run.run_work_node( - conn, node_exec_id, {"node_id": node_id}, input_schema, output_schema, run_fn, - needs_worktree=needs_worktree, repo_path=target_folder_path, sleep=sleep, now=now, - ) - if verify_fn is not None: - output = _verify_and_maybe_regenerate( - conn, run_id, org_node, skill_id, target_folder_path, node, node_id, prompt, - output, input_schema, output_schema, timeout, needs_worktree, verify_fn, sleep, now, - ) - return output - - -class WorkGraphLayerError(Exception): - """一层内一个或多个节点执行失败——携带 [(node_id,原始异常), ...] 完整 - 清单,调用方可以按需检查是不是全是同一种可恢复错误。""" - def __init__(self, errors): - self.errors = errors - summary = "; ".join("{}: {}".format(node_id, e) for node_id, e in errors) - super().__init__("{} node(s) failed in one layer: {}".format(len(errors), summary)) -``` - -**关键设计取舍——`build_prompt(node, outputs)` 里的 `outputs` 读取时机**:层内并发时,`outputs` 字典在整层的所有 `_run_one_node` 调用里都是**同一个共享字典**,但层内节点互相没有依赖,`build_prompt` 不会读到"同层兄弟节点"的输出(拓扑保证),只会读到"更早的层"已经写完的输出——这些更早层的写入在当前层开始前已经全部完成(层屏障保证),所以层内并发读 `outputs` 是安全的,不需要给 `outputs` 字典本身加锁(Python 字典的读操作本身也是线程安全的,只要没有并发写同一个 key,而层内不同节点写的是不同的 `node_id` key,不冲突)。**每个节点各自的 `outputs[node_id] = ...` 写入统一放到层执行完、`concurrent.futures.wait` 返回之后、在主线程里做**(上面伪代码里 `for future in done: outputs[node["node_id"]] = future.result()`),不在子线程里直接写共享字典,进一步避免任何并发写字典的疑虑,即使实际上没有真正的 key 冲突。 - -## 6. 错误/配额处理的真实限制(诚实记录,不回避) - -`work_node_run.run_work_node()` 内部对配额限制(`UsageLimitReachedError`→`_UsageLimitSignal`)的处理是**在函数内部原地 sleep-retry**,退避时间可以长达 30 分钟一次、session 级超时 8 小时、weekly 级超时 9 天(`work_node_run.py:42-43`)。这意味着:如果同一层里节点 A 触发了配额等待、节点 B 几秒内就正常完成,`concurrent.futures.wait(..., return_when=ALL_COMPLETED)` 会让整层卡在等 A——B 的结果虽然已经算出来了,但因为层屏障语义(下一层要等这一层全部完成),实际上整个 `run_graph()` 调用也会被拖住。 - -**这是本设计接受的限制,不是本轮要解决的问题**:Python 线程一旦启动就没有干净的"取消并回收"机制(`Future.cancel()` 只能取消还没开始跑的任务),要让 B 提前继续、A 在后台继续等配额而不阻塞整层,需要给 `run_work_node()` 引入可中断/可分离的配额等待机制——这是比本次改造大得多的另一个课题,不在本次范围内(需求文档"非目标"第 1 条已经声明"不做完整的数据流式调度器",这个限制是同一类取舍的自然延伸)。如果未来这个限制成为真实瓶颈,需要单独立项评估。 - -## 7. 断点续跑兼容性——不需要新逻辑 - -`run_work_node()` 已有的"已 `done` 节点短路返回持久化输出"逻辑对并发路径天然兼容,不需要改动:进程重启后重新调用 `run_graph()`,`topological_layers()` 重新计算分层,对每一层内的节点,`_run_one_node()` 里的 `run_work_node()` 调用会对"已完成"的节点立即返回持久化结果(几乎不占用线程池工作时间),对"未完成"的节点正常重新执行——同一层里有的节点秒回、有的节点真正重跑,线程池天然处理这种混合情况,不需要额外判断"这层要不要重跑"。 - -## 8. 可观测性 - -`_log_event`(`work_graph_orchestrate.py:74`)现有的 `node_start`/`node_completed`/`node_failed`/`quota_limit` 四类事件保持不变,每条事件本身已经带 `node_exec_id`,事件的时间戳(`execution_log.append_log` 底层写入时机)已经足够回答"这次运行到底并发了多少节点"——只要看同一个 `run_id` 下多个 `node_start` 事件的时间戳有没有重叠,不需要新增专门的"分组开始/分组结束"事件类型(呼应需求文档开放问题第 4 条:复用现有事件、靠时间戳重叠反推,不新增事件类型,保持改动面最小)。 - -## 9. 并发度默认值 - -`_DEFAULT_MAX_WORKERS = 4`。理由:现在观察到的真实 `graph_manifest` 规模是个位数节点,4 是一个不会让"一次任务同时拉起过多 `claude` 子进程"的保守值,同时足够覆盖当前能想到的 fan-out 场景(比如 Planner 未来 PRD 阶段 3-5 个场景并行)。作为 `run_graph()` 的一个具名参数(而不是硬编码常量或环境变量),调用方可以按场景覆盖,但不新增管理后台配置项(需求文档非目标已声明不做这类配置暴露,YAGNI——等真的有需要按项目/按 Org Agent 类型区分并发度时再加)。 - -## 10. 测试计划(对应需求文档验收标准 1-7) - -| 验收标准 | 测试方式 | -|---|---| -| 1. 同层节点确实并发触发 | mock `run_fn`,每次调用记录 `(node_id, 开始时间, 结束时间)` 到共享列表(用锁保护这个记录列表本身,测试代码自己的锁,不是被测代码的),断言同层两个节点的时间区间有重叠 | -| 2. 单测断言并发+层间顺序 | 同上,另外断言"下一层任意节点的开始时间 >= 上一层全部节点的结束时间"(层屏障) | -| 3. 断点续跑:已完成节点不重跑 | 第一次调用 mock 让某层其中一个节点抛异常模拟中断(`WorkGraphLayerError` 传播出来),检查 DB 里其余节点确实落库为 `done`;第二次调用同一个 `run_graph()`,断言 `run_fn` 没有被再次调用去跑那些已 `done` 的节点(用调用计数 mock 断言) | -| 4. SQLite 并发写入无异常 | 用真实 `:memory:`(或临时文件)SQLite 库跑一个多节点并发层,不 mock `conn`,跑多轮(并发问题往往概率复现)确认不抛 `database is locked` | -| 5. worktree 并发创建互不冲突 | 真实临时 git 仓库,多线程调用 `create_worktree`,断言 `git worktree list` 输出的路径数量和唯一性符合预期 | -| 6. 配额限制 + 正常完成共存 | mock 一个节点的 `run_fn` 触发 `_UsageLimitSignal` 且配置极短的超时(依赖注入的 `sleep`/`now`,不真的等待),另一个节点正常返回,断言正常节点的输出被保留在最终 `outputs` 里,且整体调用最终正确抛出/传播配额相关信号 | -| 7. 既有测试兼容性 | 跑 `src/test_work_graph_orchestrate.py`、`src/test_work_graph_execute.py`、`src/test_e2e_pipeline.py` 全部既有用例,逐个确认通过;对断言"严格串行"这一实现细节的用例(如果存在)需要单独识别并更新断言方式(改成断言"层间顺序"而不是"节点间顺序") | - -## 11. 实施步骤建议(拆成可独立验证的小步) - -1. `topological_layers()` 新增 + `topological_order()` 改写成基于它的薄封装,跑既有测试确认零回归(不改 `run_graph()`,先证明分层算法本身正确)。 -2. 引入 `_LockedConn`,先只在新代码路径里用,不碰现有串行路径。 -3. `create_worktree()` 加锁,写第 10 节测试用例 5 验证是否真的需要(哪怕结论是"其实不需要",锁本身开销极小,可以保留作为防御性设计)。 -4. `run_graph()` 改造成层级调度(第 5 节的完整实现),单节点层走同步路径、多节点层走线程池路径。 -5. 补全第 10 节全部测试用例。 -6. 找一个真实存在多节点并行结构的 `graph_manifest` 场景做一次端到端验证(如果当前还没有这样的真实场景,构造一个人工测试场景即可,不强求等 E 落地后才能验证 G 本身)。 - -## 12. 开放问题的最终决定 - -回答需求文档 `docs/work-graph-parallel-execution-requirements.md` §8 列的开放问题: - -- **`topological_layers()` 实现方式**:新增函数,`topological_order()` 改写成基于它的薄封装(第 2 节已给出实现)。 -- **`max_workers` 默认值**:4,作为 `run_graph()` 的具名参数,不做管理后台配置项(第 9 节)。 -- **FR-5 异常聚合规则**:层内全部完成后统一收集,抛出携带完整 `[(node_id, exception), ...]` 清单的 `WorkGraphLayerError`(第 5 节),调用方可以按需检查列表内容做进一步判断(比如是否全是可重试的配额问题)。 -- **`execution_event` 是否需要新事件类型**:不需要,复用现有事件+时间戳重叠反推(第 8 节)。 -- **worktree 加锁粒度**:只锁 `create_worktree()` 内部 `git worktree add` 这一条命令,不锁整个节点执行(第 4 节)。 diff --git a/docs/todo/work-graph-parallel-execution-requirements.md b/docs/todo/work-graph-parallel-execution-requirements.md deleted file mode 100644 index 6f70eba..0000000 --- a/docs/todo/work-graph-parallel-execution-requirements.md +++ /dev/null @@ -1,76 +0,0 @@ -# Work Graph 节点并行执行——需求文档 - -**日期**: 2026-08-04(2026-08-05 更新:已确认,标记为核心优先级,进入详细设计阶段) -**性质**: 需求文档(BRD/PRD 层面),不含详细技术设计。回应对 `docs/reference/Graph Engineering with Claude_ 14-Step roadmap from 0 to graph architect (Full Course).diff-with-graphlemon.md` 开放问题第 1 条的讨论——"现在的每节点独立子进程设计,断点续跑收益是否值得放弃 fan-out 并行?能否两者兼得?"。详细设计(SDD 层面:`topological_layers()` 的具体实现、线程池调度器接口、锁的粒度)留到需求确认后单独产出。 -**状态**: ✅ 已确认。验收口径固化为一句话(2026-08-05 用户原话):"只要 Work Graph 规划了 fan-out、fan-in 的并行节点,就可以在节点触发的时候并行执行"——这与本文档 FR-1/FR-2 一致,无需改动功能需求本身。详细设计已产出,见 `docs/work-graph-parallel-execution-design.md`。 - -## 1. 背景 - -`src/work_graph_orchestrate.py::run_graph()` 目前用 Kahn 算法把 `graph_manifest` 拍平成一条扁平的拓扑序,用纯 Python `for` 循环逐个节点执行——即使两个节点在 `graph_manifest` 里互相没有 `depends_on` 关系(本可以并行),现在也是严格排队执行,跑完一个才开始下一个。 - -这是一个记录在案的、有意为之的权衡(见该模块 docstring):早先方案(`docs/graphlemon_architecture.md` §10.1)设想直接复用 Claude Code 原生动态工作流的 `parallel()`/`pipeline()` 来跑整个 Work Graph,但那要求把整张图塞进一次不透明的 `claude` 子进程调用,Python 侧在执行过程中对每个节点的完成状态完全不可见——这跟 REQ-06"进程重启后从已完成节点继续、不重跑"的断点续跑硬需求根本冲突。所以最终选择了"每个节点一次独立的 `claude -p` 子进程调用 + Python 层持久化每个节点的状态",代价是放弃了图内并行。 - -随着 Work Graph 规模变大(例如 Planner 阶段 PRD/SDD 的多场景/多方案 fan-out 候选,`docs/graphlemon_architecture.md` §6.1 已经设想过、但目前从未真正在生成的 `graph_manifest` 里出现过),纯串行执行会让原本可以同时进行的工作被迫排队,拖长任务的墙钟时间。本需求要解决的问题:**在不牺牲现有断点续跑能力的前提下,让 `graph_manifest` 里互不依赖的节点能够真正并发执行**。 - -## 2. 目标 - -1. `graph_manifest` 中没有依赖关系(无论是直接 `depends_on` 还是通过其他节点间接依赖)的节点集合,能够在同一时间窗口内并发执行,而不是被迫排队。 -2. 现有的断点续跑语义(进程重启后,已经落库为 `done` 的节点不重跑,从未完成的节点继续)在并行改造后必须继续成立,不能因为改成并发就丢失这个能力。 -3. 现有的每节点独立 worktree 隔离(self-target 场景无条件强制)在并发场景下继续正确工作,不出现多个节点互相踩写文件。 -4. 现有的单节点验证-重试机制(`verify_fn`:验证失败则原地重跑一次,验证通过才允许下游节点开始)语义保持不变。 -5. 并发规模可控,不因为一次任务的 `graph_manifest` 节点数变多就无限制地同时拉起等量的 `claude` 子进程,导致失控的 token/账单开销或系统资源耗尽。 - -## 3. 非目标(本轮明确不做) - -1. **不做完整的"随时有依赖满足就调度"的数据流式调度器**。本轮只做"按拓扑层分批 + 层内并发、层间屏障"的粗粒度并行——层边界由 Kahn 算法的天然分层给出,不追求"某个节点的依赖一满足就立刻插队执行,不必等同层其他节点跑完"这种更细粒度的最优调度。理由:现在的 Work Graph 规模是个位数到十几个节点,层级屏障和真正的最优调度之间的墙钟差异预计很小,不值得为此引入更复杂的调度器和更难验证的正确性。 -2. **不改变"每个节点是一次独立 `claude -p` 子进程调用"这个既有设计**。本需求只改变"什么时候触发这次调用"(从严格串行改成同层并发触发),不改变每次调用本身的机制(仍然是单节点 workflow 脚本 + `--json-schema` 校验),也不重新尝试把整图塞进一次 Claude 原生 `parallel()`/`pipeline()` 编排脚本。 -3. **不引入按节点选模型的策略**。`_render_node_script` 已经预留的 `model` 参数、"哪个节点该用哪个模型"这层策略是独立的开放问题(见另一份对照笔记开放问题第 3 条),本需求不涉及。 -4. **不改变 Org-level Loop 的三轮上限、loop-until-dry 机制**。本需求只影响单次 `run_graph()` 调用内部的节点调度,不涉及跨 Org Graph 的重试/收敛逻辑,也不试图让 `graph_manifest` 支持图内有环(`topological_order` 目前对有环图直接报错,这个限制维持不变)。 -5. **不承诺降低 token 成本**。并行改造换来的是墙钟时间缩短,节点总数、每个节点的调用量不变,token 总消耗预计基本持平(详见「7. 风险与已知权衡」)。 - -## 4. 功能需求 - -| # | 需求描述 | 优先级 | -|---|---|---| -| FR-1 | `run_graph()` 在拿到 `graph_manifest` 后,应能识别出可以并发执行的节点分组(同一拓扑层内、彼此无依赖关系的节点集合),而不是像现在这样拍平成一条无结构信息的顺序列表。 | 必须 | -| FR-2 | 同一分组内的多个节点应能真正并发触发各自的 `claude -p` 子进程调用,而不是等前一个节点完全结束才开始下一个。 | 必须 | -| FR-3 | 并发执行期间,每个节点各自的持久化状态写入(节点行创建、状态流转、执行事件日志)必须保持数据完整性——不能因为多线程同时写同一个 SQLite 连接而出现"database is locked"报错、脏写、或状态丢失。 | 必须 | -| FR-4 | 并发执行期间,多个节点各自需要创建 git worktree 时(`needs_worktree=True` 场景)不能相互冲突,每个节点仍然拿到一个独立、干净的工作区。 | 必须 | -| FR-5 | 若同一分组内某个节点执行失败(含配额限制 `UsageLimitReachedError`、一般执行错误、`verify_fn` 验证失败且重试后仍不通过),已经在并发执行中的其他兄弟节点应当被允许自然跑完,不因为一个节点失败就中途取消其他正在消耗资源的调用;该分组内全部节点结束后再统一判定整个 `run_graph()` 调用的最终结果(成功/失败/需要升级)。 | 必须 | -| FR-6 | 并发度必须可配置且有合理的默认上限,防止一次任务的 `graph_manifest` 节点数暴涨时同时拉起等量的 `claude` 子进程。 | 必须 | -| FR-7 | 进程在某个分组执行到一半时中断重启后,恢复执行应能正确识别"这个分组里哪些节点已经 `done`、哪些还没跑完",只重新执行未完成的部分,不重跑已完成节点——这个语义必须和现在的单节点断点续跑完全等价,只是作用范围从"单个节点"扩展到了"一个分组内的多个节点"。 | 必须 | -| FR-8 | `verify_fn` 的语义保持不变:一个节点的输出必须先通过验证(或验证失败重试后通过),该节点的结果才能进入下游节点可读取的 `outputs`;依赖该节点的下游节点所在的分组,必须等到这一验证真正完成后才能开始执行,不能提前读取未经验证的中间结果。 | 必须 | -| FR-9 | 现有调用方(`e2e_pipeline.py` 等)对 `run_graph()` 的调用方式和返回值格式(`{node_id: output_payload}`)应保持兼容,不需要因为内部改成并行调度而修改调用方代码。 | 应该 | -| FR-10 | 并行执行的分组信息(哪些节点被并发调度、每组的起止时间)应当可观测——至少体现在现有的 `execution_event`/日志机制里,方便事后排查"某次运行到底并发了多少节点、并发是否生效"。 | 应该 | - -## 5. 验收标准 - -1. 给定一个 `graph_manifest`,其中包含至少一组"两个节点互不依赖、都依赖同一个上游节点"的结构(例如 A→{B, C}→D),跑一次 `run_graph()`,能观测到 B 和 C 的 `claude -p` 子进程调用在时间上有重叠(不是 B 完全结束后 C 才开始)。 -2. 用 mock 的 `run_fn`(不真实调用 `claude`,返回固定输出,可注入人为延迟)写单元测试:断言同一分组内的节点确实并发触发(例如用一个共享计数器/时间戳断言重叠),且分组之间保持严格先后顺序(下一层的节点不会在上一层未全部完成前开始)。 -3. 模拟"进程在某个分组执行到一半时中断"(例如分组内两个节点,mock 让其中一个成功落库为 `done`、另一个抛异常模拟中断),重新调用 `run_graph()`,断言已完成的节点不会被重新执行(`run_fn` 没有被再次调用),只有未完成的节点被重新调度。 -4. 并发场景下跑一轮包含多个节点写入 SQLite 的测试,不出现 `sqlite3.OperationalError: database is locked` 或类似的并发写入异常。 -5. 模拟 `needs_worktree=True` 场景下同一分组内多个节点并发创建 worktree,断言每个节点拿到的 worktree 路径互不相同、互不冲突,且 `git worktree list` 能看到全部预期数量的 worktree。 -6. 模拟同一分组内一个节点抛 `UsageLimitReachedError`、另一个节点正常完成,断言正常完成的节点的输出被正确保留(不因为兄弟节点失败而丢失),同时整体调用仍然正确地向上抛出/传播配额限制信号。 -7. 现有 `src/test_work_graph_orchestrate.py`、`src/test_work_graph_execute.py`、`src/test_e2e_pipeline.py` 中的既有测试用例在改造后无需修改断言即可继续通过(验证行为兼容性),除非该用例本身就是在断言"严格串行"这一将被改变的实现细节(这类用例需要明确识别出来,逐一确认是否要更新)。 - -## 6. 约束与假设 - -- 复用 `src/backend_server.py::_Server` 里已经验证过的模式:共享一个 `sqlite3.connect(..., check_same_thread=False)` 连接 + `threading.Lock()` 保护并发写入,不引入连接池或每线程独立连接这类更重的方案。 -- 复用现有的 `work_node_run.py` 断点续跑机制("已 `done` 的节点短路,直接返回持久化输出"),并行改造不重新设计这一层,只是让触发多个节点执行这件事本身从串行变成并发。 -- 复用现有的每节点 worktree 隔离机制(`work_node_run.py::create_worktree`),如果发现并发创建 worktree 本身有 git 层面的竞态,允许在"创建 worktree"这一步单独加锁串行化,不要求整个节点执行都串行——真正耗时的 `claude -p` 调用部分仍然并发。 -- 默认并发上限取一个保守值(具体数字留给设计阶段结合实测确定,不在需求阶段写死),避免一次运行同时拉起过多 `claude` 子进程。 - -## 7. 风险与已知权衡 - -- **不降低 token 成本,只缩短墙钟时间**:并行改造让节点调用"同时发生"而不是排队,但总的节点数量、每个节点的 token 消耗不变。如果验收标准或后续讨论里出现"并行后应该更省钱"的预期,需要提前澄清纠正。 -- **层级屏障不是最优调度**:如果某一层里有一个节点明显比同层其他节点慢很多,下一层要等这个最慢的节点跑完才能开始,即使下一层里有些节点其实只依赖同层里已经跑完的那些节点。本需求接受这个次优性(见「3. 非目标」第 1 条),如果未来图规模变大、这个次优性变成真实瓶颈,需要另立需求评估数据流式调度器。 -- **SQLite 并发写入的锁粒度需要在设计阶段仔细确定**:过粗的锁(整个 `run_graph()` 调用期间持锁)会退化成事实上的串行,失去并行的意义;过细的锁需要更仔细地审视哪些写入操作之间存在真正的竞态。这是详细设计阶段需要重点验证的部分,不是需求阶段能定论的。 -- **worktree 创建的 git 层面竞态目前只是推测,未经验证**:需要在设计/实现阶段用真实的 `git worktree add` 并发调用做实测,确认是否真的需要额外加锁,还是 git 本身已经能安全处理并发创建。 - -## 8. 开放问题(留给详细设计阶段回答) - -- [ ] `topological_layers()` 的具体实现:是在现有 `topological_order()` 旁边新增一个函数,还是重构 `topological_order()` 本身让它返回分层结构,由调用方按需拍平? -- [ ] 并发上限(`max_workers`)默认值定多少?是否需要按 Org Agent 类型(Planner 偏文档生成、Coder 偏代码修改)给不同的默认上限?是否需要暴露成可配置项(管理后台/环境变量)? -- [ ] FR-5 的"该分组内全部节点结束后再统一判定整体结果"具体的异常聚合/优先级规则是什么(比如同组里既有配额限制又有一般执行错误,该抛出哪一个、还是都记录?)? -- [ ] 是否需要在 `execution_event` 里新增一种事件类型来标记"分组开始/分组结束",还是复用现有的 `node_start`/`node_completed` 事件、靠时间戳重叠反推并发情况就够用? -- [ ] worktree 创建竞态如果实测确认存在,加锁的粒度是"整个 `create_worktree` 函数"还是可以更细(比如只锁 `git worktree add` 这一条命令的调用)? diff --git a/docs/todo/work-graph-time-travel-debugging-design.md b/docs/todo/work-graph-time-travel-debugging-design.md deleted file mode 100644 index 178d5e8..0000000 --- a/docs/todo/work-graph-time-travel-debugging-design.md +++ /dev/null @@ -1,61 +0,0 @@ -# Work Graph 执行历史 Time-Travel 调试——详细设计(SDD) - -**日期**: 2026-08-06 -**性质**: 详细设计,对应需求文档 `docs/todo/work-graph-time-travel-debugging-requirements.md`(条目 D,已确认)。回答该需求文档第 8 节的开放问题。 -**状态**: 设计草案,实施中,后端+控制台一起做(用户 2026-08-06 确认)。 - -## 1. `ux_work_graph_run_active` 比需求文档描述的风险更小 - -`src/schema.py:134` 那条唯一索引是**带 WHERE 条件的部分索引**:`WHERE status IN ('planning', 'running', 'merging', 'verifying')`——'done'/'failed'/'blocked' 状态的 run 完全不受它约束。这意味着"从一个已经跑完很久的 task 分叉调试"(最常见的真实场景)根本不会撞上这条约束。 - -**v1 范围裁剪(跟用户讨论后确认)**:只支持从**父 run 已经到达终态**(`done`/`failed`/`blocked`)的节点分叉。这样新 fork 出的 run 完全落在这条部分索引的约束范围之外,不需要碰这条索引本身,也不需要梳理"还有哪些代码路径隐式假设了一个 task_id+org_node 只有一个活跃 run"这个更大的排查面(需求文档风险第 1 条点名的工作)。从还在跑的父 run 分叉,留给以后有真实需求再做。 - -## 2. 关键设计决定:替换输出,不重新提示(解决 FR-1 的歧义) - -需求文档 FR-1 原文:"人工可以指定新的输入(覆盖原 `input_payload` 的部分或全部字段),触发从这个节点开始的重新执行。" 但 `input_payload` 在现有代码里几乎总是一个薄壳(`{"node_id": ...}`)——真正的提示词内容来自调用方各自不同的机制(Planner 靠 `artifact_type` 查 `_DOC_PROMPTS`,E 的通用路径靠节点自带的 `task_prompt` 字段,F 的评审团靠运行时闭包)。要让分叉机制对这些不同调用方都通用,"覆盖 input_payload 重新触发 LLM 调用"这条路走不通,会绑死在某一种调用方约定上。 - -**决定**:分叉点这个节点本身**不重新执行**——它的 `output_payload` 被人工提供的新内容直接替换(验收标准 1 的例子"换一个不同的 PRD 内容"本来就是这个意思:人工已经有了想要的新内容,不是要求 Agent 重新生成一个不一样的版本)。只有**严格下游**的节点才会真的重新跑一次 `claude -p`。这个决定让分叉机制完全不需要知道原图是哪个调用方产出的、用的是哪种 `build_prompt` 约定——只需要持久化的 `graph_manifest` 和各节点的 `output_payload`,这两样任何一条 Work Graph run 都有。 - -## 3. 实现机制:100% 复用 REQ-06 断点续跑,零改动执行引擎 - -`work_node_run.run_work_node()` 已有的逻辑:一个 `work_node_execution` 行如果 `status='done'`,直接返回持久化的 `output_payload`,不调用 `run_fn`。`work_graph_orchestrate.run_graph()`/`topological_layers()` 对此完全无感——它只是正常跑一遍拓扑分层调度,每层每个节点各自决定"是短路返回还是真的执行"是 `run_work_node()` 内部的事。 - -这意味着 `fork_work_graph_run()` 不需要裁剪 `graph_manifest`、不需要给 `run_graph()` 传一个"预填充的 outputs"参数(`run_graph()` 也没有这个参数,不需要新加)——**只需要在调用 `run_graph()` 之前,往新 `run_id` 下预先插入一批 `work_node_execution` 行**: - -- 分叉点本身:`status='done'`,`output_payload` = 人工提供的新内容。 -- 分叉点的**非下游**节点(严格下游子图之外的所有节点,包括分叉点的上游和跟分叉点无关的兄弟分支):`status='done'`,`output_payload` = 原始 run 里这个节点已经产出的内容,原样复制。 -- 分叉点的**严格下游**节点:什么都不插入,留给 `run_graph()` 正常调度、真实执行。 - -`run_graph()` 拿到的还是**完整的原始 `graph_manifest`**(不裁剪),对每个节点走到 `_ensure_node_row()` 时,发现 `(new_run_id, node_id)` 已经有行了(我们预先插的),直接复用那个 `node_exec_id`;`run_work_node()` 一看 `status='done'`,立刻短路返回——这条路径是 `RunGraphHappyPathTest.test_resumes_without_rerunning_done_nodes` 已经在测的行为,不是新写的逻辑。严格下游的节点走的是全新的 `node_exec_id`,正常执行,`build_prompt(node, outputs)` 拿到的 `outputs` 里已经有了(无论是复用来的还是刚替换的)上游内容——这跟"进程重启后 resume"时 `outputs` 字典的填充方式完全一样,天然支持 G 的并发调度(如果严格下游子图里有互不依赖的节点,一样会被分进同一层并发执行,不需要额外代码)。 - -`_downstream_node_ids(graph_manifest, source_node_id)`:从 `depends_on`/`edges` 正向遍历依赖图(复用 `topological_layers()` 已经在用的同一套 dependents 构建逻辑,抽出来共享,不重复实现一遍)。 - -## 4. Schema 改动 - -`work_graph_run` 新增两个可空字段:`parent_run_id`(指向被分叉出去的原始 run)、`fork_of_node_exec_id`(指向具体是从哪个节点分叉的)。纯新增字段,**不改** `ux_work_graph_run_active`(见第 1 节,v1 范围内不需要)。新数据库直接建在 `SCHEMA_SQL` 里;已有数据库用 `migrate_add_work_graph_fork_columns()`(跟 `migrate_expand_artifact_types` 同一个先例,但更简单——纯 `ALTER TABLE ADD COLUMN`,不需要建新表复制数据那套,因为不是改 CHECK 约束)。 - -## 5. 查询层 + 控制台(FR-4) - -`work_graph_run_query.list_work_graph_runs`/`get_work_graph_run`、`task_query.get_task` 的 `work_graph_runs` 列表,都加上 `parent_run_id`/`fork_of_node_exec_id` 两个字段——前端靠这两个字段判断"这条是原始记录还是分叉分支",以及"分叉自哪一条"。 - -控制台改动(`frontend/app.js`): -- 运行历史列表/任务详情页的每条 `work_graph_run` 卡片,`parent_run_id` 非空时显示一个"🔀 从 `` 分叉自 ``"的标记,点击可跳转到父 run。 -- 节点详情面板(已有的 `renderGraphVisualizerPanel`/节点 hover 面板)里,`status='done'` 的节点新增一个"从这里分叉"按钮,点开一个表单填新的 output 内容(JSON),提交调用新端点。 -- 不做 FR-5 提到的"设为主线"之类的合并操作——需求文档非目标 #3 已经说了第一版不做 diff/合并 UI,只读展示 + 触发分叉。 - -## 6. HTTP 端点 - -`POST /api/work-node-executions/:node_exec_id/fork`,body `{"new_output_payload": {...}}`,返回 `{"run_id", "outputs"}`。放在 `backend_server.py` 现有的路由风格里(正则匹配路径 + 方法分派,参考已有的 `/api/tasks/:id/...` 系列端点)。 - -## 7. 不做的部分(如实记录) - -- **从还在跑的父 run 分叉**(第 1 节裁剪掉的部分)。 -- **FR-5 的"设为主线"交互**——只读展示分叉分支,人工另外决定怎么处理分叉产物(需求文档非目标 #3 已经说了 diff/比较视图留给后续)。 -- **分叉分支的生命周期/清理策略**(需求文档 §8 开放问题最后一条)——先不清理,跟原始执行日志的保留策略是两个独立的问题,等真的堆积成问题再处理。 - -## 8. 回答需求文档第 8 节开放问题 - -- `parent_run_id`/`fork_of_node_exec_id` 具体 schema:见第 4 节,纯新增可空字段,不动唯一索引(因为 v1 范围裁剪掉了会冲突的场景,见第 1 节)。 -- 触发入口:控制台 + 一个后端 API(`POST /api/work-node-executions/:node_exec_id/fork`),两者复用同一个 `fork_work_graph_run()`。 -- FR-5 交互:不做"设为主线"按钮,纯只读展示(见第 5、7 节)。 -- 生命周期/清理策略:本轮不做(见第 7 节)。 diff --git a/docs/todo/work-graph-time-travel-debugging-requirements.md b/docs/todo/work-graph-time-travel-debugging-requirements.md deleted file mode 100644 index bf7a295..0000000 --- a/docs/todo/work-graph-time-travel-debugging-requirements.md +++ /dev/null @@ -1,73 +0,0 @@ -# Work Graph 执行历史 Time-Travel 调试——需求文档 - -**日期**: 2026-08-05 -**性质**: 需求文档(BRD/PRD 层面),不含详细技术设计。回应 `docs/todo/graph-engineering-open-items.md` 条目 D 的确认结果——"time travel 可以立项"。 -**状态**: 草案,待评审。 - -**命名消歧(仿照 CLAUDE.md「两个 Supervisor」的先例)**:本文档说的"Time-Travel 调试"跟 `docs/done/supervisor-auto-rollback-design.md` 描述的 Deploy Supervisor 自动回滚是两件完全不同的事——不要混淆: - -| | Time-Travel 调试(本文档) | Deploy Supervisor 自动回滚 | -|---|---|---| -| 回滚的对象 | 某一次任务的 Work Graph 执行过程(某个 `work_node_execution` 的中间状态) | graphlemon 自身的代码版本(git commit) | -| 触发者 | 人工调试意图("我想看看如果这一步换个输入会怎样") | 自动健康检查失败信号 | -| 影响范围 | 单个 task 的执行历史,不影响其他任务 | 整个 graphlemon 进程的运行版本 | -| 相关文档 | 本文档 | `docs/done/supervisor-auto-rollback-design.md` | - -## 1. 背景 - -综述版对照笔记(`docs/reference/graph-engineering-vs-loop-engineering-overview.diff-with-graphlemon.md` §6)指出:LangGraph 的 checkpointer 机制带来四个能力——人在回路、记忆、时间旅行调试、容错。逐项对照 graphlemon 现状: - -| LangGraph 能力 | graphlemon 现状 | -|---|---| -| 容错(部分节点失败保留已成功节点输出) | ✅ 已具备(`work_node_execution` 按节点独立持久化状态) | -| 人在回路(暂停/恢复) | ✅ 已具备(`src/task_execution.py` 的 Start/Pause/Stop) | -| 记忆(多轮交互间保留上下文) | ✅ 已具备(`.claude/agent-memory/`) | -| 时间旅行调试(回到任意历史检查点重放/分叉) | ❌ 未实现,现状是"用 git 历史 + 手动重跑" | - -`work_node_execution` 表(`src/schema.py:138`)目前按节点持久化 `input_payload`/`output_payload`/`status`/`retry_count` 等字段,`work_graph_run` 表(`src/schema.py:118`)持久化 `graph_manifest` 和整体 `status`——这些数据本身已经具备"回放某一次执行发生了什么"的信息基础,缺的是"从某个历史节点分叉出一条新的执行分支"这个操作本身。 - -## 2. 目标 - -1. 允许人工从某次 Work Graph 执行的某个历史节点(`work_node_execution`)出发,用新的输入(不同的 prompt、不同的上游产物)重新执行该节点及其下游依赖节点,产出一条与原执行并存、可独立查看的新分支,而不覆盖或污染原始执行记录。 -2. 分叉出来的新分支要能在控制台里被查看、区分(哪条是原始记录、哪条是分叉出来的调试分支)。 -3. 这个能力主要服务于调试/复盘场景(例如:"如果 SDD 阶段换一版方案,后续的 Coder 实现会有什么不同"),不是生产环境的正常任务执行路径的一部分。 - -## 3. 非目标 - -1. **不是自动化机制**。分叉重放由人工在控制台里主动触发,不是系统自主决定"要不要回溯重跑"的行为——这跟 Org-level Loop 那种系统自主触发的回退是两回事。 -2. **不追求支持任意粒度的时间点**(比如"回到某个节点执行到一半的中间状态")。粒度限定在 `work_node_execution` 这个已有的持久化单元——从"某个节点完成之后(它的 `output_payload` 已经落库)"这个时间点分叉,不试图捕获节点内部一次 `claude -p` 调用执行过程中的中间状态。 -3. **不在第一版里做完整的可视化 diff/比较工具**(比如"原分支和新分支的产物并排对比高亮差异")。第一版先解决"分叉出一条新分支并能分别查看"这个核心能力,比较/diff 视图留给后续迭代。 - -## 4. 功能需求 - -| # | 需求描述 | 优先级 | -|---|---|---| -| FR-1 | 给定一个已完成(`status='done'`)的 `work_node_execution`,人工可以指定新的输入(覆盖原 `input_payload` 的部分或全部字段),触发从这个节点开始的重新执行。 | 必须 | -| FR-2 | 分叉执行产生的是一次**新的** `work_graph_run`(不是修改原 `work_graph_run` 的记录),需要能追溯"这次运行是从哪个历史节点、哪个原始 run 分叉出来的"。 | 必须 | -| FR-3 | 分叉出的新 run 只重新执行"从分叉点开始,到该节点的下游依赖链",分叉点**之前**的节点(分叉点本身的上游)直接复用原始 run 已经产出的 `output_payload`,不重新执行——这跟现有断点续跑"复用已完成节点输出"的逻辑是同一个原则的延伸。 | 必须 | -| FR-4 | 控制台(架构文档 §13 的人机交互控制台)需要能列出"某个 task 下所有的分叉分支",并支持切换查看每条分支各自的节点执行详情(复用现有 v_task_detail 一类页面的节点级展示能力)。 | 应该 | -| FR-5 | 分叉出的调试分支不应该影响原始任务的正常执行状态——即使分叉分支的执行结果比原分支"更好",也不会自动替换/合并回原始任务的 `task.status`,是否采纳分叉分支的结果需要人工另外决定(比如人工把分叉分支的产物手动复制回主线,或者认可分叉分支本身就是新的正确路径)。 | 必须 | - -## 5. 验收标准 - -1. 给定一个已经跑完的简单 Work Graph(比如 Planner 的 BRD→PRD→SDD→Benchmark 四节点链),从 PRD 节点分叉、换一个不同的 PRD 内容,触发重新执行,验证 SDD、Benchmark 两个下游节点用新的 PRD 内容重新跑了一遍,而 BRD 节点没有被重新执行(直接复用原值)。 -2. 验证原始 run 的记录在分叉操作后完全不受影响——分叉前后分别查询原始 `work_graph_run`/`work_node_execution`,字段值一致。 -3. 验证同一个历史节点可以被分叉多次,产生多条互不干扰的分支,且每条分支都能独立追溯到"从哪个节点、哪个原始 run 分叉而来"。 -4. 控制台里能看到某个 task 下全部分支的列表,并能分别打开查看。 - -## 6. 约束与假设 - -- `work_graph_run` 表当前有一条唯一约束:`ux_work_graph_run_active`——同一个 `(task_id, org_node)` 组合同一时间只能有一个非终态的 run(`src/schema.py:134`)。分叉产生的新 run 如果沿用同一个 `task_id`,会撞上这条约束(分叉分支和原分支可能同时处于非终态)。需要在设计阶段决定:分叉出的 run 是否需要一个独立的标识维度(比如新增一个 `parent_run_id`/`fork_of_node_exec_id` 字段,并调整唯一约束的范围,使其只约束"同一条分支线"内部,而不是整个 `task_id`),这是本需求对现有数据模型影响最大的一处,必须在设计阶段仔细过一遍现有依赖这条约束的代码路径(比如断点续跑逻辑是否假设了"同一 task_id+org_node 只有一个活跃 run")。 -- 分叉执行复用现有的每节点独立子进程执行机制(`work_node_run.run_work_node`/`work_graph_execute.execute_work_graph`),不需要新的执行引擎——分叉本质上是"用一份新的 `graph_manifest`(去掉已复用的上游节点)+ 新的初始 `outputs` 字典(预填充复用节点的输出)调用一次 `run_graph()`",复用度很高。 - -## 7. 风险 - -- **数据模型改动的影响面需要在设计阶段仔细评估**,`ux_work_graph_run_active` 这条唯一约束背后可能还有其他代码路径隐式依赖"一个 task_id+org_node 只有一个活跃 run"这个假设(比如查询任务当前状态的逻辑、WebSocket 推送逻辑),改动前需要逐一排查,不能只看 schema 本身。 -- **控制台可视化工作量可能比后端数据模型改动更大**——FR-4 涉及新的 UI 交互(选择分叉点、对比分支列表),需要跟现有 `docs/wireframes/` 里的 `v_task_detail` 类页面设计对齐,这部分工作量需要在设计阶段单独评估,不要低估。 - -## 8. 开放问题(留给详细设计阶段回答) - -- [ ] `parent_run_id`/`fork_of_node_exec_id` 这类新字段的具体 schema 设计,以及 `ux_work_graph_run_active` 约束要怎么调整。 -- [ ] 分叉操作的触发入口——只在控制台里做,还是也要开一个后端 API 供其他方式调用? -- [ ] FR-5"是否采纳分叉分支结果"的人工操作具体是什么交互(一个"设为主线"按钮?还是纯只读展示,人工另外手动处理)? -- [ ] 是否需要给分叉分支设置生命周期/清理策略(长期堆积的调试分支会不会需要定期清理,类似原始执行日志的保留天数机制)? diff --git a/skills/design-dynamic-work-graph/SKILL.md b/skills/design-dynamic-work-graph/SKILL.md index 5b7a589..8296a74 100644 --- a/skills/design-dynamic-work-graph/SKILL.md +++ b/skills/design-dynamic-work-graph/SKILL.md @@ -11,7 +11,7 @@ tags: - orchestration metadata: origin: graphlemon-native - related: [G(并行执行)docs/work-graph-parallel-execution-design.md, E docs/todo/dynamic-work-graph-design-skill-design.md] + related: [G(并行执行)docs/work-graph-parallel-execution-design.md, E docs/done/dynamic-work-graph-design-skill-design.md] platforms: [linux, macos, windows] --- diff --git a/skills/unknown-scope-defect-sweep/SKILL.md b/skills/unknown-scope-defect-sweep/SKILL.md index a321ec2..0c98aaa 100644 --- a/skills/unknown-scope-defect-sweep/SKILL.md +++ b/skills/unknown-scope-defect-sweep/SKILL.md @@ -11,7 +11,7 @@ tags: - sweep metadata: origin: graphlemon-native - related: [qa-task-validation, docs/todo/validator-loop-until-dry-skill-design.md] + related: [qa-task-validation, docs/done/validator-loop-until-dry-skill-design.md] platforms: [linux, macos, windows] --- diff --git a/src/agent_profile.py b/src/agent_profile.py index a696eaf..07bba91 100644 --- a/src/agent_profile.py +++ b/src/agent_profile.py @@ -33,7 +33,7 @@ read by this module's own Python code; `json.load` is the zero-risk choice, no hand-rolled parser. role.json content is deliberately a REFERENCE manifest, not a content copy: `identity.soul_md_path`/`agents_md_path` still point at the same `agents//SOUL.md`/`AGENTS.md` files, unmoved -- -see docs/todo/role-package-abstraction-design.md §2 for why keeping the +see docs/done/role-package-abstraction-design.md §2 for why keeping the existing identity files as the single source of prose content makes the migration's "deployed output must stay byte-identical" requirement close to automatic (only the frontmatter's data SOURCE changed, not the body-assembly diff --git a/src/org_level_loop.py b/src/org_level_loop.py index 740b479..6def114 100644 --- a/src/org_level_loop.py +++ b/src/org_level_loop.py @@ -17,7 +17,7 @@ def run_fix_loop(conn, task_id, target_folder_path, bug_description, context, fi function doesn't add its own fallback for that, matching 04-P's own "升级人工" answer to disagreement). - **F (2026-08-06, docs/todo/verifier-fan-out-jury-design.md)**: root + **F (2026-08-06, docs/done/verifier-fan-out-jury-design.md)**: root cause judgment now runs as a real Work Graph fan-out (each perspective is its own Work Node with its own clean context) instead of three sequential Python closures calling org_node_trigger.trigger_org_node -- diff --git a/src/root_cause_trace.py b/src/root_cause_trace.py index 72f616d..7e5be79 100644 --- a/src/root_cause_trace.py +++ b/src/root_cause_trace.py @@ -15,7 +15,7 @@ concrete question each perspective asks is a prompt-engineering concern for the caller (see real_validator_perspective below for the real-call version), not this module's. -**F (2026-08-06, docs/todo/verifier-fan-out-jury-design.md)**: judge_root_cause +**F (2026-08-06, docs/done/verifier-fan-out-jury-design.md)**: judge_root_cause above is the closure-based implementation -- each perspective is a Python function call, sequentially, in the SAME process/context. Kept as-is (not replaced): it's the cheap path for scenarios that don't need worktree/Work diff --git a/src/schema.py b/src/schema.py index 1cc0ed1..4270e0c 100644 --- a/src/schema.py +++ b/src/schema.py @@ -84,7 +84,7 @@ CREATE TABLE skill ( -- ('planner','coder','validator')) -- relaxed to a plain non-empty-string -- check so a new role, added purely via roles//role.json (no -- schema.py/agent_profile.py code change), isn't rejected by a stale - -- hardcoded enum. See docs/todo/role-package-abstraction-design.md §4. + -- hardcoded enum. See docs/done/role-package-abstraction-design.md §4. owner_org_node TEXT NOT NULL CHECK (owner_org_node != ''), file_path TEXT NOT NULL, current_git_commit_sha TEXT NOT NULL, diff --git a/src/test_org_level_loop.py b/src/test_org_level_loop.py index 2eb4833..2d54200 100644 --- a/src/test_org_level_loop.py +++ b/src/test_org_level_loop.py @@ -5,7 +5,7 @@ Testing constraint). Real coverage is in src/test_org_level_loop_e2e.py **F (2026-08-06)**: root-cause judgment now runs through root_cause_trace.judge_root_cause_via_work_graph (a real Work Graph fan-out, -see docs/todo/verifier-fan-out-jury-design.md), so the 3 perspective calls +see docs/done/verifier-fan-out-jury-design.md), so the 3 perspective calls are mocked at work_graph_execute.write_workflow_script/execute_work_graph (same boundary as test_work_graph_orchestrate.py), not at org_node_trigger.trigger_org_node -- that mock is now only used for the diff --git a/src/test_work_graph_fork.py b/src/test_work_graph_fork.py index 5eb95ef..f5f4613 100644 --- a/src/test_work_graph_fork.py +++ b/src/test_work_graph_fork.py @@ -1,7 +1,7 @@ """D (2026-08-06) tests: work_graph_fork.py, mocked at the claude subprocess boundary (CLAUDE.md Testing constraint) -- same pattern as test_work_graph_orchestrate.py. Covers the requirements doc's acceptance -standards 1-3 (docs/todo/work-graph-time-travel-debugging-requirements.md). +standards 1-3 (docs/done/work-graph-time-travel-debugging-requirements.md). Run: python3 -m unittest src.test_work_graph_fork -v """ diff --git a/src/work_graph_fork.py b/src/work_graph_fork.py index 9393665..14a1f1f 100644 --- a/src/work_graph_fork.py +++ b/src/work_graph_fork.py @@ -3,7 +3,7 @@ work_graph_run from an already-'done' work_node_execution, substituting a human-provided output for that node and re-executing everything strictly downstream of it for real, while every other node reuses the original run's persisted output_payload untouched. See -docs/todo/work-graph-time-travel-debugging-design.md for the full design. +docs/done/work-graph-time-travel-debugging-design.md for the full design. **Output substitution, not re-prompting** (design doc §2, resolving the requirements doc's FR-1 ambiguity): the fork point's own output_payload is diff --git a/src/work_graph_orchestrate.py b/src/work_graph_orchestrate.py index a870b3f..21fab05 100644 --- a/src/work_graph_orchestrate.py +++ b/src/work_graph_orchestrate.py @@ -352,7 +352,7 @@ def run_graph( independent work, no reason to throw away completed work), then raised together as `WorkGraphLayerError` once the whole layer has settled. One real limitation, accepted deliberately (see - `docs/todo/work-graph-parallel-execution-design.md` §6): a node + `docs/done/work-graph-parallel-execution-design.md` §6): a node waiting on a quota-backoff sleep inside `run_work_node` blocks the thread pool worker running it, which in turn blocks the layer barrier even after every sibling has finished -- Python threads have no clean diff --git a/src/work_graph_plan.py b/src/work_graph_plan.py index 8a26d16..e4fa23e 100644 --- a/src/work_graph_plan.py +++ b/src/work_graph_plan.py @@ -80,7 +80,7 @@ _PROMPT_TEMPLATE = ( # E (2026-08-06): the generic, cross-Org-Agent path -- used for every # org_node OTHER than 'planner'. Planner's own doc-writing chain stays on # _PROMPT_TEMPLATE above unchanged (zero regression risk, see -# docs/todo/dynamic-work-graph-design-skill-design.md §3): that chain is a +# docs/done/dynamic-work-graph-design-skill-design.md §3): that chain is a # fixed architectural constant (brd->prd->sdd->benchmark->...), not # something that should vary call to call, whereas Coder/Validator's # sub-tasks have no such fixed small vocabulary of node types. Each node -- libgit2 0.21.4