From 38b9542017384611bf069b2c133bb75e9da7d92d Mon Sep 17 00:00:00 2001 From: graphlemon Date: Thu, 6 Aug 2026 00:20:43 +0800 Subject: [PATCH] graphlemon: auto-commit uncommitted changes for 81700b35-ea5b-4718-9846-4ff0f924c231-main-checkout-drift --- .claude/agent-memory/planner/MEMORY.md | 3 ++- .claude/agent-memory/planner/project_artifact_overview_click_through_bug.md | 16 ++++++++++++---- .claude/agent-memory/planner/project_loop_round_bug_already_fixed.md | 4 ++-- .claude/agent-memory/planner/project_org_strip_click_through_bug.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ .claude/agent-memory/validator/project_self_hosting_loop_round_probe.md | 4 ++-- CLAUDE.md | 4 ++-- docs/Graph Engineering with Claude_ 14-Step roadmap from 0 to graph architect (Full Course).md | 844 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- docs/build-report.md | 148 ---------------------------------------------------------------------------------------------------------------------------------------------------- docs/development-plan.md | 6 +++--- docs/done/build-report.md | 148 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/done/self-development-feasibility.md | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/done/supervisor-auto-rollback-design.md | 170 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/done/supervisor-design-comparison.md | 109 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/dynamic-work-graph-design-skill-requirements.md | 68 -------------------------------------------------------------------- docs/implementation-gap-analysis.md | 6 +++--- docs/reference/graph-engineering-1000-agents-5-steps.md | 2 +- docs/reference/graph-engineering-vs-loop-engineering-overview.md | 2 +- docs/sdd/00-SDD-生成记录.md | 2 +- docs/sdd/01-M-模型设计.md | 4 ++-- docs/sdd/03-C-控制与接口.md | 2 +- docs/sdd/04-P-流程与状态流转.md | 2 +- docs/sdd/05-实施约束与依赖.md | 4 ++-- docs/sdd/06-验收与测试锚点.md | 2 +- docs/self-development-feasibility.md | 68 -------------------------------------------------------------------- docs/supervisor-auto-rollback-design.md | 170 -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- docs/supervisor-design-comparison.md | 109 ------------------------------------------------------------------------------------------------------------- docs/todo/dynamic-work-graph-design-skill-requirements.md | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/todo/graph-engineering-open-items.md | 4 ++-- docs/todo/validator-loop-until-dry-skill-requirements.md | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 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-requirements.md | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/validator-loop-until-dry-skill-requirements.md | 59 ----------------------------------------------------------- docs/verifier-fan-out-jury-requirements.md | 69 --------------------------------------------------------------------- docs/work-graph-parallel-execution-requirements.md | 76 ---------------------------------------------------------------------------- docs/work-graph-time-travel-debugging-requirements.md | 73 ------------------------------------------------------------------------- src/schema.py | 2 +- supervisor/deploy_supervisor.py | 2 +- supervisor/isolation/__init__.py | 6 +++--- 40 files changed, 1211 insertions(+), 1718 deletions(-) create mode 100644 .claude/agent-memory/planner/project_org_strip_click_through_bug.md delete mode 100644 docs/Graph Engineering with Claude_ 14-Step roadmap from 0 to graph architect (Full Course).md delete mode 100644 docs/build-report.md create mode 100644 docs/done/build-report.md create mode 100644 docs/done/self-development-feasibility.md create mode 100644 docs/done/supervisor-auto-rollback-design.md create mode 100644 docs/done/supervisor-design-comparison.md delete mode 100644 docs/dynamic-work-graph-design-skill-requirements.md delete mode 100644 docs/self-development-feasibility.md delete mode 100644 docs/supervisor-auto-rollback-design.md delete mode 100644 docs/supervisor-design-comparison.md create mode 100644 docs/todo/dynamic-work-graph-design-skill-requirements.md create mode 100644 docs/todo/validator-loop-until-dry-skill-requirements.md create mode 100644 docs/todo/verifier-fan-out-jury-requirements.md create mode 100644 docs/todo/work-graph-parallel-execution-design.md create mode 100644 docs/todo/work-graph-parallel-execution-requirements.md create mode 100644 docs/todo/work-graph-time-travel-debugging-requirements.md delete mode 100644 docs/validator-loop-until-dry-skill-requirements.md delete mode 100644 docs/verifier-fan-out-jury-requirements.md delete mode 100644 docs/work-graph-parallel-execution-requirements.md delete mode 100644 docs/work-graph-time-travel-debugging-requirements.md diff --git a/.claude/agent-memory/planner/MEMORY.md b/.claude/agent-memory/planner/MEMORY.md index c587ff5..315b793 100644 --- a/.claude/agent-memory/planner/MEMORY.md +++ b/.claude/agent-memory/planner/MEMORY.md @@ -2,4 +2,5 @@ - [scheduler.py busy-timeout comment missing](project_scheduler_busy_timeout_comment_missing.md) — as of 2026-08-03 _BUSY_TIMEOUT_MS (scheduler.py:78) has no inline comment; PL does not self-edit, packages as a Unit for Code Agent - [deploy_health_check.py startup-timeout comment: RESOLVED](project_deploy_health_check_startup_timeout_comment_missing.md) — comment now present above HEALTH_CHECK_STARTUP_TIMEOUT (line 24); re-verify by reading file before re-flagging - [deploy_health_check.py HTTP-timeout comment missing](project_deploy_health_check_http_timeout_comment_missing.md) — HEALTH_CHECK_HTTP_TIMEOUT (line 26) has no comment directly above it, unlike its sibling STARTUP_TIMEOUT; distinct open fact, don't conflate the two -- [artifact overview click-through bug: 3 real gaps](project_artifact_overview_click_through_bug.md) — renderArtifactSummaryList has no data-artifact-id/click (confirmed); router has zero query-string support (#/artifacts?id=x falls through to Not found); showPreview() already exists, just never called from a URL param +- [artifact overview click-through bug: RESOLVED](project_artifact_overview_click_through_bug.md) — re-verified 2026-08-06, all 3 gaps fixed (data-artifact-id, #/artifacts/:id route, mountArtifactBrowser seeding); re-check before trusting "resolved" long-term +- [org-strip click-through bug: 3 render sites, not 1](project_org_strip_click_through_bug.md) — renderOrgStrip (dashboard), v_task_detail's inline org-strip, and #/graph's renderOrgGraphTopology all share the same no-click gap; DAG/log/artifact display mechanisms already exist and must be reused, not rebuilt; no new backend endpoint needed diff --git a/.claude/agent-memory/planner/project_artifact_overview_click_through_bug.md b/.claude/agent-memory/planner/project_artifact_overview_click_through_bug.md index 9e8b2ac..66e860b 100644 --- a/.claude/agent-memory/planner/project_artifact_overview_click_through_bug.md +++ b/.claude/agent-memory/planner/project_artifact_overview_click_through_bug.md @@ -1,13 +1,21 @@ --- name: project-artifact-overview-click-through-bug -description: Investigation findings for the "Planner 产出总览" list-item click-through bug (frontend/app.js), needed by whoever writes the SDD/benchmark content for this task +description: "RESOLVED 2026-08-06 (re-verified by direct file read): all 3 gaps fixed — data-artifact-id, #/artifacts/:id route, and mountArtifactBrowser(initialArtifactId) all present." metadata: type: project --- -Investigated 2026-08-04 for the "Planner 产出总览面板列表项点击无反应" bug-fix task -(graph_manifest planned same session). Root cause is real and narrower than it first -looks — three separate gaps, not one: +**RESOLVED as of 2026-08-06** (re-verified by reading `frontend/app.js` directly while +investigating the unrelated Org Graph click-through bug, see [[project-org-strip-click-through-bug]]): +all three gaps below have been fixed. `renderArtifactSummaryList`/`renderArtifactSummaryList`'s +`
  • ` now carries `data-artifact-id` (app.js:1360), `ARTIFACT_DETAIL_RE` (`#/artifacts/:id`) is a +real dynamic route wired in `render()` (app.js:99,125-129), and `mountArtifactBrowser(initialArtifactId)` +(app.js:1603) seeds `selectedArtifactId` from it and calls the existing `showPreview()` flow. Don't +re-flag this as open without re-reading the file first — but do keep re-verifying, per CLAUDE.md's +general warning that autonomous self-hosting runs can fix or re-break things between sessions. + +**Original investigation (2026-08-04)**, kept for history — root cause was real and narrower than +it first looked, three separate gaps, not one: 1. `renderArtifactSummaryList()` (frontend/app.js:1344) renders bare `
  • ` with no `data-artifact-id` and no click handler — confirmed, matches the task's stated root cause. diff --git a/.claude/agent-memory/planner/project_loop_round_bug_already_fixed.md b/.claude/agent-memory/planner/project_loop_round_bug_already_fixed.md index 221590f..67c0980 100644 --- a/.claude/agent-memory/planner/project_loop_round_bug_already_fixed.md +++ b/.claude/agent-memory/planner/project_loop_round_bug_already_fixed.md @@ -17,7 +17,7 @@ i.e. a follow-up request to "add a one-line clarifying comment above `_LOOP_LIMI REQ-05/REQ-09" is *also* already satisfied verbatim in substance. Two independent asks about this same three-line region of the file have both turned out to be already-done on inspection. -**Why:** `docs/self-development-feasibility.md` §"场景" uses "fix the loop_round off-by-one +**Why:** `docs/done/self-development-feasibility.md` §"场景" uses "fix the loop_round off-by-one (`loop_round > 3` should be `loop_round >= 3`)" as the canonical illustrative example goal for graphlemon's self-hosting/self-modification demo, and this exact string is hardcoded as the goal text in `supervisor/test_self_hosting_e2e.py`'s `RealSelfHostingClosedLoopTest` (gated behind @@ -26,7 +26,7 @@ glance, but it is a fixture/example string, not a currently-true bug report — was written already-correct. **How to apply:** If asked again to plan/fix/investigate this specific goal text (or anything -that cites `docs/self-development-feasibility.md`'s example scenario), re-verify current state of +that cites `docs/done/self-development-feasibility.md`'s example scenario), re-verify current state of `src/org_loop_route.py` and `src/test_org_loop_route.py` first rather than assuming the bug is still open — it may have been fixed between conversations, or the request may actually be testing the *planning process* (e.g. via the self-hosting E2E harness) rather than asking for a real code diff --git a/.claude/agent-memory/planner/project_org_strip_click_through_bug.md b/.claude/agent-memory/planner/project_org_strip_click_through_bug.md new file mode 100644 index 0000000..d323796 --- /dev/null +++ b/.claude/agent-memory/planner/project_org_strip_click_through_bug.md @@ -0,0 +1,51 @@ +--- +name: project-org-strip-click-through-bug +description: Investigation findings for the Org Graph (Planner/Coder/Validator) node click-through bug across the 3 places it's rendered — needed for the SDD/benchmark content of this task. +metadata: + type: project +--- + +Investigated 2026-08-06 for "控制台 Org Graph 三节点条点击没有反应" (graph_manifest planned same +session). The stated root cause (`renderOrgStrip()` renders bare divs, no `data-*`, no click, no +`cursor:pointer`) is real, but it is **one of three separate render sites with the same gap**, not +the only one — don't scope the fix to just `renderOrgStrip`: + +1. `renderOrgStrip()` (frontend/app.js:589-598, mounted in `mountDashboard`, `id="org_strip"`, + under the "Org Graph 三节点状态" heading in `v_dashboard`). Confirmed: no `data-*`, no listener. +2. `v_task_detail`'s inline org-strip (app.js:1085-1088, inside `renderDetail()`, under an actual + `

    Org Graph

    ` heading) — same `.org-strip`/`.org-node` markup pattern, same gap, built + inline rather than via a shared function. If `renderOrgStrip` gets a shared click-handling + helper, this call site needs the same wiring, not a copy-paste duplicate. +3. `renderOrgGraphTopology()` (app.js:1322-1347, used only by `#/graph`'s `mountGraphConsole`) is a + *different, newer* implementation (`.graph-topo-node`, not `.org-strip`/`.org-node` — added + 2026-08-03 as `v_graph_console` FR-1, explicitly "not org-strip" per the comment at app.js:1314) + that already shows per-node `status-*` classes and an active/retry-loop edge, but its 3 nodes + are *also* not clickable — only the Work Graph sub-nodes inside it are (`mountWorkNodeClickExpand`, + wired via `renderGraphVisualizerPanel(run, { enableNodeClick: true })`, which already has real + click → `renderLogViewerHtml`/`mountLogViewer` wiring, confirmed working). + +**What already exists and must be reused, not rebuilt** (confirmed by reading the code, not by +trusting the task's framing): `renderGraphVisualizerPanel(run, {enableNodeClick:true})` + +`mountWorkNodeClickExpand` + `renderLogViewerHtml`/`mountLogViewer` (real WS live-tail + +`c_execution_log_get` static fallback, `frontend/app.js:942-1016`) is the exact combo `#/graph` +already uses per-task. `mountArtifactsOverviewPanel`'s doc/code lists already link through to +`#/artifacts/:artifact_id` (`v_artifact_browser`, `showPreview()`), see +[[project-artifact-overview-click-through-bug]] (now resolved) for that mechanism's shape. + +**Backend note**: no new endpoint needed. `GET /api/tasks/:task_id` (`task_query.get_task`, +`src/task_query.py:115-171`) already returns `work_graph_runs` (each with `org_node`, `run_id`, +`status`, `nodes`) ordered by `started_at` ASC, and `artifacts` (each with `produced_by_run_id` +linking back to a `work_graph_run.run_id`) ordered by `created_at` ASC. "Latest work_graph_run for +org_node X" = filter `work_graph_runs` by `org_node === X` and take the last (loop retries can +produce more than one run per org_node, e.g. `full_replan` re-triggers planner) — this is pure +frontend filtering of data already on the wire, not a new query. + +**CSS**: `.org-node`/`.graph-topo-node` (`frontend/style.css:42-47,127-139`) have no `cursor:pointer` +today, unlike `.work-node` (style.css:104, already clickable) and `ul.artifact-list li` (style.css:156). + +**How to apply**: when writing the SDD, scope the fix as one shared click-handling mechanism +(scoped to the *current task* being viewed, not global across tasks) applied at all 2-3 render +sites that actually ship on this pass, composing already-existing panels (DAG + log viewer + +artifact link-through) rather than inventing a 4th display mechanism. Decide and record explicitly +whether `renderOrgGraphTopology`'s topology nodes are in scope for this task (they're a newer, +separate implementation with the same gap) or deferred — don't silently skip them without a note. diff --git a/.claude/agent-memory/validator/project_self_hosting_loop_round_probe.md b/.claude/agent-memory/validator/project_self_hosting_loop_round_probe.md index 6248861..3a4cb68 100644 --- a/.claude/agent-memory/validator/project_self_hosting_loop_round_probe.md +++ b/.claude/agent-memory/validator/project_self_hosting_loop_round_probe.md @@ -9,7 +9,7 @@ Task `14fd9ab6-e2c8-4233-8381-002419de85bb` ("fix loop_round > 3 should be >= 3 p_org_task_lifecycle") turned out to be a **no-op investigation task**: `src/org_loop_route.py` has used `loop_round >= _LOOP_LIMIT` (limit=3) since the very first commit that introduced the file (`59749fc`, U-016) — it was never `> 3`. The `> 3` phrasing only appears in -`docs/self-development-feasibility.md:50`, an explicitly-labeled **hypothetical illustration** +`docs/done/self-development-feasibility.md:50`, an explicitly-labeled **hypothetical illustration** in §3 "模拟走一遍:修复自己状态机里的一个边界 bug" (a PH5 self-hosting feasibility walkthrough, not a real bug report). I independently re-derived this from `git log -p --follow` on the file and by running `python3 -m unittest src.test_org_loop_route -v` (11/11 pass) — confirmed the @@ -17,7 +17,7 @@ Planner/Coder's claim rather than trusting it. **Why this matters:** this repo (graphlemon) sometimes runs tasks *against its own source tree* (self-hosting/self-modification, see CLAUDE.md's "Two things named Supervisor" section and -`docs/self-development-feasibility.md`). PH5 (actual self-hosting execution, U-034~U-043) is +`docs/done/self-development-feasibility.md`). PH5 (actual self-hosting execution, U-034~U-043) is explicitly **not in scope** per CLAUDE.md yet — but *investigation-only* self-referential tasks like this one (verify graphlemon's own code against its own spec) apparently are being run ahead of PH5, and "no code change, verified no defect" is a legitimate, complete outcome for such a diff --git a/CLAUDE.md b/CLAUDE.md index 6e0a36f..be5c3e1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -131,7 +131,7 @@ python3 -m src.backend_server --port 8000 # default --port 0 means OS-assigned (U-034~U-043): `deploy_stage.py` (stage a candidate), `deploy_health_check.py` (poll for startup readiness, not one-shot — see U-038 fix), `deploy_cutover.py`, `deploy_monitor.py`, `deploy_rollback.py`. Fail-closed, never-auto-restart philosophy adapted (not imported) from - graphloop's `hermes_watchdog.py`. See `docs/supervisor-auto-rollback-design.md` for the full + graphloop's `hermes_watchdog.py`. See `docs/done/supervisor-auto-rollback-design.md` for the full design and the disambiguation note at the top of that file. These are unrelated components that happen to share a root word in casual English. Identifiers @@ -148,7 +148,7 @@ outside its isolated workspace, a 300s timeout, a stale-PID recovery incident). worth reading for architecture patterns; its `hermes_watchdog.py`/`phase10_controller.py` are worth reading for the "fail-closed, deterministic, never-auto-restart" supervision philosophy (graphlemon's Deploy Supervisor deliberately extends this into auto-rollback rather than just -auto-halt — see `docs/supervisor-design-comparison.md` for why). Do not import graphloop code +auto-halt — see `docs/done/supervisor-design-comparison.md` for why). Do not import graphloop code directly; anything worth reusing has already been copied and adapted into this repo (`supervisor/isolation/`, adapted verbatim per the U-036 note in `development-plan.md`). diff --git a/docs/Graph Engineering with Claude_ 14-Step roadmap from 0 to graph architect (Full Course).md b/docs/Graph Engineering with Claude_ 14-Step roadmap from 0 to graph architect (Full Course).md deleted file mode 100644 index 104bd30..0000000 --- a/docs/Graph Engineering with Claude_ 14-Step roadmap from 0 to graph architect (Full Course).md +++ /dev/null @@ -1,844 +0,0 @@ -# Graph Engineering with Claude: 14\-Step roadmap from 0 to graph architect \(Full Course\) - - - -img\_placeholder - - - -大多数人尝试构建多步智能体时,最终得到的都是一条直线。第一步、第二步、第三步——每一步都乖乖地等上一步做完才开始。 - -Most people who try to build a multi\-step agent end up with a straight line\. Step one, step two, step three \- each waiting politely for the last to finish before it starts\. - - - -十分之九的人会发现,其中一半的步骤根本不需要等待。 - -**9/10 notice that half those steps never needed to wait at all\.** - - - -它们不路由,不分支,不并行。它们只是排队——一个脑袋,一个上下文,一次只做一件事——直到窗口被填满,智能体忘记自己在做什么。 - -They don’t **route\.** They don’t **branch\.** They don’t **parallelize\.** They just queue \- one head, one context, one thing at a time, until the window fills up and the agent forgets what it was doing\. - - - -> Follow my Substack to get fresh AI alpha: -> -> [movez\.substack\.com](https://movez.substack.com/) -> -> -> -> - -这套十四步执行路线图,能把单文件单行式简单任务拆解成完整任务图谱:该图谱可分发至海量智能体集群并行运算、自主校验全部检索结果,最终收敛输出一套单智能体完全无法承载处理规模的完整结论。 - -***This is the 14\-step roadmap*** that turns that single\-file line into a **graph**: one that fans out across a fleet, verifies its own findings, and converges on a result a lone agent could never hold\. - -![Image](https://internal-api-drive-stream.feishu.cn/space/api/box/stream/download/authcode/?code=Yzk2MWVlZGE5MTQ0ZTQwYjFjNjMxODg5NDgzZTg2OTJfZTVhYTBjOThlMzY3NDBjZmE2YWRmZTEwMTRkZDM2ZmJfSUQ6NzY2NDkxMjQwNTYwNDgxNDAyM18xNzg1MzI4NTM1OjE3ODU0MTQ5MzVfVjM) - - - -这里有一个没人明说的转变:prompt提示词是一句话,loop循环是一个周期,harness框架是智能体脚下的地板。 - -Here’s the shift nobody spells out\. A prompt is a sentence\. A loop is a cycle\. A harness is the floor the agent stands on\. - - - -但工作本身的形状——什么在什么之前运行、什么可以同时运行、什么必须等待其他所有东西——这个形状就是一个图。节点负责思考,边负责传递结果。 - -But the **shape of the work itself** \- what runs before what, what can run at the same time, what has to wait for everything else \- that shape is a graph\. Nodes do the thinking\. Edges carry the results\. - - - -Claude Code 已经内置了直接构建这些图的工具:动态工作流。 - -Claude Code shipped the tooling to build these graphs directly: **dynamic workflows**\. - - - -Claude 编写一个普通的 JavaScript 编排脚本,然后生成一个协调的子智能体舰队来执行它——而协调本身不消耗任何模型 token,因为它是代码,不是对话。 - -Claude writes a plain JavaScript orchestration script, then spawns a coordinated fleet of subagents to execute it \- and the coordination itself costs zero model tokens, because it’s code, not a conversation\. - - - - - -## Nodes are jobs\. Edges are what *flows\.* - -\#\# 节点代表工作,边代表流转关系 - - - -一个图只有两样东西(节点\+边 Nodes\+Edges),把它们搞清楚就能解决大部分困惑。 - -**节点**是一个工作单元——一个智能体、一个有界任务、一个输入一个输出。 - -A graph has exactly two things, and getting them straight fixes most of the confusion\. - -A **node** is a unit of work \- one agent, one bounded job, one input in and one output out\. - - - -**边**是一个依赖关系:含义是这个节点的输出作为另一个节点的输入。仅此而已。 - -An **edge** is a dependency: it says *this node’s output feeds that node’s input\.* Nothing more\. - -![Image](https://internal-api-drive-stream.feishu.cn/space/api/box/stream/download/authcode/?code=MzY1ODVjYWExNjBmMTY0NGU2ZDkxNGEzNDMzMjU4MmNfZjE2MzI4ZTIyOTgxZDMxOWZiYjEzZGI5NzI3ZTA4ZjdfSUQ6NzY2NDkxMzQ2MDEzMzM2Njc0MV8xNzg1MzI4NTM1OjE3ODU0MTQ5MzVfVjM) - -常见的错误是把「然后」当作边。「总结文件然后告诉我天气」——这两者之间没有边,天气不消费摘要的数据。 - -The mistake is treating “and then” as an edge\. “Summarize the file *and then* tell me the weather” has no edge between the two \- the weather doesn’t consume the summary\. - - - -那是两个不相连的节点,被线性脚本不必要地串联在一起。边只存在于数据真正跨越它的时候。 - -That’s two disconnected nodes that a linear script needlessly chains\. The edge only exists when data actually moves across it\. - - - -学会对你的智能体中每个「然后」提问:下一步是否读取上一步的输出?如果不是,就没有边,等待是浪费的。 - -Learn to ask, for every “and then” in your agent: **does the next step read the last step’s output?** If not, there is no edge, and the wait is wasted\. - - - -python - -1\. 用方框和箭头画出来。方框是一个 agent\(\) 调用,箭头是一个变量从一个调用的返回值传到另一个的提示词里。如果你画不出箭头——如果没有变量跨越——两个方框就是独立的,而独立性是你在本课程剩余部分要利用的东西。 - -```Python -Draw it as boxes and arrows. A box is an agent() call. -An arrow is a variable passed from one call’s return into another’s -prompt. If you can’t draw the arrow - if no variable crosses - the two -boxes are independent, and independence is the thing you’ll exploit -for the rest of this course. -``` - - - - - -## Your linear script is a *degenerate* graph - -\#\# 你的线性脚本是一个退化的图 - - - -当你把智能体写成「先做A,再做B,再做C,再做D」时,你已经画了一个图——一条不分叉的单链。每个节点恰好有一条边进、一条边出。 - -When you write an agent as “do A, then B, then C, then D,” you’ve drawn a graph \- a single unbranching chain\. Every node has exactly one edge in and one edge out\. - - - -它能正确运行,但也运行得又慢又脆,因为链条没有冗余:如果C卡住了,D永远不会执行,A的工作被困在上游无处可去。 - -It runs correctly\. It also runs slowly and fragile, because a chain has no redundancy: if C stalls, D never happens, and A’s work is trapped upstream with nowhere to go\. - - - -![Image](https://internal-api-drive-stream.feishu.cn/space/api/box/stream/download/authcode/?code=MGU2ZWQwYzllODU3MDlhZTViMzM4NWE5ZTE1M2FjMTNfYWY0YzE4YzRkNjdkYzk3ZDExZWFlN2E4OWVkMWY0YzNfSUQ6NzY2NDkwOTcxODAyNDQ0MDc2OF8xNzg1MzI4NTM1OjE3ODU0MTQ5MzVfVjM) - -图工程的第一项真正技能是重画这条链。拿起你的线性智能体,对每条箭头问第一步的问题。 - -The first real skill of graph engineering is **redrawing the chain**\. Take your linear agent and, for each arrow, ask the Step 1 question\. - - - -大多数链条有两三条不携带数据的箭头——它们只是你碰巧输入东西的顺序。 - -Most chains have two or three arrows that don’t carry data \- they’re just the order you happened to type things in\. - - - -剪掉这些箭头,链条就坍缩成更宽的东西:几个可以同时运行的独立节点,喂给一个需要它们全部的单一节点。 - -Cut those arrows and the chain collapses into something wider: a few independent nodes that could all run at once, feeding a single node that needs them all\. - - - - - -## Give every node a *contract* - -\#\# 给每个节点一个契约 - -一个你无法推理的节点就是一个你无法并行的节点。解决方案是契约:有界输入、有界输出、恰好一个任务。 - -A node you can’t reason about is a node you can’t parallelize\. The fix is a contract: **bounded input, bounded output, exactly one job\.** - - - -输入是节点读取的任何东西——显式传入,永远不从共享窗口假设。输出是一个定义好的形状,最好经过验证,这样下一个节点可以无猜测地消费它。 - -The input is whatever the node reads \- passed in explicitly, never assumed from a shared window\. The output is a defined shape, ideally validated, so the next node can consume it without guessing\. - -![Image](https://internal-api-drive-stream.feishu.cn/space/api/box/stream/download/authcode/?code=MjMzYTQwYjViZjUyMTA5YmVhZjBlY2JmZjhiMjE4NmNfYjQ5OWNjZjk0YmUzOTQ4ZDdjNmJmMzZiZTRkYzYwZjBfSUQ6NzY2NDkwOTgwMTkzNTExMzQxOV8xNzg1MzI4NTM1OjE3ODU0MTQ5MzVfVjM) - -在工作流中,这个契约用 schema 强制执行。当你给 Claude 一个带 JSON schema 的 agent\(\) 调用时,Claude 生成的子智能体被迫返回验证过的结构化数据——验证发生在工具调用层,所以 Claude 在不匹配时重试,而不是给你一段需要解析和祈祷的自由文本。 - -In a workflow this contract is enforced with a **schema**\. When you hand Claude an agent\(\) call with a JSON schema, the subagent Claude spawns is forced to return validated structured data \- validation happens at the tool\-call layer, so Claude retries on mismatch instead of handing you free text you have to parse and pray over\. - - - -这就是一个 Claude 能接入图的节点和一个只有人类读输出才能工作的节点之间的区别。 - -This is the difference between a node Claude can wire into a graph and a node that only works when a human reads its output\. - - - -python - -// 真正有契约的节点:有界输入,验证输出,单一任务。 - -```Python -// A node with a real contract: bounded in, validated out, one job. -const ITEM = { - type: 'object', additionalProperties: false, - properties: { - title: { type: 'string' }, - url: { type: 'string' }, - impact: { type: 'string', enum: ['high', 'medium', 'low'] }, - }, - required: ['title', 'url', 'impact'], -}; -const result = await agent(source.prompt, { - label: `research:${source.key}`, - schema: ITEM, // forces validated structured output - agentType: 'general-purpose', -}); -// result is now a shape the next node can trust — not free text. -``` - - - - - -## 04\. Treat the edge as a *data contract* - -\#\# 04\. 把边当作数据契约 - - - -边不只是简单表示 “任务 A 执行完再跑任务 B”,它更是一份数据传输约定:A 输出固定格式的数据结构,而 B 的设计本身就适配读取该结构。如果以传输的数据(而非执行先后顺序)来命名边,两件事会变得清晰易懂。 - -An edge isn’t just “B comes after A\.” It’s a promise about what crosses: A produces *this shape*, and B is built to consume *this shape\.* When you name the edge by its data \- not its order \- two things get easier\. - - - -![Image](https://internal-api-drive-stream.feishu.cn/space/api/box/stream/download/authcode/?code=MTgwN2M4N2JiYjk0ZDRiNjEzZWE4YWFhYTE0ZDU5YTVfZTk3NmZiMWFiMTgxZmFiYmRhZjdjNjhkZWM1ZTIyYThfSUQ6NzY2NDkxNzIzMzc1NjA2NDcyNF8xNzg1MzI4NTM1OjE3ODU0MTQ5MzVfVjM) - -你可以一眼看出边是否真实(数据真的在移动吗?),而且只要形状保持不变,你可以替换任一端的节点而不破坏图。 - -You can see instantly whether the edge is real \(does data actually move?\), and you can swap the node on either end without breaking the graph, as long as the shape holds\. - - - -实际实现中,边由原生 JavaScript 编写。在任务分发与结果汇总之间的归约步骤(扁平化、去重、过滤),本质就是一段处理各节点输出数据结构的代码。 - -In practice, the edge lives in plain JavaScript\. The reduce step between fan\-out and synthesis \- flatten, dedupe, filter \- is just code operating on the shapes your nodes returned\. - - - -全程无需智能体参与。基于图谱架构设计有一项不易察觉的显著优势:大量原本需要消耗大模型算力令牌处理的逻辑,本质只是一条数据流边,而数据流边完全零算力开销。 - -**No agent needed\.** One of the quiet wins of graph thinking: a huge amount of what people burn model tokens on is really an edge, and edges are free\. - - - -python - -人们很容易下意识新建智能体来 “合并多份结果”,但一定要避免这种做法。 如果合并操作仅需扁平化、去重,直接用 `results.flatMap(...)` 搭配集合(Set)即可;运算结果确定、瞬时完成,完全不消耗模型 Token。 智能体应当留给需要主观判断的场景,而非单纯的数据流转处理。 倘若图谱里每条数据边都依靠智能体中转,相当于为自身的基础数据流链路持续付出不必要的算力成本。 - -```Python -The temptation is to spawn an agent to “combine the results.” Resist -it. If combining means flatten-and-dedupe, that’s results.flatMap(...) -and a Set — deterministic, instant, zero tokens. Save agents for -judgment, not for plumbing. A graph where every edge is an agent is a -graph paying rent on its own wiring. -``` - - - - - -## Fan out with *parallel\(\)* - -\#\# 用 parallel\(\) 实现任务并行分发 - - - -这是能大幅压缩整体资源开销的核心设计思路。当存在 N 个相互独立节点(例如 N 份待核查数据源、N 个待评审文件、N 条待审计路由)时,切勿采用串行链式执行方案。 - -This is the move that pays for everything\. When you have N independent nodes \- N sources to check, N files to review, N routes to audit \- you don’t chain them\. - - - -你告诉 Claude 把它们fan\-out(任务分解分发)并同时运行。在工作流中这就是 parallel\(\):Claude 接收一个延迟执行函数 thunk 数组,为每个 thunk 单独生成一个子智能体,全部并发执行,然后把结果数组返回给你。 - -You tell Claude to fan them out and run them at once\. In a workflow that’s parallel\(\): Claude takes an array of thunks and spawns one subagent per thunk, all executing concurrently, then hands you back the array of results\. - -![Image](https://internal-api-drive-stream.feishu.cn/space/api/box/stream/download/authcode/?code=ZDMyYzUzZmUxZjI4MGQ1ZmIyMGJhYmRkMzRlYmMxZTBfZTA3OTE0MWE3MWEyMjdlYjEyOTM2NzQxNDdhMTMwMDhfSUQ6NzY2NDkyMTcxMDI1Mjc4ODkxNl8xNzg1MzI4NTM1OjE3ODU0MTQ5MzVfVjM) - -两个细节让它稳健。第一,parallel\(\) 是一个屏障——它等所有 thunk 完成才返回,所以下一阶段看到的是完整集合。第二,抛异常的 thunk 解析为 null 而不是拒绝整个批次,所以一个不稳定的智能体不会搞砸整个运行。 - -Two details make it robust\. First, parallel\(\) is a **barrier** \- it waits for every thunk before it returns, so the next stage sees the complete set\. Second, a thunk that throws resolves to null instead of rejecting the whole batch, so one flaky agent can’t sink the run\. - - - -总是对结果做 \.filter\(Boolean\)。并发量受限于你的核心数,多余的会排队,所以你可以传一百个 thunk 它们都会完成——只是每次跑一小批。 - -Always \.filter\(Boolean\) the results\. Concurrency is capped around your core count and the excess queues, so you can pass a hundred thunks and they’ll all finish \- just a handful at a time\. - - - -python - -1\. phase\('Research'\); -// 九个来源,九个智能体,全部同时。 -const raw = await parallel\( - SOURCES\.map\(\(s\) =\> \(\) =\> - agent\(s\.prompt, \{ - label: \`research:$\{s\.key\}\`, - phase: 'Research', - schema: ITEM\_SCHEMA, // 每个节点返回验证过的 JSON - agentType: 'general\-purpose', - \}\), - \), -\); -const collected = raw\.filter\(Boolean\); // 丢掉失败智能体的 null - -```Python -phase('Research'); -// Nine sources, nine agents, all at once. -const raw = await parallel( - SOURCES.map((s) => () => - agent(s.prompt, { - label: `research:${s.key}`, - phase: 'Research', - schema: ITEM_SCHEMA, // each node returns validated JSON - agentType: 'general-purpose', - }), - ), -); -const collected = raw.filter(Boolean); // drop the nulls from failed agents -``` - -fan\-out(分发并行执行)存在于 Claude 写的代码中,不在模型对话中。Claude 自己的上下文从不同时持有九个来源——每个子智能体携带自己的上下文,只有最终答案回来。 - -The fan\-out lives in code Claude wrote, not in a model conversation\. Claude’s own context never holds nine sources at once \- each subagent carries its own, and only the final answer comes back\. - - - -这就是让 Claude 把工作流扩展到几十或几百个子智能体而不淹没会话的原因。编排层不消耗 token,因为它不是 Claude 思考的另一轮。 - -That’s what lets Claude scale a workflow to **dozens or hundreds of subagents** without drowning the session\. The orchestration layer costs zero tokens because it isn’t another turn of Claude thinking\. - - - - - -## Fan in at a *barrier* - -\#\# 基于屏障节点完成结果扇入聚合 - -Fan\-in :结果聚合,汇总归约 - - - -fan\-out只有在有东西收集它时才有用。fin\-in是边汇聚的节点——一个智能体(或一段代码)同时看到所有上游结果,并做需要整个集合才能做的事:跨来源去重、按影响排序、总量为空时提前退出。这是屏障值得其墙钟成本的唯一地方。 - -A fan\-out is only useful if something gathers it\. The fan\-in is the node where edges converge \- where one agent \(or one piece of code\) sees *all* the upstream results at once and does something that requires the whole set: dedupe across sources, rank by impact, early\-exit if the total came back empty\. This is the one place a barrier earns its wall\-clock cost\. - -![Image](https://internal-api-drive-stream.feishu.cn/space/api/box/stream/download/authcode/?code=YjBmNzYxM2E3ODNhZGVlZWE1ZTIyNzRlOTQwM2JhMWJfNzhlNzg1MDJhMmQ2NDg4YjNjYWY3NTNlNDQxNzhlNDhfSUQ6NzY2NDkyMTcyNDIxNjk3MDQyNV8xNzg1MzI4NTM1OjE3ODU0MTQ5MzVfVjM) - -保障任务图谱高效运行的核心准则: - -仅当某一阶段确实需要集齐所有前置任务结果时,才使用屏障同步机制。需要对全部数据源统一去重?那就启用屏障 —— 这么做是合理的。 - -The rule that keeps graphs fast: **use a barrier only when a stage genuinely needs every prior result together\.** Deduping across all sources? Barrier \- correct\. - - - -python - -1\. // 边:纯 JS,无智能体,零 token。 -const flat = collected\.flatMap\(\(c\) =\> c\.items\); -log\(\`Collected $\{flat\.length\} items\`\); -phase\('Curate'\); -// 屏障节点:需要整个集合来去重\+排序。 -const curated = await agent\( - \`Dedupe and rank these by impact:\\n$\{JSON\.stringify\(flat\)\}\`, - \{ phase: 'Curate', schema: CURATED\_SCHEMA \}, -\); - -```Python -// The edge: plain JS, no agent, zero tokens. -const flat = collected.flatMap((c) => c.items); -log(`Collected ${flat.length} items`); -phase('Curate'); -// The barrier node: needs the WHOLE set to dedupe + rank. -const curated = await agent( - `Dedupe and rank these by impact:\n${JSON.stringify(flat)}`, - { phase: 'Curate', schema: CURATED_SCHEMA }, -); -``` - - - -如果只是单纯对列表做扁平化处理?直接依托数据流边内联处理即可。 有一套直白严苛的判断标准:倘若你的代码写成「并行分发 → 数据转换 → 再次并行分发」,且中间转换步骤不存在多条数据之间的交叉依赖,那本该改用流式管道处理,完全省去屏障同步。 - -Just flattening a list? That’s an edge, do it inline\. The smell test is brutal and simple: if you wrote parallel → transform → parallel, and that middle transform has no cross\-item dependency, you should have used a pipeline and skipped the barrier entirely\. - - - - - -## The *diamond*: split → work → merge - -\#\# 菱形执行模型:拆分任务 → 并行处理 → 汇总合并 - - - -把fan\-out和fan\-in放在一起,你就得到了每个严肃智能体图的主力拓扑:菱形执行模型。 - -Put fan\-out and fan\-in together and you get the workhorse topology of every serious agent graph: the **diamond**\. - - - -单个节点负责任务拆分,大量节点并行执行处理,再由单个节点汇总合并。市场信息巡检、依赖项审计、代码评审、深度调研报告等场景底层都采用这套拓扑结构;只需替换数据源与提示词,同一套基础框架就能适配各类业务需求。 - - - -One node splits the job, many nodes do the work in parallel, one node merges\. It’s the shape behind a market scan, a dependency audit, a code review, a research report \- swap the sources and prompts and the same skeleton adapts\. - -![Image](https://internal-api-drive-stream.feishu.cn/space/api/box/stream/download/authcode/?code=M2Q5NWJiOGI4ZGIxMjU4YTkxZDliZWYwNmI3NmM1NzVfYjZhMjllZmY5ZTczNWZhMTI3YTYzNzY3MDQ4ZmQyMGJfSUQ6NzY2NDkxMDIxNzY5NjcwOTU3MF8xNzg1MzI4NTM1OjE3ODU0MTQ5MzVfVjM) - -这套标准范式有个值得牢记的命名:扇出分发 → 归约处理 → 汇总生成。 扇出阶段拓宽信息覆盖范围;依靠原生代码做归约,精简海量原始数据;最后交由终端智能体整合输出完整结论。 - -The canonical form has a name worth memorizing: **fan out → reduce → synthesize\.** Fan out to gather breadth, reduce with plain code to compress it, synthesize with a final agent to write the answer\. - - - -一旦你看到了菱形,你就不再问「如何让我的智能体做更多步骤」,而是开始问「拆分在哪里,合并在哪里」——这才是真正能扩展的问题。 - -Once you see the diamond, you stop asking “how do I make my agent do more steps” and start asking “where’s the split, where’s the merge” \- which is the question that actually scales\. - - - - - -## Route the edge at runtime with a *conditional* - -\#\# 通过条件分支在运行时动态路由数据流边 - - - -并非所有任务图谱都是静态固定的。有时要走哪条数据流边,取决于上游节点的输出结果。路由节点会解析返回数据,判定启用哪一条下游执行分支:比如先对工单分类,再分流至对应的处理程序;或是先判断代码变更体量,再选择快速简易评审,或是启动完整全量审计。 - -Not every graph is fixed\. Sometimes the edge to take depends on what a node found\. A **router node** inspects a result and decides which downstream path fires \- classify the ticket, then branch to the right handler; check the diff size, then either do a quick review or spin up a full audit\. - - - -在工作流中这只是对节点验证输出的 JavaScript if 或 switch,因为控制流存在于代码中。 - -In a workflow this is just a JavaScript if or switch on a node’s validated output, because control flow lives in code\. - - - -这里确定性成了特性而非限制。路由器的决策可以是 Claude 驱动的(子智能体分类),但路由是 Claude 写的代码——所以对相同分类每次都以相同方式运行。 - -This is where determinism becomes a feature, not a limitation\. The router’s *decision* can be Claude\-powered \(a subagent classifies\), but the *routing* is code Claude wrote \- so it runs the same way every time for the same classification\. - -![Image](https://internal-api-drive-stream.feishu.cn/space/api/box/stream/download/authcode/?code=ZjBlZjIwZWRhZTc1ZjUxMDU3ZDQ4NDI0YzhiZTY1OGJfZTZkNGEyNTg3NDI4MjAxMjFjNzg0NjFmOWE2ZGQ1MDZfSUQ6NzY2NDkxMDM3NTA3OTE4NTM4NF8xNzg1MzI4NTM1OjE3ODU0MTQ5MzVfVjM) - - - -节点环节依托 Claude 的大模型判断能力,数据流边环节依靠脚本代码稳定可靠。不会出现意外情况 —— 比如莫名其妙触发 “Claude 自行跳过审计流程”,因为跳过流程的逻辑必须显式定义在任务图谱中,而当前图谱并未配置该分支。 - -You get Claude’s judgment at the node and the script’s reliability at the edge\. No emergent “Claude decided to skip the audit” surprises \- because the skip would have to be written into the graph, and it isn’t\. - - - -python - -1\. // 路由节点:智能体分类,代码选边。 -const \{ severity \} = await agent\( - \`Classify this diff's risk:\\n$\{diff\}\`, - \{ schema: \{ type: 'object', - properties: \{ severity: \{ enum: \['low', 'high'\] \} \}, - required: \['severity'\] \} \}, -\); -let review; -if \(severity === 'high'\) \{ - // 重路径:全面并行审计 - review = await parallel\(FILES\.map\(\(f\) =\> \(\) =\> agent\(\`Audit $\{f\}\`\)\)\); -\} else \{ - // 轻路径:一次快速审查 - review = await agent\(\`Quick review of $\{diff\}\`\); -\} - -```Python -// Router node: an agent classifies, code picks the edge. -const { severity } = await agent( - `Classify this diff's risk:\n${diff}`, - { schema: { type: 'object', - properties: { severity: { enum: ['low', 'high'] } }, - required: ['severity'] } }, -); -let review; -if (severity === 'high') { - // heavy path: full parallel audit - review = await parallel(FILES.map((f) => () => agent(`Audit ${f}`))); -} else { - // light path: one quick pass - review = await agent(`Quick review of ${diff}`); -} -``` - - - - - -## Put a *verifier* on the edge - -\#\# 在数据流边配置放验证器 - - - -任务图谱真正的核心优势,不在于创建更多智能体,而在于你能搭建一套标准化流程框架包裹智能体,让输出结果具备可信赖度。 - -The real leverage of a graph isn’t more agents \- it’s the structure you can wrap around them to produce confidence\. - - - -校验节点部署在数据流边上,数据放行至下游前会先经过它,它的唯一作用就是筛除无效检测结果。数据校验通过才可继续流转;一旦校验失败,该结果就不会参与最终结论的生成。 - -A verifier node sits on the edge before a result is allowed downstream, and its only job is to try to *kill* the finding\. If it survives, it passes\. If not, it never reaches the answer\. - - - -![Image](https://internal-api-drive-stream.feishu.cn/space/api/box/stream/download/authcode/?code=Y2YxMmE4OTE3YTIzZjg0ODRiYWJlOGFhYjU5M2E0OTZfYWNmNDMyYWVlZWIyODU4YTJlYTk0ZTZhMzUxMThiOTdfSUQ6NzY2NDkxMDQ1MzUwNDU2MDA4MF8xNzg1MzI4NTM1OjE3ODU0MTQ5MzVfVjM) - -有三种模式值得掌握。 - -Three patterns are worth having in your hands\. - -对抗式校验:针对每一条检测结论,启动 N 个独立质疑智能体,指令其逐条反驳该结论;仅当多数质疑校验均未推翻结论时,才保留这条结果。 - -多视角差异化校验:为每个校验器分配专属评判维度 —— 逻辑正确性、安全风险、问题可复现性;多元视角能捕捉到批量相同校验无法发现的各类失效场景。 - -评审专家组机制:从不同角度生成 N 套解决方案,并行交由多名评审打分;以最优方案为基底汇总输出,同时吸纳其他备选方案中的优质内容。 - -- **Adversarial verify:** for each finding, spawn N independent skeptics prompted to refute it; keep it only if a majority survive\. - -- **Perspective\-diverse verify:** give each verifier a distinct lens \- correctness, security, does\-it\-reproduce \- because diversity catches failure modes that N identical checks never will\. - -- **Judge panel:** generate N attempts from different angles, score them with parallel judges, synthesize from the winner while grafting the best of the runners\-up\. - -这正是让一个真实团队移植 Bun 运行时并把对抗性代码审查内建到循环中的模式。 - -This is exactly the pattern that let a real team port the Bun runtime with adversarial code review baked into the loop\. - - - - - -## Isolate nodes so one failure can’t *poison* the graph - -\#\# 节点隔离,避免单点故障污染整个任务图谱 - -### - -串行链路中故障会连锁扩散:若节点 C 执行失败,节点 D 将无法启动,整条流程直接中断。而在任务图谱架构里,故障应当被限制在发生问题的单个节点内。 - -In a chain, a failure cascades \- C dies, D never runs, the whole thing halts\. In a graph, failure should be **contained to its node\.** - - - -这一点其实已经部分实现:在 `parallel()` 内部,若某个延迟执行函数抛出异常,其结果会解析为 `null`;即便有一个子代理执行失败,其余八个正常的子代理仍能正常返回数据。调用 `.filter(Boolean)` 过滤空值,就是实现故障隔离的手段。 - -That’s already partly true: a thunk that throws inside parallel\(\) resolves to null, so eight good agents still return while one bad one drops out\. Your \.filter\(Boolean\) is the containment\. - - - -设计每一处扇入聚合逻辑时,都要兼容缺失输入数据,不要默认所有上游结果都齐全。 - -Design every fan\-in to tolerate missing inputs rather than assume a full set\. - -![Image](https://internal-api-drive-stream.feishu.cn/space/api/box/stream/download/authcode/?code=OTNmZmEwNDUwMGI5N2UwN2RhZGZmMDk0NjA1MDUwZDlfMjYyMjY0N2EyMzNkOGI4ZTcwNjY5YTQ2OTIwMDUxYmRfSUQ6NzY2NDkxMDU0MjgwODg3ODM0OV8xNzg1MzI4NTM1OjE3ODU0MTQ5MzVfVjM) - -更微妙的失败是节点互相踩踏。当智能体并行写文件时,它们可能冲突。 - -The subtler failure is nodes stepping on each other\. When agents write files in parallel, they can collide\. - - - -解决方案是隔离:worktree——每个智能体在自己的 git worktree 中运行,在沙箱中做工作,然后干净地合并。 - -The fix is isolation: "worktree" \- each agent runs in its own git worktree, does its work in a sandbox, and merges cleanly\. - - - -仅当多个节点确实并行执行时,才启用屏障同步机制。屏障是专为并行拓扑配套的安全防护措施,并非所有流程运行都必须承担的固定性能开销。 - -Reach for it only when nodes actually write in parallel\. it’s the seatbelt for the one topology that needs it, not a default tax on every run\. - - - - - -## Add a cycle \- but make it *converge* - -\#\# 加一个循环——但要让它收敛 - - - -有时你直到开始才知道工作有多大:未知规模的探索、发现一个bug会暴露三个更多的bug扫描。这需要循环——一条回到更早节点的受控边。 - -Sometimes you don’t know how big the job is until you’re in it: unknown\-size discovery, a bug sweep where finding one bug reveals three more\. That needs a **cycle** \- a controlled edge back to an earlier node\. - - - -危险很明显:不收敛的循环是无限循环,会生成智能体直到你的预算耗尽。 - -The danger is obvious: a cycle that doesn’t converge is an infinite loop that spawns agents until your budget is gone\. - -![Image](https://internal-api-drive-stream.feishu.cn/space/api/box/stream/download/authcode/?code=NjJlOTJhZmMwNjdkMzY1ZDgzNzNlNWE4MTI1MjkwOGVfOTRmNmQ5MGQyY2Q3ZmFjNjQwMzY1NGRjYjQ5ZDNiMjVfSUQ6NzY2NDkyMTkyNzk1Mzg0NTUxNV8xNzg1MzI4NTM1OjE3ODU0MTQ5MzVfVjM) - -这套收敛模式叫做「循环直至无新增结果」:持续启动检索探查节点,连续 K 轮都不再产出新内容时,才终止循环。决定该模式成败的关键细节 —— 也是绝大多数人初次实现都会踩的坑 —— 是去重比对所依据的基准对象 - -The pattern that converges is **loop\-until\-dry:** keep spawning finders until K consecutive rounds surface nothing new, then stop\. The one detail that makes or breaks it \- and the mistake almost everyone makes the first time \- is what you dedupe against\. - - - -去重校验要和所有已检索到的条目做比对,不能只拿已确认通过的结果去重。否则被校验驳回的无效结论会每一轮重复出现,循环永远无法收敛;最终你的系统会持续消耗算力,反复挖掘完全相同的无效死胡同。 - -Dedupe against everything *seen*, not just against confirmed results\. Otherwise rejected findings reappear every round, the loop never runs dry, and you’ve built a machine that pays to rediscover the same dead ends forever\. - - - -```Plain Text -const seen = new Set(); const confirmed = []; let dry = 0; - -while (dry < 2) { // stop after 2 empty rounds - const found = (await parallel( - FINDERS.map((f) => () => agent(f.prompt, { schema: BUGS })) - )).filter(Boolean).flatMap((r) => r.bugs); - - const fresh = found.filter((b) => !seen.has(key(b))); - if (!fresh.length) { dry++; continue; } // nothing new → toward dry - dry = 0; - fresh.forEach((b) => seen.add(key(b))); // dedupe vs SEEN, not confirmed - - // diverse-lens verify each fresh finding before it counts - const judged = await parallel(fresh.map((b) => () => - parallel(['correctness', 'security', 'repro'].map((lens) => () => - agent(`Judge "${b.desc}" via ${lens} — real?`, { schema: VERDICT }))) - .then((v) => ({ b, real: v.filter(Boolean).filter((x) => x.real).length >= 2 })))); - - confirmed.push(...judged.filter((v) => v.real).map((v) => v.b)); -} -``` - - - - - -## Tier the models across the *nodes* - -\#\# 按节点职能分层选用梯度规格的大模型 - -## - -不是每个节点都需要你最好的模型。图让这变得明显,而单个智能体永远不会:有些节点是有界且重复的(提取这个字段、分类这个工单),有些承载真正的判断(综合报告、裁决发现)。 - -Not every node needs your best model\. A graph makes this obvious in a way a single agent never does: some nodes are bounded and repetitive \(extract this field, classify this ticket\), and some carry the real judgment \(synthesize the report, adjudicate the finding\)\. - - - -无聊的节点用更便宜的模型跑,把你的昂贵 token 花在判断真正存在的地方。 - -Run the boring nodes on a cheaper model and spend your expensive tokens where judgment actually lives\. - -![Image](https://internal-api-drive-stream.feishu.cn/space/api/box/stream/download/authcode/?code=OGYxOGJkNDBhZjExYmJhMTA0NGE5ZTZhM2I5MDRkYWFfYTk0M2Y2OGMwZDI2YWZkMDhmYTcwZmUzZTE0MmU1MDVfSUQ6NzY2NDkxMDY2NTA2ODgwOTQ5OF8xNzg1MzI4NTM1OjE3ODU0MTQ5MzVfVjM) - -在工作流中,Claude 生成的每个子智能体默认继承你的会话模型,除非脚本覆盖它——所以默认情况下一次大运行全部按你的会话层级计费。单个 agent\(\) 调用上的 model 选项告诉 Claude 只把那个节点路由到别处。 - -In a workflow every subagent Claude spawns inherits your session model unless the script overrides it \- so by default a big run bills entirely at your session tier\. The model option on a single agent\(\) call tells Claude to route just that node elsewhere\. - - - -大运行前检查 /model,然后让 Claude 把扇出的重复节点路由到更便宜的模型,把合并节点留在高端。这就是把一个吃 token 的图从昂贵变成经济的杠杆,不需要改变它的形状。 - -**Check /model before a large run,** then have Claude route the fan\-out’s repetitive nodes down to a cheaper model and keep the merge node up\. This is the lever that turns a token\-hungry graph from expensive into economical without touching its shape\. - - - - - -## Topology *is* your cost and latency - -\#\# 拓扑就是你的成本和延迟 - - - -任务图谱的拓扑结构绝非锦上添花的表面设计,而是直接影响实际运行耗时的核心关键变量。绝大多数人都会踩坑的一个选择:`parallel()`(并行)与 `pipeline()`(流式管道)。`parallel()` 依赖屏障机制,所有分支必须等待最慢的节点执行完毕,下一阶段才能启动。 - -The shape of the graph isn’t cosmetic \- it’s the single biggest lever on wall\-clock time\. The choice that trips everyone up: parallel\(\) versus pipeline\(\)\. A parallel\(\) barrier makes *everything* wait for the slowest node before the next stage starts\. - - - -`pipeline()` 以流式方式让每条数据独立走完全流程,全程无屏障阻塞:条目 A 处理到第三阶段时,条目 B 可能还停留在第一阶段。处理速度快的数据会提前完成,不会卡在慢速任务后方空等。 - -A pipeline\(\) streams each item through all stages independently, with no barrier \- item A can be in stage 3 while item B is still in stage 1\. Fast items finish early instead of idling behind slow ones\. - -![Image](https://internal-api-drive-stream.feishu.cn/space/api/box/stream/download/authcode/?code=YWFjODU1YjQyZTkyM2FkZGFlYTMyMDgxYmRmOWU3ZjhfNTU2MjFmMzljNTYyY2RlYjM1YjA1NGEwN2Q1ODc5YzdfSUQ6NzY2NDkxMDc1MTU4NDczNDQyNl8xNzg1MzI4NTM1OjE3ODU0MTQ5MzVfVjM) - - - -默认采用流式管道 `pipeline()`;仅当某一阶段确实需要一次性拿到上游全部结果时,才使用屏障机制 —— 例如全量数据去重、基于全部统计结果提前终止流程、需要对比多条检索结论的综合提示词场景。 “代码写起来更整洁”“各阶段逻辑划分清晰” 都不足以作为启用屏障的理由;屏障带来的延迟是真实、可量化、纯粹浪费的耗时。逻辑分层清晰,不等于必须同步阻塞等待所有任务。 - -**Default to pipeline\(\)\.** Reach for a barrier only when a stage truly needs every prior result at once \- a cross\-set dedupe, an early\-exit on the total, a prompt that compares against “the other findings\.” “It’s cleaner code” and “the stages feel separate” are not reasons; barrier latency is real, measurable, wasted time\. Separate is not the same as synchronized\. - - - - - -## Let Claude draw the graph \- *self\-routing* - -\#\# 交由 Claude 自主生成任务图谱:自路由机制 - - - -最后一种方案:对于无法预先规划流程的任务,不再手动编写固化的任务图谱。 - -The final move is to stop drawing the graph by hand for jobs you can’t plan in advance\. - - - -借助动态工作流,你只需描述最终目标,Claude 会自行生成任务编排脚本:拆解整体任务、选择并行分发策略、调度一组协同工作的子智能体,最后整合输出结论。系统会生成适配本次任务专属的流程拓扑,而非一套寄希望于能适配所有场景的固化图谱。 - -With **dynamic workflows**, you describe the objective and Claude writes the orchestration script itself\- decomposing the task, choosing the fan\-out, spawning a coordinated fleet of subagents, and synthesizing the result\. You get a graph tailored to *this* run instead of a fixed one you hoped would fit\. - -![Image](https://internal-api-drive-stream.feishu.cn/space/api/box/stream/download/authcode/?code=N2MzMWVjNDVkMmUwZDZmZmYzYzZkZTYxN2NlNDA0MTRfYTVmODRmZDk5Nzk4NzVhZjljMWMxNWVhMjQxZDBlYmNfSUQ6NzY2NDkxMDgxNjU4OTk5MDg4NV8xNzg1MzI4NTM1OjE3ODU0MTQ5MzVfVjM) - -一共有三种启用动态工作流的方式: - -第一种,在提示词中直接写明 “workflow(工作流)”,Claude 会为当前任务自动生成一套流程; - -第二种,调用已保存或打包好的预置工作流 ——`/deep-research` 就是一套已投产上线的标准任务图谱,完整链路为:范围界定 → 并行检索 → 内容拉取 → 对抗式校验 → 结果综合,和本教程讲解的标准框架完全一致。 - -There are three ways in\. Say the word **“workflow”** in your prompt and Claude writes one for the task\. Run a saved or bundled one \- /deep\-research is a real graph shipping in production: scope → parallel search → fetch → adversarial verify → synthesize, the exact skeleton from this course\. - - - -第三种方式:开启极速代码模式(ultracode),会话内每一项有一定复杂度的任务,Claude 都会自动规划专属工作流。 某次执行效果理想时,按下快捷键 `s` 即可将编排脚本保存至 `.claude/workflows/` 目录;脚本自带版本管理,可通过名称重复调用,任何克隆该代码仓库的人都能直接启动这套任务图谱。 - -Or turn on ultracode and Claude plans a workflow for every substantial task in the session\. When a run is good, press s to save its script into \.claude/workflows/ \- version\-controlled, re\-runnable by name, a graph anyone who clones the repo can launch\. - - - -python - -执行一套工作流,对 `src/routes/` 下所有接口路由开展鉴权缺失审计:为每一个路由文件单独创建一个子智能体,每条检测结果都先经过校验,再输出审计报告。 ● Claude 已生成任务编排脚本・后台启动执行中… /workflows — auth\-audit(鉴权审计工作流)・运行中 ✓ 范围界定 1/1,消耗 2100 Token,耗时 4 秒 ✓ 并行扇出 18/18,18 个路由文件各分配独立子智能体 ◯ 校验环节 11/18,每条检测结论配备 3 票制对抗质疑校验器…… ○ 汇总合成 0/1,等待校验阶段完成 会话全程保持可交互状态,子智能体集群后台持续运算不阻塞界面 - -```Python -› Run a workflow to audit every route under src/routes/ for missing -auth. Spawn one agent per route file, then verify each finding before -reporting. ● Claude wrote an orchestration script · launching in -background… /workflows — auth-audit · running ✓ Scope 1/1 2.1k tok · -4s ✓ Fan-out 18/18 one agent per route file ◯ Verify 11/18 3-vote -skeptics per finding… ○ Synthesize 0/1 waiting on verify session stays -responsive — keep working while the fleet runs -``` - - - -\#\# 本周用 Claude 构建的六个图 - -## Six graphs to build with *Claude* this week - -![Image](https://internal-api-drive-stream.feishu.cn/space/api/box/stream/download/authcode/?code=ODcyMWUwMDMxYmU4Yjc2ZDgyYzIxODllNjkwZTU2NzlfMmViMzY3NzM4MzBmZWMxMDU1MzYzMjA4Yzc3NjlkZjNfSUQ6NzY2NDkxMDkwODkxNzI3MTUwNV8xNzg1MzI4NTM1OjE3ODU0MTQ5MzVfVjM) - -- **Security sweep across every route\. **Claude spawns **one subagent per route file**, each hunting for missing auth checks, then a verifier pass confirms every finding before it reaches the report\. Breadth no single context could hold\. - -全链路安全扫描 - -对每一条业务路径执行安全巡检。Claude 会为每个路由文件单独生成一个子智能体,分别检索缺失的权限校验逻辑;所有风险点需经过校验器二次核验,确认无误后才会录入报告。覆盖广度远超单一上下文所能承载的分析范围。 - - - -- **Cited report with /deep\-research\.** A graph that ships in Claude Code already\. Claude decomposes your question into distinct angles, runs parallel searches, dedupes sources, then **adversarially verifies every claim** with three\-vote skeptics before writing\. - -基于 /deep\-research 指令生成引证调研报告 - -该功能图谱已内置在 Claude Code 中。Claude 将你的查询拆解为多个独立分析维度,并行执行多源检索、去重重复资料;随后启用三方复核机制对每一条结论交叉质证,全部核验通过后再撰写最终报告。 - - - -- **Port a module, file by file\.** The Bun ceiling, scaled to your repo\. Claude fans out translation across files, runs the test suite as a gate on each, and loops the failures back \- **adversarial review** catching what a single pass would ship broken\. - -逐文件迁移代码模块 - -突破 Bun 运行时性能上限,适配你的完整代码仓库。Claude 并行批量转换多个文件,每完成一份文件就执行全套测试用例作为准入关卡;编译 / 测试失败的文件会回流重处理。对抗式复核机制能揪出单次粗转极易遗留的代码缺陷。 - - - -- **Adversarial review of a diff\.** Claude routes on diff size: a small change gets one quick pass, a large one triggers a **full parallel audit** with reviewers on distinct lenses \- correctness, security, performance \- then a judge panel synthesizes\. - -代码变更差分对抗评审 - -Claude 根据代码改动规模分流评审策略:小幅变更执行一轮快速核查;大规模改动将启动全量并行审计,多评审智能体分别从逻辑正确性、安全漏洞、运行性能三大维度独立审查,最后由合议模块整合各方结论输出综合意见。 - - - -- Ecosystem scan on a schedule\. Save it once, re\-run it forever\. Claude checks many sources in parallel \- eleases, blogs, discussion \- ranks by impact at a barrier, and writes the digest\. **Version\-controlled in \.claude/workflows/**, launchable by name\. - -定时生态全域巡检 - -配置一次,永久复用。Claude 并行拉取多渠道信息:版本发布公告、技术博客、社区讨论帖;按业务影响权重过滤低价值内容,自动生成精简摘要。工作流配置存入版本库 `.claude/workflows/` 目录,支持通过名称一键启动。 - - - -- **Discovery of unknown size\.** You don’t know how many bugs are there\. Claude runs finders in parallel, dedupes each new find **against everything seen**, verifies survivors, and keeps looping until two rounds turn up nothing new \- then stops\. - -未知范围全域漏洞探查 - -适用于漏洞总量未知的场景。Claude 并行启动多套漏洞检索器,新发现问题自动与已有结果去重,留存条目全部完成核验;持续循环检索,连续两轮无新增问题后自动终止扫描。 - - - - - -## Conclusion: - -\#\# 结语: - - - -提示者问问题。架构师画图。 - -A prompter asks a question\. An architect draws a *graph\.* - - - -线性智能体从来不是天花板——它只是第一个形状,每个人都会伸手去拿的那个,因为它匹配我们打字的方式。一行,一个脑袋,一次一件事。 - -The linear agent was never the ceiling \- it was just the first shape, the one everyone reaches for because it matches how we type\. **One line, one head, one thing at a time\.** - - - -一旦你能看到节点和边,你就不再要求智能体做更多,而是要求图做得更宽:在独立工作处扇出,在信心要紧处给边加门,在判断不需要处分层模型。 - -Once you can see the nodes and the edges, you stop asking the agent to do more and start asking the graph to do it wider: fan out where the work is independent, gate the edges where confidence matters, tier the models where judgment doesn’t\. - - - -大多数人会继续排队走直线。学会画图的人将运行一个舰队——而永远不会注意到其他人卡在的天花板。 - -Most people will keep queueing steps in a line\. **The ones who learn to draw the graph will run a fleet** \- and never notice the ceiling the rest are stuck under\. - diff --git a/docs/build-report.md b/docs/build-report.md deleted file mode 100644 index 4ef28b9..0000000 --- a/docs/build-report.md +++ /dev/null @@ -1,148 +0,0 @@ -# graphlemon 构建报告 - -**六个 Phase,45 个 Unit,一套真正跑起来的"熄灯"软件工厂。** - -graphlemon 是一个 Level 5 自主研发系统:固定的 Planner → Coder → Validator Org Graph,驱动每个任务 -动态生成的 Work Graph,完全跑在 Claude Code 自己的 `claude -p` 后端之上——能写代码、能抓自己的回归, -到本次会话结束时,还能在无人读 diff 的情况下重新部署自己正在运行的进程。 - -- **状态**:PH0~PH5 全部完成,45 / 45 个 Unit -- **测试**:552 个测试全绿 -- **Git**:`fc94ae8`(初始基线)→ `59e96d3`(U-043,当前 tip) -- **数据模型**:17 张 sqlite 表 -- **Unit 分类**:Risk Gate 18 / Value Path 17 / Enabler 10 -- **过程中"停下来问用户"的关键决策**:3 次 - ---- - -## Phase 链路 - -``` -PH0(9 units,Enabler)→ PH1(6 units,MVP 闭环)→ PH2(7 units,质量闭环) - → PH3(10 units,控制台)→ PH4(3 units,全量回归收口)→ PH5(10 units,自举能力) -``` - ---- - -## PH0:脚手架与第一个高风险假设 - -**Unit**:U-001 ~ U-008、U-004.5 - -15 张核心表的数据模型、Hermes 人设迁移到 `.claude/agents/*.md`、一套受 `RUN_REAL_AGENT_E2E` 环境变量 -门控的真实 API 测试 harness,以及本地后端/前端的骨架——后面一切工作的地基。 - -> **假设验证——不是"干净通过"**:A-2 假设在这里被真正验证:Planner/Validator 的人设几乎可以原样迁移, -> 但 Coder 的 `SOUL.md` 不行——里面硬编码了一个真实的飞书 Base API token、整段"认领"机制强绑定 -> `lark-cli`、还指示自己再跑一次 `claude --dangerously-skip-permissions`,三处都必须删除或重写才能安全 -> 运行。 - ---- - -## PH1:第一条真正跑通的闭环 - -**Unit**:U-009 ~ U-014 - -用"实现贪吃蛇游戏"这个目标真实跑通全链路:Planner 产出 BRD→PRD→SDD→Benchmark,Coder 实现并自测, -Validator 复核并给出根因归属——架构文档 §11 验收标准第 1-3 条,是真跑出来的,不是模拟的。 - -> **假设确认**:A-1 假设在这里被确认——一次真实花费 $0.46 的 spike 证明了 Claude Code 的动态工作流原语 -> (`agent()`/`parallel()`/`pipeline()`)可以通过 `claude -p` 无交互调用,并返回可解析的结构化结果。 - -> **平台层怪癖(实测发现)**:`--json-schema` 一旦和 `--agent` 同时传,会被静默忽略——模型退化成把结构化 -> 内容塞进自由文本里的一个 ```` ```json ```` 围栏代码块,而不是填充 `structured_output`。官方文档没有 -> 说明,是靠真实调用发现的。 - ---- - -## PH2:教会它从自己的错误里恢复 - -**Unit**:U-015 ~ U-021 - -三视角根因判定、Org-level Loop 的 `fast_lane`/`full_replan` 路由、双层预算护栏、Claude plan 额度耗尽的 -等待重试机制、Graph Engine 断点恢复。人为植入一个 bug,从一端进去,从另一端出来时已经是一次真实的修复、 -重新验证、任务关闭。 - ---- - -## PH3:一个不需要构建步骤的控制台 - -**Unit**:U-022 ~ U-030、U-024.5 - -项目创建、目标输入、实时仪表盘、任务看板与详情页、DAG 图可视化、实时日志查看器、Artifact/Skill 浏览器、 -配置页——纯原生 HTML/CSS/JS,配一个手写的 WebSocket 服务端,因为"纯 stdlib、无第三方依赖、无构建步骤" -这条约束对 graphlemon 自己的控制台同样适用。 - -> **真实 bug**:HTTP 服务端各请求线程和 WebSocket 轮询线程共享同一个 sqlite 连接时,如果关闭连接没有 -> 持有和读写操作同一把锁,会**直接把解释器段错误崩溃**——"关闭"看起来像一次性操作,但对同一个连接对象 -> 而言并不天然安全。修复后连续压测多次全部稳定。 - -> **后期补漏**:一次收口审计中发现,WebSocket 重连后任务看板和任务详情页从未真正重新拉取过状态快照—— -> 只有仪表盘做出反应,而且也只是切换了一个提示条。现在三处重连后都会真正刷新,符合 REQ-12 自己的边界 -> 用例要求。 - ---- - -## PH4:两轮审计,十个真实缺口 - -**Unit**:U-031 ~ U-033 - -把 06 节 REQ-01~REQ-14 的全部验收用例,逐条对照实际测试覆盖重新核对了两遍——而不是相信"单测全绿"就 -等于"验收要求真的被覆盖了"。 - -> **结构性缺口**:Work Graph 的三个核心控制器(规划、执行、单节点运行)各自都做过完整单测,但**从来没有 -> 在生产代码里被真正串联调用过**——唯一验证过的闭环(PH1 的贪吃蛇场景)悄悄绕开了它们,走了一条更简单 -> 的并行路径。补上这层编排后,真实 API 调用又暴露出两个新 bug(一个 manifest 字段名不匹配、一个缺失的 -> 工作目录),都是只有真实调用才会暴露的问题。 - -> **第二轮审计**:PH5 开工前对验收用例的又一次逐条复核,发现了 5 个新的真实缺口:一处遗漏的中间状态写入、 -> 一个从未真正测过的调度器场景、三条"节点被标记 blocked 但从未告知父任务"的失败路径、一个原本完全不存在 -> 的 PRD 一致性检查、外加一处偏弱的测试断言。其中一次修复(新增的一致性检查调用)当场抓到模型把 -> `consistent` 字段返回成 JSON **字符串** `"false"`,而 Python 的真值判断把它当成了 `True`。 - -> **假设正式收口**:A-2 假设在这里正式收口,结论仍是"部分成立"——PH0 之后又有四个不同 Unit 里真实调用 -> 过迁移后的人设,没有再暴露新的偏差。但结论不会因此升级为"confirmed":那次重写已经发生了,后面跑得再 -> 干净也不能把它抹掉。 - ---- - -## PH5:教会它重新部署自己 - -**Unit**:U-034 ~ U-043 - -一整套蓝绿自部署机制:在独立的 git worktree 里拉起候选实例、跑三项 cutover 前健康检查、切换生产流量、 -在监控窗口内采样四类信号、必要时回滚——包括"回滚本身也失败"这条兜底路径。 - -> **SDD 内部冲突**:设计文档和正式规范对"到底什么条件会触发一次自部署"有分歧——设计文档说"self-target -> 改动通过验收即可",规范原文却把它挂在一条已存在的 `core_engine` bug 记录上——而一次干净的自我改进任务 -> 根本不会产生这条记录。没有挑一种理解悄悄往下走,而是直接问了用户。 - -> **真实 bug**:用 `kill(pid, 0)` 做存活检测分不清"真的在跑"和"已经退出但从未被父进程回收"——一个僵尸 -> 进程能让这个检测正常通过。一个真实(未 mock)的、真的等待进程消失的测试因此一直挂起,直到补上正确的 -> 进程回收逻辑才修好。 - -> **其他发现**:缺一个跨重启追踪候选实例 checkout 目录的字段、后端服务完全没有命令行入口、参数表里唯独 -> 缺一个固定的生产端口、也没有 `/api/health` 端点给健康检查调用。 - ---- - -## ⚠️ 尚未真实执行的一项 - -U-042 的验收测试——对本仓库执行一次真实的自举 cutover,真实调用 `claude`、真实替换正在运行的进程——已经 -建好并且在 mock 下全部通过,但被一个刻意设置得更严格、和项目里其他真实测试用的环境变量**不同**的开关 -(`RUN_REAL_SELF_HOSTING_E2E=1`)挡住,因为这是整个代码库里爆炸半径最大的一个动作:它能替换掉正在运行 -的 graphlemon 进程本身,还会在这个真实仓库上执行 git revert。 - -**这项测试从未被真正执行过。** 是否要真的跑一次,是需要用户决定的事,不是构建过程自己能替你做的决定。 - ---- - -## 假设台账 - -| ID | 假设 | 结论 | -|---|---|---| -| A-1 | Claude Code 的动态工作流原语可以通过 `claude -p` 无交互调用,并返回可解析的结构化结果 | **confirmed** | -| A-2 | 本地 Hermes 人设文件(`SOUL.md`/`AGENTS.md`)可以原样迁移到 `.claude/agents/*.md`,无需大改 | **部分成立**(已收口) | - ---- - -*graphlemon · 纯 stdlib Python 实现,无任何第三方依赖 · 552 个测试 · 17 张表 · `fc94ae8` → `59e96d3`* diff --git a/docs/development-plan.md b/docs/development-plan.md index 28a091e..9d202ef 100644 --- a/docs/development-plan.md +++ b/docs/development-plan.md @@ -27,7 +27,7 @@ | PH2 | 质量闭环:回路、Skill 迭代、护栏、断点恢复 | Risk Gate | 架构文档 §11 验收标准第 4-5 条;REQ-04/05/06/08/10/14 | | PH3 | 人机交互控制台 | Value Path | REQ-11/12/13 | | PH4 | 全量验收与固化 | Risk Gate | 06 节全部 14 条 REQ 完整回归通过一次 | -| PH5(2026-07-30 新增) | 自举能力:Deploy Supervisor + 蓝绿部署自动回滚 | Risk Gate | `docs/supervisor-auto-rollback-design.md` 状态机全部路径(health_check_failed / rolled_back / rollback_failed / finalized)至少各复现一次;`self-development-feasibility.md` 9 步走查场景真实跑通一次 | +| PH5(2026-07-30 新增) | 自举能力:Deploy Supervisor + 蓝绿部署自动回滚 | Risk Gate | `docs/done/supervisor-auto-rollback-design.md` 状态机全部路径(health_check_failed / rolled_back / rollback_failed / finalized)至少各复现一次;`self-development-feasibility.md` 9 步走查场景真实跑通一次 | 依赖顺序:PH0 → PH1 → PH2,PH3 可在 PH1 完成后与 PH2 并行(控制台是对已产生数据的可视化层,不阻塞回路/护栏逻辑本身的开发),PH4 收口需要 PH1/PH2/PH3 全部完成。**PH5 依赖 PH4 收口**(自我修改的安全网必须建立在"系统本身已通过全量验收、行为可信"的前提上,对已知不稳定的系统做自动回滚没有意义),且 PH5 不阻塞、不改变 PH0-PH4 的开发方式——PH0-PH4 期间 graphlemon 自身仍是由人类 + Claude Code 交互式开发(这是任何自举系统的必然前提,见 `self-development-feasibility.md` §1),PH5 只在"PH0-PH4 都跑通之后,graphlemon 开始被允许对自己的代码做自主修改"这个新增能力上生效。 @@ -333,8 +333,8 @@ PH2-S1/S2/S3/S4 之间除标注的依赖外相互独立,可并行推进。 **命名说明**:本 Phase 的"Deploy Supervisor"和架构文档里"Supervisor(调度子 Agent,Coder Work Graph 内部监控节点)"是两个不同概念,详见 `supervisor-auto-rollback-design.md` 开头的命名说明,不要混用。 **Goal**:graphlemon 具备安全地自主修改自己代码(含 Org Agent 身份文件、调度器/执行器核心逻辑)的能力——出问题时自动回滚,不需要人工评审代码内容把关。 -**Phase 验收**:`docs/supervisor-auto-rollback-design.md` 状态机的四个终态(`health_check_failed`、`rolled_back`、`rollback_failed → blocked_pending_human`、`finalized`)都能真实触发一次;`self-development-feasibility.md` 第 3 节的 9 步走查场景改成真实执行(不是模拟),从"人类发现 bug"到"Deploy Supervisor 自动 finalize"全程无需人工介入代码评审。 -**设计依据**:`docs/supervisor-design-comparison.md`(方案选型)+ `docs/supervisor-auto-rollback-design.md`(状态机/数据模型/健康检查/回滚流程/MVP 参数)。 +**Phase 验收**:`docs/done/supervisor-auto-rollback-design.md` 状态机的四个终态(`health_check_failed`、`rolled_back`、`rollback_failed → blocked_pending_human`、`finalized`)都能真实触发一次;`self-development-feasibility.md` 第 3 节的 9 步走查场景改成真实执行(不是模拟),从"人类发现 bug"到"Deploy Supervisor 自动 finalize"全程无需人工介入代码评审。 +**设计依据**:`docs/done/supervisor-design-comparison.md`(方案选型)+ `docs/done/supervisor-auto-rollback-design.md`(状态机/数据模型/健康检查/回滚流程/MVP 参数)。 ### PH5-S1 数据模型与治理规则 diff --git a/docs/done/build-report.md b/docs/done/build-report.md new file mode 100644 index 0000000..4ef28b9 --- /dev/null +++ b/docs/done/build-report.md @@ -0,0 +1,148 @@ +# graphlemon 构建报告 + +**六个 Phase,45 个 Unit,一套真正跑起来的"熄灯"软件工厂。** + +graphlemon 是一个 Level 5 自主研发系统:固定的 Planner → Coder → Validator Org Graph,驱动每个任务 +动态生成的 Work Graph,完全跑在 Claude Code 自己的 `claude -p` 后端之上——能写代码、能抓自己的回归, +到本次会话结束时,还能在无人读 diff 的情况下重新部署自己正在运行的进程。 + +- **状态**:PH0~PH5 全部完成,45 / 45 个 Unit +- **测试**:552 个测试全绿 +- **Git**:`fc94ae8`(初始基线)→ `59e96d3`(U-043,当前 tip) +- **数据模型**:17 张 sqlite 表 +- **Unit 分类**:Risk Gate 18 / Value Path 17 / Enabler 10 +- **过程中"停下来问用户"的关键决策**:3 次 + +--- + +## Phase 链路 + +``` +PH0(9 units,Enabler)→ PH1(6 units,MVP 闭环)→ PH2(7 units,质量闭环) + → PH3(10 units,控制台)→ PH4(3 units,全量回归收口)→ PH5(10 units,自举能力) +``` + +--- + +## PH0:脚手架与第一个高风险假设 + +**Unit**:U-001 ~ U-008、U-004.5 + +15 张核心表的数据模型、Hermes 人设迁移到 `.claude/agents/*.md`、一套受 `RUN_REAL_AGENT_E2E` 环境变量 +门控的真实 API 测试 harness,以及本地后端/前端的骨架——后面一切工作的地基。 + +> **假设验证——不是"干净通过"**:A-2 假设在这里被真正验证:Planner/Validator 的人设几乎可以原样迁移, +> 但 Coder 的 `SOUL.md` 不行——里面硬编码了一个真实的飞书 Base API token、整段"认领"机制强绑定 +> `lark-cli`、还指示自己再跑一次 `claude --dangerously-skip-permissions`,三处都必须删除或重写才能安全 +> 运行。 + +--- + +## PH1:第一条真正跑通的闭环 + +**Unit**:U-009 ~ U-014 + +用"实现贪吃蛇游戏"这个目标真实跑通全链路:Planner 产出 BRD→PRD→SDD→Benchmark,Coder 实现并自测, +Validator 复核并给出根因归属——架构文档 §11 验收标准第 1-3 条,是真跑出来的,不是模拟的。 + +> **假设确认**:A-1 假设在这里被确认——一次真实花费 $0.46 的 spike 证明了 Claude Code 的动态工作流原语 +> (`agent()`/`parallel()`/`pipeline()`)可以通过 `claude -p` 无交互调用,并返回可解析的结构化结果。 + +> **平台层怪癖(实测发现)**:`--json-schema` 一旦和 `--agent` 同时传,会被静默忽略——模型退化成把结构化 +> 内容塞进自由文本里的一个 ```` ```json ```` 围栏代码块,而不是填充 `structured_output`。官方文档没有 +> 说明,是靠真实调用发现的。 + +--- + +## PH2:教会它从自己的错误里恢复 + +**Unit**:U-015 ~ U-021 + +三视角根因判定、Org-level Loop 的 `fast_lane`/`full_replan` 路由、双层预算护栏、Claude plan 额度耗尽的 +等待重试机制、Graph Engine 断点恢复。人为植入一个 bug,从一端进去,从另一端出来时已经是一次真实的修复、 +重新验证、任务关闭。 + +--- + +## PH3:一个不需要构建步骤的控制台 + +**Unit**:U-022 ~ U-030、U-024.5 + +项目创建、目标输入、实时仪表盘、任务看板与详情页、DAG 图可视化、实时日志查看器、Artifact/Skill 浏览器、 +配置页——纯原生 HTML/CSS/JS,配一个手写的 WebSocket 服务端,因为"纯 stdlib、无第三方依赖、无构建步骤" +这条约束对 graphlemon 自己的控制台同样适用。 + +> **真实 bug**:HTTP 服务端各请求线程和 WebSocket 轮询线程共享同一个 sqlite 连接时,如果关闭连接没有 +> 持有和读写操作同一把锁,会**直接把解释器段错误崩溃**——"关闭"看起来像一次性操作,但对同一个连接对象 +> 而言并不天然安全。修复后连续压测多次全部稳定。 + +> **后期补漏**:一次收口审计中发现,WebSocket 重连后任务看板和任务详情页从未真正重新拉取过状态快照—— +> 只有仪表盘做出反应,而且也只是切换了一个提示条。现在三处重连后都会真正刷新,符合 REQ-12 自己的边界 +> 用例要求。 + +--- + +## PH4:两轮审计,十个真实缺口 + +**Unit**:U-031 ~ U-033 + +把 06 节 REQ-01~REQ-14 的全部验收用例,逐条对照实际测试覆盖重新核对了两遍——而不是相信"单测全绿"就 +等于"验收要求真的被覆盖了"。 + +> **结构性缺口**:Work Graph 的三个核心控制器(规划、执行、单节点运行)各自都做过完整单测,但**从来没有 +> 在生产代码里被真正串联调用过**——唯一验证过的闭环(PH1 的贪吃蛇场景)悄悄绕开了它们,走了一条更简单 +> 的并行路径。补上这层编排后,真实 API 调用又暴露出两个新 bug(一个 manifest 字段名不匹配、一个缺失的 +> 工作目录),都是只有真实调用才会暴露的问题。 + +> **第二轮审计**:PH5 开工前对验收用例的又一次逐条复核,发现了 5 个新的真实缺口:一处遗漏的中间状态写入、 +> 一个从未真正测过的调度器场景、三条"节点被标记 blocked 但从未告知父任务"的失败路径、一个原本完全不存在 +> 的 PRD 一致性检查、外加一处偏弱的测试断言。其中一次修复(新增的一致性检查调用)当场抓到模型把 +> `consistent` 字段返回成 JSON **字符串** `"false"`,而 Python 的真值判断把它当成了 `True`。 + +> **假设正式收口**:A-2 假设在这里正式收口,结论仍是"部分成立"——PH0 之后又有四个不同 Unit 里真实调用 +> 过迁移后的人设,没有再暴露新的偏差。但结论不会因此升级为"confirmed":那次重写已经发生了,后面跑得再 +> 干净也不能把它抹掉。 + +--- + +## PH5:教会它重新部署自己 + +**Unit**:U-034 ~ U-043 + +一整套蓝绿自部署机制:在独立的 git worktree 里拉起候选实例、跑三项 cutover 前健康检查、切换生产流量、 +在监控窗口内采样四类信号、必要时回滚——包括"回滚本身也失败"这条兜底路径。 + +> **SDD 内部冲突**:设计文档和正式规范对"到底什么条件会触发一次自部署"有分歧——设计文档说"self-target +> 改动通过验收即可",规范原文却把它挂在一条已存在的 `core_engine` bug 记录上——而一次干净的自我改进任务 +> 根本不会产生这条记录。没有挑一种理解悄悄往下走,而是直接问了用户。 + +> **真实 bug**:用 `kill(pid, 0)` 做存活检测分不清"真的在跑"和"已经退出但从未被父进程回收"——一个僵尸 +> 进程能让这个检测正常通过。一个真实(未 mock)的、真的等待进程消失的测试因此一直挂起,直到补上正确的 +> 进程回收逻辑才修好。 + +> **其他发现**:缺一个跨重启追踪候选实例 checkout 目录的字段、后端服务完全没有命令行入口、参数表里唯独 +> 缺一个固定的生产端口、也没有 `/api/health` 端点给健康检查调用。 + +--- + +## ⚠️ 尚未真实执行的一项 + +U-042 的验收测试——对本仓库执行一次真实的自举 cutover,真实调用 `claude`、真实替换正在运行的进程——已经 +建好并且在 mock 下全部通过,但被一个刻意设置得更严格、和项目里其他真实测试用的环境变量**不同**的开关 +(`RUN_REAL_SELF_HOSTING_E2E=1`)挡住,因为这是整个代码库里爆炸半径最大的一个动作:它能替换掉正在运行 +的 graphlemon 进程本身,还会在这个真实仓库上执行 git revert。 + +**这项测试从未被真正执行过。** 是否要真的跑一次,是需要用户决定的事,不是构建过程自己能替你做的决定。 + +--- + +## 假设台账 + +| ID | 假设 | 结论 | +|---|---|---| +| A-1 | Claude Code 的动态工作流原语可以通过 `claude -p` 无交互调用,并返回可解析的结构化结果 | **confirmed** | +| A-2 | 本地 Hermes 人设文件(`SOUL.md`/`AGENTS.md`)可以原样迁移到 `.claude/agents/*.md`,无需大改 | **部分成立**(已收口) | + +--- + +*graphlemon · 纯 stdlib Python 实现,无任何第三方依赖 · 552 个测试 · 17 张表 · `fc94ae8` → `59e96d3`* diff --git a/docs/done/self-development-feasibility.md b/docs/done/self-development-feasibility.md new file mode 100644 index 0000000..5293231 --- /dev/null +++ b/docs/done/self-development-feasibility.md @@ -0,0 +1,68 @@ +# graphlemon 自举可行性分析:能不能自己开发自己 + +**日期**: 2026-07-29(2026-07-30 更新:缺口 2/3 已完成详细设计,见更新记录) +**性质**: 探索性分析,不是已确认的架构决策,未写入 `sdd-mvcp-contract.json`。若决定采纳,需要走一次正式的 AskUserQuestion 决策流程,再回填进 05-实施约束与依赖.md 和 contract.json。 +**结论先说**:机制上大部分已经具备(三个 Org Agent 落在目标项目 `.claude/agents/*.md`、`c_project_create` 对 `target_folder_path` 没有排除平台自身路径、git worktree 隔离已经是既定决策),但有三个真缺口会让"自己改自己"从"能跑"变成"跑了但可能很危险"。下面先列缺口,再用一个具体场景走一遍,标出每一步是"✅ 已具备"还是"⚠️ 缺口"。 + +**2026-07-30 更新记录**:用户就三个缺口逐条给出方向性决策(1. 确认无条件强制 worktree;2. 确认需要独立 supervisor 进程,可考虑复用 Hermes CLI;3. **明确否决"核心文件强制人工评审"的原提案**,改为"supervisor 自动监督回滚即可,不需要人工评审")。据此完成了 `docs/done/supervisor-design-comparison.md`(方案选型:不把 Hermes CLI 接入 supervisor 决策链路,改为复用 graphloop 已验证过的隔离/manifest/apply 库,见 `supervisor/isolation/`)和 `docs/done/supervisor-auto-rollback-design.md`(缺口 2 的蓝绿实例机制 + 缺口 3 的自动回滚机制完整状态机设计)。下面缺口 2/3 的原始描述保留作为问题记录,其"推荐方案"段落已更新为指向这两份新文档。 + +## 1. 前提:自举不是从零开始的 + +自我迭代能力只有在 PH0/PH1(development-plan.md)已经由人类(配合 Claude Code 交互式开发,就是我们现在这个对话在做的事)跑通一次之后才谈得上——这不是缺口,是任何自举系统的必然前提,就像第一版 C 编译器不可能用 C 自己写。development-plan.md 的关键路径已经隐含了这一点。 + +## 2. 三个真缺口 + +### 缺口 1:worktree 隔离目前只在"并行"时强制,自我修改时必须无条件强制 + +03-C 3.6 节现有约束是"**并行**写代码的节点必须先申请 `git_worktree_ref`"。改别的 target project 时,单个 Coder 节点串行工作直接改主 checkout 问题不大。但如果 target project 就是 graphlemon 自己,正在跑这个任务的后端进程本身就在读取这份代码——直接改主 checkout 有实时干扰正在运行的调度器的风险。 + +**需要新增规则**:`target_folder_path == graphlemon 平台自身安装路径` 时,无论是否并行,一律强制 `git_worktree_ref`,且 Coder/Validator 的自测/复核全部在 worktree 副本里跑,不碰主 checkout。 + +### 缺口 2:没有"应用变更 + 生效"这一步——这是最大的缺口 + +Validator 在 worktree 里 PASS 之后,现有机制(git_worktree_ref 执行完成后清理并合并回主分支)能把代码合并进 graphlemon 自己仓库的主分支。**但合并到主分支的代码不会被正在运行的那个 Node.js/Python 进程自动重新加载**。需要重启才能生效,而"正在执行调度逻辑的那个进程"不可能自己发指令重启自己——发出重启指令的那一刻,进程就要被杀掉了。 + +当前 SDD 里没有任何 controller/process 定义这一步。这不是简单补个接口能解决的,需要一个新的架构组件。 + +**推荐方案(蓝绿实例,而不是原地重启)**:不在主进程里做"重启自己",而是让 Validator 直接对着 worktree 里跑起来的一个**独立的第二份 graphlemon 实例**(换端口、换一份临时/只读数据库)做验证;通过后由一个**在 Graph Engine 控制范围之外**的独立 **Deploy Supervisor**(与架构文档里 Coder Work Graph 内部的"Supervisor(调度子 Agent)"是两个不同概念,注意区分)执行切换:停旧实例 → merge 分支到主分支 → 起新实例。这样"运行中的自己"永远不需要重启自己。 + +**(2026-07-30 已完成详细设计并回填正式 SDD)** 完整状态机、健康检查信号、cutover/回滚流程、MVP 固定参数见 `docs/done/supervisor-auto-rollback-design.md`;已同步进 01-M/03-C/04-P/05/06/contract.json(新增 REQ-15)。方案选型(为什么不直接用 Hermes CLI 当 Deploy Supervisor 的决策大脑,而是复用 graphloop 已验证的隔离/manifest/apply 库)见 `docs/done/supervisor-design-comparison.md`。 + +### 缺口 3:核心代码/身份文件的自我修改没有对应缺口 3.5 节 `org_graph_definition` 那样的强制人工评审 + +05 节 §5.6 已经规定 `org_graph_definition`(Org Graph 结构 YAML)的变更必须走人工 PR 评审,不允许系统自主提交;`skill`(Work Graph 技能定义)允许系统自主迭代但要求 Git 可回滚。但这两条规则都没覆盖: + +1. **`.claude/agents/{planner,coder,validator}.md`**(Org Agent 身份文件,对应 SOUL.md)——如果 Coder 被要求"优化一下 Validator 的判断逻辑",改的是判定裁决者本身的人设,一旦改坏(比如让它变得更宽松、更容易判 PASS),系统会在没有外部校验的情况下自我欺骗,而且没人会发现,因为发现问题的机制本身已经被改坏了。 +2. **调度器/执行器抽象层等核心引擎代码**——和改一个 UI 组件的风险完全不是一个量级。 + +**原提案(已被用户否决,保留作记录)**:曾建议 `.claude/agents/*.md` 的变更、以及触碰 Graph Engine 调度器/执行器抽象层核心逻辑的变更一律强制 `blocked_pending_human`,走和 `org_graph_definition` 一样的人工评审通道。 + +**用户更正(2026-07-30,原话)**:"改坏了的 validator 没有人工评审倒不是问题,坏了由 supervisor 能够监督回滚即可。所以核心是需要一个三方的 agent 来作为补充 supervisor 接口。" —— 即:不要求核心文件改动必须先经人工评审才能生效,而是允许系统自主提交,由缺口 2 的同一个 Supervisor 在部署后持续监控行为信号(例如 Validator 的 approve 率是否偏离历史基线),一旦检测到异常自动回滚,不需要人在"改之前"把关。 + +**(2026-07-30 已完成详细设计)** `docs/done/supervisor-auto-rollback-design.md` 第 6 节 `validator_approval_rate` 信号即直接对应"改坏了的 validator"这个场景:从 `execution_event`/`work_node_execution` 表按滚动窗口统计 Validator 判定 PASS 的比例,与部署前的历史基线做纯算术比较,偏离阈值即触发自动回滚(该文档第 7.2 节),全程不需要人工评审代码内容。仍保留的唯一人工介入点是"自动回滚机制本身失败"(例如连上一个已知良好版本都起不来)这类基础设施兜底场景(该文档第 7.3 节),而不是"代码判断谁对谁错"。 + +`bug_record.root_cause_category` 新增 `core_engine` 分类的建议仍然保留,但用途从"触发强制人工评审"改为"触发时跳过 `fast_lane`、走完整的蓝绿部署+监控流程"(普通 Skill 级别的自主迭代可能不需要这么重的验证)。 + +同理,如果这次自我修改涉及 01-M 自身的数据库 schema(比如给 `work_node_execution` 加字段),05 §5.6 现有的"允许停机迁移"假设是给人工运维用的,没覆盖"系统自主决定给自己数据库做 schema 变更"——这类改动同样纳入蓝绿部署+监控流程,而不是强制转人工,具体的 schema 迁移在蓝绿切换中的时序(迁移脚本什么时候跑、跑坏了怎么回滚)是 `supervisor-auto-rollback-design.md` 未覆盖的开放问题,需要后续补充。 + +## 3. 模拟走一遍:修复自己状态机里的一个边界 bug + +**场景**:人类发现 graphlemon 自己的 `p_org_task_lifecycle` 状态机实现里,`loop_round >= 3` 转 `blocked_pending_human` 的判断写成了 `loop_round > 3`(off-by-one,第 3 轮本该拦截却继续跑了)。这段逻辑就在 graphlemon 自己的调度器代码里——一个很好的自举测试用例,因为它既真实又刚好踩中"核心引擎代码"这条线。 + +| 步骤 | 操作 | 状态 | +|---|---|---| +| 1. 项目登记 | `c_pick_folder` 选中 graphlemon 自己的仓库路径,`c_project_create` 校验 `has_git_repo=true`(它自己确实是),创建 `.graphlemon/` 子目录 | ✅ 已具备,架构对 `target_folder_path` 没有排除平台自身路径 | +| 2. 提出目标 | `c_task_create({goal_text: "修复 p_org_task_lifecycle 里 loop_round 边界判断的 off-by-one", project_id: })` | ✅ 已具备 | +| 3. Planner 触发 | `c_org_node_trigger(planner)`,Planner 读取的正是 graphlemon 自己的 04-P 文档 + 对应源码 | ⚠️ 轻微缺口:Planner 的 skill 是按"从 PRD 写新 SDD"设计的,这次是"改一处已有实现",`c_work_graph_plan` 目前没有区分这两种 `graph_manifest` 生成路径。大概率能工作(Planner 有能力判断这是小范围修复),但没有显式设计,建议补一条轻量修复路径的规则 | +| 4. Coder 节点执行 | `c_work_node_run` 触发 Coder,按缺口 1 的新规则强制走 `git_worktree_ref`,在隔离副本里改代码、跑单测 | ⚠️ 命中缺口 1(需要新规则:self-target 场景无条件强制 worktree) | +| 5. Validator 复核 | Validator 在 worktree 副本里复现原 bug 场景,确认修复且无回归 | ⚠️→✅ 命中缺口 3:这次改动是调度器核心代码,`root_cause_category` 应标记 `core_engine`,触发的是完整蓝绿部署+监控流程(而不是强制转人工),机制已设计(`supervisor-auto-rollback-design.md`) | +| 6. 合并到主分支 | worktree 分支 merge 回 graphlemon 自己仓库的主分支 | ✅ 机制已具备(07 节"执行完成后清理 worktree 并合并回主分支") | +| 7. 生效 | 需要让正在运行的后端进程加载新代码 | ⚠️→✅ 命中缺口 2(最大的缺口):需要蓝绿实例 + 独立于 Graph Engine 之外的 Deploy Supervisor 来完成切换,完整状态机/健康检查/回滚流程已设计并回填 SDD(`supervisor-auto-rollback-design.md`),尚未实现代码 | +| 8. 数据库 schema | 本例的判断逻辑修复不涉及 01-M schema 变更,不触发 | (本例未命中,但换个例子——比如给 `work_node_execution` 加字段——会命中缺口 3 的 schema 部分,`supervisor-auto-rollback-design.md` 第 10 节已列为未覆盖的开放问题) | +| 9. 完成 | 蓝绿切换完成(Deploy Supervisor 健康检查通过后自动完成,无需人工确认)后 `task.status=done` | 走到这一步,一次"系统被告知自己的缺陷 → 自己诊断根因 → 自己修复 → 自己验证 → 自动部署并被 Deploy Supervisor 持续监控(异常自动回滚)"的完整闭环就跑通了,且全程不需要人工介入代码内容评审 | + +## 4. 小结 + +9 步里 3 步直接可用,1 步大概率可用但没显式设计,剩下 5 步(含最关键的"合并后如何生效")命中三个真缺口。**2026-07-30 更新**:缺口 2(蓝绿实例生效)和缺口 3(核心文件自我修改的治理,按用户更正为"自动回滚而非强制人工评审")已完成详细设计,分别落在 `docs/done/supervisor-auto-rollback-design.md`(状态机/健康检查/回滚流程/MVP 参数)和 `docs/done/supervisor-design-comparison.md`(方案选型依据)。缺口 1(worktree 无条件强制)仍是待写入 05 节的一条治理规则,成本低。 + +如果要推进,建议顺序:先把缺口 1(一条治理规则)写进 05 节和 contract.json,成本低、马上能落地;缺口 2/3 的设计已经具备可实施粒度,可以作为 PH4 之后的独立 Phase(PH5:自举能力)排入 `development-plan.md`,不影响当前 PH0-PH4 主线——具体是现在就把 PH5 的 Unit 分解也做出来,还是先确认设计稿本身没问题再排,由用户决定。 diff --git a/docs/done/supervisor-auto-rollback-design.md b/docs/done/supervisor-auto-rollback-design.md new file mode 100644 index 0000000..d4a770e --- /dev/null +++ b/docs/done/supervisor-auto-rollback-design.md @@ -0,0 +1,170 @@ +# Deploy Supervisor 蓝绿实例自动回滚机制设计 + +**日期**: 2026-07-30(同日更新:完成 Supervisor 概念改名消歧,见下方命名说明) +**性质**: 详细设计,回应用户"自动回滚机制现在就设计一下,省得到 PH5 阶段有那个阶段的问题"。落地 `self-development-feasibility.md` 缺口 2(应用变更+生效)与缺口 3(用户更正版:不要求人工评审,要求 supervisor 自动监督回滚)。 +**依赖**: `docs/done/supervisor-design-comparison.md`(方案选型结论:复用 graphloop 隔离/manifest/apply 库,见 `supervisor/isolation/`;Deploy Supervisor 自身决策逻辑必须是确定性代码,不接入 LLM)。 +**状态**: 设计已回填进正式 SDD(`01-M`/`03-C`/`04-P`/`05`/`06`/`sdd-mvcp-contract.json`),代码尚未实现(`supervisor/isolation/` 除外,那是通用库不是本机制专属)。 + +**命名说明(2026-07-30)**:`graphlemon_architecture.md` 里已经有一个"Supervisor(调度子 Agent)"概念(Coder Work Graph 内部的常驻监控节点,负责监控 Work Node 执行状态、触发 reflection/重新调度)。本文档描述的是完全不同的另一个组件——一个**独立于 Graph Engine 之外**、专门负责 graphlemon 自我修改后蓝绿部署与自动回滚的进程。为避免和架构文档里的既有概念混淆,本文档统一称之为 **Deploy Supervisor(部署监督进程)**,所有标识符前缀使用 `c_deploy_supervisor_*`,与既有的 `Supervisor(调度子 Agent)`概念完全区分开:两者职责不同(一个管 Work Node 执行进度,一个管整个 graphlemon 进程的部署/回滚)、进程边界不同(一个在 Graph Engine 进程内,一个在 Graph Engine 进程外)、生命周期不同(一个随 Work Graph 任务存在,一个是常驻的独立进程)。 + +## 1. 触发范围 + +本机制**只在** `target_folder_path == graphlemon 平台自身安装路径`(self-target 场景,`self-development-feasibility.md` 缺口 1 已定义的判断条件)且 Validator 对该次修改判定 PASS 之后触发。改别的 target project 时,走既有的"worktree 合并回主分支"流程即可,不涉及蓝绿实例——因为改的代码不是正在跑的这个进程本身。 + +触发时点:Validator PASS → worktree 分支 merge 回 graphlemon 自身仓库主分支(既有机制,`self-development-feasibility.md` 步骤 6)→ **Deploy Supervisor 从这里接管**,直到新代码安全生效或安全回滚为止。 + +## 2. 组件与进程边界 + +三个独立操作系统进程,任何一个崩溃不影响另外两个: + +1. **Live 实例**:当前正在服务的 graphlemon 后端(Graph Engine + 控制台 API),监听固定端口 `LIVE_PORT`,使用固定路径的生产 SQLite 库 `PROD_DB_PATH`。 +2. **Candidate 实例**:本次自我修改合并后的新版本,Deploy Supervisor 拉起来做验证,监听临时端口,使用**一次性 DB 副本**,与生产 DB 完全隔离,验证期间无论 Candidate 干什么都不影响 Live。 +3. **Deploy Supervisor 进程**:独立于以上两者的确定性 Python 脚本(stdlib only,无 LLM 调用),是本设计的主体。职责:拉起/健康检查/切换/监控/回滚 Candidate 与 Live,自身不做任何"业务判断"(比如不判断代码写得好不好),只做"这几个信号是否符合预设阈值"这种算术判断。 + +Deploy Supervisor 与 Live/Candidate 之间没有共享内存/共享失败域——这正是用户"第三方 agent"要求的字面落地:Deploy Supervisor 挂了不会拖死 Live,Live 挂了 Deploy Supervisor 还能观察到并处理。 + +## 3. 数据模型(已回填 `01-M-模型设计.md`) + +两个新实体: + +### `deployment` + +| 字段 | 类型 | 说明 | +|---|---|---| +| `deployment_id` | string, PK | | +| `git_commit_hash` | string | 本次候选版本对应的 commit | +| `previous_commit_hash` | string | 回滚目标(上一个 `status=finalized` 的 deployment 的 `git_commit_hash`) | +| `status` | enum | 见第 5 节状态机 | +| `candidate_port` | int, nullable | 健康检查阶段临时端口 | +| `candidate_db_path` | string, nullable | 一次性 DB 副本路径,验证结束后清理 | +| `started_at` / `cutover_at` / `monitoring_closed_at` / `rollback_at` | timestamp, nullable | | +| `rollback_reason` | string, nullable | 见第 6 节信号类型 | +| `previous_instance_dir` | string | 保留的上一个已知良好版本 checkout 目录,直到本次 deployment 监控窗口正常关闭才允许清理 | + +### `deploy_health_signal` + +| 字段 | 类型 | 说明 | +|---|---|---| +| `signal_id` | string, PK | | +| `deployment_id` | string, FK | | +| `sampled_at` | timestamp | | +| `signal_type` | enum: `process_alive` \| `http_health` \| `canary_task` \| `validator_approval_rate` | | +| `status` | enum: `ok` \| `degraded` \| `fail` | | +| `detail` | string | 供人工事后排查,不参与自动判断 | + +## 4. Cutover 机制(部署新版本) + +1. Deploy Supervisor 拉取合并后的主分支到全新目录(不是原地 `git pull`,避免影响可能仍在读取源码的 Live 进程),生成 `deployment` 记录,`status=staged`。 +2. 复制 `PROD_DB_PATH` 到 `candidate_db_path`(一次性副本,用完即删,绝不让 Candidate 直接碰生产库)。 +3. 用 `supervisor/isolation/isolated_workspace.isolate_workspace()` 家族同款隔离原则拉起 Candidate 进程:`subprocess.Popen`,`shell=False`,固定 argv 形态,监听 `candidate_port`(生产端口 +1,或探测一个空闲端口),指向 `candidate_db_path`。`status=candidate_starting`。 +4. 跑健康检查(见第 6 节),`status=health_check_running`。 + - 全部通过 → `status=health_check_passed`,进入 5。 + - 任一失败/超时 → `status=health_check_failed` → 走**回滚路径 A**(第 7.1 节)。 +5. **Cutover 本体**(本地单用户桌面应用,不需要处理并发外部流量/负载均衡,这一条假设需要用户确认,见第 9 节开放问题): + - Live 停止接受新的 Org Node 触发(不接受新 `c_org_node_trigger` 调用),但允许已在执行的 `work_node_execution` 走到可安全中断的检查点(已有的持久化恢复机制保证中断后可恢复,不强求"优雅排空")。 + - Live 进程退出。 + - Candidate 关闭一次性 DB 副本连接,改连 `PROD_DB_PATH`,改绑 `LIVE_PORT`。 + - Candidate 成为新的 Live。`status=live`,`cutover_at=now`。 + - 原来的 Live checkout 目录保留为 `previous_instance_dir`,**不删除**,直到监控窗口正常结束。 + +## 5. 完整状态机 + +``` +staged + -> candidate_starting + -> health_check_running + -> health_check_failed [回滚路径 A:见 7.1,不影响 Live,直接结束] + -> health_check_passed + -> cutover_in_progress + -> live + -> monitoring (进入监控窗口) + -> anomaly_detected -> rollback_in_progress + -> rolled_back [回滚路径 B:见 7.2,Live 换回旧版本] + -> rollback_failed -> blocked_pending_human [兜底,见 7.3] + -> monitoring_window_clean -> finalized (旧 previous_instance_dir 可清理) +``` + +`finalized` 是唯一的"安全终态";`blocked_pending_human` 是唯一需要人工介入的终态,且触发条件严格限定为"自动回滚机制本身失败",不是"代码写得不好"——这与用户对缺口 3 的更正(不要求人工评审代码内容)不冲突,是不同层面的安全网。 + +## 6. 健康检查 / 监控信号(全部确定性,不接入任何 LLM 判断) + +### Pre-cutover 健康检查(`health_check_running`,有界超时,全部通过才能 cutover) + +| 检查项 | 方法 | 判定 | +|---|---|---| +| `process_alive` | 启动后轮询 `os.kill(pid, 0)`,超时 `HEALTH_CHECK_STARTUP_TIMEOUT` | 超时未存活 = fail | +| `http_health` | 请求 Candidate 的 `/api/health`,超时 `HEALTH_CHECK_HTTP_TIMEOUT` | 非 200 或超时 = fail | +| `canary_task` | 对 Candidate 跑一个固定的、结果可预期的最小任务(例如创建一个临时 project + 一个已知会通过的 trivial task,断言最终 `task.status` 与预期字段一致),超时 `HEALTH_CHECK_CANARY_TIMEOUT` | 结果不符/超时/异常 = fail | + +任一 fail 立即进入回滚路径 A,不重试(重试属于"给坏版本第二次机会",不符合 fail-closed 原则;要重试整个自我修改,走 Planner 重新出一版)。 + +### Post-cutover 监控(`monitoring`,窗口内持续采样) + +| 信号 | 方法 | 判定阈值(MVP 固定值,不做用户可配) | +|---|---|---| +| `process_alive` | 每 `MONITOR_SAMPLE_INTERVAL` 探活 | 连续崩溃/重启 ≥ `MAX_CRASH_COUNT` 次 / `CRASH_WINDOW` 内 = anomaly | +| `http_health` | 同上频率请求 `/api/health` | 连续 `MAX_CONSECUTIVE_HTTP_FAIL` 次失败 = anomaly | +| `canary_task` | 每 `CANARY_INTERVAL` 跑一次固定 canary 任务 | 单次失败即 anomaly(canary 本身就是"已知应该过"的任务,失败没有灰色地带) | +| `validator_approval_rate` | 从 `execution_event`/`work_node_execution` 表按滚动窗口统计 Validator 节点 `approved` 占比,与 deployment 之前的历史基线比较 | 偏离基线超过 `VALIDATOR_RATE_DEVIATION_THRESHOLD`(绝对值,如 ±30 个百分点)= anomaly。**这一条直接对应用户点名的场景**:"改坏了的 validator"——如果新版本的 Validator 变得异常宽松(approve 率骤升)或异常严格(approve 率骤降到几乎全部拒绝),都会被这条信号捕获,且判断是纯算术,不需要另一个 LLM 来"评价 Validator 判得对不对" | + +监控窗口:`MONITORING_WINDOW_HOURS`(固定值,如 24h)或 `MONITORING_MIN_TASK_COUNT`(固定值,如 10 个真实任务)先到者为准,之后无 anomaly 则 `monitoring_window_clean → finalized`。 + +## 7. 回滚路径 + +### 7.1 路径 A:Pre-cutover 回滚(健康检查未过) + +Live 从未被动过,回滚成本最低: + +1. 终止 Candidate 进程(`SIGTERM`,参考 `hermes_watchdog.py` 的终止哲学)。 +2. 删除 `candidate_db_path` 一次性副本。 +3. `git revert` 本次合并 commit(不是 `reset`/force-push,保留历史可追溯,这一步复用 `workspace_security.validate_subprocess_command()` 的 argv 校验)。 +4. 发一个 `bug_record`,`root_cause_category=core_engine`,把健康检查失败的具体信号/日志作为 evidence 挂上去,供下一轮 Planner/Coder 重新尝试时参考。 +5. `status=health_check_failed`,记录结束,Live 全程未受影响,调度立即可以继续处理下一个任务。 + +### 7.2 路径 B:Post-cutover 回滚(监控期间检测到 anomaly) + +Live 已经是 Candidate,需要真正"换回去": + +1. 记录触发回滚的信号快照(哪个 signal_type、哪次采样、什么值)到 `deployment.rollback_reason`。 +2. 当前 Live(即出问题的新版本)停止接受新触发,同样允许在途节点走到可恢复检查点。 +3. 用 `previous_instance_dir`(第 4 步保留的上一版本 checkout)重新拉起进程,绑定 `LIVE_PORT`,连接 `PROD_DB_PATH`(生产库这次全程没有被换过,只是换了跑在它前面的代码进程,数据不受影响——这是 blue-green 相对"原地改代码"最大的安全收益)。 +4. 对"复活的旧版本"跑一遍 7.1 同款的三项健康检查,确认旧版本本身没问题(正常情况下必过,因为它是上一次已经 `finalized` 的版本)。 +5. 通过 → `status=rolled_back`,当前 Live = 旧版本,本次自我修改的 commit 在主分支上被 `git revert`,发 `bug_record`(同路径 A 第 4 步,evidence 换成监控期间的异常信号),调度恢复正常。 +6. 不通过(极端情况:连旧版本都起不来,比如磁盘满了这类环境问题)→ 第 7.3 节。 + +### 7.3 兜底:回滚本身失败 + +`status=rollback_failed → blocked_pending_human`。这是本设计里唯一保留的人工介入点,且明确限定触发条件仅为"自动恢复机制本身失效"(旧版本也起不来、DB 不可写这类基础设施问题),不是"代码判断谁对谁错"。控制台需要能清楚展示这个区别,避免用户以为又回到了"需要人工评审代码改得好不好"的老路。 + +## 8. 与既有 SDD 的对接点(2026-07-30 已回填,见第二次更新记录) + +- `01-M-模型设计.md`:新增 `deployment`、`deploy_health_signal` 两个实体(第 3 节)。**已完成。** +- `03-C-控制与接口.md`:新增 `c_deploy_supervisor_stage` / `c_deploy_supervisor_health_check` / `c_deploy_supervisor_cutover` / `c_deploy_supervisor_monitor` / `c_deploy_supervisor_rollback` 五个控制操作;`c_org_loop_route` 扩展 `root_cause_category=core_engine` 的路由分支。**已完成。** +- `04-P-流程与状态流转.md`:新增 `p_deploy_supervisor_blue_green` 流程,状态转移表照搬第 5 节。**已完成。** +- `05-实施约束与依赖.md`:新增本机制的 MVP 固定参数表(第 9 节),比照现有 `p_usage_limit_wait` 参数(5→10→20→30min backoff、8h/9d 超时)"MVP 阶段硬编码、不做用户可配"的先例,登记同一张表里;新增 self-target worktree 无条件强制规则。**已完成。** +- `06-验收与测试锚点.md`:新增 REQ-15,覆盖:健康检查全过正常 cutover、健康检查失败路径 A、监控期 anomaly 触发路径 B、回滚本身失败兜底到 `blocked_pending_human` 四类 happy/failure 用例。**已完成。** +- `sdd-mvcp-contract.json`:同步以上四类变更,重新跑 `jsonschema.validate()`。**已完成。** +- `self-development-feasibility.md`:缺口 2/3 的"推荐方案"段落指向本文档。**已完成(2026-07-30 更新记录)。** + +## 9. MVP 固定参数(草案值,需要在实现前用真实测量校准,参考 A-1 的验证方法论) + +| 参数 | 草案值 | 说明 | +|---|---|---| +| `HEALTH_CHECK_STARTUP_TIMEOUT` | 30s | Candidate 进程从拉起到能响应探活 | +| `HEALTH_CHECK_HTTP_TIMEOUT` | 10s | 单次 `/api/health` 请求 | +| `HEALTH_CHECK_CANARY_TIMEOUT` | 120s | 一次最小 canary 任务从提交到出结果 | +| `MONITOR_SAMPLE_INTERVAL` | 5min | process/http 采样间隔 | +| `CANARY_INTERVAL` | 1h | 监控期 canary 任务运行间隔 | +| `MAX_CRASH_COUNT` / `CRASH_WINDOW` | 2 次 / 10min | 判定进程反复崩溃 | +| `MAX_CONSECUTIVE_HTTP_FAIL` | 3 次 | 判定健康端点持续不可用 | +| `VALIDATOR_RATE_DEVIATION_THRESHOLD` | ±30 个百分点 | 判定 Validator 行为异常 | +| `MONITORING_WINDOW_HOURS` | 24h | 监控窗口时长上限 | +| `MONITORING_MIN_TASK_COUNT` | 10 个真实任务 | 监控窗口任务数上限,先到者结束 | + +以上数值全部是工程经验草案,不是实测结果——和 `05-实施约束与依赖.md` §5.4.2 现有的额度等待退避参数一样,需要在真正实现并跑几轮真实自我修改后重新校准,不建议直接当最终值写进 contract.json 的强约束里(建议标注为"可调,需实测校准"而不是"硬编码不可变",这一点和额度等待参数刻意标注为"MVP-fixed"略有不同,因为额度等待的时间线是 Anthropic 平台文档定义的外部事实,而这里的阈值是纯粹的工程经验值)。 + +## 10. 未覆盖 / 开放问题(留给实现阶段决定,不阻塞本设计定稿) + +1. **单用户桌面场景假设**:第 4 节 cutover 假设不需要处理并发外部流量/连接排空,只有 Cowork 控制台这一个使用者,断线可重连。如果未来 graphlemon 要支持多用户/多终端同时连接,这里的"直接切端口"简化模型需要重新评估。 +2. **`candidate_db_path` 一次性副本的体积/耗时**:如果生产 DB 随着长期使用变得很大,每次 cutover 前整库复制可能变慢,需要的话可以后续改成"复制 schema + 近期数据子集"而不是整库,MVP 阶段先用整库复制,简单可靠优先。 +3. **`git revert` 冲突**:如果本次自我修改之后主分支又有新的(比如另一个并发自我修改任务的)提交,revert 可能冲突,需要 Deploy Supervisor 能识别 revert 失败并转 `blocked_pending_human`(不在当前状态机图里显式画出,应该并入 7.3 兜底路径,而不是新开一个分支——这样能保持状态机简单)。 diff --git a/docs/done/supervisor-design-comparison.md b/docs/done/supervisor-design-comparison.md new file mode 100644 index 0000000..c89280a --- /dev/null +++ b/docs/done/supervisor-design-comparison.md @@ -0,0 +1,109 @@ +# Deploy Supervisor 设计方案对比:自建 vs 复用 Hermes CLI + +**日期**: 2026-07-30(同日更新:完成命名消歧,见下方说明;结论已回填正式 SDD) +**性质**: 方案选型分析,回应用户 "都不确定,先看两种方案的详细对比",已回填 SDD/contract.json(见 `supervisor-auto-rollback-design.md` §8)。 +**输入**: 对 `graphloop`(同一项目前身,`/Users/yibo/Documents/lemonAI/graphloop`)的实地代码/任务记录调查。 + +**命名说明**:本文档讨论的"Deploy Supervisor"是独立于 Graph Engine 之外、负责 graphlemon 自我修改后蓝绿部署与自动回滚的进程,和架构文档里已有的"Supervisor(调度子 Agent,Coder Work Graph 内部监控节点)"是两个不同的概念,详见 `supervisor-auto-rollback-design.md` 开头的命名说明。本文档下文提到"supervisor"泛指这个待选型的组件本身,涉及 graphloop 自己代码里的 `hermes_watchdog.py`/`phase10_controller.py` 时沿用 graphloop 原始命名,不额外改名。 + +## 0. 结论先说 + +问题不是"自建 vs 用 Hermes CLI 当 Deploy Supervisor 的大脑"这个二选一——graphloop 自己的实现已经给出了答案,而且答案跟两个选项字面意思都不完全一样: + +- graphloop 的 supervisor 层(`hermes_watchdog.py` + `phase10_controller.py`)**自身的监督/裁决逻辑是纯 stdlib 确定性 Python,完全不调用任何 LLM**(Hermes 或 Claude 都不调)。LLM 只用来执行"被监督的那份工作",监督决策(存活判断、是否 blocked、evidence 是否齐全)是 if/else,不是 prompt。 +- 但 graphloop 的监督哲学是 **"fail-closed 后停下等人工"**,不是用户要的 **"自动回滚后继续跑"**。这是个真实的哲学缺口,两个候选方案都没有现成覆盖。 +- graphloop 已经有一套经过真实运行验证(含真实失败案例)的隔离/manifest/选择性写回机制,直接复用价值很高,且这部分代码同样是纯 stdlib、和"用不用 Hermes CLI"这个问题正交。 + +所以推荐路径不是"选 A 还是选 B",而是"复用 C(graphloop 的隔离/manifest/apply 库)+ 在其之上新写一个自动回滚层",详见第 4 节。 + +## 1. graphloop 里跟 supervisor 直接相关的既有实现 + +### 1.1 `hermes_watchdog.py`(109 行,已读全文) + +纯 stdlib,无 LLM 调用。核心逻辑: + +``` +inspect(state_dir, stall_seconds=600.0, action="report"|"terminate"): + 扫描 state_dir 下 status=="running" 的 *.json 状态文件 + 用 last_output_at/updated_at 或日志 mtime 算 silent_for + if stalled and action=="terminate": + os.kill(pid, SIGTERM) + 原子写 {"status": "blocked", "blocked_reason": "worker dead or silent beyond watchdog threshold"} +``` + +模块 docstring 原话:"The watchdog never restarts a task. A dead or silent worker is terminated once, marked blocked, and left for an explicit recovery decision." —— **这是关键**:graphloop 的哲学是"发现异常 → 杀掉 → 标记 blocked → 停下等人",不是"发现异常 → 自动回滚 → 继续跑"。 + +### 1.2 `phase10_controller.py`(自我演进调度器,已读核心结构) + +同样纯 stdlib,无 LLM 参与调度决策本身: + +``` +main(): + acquire_tick_lock() # 单飞锁,防止并发 tick + 读 progress.json 找 current_task + if 上一个任务的 worker PID 不存活: mark_stale() → blocked_stale,停 + if task_id 在 blocked_steps 或历史记录里是 failed/blocked: blocked_task,停 + completion_is_admissible(): 校验 evidence dict 是否包含必需 key(specialized_test/py_compile/full_regression), + blockers 字段非空则直接判不可采信 + 只有 evidence 完整且无 blocker 才推进 phase,否则原地 blocked,绝不自动重试/自动回滚 +``` + +调度器本身只负责"要不要放行这一次 tick",真正干活的是它 shell 出去的 `hermes chat --yolo ...`。 + +### 1.3 隔离 / manifest / 选择性写回(`CLAUDE.md` 架构章节 + 已读代码路径) + +这是最值得直接复用的部分,和"用不用 Hermes CLI"完全无关,纯 stdlib: + +- `isolated_coding_workspace.isolate_workspace(source_dir)`:`tempfile.mkdtemp` 复制,源目录永不直接改动 +- `workspace_security.validate_artifacts()`:拒绝绝对路径、`..`、逐段检查 symlink(防 TOCTOU 逃逸)、最终 `realpath`+`commonpath` 校验 +- `workspace_security.validate_subprocess_command()`:只接受 `[python可执行文件, 已存在脚本, ...]`,拒绝 shell 元字符(`shell=False` 之外的纵深防御) +- `change_manifest.py`:源 vs 隔离副本的 SHA-256 diff,生成 changed_files manifest +- `selective_apply.apply_manifest()`:**唯一**写回真实源码的入口,校验每个 approved path、双向拒绝 symlink 逃逸、写入前先读全部字节(stage-before-commit)、`tempfile.mkstemp`+`os.replace` 原子写 +- `run_graph_task.py` 的 `SafetyArgumentParser`:`--apply` 必须搭配显式 `--approve-path`/`--approve-all`,`--approve-all` 不带 `--apply` 直接拒绝——这是人工审批卡点,不是自动的 + +这套机制直接对应 graphlemon `self-development-feasibility.md` 缺口 1(worktree 隔离强制)的解法,而且是**已经在真实失败场景里验证过边界**的实现(见下一节)。 + +## 2. 真实失败证据(`tasks/phase10-progress.json`)及其含义 + +graphloop 自己跑 Phase 1-10 自我演进时卡在 Phase 2(`phase10_ready: false`),留下三个具体故障,每个都直接对应 graphlemon 架构里的一个假设: + +| 故障 | 现象 | 对 graphlemon 的启示 | +|---|---|---| +| `plan-schema-001` 超时 | Hermes worker 300s 未完成,判 failed | 03-C 里 `max_step_timeout`/`c_work_node_run` 超时参数不能拍脑袋定小了;graphloop 用的是 300s 默认值,实测不够 | +| `step-fanout-001` **隔离被击穿** | Hermes worker 把实现文件直接写进了**源工作区**而不是隔离工作区;隔离 manifest 里没有任何变更,controller 判定"证据不可采信",清理了源目录里两个未授权文件,恢复快照 | **最关键的一条**:即使明确要求"在隔离工作区里改",执行 agent(无论 Claude 还是 Hermes)仍可能不遵守,隔离必须由 supervisor/controller 层做外部校验("我检查你改了哪些文件、改在哪"),不能只靠 prompt 里一句"请在隔离环境里改"。这直接印证了 `self-development-feasibility.md` 缺口 1 的判断,而且不是理论推测,是实测复现 | +| worker 状态陈旧(PID 不存活但状态仍是 running) | `active_run.status="stale"`,controller 判定 `stale_reason="recorded worker PID is not alive"` | 印证了需要独立于被监督进程之外的存活探测(`os.kill(pid,0)` 这类外部判活),不能依赖被监督进程自己上报"我还活着" | + +## 3. 关键缺口:graphloop 的模型是"停下",graphlemon 要的是"回滚后继续" + +用户在缺口 3 的更正原话:"改坏了的 validator 没有人工评审倒不是问题,坏了由 supervisor 能够监督回滚即可"——这要求 Deploy Supervisor 具备**检测到问题后自动执行回滚动作、并让系统恢复到可继续工作的状态**的能力。 + +graphloop 现有两处监督实现(`hermes_watchdog.py`、`phase10_controller.py`)都止步于"标记 blocked,等人工",没有"自动回滚到上一个已知良好版本并恢复运行"这一步。这不是实现细节缺失,是设计哲学的差异: + +- graphloop:安全边界 = 停下(fail-closed to a halt) +- graphlemon 要求:安全边界 = 回滚到已知良好状态后继续(fail-closed to a rollback, then resume) + +后者严格来说更难——"回滚"在 graphloop 的场景里相对简单(还没 apply,源码没动),但在 graphlemon 的蓝绿实例场景里,"回滚"意味着:停正在跑坏的新实例 → 把主分支 revert 或切回上一个 tag → 重新拉起旧版本实例 → 确认健康 → 恢复调度。这一段目前两个项目都没有代码。 + +## 4. 方案对比 + +| 维度 | 方案 A:全新自建 Deploy Supervisor | 方案 B:Hermes CLI 直接当"大脑" | 方案 C(推荐,已采纳):复用 graphloop 隔离/manifest/apply 库 + 新写确定性回滚层 | +|---|---|---|---| +| 隔离机制 | 从零写,风险是重新踩 graphloop 已经踩过的坑(symlink 逃逸、TOCTOU) | 同左,Hermes CLI 本身不提供这层,还是要自己写 | 直接复用 `isolated_coding_workspace.py`/`workspace_security.py`/`change_manifest.py`/`selective_apply.py`,已验证、纯 stdlib、零外部依赖 | +| 存活/异常检测 | 从零写 | 同左 | 复用 `hermes_watchdog.py` 的判活逻辑(`os.kill(pid,0)` + staleness),改 action 分支 | +| 决策逻辑是否含 LLM | 看怎么设计 | 是(如果真把 Hermes CLI 当决策者,等于让一个不确定性的 LLM 做安全关键判断) | 否——沿用 graphloop 的判断:安全关键的"要不要回滚"必须是确定性代码,不能是 LLM 判断 | +| 自动回滚能力 | 需要新写(工作量:中,蓝绿切换脚本 + 健康检查 + revert 逻辑) | 需要新写,且如果决策链路里有 LLM 调用,回滚这种要求"确定性、可审计、不能出错"的动作反而更不适合让 LLM 参与 | 需要新写(同方案 A 工作量),但隔离/apply 部分零重复造轮子,净新增工作量最小 | +| 与 Graph Engine 的独立性 | 完全独立,符合用户要求"第三方 agent" | 完全独立(Hermes CLI 本来就在另一套工具链) | 完全独立;graphloop 的 watchdog/controller 本来就是独立进程,架构上现成符合"third-party supervisor"要求 | +| 已验证程度 | 零(全新代码) | 部分(graphloop 已用 Hermes CLI 跑了 Phase 1,但 Phase 2 卡住暴露隔离被击穿的问题——说明"用 Hermes CLI 执行任务"这条路径本身有已知未解决的可靠性问题) | 隔离/apply 部分已验证(含真实失败复现);回滚部分未验证(新代码,但范围小) | +| 与既有决策的一致性 | 中性 | 有冲突:graphlemon 已明确决定 MVP 只用 Claude Code ACP 做 Org Agent 执行后端,不引入 Hermes CLI 执行任务;若 supervisor 内部又跑 Hermes CLI 去执行"诊断/回滚"这类任务,等于变相引入了第二个执行后端,需要重新论证 | 一致:只复用 graphloop 里**不调用任何 LLM 的确定性 Python 模块**(隔离/manifest/apply/watchdog 判活逻辑),不引入 Hermes CLI 作为执行后端,符合"只用 Claude Code ACP"的既有决策边界 | + +## 5. 推荐 + +**方案 C**:把 graphloop 的 `isolated_coding_workspace.py` / `workspace_security.py` / `change_manifest.py` / `selective_apply.py` 四个模块(纯 stdlib,无 Hermes/Claude 依赖)作为库直接搬到 graphlemon 里复用或轻改,解决 `self-development-feasibility.md` 缺口 1;`hermes_watchdog.py` 的判活/staleness 检测逻辑作为参考实现复用,但把它的终态从 `blocked`(停下等人)改造成新的 `blocked → 尝试自动回滚 → 回滚成功则恢复 running → 回滚失败才真正 blocked_pending_human` 状态机,解决缺口 2(蓝绿实例切换)并落实用户对缺口 3 的更正(自动回滚代替强制人工评审)。 + +不建议把 Hermes CLI 作为 supervisor 决策链路的一部分:graphloop 自己的实践已经证明——(a) 安全关键判断在其自身架构里也是纯确定性代码,从不假手 LLM;(b) 用 Hermes CLI 执行被监督的任务这条路径本身有实测的可靠性问题(隔离被击穿);(c) 引入 Hermes CLI 会和"MVP 只用 Claude Code ACP"这条已确认决策产生冲突,需要重新开一次论证。 + +## 6. 决策记录(2026-07-30,用户已逐条确认) + +1. **同意方案 C 方向**:"同意方案 c 方向,只在这一个环节复用,问题不大"——只复用隔离/apply 库这一部分,不引入 Hermes CLI 到决策链路。 +2. **复制而非依赖**:"复制一份到 graphlemon 仓库改造,那边已经不维护了"——四个模块已复制到 `supervisor/isolation/` 并按 graphlemon 自身约定改造(详见该目录 `__init__.py` 的来源说明),不对 graphloop 仓库产生 import 依赖。 +3. **自动回滚状态机现在就设计**:"自动回滚机制现在就设计一下,省得到 ph5 阶段有那个阶段的问题"——已完成,见 `supervisor-auto-rollback-design.md`,并已回填正式 SDD。 diff --git a/docs/dynamic-work-graph-design-skill-requirements.md b/docs/dynamic-work-graph-design-skill-requirements.md deleted file mode 100644 index a64ce76..0000000 --- a/docs/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/implementation-gap-analysis.md b/docs/implementation-gap-analysis.md index dbd0803..3899c26 100644 --- a/docs/implementation-gap-analysis.md +++ b/docs/implementation-gap-analysis.md @@ -2,9 +2,9 @@ **日期**: 2026-08-03 **性质**: 现状快照,回应用户 "现在整个系统的实现和我们的文档的 goal 和 brd prd sdd 还有哪些差距"。基于对 `git log`、 -`docs/development-plan.md`(含各 Unit 事后补记)、`docs/build-report.md`、`docs/sdd/06-验收与测试锚点.md` 和实际 +`docs/development-plan.md`(含各 Unit 事后补记)、`docs/done/build-report.md`、`docs/sdd/06-验收与测试锚点.md` 和实际 代码(`src/`、`supervisor/`)的交叉核对得出,不是重新跑一遍验收。 -**结论先说**: 45 个 Unit(U-001~U-043 + U-004.5/U-024.5)全部有对应实现和提交,`docs/build-report.md` 的 +**结论先说**: 45 个 Unit(U-001~U-043 + U-004.5/U-024.5)全部有对应实现和提交,`docs/done/build-report.md` 的 "全部完成"判断在"每个 Unit 都有代码落地"这个意义上是对的。但"全部完成"不等于"和文档设定的目标完全对齐"—— 差距集中在 PH5 的"自动"两个字还没完全兑现、系统还不是真正自驱动的、以及若干 SDD 契约的已知简化。逐条见下。 **如何维护本文档**: 这是一次性快照,会随实现推进而过时。发现某条已经补上或又出现新缺口时,直接编辑对应条目 @@ -77,7 +77,7 @@ - `development-plan.md`/`unit_matrix.json` 从写就没有随实现更新"done"状态,只能靠 `git log` 和散落在各 Unit 条目下的事后补记("实现补充"/"真实 bug")判断真实进度。 -- `docs/build-report.md` 里"⚠️ 尚未真实执行的一项"那一段(称 `RUN_REAL_SELF_HOSTING_E2E` 从未跑过)本身也已 +- `docs/done/build-report.md` 里"⚠️ 尚未真实执行的一项"那一段(称 `RUN_REAL_SELF_HOSTING_E2E` 从未跑过)本身也已 经过时——`development-plan.md` U-042 条目下 2026-08-03 的记录显示之后确实做了 13 次真实自举压测(`git log` 里那一长串 `graphlemon: auto-commit`/`merge`/artifact 提交就是证据),只是仍未到达 `finalized`。连"总结性 收口文档"都会在自举持续运行的过程中被甩在后面——这是个会持续放大的风险,不是一次性的疏漏。 diff --git a/docs/reference/graph-engineering-1000-agents-5-steps.md b/docs/reference/graph-engineering-1000-agents-5-steps.md index 49ed228..65773c8 100644 --- a/docs/reference/graph-engineering-1000-agents-5-steps.md +++ b/docs/reference/graph-engineering-1000-agents-5-steps.md @@ -3,7 +3,7 @@ - **来源**:微信公众号文章,https://mp.weixin.qq.com/s/tR0Z_3Yka2dNXQPO7P0OsA - **收录方式**:原文由用户在对话中手动粘贴全文文本(微信有反爬验证,工具无法直接抓取正文);正文引用的插图未能一并保存,下文以 `[图片]` 标记原文插图位置。 - **入库日期**:2026-08-04 -- **说明**:本文与 `docs/Graph Engineering with Claude_ 14-Step roadmap from 0 to graph architect (Full Course).md`(14 步版)是同一主题的姊妹篇/精简版,均围绕 Claude Code 的 Dynamic Workflows 功能展开,可与该文对照阅读。 +- **说明**:本文与 `docs/reference/Graph Engineering with Claude_ 14-Step roadmap from 0 to graph architect (Full Course).md`(14 步版)是同一主题的姊妹篇/精简版,均围绕 Claude Code 的 Dynamic Workflows 功能展开,可与该文对照阅读。 --- diff --git a/docs/reference/graph-engineering-vs-loop-engineering-overview.md b/docs/reference/graph-engineering-vs-loop-engineering-overview.md index 69c5514..11c9c79 100644 --- a/docs/reference/graph-engineering-vs-loop-engineering-overview.md +++ b/docs/reference/graph-engineering-vs-loop-engineering-overview.md @@ -3,7 +3,7 @@ - **来源**:腾讯云开发者微信公众号文章,https://mp.weixin.qq.com/s/0DGAr-N-rrYJrzxc2yiGEw - **收录方式**:原文由用户在对话中手动粘贴全文文本(微信有反爬验证,工具无法直接抓取正文);正文引用的插图未能一并保存,下文以 `[图片]` 标记原文插图位置。 - **入库日期**:2026-08-04 -- **说明**:与 `docs/reference/graph-engineering-1000-agents-5-steps.md`(5 步版)、`docs/Graph Engineering with Claude_ 14-Step roadmap from 0 to graph architect (Full Course).md`(14 步版)是同一主题下的第三篇材料,但视角不同——前两篇是"怎么用 Claude Code Dynamic Workflows 动手搭图"的实操教程,本文是更宏观的行业综述:术语起源、五层演进关系、Anthropic 官方模式总结、框架对比(LangGraph/AutoGen/ADK)与真实生产案例(LinkedIn、Uber)。 +- **说明**:与 `docs/reference/graph-engineering-1000-agents-5-steps.md`(5 步版)、`docs/reference/Graph Engineering with Claude_ 14-Step roadmap from 0 to graph architect (Full Course).md`(14 步版)是同一主题下的第三篇材料,但视角不同——前两篇是"怎么用 Claude Code Dynamic Workflows 动手搭图"的实操教程,本文是更宏观的行业综述:术语起源、五层演进关系、Anthropic 官方模式总结、框架对比(LangGraph/AutoGen/ADK)与真实生产案例(LinkedIn、Uber)。 --- diff --git a/docs/sdd/00-SDD-生成记录.md b/docs/sdd/00-SDD-生成记录.md index 6fcf552..f8b8f64 100644 --- a/docs/sdd/00-SDD-生成记录.md +++ b/docs/sdd/00-SDD-生成记录.md @@ -5,7 +5,7 @@ | 字段 | 值 | |---|---| | project | graphlemon | -| version | v1.7.0(决策:MVP 阶段只用 Claude Code ACP 一种执行后端,不考虑 Hermes CLI,见架构文档 7.2 节;新增 Claude Code plan 额度耗尽的等待重试机制 `p_usage_limit_wait`,见 03-C 3.6 节、04-P 4.4.1 节;新增 PH5 自举能力——Deploy Supervisor 蓝绿部署与自动回滚,见 01-M 1.15/1.16 节、03-C 3.15 节、04-P 4.6 节、06 节 REQ-15,详细设计见 `docs/supervisor-auto-rollback-design.md`) | +| version | v1.7.0(决策:MVP 阶段只用 Claude Code ACP 一种执行后端,不考虑 Hermes CLI,见架构文档 7.2 节;新增 Claude Code plan 额度耗尽的等待重试机制 `p_usage_limit_wait`,见 03-C 3.6 节、04-P 4.4.1 节;新增 PH5 自举能力——Deploy Supervisor 蓝绿部署与自动回滚,见 01-M 1.15/1.16 节、03-C 3.15 节、04-P 4.6 节、06 节 REQ-15,详细设计见 `docs/done/supervisor-auto-rollback-design.md`) | | owner | bboonnee | | effective_date | 2026-07-30 | | 方法论 | 复用本地 Hermes `planning` profile 的 `project-sdd-planning` skill(MVCP:Model/View/Controller/Process) | diff --git a/docs/sdd/01-M-模型设计.md b/docs/sdd/01-M-模型设计.md index 3530779..fc9d8b6 100644 --- a/docs/sdd/01-M-模型设计.md +++ b/docs/sdd/01-M-模型设计.md @@ -223,7 +223,7 @@ - `updated_by` string,not null - `updated_at` timestamp -## 1.15 实体:`deployment`(PH5 新增,2026-07-30,见 `docs/supervisor-auto-rollback-design.md`) +## 1.15 实体:`deployment`(PH5 新增,2026-07-30,见 `docs/done/supervisor-auto-rollback-design.md`) **目的**:Deploy Supervisor 对 graphlemon 自身(self-target 场景)执行一次蓝绿部署的记录,覆盖从候选实例拉起到最终 `finalized`/`rolled_back`/`blocked_pending_human` 的完整生命周期。Deploy Supervisor 是独立于 Graph Engine 之外的进程,不要与架构文档里"Supervisor(调度子 Agent,Coder Work Graph 内部监控节点)"混淆,两者是完全不同的概念(详见 `supervisor-auto-rollback-design.md` 开头命名说明)。 @@ -238,7 +238,7 @@ - `previous_instance_dir` string,not null —— 上一个已知良好版本的 checkout 目录,在本次 deployment 监控窗口正常关闭(`finalized`)前不允许清理 - 约束:同一时刻 graphlemon 自身仓库只允许一条处于非终态(不在 `finalized`\|`rolled_back`\|`blocked_pending_human`)的 `deployment` 记录,避免并发蓝绿部署互相干扰 -## 1.16 实体:`deploy_health_signal`(PH5 新增,2026-07-30,见 `docs/supervisor-auto-rollback-design.md`) +## 1.16 实体:`deploy_health_signal`(PH5 新增,2026-07-30,见 `docs/done/supervisor-auto-rollback-design.md`) **目的**:`deployment` 生命周期内每次健康检查/监控采样的记录,全部是确定性判断结果,不涉及任何 LLM 判断。 diff --git a/docs/sdd/03-C-控制与接口.md b/docs/sdd/03-C-控制与接口.md index a5fef69..19b5862 100644 --- a/docs/sdd/03-C-控制与接口.md +++ b/docs/sdd/03-C-控制与接口.md @@ -259,7 +259,7 @@ - 断线重连约束:客户端重连后需带上 `last_event_id` 查询自该事件之后的遗漏事件(通过 `c_task_get` 系接口的时间线字段补齐,而非 WebSocket 层做消息重放缓冲,MVP 阶段简化处理) - `owner_team`:control-plane -## 3.15 Deploy Supervisor 控制操作(PH5 新增,2026-07-30,见 `docs/supervisor-auto-rollback-design.md`) +## 3.15 Deploy Supervisor 控制操作(PH5 新增,2026-07-30,见 `docs/done/supervisor-auto-rollback-design.md`) 以下五个操作均由 Deploy Supervisor 进程调用,不是控制台前端直接调用的 HTTP API(Deploy Supervisor 是独立于 Graph Engine 之外的确定性 Python 进程,见 01-M 1.15 节),标记为内部接口。所有决策逻辑均为确定性代码,不接入任何 LLM 判断。触发前提:`task` 关联的 `project.target_folder_path == graphlemon 平台自身安装路径` 且 Validator 判定 PASS 后,worktree 分支已 merge 回主分支(见 3.11 节 `core_engine` 路由分支)。 diff --git a/docs/sdd/04-P-流程与状态流转.md b/docs/sdd/04-P-流程与状态流转.md index e97f9fd..f7fb233 100644 --- a/docs/sdd/04-P-流程与状态流转.md +++ b/docs/sdd/04-P-流程与状态流转.md @@ -101,7 +101,7 @@ 约束:清理过程只删除物理文件和更新 `execution_log_file` 状态,不删除 `execution_log_file` 记录本身、不删除任何 `execution_event`/`task`/`artifact` 等业务数据,保证"日志曾经存在过"这件事可审计(对应 01-M 1.13 节约束)。 -## 4.6 流程:`p_deploy_supervisor_blue_green`(PH5 新增,2026-07-30,见 `docs/supervisor-auto-rollback-design.md`) +## 4.6 流程:`p_deploy_supervisor_blue_green`(PH5 新增,2026-07-30,见 `docs/done/supervisor-auto-rollback-design.md`) - `start_state`:`staged` - `end_state`:`finalized`(安全终态)| `blocked_pending_human`(唯一需人工介入的终态,触发条件严格限定为"自动回滚机制本身失败",不是"代码写得不好") diff --git a/docs/sdd/05-实施约束与依赖.md b/docs/sdd/05-实施约束与依赖.md index 8b160b6..99b1563 100644 --- a/docs/sdd/05-实施约束与依赖.md +++ b/docs/sdd/05-实施约束与依赖.md @@ -110,9 +110,9 @@ - **worktree 无条件强制**:`target_folder_path == graphlemon 平台自身安装路径` 时,无论并行还是串行,Coder/Validator 一律强制使用 `git_worktree_ref`,不适用 03-C 3.6 节"仅并行写代码节点才强制申请 worktree"的一般规则——这是该规则的 self-target 特例 - **`core_engine` 分类路由**:`bug_record.root_cause_category` 新增枚举值 `core_engine`,覆盖 `.claude/agents/*.md` 身份文件变更、Graph Engine 调度器/执行器抽象层核心逻辑变更、01-M 自身数据库 schema 变更三类场景。命中 `core_engine` 时,`c_org_loop_route`(03-C)路由到 PH5 的 `p_deploy_supervisor_blue_green` 流程(04-P),而不是常规的 `fast_lane`/`full_replan` -- **不要求提交前人工评审**:`core_engine` 改动不强制在合并主分支前经过人工评审(此前"比照 `org_graph_definition` 强制人工 PR 评审"的提案已被用户明确否决),改为合并后由 Deploy Supervisor 持续监控运行时行为、检测异常时自动回滚(见 04-P `p_deploy_supervisor_blue_green`、`docs/supervisor-auto-rollback-design.md`)。5.6 节"允许停机迁移"的简化假设不适用于这类 self-target 场景——self-target 的 schema 变更同样走蓝绿部署+监控流程,不是简单停机迁移 +- **不要求提交前人工评审**:`core_engine` 改动不强制在合并主分支前经过人工评审(此前"比照 `org_graph_definition` 强制人工 PR 评审"的提案已被用户明确否决),改为合并后由 Deploy Supervisor 持续监控运行时行为、检测异常时自动回滚(见 04-P `p_deploy_supervisor_blue_green`、`docs/done/supervisor-auto-rollback-design.md`)。5.6 节"允许停机迁移"的简化假设不适用于这类 self-target 场景——self-target 的 schema 变更同样走蓝绿部署+监控流程,不是简单停机迁移 -## 5.9 Deploy Supervisor MVP 固定参数(`docs/supervisor-auto-rollback-design.md` §9,2026-07-30 回填) +## 5.9 Deploy Supervisor MVP 固定参数(`docs/done/supervisor-auto-rollback-design.md` §9,2026-07-30 回填) | 参数 | 草案值 | 说明 | |---|---|---| diff --git a/docs/sdd/06-验收与测试锚点.md b/docs/sdd/06-验收与测试锚点.md index f8e4e6c..cc04953 100644 --- a/docs/sdd/06-验收与测试锚点.md +++ b/docs/sdd/06-验收与测试锚点.md @@ -114,7 +114,7 @@ - boundary:配置 `retention_days=1`,构造一条 `created_at` 超过 1 天的 `closed` 状态日志,清理任务运行后应转为 `purged` 且物理文件被删除;`v_log_viewer` 打开该日志时展示"已被清理"提示而非报错 - failure:订阅一个不存在的 `node_exec_id` 的日志流,应返回 `NODE_EXEC_NOT_FOUND`;清理任务遇到某个文件删除失败(如被占用)时,应仅记录该项失败、不影响其余候选项的清理,且该条目下次周期继续重试 -## REQ-15:Deploy Supervisor 蓝绿部署与自动回滚(PH5 新增,2026-07-30,见 `docs/supervisor-auto-rollback-design.md`) +## REQ-15:Deploy Supervisor 蓝绿部署与自动回滚(PH5 新增,2026-07-30,见 `docs/done/supervisor-auto-rollback-design.md`) - 覆盖:M=`deployment`,`deploy_health_signal`,`bug_record`(`root_cause_category=core_engine`);C=`c_deploy_supervisor_stage`,`c_deploy_supervisor_health_check`,`c_deploy_supervisor_cutover`,`c_deploy_supervisor_monitor`,`c_deploy_supervisor_rollback`,`c_org_loop_route`;P=`p_deploy_supervisor_blue_green` - 验收用例: diff --git a/docs/self-development-feasibility.md b/docs/self-development-feasibility.md deleted file mode 100644 index 9d636b5..0000000 --- a/docs/self-development-feasibility.md +++ /dev/null @@ -1,68 +0,0 @@ -# graphlemon 自举可行性分析:能不能自己开发自己 - -**日期**: 2026-07-29(2026-07-30 更新:缺口 2/3 已完成详细设计,见更新记录) -**性质**: 探索性分析,不是已确认的架构决策,未写入 `sdd-mvcp-contract.json`。若决定采纳,需要走一次正式的 AskUserQuestion 决策流程,再回填进 05-实施约束与依赖.md 和 contract.json。 -**结论先说**:机制上大部分已经具备(三个 Org Agent 落在目标项目 `.claude/agents/*.md`、`c_project_create` 对 `target_folder_path` 没有排除平台自身路径、git worktree 隔离已经是既定决策),但有三个真缺口会让"自己改自己"从"能跑"变成"跑了但可能很危险"。下面先列缺口,再用一个具体场景走一遍,标出每一步是"✅ 已具备"还是"⚠️ 缺口"。 - -**2026-07-30 更新记录**:用户就三个缺口逐条给出方向性决策(1. 确认无条件强制 worktree;2. 确认需要独立 supervisor 进程,可考虑复用 Hermes CLI;3. **明确否决"核心文件强制人工评审"的原提案**,改为"supervisor 自动监督回滚即可,不需要人工评审")。据此完成了 `docs/supervisor-design-comparison.md`(方案选型:不把 Hermes CLI 接入 supervisor 决策链路,改为复用 graphloop 已验证过的隔离/manifest/apply 库,见 `supervisor/isolation/`)和 `docs/supervisor-auto-rollback-design.md`(缺口 2 的蓝绿实例机制 + 缺口 3 的自动回滚机制完整状态机设计)。下面缺口 2/3 的原始描述保留作为问题记录,其"推荐方案"段落已更新为指向这两份新文档。 - -## 1. 前提:自举不是从零开始的 - -自我迭代能力只有在 PH0/PH1(development-plan.md)已经由人类(配合 Claude Code 交互式开发,就是我们现在这个对话在做的事)跑通一次之后才谈得上——这不是缺口,是任何自举系统的必然前提,就像第一版 C 编译器不可能用 C 自己写。development-plan.md 的关键路径已经隐含了这一点。 - -## 2. 三个真缺口 - -### 缺口 1:worktree 隔离目前只在"并行"时强制,自我修改时必须无条件强制 - -03-C 3.6 节现有约束是"**并行**写代码的节点必须先申请 `git_worktree_ref`"。改别的 target project 时,单个 Coder 节点串行工作直接改主 checkout 问题不大。但如果 target project 就是 graphlemon 自己,正在跑这个任务的后端进程本身就在读取这份代码——直接改主 checkout 有实时干扰正在运行的调度器的风险。 - -**需要新增规则**:`target_folder_path == graphlemon 平台自身安装路径` 时,无论是否并行,一律强制 `git_worktree_ref`,且 Coder/Validator 的自测/复核全部在 worktree 副本里跑,不碰主 checkout。 - -### 缺口 2:没有"应用变更 + 生效"这一步——这是最大的缺口 - -Validator 在 worktree 里 PASS 之后,现有机制(git_worktree_ref 执行完成后清理并合并回主分支)能把代码合并进 graphlemon 自己仓库的主分支。**但合并到主分支的代码不会被正在运行的那个 Node.js/Python 进程自动重新加载**。需要重启才能生效,而"正在执行调度逻辑的那个进程"不可能自己发指令重启自己——发出重启指令的那一刻,进程就要被杀掉了。 - -当前 SDD 里没有任何 controller/process 定义这一步。这不是简单补个接口能解决的,需要一个新的架构组件。 - -**推荐方案(蓝绿实例,而不是原地重启)**:不在主进程里做"重启自己",而是让 Validator 直接对着 worktree 里跑起来的一个**独立的第二份 graphlemon 实例**(换端口、换一份临时/只读数据库)做验证;通过后由一个**在 Graph Engine 控制范围之外**的独立 **Deploy Supervisor**(与架构文档里 Coder Work Graph 内部的"Supervisor(调度子 Agent)"是两个不同概念,注意区分)执行切换:停旧实例 → merge 分支到主分支 → 起新实例。这样"运行中的自己"永远不需要重启自己。 - -**(2026-07-30 已完成详细设计并回填正式 SDD)** 完整状态机、健康检查信号、cutover/回滚流程、MVP 固定参数见 `docs/supervisor-auto-rollback-design.md`;已同步进 01-M/03-C/04-P/05/06/contract.json(新增 REQ-15)。方案选型(为什么不直接用 Hermes CLI 当 Deploy Supervisor 的决策大脑,而是复用 graphloop 已验证的隔离/manifest/apply 库)见 `docs/supervisor-design-comparison.md`。 - -### 缺口 3:核心代码/身份文件的自我修改没有对应缺口 3.5 节 `org_graph_definition` 那样的强制人工评审 - -05 节 §5.6 已经规定 `org_graph_definition`(Org Graph 结构 YAML)的变更必须走人工 PR 评审,不允许系统自主提交;`skill`(Work Graph 技能定义)允许系统自主迭代但要求 Git 可回滚。但这两条规则都没覆盖: - -1. **`.claude/agents/{planner,coder,validator}.md`**(Org Agent 身份文件,对应 SOUL.md)——如果 Coder 被要求"优化一下 Validator 的判断逻辑",改的是判定裁决者本身的人设,一旦改坏(比如让它变得更宽松、更容易判 PASS),系统会在没有外部校验的情况下自我欺骗,而且没人会发现,因为发现问题的机制本身已经被改坏了。 -2. **调度器/执行器抽象层等核心引擎代码**——和改一个 UI 组件的风险完全不是一个量级。 - -**原提案(已被用户否决,保留作记录)**:曾建议 `.claude/agents/*.md` 的变更、以及触碰 Graph Engine 调度器/执行器抽象层核心逻辑的变更一律强制 `blocked_pending_human`,走和 `org_graph_definition` 一样的人工评审通道。 - -**用户更正(2026-07-30,原话)**:"改坏了的 validator 没有人工评审倒不是问题,坏了由 supervisor 能够监督回滚即可。所以核心是需要一个三方的 agent 来作为补充 supervisor 接口。" —— 即:不要求核心文件改动必须先经人工评审才能生效,而是允许系统自主提交,由缺口 2 的同一个 Supervisor 在部署后持续监控行为信号(例如 Validator 的 approve 率是否偏离历史基线),一旦检测到异常自动回滚,不需要人在"改之前"把关。 - -**(2026-07-30 已完成详细设计)** `docs/supervisor-auto-rollback-design.md` 第 6 节 `validator_approval_rate` 信号即直接对应"改坏了的 validator"这个场景:从 `execution_event`/`work_node_execution` 表按滚动窗口统计 Validator 判定 PASS 的比例,与部署前的历史基线做纯算术比较,偏离阈值即触发自动回滚(该文档第 7.2 节),全程不需要人工评审代码内容。仍保留的唯一人工介入点是"自动回滚机制本身失败"(例如连上一个已知良好版本都起不来)这类基础设施兜底场景(该文档第 7.3 节),而不是"代码判断谁对谁错"。 - -`bug_record.root_cause_category` 新增 `core_engine` 分类的建议仍然保留,但用途从"触发强制人工评审"改为"触发时跳过 `fast_lane`、走完整的蓝绿部署+监控流程"(普通 Skill 级别的自主迭代可能不需要这么重的验证)。 - -同理,如果这次自我修改涉及 01-M 自身的数据库 schema(比如给 `work_node_execution` 加字段),05 §5.6 现有的"允许停机迁移"假设是给人工运维用的,没覆盖"系统自主决定给自己数据库做 schema 变更"——这类改动同样纳入蓝绿部署+监控流程,而不是强制转人工,具体的 schema 迁移在蓝绿切换中的时序(迁移脚本什么时候跑、跑坏了怎么回滚)是 `supervisor-auto-rollback-design.md` 未覆盖的开放问题,需要后续补充。 - -## 3. 模拟走一遍:修复自己状态机里的一个边界 bug - -**场景**:人类发现 graphlemon 自己的 `p_org_task_lifecycle` 状态机实现里,`loop_round >= 3` 转 `blocked_pending_human` 的判断写成了 `loop_round > 3`(off-by-one,第 3 轮本该拦截却继续跑了)。这段逻辑就在 graphlemon 自己的调度器代码里——一个很好的自举测试用例,因为它既真实又刚好踩中"核心引擎代码"这条线。 - -| 步骤 | 操作 | 状态 | -|---|---|---| -| 1. 项目登记 | `c_pick_folder` 选中 graphlemon 自己的仓库路径,`c_project_create` 校验 `has_git_repo=true`(它自己确实是),创建 `.graphlemon/` 子目录 | ✅ 已具备,架构对 `target_folder_path` 没有排除平台自身路径 | -| 2. 提出目标 | `c_task_create({goal_text: "修复 p_org_task_lifecycle 里 loop_round 边界判断的 off-by-one", project_id: })` | ✅ 已具备 | -| 3. Planner 触发 | `c_org_node_trigger(planner)`,Planner 读取的正是 graphlemon 自己的 04-P 文档 + 对应源码 | ⚠️ 轻微缺口:Planner 的 skill 是按"从 PRD 写新 SDD"设计的,这次是"改一处已有实现",`c_work_graph_plan` 目前没有区分这两种 `graph_manifest` 生成路径。大概率能工作(Planner 有能力判断这是小范围修复),但没有显式设计,建议补一条轻量修复路径的规则 | -| 4. Coder 节点执行 | `c_work_node_run` 触发 Coder,按缺口 1 的新规则强制走 `git_worktree_ref`,在隔离副本里改代码、跑单测 | ⚠️ 命中缺口 1(需要新规则:self-target 场景无条件强制 worktree) | -| 5. Validator 复核 | Validator 在 worktree 副本里复现原 bug 场景,确认修复且无回归 | ⚠️→✅ 命中缺口 3:这次改动是调度器核心代码,`root_cause_category` 应标记 `core_engine`,触发的是完整蓝绿部署+监控流程(而不是强制转人工),机制已设计(`supervisor-auto-rollback-design.md`) | -| 6. 合并到主分支 | worktree 分支 merge 回 graphlemon 自己仓库的主分支 | ✅ 机制已具备(07 节"执行完成后清理 worktree 并合并回主分支") | -| 7. 生效 | 需要让正在运行的后端进程加载新代码 | ⚠️→✅ 命中缺口 2(最大的缺口):需要蓝绿实例 + 独立于 Graph Engine 之外的 Deploy Supervisor 来完成切换,完整状态机/健康检查/回滚流程已设计并回填 SDD(`supervisor-auto-rollback-design.md`),尚未实现代码 | -| 8. 数据库 schema | 本例的判断逻辑修复不涉及 01-M schema 变更,不触发 | (本例未命中,但换个例子——比如给 `work_node_execution` 加字段——会命中缺口 3 的 schema 部分,`supervisor-auto-rollback-design.md` 第 10 节已列为未覆盖的开放问题) | -| 9. 完成 | 蓝绿切换完成(Deploy Supervisor 健康检查通过后自动完成,无需人工确认)后 `task.status=done` | 走到这一步,一次"系统被告知自己的缺陷 → 自己诊断根因 → 自己修复 → 自己验证 → 自动部署并被 Deploy Supervisor 持续监控(异常自动回滚)"的完整闭环就跑通了,且全程不需要人工介入代码内容评审 | - -## 4. 小结 - -9 步里 3 步直接可用,1 步大概率可用但没显式设计,剩下 5 步(含最关键的"合并后如何生效")命中三个真缺口。**2026-07-30 更新**:缺口 2(蓝绿实例生效)和缺口 3(核心文件自我修改的治理,按用户更正为"自动回滚而非强制人工评审")已完成详细设计,分别落在 `docs/supervisor-auto-rollback-design.md`(状态机/健康检查/回滚流程/MVP 参数)和 `docs/supervisor-design-comparison.md`(方案选型依据)。缺口 1(worktree 无条件强制)仍是待写入 05 节的一条治理规则,成本低。 - -如果要推进,建议顺序:先把缺口 1(一条治理规则)写进 05 节和 contract.json,成本低、马上能落地;缺口 2/3 的设计已经具备可实施粒度,可以作为 PH4 之后的独立 Phase(PH5:自举能力)排入 `development-plan.md`,不影响当前 PH0-PH4 主线——具体是现在就把 PH5 的 Unit 分解也做出来,还是先确认设计稿本身没问题再排,由用户决定。 diff --git a/docs/supervisor-auto-rollback-design.md b/docs/supervisor-auto-rollback-design.md deleted file mode 100644 index ac72a9a..0000000 --- a/docs/supervisor-auto-rollback-design.md +++ /dev/null @@ -1,170 +0,0 @@ -# Deploy Supervisor 蓝绿实例自动回滚机制设计 - -**日期**: 2026-07-30(同日更新:完成 Supervisor 概念改名消歧,见下方命名说明) -**性质**: 详细设计,回应用户"自动回滚机制现在就设计一下,省得到 PH5 阶段有那个阶段的问题"。落地 `self-development-feasibility.md` 缺口 2(应用变更+生效)与缺口 3(用户更正版:不要求人工评审,要求 supervisor 自动监督回滚)。 -**依赖**: `docs/supervisor-design-comparison.md`(方案选型结论:复用 graphloop 隔离/manifest/apply 库,见 `supervisor/isolation/`;Deploy Supervisor 自身决策逻辑必须是确定性代码,不接入 LLM)。 -**状态**: 设计已回填进正式 SDD(`01-M`/`03-C`/`04-P`/`05`/`06`/`sdd-mvcp-contract.json`),代码尚未实现(`supervisor/isolation/` 除外,那是通用库不是本机制专属)。 - -**命名说明(2026-07-30)**:`graphlemon_architecture.md` 里已经有一个"Supervisor(调度子 Agent)"概念(Coder Work Graph 内部的常驻监控节点,负责监控 Work Node 执行状态、触发 reflection/重新调度)。本文档描述的是完全不同的另一个组件——一个**独立于 Graph Engine 之外**、专门负责 graphlemon 自我修改后蓝绿部署与自动回滚的进程。为避免和架构文档里的既有概念混淆,本文档统一称之为 **Deploy Supervisor(部署监督进程)**,所有标识符前缀使用 `c_deploy_supervisor_*`,与既有的 `Supervisor(调度子 Agent)`概念完全区分开:两者职责不同(一个管 Work Node 执行进度,一个管整个 graphlemon 进程的部署/回滚)、进程边界不同(一个在 Graph Engine 进程内,一个在 Graph Engine 进程外)、生命周期不同(一个随 Work Graph 任务存在,一个是常驻的独立进程)。 - -## 1. 触发范围 - -本机制**只在** `target_folder_path == graphlemon 平台自身安装路径`(self-target 场景,`self-development-feasibility.md` 缺口 1 已定义的判断条件)且 Validator 对该次修改判定 PASS 之后触发。改别的 target project 时,走既有的"worktree 合并回主分支"流程即可,不涉及蓝绿实例——因为改的代码不是正在跑的这个进程本身。 - -触发时点:Validator PASS → worktree 分支 merge 回 graphlemon 自身仓库主分支(既有机制,`self-development-feasibility.md` 步骤 6)→ **Deploy Supervisor 从这里接管**,直到新代码安全生效或安全回滚为止。 - -## 2. 组件与进程边界 - -三个独立操作系统进程,任何一个崩溃不影响另外两个: - -1. **Live 实例**:当前正在服务的 graphlemon 后端(Graph Engine + 控制台 API),监听固定端口 `LIVE_PORT`,使用固定路径的生产 SQLite 库 `PROD_DB_PATH`。 -2. **Candidate 实例**:本次自我修改合并后的新版本,Deploy Supervisor 拉起来做验证,监听临时端口,使用**一次性 DB 副本**,与生产 DB 完全隔离,验证期间无论 Candidate 干什么都不影响 Live。 -3. **Deploy Supervisor 进程**:独立于以上两者的确定性 Python 脚本(stdlib only,无 LLM 调用),是本设计的主体。职责:拉起/健康检查/切换/监控/回滚 Candidate 与 Live,自身不做任何"业务判断"(比如不判断代码写得好不好),只做"这几个信号是否符合预设阈值"这种算术判断。 - -Deploy Supervisor 与 Live/Candidate 之间没有共享内存/共享失败域——这正是用户"第三方 agent"要求的字面落地:Deploy Supervisor 挂了不会拖死 Live,Live 挂了 Deploy Supervisor 还能观察到并处理。 - -## 3. 数据模型(已回填 `01-M-模型设计.md`) - -两个新实体: - -### `deployment` - -| 字段 | 类型 | 说明 | -|---|---|---| -| `deployment_id` | string, PK | | -| `git_commit_hash` | string | 本次候选版本对应的 commit | -| `previous_commit_hash` | string | 回滚目标(上一个 `status=finalized` 的 deployment 的 `git_commit_hash`) | -| `status` | enum | 见第 5 节状态机 | -| `candidate_port` | int, nullable | 健康检查阶段临时端口 | -| `candidate_db_path` | string, nullable | 一次性 DB 副本路径,验证结束后清理 | -| `started_at` / `cutover_at` / `monitoring_closed_at` / `rollback_at` | timestamp, nullable | | -| `rollback_reason` | string, nullable | 见第 6 节信号类型 | -| `previous_instance_dir` | string | 保留的上一个已知良好版本 checkout 目录,直到本次 deployment 监控窗口正常关闭才允许清理 | - -### `deploy_health_signal` - -| 字段 | 类型 | 说明 | -|---|---|---| -| `signal_id` | string, PK | | -| `deployment_id` | string, FK | | -| `sampled_at` | timestamp | | -| `signal_type` | enum: `process_alive` \| `http_health` \| `canary_task` \| `validator_approval_rate` | | -| `status` | enum: `ok` \| `degraded` \| `fail` | | -| `detail` | string | 供人工事后排查,不参与自动判断 | - -## 4. Cutover 机制(部署新版本) - -1. Deploy Supervisor 拉取合并后的主分支到全新目录(不是原地 `git pull`,避免影响可能仍在读取源码的 Live 进程),生成 `deployment` 记录,`status=staged`。 -2. 复制 `PROD_DB_PATH` 到 `candidate_db_path`(一次性副本,用完即删,绝不让 Candidate 直接碰生产库)。 -3. 用 `supervisor/isolation/isolated_workspace.isolate_workspace()` 家族同款隔离原则拉起 Candidate 进程:`subprocess.Popen`,`shell=False`,固定 argv 形态,监听 `candidate_port`(生产端口 +1,或探测一个空闲端口),指向 `candidate_db_path`。`status=candidate_starting`。 -4. 跑健康检查(见第 6 节),`status=health_check_running`。 - - 全部通过 → `status=health_check_passed`,进入 5。 - - 任一失败/超时 → `status=health_check_failed` → 走**回滚路径 A**(第 7.1 节)。 -5. **Cutover 本体**(本地单用户桌面应用,不需要处理并发外部流量/负载均衡,这一条假设需要用户确认,见第 9 节开放问题): - - Live 停止接受新的 Org Node 触发(不接受新 `c_org_node_trigger` 调用),但允许已在执行的 `work_node_execution` 走到可安全中断的检查点(已有的持久化恢复机制保证中断后可恢复,不强求"优雅排空")。 - - Live 进程退出。 - - Candidate 关闭一次性 DB 副本连接,改连 `PROD_DB_PATH`,改绑 `LIVE_PORT`。 - - Candidate 成为新的 Live。`status=live`,`cutover_at=now`。 - - 原来的 Live checkout 目录保留为 `previous_instance_dir`,**不删除**,直到监控窗口正常结束。 - -## 5. 完整状态机 - -``` -staged - -> candidate_starting - -> health_check_running - -> health_check_failed [回滚路径 A:见 7.1,不影响 Live,直接结束] - -> health_check_passed - -> cutover_in_progress - -> live - -> monitoring (进入监控窗口) - -> anomaly_detected -> rollback_in_progress - -> rolled_back [回滚路径 B:见 7.2,Live 换回旧版本] - -> rollback_failed -> blocked_pending_human [兜底,见 7.3] - -> monitoring_window_clean -> finalized (旧 previous_instance_dir 可清理) -``` - -`finalized` 是唯一的"安全终态";`blocked_pending_human` 是唯一需要人工介入的终态,且触发条件严格限定为"自动回滚机制本身失败",不是"代码写得不好"——这与用户对缺口 3 的更正(不要求人工评审代码内容)不冲突,是不同层面的安全网。 - -## 6. 健康检查 / 监控信号(全部确定性,不接入任何 LLM 判断) - -### Pre-cutover 健康检查(`health_check_running`,有界超时,全部通过才能 cutover) - -| 检查项 | 方法 | 判定 | -|---|---|---| -| `process_alive` | 启动后轮询 `os.kill(pid, 0)`,超时 `HEALTH_CHECK_STARTUP_TIMEOUT` | 超时未存活 = fail | -| `http_health` | 请求 Candidate 的 `/api/health`,超时 `HEALTH_CHECK_HTTP_TIMEOUT` | 非 200 或超时 = fail | -| `canary_task` | 对 Candidate 跑一个固定的、结果可预期的最小任务(例如创建一个临时 project + 一个已知会通过的 trivial task,断言最终 `task.status` 与预期字段一致),超时 `HEALTH_CHECK_CANARY_TIMEOUT` | 结果不符/超时/异常 = fail | - -任一 fail 立即进入回滚路径 A,不重试(重试属于"给坏版本第二次机会",不符合 fail-closed 原则;要重试整个自我修改,走 Planner 重新出一版)。 - -### Post-cutover 监控(`monitoring`,窗口内持续采样) - -| 信号 | 方法 | 判定阈值(MVP 固定值,不做用户可配) | -|---|---|---| -| `process_alive` | 每 `MONITOR_SAMPLE_INTERVAL` 探活 | 连续崩溃/重启 ≥ `MAX_CRASH_COUNT` 次 / `CRASH_WINDOW` 内 = anomaly | -| `http_health` | 同上频率请求 `/api/health` | 连续 `MAX_CONSECUTIVE_HTTP_FAIL` 次失败 = anomaly | -| `canary_task` | 每 `CANARY_INTERVAL` 跑一次固定 canary 任务 | 单次失败即 anomaly(canary 本身就是"已知应该过"的任务,失败没有灰色地带) | -| `validator_approval_rate` | 从 `execution_event`/`work_node_execution` 表按滚动窗口统计 Validator 节点 `approved` 占比,与 deployment 之前的历史基线比较 | 偏离基线超过 `VALIDATOR_RATE_DEVIATION_THRESHOLD`(绝对值,如 ±30 个百分点)= anomaly。**这一条直接对应用户点名的场景**:"改坏了的 validator"——如果新版本的 Validator 变得异常宽松(approve 率骤升)或异常严格(approve 率骤降到几乎全部拒绝),都会被这条信号捕获,且判断是纯算术,不需要另一个 LLM 来"评价 Validator 判得对不对" | - -监控窗口:`MONITORING_WINDOW_HOURS`(固定值,如 24h)或 `MONITORING_MIN_TASK_COUNT`(固定值,如 10 个真实任务)先到者为准,之后无 anomaly 则 `monitoring_window_clean → finalized`。 - -## 7. 回滚路径 - -### 7.1 路径 A:Pre-cutover 回滚(健康检查未过) - -Live 从未被动过,回滚成本最低: - -1. 终止 Candidate 进程(`SIGTERM`,参考 `hermes_watchdog.py` 的终止哲学)。 -2. 删除 `candidate_db_path` 一次性副本。 -3. `git revert` 本次合并 commit(不是 `reset`/force-push,保留历史可追溯,这一步复用 `workspace_security.validate_subprocess_command()` 的 argv 校验)。 -4. 发一个 `bug_record`,`root_cause_category=core_engine`,把健康检查失败的具体信号/日志作为 evidence 挂上去,供下一轮 Planner/Coder 重新尝试时参考。 -5. `status=health_check_failed`,记录结束,Live 全程未受影响,调度立即可以继续处理下一个任务。 - -### 7.2 路径 B:Post-cutover 回滚(监控期间检测到 anomaly) - -Live 已经是 Candidate,需要真正"换回去": - -1. 记录触发回滚的信号快照(哪个 signal_type、哪次采样、什么值)到 `deployment.rollback_reason`。 -2. 当前 Live(即出问题的新版本)停止接受新触发,同样允许在途节点走到可恢复检查点。 -3. 用 `previous_instance_dir`(第 4 步保留的上一版本 checkout)重新拉起进程,绑定 `LIVE_PORT`,连接 `PROD_DB_PATH`(生产库这次全程没有被换过,只是换了跑在它前面的代码进程,数据不受影响——这是 blue-green 相对"原地改代码"最大的安全收益)。 -4. 对"复活的旧版本"跑一遍 7.1 同款的三项健康检查,确认旧版本本身没问题(正常情况下必过,因为它是上一次已经 `finalized` 的版本)。 -5. 通过 → `status=rolled_back`,当前 Live = 旧版本,本次自我修改的 commit 在主分支上被 `git revert`,发 `bug_record`(同路径 A 第 4 步,evidence 换成监控期间的异常信号),调度恢复正常。 -6. 不通过(极端情况:连旧版本都起不来,比如磁盘满了这类环境问题)→ 第 7.3 节。 - -### 7.3 兜底:回滚本身失败 - -`status=rollback_failed → blocked_pending_human`。这是本设计里唯一保留的人工介入点,且明确限定触发条件仅为"自动恢复机制本身失效"(旧版本也起不来、DB 不可写这类基础设施问题),不是"代码判断谁对谁错"。控制台需要能清楚展示这个区别,避免用户以为又回到了"需要人工评审代码改得好不好"的老路。 - -## 8. 与既有 SDD 的对接点(2026-07-30 已回填,见第二次更新记录) - -- `01-M-模型设计.md`:新增 `deployment`、`deploy_health_signal` 两个实体(第 3 节)。**已完成。** -- `03-C-控制与接口.md`:新增 `c_deploy_supervisor_stage` / `c_deploy_supervisor_health_check` / `c_deploy_supervisor_cutover` / `c_deploy_supervisor_monitor` / `c_deploy_supervisor_rollback` 五个控制操作;`c_org_loop_route` 扩展 `root_cause_category=core_engine` 的路由分支。**已完成。** -- `04-P-流程与状态流转.md`:新增 `p_deploy_supervisor_blue_green` 流程,状态转移表照搬第 5 节。**已完成。** -- `05-实施约束与依赖.md`:新增本机制的 MVP 固定参数表(第 9 节),比照现有 `p_usage_limit_wait` 参数(5→10→20→30min backoff、8h/9d 超时)"MVP 阶段硬编码、不做用户可配"的先例,登记同一张表里;新增 self-target worktree 无条件强制规则。**已完成。** -- `06-验收与测试锚点.md`:新增 REQ-15,覆盖:健康检查全过正常 cutover、健康检查失败路径 A、监控期 anomaly 触发路径 B、回滚本身失败兜底到 `blocked_pending_human` 四类 happy/failure 用例。**已完成。** -- `sdd-mvcp-contract.json`:同步以上四类变更,重新跑 `jsonschema.validate()`。**已完成。** -- `self-development-feasibility.md`:缺口 2/3 的"推荐方案"段落指向本文档。**已完成(2026-07-30 更新记录)。** - -## 9. MVP 固定参数(草案值,需要在实现前用真实测量校准,参考 A-1 的验证方法论) - -| 参数 | 草案值 | 说明 | -|---|---|---| -| `HEALTH_CHECK_STARTUP_TIMEOUT` | 30s | Candidate 进程从拉起到能响应探活 | -| `HEALTH_CHECK_HTTP_TIMEOUT` | 10s | 单次 `/api/health` 请求 | -| `HEALTH_CHECK_CANARY_TIMEOUT` | 120s | 一次最小 canary 任务从提交到出结果 | -| `MONITOR_SAMPLE_INTERVAL` | 5min | process/http 采样间隔 | -| `CANARY_INTERVAL` | 1h | 监控期 canary 任务运行间隔 | -| `MAX_CRASH_COUNT` / `CRASH_WINDOW` | 2 次 / 10min | 判定进程反复崩溃 | -| `MAX_CONSECUTIVE_HTTP_FAIL` | 3 次 | 判定健康端点持续不可用 | -| `VALIDATOR_RATE_DEVIATION_THRESHOLD` | ±30 个百分点 | 判定 Validator 行为异常 | -| `MONITORING_WINDOW_HOURS` | 24h | 监控窗口时长上限 | -| `MONITORING_MIN_TASK_COUNT` | 10 个真实任务 | 监控窗口任务数上限,先到者结束 | - -以上数值全部是工程经验草案,不是实测结果——和 `05-实施约束与依赖.md` §5.4.2 现有的额度等待退避参数一样,需要在真正实现并跑几轮真实自我修改后重新校准,不建议直接当最终值写进 contract.json 的强约束里(建议标注为"可调,需实测校准"而不是"硬编码不可变",这一点和额度等待参数刻意标注为"MVP-fixed"略有不同,因为额度等待的时间线是 Anthropic 平台文档定义的外部事实,而这里的阈值是纯粹的工程经验值)。 - -## 10. 未覆盖 / 开放问题(留给实现阶段决定,不阻塞本设计定稿) - -1. **单用户桌面场景假设**:第 4 节 cutover 假设不需要处理并发外部流量/连接排空,只有 Cowork 控制台这一个使用者,断线可重连。如果未来 graphlemon 要支持多用户/多终端同时连接,这里的"直接切端口"简化模型需要重新评估。 -2. **`candidate_db_path` 一次性副本的体积/耗时**:如果生产 DB 随着长期使用变得很大,每次 cutover 前整库复制可能变慢,需要的话可以后续改成"复制 schema + 近期数据子集"而不是整库,MVP 阶段先用整库复制,简单可靠优先。 -3. **`git revert` 冲突**:如果本次自我修改之后主分支又有新的(比如另一个并发自我修改任务的)提交,revert 可能冲突,需要 Deploy Supervisor 能识别 revert 失败并转 `blocked_pending_human`(不在当前状态机图里显式画出,应该并入 7.3 兜底路径,而不是新开一个分支——这样能保持状态机简单)。 diff --git a/docs/supervisor-design-comparison.md b/docs/supervisor-design-comparison.md deleted file mode 100644 index c89280a..0000000 --- a/docs/supervisor-design-comparison.md +++ /dev/null @@ -1,109 +0,0 @@ -# Deploy Supervisor 设计方案对比:自建 vs 复用 Hermes CLI - -**日期**: 2026-07-30(同日更新:完成命名消歧,见下方说明;结论已回填正式 SDD) -**性质**: 方案选型分析,回应用户 "都不确定,先看两种方案的详细对比",已回填 SDD/contract.json(见 `supervisor-auto-rollback-design.md` §8)。 -**输入**: 对 `graphloop`(同一项目前身,`/Users/yibo/Documents/lemonAI/graphloop`)的实地代码/任务记录调查。 - -**命名说明**:本文档讨论的"Deploy Supervisor"是独立于 Graph Engine 之外、负责 graphlemon 自我修改后蓝绿部署与自动回滚的进程,和架构文档里已有的"Supervisor(调度子 Agent,Coder Work Graph 内部监控节点)"是两个不同的概念,详见 `supervisor-auto-rollback-design.md` 开头的命名说明。本文档下文提到"supervisor"泛指这个待选型的组件本身,涉及 graphloop 自己代码里的 `hermes_watchdog.py`/`phase10_controller.py` 时沿用 graphloop 原始命名,不额外改名。 - -## 0. 结论先说 - -问题不是"自建 vs 用 Hermes CLI 当 Deploy Supervisor 的大脑"这个二选一——graphloop 自己的实现已经给出了答案,而且答案跟两个选项字面意思都不完全一样: - -- graphloop 的 supervisor 层(`hermes_watchdog.py` + `phase10_controller.py`)**自身的监督/裁决逻辑是纯 stdlib 确定性 Python,完全不调用任何 LLM**(Hermes 或 Claude 都不调)。LLM 只用来执行"被监督的那份工作",监督决策(存活判断、是否 blocked、evidence 是否齐全)是 if/else,不是 prompt。 -- 但 graphloop 的监督哲学是 **"fail-closed 后停下等人工"**,不是用户要的 **"自动回滚后继续跑"**。这是个真实的哲学缺口,两个候选方案都没有现成覆盖。 -- graphloop 已经有一套经过真实运行验证(含真实失败案例)的隔离/manifest/选择性写回机制,直接复用价值很高,且这部分代码同样是纯 stdlib、和"用不用 Hermes CLI"这个问题正交。 - -所以推荐路径不是"选 A 还是选 B",而是"复用 C(graphloop 的隔离/manifest/apply 库)+ 在其之上新写一个自动回滚层",详见第 4 节。 - -## 1. graphloop 里跟 supervisor 直接相关的既有实现 - -### 1.1 `hermes_watchdog.py`(109 行,已读全文) - -纯 stdlib,无 LLM 调用。核心逻辑: - -``` -inspect(state_dir, stall_seconds=600.0, action="report"|"terminate"): - 扫描 state_dir 下 status=="running" 的 *.json 状态文件 - 用 last_output_at/updated_at 或日志 mtime 算 silent_for - if stalled and action=="terminate": - os.kill(pid, SIGTERM) - 原子写 {"status": "blocked", "blocked_reason": "worker dead or silent beyond watchdog threshold"} -``` - -模块 docstring 原话:"The watchdog never restarts a task. A dead or silent worker is terminated once, marked blocked, and left for an explicit recovery decision." —— **这是关键**:graphloop 的哲学是"发现异常 → 杀掉 → 标记 blocked → 停下等人",不是"发现异常 → 自动回滚 → 继续跑"。 - -### 1.2 `phase10_controller.py`(自我演进调度器,已读核心结构) - -同样纯 stdlib,无 LLM 参与调度决策本身: - -``` -main(): - acquire_tick_lock() # 单飞锁,防止并发 tick - 读 progress.json 找 current_task - if 上一个任务的 worker PID 不存活: mark_stale() → blocked_stale,停 - if task_id 在 blocked_steps 或历史记录里是 failed/blocked: blocked_task,停 - completion_is_admissible(): 校验 evidence dict 是否包含必需 key(specialized_test/py_compile/full_regression), - blockers 字段非空则直接判不可采信 - 只有 evidence 完整且无 blocker 才推进 phase,否则原地 blocked,绝不自动重试/自动回滚 -``` - -调度器本身只负责"要不要放行这一次 tick",真正干活的是它 shell 出去的 `hermes chat --yolo ...`。 - -### 1.3 隔离 / manifest / 选择性写回(`CLAUDE.md` 架构章节 + 已读代码路径) - -这是最值得直接复用的部分,和"用不用 Hermes CLI"完全无关,纯 stdlib: - -- `isolated_coding_workspace.isolate_workspace(source_dir)`:`tempfile.mkdtemp` 复制,源目录永不直接改动 -- `workspace_security.validate_artifacts()`:拒绝绝对路径、`..`、逐段检查 symlink(防 TOCTOU 逃逸)、最终 `realpath`+`commonpath` 校验 -- `workspace_security.validate_subprocess_command()`:只接受 `[python可执行文件, 已存在脚本, ...]`,拒绝 shell 元字符(`shell=False` 之外的纵深防御) -- `change_manifest.py`:源 vs 隔离副本的 SHA-256 diff,生成 changed_files manifest -- `selective_apply.apply_manifest()`:**唯一**写回真实源码的入口,校验每个 approved path、双向拒绝 symlink 逃逸、写入前先读全部字节(stage-before-commit)、`tempfile.mkstemp`+`os.replace` 原子写 -- `run_graph_task.py` 的 `SafetyArgumentParser`:`--apply` 必须搭配显式 `--approve-path`/`--approve-all`,`--approve-all` 不带 `--apply` 直接拒绝——这是人工审批卡点,不是自动的 - -这套机制直接对应 graphlemon `self-development-feasibility.md` 缺口 1(worktree 隔离强制)的解法,而且是**已经在真实失败场景里验证过边界**的实现(见下一节)。 - -## 2. 真实失败证据(`tasks/phase10-progress.json`)及其含义 - -graphloop 自己跑 Phase 1-10 自我演进时卡在 Phase 2(`phase10_ready: false`),留下三个具体故障,每个都直接对应 graphlemon 架构里的一个假设: - -| 故障 | 现象 | 对 graphlemon 的启示 | -|---|---|---| -| `plan-schema-001` 超时 | Hermes worker 300s 未完成,判 failed | 03-C 里 `max_step_timeout`/`c_work_node_run` 超时参数不能拍脑袋定小了;graphloop 用的是 300s 默认值,实测不够 | -| `step-fanout-001` **隔离被击穿** | Hermes worker 把实现文件直接写进了**源工作区**而不是隔离工作区;隔离 manifest 里没有任何变更,controller 判定"证据不可采信",清理了源目录里两个未授权文件,恢复快照 | **最关键的一条**:即使明确要求"在隔离工作区里改",执行 agent(无论 Claude 还是 Hermes)仍可能不遵守,隔离必须由 supervisor/controller 层做外部校验("我检查你改了哪些文件、改在哪"),不能只靠 prompt 里一句"请在隔离环境里改"。这直接印证了 `self-development-feasibility.md` 缺口 1 的判断,而且不是理论推测,是实测复现 | -| worker 状态陈旧(PID 不存活但状态仍是 running) | `active_run.status="stale"`,controller 判定 `stale_reason="recorded worker PID is not alive"` | 印证了需要独立于被监督进程之外的存活探测(`os.kill(pid,0)` 这类外部判活),不能依赖被监督进程自己上报"我还活着" | - -## 3. 关键缺口:graphloop 的模型是"停下",graphlemon 要的是"回滚后继续" - -用户在缺口 3 的更正原话:"改坏了的 validator 没有人工评审倒不是问题,坏了由 supervisor 能够监督回滚即可"——这要求 Deploy Supervisor 具备**检测到问题后自动执行回滚动作、并让系统恢复到可继续工作的状态**的能力。 - -graphloop 现有两处监督实现(`hermes_watchdog.py`、`phase10_controller.py`)都止步于"标记 blocked,等人工",没有"自动回滚到上一个已知良好版本并恢复运行"这一步。这不是实现细节缺失,是设计哲学的差异: - -- graphloop:安全边界 = 停下(fail-closed to a halt) -- graphlemon 要求:安全边界 = 回滚到已知良好状态后继续(fail-closed to a rollback, then resume) - -后者严格来说更难——"回滚"在 graphloop 的场景里相对简单(还没 apply,源码没动),但在 graphlemon 的蓝绿实例场景里,"回滚"意味着:停正在跑坏的新实例 → 把主分支 revert 或切回上一个 tag → 重新拉起旧版本实例 → 确认健康 → 恢复调度。这一段目前两个项目都没有代码。 - -## 4. 方案对比 - -| 维度 | 方案 A:全新自建 Deploy Supervisor | 方案 B:Hermes CLI 直接当"大脑" | 方案 C(推荐,已采纳):复用 graphloop 隔离/manifest/apply 库 + 新写确定性回滚层 | -|---|---|---|---| -| 隔离机制 | 从零写,风险是重新踩 graphloop 已经踩过的坑(symlink 逃逸、TOCTOU) | 同左,Hermes CLI 本身不提供这层,还是要自己写 | 直接复用 `isolated_coding_workspace.py`/`workspace_security.py`/`change_manifest.py`/`selective_apply.py`,已验证、纯 stdlib、零外部依赖 | -| 存活/异常检测 | 从零写 | 同左 | 复用 `hermes_watchdog.py` 的判活逻辑(`os.kill(pid,0)` + staleness),改 action 分支 | -| 决策逻辑是否含 LLM | 看怎么设计 | 是(如果真把 Hermes CLI 当决策者,等于让一个不确定性的 LLM 做安全关键判断) | 否——沿用 graphloop 的判断:安全关键的"要不要回滚"必须是确定性代码,不能是 LLM 判断 | -| 自动回滚能力 | 需要新写(工作量:中,蓝绿切换脚本 + 健康检查 + revert 逻辑) | 需要新写,且如果决策链路里有 LLM 调用,回滚这种要求"确定性、可审计、不能出错"的动作反而更不适合让 LLM 参与 | 需要新写(同方案 A 工作量),但隔离/apply 部分零重复造轮子,净新增工作量最小 | -| 与 Graph Engine 的独立性 | 完全独立,符合用户要求"第三方 agent" | 完全独立(Hermes CLI 本来就在另一套工具链) | 完全独立;graphloop 的 watchdog/controller 本来就是独立进程,架构上现成符合"third-party supervisor"要求 | -| 已验证程度 | 零(全新代码) | 部分(graphloop 已用 Hermes CLI 跑了 Phase 1,但 Phase 2 卡住暴露隔离被击穿的问题——说明"用 Hermes CLI 执行任务"这条路径本身有已知未解决的可靠性问题) | 隔离/apply 部分已验证(含真实失败复现);回滚部分未验证(新代码,但范围小) | -| 与既有决策的一致性 | 中性 | 有冲突:graphlemon 已明确决定 MVP 只用 Claude Code ACP 做 Org Agent 执行后端,不引入 Hermes CLI 执行任务;若 supervisor 内部又跑 Hermes CLI 去执行"诊断/回滚"这类任务,等于变相引入了第二个执行后端,需要重新论证 | 一致:只复用 graphloop 里**不调用任何 LLM 的确定性 Python 模块**(隔离/manifest/apply/watchdog 判活逻辑),不引入 Hermes CLI 作为执行后端,符合"只用 Claude Code ACP"的既有决策边界 | - -## 5. 推荐 - -**方案 C**:把 graphloop 的 `isolated_coding_workspace.py` / `workspace_security.py` / `change_manifest.py` / `selective_apply.py` 四个模块(纯 stdlib,无 Hermes/Claude 依赖)作为库直接搬到 graphlemon 里复用或轻改,解决 `self-development-feasibility.md` 缺口 1;`hermes_watchdog.py` 的判活/staleness 检测逻辑作为参考实现复用,但把它的终态从 `blocked`(停下等人)改造成新的 `blocked → 尝试自动回滚 → 回滚成功则恢复 running → 回滚失败才真正 blocked_pending_human` 状态机,解决缺口 2(蓝绿实例切换)并落实用户对缺口 3 的更正(自动回滚代替强制人工评审)。 - -不建议把 Hermes CLI 作为 supervisor 决策链路的一部分:graphloop 自己的实践已经证明——(a) 安全关键判断在其自身架构里也是纯确定性代码,从不假手 LLM;(b) 用 Hermes CLI 执行被监督的任务这条路径本身有实测的可靠性问题(隔离被击穿);(c) 引入 Hermes CLI 会和"MVP 只用 Claude Code ACP"这条已确认决策产生冲突,需要重新开一次论证。 - -## 6. 决策记录(2026-07-30,用户已逐条确认) - -1. **同意方案 C 方向**:"同意方案 c 方向,只在这一个环节复用,问题不大"——只复用隔离/apply 库这一部分,不引入 Hermes CLI 到决策链路。 -2. **复制而非依赖**:"复制一份到 graphlemon 仓库改造,那边已经不维护了"——四个模块已复制到 `supervisor/isolation/` 并按 graphlemon 自身约定改造(详见该目录 `__init__.py` 的来源说明),不对 graphloop 仓库产生 import 依赖。 -3. **自动回滚状态机现在就设计**:"自动回滚机制现在就设计一下,省得到 ph5 阶段有那个阶段的问题"——已完成,见 `supervisor-auto-rollback-design.md`,并已回填正式 SDD。 diff --git a/docs/todo/dynamic-work-graph-design-skill-requirements.md b/docs/todo/dynamic-work-graph-design-skill-requirements.md new file mode 100644 index 0000000..a64ce76 --- /dev/null +++ b/docs/todo/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/todo/graph-engineering-open-items.md b/docs/todo/graph-engineering-open-items.md index 0ea9d3f..65bb6ed 100644 --- a/docs/todo/graph-engineering-open-items.md +++ b/docs/todo/graph-engineering-open-items.md @@ -70,7 +70,7 @@ **确认结果(2026-08-05)**:立项。 -**待办**:✅ 需求文档已产出 → `docs/work-graph-time-travel-debugging-requirements.md`。已明确跟 Deploy Supervisor 自动回滚(`docs/supervisor-auto-rollback-design.md`)的命名消歧。最大的数据模型影响点:`work_graph_run` 表现有的 `ux_work_graph_run_active` 唯一约束(同一 `task_id`+`org_node` 同时只能有一个非终态 run)会挡住分叉分支,需要在设计阶段决定怎么扩展。 +**待办**:✅ 需求文档已产出 → `docs/work-graph-time-travel-debugging-requirements.md`。已明确跟 Deploy Supervisor 自动回滚(`docs/done/supervisor-auto-rollback-design.md`)的命名消歧。最大的数据模型影响点:`work_graph_run` 表现有的 `ux_work_graph_run_active` 唯一约束(同一 `task_id`+`org_node` 同时只能有一个非终态 run)会挡住分叉分支,需要在设计阶段决定怎么扩展。 --- @@ -114,7 +114,7 @@ **确认结果(2026-08-05)**:确认要做,用户强调"这个很核心"。触发规则明确为:**只要 Work Graph 规划了 fan-out/fan-in 的并行节点,节点触发时就应该并行执行**——这跟需求文档 FR-1/FR-2 已经写的内容一致,不需要改需求文档的功能需求本身,只需要在文档里把这句话作为明确的验收口径固化下来。 -**待办**:进入详细设计阶段(`topological_layers()` 的具体实现、线程池调度器接口、锁的粒度),需求文档里第 8 节列的开放问题需要在设计阶段逐一敲定。这是 E、F 两条的前置依赖,建议优先推进。 +**待办**:✅ 详细设计已产出 → `docs/work-graph-parallel-execution-design.md`。核心设计:新增 `topological_layers()`(层内并发、层间屏障),`_LockedConn` 线程安全代理复用 `backend_server.py` 的锁模式,`create_worktree()` 加防御性锁,`WorkGraphLayerError` 聚合层内多个失败。已知遗留限制(诚实记录,非本轮解决):单节点配额等待可长达数小时,会拖住整层其他已完成节点的返回——需要更细粒度的可中断调度才能解决,超出本次范围。下一步:实施(见设计文档第 11 节的分步实施建议),或先评审设计。 --- diff --git a/docs/todo/validator-loop-until-dry-skill-requirements.md b/docs/todo/validator-loop-until-dry-skill-requirements.md new file mode 100644 index 0000000..fa295d1 --- /dev/null +++ b/docs/todo/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/todo/verifier-fan-out-jury-requirements.md b/docs/todo/verifier-fan-out-jury-requirements.md new file mode 100644 index 0000000..39eba7a --- /dev/null +++ b/docs/todo/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/todo/work-graph-parallel-execution-design.md b/docs/todo/work-graph-parallel-execution-design.md new file mode 100644 index 0000000..ed6fe2e --- /dev/null +++ b/docs/todo/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/todo/work-graph-parallel-execution-requirements.md b/docs/todo/work-graph-parallel-execution-requirements.md new file mode 100644 index 0000000..6f70eba --- /dev/null +++ b/docs/todo/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/todo/work-graph-time-travel-debugging-requirements.md b/docs/todo/work-graph-time-travel-debugging-requirements.md new file mode 100644 index 0000000..bf7a295 --- /dev/null +++ b/docs/todo/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/validator-loop-until-dry-skill-requirements.md b/docs/validator-loop-until-dry-skill-requirements.md deleted file mode 100644 index fa295d1..0000000 --- a/docs/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/verifier-fan-out-jury-requirements.md b/docs/verifier-fan-out-jury-requirements.md deleted file mode 100644 index 39eba7a..0000000 --- a/docs/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/work-graph-parallel-execution-requirements.md b/docs/work-graph-parallel-execution-requirements.md deleted file mode 100644 index be99657..0000000 --- a/docs/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/todo/graph-engineering-open-items.md` 条目 G。 - -## 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/work-graph-time-travel-debugging-requirements.md b/docs/work-graph-time-travel-debugging-requirements.md deleted file mode 100644 index ffabb5a..0000000 --- a/docs/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/supervisor-auto-rollback-design.md` 描述的 Deploy Supervisor 自动回滚是两件完全不同的事——不要混淆: - -| | Time-Travel 调试(本文档) | Deploy Supervisor 自动回滚 | -|---|---|---| -| 回滚的对象 | 某一次任务的 Work Graph 执行过程(某个 `work_node_execution` 的中间状态) | graphlemon 自身的代码版本(git commit) | -| 触发者 | 人工调试意图("我想看看如果这一步换个输入会怎样") | 自动健康检查失败信号 | -| 影响范围 | 单个 task 的执行历史,不影响其他任务 | 整个 graphlemon 进程的运行版本 | -| 相关文档 | 本文档 | `docs/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/src/schema.py b/src/schema.py index fae17d6..1ca4610 100644 --- a/src/schema.py +++ b/src/schema.py @@ -276,7 +276,7 @@ CREATE TABLE log_retention_config ( updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ); --- 1.15 deployment (PH5, U-034, 2026-08-02 -- see docs/supervisor-auto-rollback-design.md) +-- 1.15 deployment (PH5, U-034, 2026-08-02 -- see docs/done/supervisor-auto-rollback-design.md) CREATE TABLE deployment ( deployment_id TEXT PRIMARY KEY, git_commit_hash TEXT NOT NULL, diff --git a/supervisor/deploy_supervisor.py b/supervisor/deploy_supervisor.py index b33e0cd..d26fb73 100644 --- a/supervisor/deploy_supervisor.py +++ b/supervisor/deploy_supervisor.py @@ -1,7 +1,7 @@ """Deploy Supervisor process skeleton (U-036). Independent OS process (started via `python3 -m supervisor.deploy_supervisor`), -pure stdlib, no LLM CLI dependency -- per docs/supervisor-auto-rollback-design.md +pure stdlib, no LLM CLI dependency -- per docs/done/supervisor-auto-rollback-design.md §2's "three independent processes, no shared failure domain" principle: Deploy Supervisor crashing must not affect Live/Candidate, and Live/Candidate crashing must not affect Deploy Supervisor's own ability to keep observing diff --git a/supervisor/isolation/__init__.py b/supervisor/isolation/__init__.py index 285a380..188c425 100644 --- a/supervisor/isolation/__init__.py +++ b/supervisor/isolation/__init__.py @@ -3,7 +3,7 @@ Deploy Supervisor (the independent process that handles blue-green deploy + automatic rollback for graphlemon's own self-modification -- not to be confused with the "Supervisor(调度子 Agent)" role documented in graphlemon_architecture.md, which is an in-process monitoring node inside -the Coder Work Graph. See docs/supervisor-auto-rollback-design.md for the +the Coder Work Graph. See docs/done/supervisor-auto-rollback-design.md for the disambiguation. Adapted 2026-07-30 from graphloop (`/Users/yibo/Documents/lemonAI/graphloop`, @@ -16,8 +16,8 @@ source workspace instead of the isolated copy; `change_manifest`'s symlink-rejecting isolated-tree snapshot plus the caller's manifest check caught it before anything false was reported as evidence). -See docs/supervisor-design-comparison.md and -docs/supervisor-auto-rollback-design.md for why this code lives here and how +See docs/done/supervisor-design-comparison.md and +docs/done/supervisor-auto-rollback-design.md for why this code lives here and how it plugs into the blue-green deploy/rollback state machine. Module map (unchanged from graphloop): -- libgit2 0.21.4