Commit fc94ae8bb0b2e8a4ec175aab628db13e4ecd7efc
0 parents
Initial baseline: SDD v1.7.0, development plan, staged utilities
Establishes the first commit for graphlemon before U-001 work begins: architecture/SDD docs (including the PH5 self-hosting design already folded into 01-M/03-C/04-P/mvcp-contract v1.7.0), the A-1 spike results, the graphloop-adapted supervisor/isolation utilities, PH3 wireframe prototypes (now cross-referenced from 02-V), and this session's development-plan review (U-024 dependency split, U-035 regression requirement, new U-004.5 e2e-harness unit, src/ directory convention). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Showing
42 changed files
with
6464 additions
and
0 deletions
.gitignore
0 → 100644
CLAUDE.md
0 → 100644
| 1 | +++ a/CLAUDE.md | ||
| 1 | +# CLAUDE.md | ||
| 2 | + | ||
| 3 | +This file orients Claude Code when working in this repository. Read this first, then follow the | ||
| 4 | +pointers below — do not start writing code before reading `docs/development-plan.md` and the | ||
| 5 | +current Unit's coverage references in `docs/sdd/`. | ||
| 6 | + | ||
| 7 | +## What this is | ||
| 8 | + | ||
| 9 | +**graphlemon** is a Level 5 autonomous "lights-out software factory": a two-layer Organizer Agent | ||
| 10 | +architecture (Org Graph = fixed Planner→Coder→Validator; Work Graph = dynamic per-task execution | ||
| 11 | +graph) that drives real software development end-to-end, backed entirely by Claude Code ACP | ||
| 12 | +(`claude -p` non-interactive calls) — there is no Hermes CLI execution backend in this project | ||
| 13 | +(decision, 2026-07-29, see `docs/sdd/05-实施约束与依赖.md` §5.3). | ||
| 14 | + | ||
| 15 | +The authoritative spec is `docs/sdd/` (00-06 + `docs/sdd/sdd-mvcp-contract.json`), generated via | ||
| 16 | +the MVCP (Model/View/Controller/Process) methodology from `docs/graphlemon_architecture.md`. | ||
| 17 | +**Do not treat `graphlemon_architecture.md` as the spec to implement against** — it's the PRD- | ||
| 18 | +equivalent input that produced the SDD. When they conflict, the SDD wins (it's newer and more | ||
| 19 | +detailed); if a conflict looks unresolved, stop and ask rather than picking one silently. | ||
| 20 | + | ||
| 21 | +## Where to start | ||
| 22 | + | ||
| 23 | +1. Read `docs/development-plan.md` — Phase → Sub-phase → Unit breakdown (`docs/sdd/unit_matrix.json` | ||
| 24 | + is the machine-readable mirror). Work is organized as Development Units, each classified | ||
| 25 | + Value Path / Risk Gate / Enabler, each with explicit `depends_on` and a `done` criterion that | ||
| 26 | + references a specific REQ in `docs/sdd/06-验收与测试锚点.md`. | ||
| 27 | +2. The critical path starts at **U-001** (01-M schema) → U-002 → U-004 → ... Follow | ||
| 28 | + `development-plan.md` §3 "关键路径" — do not skip ahead to later Units even if they look more | ||
| 29 | + interesting; most have real `depends_on` edges. | ||
| 30 | +3. **U-004 (A-2 assumption) is the next unvalidated risk on the critical path.** A-1 (dynamic | ||
| 31 | + workflow programmatic invocation) was already spike-validated (`spikes/a1-dynamic-workflows/`, | ||
| 32 | + confirmed 2026-07-29). A-2 (Hermes profile SOUL.md/AGENTS.md migrating cleanly into | ||
| 33 | + `.claude/agents/*.md` without major rewrites) is not yet tested. Validate it early with a | ||
| 34 | + minimal spike (per `development-plan.md`'s explicit recommendation) rather than discovering | ||
| 35 | + problems after U-010/U-011 are fully built on top of it. | ||
| 36 | + | ||
| 37 | +## Commands | ||
| 38 | + | ||
| 39 | +No build, lint, or test tooling exists yet — the repo is pre-scaffolding (PH0, no commits made | ||
| 40 | +yet). `supervisor/isolation/` has no test suite of its own (that's U-036); `docs/` and `spikes/` | ||
| 41 | +aren't packages. The one real executable entry point today: | ||
| 42 | + | ||
| 43 | +```bash | ||
| 44 | +# A-1 spike: verify Claude Code dynamic workflows can be invoked headlessly (already validated, | ||
| 45 | +# kept here for re-running / reference — see spikes/a1-dynamic-workflows/RESULTS.md) | ||
| 46 | +./spikes/a1-dynamic-workflows/run-spike.sh /path/to/some/git/repo | ||
| 47 | +``` | ||
| 48 | + | ||
| 49 | +Once U-002/U-003 scaffold `src/`, tests run via stdlib `unittest` only (see Testing below) — there | ||
| 50 | +is no repo convention yet for a single-test invocation beyond the standard | ||
| 51 | +`python3 -m unittest path.to.test_module.TestClass.test_method`; don't invent a different pattern. | ||
| 52 | + | ||
| 53 | +## Hard constraints (do not relitigate these — they're settled decisions) | ||
| 54 | + | ||
| 55 | +- **Execution backend**: Claude Code ACP only (`claude -p ... --output-format json --json-schema | ||
| 56 | + <schema> --permission-mode auto`, no `--bare`, no `--dangerously-skip-permissions`). See | ||
| 57 | + 05 §5.3, §5.3.1, §5.4. | ||
| 58 | +- **Implementation language**: graphlemon's own backend/frontend/Deploy Supervisor are **pure | ||
| 59 | + stdlib Python** — no Flask/FastAPI/Express/Node.js, no npm build step. This mirrors the | ||
| 60 | + predecessor project graphloop's convention. See 05 §5.7. (This constraint is on graphlemon's | ||
| 61 | + own implementation only — it does NOT apply to the code graphlemon generates for target | ||
| 62 | + projects, which follows whatever stack that target project uses.) | ||
| 63 | +- **Testing**: stdlib `unittest` only, no pytest/fixtures. Mock the `subprocess`/`claude -p` | ||
| 64 | + boundary in normal tests; real end-to-end tests that actually shell out to `claude` should be | ||
| 65 | + gated behind an explicit env var and skipped by default (mirror graphloop's | ||
| 66 | + `RUN_REAL_AGENT_E2E=1` pattern) — this project doesn't have that harness yet, build it as part | ||
| 67 | + of the relevant Unit rather than improvising a different pattern per test file. | ||
| 68 | +- **Isolation**: parallel coding nodes must use `git_worktree_ref`. If `target_folder_path` is | ||
| 69 | + graphlemon's own repo (self-hosting/self-modification, PH5 only), worktree is **unconditionally | ||
| 70 | + mandatory** even for serial single-node changes — see 05 §5.8. | ||
| 71 | + | ||
| 72 | +## Two things named "Supervisor" — do not confuse them | ||
| 73 | + | ||
| 74 | +1. **"Supervisor(调度子 Agent)"** — an in-process monitoring node inside the Coder Work Graph | ||
| 75 | + (architecture doc, existing concept). Watches Work Node execution, triggers reflection/ | ||
| 76 | + rescheduling. Part of the Graph Engine itself. | ||
| 77 | +2. **"Deploy Supervisor"** — an independent OS process, entirely outside the Graph Engine, that | ||
| 78 | + handles blue-green deployment and automatic rollback when graphlemon modifies its own code | ||
| 79 | + (PH5 only). See `docs/supervisor-auto-rollback-design.md` for the full design and the | ||
| 80 | + disambiguation note at the top of that file. | ||
| 81 | + | ||
| 82 | +These are unrelated components that happen to share a root word in casual English. Identifiers | ||
| 83 | +for #2 are always prefixed `c_deploy_supervisor_*` / `deploy_health_signal` — never abbreviate to | ||
| 84 | +just "supervisor" in code or docs. | ||
| 85 | + | ||
| 86 | +## PH5 (Deploy Supervisor / self-hosting) is not in scope yet | ||
| 87 | + | ||
| 88 | +`docs/development-plan.md` PH5 (Units U-034~U-043) depends on PH4 closing first — self-modification | ||
| 89 | +safety only makes sense once the base system has passed full regression. Don't build any of | ||
| 90 | +U-034~U-043 before PH0-PH4 are done, even though the design (`docs/supervisor-auto-rollback-design.md`) | ||
| 91 | +and its four reusable isolation modules (`supervisor/isolation/`) are already staged. Those modules | ||
| 92 | +were adapted from graphloop and have had exactly one manual smoke test — they still need a real | ||
| 93 | +`unittest` suite (U-036) before anyone should trust them in a real rollback path. | ||
| 94 | + | ||
| 95 | +## Prior art worth reading, not worth copying wholesale | ||
| 96 | + | ||
| 97 | +`/Users/yibo/Documents/lemonAI/graphloop` is an earlier, unmaintained attempt at this same | ||
| 98 | +architecture — substantially implemented (two-layer graph, isolated workspace, change manifest, | ||
| 99 | +selective apply, fan-in/fan-out, real Claude-based Planner/Worker/Reviewer agents) but its own | ||
| 100 | +Phase 10 self-evolution attempt stalled with concrete documented failures (a worker writing files | ||
| 101 | +outside its isolated workspace, a 300s timeout, a stale-PID recovery incident). Its `CLAUDE.md` is | ||
| 102 | +worth reading for architecture patterns; its `hermes_watchdog.py`/`phase10_controller.py` are | ||
| 103 | +worth reading for the "fail-closed, deterministic, never-auto-restart" supervision philosophy | ||
| 104 | +(graphlemon's Deploy Supervisor deliberately extends this into auto-rollback rather than just | ||
| 105 | +auto-halt — see `docs/supervisor-design-comparison.md` for why). Do not import graphloop code | ||
| 106 | +directly; anything worth reusing has already been copied and adapted into this repo | ||
| 107 | +(`supervisor/isolation/`). | ||
| 108 | + | ||
| 109 | +## Repo layout (as of PH0, before scaffolding exists) | ||
| 110 | + | ||
| 111 | +- `docs/` — architecture, SDD, development plan (see above) | ||
| 112 | +- `docs/wireframes/` — static no-build HTML prototypes for the PH3 console views (`v_*.html`), referenced | ||
| 113 | + per-page from `docs/sdd/02-V-界面与交互.md` §2.1-2.13 — implementation starting point once PH3 begins, | ||
| 114 | + DOM/class names map 1:1 to that doc's field lists. | ||
| 115 | +- `supervisor/isolation/` — staged utility library (isolate/diff/apply), not yet wired to anything | ||
| 116 | +- `spikes/` — one-off validation spikes (currently just A-1) | ||
| 117 | +- `src/` (created by U-001) — graphlemon's own backend source (schema, then U-007+ HTTP/WebSocket server). | ||
| 118 | + **Not the same `src/` U-002 generates inside a target project's own repo** — those are two different | ||
| 119 | + physical repos that can coexist on disk during PH5 self-target. See 05-实施约束与依赖.md §5.7. | ||
| 120 | +- Everything else (`.claude/agents/`, `skills/`, `runs/`, per-target-project scaffolding) gets created by | ||
| 121 | + U-002/U-003 — it doesn't exist yet, and that's expected at this stage. |
docs/Graph Engineering with Claude_ 14-Step roadmap from 0 to graph architect (Full Course).md
0 → 100644
| 1 | +++ a/docs/Graph Engineering with Claude_ 14-Step roadmap from 0 to graph architect (Full Course).md | ||
| 1 | +# Graph Engineering with Claude: 14\-Step roadmap from 0 to graph architect \(Full Course\) | ||
| 2 | + | ||
| 3 | + | ||
| 4 | + | ||
| 5 | +img\_placeholder | ||
| 6 | + | ||
| 7 | + | ||
| 8 | + | ||
| 9 | +大多数人尝试构建多步智能体时,最终得到的都是一条直线。第一步、第二步、第三步——每一步都乖乖地等上一步做完才开始。 | ||
| 10 | + | ||
| 11 | +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\. | ||
| 12 | + | ||
| 13 | + | ||
| 14 | + | ||
| 15 | +十分之九的人会发现,其中一半的步骤根本不需要等待。 | ||
| 16 | + | ||
| 17 | +**9/10 notice that half those steps never needed to wait at all\.** | ||
| 18 | + | ||
| 19 | + | ||
| 20 | + | ||
| 21 | +它们不路由,不分支,不并行。它们只是排队——一个脑袋,一个上下文,一次只做一件事——直到窗口被填满,智能体忘记自己在做什么。 | ||
| 22 | + | ||
| 23 | +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\. | ||
| 24 | + | ||
| 25 | + | ||
| 26 | + | ||
| 27 | +> Follow my Substack to get fresh AI alpha: | ||
| 28 | +> | ||
| 29 | +> [movez\.substack\.com](https://movez.substack.com/) | ||
| 30 | +> | ||
| 31 | +> | ||
| 32 | +> | ||
| 33 | +> | ||
| 34 | + | ||
| 35 | +这套十四步执行路线图,能把单文件单行式简单任务拆解成完整任务图谱:该图谱可分发至海量智能体集群并行运算、自主校验全部检索结果,最终收敛输出一套单智能体完全无法承载处理规模的完整结论。 | ||
| 36 | + | ||
| 37 | +***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\. | ||
| 38 | + | ||
| 39 | + | ||
| 40 | + | ||
| 41 | + | ||
| 42 | + | ||
| 43 | +这里有一个没人明说的转变:prompt提示词是一句话,loop循环是一个周期,harness框架是智能体脚下的地板。 | ||
| 44 | + | ||
| 45 | +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\. | ||
| 46 | + | ||
| 47 | + | ||
| 48 | + | ||
| 49 | +但工作本身的形状——什么在什么之前运行、什么可以同时运行、什么必须等待其他所有东西——这个形状就是一个图。节点负责思考,边负责传递结果。 | ||
| 50 | + | ||
| 51 | +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\. | ||
| 52 | + | ||
| 53 | + | ||
| 54 | + | ||
| 55 | +Claude Code 已经内置了直接构建这些图的工具:动态工作流。 | ||
| 56 | + | ||
| 57 | +Claude Code shipped the tooling to build these graphs directly: **dynamic workflows**\. | ||
| 58 | + | ||
| 59 | + | ||
| 60 | + | ||
| 61 | +Claude 编写一个普通的 JavaScript 编排脚本,然后生成一个协调的子智能体舰队来执行它——而协调本身不消耗任何模型 token,因为它是代码,不是对话。 | ||
| 62 | + | ||
| 63 | +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\. | ||
| 64 | + | ||
| 65 | + | ||
| 66 | + | ||
| 67 | + | ||
| 68 | + | ||
| 69 | +## Nodes are jobs\. Edges are what *flows\.* | ||
| 70 | + | ||
| 71 | +\#\# 节点代表工作,边代表流转关系 | ||
| 72 | + | ||
| 73 | + | ||
| 74 | + | ||
| 75 | +一个图只有两样东西(节点\+边 Nodes\+Edges),把它们搞清楚就能解决大部分困惑。 | ||
| 76 | + | ||
| 77 | +**节点**是一个工作单元——一个智能体、一个有界任务、一个输入一个输出。 | ||
| 78 | + | ||
| 79 | +A graph has exactly two things, and getting them straight fixes most of the confusion\. | ||
| 80 | + | ||
| 81 | +A **node** is a unit of work \- one agent, one bounded job, one input in and one output out\. | ||
| 82 | + | ||
| 83 | + | ||
| 84 | + | ||
| 85 | +**边**是一个依赖关系:含义是这个节点的输出作为另一个节点的输入。仅此而已。 | ||
| 86 | + | ||
| 87 | +An **edge** is a dependency: it says *this node’s output feeds that node’s input\.* Nothing more\. | ||
| 88 | + | ||
| 89 | + | ||
| 90 | + | ||
| 91 | +常见的错误是把「然后」当作边。「总结文件然后告诉我天气」——这两者之间没有边,天气不消费摘要的数据。 | ||
| 92 | + | ||
| 93 | +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\. | ||
| 94 | + | ||
| 95 | + | ||
| 96 | + | ||
| 97 | +那是两个不相连的节点,被线性脚本不必要地串联在一起。边只存在于数据真正跨越它的时候。 | ||
| 98 | + | ||
| 99 | +That’s two disconnected nodes that a linear script needlessly chains\. The edge only exists when data actually moves across it\. | ||
| 100 | + | ||
| 101 | + | ||
| 102 | + | ||
| 103 | +学会对你的智能体中每个「然后」提问:下一步是否读取上一步的输出?如果不是,就没有边,等待是浪费的。 | ||
| 104 | + | ||
| 105 | +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\. | ||
| 106 | + | ||
| 107 | + | ||
| 108 | + | ||
| 109 | +python | ||
| 110 | + | ||
| 111 | +1\. 用方框和箭头画出来。方框是一个 agent\(\) 调用,箭头是一个变量从一个调用的返回值传到另一个的提示词里。如果你画不出箭头——如果没有变量跨越——两个方框就是独立的,而独立性是你在本课程剩余部分要利用的东西。 | ||
| 112 | + | ||
| 113 | +```Python | ||
| 114 | +Draw it as boxes and arrows. A box is an agent() call. | ||
| 115 | +An arrow is a variable passed from one call’s return into another’s | ||
| 116 | +prompt. If you can’t draw the arrow - if no variable crosses - the two | ||
| 117 | +boxes are independent, and independence is the thing you’ll exploit | ||
| 118 | +for the rest of this course. | ||
| 119 | +``` | ||
| 120 | + | ||
| 121 | + | ||
| 122 | + | ||
| 123 | + | ||
| 124 | + | ||
| 125 | +## Your linear script is a *degenerate* graph | ||
| 126 | + | ||
| 127 | +\#\# 你的线性脚本是一个退化的图 | ||
| 128 | + | ||
| 129 | + | ||
| 130 | + | ||
| 131 | +当你把智能体写成「先做A,再做B,再做C,再做D」时,你已经画了一个图——一条不分叉的单链。每个节点恰好有一条边进、一条边出。 | ||
| 132 | + | ||
| 133 | +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\. | ||
| 134 | + | ||
| 135 | + | ||
| 136 | + | ||
| 137 | +它能正确运行,但也运行得又慢又脆,因为链条没有冗余:如果C卡住了,D永远不会执行,A的工作被困在上游无处可去。 | ||
| 138 | + | ||
| 139 | +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\. | ||
| 140 | + | ||
| 141 | + | ||
| 142 | + | ||
| 143 | + | ||
| 144 | + | ||
| 145 | +图工程的第一项真正技能是重画这条链。拿起你的线性智能体,对每条箭头问第一步的问题。 | ||
| 146 | + | ||
| 147 | +The first real skill of graph engineering is **redrawing the chain**\. Take your linear agent and, for each arrow, ask the Step 1 question\. | ||
| 148 | + | ||
| 149 | + | ||
| 150 | + | ||
| 151 | +大多数链条有两三条不携带数据的箭头——它们只是你碰巧输入东西的顺序。 | ||
| 152 | + | ||
| 153 | +Most chains have two or three arrows that don’t carry data \- they’re just the order you happened to type things in\. | ||
| 154 | + | ||
| 155 | + | ||
| 156 | + | ||
| 157 | +剪掉这些箭头,链条就坍缩成更宽的东西:几个可以同时运行的独立节点,喂给一个需要它们全部的单一节点。 | ||
| 158 | + | ||
| 159 | +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\. | ||
| 160 | + | ||
| 161 | + | ||
| 162 | + | ||
| 163 | + | ||
| 164 | + | ||
| 165 | +## Give every node a *contract* | ||
| 166 | + | ||
| 167 | +\#\# 给每个节点一个契约 | ||
| 168 | + | ||
| 169 | +一个你无法推理的节点就是一个你无法并行的节点。解决方案是契约:有界输入、有界输出、恰好一个任务。 | ||
| 170 | + | ||
| 171 | +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\.** | ||
| 172 | + | ||
| 173 | + | ||
| 174 | + | ||
| 175 | +输入是节点读取的任何东西——显式传入,永远不从共享窗口假设。输出是一个定义好的形状,最好经过验证,这样下一个节点可以无猜测地消费它。 | ||
| 176 | + | ||
| 177 | +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\. | ||
| 178 | + | ||
| 179 | + | ||
| 180 | + | ||
| 181 | +在工作流中,这个契约用 schema 强制执行。当你给 Claude 一个带 JSON schema 的 agent\(\) 调用时,Claude 生成的子智能体被迫返回验证过的结构化数据——验证发生在工具调用层,所以 Claude 在不匹配时重试,而不是给你一段需要解析和祈祷的自由文本。 | ||
| 182 | + | ||
| 183 | +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\. | ||
| 184 | + | ||
| 185 | + | ||
| 186 | + | ||
| 187 | +这就是一个 Claude 能接入图的节点和一个只有人类读输出才能工作的节点之间的区别。 | ||
| 188 | + | ||
| 189 | +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\. | ||
| 190 | + | ||
| 191 | + | ||
| 192 | + | ||
| 193 | +python | ||
| 194 | + | ||
| 195 | +// 真正有契约的节点:有界输入,验证输出,单一任务。 | ||
| 196 | + | ||
| 197 | +```Python | ||
| 198 | +// A node with a real contract: bounded in, validated out, one job. | ||
| 199 | +const ITEM = { | ||
| 200 | + type: 'object', additionalProperties: false, | ||
| 201 | + properties: { | ||
| 202 | + title: { type: 'string' }, | ||
| 203 | + url: { type: 'string' }, | ||
| 204 | + impact: { type: 'string', enum: ['high', 'medium', 'low'] }, | ||
| 205 | + }, | ||
| 206 | + required: ['title', 'url', 'impact'], | ||
| 207 | +}; | ||
| 208 | +const result = await agent(source.prompt, { | ||
| 209 | + label: `research:${source.key}`, | ||
| 210 | + schema: ITEM, // forces validated structured output | ||
| 211 | + agentType: 'general-purpose', | ||
| 212 | +}); | ||
| 213 | +// result is now a shape the next node can trust — not free text. | ||
| 214 | +``` | ||
| 215 | + | ||
| 216 | + | ||
| 217 | + | ||
| 218 | + | ||
| 219 | + | ||
| 220 | +## 04\. Treat the edge as a *data contract* | ||
| 221 | + | ||
| 222 | +\#\# 04\. 把边当作数据契约 | ||
| 223 | + | ||
| 224 | + | ||
| 225 | + | ||
| 226 | +边不只是简单表示 “任务 A 执行完再跑任务 B”,它更是一份数据传输约定:A 输出固定格式的数据结构,而 B 的设计本身就适配读取该结构。如果以传输的数据(而非执行先后顺序)来命名边,两件事会变得清晰易懂。 | ||
| 227 | + | ||
| 228 | +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\. | ||
| 229 | + | ||
| 230 | + | ||
| 231 | + | ||
| 232 | + | ||
| 233 | + | ||
| 234 | +你可以一眼看出边是否真实(数据真的在移动吗?),而且只要形状保持不变,你可以替换任一端的节点而不破坏图。 | ||
| 235 | + | ||
| 236 | +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\. | ||
| 237 | + | ||
| 238 | + | ||
| 239 | + | ||
| 240 | +实际实现中,边由原生 JavaScript 编写。在任务分发与结果汇总之间的归约步骤(扁平化、去重、过滤),本质就是一段处理各节点输出数据结构的代码。 | ||
| 241 | + | ||
| 242 | +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\. | ||
| 243 | + | ||
| 244 | + | ||
| 245 | + | ||
| 246 | +全程无需智能体参与。基于图谱架构设计有一项不易察觉的显著优势:大量原本需要消耗大模型算力令牌处理的逻辑,本质只是一条数据流边,而数据流边完全零算力开销。 | ||
| 247 | + | ||
| 248 | +**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\. | ||
| 249 | + | ||
| 250 | + | ||
| 251 | + | ||
| 252 | +python | ||
| 253 | + | ||
| 254 | +人们很容易下意识新建智能体来 “合并多份结果”,但一定要避免这种做法。 如果合并操作仅需扁平化、去重,直接用 `results.flatMap(...)` 搭配集合(Set)即可;运算结果确定、瞬时完成,完全不消耗模型 Token。 智能体应当留给需要主观判断的场景,而非单纯的数据流转处理。 倘若图谱里每条数据边都依靠智能体中转,相当于为自身的基础数据流链路持续付出不必要的算力成本。 | ||
| 255 | + | ||
| 256 | +```Python | ||
| 257 | +The temptation is to spawn an agent to “combine the results.” Resist | ||
| 258 | +it. If combining means flatten-and-dedupe, that’s results.flatMap(...) | ||
| 259 | +and a Set — deterministic, instant, zero tokens. Save agents for | ||
| 260 | +judgment, not for plumbing. A graph where every edge is an agent is a | ||
| 261 | +graph paying rent on its own wiring. | ||
| 262 | +``` | ||
| 263 | + | ||
| 264 | + | ||
| 265 | + | ||
| 266 | + | ||
| 267 | + | ||
| 268 | +## Fan out with *parallel\(\)* | ||
| 269 | + | ||
| 270 | +\#\# 用 parallel\(\) 实现任务并行分发 | ||
| 271 | + | ||
| 272 | + | ||
| 273 | + | ||
| 274 | +这是能大幅压缩整体资源开销的核心设计思路。当存在 N 个相互独立节点(例如 N 份待核查数据源、N 个待评审文件、N 条待审计路由)时,切勿采用串行链式执行方案。 | ||
| 275 | + | ||
| 276 | +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\. | ||
| 277 | + | ||
| 278 | + | ||
| 279 | + | ||
| 280 | +你告诉 Claude 把它们fan\-out(任务分解分发)并同时运行。在工作流中这就是 parallel\(\):Claude 接收一个延迟执行函数 thunk 数组,为每个 thunk 单独生成一个子智能体,全部并发执行,然后把结果数组返回给你。 | ||
| 281 | + | ||
| 282 | +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\. | ||
| 283 | + | ||
| 284 | + | ||
| 285 | + | ||
| 286 | +两个细节让它稳健。第一,parallel\(\) 是一个屏障——它等所有 thunk 完成才返回,所以下一阶段看到的是完整集合。第二,抛异常的 thunk 解析为 null 而不是拒绝整个批次,所以一个不稳定的智能体不会搞砸整个运行。 | ||
| 287 | + | ||
| 288 | +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\. | ||
| 289 | + | ||
| 290 | + | ||
| 291 | + | ||
| 292 | +总是对结果做 \.filter\(Boolean\)。并发量受限于你的核心数,多余的会排队,所以你可以传一百个 thunk 它们都会完成——只是每次跑一小批。 | ||
| 293 | + | ||
| 294 | +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\. | ||
| 295 | + | ||
| 296 | + | ||
| 297 | + | ||
| 298 | +python | ||
| 299 | + | ||
| 300 | +1\. phase\('Research'\); | ||
| 301 | +// 九个来源,九个智能体,全部同时。 | ||
| 302 | +const raw = await parallel\( | ||
| 303 | + SOURCES\.map\(\(s\) =\> \(\) =\> | ||
| 304 | + agent\(s\.prompt, \{ | ||
| 305 | + label: \`research:$\{s\.key\}\`, | ||
| 306 | + phase: 'Research', | ||
| 307 | + schema: ITEM\_SCHEMA, // 每个节点返回验证过的 JSON | ||
| 308 | + agentType: 'general\-purpose', | ||
| 309 | + \}\), | ||
| 310 | + \), | ||
| 311 | +\); | ||
| 312 | +const collected = raw\.filter\(Boolean\); // 丢掉失败智能体的 null | ||
| 313 | + | ||
| 314 | +```Python | ||
| 315 | +phase('Research'); | ||
| 316 | +// Nine sources, nine agents, all at once. | ||
| 317 | +const raw = await parallel( | ||
| 318 | + SOURCES.map((s) => () => | ||
| 319 | + agent(s.prompt, { | ||
| 320 | + label: `research:${s.key}`, | ||
| 321 | + phase: 'Research', | ||
| 322 | + schema: ITEM_SCHEMA, // each node returns validated JSON | ||
| 323 | + agentType: 'general-purpose', | ||
| 324 | + }), | ||
| 325 | + ), | ||
| 326 | +); | ||
| 327 | +const collected = raw.filter(Boolean); // drop the nulls from failed agents | ||
| 328 | +``` | ||
| 329 | + | ||
| 330 | +fan\-out(分发并行执行)存在于 Claude 写的代码中,不在模型对话中。Claude 自己的上下文从不同时持有九个来源——每个子智能体携带自己的上下文,只有最终答案回来。 | ||
| 331 | + | ||
| 332 | +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\. | ||
| 333 | + | ||
| 334 | + | ||
| 335 | + | ||
| 336 | +这就是让 Claude 把工作流扩展到几十或几百个子智能体而不淹没会话的原因。编排层不消耗 token,因为它不是 Claude 思考的另一轮。 | ||
| 337 | + | ||
| 338 | +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\. | ||
| 339 | + | ||
| 340 | + | ||
| 341 | + | ||
| 342 | + | ||
| 343 | + | ||
| 344 | +## Fan in at a *barrier* | ||
| 345 | + | ||
| 346 | +\#\# 基于屏障节点完成结果扇入聚合 | ||
| 347 | + | ||
| 348 | +Fan\-in :结果聚合,汇总归约 | ||
| 349 | + | ||
| 350 | + | ||
| 351 | + | ||
| 352 | +fan\-out只有在有东西收集它时才有用。fin\-in是边汇聚的节点——一个智能体(或一段代码)同时看到所有上游结果,并做需要整个集合才能做的事:跨来源去重、按影响排序、总量为空时提前退出。这是屏障值得其墙钟成本的唯一地方。 | ||
| 353 | + | ||
| 354 | +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\. | ||
| 355 | + | ||
| 356 | + | ||
| 357 | + | ||
| 358 | +保障任务图谱高效运行的核心准则: | ||
| 359 | + | ||
| 360 | +仅当某一阶段确实需要集齐所有前置任务结果时,才使用屏障同步机制。需要对全部数据源统一去重?那就启用屏障 —— 这么做是合理的。 | ||
| 361 | + | ||
| 362 | +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\. | ||
| 363 | + | ||
| 364 | + | ||
| 365 | + | ||
| 366 | +python | ||
| 367 | + | ||
| 368 | +1\. // 边:纯 JS,无智能体,零 token。 | ||
| 369 | +const flat = collected\.flatMap\(\(c\) =\> c\.items\); | ||
| 370 | +log\(\`Collected $\{flat\.length\} items\`\); | ||
| 371 | +phase\('Curate'\); | ||
| 372 | +// 屏障节点:需要整个集合来去重\+排序。 | ||
| 373 | +const curated = await agent\( | ||
| 374 | + \`Dedupe and rank these by impact:\\n$\{JSON\.stringify\(flat\)\}\`, | ||
| 375 | + \{ phase: 'Curate', schema: CURATED\_SCHEMA \}, | ||
| 376 | +\); | ||
| 377 | + | ||
| 378 | +```Python | ||
| 379 | +// The edge: plain JS, no agent, zero tokens. | ||
| 380 | +const flat = collected.flatMap((c) => c.items); | ||
| 381 | +log(`Collected ${flat.length} items`); | ||
| 382 | +phase('Curate'); | ||
| 383 | +// The barrier node: needs the WHOLE set to dedupe + rank. | ||
| 384 | +const curated = await agent( | ||
| 385 | + `Dedupe and rank these by impact:\n${JSON.stringify(flat)}`, | ||
| 386 | + { phase: 'Curate', schema: CURATED_SCHEMA }, | ||
| 387 | +); | ||
| 388 | +``` | ||
| 389 | + | ||
| 390 | + | ||
| 391 | + | ||
| 392 | +如果只是单纯对列表做扁平化处理?直接依托数据流边内联处理即可。 有一套直白严苛的判断标准:倘若你的代码写成「并行分发 → 数据转换 → 再次并行分发」,且中间转换步骤不存在多条数据之间的交叉依赖,那本该改用流式管道处理,完全省去屏障同步。 | ||
| 393 | + | ||
| 394 | +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\. | ||
| 395 | + | ||
| 396 | + | ||
| 397 | + | ||
| 398 | + | ||
| 399 | + | ||
| 400 | +## The *diamond*: split → work → merge | ||
| 401 | + | ||
| 402 | +\#\# 菱形执行模型:拆分任务 → 并行处理 → 汇总合并 | ||
| 403 | + | ||
| 404 | + | ||
| 405 | + | ||
| 406 | +把fan\-out和fan\-in放在一起,你就得到了每个严肃智能体图的主力拓扑:菱形执行模型。 | ||
| 407 | + | ||
| 408 | +Put fan\-out and fan\-in together and you get the workhorse topology of every serious agent graph: the **diamond**\. | ||
| 409 | + | ||
| 410 | + | ||
| 411 | + | ||
| 412 | +单个节点负责任务拆分,大量节点并行执行处理,再由单个节点汇总合并。市场信息巡检、依赖项审计、代码评审、深度调研报告等场景底层都采用这套拓扑结构;只需替换数据源与提示词,同一套基础框架就能适配各类业务需求。 | ||
| 413 | + | ||
| 414 | + | ||
| 415 | + | ||
| 416 | +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\. | ||
| 417 | + | ||
| 418 | + | ||
| 419 | + | ||
| 420 | +这套标准范式有个值得牢记的命名:扇出分发 → 归约处理 → 汇总生成。 扇出阶段拓宽信息覆盖范围;依靠原生代码做归约,精简海量原始数据;最后交由终端智能体整合输出完整结论。 | ||
| 421 | + | ||
| 422 | +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\. | ||
| 423 | + | ||
| 424 | + | ||
| 425 | + | ||
| 426 | +一旦你看到了菱形,你就不再问「如何让我的智能体做更多步骤」,而是开始问「拆分在哪里,合并在哪里」——这才是真正能扩展的问题。 | ||
| 427 | + | ||
| 428 | +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\. | ||
| 429 | + | ||
| 430 | + | ||
| 431 | + | ||
| 432 | + | ||
| 433 | + | ||
| 434 | +## Route the edge at runtime with a *conditional* | ||
| 435 | + | ||
| 436 | +\#\# 通过条件分支在运行时动态路由数据流边 | ||
| 437 | + | ||
| 438 | + | ||
| 439 | + | ||
| 440 | +并非所有任务图谱都是静态固定的。有时要走哪条数据流边,取决于上游节点的输出结果。路由节点会解析返回数据,判定启用哪一条下游执行分支:比如先对工单分类,再分流至对应的处理程序;或是先判断代码变更体量,再选择快速简易评审,或是启动完整全量审计。 | ||
| 441 | + | ||
| 442 | +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\. | ||
| 443 | + | ||
| 444 | + | ||
| 445 | + | ||
| 446 | +在工作流中这只是对节点验证输出的 JavaScript if 或 switch,因为控制流存在于代码中。 | ||
| 447 | + | ||
| 448 | +In a workflow this is just a JavaScript if or switch on a node’s validated output, because control flow lives in code\. | ||
| 449 | + | ||
| 450 | + | ||
| 451 | + | ||
| 452 | +这里确定性成了特性而非限制。路由器的决策可以是 Claude 驱动的(子智能体分类),但路由是 Claude 写的代码——所以对相同分类每次都以相同方式运行。 | ||
| 453 | + | ||
| 454 | +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\. | ||
| 455 | + | ||
| 456 | + | ||
| 457 | + | ||
| 458 | + | ||
| 459 | + | ||
| 460 | +节点环节依托 Claude 的大模型判断能力,数据流边环节依靠脚本代码稳定可靠。不会出现意外情况 —— 比如莫名其妙触发 “Claude 自行跳过审计流程”,因为跳过流程的逻辑必须显式定义在任务图谱中,而当前图谱并未配置该分支。 | ||
| 461 | + | ||
| 462 | +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\. | ||
| 463 | + | ||
| 464 | + | ||
| 465 | + | ||
| 466 | +python | ||
| 467 | + | ||
| 468 | +1\. // 路由节点:智能体分类,代码选边。 | ||
| 469 | +const \{ severity \} = await agent\( | ||
| 470 | + \`Classify this diff's risk:\\n$\{diff\}\`, | ||
| 471 | + \{ schema: \{ type: 'object', | ||
| 472 | + properties: \{ severity: \{ enum: \['low', 'high'\] \} \}, | ||
| 473 | + required: \['severity'\] \} \}, | ||
| 474 | +\); | ||
| 475 | +let review; | ||
| 476 | +if \(severity === 'high'\) \{ | ||
| 477 | + // 重路径:全面并行审计 | ||
| 478 | + review = await parallel\(FILES\.map\(\(f\) =\> \(\) =\> agent\(\`Audit $\{f\}\`\)\)\); | ||
| 479 | +\} else \{ | ||
| 480 | + // 轻路径:一次快速审查 | ||
| 481 | + review = await agent\(\`Quick review of $\{diff\}\`\); | ||
| 482 | +\} | ||
| 483 | + | ||
| 484 | +```Python | ||
| 485 | +// Router node: an agent classifies, code picks the edge. | ||
| 486 | +const { severity } = await agent( | ||
| 487 | + `Classify this diff's risk:\n${diff}`, | ||
| 488 | + { schema: { type: 'object', | ||
| 489 | + properties: { severity: { enum: ['low', 'high'] } }, | ||
| 490 | + required: ['severity'] } }, | ||
| 491 | +); | ||
| 492 | +let review; | ||
| 493 | +if (severity === 'high') { | ||
| 494 | + // heavy path: full parallel audit | ||
| 495 | + review = await parallel(FILES.map((f) => () => agent(`Audit ${f}`))); | ||
| 496 | +} else { | ||
| 497 | + // light path: one quick pass | ||
| 498 | + review = await agent(`Quick review of ${diff}`); | ||
| 499 | +} | ||
| 500 | +``` | ||
| 501 | + | ||
| 502 | + | ||
| 503 | + | ||
| 504 | + | ||
| 505 | + | ||
| 506 | +## Put a *verifier* on the edge | ||
| 507 | + | ||
| 508 | +\#\# 在数据流边配置放验证器 | ||
| 509 | + | ||
| 510 | + | ||
| 511 | + | ||
| 512 | +任务图谱真正的核心优势,不在于创建更多智能体,而在于你能搭建一套标准化流程框架包裹智能体,让输出结果具备可信赖度。 | ||
| 513 | + | ||
| 514 | +The real leverage of a graph isn’t more agents \- it’s the structure you can wrap around them to produce confidence\. | ||
| 515 | + | ||
| 516 | + | ||
| 517 | + | ||
| 518 | +校验节点部署在数据流边上,数据放行至下游前会先经过它,它的唯一作用就是筛除无效检测结果。数据校验通过才可继续流转;一旦校验失败,该结果就不会参与最终结论的生成。 | ||
| 519 | + | ||
| 520 | +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\. | ||
| 521 | + | ||
| 522 | + | ||
| 523 | + | ||
| 524 | + | ||
| 525 | + | ||
| 526 | +有三种模式值得掌握。 | ||
| 527 | + | ||
| 528 | +Three patterns are worth having in your hands\. | ||
| 529 | + | ||
| 530 | +对抗式校验:针对每一条检测结论,启动 N 个独立质疑智能体,指令其逐条反驳该结论;仅当多数质疑校验均未推翻结论时,才保留这条结果。 | ||
| 531 | + | ||
| 532 | +多视角差异化校验:为每个校验器分配专属评判维度 —— 逻辑正确性、安全风险、问题可复现性;多元视角能捕捉到批量相同校验无法发现的各类失效场景。 | ||
| 533 | + | ||
| 534 | +评审专家组机制:从不同角度生成 N 套解决方案,并行交由多名评审打分;以最优方案为基底汇总输出,同时吸纳其他备选方案中的优质内容。 | ||
| 535 | + | ||
| 536 | +- **Adversarial verify:** for each finding, spawn N independent skeptics prompted to refute it; keep it only if a majority survive\. | ||
| 537 | + | ||
| 538 | +- **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\. | ||
| 539 | + | ||
| 540 | +- **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\. | ||
| 541 | + | ||
| 542 | +这正是让一个真实团队移植 Bun 运行时并把对抗性代码审查内建到循环中的模式。 | ||
| 543 | + | ||
| 544 | +This is exactly the pattern that let a real team port the Bun runtime with adversarial code review baked into the loop\. | ||
| 545 | + | ||
| 546 | + | ||
| 547 | + | ||
| 548 | + | ||
| 549 | + | ||
| 550 | +## Isolate nodes so one failure can’t *poison* the graph | ||
| 551 | + | ||
| 552 | +\#\# 节点隔离,避免单点故障污染整个任务图谱 | ||
| 553 | + | ||
| 554 | +### | ||
| 555 | + | ||
| 556 | +串行链路中故障会连锁扩散:若节点 C 执行失败,节点 D 将无法启动,整条流程直接中断。而在任务图谱架构里,故障应当被限制在发生问题的单个节点内。 | ||
| 557 | + | ||
| 558 | +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\.** | ||
| 559 | + | ||
| 560 | + | ||
| 561 | + | ||
| 562 | +这一点其实已经部分实现:在 `parallel()` 内部,若某个延迟执行函数抛出异常,其结果会解析为 `null`;即便有一个子代理执行失败,其余八个正常的子代理仍能正常返回数据。调用 `.filter(Boolean)` 过滤空值,就是实现故障隔离的手段。 | ||
| 563 | + | ||
| 564 | +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\. | ||
| 565 | + | ||
| 566 | + | ||
| 567 | + | ||
| 568 | +设计每一处扇入聚合逻辑时,都要兼容缺失输入数据,不要默认所有上游结果都齐全。 | ||
| 569 | + | ||
| 570 | +Design every fan\-in to tolerate missing inputs rather than assume a full set\. | ||
| 571 | + | ||
| 572 | + | ||
| 573 | + | ||
| 574 | +更微妙的失败是节点互相踩踏。当智能体并行写文件时,它们可能冲突。 | ||
| 575 | + | ||
| 576 | +The subtler failure is nodes stepping on each other\. When agents write files in parallel, they can collide\. | ||
| 577 | + | ||
| 578 | + | ||
| 579 | + | ||
| 580 | +解决方案是隔离:worktree——每个智能体在自己的 git worktree 中运行,在沙箱中做工作,然后干净地合并。 | ||
| 581 | + | ||
| 582 | +The fix is isolation: "worktree" \- each agent runs in its own git worktree, does its work in a sandbox, and merges cleanly\. | ||
| 583 | + | ||
| 584 | + | ||
| 585 | + | ||
| 586 | +仅当多个节点确实并行执行时,才启用屏障同步机制。屏障是专为并行拓扑配套的安全防护措施,并非所有流程运行都必须承担的固定性能开销。 | ||
| 587 | + | ||
| 588 | +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\. | ||
| 589 | + | ||
| 590 | + | ||
| 591 | + | ||
| 592 | + | ||
| 593 | + | ||
| 594 | +## Add a cycle \- but make it *converge* | ||
| 595 | + | ||
| 596 | +\#\# 加一个循环——但要让它收敛 | ||
| 597 | + | ||
| 598 | + | ||
| 599 | + | ||
| 600 | +有时你直到开始才知道工作有多大:未知规模的探索、发现一个bug会暴露三个更多的bug扫描。这需要循环——一条回到更早节点的受控边。 | ||
| 601 | + | ||
| 602 | +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\. | ||
| 603 | + | ||
| 604 | + | ||
| 605 | + | ||
| 606 | +危险很明显:不收敛的循环是无限循环,会生成智能体直到你的预算耗尽。 | ||
| 607 | + | ||
| 608 | +The danger is obvious: a cycle that doesn’t converge is an infinite loop that spawns agents until your budget is gone\. | ||
| 609 | + | ||
| 610 | + | ||
| 611 | + | ||
| 612 | +这套收敛模式叫做「循环直至无新增结果」:持续启动检索探查节点,连续 K 轮都不再产出新内容时,才终止循环。决定该模式成败的关键细节 —— 也是绝大多数人初次实现都会踩的坑 —— 是去重比对所依据的基准对象 | ||
| 613 | + | ||
| 614 | +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\. | ||
| 615 | + | ||
| 616 | + | ||
| 617 | + | ||
| 618 | +去重校验要和所有已检索到的条目做比对,不能只拿已确认通过的结果去重。否则被校验驳回的无效结论会每一轮重复出现,循环永远无法收敛;最终你的系统会持续消耗算力,反复挖掘完全相同的无效死胡同。 | ||
| 619 | + | ||
| 620 | +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\. | ||
| 621 | + | ||
| 622 | + | ||
| 623 | + | ||
| 624 | +```Plain Text | ||
| 625 | +const seen = new Set(); const confirmed = []; let dry = 0; | ||
| 626 | + | ||
| 627 | +while (dry < 2) { // stop after 2 empty rounds | ||
| 628 | + const found = (await parallel( | ||
| 629 | + FINDERS.map((f) => () => agent(f.prompt, { schema: BUGS })) | ||
| 630 | + )).filter(Boolean).flatMap((r) => r.bugs); | ||
| 631 | + | ||
| 632 | + const fresh = found.filter((b) => !seen.has(key(b))); | ||
| 633 | + if (!fresh.length) { dry++; continue; } // nothing new → toward dry | ||
| 634 | + dry = 0; | ||
| 635 | + fresh.forEach((b) => seen.add(key(b))); // dedupe vs SEEN, not confirmed | ||
| 636 | + | ||
| 637 | + // diverse-lens verify each fresh finding before it counts | ||
| 638 | + const judged = await parallel(fresh.map((b) => () => | ||
| 639 | + parallel(['correctness', 'security', 'repro'].map((lens) => () => | ||
| 640 | + agent(`Judge "${b.desc}" via ${lens} — real?`, { schema: VERDICT }))) | ||
| 641 | + .then((v) => ({ b, real: v.filter(Boolean).filter((x) => x.real).length >= 2 })))); | ||
| 642 | + | ||
| 643 | + confirmed.push(...judged.filter((v) => v.real).map((v) => v.b)); | ||
| 644 | +} | ||
| 645 | +``` | ||
| 646 | + | ||
| 647 | + | ||
| 648 | + | ||
| 649 | + | ||
| 650 | + | ||
| 651 | +## Tier the models across the *nodes* | ||
| 652 | + | ||
| 653 | +\#\# 按节点职能分层选用梯度规格的大模型 | ||
| 654 | + | ||
| 655 | +## | ||
| 656 | + | ||
| 657 | +不是每个节点都需要你最好的模型。图让这变得明显,而单个智能体永远不会:有些节点是有界且重复的(提取这个字段、分类这个工单),有些承载真正的判断(综合报告、裁决发现)。 | ||
| 658 | + | ||
| 659 | +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\)\. | ||
| 660 | + | ||
| 661 | + | ||
| 662 | + | ||
| 663 | +无聊的节点用更便宜的模型跑,把你的昂贵 token 花在判断真正存在的地方。 | ||
| 664 | + | ||
| 665 | +Run the boring nodes on a cheaper model and spend your expensive tokens where judgment actually lives\. | ||
| 666 | + | ||
| 667 | + | ||
| 668 | + | ||
| 669 | +在工作流中,Claude 生成的每个子智能体默认继承你的会话模型,除非脚本覆盖它——所以默认情况下一次大运行全部按你的会话层级计费。单个 agent\(\) 调用上的 model 选项告诉 Claude 只把那个节点路由到别处。 | ||
| 670 | + | ||
| 671 | +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\. | ||
| 672 | + | ||
| 673 | + | ||
| 674 | + | ||
| 675 | +大运行前检查 /model,然后让 Claude 把扇出的重复节点路由到更便宜的模型,把合并节点留在高端。这就是把一个吃 token 的图从昂贵变成经济的杠杆,不需要改变它的形状。 | ||
| 676 | + | ||
| 677 | +**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\. | ||
| 678 | + | ||
| 679 | + | ||
| 680 | + | ||
| 681 | + | ||
| 682 | + | ||
| 683 | +## Topology *is* your cost and latency | ||
| 684 | + | ||
| 685 | +\#\# 拓扑就是你的成本和延迟 | ||
| 686 | + | ||
| 687 | + | ||
| 688 | + | ||
| 689 | +任务图谱的拓扑结构绝非锦上添花的表面设计,而是直接影响实际运行耗时的核心关键变量。绝大多数人都会踩坑的一个选择:`parallel()`(并行)与 `pipeline()`(流式管道)。`parallel()` 依赖屏障机制,所有分支必须等待最慢的节点执行完毕,下一阶段才能启动。 | ||
| 690 | + | ||
| 691 | +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\. | ||
| 692 | + | ||
| 693 | + | ||
| 694 | + | ||
| 695 | +`pipeline()` 以流式方式让每条数据独立走完全流程,全程无屏障阻塞:条目 A 处理到第三阶段时,条目 B 可能还停留在第一阶段。处理速度快的数据会提前完成,不会卡在慢速任务后方空等。 | ||
| 696 | + | ||
| 697 | +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\. | ||
| 698 | + | ||
| 699 | + | ||
| 700 | + | ||
| 701 | + | ||
| 702 | + | ||
| 703 | +默认采用流式管道 `pipeline()`;仅当某一阶段确实需要一次性拿到上游全部结果时,才使用屏障机制 —— 例如全量数据去重、基于全部统计结果提前终止流程、需要对比多条检索结论的综合提示词场景。 “代码写起来更整洁”“各阶段逻辑划分清晰” 都不足以作为启用屏障的理由;屏障带来的延迟是真实、可量化、纯粹浪费的耗时。逻辑分层清晰,不等于必须同步阻塞等待所有任务。 | ||
| 704 | + | ||
| 705 | +**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\. | ||
| 706 | + | ||
| 707 | + | ||
| 708 | + | ||
| 709 | + | ||
| 710 | + | ||
| 711 | +## Let Claude draw the graph \- *self\-routing* | ||
| 712 | + | ||
| 713 | +\#\# 交由 Claude 自主生成任务图谱:自路由机制 | ||
| 714 | + | ||
| 715 | + | ||
| 716 | + | ||
| 717 | +最后一种方案:对于无法预先规划流程的任务,不再手动编写固化的任务图谱。 | ||
| 718 | + | ||
| 719 | +The final move is to stop drawing the graph by hand for jobs you can’t plan in advance\. | ||
| 720 | + | ||
| 721 | + | ||
| 722 | + | ||
| 723 | +借助动态工作流,你只需描述最终目标,Claude 会自行生成任务编排脚本:拆解整体任务、选择并行分发策略、调度一组协同工作的子智能体,最后整合输出结论。系统会生成适配本次任务专属的流程拓扑,而非一套寄希望于能适配所有场景的固化图谱。 | ||
| 724 | + | ||
| 725 | +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\. | ||
| 726 | + | ||
| 727 | + | ||
| 728 | + | ||
| 729 | +一共有三种启用动态工作流的方式: | ||
| 730 | + | ||
| 731 | +第一种,在提示词中直接写明 “workflow(工作流)”,Claude 会为当前任务自动生成一套流程; | ||
| 732 | + | ||
| 733 | +第二种,调用已保存或打包好的预置工作流 ——`/deep-research` 就是一套已投产上线的标准任务图谱,完整链路为:范围界定 → 并行检索 → 内容拉取 → 对抗式校验 → 结果综合,和本教程讲解的标准框架完全一致。 | ||
| 734 | + | ||
| 735 | +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\. | ||
| 736 | + | ||
| 737 | + | ||
| 738 | + | ||
| 739 | +第三种方式:开启极速代码模式(ultracode),会话内每一项有一定复杂度的任务,Claude 都会自动规划专属工作流。 某次执行效果理想时,按下快捷键 `s` 即可将编排脚本保存至 `.claude/workflows/` 目录;脚本自带版本管理,可通过名称重复调用,任何克隆该代码仓库的人都能直接启动这套任务图谱。 | ||
| 740 | + | ||
| 741 | +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\. | ||
| 742 | + | ||
| 743 | + | ||
| 744 | + | ||
| 745 | +python | ||
| 746 | + | ||
| 747 | +执行一套工作流,对 `src/routes/` 下所有接口路由开展鉴权缺失审计:为每一个路由文件单独创建一个子智能体,每条检测结果都先经过校验,再输出审计报告。 ● Claude 已生成任务编排脚本・后台启动执行中… /workflows — auth\-audit(鉴权审计工作流)・运行中 ✓ 范围界定 1/1,消耗 2100 Token,耗时 4 秒 ✓ 并行扇出 18/18,18 个路由文件各分配独立子智能体 ◯ 校验环节 11/18,每条检测结论配备 3 票制对抗质疑校验器…… ○ 汇总合成 0/1,等待校验阶段完成 会话全程保持可交互状态,子智能体集群后台持续运算不阻塞界面 | ||
| 748 | + | ||
| 749 | +```Python | ||
| 750 | +› Run a workflow to audit every route under src/routes/ for missing | ||
| 751 | +auth. Spawn one agent per route file, then verify each finding before | ||
| 752 | +reporting. ● Claude wrote an orchestration script · launching in | ||
| 753 | +background… /workflows — auth-audit · running ✓ Scope 1/1 2.1k tok · | ||
| 754 | +4s ✓ Fan-out 18/18 one agent per route file ◯ Verify 11/18 3-vote | ||
| 755 | +skeptics per finding… ○ Synthesize 0/1 waiting on verify session stays | ||
| 756 | +responsive — keep working while the fleet runs | ||
| 757 | +``` | ||
| 758 | + | ||
| 759 | + | ||
| 760 | + | ||
| 761 | +\#\# 本周用 Claude 构建的六个图 | ||
| 762 | + | ||
| 763 | +## Six graphs to build with *Claude* this week | ||
| 764 | + | ||
| 765 | + | ||
| 766 | + | ||
| 767 | +- **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\. | ||
| 768 | + | ||
| 769 | +全链路安全扫描 | ||
| 770 | + | ||
| 771 | +对每一条业务路径执行安全巡检。Claude 会为每个路由文件单独生成一个子智能体,分别检索缺失的权限校验逻辑;所有风险点需经过校验器二次核验,确认无误后才会录入报告。覆盖广度远超单一上下文所能承载的分析范围。 | ||
| 772 | + | ||
| 773 | + | ||
| 774 | + | ||
| 775 | +- **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\. | ||
| 776 | + | ||
| 777 | +基于 /deep\-research 指令生成引证调研报告 | ||
| 778 | + | ||
| 779 | +该功能图谱已内置在 Claude Code 中。Claude 将你的查询拆解为多个独立分析维度,并行执行多源检索、去重重复资料;随后启用三方复核机制对每一条结论交叉质证,全部核验通过后再撰写最终报告。 | ||
| 780 | + | ||
| 781 | + | ||
| 782 | + | ||
| 783 | +- **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\. | ||
| 784 | + | ||
| 785 | +逐文件迁移代码模块 | ||
| 786 | + | ||
| 787 | +突破 Bun 运行时性能上限,适配你的完整代码仓库。Claude 并行批量转换多个文件,每完成一份文件就执行全套测试用例作为准入关卡;编译 / 测试失败的文件会回流重处理。对抗式复核机制能揪出单次粗转极易遗留的代码缺陷。 | ||
| 788 | + | ||
| 789 | + | ||
| 790 | + | ||
| 791 | +- **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\. | ||
| 792 | + | ||
| 793 | +代码变更差分对抗评审 | ||
| 794 | + | ||
| 795 | +Claude 根据代码改动规模分流评审策略:小幅变更执行一轮快速核查;大规模改动将启动全量并行审计,多评审智能体分别从逻辑正确性、安全漏洞、运行性能三大维度独立审查,最后由合议模块整合各方结论输出综合意见。 | ||
| 796 | + | ||
| 797 | + | ||
| 798 | + | ||
| 799 | +- 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\. | ||
| 800 | + | ||
| 801 | +定时生态全域巡检 | ||
| 802 | + | ||
| 803 | +配置一次,永久复用。Claude 并行拉取多渠道信息:版本发布公告、技术博客、社区讨论帖;按业务影响权重过滤低价值内容,自动生成精简摘要。工作流配置存入版本库 `.claude/workflows/` 目录,支持通过名称一键启动。 | ||
| 804 | + | ||
| 805 | + | ||
| 806 | + | ||
| 807 | +- **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\. | ||
| 808 | + | ||
| 809 | +未知范围全域漏洞探查 | ||
| 810 | + | ||
| 811 | +适用于漏洞总量未知的场景。Claude 并行启动多套漏洞检索器,新发现问题自动与已有结果去重,留存条目全部完成核验;持续循环检索,连续两轮无新增问题后自动终止扫描。 | ||
| 812 | + | ||
| 813 | + | ||
| 814 | + | ||
| 815 | + | ||
| 816 | + | ||
| 817 | +## Conclusion: | ||
| 818 | + | ||
| 819 | +\#\# 结语: | ||
| 820 | + | ||
| 821 | + | ||
| 822 | + | ||
| 823 | +提示者问问题。架构师画图。 | ||
| 824 | + | ||
| 825 | +A prompter asks a question\. An architect draws a *graph\.* | ||
| 826 | + | ||
| 827 | + | ||
| 828 | + | ||
| 829 | +线性智能体从来不是天花板——它只是第一个形状,每个人都会伸手去拿的那个,因为它匹配我们打字的方式。一行,一个脑袋,一次一件事。 | ||
| 830 | + | ||
| 831 | +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\.** | ||
| 832 | + | ||
| 833 | + | ||
| 834 | + | ||
| 835 | +一旦你能看到节点和边,你就不再要求智能体做更多,而是要求图做得更宽:在独立工作处扇出,在信心要紧处给边加门,在判断不需要处分层模型。 | ||
| 836 | + | ||
| 837 | +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\. | ||
| 838 | + | ||
| 839 | + | ||
| 840 | + | ||
| 841 | +大多数人会继续排队走直线。学会画图的人将运行一个舰队——而永远不会注意到其他人卡在的天花板。 | ||
| 842 | + | ||
| 843 | +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\. | ||
| 844 | + |
docs/development-plan.md
0 → 100644
| 1 | +++ a/docs/development-plan.md | ||
| 1 | +# graphlemon 开发计划 | ||
| 2 | + | ||
| 3 | +**版本**: v1.0 | ||
| 4 | +**日期**: 2026-07-29 | ||
| 5 | +**输入来源**: `graphlemon_architecture.md`(v0.11)+ `docs/sdd/`(00-06 + `sdd-mvcp-contract.json` v1.6.0) | ||
| 6 | +**方法论**: 复用本地 Hermes `planning` profile 的 `project-unit-decomposition`(Development Unit 分解 + 依赖 DAG)+ `engineering-work-planning`(Phase/Sub-phase/Unit 三层分类:Epic 级/Feature 级/Task 级,垂直切片优先于技术分层) | ||
| 7 | + | ||
| 8 | +## 0. 怎么读这份文档 | ||
| 9 | + | ||
| 10 | +- **Phase**(大阶段,对应 Epic 级):有自己的验收标准和"完成"定义,多个 Sub-phase 组成。 | ||
| 11 | +- **Sub-phase**(小阶段,对应 Feature 级):一个可独立验收的能力切片,多个 Unit 组成。 | ||
| 12 | +- **Unit**(子任务,对应 Task 级,即 Development Unit):一个明确的工程产物 + 完成标准,映射到至少一条 SDD 条目(M/V/C/P/REQ),标注风险分类: | ||
| 13 | + - **[Value Path]** 主线价值切片,直接对应用户/系统可感知的能力 | ||
| 14 | + - **[Risk Gate]** 高风险校验/回路/护栏,不做会在生产阶段暴露风险 | ||
| 15 | + - **[Enabler]** 使能型基础设施,本身不直接产出用户可见价值,但阻塞其他 Unit | ||
| 16 | +- 每个 Unit 标注 `依赖`(DAG 上游)和 `Done 标准`(可观察、可验证,优先复用 06-验收与测试锚点.md 已有的 REQ 用例,不重新发明验收口径)。 | ||
| 17 | +- 机器可读版本见 `docs/sdd/unit_matrix.json`(Unit ID、依赖、REQ/M/V/C/P 覆盖、风险分类,供后续 Coder Agent 或调度器直接消费)。 | ||
| 18 | + | ||
| 19 | +**关于阶段排序的说明**:Phase 0 是纯 Enabler(无独立用户价值),Phase 1 起才是垂直切片(贪吃蛇场景端到端跑通)。这不违反"避免按技术分层切故事"的原则——Phase 0 内部的确按技术层(数据模型→后端集成→控制台骨架)排列,但这是因为它们是共同阻塞后续所有价值切片的地基工作,不是把一个本该完整的价值切片强行拆成"先做数据库、再做 API、再做前端"三个假装独立的"Phase"。真正的价值切片从 Phase 1 开始,一路贯穿 Org Graph→Work Graph→端到端验收,不按技术层拆分。 | ||
| 20 | + | ||
| 21 | +## 1. Phase 总览 | ||
| 22 | + | ||
| 23 | +| Phase | 目标 | 类型 | 验收锚点 | | ||
| 24 | +|---|---|---|---| | ||
| 25 | +| PH0 | 基础设施与脚手架就绪 | Enabler | 能创建目标项目、Git 仓库结构符合约定、15 个实体可建表 | | ||
| 26 | +| PH1 | MVP 全链路闭环跑通(贪吃蛇场景) | Value Path | 架构文档 §11 验收标准第 1-3 条;REQ-01/02/03/07 | | ||
| 27 | +| PH2 | 质量闭环:回路、Skill 迭代、护栏、断点恢复 | Risk Gate | 架构文档 §11 验收标准第 4-5 条;REQ-04/05/06/08/10/14 | | ||
| 28 | +| PH3 | 人机交互控制台 | Value Path | REQ-11/12/13 | | ||
| 29 | +| PH4 | 全量验收与固化 | Risk Gate | 06 节全部 14 条 REQ 完整回归通过一次 | | ||
| 30 | +| 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 步走查场景真实跑通一次 | | ||
| 31 | + | ||
| 32 | +依赖顺序: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 开始被允许对自己的代码做自主修改"这个新增能力上生效。 | ||
| 33 | + | ||
| 34 | +--- | ||
| 35 | + | ||
| 36 | +## PH0:基础设施与脚手架(Enabler) | ||
| 37 | + | ||
| 38 | +**Goal**:为后续所有开发建立可靠地基,本身不产出用户可见能力。 | ||
| 39 | +**Phase 验收**:给定一个空 git 仓库路径,能自动初始化出完整的 graphlemon 目录骨架;15 个数据实体全部建表成功并通过约束校验;`claude --agent planner` 之类的 subagent 调用能跑通最简单的一次往返。 | ||
| 40 | + | ||
| 41 | +### PH0-S1 数据模型与仓库骨架 | ||
| 42 | + | ||
| 43 | +可与 PH0-S3 并行。 | ||
| 44 | + | ||
| 45 | +- **U-001 [Enabler]** 落地 01-M 全部 15 个实体的物理数据库 schema(MVP 单机场景可用 SQLite),含全部枚举约束、外键、唯一索引 | ||
| 46 | + - 依赖:无(起点) | ||
| 47 | + - Done:15 张表全部建表成功;`project.target_folder_path` 唯一约束、`work_node_execution.status` 枚举约束等关键约束有单测覆盖 | ||
| 48 | + - 覆盖:01-M 全部实体 | ||
| 49 | + - 源码位置:graphlemon 仓库根目录 `src/`(见 05 节 §5.7"源码目录约定",与 U-002 在目标项目里生成的 `src/` 是两回事) | ||
| 50 | + | ||
| 51 | +- **U-002 [Enabler]** 目标项目 Git 仓库目录骨架初始化逻辑(架构文档 §7.3 目录树:`.claude/agents/`、`.claude/agent-memory/`、`.claude/workflows/`、`skills/`、`runs/`、`src/`)+ `.graphlemon/` 子目录(架构文档 §13.2) | ||
| 52 | + - 依赖:U-001(`project.graphlemon_subdir_initialized` 字段需要有实体支撑) | ||
| 53 | + - Done:给定一个空 git 仓库,能生成完整目录骨架并产生一次初始化 commit;非 git 仓库时按 02-V 2.1 节的确认态流程走(不静默 `git init`) | ||
| 54 | + - 覆盖:01-M `project`;03-C `c_project_create` | ||
| 55 | + | ||
| 56 | +- **U-003 [Enabler]** graphlemon 平台自身的仓库/运行时目录结构(与目标项目仓库物理分离;本地非 Git 的原始日志目录,见架构文档 §13.2) | ||
| 57 | + - 依赖:无 | ||
| 58 | + - Done:平台目录和任意目标项目目录可以是完全独立的两个路径,互不干扰;日志目录不纳入平台自身 Git 历史 | ||
| 59 | + | ||
| 60 | +### PH0-S2 Claude Code 后端集成 | ||
| 61 | + | ||
| 62 | +依赖 PH0-S1。这是全项目风险最集中的 Enabler 子阶段,直接决定 A-1(已 confirmed)之后的下一个高风险假设 A-2 能否成立。 | ||
| 63 | + | ||
| 64 | +- **U-004 [Risk Gate]** Hermes profile 内容迁移到 `.claude/agents/{planner,coder,validator}.md`(对应 SDD 00 节 A-2 假设的实际验证动作) | ||
| 65 | + - 依赖:U-002 | ||
| 66 | + - Done:三个 subagent 定义文件可被 Claude Code 发现,`claude --agent planner -p "..."` 能正常启动且系统提示词内容正确;`skills:`/`memory: project`/`tools:`/`model:` frontmatter 字段配置完整(见 05-实施约束与依赖.md §5.1) | ||
| 67 | + - 覆盖:架构文档 §7.2/§9;05 节 §5.1;A-2 假设 | ||
| 68 | + - **注意**:若迁移后发现人设内容需要大改才能在新形态下正常工作,A-2 应记录为"部分成立"而非直接标 confirmed,这会影响 U-005/U-011 的返工范围,是本 Unit 最主要的不确定性来源 | ||
| 69 | + | ||
| 70 | +- **U-004.5(2026-07-30 新增)[Enabler]** 搭建 `RUN_REAL_AGENT_E2E` 门控测试 harness(真实 shell out 到 `claude` 的端到端测试默认跳过,需显式环境变量开启;镜像 `CLAUDE.md` Testing 约束里点名的 graphloop 先例,避免 U-005/U-006/U-010 等后续 Unit 各自发明一套) | ||
| 71 | + - 依赖:U-004 | ||
| 72 | + - Done:仓库里有一个可复用的 skip 装饰器/测试基类(例如 `skip_unless_env("RUN_REAL_AGENT_E2E")`);日常 `python3 -m unittest` 默认跳过任何真实调用 `claude` 子进程的测试;显式设置该环境变量时,对 U-004 落地的某个 subagent 做一次真实 `-p` 往返调用能跑通 | ||
| 73 | + - 覆盖:`CLAUDE.md` Testing 约束 | ||
| 74 | + | ||
| 75 | +- **U-005 [Enabler]** `c_org_node_trigger` 的 `claude --agent <name> -p` 子进程调用封装(骨架,不含完整状态机) | ||
| 76 | + - 依赖:U-004, U-004.5 | ||
| 77 | + - Done:手工调用能触发三个 Org Agent 之一并拿到文本响应 | ||
| 78 | + | ||
| 79 | +- **U-006 [Enabler]** Work Graph 执行层调用封装(workflow 脚本写入 + `claude -p` 调用,直接复用 `spikes/a1-dynamic-workflows/` 已验证的代码路径,见 03-C 3.5 节实现契约) | ||
| 80 | + - 依赖:U-004, U-004.5 | ||
| 81 | + - Done:封装成可被服务端函数调用的接口,单测通过,`--permission-mode auto` 生效(不是 `--bare`,不是 `bypassPermissions`,见 05 节 §5.3.1) | ||
| 82 | + | ||
| 83 | +### PH0-S3 控制台骨架 | ||
| 84 | + | ||
| 85 | +可与 PH0-S1/S2 并行,无相互依赖。 | ||
| 86 | + | ||
| 87 | +- **U-007 [Enabler]** 本地后端进程骨架(纯 stdlib Python `http.server`/`socketserver` 实现 HTTP API + WebSocket/SSE,不引入 Flask/FastAPI 等第三方包,见 05 节 §5.7)+ `c_pick_folder` 的 macOS 实现(`osascript`,Windows/Linux 留到 PH3 补齐) | ||
| 88 | + - 依赖:无 | ||
| 89 | + - Done:后端进程可启停;macOS 上 `/api/system/pick-folder` 能真实弹出系统对话框并返回路径;后端代码零第三方依赖(`pip list`/`import` 检查可验证) | ||
| 90 | + - 覆盖:03-C `c_pick_folder`;05 节 §5.4.1, §5.7 | ||
| 91 | + | ||
| 92 | +- **U-008 [Enabler]** 前端骨架(本地 Web 应用,原生 HTML/CSS/JS,无构建步骤,无 npm 依赖,见 05 节 §5.7) | ||
| 93 | + - 依赖:U-007 | ||
| 94 | + - Done:浏览器访问 `localhost` 能看到路由跳转正常的空壳应用;前端不依赖 npm install/构建工具链 | ||
| 95 | + | ||
| 96 | +--- | ||
| 97 | + | ||
| 98 | +## PH1:MVP 全链路闭环(Value Path,贪吃蛇场景) | ||
| 99 | + | ||
| 100 | +**Goal**:端到端跑通一次从"提出目标"到"任务完成"的完整闭环,不要求 UI,可以用手工 API 调用驱动。 | ||
| 101 | +**Phase 验收**:REQ-01/02/03/07 的 happy path 全部通过;架构文档 §11 验收标准第 1-3 条达成。 | ||
| 102 | + | ||
| 103 | +### PH1-S1 Org Graph 层单点触发 | ||
| 104 | + | ||
| 105 | +- **U-009 [Value Path]** `c_project_create` + `c_task_create` 实现,`task` 状态机(`p_org_task_lifecycle`)落地到 `queued` 态 | ||
| 106 | + - 依赖:U-001, U-002 | ||
| 107 | + - Done:REQ-01 happy/boundary/failure 三个用例全部通过 | ||
| 108 | + - 覆盖:01-M `task`;03-C `c_task_create`;04-P `p_org_task_lifecycle`;REQ-01 | ||
| 109 | + | ||
| 110 | +- **U-010 [Value Path]** `c_org_node_trigger` 完整实现(含 `TASK_NOT_READY`/`CONCURRENCY_LIMIT_REACHED`/`USAGE_LIMIT_REACHED` 错误处理) | ||
| 111 | + - 依赖:U-005, U-009 | ||
| 112 | + - Done:手工依次触发 planner/coder/validator 三个 Org Agent,`task.status` 按状态机正确流转(对应架构文档 §11"手工调用验证"里程碑) | ||
| 113 | + - 覆盖:03-C `c_org_node_trigger` | ||
| 114 | + | ||
| 115 | +### PH1-S2 Work Graph 层集成(本项目风险最高的 Sub-phase) | ||
| 116 | + | ||
| 117 | +- **U-011 [Value Path]** `c_work_graph_plan` 实现(Planning Skill 调用,输出 `graph_manifest`) | ||
| 118 | + - 依赖:U-004, U-010 | ||
| 119 | + - Done:REQ-02 happy 用例通过(Planner 产出 BRD/PRD/SDD/Benchmark 四类 artifact 且血缘完整) | ||
| 120 | + - 覆盖:01-M `artifact`/`work_graph_run`;03-C `c_work_graph_plan`;REQ-02 | ||
| 121 | + | ||
| 122 | +- **U-012 [Value Path]** `c_work_graph_execute` 完整实现(`auto` 权限模式、`USAGE_LIMIT_REACHED` 退避重试 `p_usage_limit_wait`、`CLASSIFIER_BLOCKED` 处理) | ||
| 123 | + - 依赖:U-006, U-011 | ||
| 124 | + - Done:REQ-07 全部用例通过;REQ-14(`p_usage_limit_wait`)happy 用例通过(boundary/failure 场景在 PH2-S3 补齐完整覆盖) | ||
| 125 | + - 覆盖:03-C `c_work_graph_execute`;04-P `p_work_graph_execution`/`p_usage_limit_wait`;REQ-07/REQ-14 | ||
| 126 | + | ||
| 127 | +- **U-013 [Value Path]** `c_work_node_run` + `git_worktree` 隔离(并行写代码节点) | ||
| 128 | + - 依赖:U-012 | ||
| 129 | + - Done:REQ-03 happy/boundary 通过(两模块并行编码使用独立 worktree,合并后代码无冲突) | ||
| 130 | + - 覆盖:01-M `work_node_execution`;03-C `c_work_node_run`;REQ-03 | ||
| 131 | + | ||
| 132 | +### PH1-S3 端到端验收(收口) | ||
| 133 | + | ||
| 134 | +- **U-014 [Value Path]** 完整跑通"实现贪吃蛇游戏"验收场景,从 `goal_text` 到 `task.status=done` | ||
| 135 | + - 依赖:U-011, U-012, U-013 | ||
| 136 | + - Done:架构文档 §11 MVP 验收标准第 1-3 条全部达成(Planner 出四件套,Coder 自测通过,Validator 归因产出 bug list) | ||
| 137 | + | ||
| 138 | +--- | ||
| 139 | + | ||
| 140 | +## PH2:质量闭环(Risk Gate,回路与护栏) | ||
| 141 | + | ||
| 142 | +**Goal**:系统出错时能自主定位、自主修复,或安全地转人工,不会无限空转也不会裸奔超支。 | ||
| 143 | +**Phase 验收**:完整跑通一次 Org-level Loop(人为植入 bug → 定位 → 修复);REQ-04/05/06/08/09/10/14 全部通过。 | ||
| 144 | + | ||
| 145 | +PH2-S1/S2/S3/S4 之间除标注的依赖外相互独立,可并行推进。 | ||
| 146 | + | ||
| 147 | +### PH2-S1 Validator 回路与根因溯源 | ||
| 148 | + | ||
| 149 | +- **U-015 [Risk Gate]** `p_validator_root_cause_trace` 实现(三态判定 PASS/FAIL/INCONCLUSIVE,双向可追溯矩阵) | ||
| 150 | + - 依赖:U-013 | ||
| 151 | + - Done:REQ-04 happy/boundary/failure 通过 | ||
| 152 | + - 覆盖:01-M `bug_record`;03-C `c_bug_record_create`;04-P `p_validator_root_cause_trace`;REQ-04 | ||
| 153 | + | ||
| 154 | +- **U-016 [Risk Gate]** `c_org_loop_route` + `fast_lane`/`full_replan` 快速通道 | ||
| 155 | + - 依赖:U-015 | ||
| 156 | + - Done:REQ-05/REQ-09 全部用例通过(含 3 轮上限 → `blocked_pending_human`) | ||
| 157 | + - 覆盖:03-C `c_org_loop_route`;REQ-05/REQ-09 | ||
| 158 | + | ||
| 159 | +- **U-017 [Risk Gate]** 人为植入 bug 完整闭环验证(架构文档 §11 验收标准第 4 条) | ||
| 160 | + - 依赖:U-016 | ||
| 161 | + - Done:引入一个人为植入的 bug,系统自主定位根因并驱动 Planner/Coder 完成修复闭环 | ||
| 162 | + | ||
| 163 | +### PH2-S2 Skill 自主迭代 | ||
| 164 | + | ||
| 165 | +- **U-018 [Risk Gate]** `c_skill_update` + `c_skill_rollback` + Git commit 留痕 | ||
| 166 | + - 依赖:U-015(需要 `root_cause_category=skill_defect` 触发) | ||
| 167 | + - Done:REQ-08 全部用例通过 | ||
| 168 | + - 覆盖:01-M `skill`/`skill_iteration`;03-C `c_skill_update`/`c_skill_rollback`;REQ-08 | ||
| 169 | + | ||
| 170 | +### PH2-S3 护栏双层机制 | ||
| 171 | + | ||
| 172 | +- **U-019 [Risk Gate]** `budget_guardrail_config` + `c_budget_check`(人工配置层) | ||
| 173 | + - 依赖:U-012 | ||
| 174 | + - Done:REQ-10 happy/boundary 通过 | ||
| 175 | + - 覆盖:01-M `budget_guardrail_config`;03-C `c_budget_check`;04-P `p_budget_guardrail`;REQ-10 | ||
| 176 | + | ||
| 177 | +- **U-020 [Risk Gate]** `p_usage_limit_wait` 完整验收(`session`/`weekly`/`opus` 三种 `quota_limit_type` + 超时兜底) | ||
| 178 | + - 依赖:U-012(骨架已在 U-012 实现,本 Unit 补齐边界/失败场景) | ||
| 179 | + - Done:REQ-14 全部用例通过(含超时转 `blocked_pending_human, blocked_reason='usage_limit_wait_timeout'`) | ||
| 180 | + - 覆盖:04-P `p_usage_limit_wait`;REQ-14 | ||
| 181 | + | ||
| 182 | +### PH2-S4 断点恢复 | ||
| 183 | + | ||
| 184 | +- **U-021 [Risk Gate]** Graph Engine 持久化状态 + 断点恢复 | ||
| 185 | + - 依赖:U-012, U-013 | ||
| 186 | + - Done:REQ-06 happy/boundary/failure 通过(执行中途中断进程后从持久化状态恢复,不重跑已完成节点) | ||
| 187 | + - 覆盖:01-M `work_graph_run`/`work_node_execution`;04-P `p_work_graph_execution`;REQ-06 | ||
| 188 | + | ||
| 189 | +--- | ||
| 190 | + | ||
| 191 | +## PH3:人机交互控制台(Value Path,可观测性与操作入口) | ||
| 192 | + | ||
| 193 | +**Goal**:人可以通过浏览器创建项目、提出目标、反馈 bug、实时看到运行过程、管理 Skill 库。 | ||
| 194 | +**Phase 验收**:REQ-11/12/13 全部通过。 | ||
| 195 | +**并行关系**:可在 PH1 完成后与 PH2 并行开工(控制台是对已产生数据的可视化/操作层,不阻塞回路/护栏本身的开发逻辑)。PH3-S2(实时看板)分两级:U-024(推送链路本身)只需 U-012/U-013 落地即可开工,比原计划更早;REQ-12 的完整验收挪到 U-024.5,需要排在 PH2 大部分 Unit 完成、`execution_event` 各类场景都已产生真实数据之后再联调。 | ||
| 196 | + | ||
| 197 | +### PH3-S1 项目与目标入口 | ||
| 198 | + | ||
| 199 | +- **U-022 [Value Path]** `v_project_setup` + `c_pick_folder` 全平台补齐(Windows `PowerShell`/Linux `zenity`/`kdialog`) | ||
| 200 | + - 依赖:U-007, U-008 | ||
| 201 | + - Done:REQ-11 happy/boundary/failure 通过 | ||
| 202 | + - 覆盖:02-V `v_project_setup`;03-C `c_pick_folder`;05 节 §5.4.1;REQ-11 | ||
| 203 | + | ||
| 204 | +- **U-023 [Value Path]** `v_goal_composer` + `v_bug_feedback` | ||
| 205 | + - 依赖:U-022 | ||
| 206 | + - Done:能通过 UI 创建 `task` 和人工发起的 `bug_record`(`c_bug_report_create`) | ||
| 207 | + - 覆盖:02-V `v_goal_composer`/`v_bug_feedback`;03-C `c_bug_report_create` | ||
| 208 | + | ||
| 209 | +### PH3-S2 实时看板 | ||
| 210 | + | ||
| 211 | +- **U-024 [Value Path]** `c_ws_subscribe` + `execution_event` 实时推送(最小连通版) | ||
| 212 | + - 依赖:U-012, U-013(work graph 执行、work node run 是最早、最稳定产出 `execution_event` 的两个来源,足够验证推送链路本身是否通) | ||
| 213 | + - Done:`c_ws_subscribe` 对真实 `execution_event` 做实时推送,≤2 秒延迟达成;REQ-12 happy 用例通过(boundary/failure——尤其是断线重连拉快照在多样事件场景下的表现——留给 U-024.5 补齐完整覆盖) | ||
| 214 | + - 覆盖:01-M `execution_event`;03-C `c_ws_subscribe`;REQ-12(部分) | ||
| 215 | + | ||
| 216 | +- **U-024.5(2026-07-30 新增)[Value Path]** REQ-12 完整验收(事件多样性联调) | ||
| 217 | + - 依赖:U-024, U-015, U-016, U-018, U-019, U-020, U-021(需要 PH2 大部分 Unit 已上线,事件类型覆盖足够丰富才能真正验证 boundary/failure 场景——例如 usage-limit 等待、护栏拦截、Skill 回滚各自产生的 `execution_event` 都要真实出现过) | ||
| 218 | + - Done:REQ-12 全部 happy/boundary/failure 用例通过;PH3 Phase 验收里的 REQ-12 以本 Unit 为准,U-024 本身只解决"链路通不通" | ||
| 219 | + - 覆盖:REQ-12(完整) | ||
| 220 | + | ||
| 221 | +- **U-025 [Value Path]** `v_dashboard` + `v_task_board` + `v_task_detail` | ||
| 222 | + - 依赖:U-024 | ||
| 223 | + | ||
| 224 | +- **U-026 [Value Path]** `v_graph_visualizer`(Org Graph + Work Graph 双层可视化,含 `waiting_for_quota` 信息态展示) | ||
| 225 | + - 依赖:U-024 | ||
| 226 | + | ||
| 227 | +### PH3-S3 日志与 Artifact/Skill 管理 | ||
| 228 | + | ||
| 229 | +- **U-027 [Value Path]** `execution_log_file` + `c_execution_log_get` + `c_ws_log_tail` + `v_log_viewer` | ||
| 230 | + - 依赖:U-024 | ||
| 231 | + - Done:REQ-13 happy/boundary/failure 通过 | ||
| 232 | + - 覆盖:01-M `execution_log_file`/`log_retention_config`;02-V `v_log_viewer`;03-C `c_execution_log_get`/`c_ws_log_tail`;REQ-13 | ||
| 233 | + | ||
| 234 | +- **U-028 [Value Path]** `v_artifact_browser` | ||
| 235 | + - 依赖:U-011 | ||
| 236 | + | ||
| 237 | +- **U-029 [Value Path]** `v_skill_library` + `v_skill_audit_log` | ||
| 238 | + - 依赖:U-018 | ||
| 239 | + | ||
| 240 | +### PH3-S4 配置页 | ||
| 241 | + | ||
| 242 | +- **U-030 [Value Path]** `v_concurrency_settings` + `v_budget_settings` + `v_log_retention_settings` | ||
| 243 | + - 依赖:U-019(预算配置)、U-021 涉及的 `log_retention_config`(U-001 已建表,此处只是配置页 UI) | ||
| 244 | + | ||
| 245 | +--- | ||
| 246 | + | ||
| 247 | +## PH4:全量验收与固化(Risk Gate,收口) | ||
| 248 | + | ||
| 249 | +**Goal**:确认 MVP 验收标准(架构文档 §11)全部达成,系统可以长期无人值守运行。 | ||
| 250 | +**Phase 验收**:06-验收与测试锚点.md 全部 14 条 REQ 的全部用例(happy/boundary/failure)完整回归通过一次。 | ||
| 251 | + | ||
| 252 | +- **U-031 [Risk Gate]** REQ-01~14 全量自动化回归套件 | ||
| 253 | + - 依赖:PH1/PH2/PH3 全部 Unit | ||
| 254 | + | ||
| 255 | +- **U-032 [Risk Gate]** 多 Goal 并发(`org_concurrency_config`)+ 长时间运行稳定性压测 | ||
| 256 | + - 依赖:U-031 | ||
| 257 | + - 覆盖:01-M `org_concurrency_config`;架构文档 §7.4"多任务并发"决策 | ||
| 258 | + | ||
| 259 | +- **U-033 [Risk Gate]** A-2 假设收口:Hermes SOUL.md 迁移效果复核,确认 `confirmed` 或记录偏差 | ||
| 260 | + - 依赖:U-004(此时已有足够真实运行数据判断人设迁移是否真的"不需要大改") | ||
| 261 | + | ||
| 262 | +--- | ||
| 263 | + | ||
| 264 | +## PH5:自举能力(Risk Gate,Deploy Supervisor + 蓝绿部署自动回滚) | ||
| 265 | + | ||
| 266 | +**命名说明**:本 Phase 的"Deploy Supervisor"和架构文档里"Supervisor(调度子 Agent,Coder Work Graph 内部监控节点)"是两个不同概念,详见 `supervisor-auto-rollback-design.md` 开头的命名说明,不要混用。 | ||
| 267 | + | ||
| 268 | +**Goal**:graphlemon 具备安全地自主修改自己代码(含 Org Agent 身份文件、调度器/执行器核心逻辑)的能力——出问题时自动回滚,不需要人工评审代码内容把关。 | ||
| 269 | +**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"全程无需人工介入代码评审。 | ||
| 270 | +**设计依据**:`docs/supervisor-design-comparison.md`(方案选型)+ `docs/supervisor-auto-rollback-design.md`(状态机/数据模型/健康检查/回滚流程/MVP 参数)。 | ||
| 271 | + | ||
| 272 | +### PH5-S1 数据模型与治理规则 | ||
| 273 | + | ||
| 274 | +- **U-034 [Enabler]** 01-M 扩展:新增 `deployment`、`deploy_health_signal` 两个实体建表;`bug_record.root_cause_category` 新增 `core_engine` 枚举值 | ||
| 275 | + - 依赖:U-001 | ||
| 276 | + - Done:两张新表建表成功并通过约束校验;`bug_record` 现有记录/查询逻辑对新枚举值向后兼容(不破坏 U-015/U-018 已有测试) | ||
| 277 | + - 覆盖:`supervisor-auto-rollback-design.md` §3;`self-development-feasibility.md` 缺口 3 | ||
| 278 | + | ||
| 279 | +- **U-035 [Risk Gate]** self-target 场景 worktree 无条件强制规则(缺口 1)+ `root_cause_category=core_engine` 命中时路由到蓝绿部署流程而非 `fast_lane`(`c_org_loop_route` 扩展) | ||
| 280 | + - 依赖:U-013(worktree 机制)、U-016(`c_org_loop_route`)、U-034(`core_engine` 枚举) | ||
| 281 | + - Done:`target_folder_path == graphlemon 自身安装路径` 时,即使是单节点串行修改也强制走 worktree(不再要求"仅并行时才强制");`root_cause_category=core_engine` 的 bug 被 `c_org_loop_route` 正确路由到 PH5-S2/S3 的蓝绿流程;**且改动落地后必须重跑一次 U-013/REQ-03(worktree 隔离)现有回归用例全部通过**——这是 PH4 全量收口之后第一个真正 touch 生产核心执行路径的 Unit,不能假设"没测出问题就是没问题",否则等于在 PH5 一开工就悄悄打破 PH4 刚建立起来的信任前提 | ||
| 282 | + - 覆盖:REQ-03(回归) | ||
| 283 | + | ||
| 284 | +### PH5-S2 Deploy Supervisor 核心机制 | ||
| 285 | + | ||
| 286 | +- **U-036 [Enabler]** `supervisor/isolation/` 四模块补齐单元测试(当前从 graphloop 复制过来的 `isolated_workspace.py`/`workspace_security.py`/`change_manifest.py`/`selective_apply.py` 只做过一次手工冒烟测试,没有正式 `unittest` 套件)+ Deploy Supervisor 进程骨架(独立操作系统进程,纯 stdlib,不依赖任何 LLM CLI,PID/状态文件约定参考 graphloop `hermes_watchdog.py` 的原子写模式) | ||
| 287 | + - 依赖:无(代码已存在,是补测试和补骨架,不是从零开发) | ||
| 288 | + - Done:四模块各自有对应 `test_*.py`(可直接照搬/改造 graphloop 的 `test_change_manifest.py`/`test_selective_apply.py`/`test_workspace_security.py` 等,逻辑未改动,测试也基本不用改);Deploy Supervisor 骨架能独立启停,不受 Live/Candidate 进程崩溃影响 | ||
| 289 | + - **待定**:`supervisor/` 目录最终应归入 U-002 的目标项目树(`.claude/`、`skills/`、`src/` 那套,因为理论上任何 target project 都可能需要)还是 U-003 的"graphlemon 平台自身运行时目录"(因为 Deploy Supervisor 目前的唯一触发场景就是 self-target)——本 Unit 暂按"platform 自身工具"处理,若后续发现别的 target project 也要用同一套蓝绿回滚能力,再迁移 | ||
| 290 | + | ||
| 291 | +- **U-037 [Risk Gate]** `c_deploy_supervisor_stage`:候选实例拉起(全新目录 checkout,不 `git pull` 原地改)+ 一次性 DB 副本机制(候选实例验证期间绝不接触生产库) | ||
| 292 | + - 依赖:U-034, U-036 | ||
| 293 | + - Done:给定一个已合并到主分支的 commit,能生成 `deployment` 记录(`status=staged`)并拉起一个监听临时端口、连接一次性 DB 副本的候选进程 | ||
| 294 | + | ||
| 295 | +- **U-038 [Risk Gate]** `c_deploy_supervisor_health_check`:`process_alive`/`http_health`/`canary_task` 三项 pre-cutover 检查 | ||
| 296 | + - 依赖:U-037 | ||
| 297 | + - Done:三项检查全部通过 → `status=health_check_passed`;任一失败/超时 → `status=health_check_failed`,Live 全程未受影响(对应回滚路径 A) | ||
| 298 | + | ||
| 299 | +- **U-039 [Risk Gate]** `c_deploy_supervisor_cutover`:蓝绿切换本体(Live 停止接受新触发 → 候选切换到生产端口/生产库 → 旧 Live checkout 保留为 `previous_instance_dir`) | ||
| 300 | + - 依赖:U-038 | ||
| 301 | + - Done:健康检查通过后能完成真实切换,切换过程中在途 `work_node_execution` 能在重启后从持久化状态恢复(复用 U-021 断点恢复机制),`previous_instance_dir` 在监控窗口结束前不被清理 | ||
| 302 | + | ||
| 303 | +### PH5-S3 监控与回滚 | ||
| 304 | + | ||
| 305 | +- **U-040 [Risk Gate]** `c_deploy_supervisor_monitor`:post-cutover 监控窗口内四类信号采样(`process_alive`/`http_health`/`canary_task`/`validator_approval_rate`) | ||
| 306 | + - 依赖:U-039 | ||
| 307 | + - Done:`validator_approval_rate` 信号能从 `execution_event`/`work_node_execution` 正确计算滚动窗口内 Validator 的 approve 占比,人为把该占比推到偏离基线阈值之外时能触发 `anomaly_detected` | ||
| 308 | + | ||
| 309 | +- **U-041 [Risk Gate]** `c_deploy_supervisor_rollback`:回滚路径 A(pre-cutover,终止候选+`git revert`+发 `bug_record`)与路径 B(post-cutover,换回 `previous_instance_dir`+健康检查+`git revert`)完整实现,含"回滚本身失败"兜底转 `blocked_pending_human` | ||
| 310 | + - 依赖:U-038, U-040 | ||
| 311 | + - Done:两条回滚路径各自跑通一次;人为制造"旧版本也起不来"的场景能正确终态到 `blocked_pending_human` 而不是无限重试或静默失败;`git revert` 冲突场景(`supervisor-auto-rollback-design.md` §10 开放问题 3)有明确处理,不能是本 Unit 唯一遗留的未定义行为 | ||
| 312 | + | ||
| 313 | +- **U-042 [Risk Gate]** 自举闭环端到端验收:真实复现 `self-development-feasibility.md` 第 3 节的 9 步场景(修复 graphlemon 自己 `p_org_task_lifecycle` 状态机里的一个真实植入 bug),走完整"部署→健康检查→cutover→监控→finalize"或"检测异常→自动回滚"两条路径中至少一条 | ||
| 314 | + - 依赖:U-035, U-041 | ||
| 315 | + - Done:全程无需人工评审代码内容;参照 graphloop `RUN_REAL_AGENT_E2E=1` 的先例,真实拉起两个 graphlemon 实例的端到端用例默认跳过,需要显式环境变量开启,日常回归只跑 mock 版本 | ||
| 316 | + | ||
| 317 | +### PH5-S4 可观测性(非阻塞,可延后) | ||
| 318 | + | ||
| 319 | +- **U-043 [Value Path]** 控制台展示 `deployment` 状态与健康信号(02-V 新增视图) | ||
| 320 | + - 依赖:U-024, U-040 | ||
| 321 | + - Done:能在控制台看到当前是否有 deployment 在进行中、处于状态机的哪个状态、最近一次健康信号采样结果 | ||
| 322 | + | ||
| 323 | +### PH5 已知交叉缺口(不阻塞设计定稿,实现前需要留意) | ||
| 324 | + | ||
| 325 | +1. `supervisor/isolation/` 现有代码目前只做过一次手工冒烟测试(本次会话里跑的 isolate → diff → apply 往返验证),不是正式 `unittest` 套件——U-036 里已列为待办,暂不算"已完成"。 | ||
| 326 | +2. `supervisor/` 目录归属 U-002 还是 U-003 的问题目前只在 U-036 里留了一个临时决定,还没有真正的架构决策支撑,PH0 真正实现 U-002/U-003 时应该回头确认这个临时决定是否仍然成立。 | ||
| 327 | +3. U-001 的"15 张表"这个数字会在 PH5 落地后变成 17 张(`deployment`/`deploy_health_signal`),后续如果有人只看 PH0 的验收标准会觉得和实际情况对不上,属于文档层面需要注意的地方,不是设计缺陷。 | ||
| 328 | +4. `supervisor-auto-rollback-design.md` 里的参数草案值(健康检查超时、监控窗口时长等)明确标注"工程经验值,需要实测校准",U-036~U-042 实现时不要把这些数字当成已经验证过的常量直接硬编码进测试断言里,要留出可调整的空间。 | ||
| 329 | + | ||
| 330 | +--- | ||
| 331 | + | ||
| 332 | +## 2. Unit 分类汇总 | ||
| 333 | + | ||
| 334 | +| 分类 | Unit 数量 | Unit 列表 | | ||
| 335 | +|---|---|---| | ||
| 336 | +| Value Path(主线) | 17 | U-009~U-014, U-022~U-030, U-024.5, U-043 | | ||
| 337 | +| Risk Gate(高风险校验/护栏/收口) | 18 | U-004, U-015~U-021, U-031~U-033, U-035, U-037~U-042 | | ||
| 338 | +| Enabler(使能基础设施) | 10 | U-001~U-003, U-004.5, U-005~U-008, U-034, U-036 | | ||
| 339 | + | ||
| 340 | +(PH0-PH4 原有 33 个 Unit 分类不变,PH5 新增 10 个:U-034~U-043,其中 Value Path +1、Risk Gate +7、Enabler +2。 | ||
| 341 | +2026-07-30 计划评审追加 2 个 Unit:`U-004.5`[Enabler](RUN_REAL_AGENT_E2E harness 归属)、`U-024.5`[Value Path](U-024 依赖分级后的 REQ-12 完整验收),不改变原有编号顺序,只插入在对应 Unit 之后) | ||
| 342 | + | ||
| 343 | +## 3. 关键路径(Critical Path) | ||
| 344 | + | ||
| 345 | +风险最高、返工代价最大的一条依赖链(PH0-PH4 主线): | ||
| 346 | + | ||
| 347 | +``` | ||
| 348 | +U-001 → U-002 → U-004(A-2 假设验证)→ U-004.5(RUN_REAL_AGENT_E2E harness)→ U-005/U-006 → U-010 → U-011 → U-012(A-1 已 confirmed,实现契约已验证)→ U-013 → U-014(PH1 收口) | ||
| 349 | +``` | ||
| 350 | + | ||
| 351 | +U-004 是全链路里唯一还未经实测验证的假设节点(A-2 pending)。建议 U-004 完成后立即跑一次 U-005(最小触发验证),而不是等到 U-010/U-011 完整实现后才发现人设迁移有问题——这是应用 03-C/05 节里对 A-1 采用的"前置 spike 验证"同一原则。U-004.5 的插入不改变关键路径的性质,只是把"门控 e2e 测试模式怎么搭"这个原本会摊到 U-005/U-006/U-010 各自解决的隐性工作,显式化成一个前置节点,避免重复发明。 | ||
| 352 | + | ||
| 353 | +**PH5 关键路径**(与上面这条主线不合并——PH5 依赖 PH4 全部收口,是完全独立的第二条链): | ||
| 354 | + | ||
| 355 | +``` | ||
| 356 | +U-034 → U-035 → U-036 → U-037 → U-038 → U-039 → U-040 → U-041 → U-042(PH5 收口) | ||
| 357 | +``` | ||
| 358 | + | ||
| 359 | +U-041(回滚路径实现,含 §10 提到的 `git revert` 冲突处理)是 PH5 里返工风险最集中的一环,因为"回滚"本身失败没有再下一层的自动兜底,只能转人工——建议 U-041 完成后立即用人为制造的"旧版本也起不来"场景验证兜底路径,而不是假设它"大概率不会发生"就跳过测试。 | ||
| 360 | + | ||
| 361 | +## 4. 与 06-验收与测试锚点.md 的对应关系 | ||
| 362 | + | ||
| 363 | +本文档不重新定义验收标准,Unit 的 Done 标准直接引用 06 节已有的 REQ-01~14 用例(happy/boundary/failure)。如果后续 06 节的验收用例发生变更,本文档对应 Unit 的 Done 标准需要同步更新,避免两份文档各说各话。 |
docs/graphlemon_architecture.md
0 → 100644
| 1 | +++ a/docs/graphlemon_architecture.md | ||
| 1 | +# graphlemon 架构对齐文档 | ||
| 2 | + | ||
| 3 | +**版本**: v0.11(第 7.2 节决策:MVP 阶段只用 Claude Code ACP 一种执行后端,不考虑 Hermes CLI,Org Agent 落地为 Claude Code subagent 定义;第 10.1 节方案 A 可行性已通过 spike 实测确认;此前决策点见第 8 / 9 / 10.1 / 12.1 / 13.1 节) | ||
| 4 | +**日期**: 2026-07-29 | ||
| 5 | +**定位**: 本文档用于对齐 Org Graph / Work Graph 双层架构的概念模型、边界与机制,作为后续 PRD / SDD / 任务拆解的上游依据。不含具体排期与代码级设计。 | ||
| 6 | + | ||
| 7 | +--- | ||
| 8 | + | ||
| 9 | +## 1. 项目目标 | ||
| 10 | + | ||
| 11 | +做一个软件项目的"黑灯工厂"(Lights-Out Software Factory):给定一个目标(Goal),系统 7×24 小时自主完成规划、编码、验证、迭代的完整闭环,无需人工干预即可持续产出可用软件,并具备自我纠错、自我改进的能力(迭代到 skill / 流程本身,而不仅是迭代代码)。 | ||
| 12 | + | ||
| 13 | +自主性对应 Anthropic/行业常用的 Agent 自主性分级(Level 5 = 完全自主运行,人类只设定目标和护栏,不参与执行环节)。这意味着系统需要具备: | ||
| 14 | + | ||
| 15 | +- 目标到任务的自主分解能力(Planning) | ||
| 16 | +- 任务到产出的自主执行能力(Coding) | ||
| 17 | +- 产出到质量判定的自主验证能力(Validate / Benchmark / Test) | ||
| 18 | +- 失败到根因到改进的自主学习能力(Iterate,包括改代码,也包括改 skill / 流程本身) | ||
| 19 | + | ||
| 20 | +## 2. 核心概念与术语 | ||
| 21 | + | ||
| 22 | +| 术语 | 定义 | | ||
| 23 | +|---|---| | ||
| 24 | +| **Org Graph(Organizer Graph)** | 固定的、由人预先设计的顶层多 Agent 主流程图。节点是"角色"(Planner / Coder / Validator),边是角色间的移交(handoff)与回退(loop-back)关系。一个 Org Graph 定义一类"工厂"(如 Dev Organizer)。 | | ||
| 25 | +| **Work Graph** | Org Graph 中某个节点 Agent 接到任务后,动态规划并生成的执行图。节点是"subagent + skill 的一次执行",边支持串行、并行 fan-out/fan-in、循环(loop)。每次任务执行都重新规划、重新生成,不复用上一次的图实例(但可以复用/学习历史图的经验)。 | | ||
| 26 | +| **Org Agent** | Org Graph 上的固定节点角色,例如 Planner Agent、Coder Agent、Validator Agent。每个 Org Agent 拥有一个"Planning Skill",用于在接到任务时生成自己的 Work Graph。 | | ||
| 27 | +| **Work Node** | Work Graph 中的最小执行单元 = 一个 Subagent 实例 + 它调用的一个(或一组)Skill + 明确的输入/输出契约。 | | ||
| 28 | +| **Skill** | 可复用的能力单元(提示词模板 + 工具集 + 产出规范),是 Work Node 执行任务时调用的"方法"。Skill 本身也是可被迭代的对象。 | | ||
| 29 | +| **Fan-out / Fan-in** | 任务分解为多个并行子任务分派给多个 Subagent(fan-out),执行完成后合并/裁决为单一结果(fan-in)。 | | ||
| 30 | +| **Reflection** | Work Node 或调度子 Agent 在执行受阻/产出低质时,对已有产出与目标的差距进行自我评估并决定重试/改路径/升级人工介入的动作。 | | ||
| 31 | +| **Supervisor(调度子 Agent)** | 存在于部分 Work Graph 中(当前明确用于 Coder 的 Work Graph),负责监控其余 Work Node 的运行状态、发现卡点、触发 reflection 或重新调度,直至该层任务完成。 | | ||
| 32 | +| **Artifact** | Work Graph 执行过程中产出的结构化产物,如 BRD、PRD、SDD、代码、测试报告、bug list。是节点间"共享结果数据"的载体。 | | ||
| 33 | +| **Bug / Issue 溯源** | Validator 发现问题后,向上追溯问题源头属于 Planning 阶段的哪个环节,还是 Coder 阶段的哪个环节,并进一步判断是"这次执行的偶发失误"还是"Skill 本身存在缺陷"。 | | ||
| 34 | +| **Org-level Loop** | Org Graph 层面的回退边:Validator → Planner,把验证阶段发现的问题作为新任务反馈给 Planner,驱动下一轮 Org Graph 循环。 | | ||
| 35 | + | ||
| 36 | +## 3. 整体架构:两层图 | ||
| 37 | + | ||
| 38 | +``` | ||
| 39 | +┌─────────────────────────────────────────────────────────────┐ | ||
| 40 | +│ Org Graph(固定) │ | ||
| 41 | +│ │ | ||
| 42 | +│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ | ||
| 43 | +│ │ Planner │ ───▶ │ Coder │ ───▶ │ Validator │ │ | ||
| 44 | +│ │ Agent │ │ Agent │ │ Agent │ │ | ||
| 45 | +│ └───────────┘ └───────────┘ └─────┬─────┘ │ | ||
| 46 | +│ ▲ │ │ | ||
| 47 | +│ └───────────── bug / 迭代任务 ───────────┘ │ | ||
| 48 | +└─────────────────────────────────────────────────────────────┘ | ||
| 49 | + │ 接到任务后各自触发 │ │ | ||
| 50 | + ▼ ▼ ▼ | ||
| 51 | +┌───────────────┐ ┌───────────────┐ ┌───────────────┐ | ||
| 52 | +│ Planner 的 │ │ Coder 的 │ │ Validator 的 │ | ||
| 53 | +│ Work Graph │ │ Work Graph │ │ Work Graph │ | ||
| 54 | +│ (动态生成) │ │ (动态生成) │ │ (动态生成) │ | ||
| 55 | +│ BRD→PRD→SDD │ │ to-do 拆解→ │ │ validate/test/ │ | ||
| 56 | +│ →Benchmark │ │ 编码→reflection │ │ benchmark→ │ | ||
| 57 | +│ (含 fan-out) │ │ (含 supervisor) │ │ 根因溯源→bug记录 │ | ||
| 58 | +└───────────────┘ └───────────────┘ └───────────────┘ | ||
| 59 | +``` | ||
| 60 | + | ||
| 61 | +**关键设计原则**: | ||
| 62 | + | ||
| 63 | +1. Org Graph 结构人工定义、代码级固定,改动 Org Graph 本身属于"架构变更",需要人工评审,不由系统自主修改。 | ||
| 64 | +2. Work Graph 每次任务全新规划生成,不假设可复用上次的图结构,但**规划过程可以参考历史 Work Graph 执行记录**(作为 few-shot 或经验库输入)以提升规划质量。 | ||
| 65 | +3. 两层图都是**图**(不是线性流程),因此都需要统一的图执行语义:节点状态机、边的条件、fan-out/fan-in 的合并策略、loop 的终止条件。 | ||
| 66 | +4. Org Agent 与 Work Node 中的 Subagent 复用同一套底层执行能力,区别只在于"图的层级"和"节点的职责范围"。 | ||
| 67 | + | ||
| 68 | +## 4. Org Graph 层设计(Dev Organizer 实例) | ||
| 69 | + | ||
| 70 | +MVP 范围内的 Org Graph 固定为三节点:**Planner → Coder → Validator**,以及一条回退边 **Validator → Planner**。 | ||
| 71 | + | ||
| 72 | +### 4.1 节点职责与边界 | ||
| 73 | + | ||
| 74 | +| 节点 | 输入 | 输出 | 职责边界 | | ||
| 75 | +|---|---|---|---| | ||
| 76 | +| **Planner Agent** | Goal(自然语言目标)或 Validator 回传的 bug/迭代任务 | BRD、PRD、SDD、Benchmark & 测试用例集 | 只负责"定义要做什么、做到什么标准",不写实现代码,不做验证执行。 | | ||
| 77 | +| **Coder Agent** | Planner 产出的 BRD/PRD/SDD | 代码变更(commit/PR)、单元测试、Coder 自测报告 | 只负责"按规格实现并做第一层自测(reflection + 基础 test)",不负责最终验收判定,不负责重新定义需求。 | | ||
| 78 | +| **Validator Agent** | Coder 产出的代码 + Planner 产出的 Benchmark/测试用例 + SDD/PRD | 验收结论(通过/不通过)、bug list(含根因归属)、必要时的 Skill 迭代建议 | 只负责"判定是否达标 + 定位问题归属",不直接改代码、不直接改需求文档,而是把问题结构化后交回 Org Graph 循环。 | | ||
| 79 | + | ||
| 80 | +### 4.2 节点间数据契约(Artifact 规范) | ||
| 81 | + | ||
| 82 | +为了让三个节点能"共享结果数据",需要为每种 Artifact 定义 schema,至少包括: | ||
| 83 | + | ||
| 84 | +- BRD:目标、背景、成功标准(可衡量) | ||
| 85 | +- PRD:功能范围、用户场景、边界条件(做什么/不做什么) | ||
| 86 | +- SDD:架构方案、模块划分、接口约定、技术选型理由 | ||
| 87 | +- Benchmark & 测试用例集:可执行的验收标准(不是文字描述,是可跑的测试/评测脚本或数据集) | ||
| 88 | +- Bug/Issue 记录:问题描述、复现步骤、根因归属(Planning 阶段 / Coder 阶段 / Skill 缺陷)、关联的原始 Artifact 版本 | ||
| 89 | + | ||
| 90 | +这些 Artifact 需要有版本和血缘关系(例如:本次代码是基于哪个版本的 SDD 写的,本次 bug 是针对哪个版本的代码和哪个版本的测试用例)。这是 Validator 做根因溯源的前提。 | ||
| 91 | + | ||
| 92 | +### 4.3 Org-level Loop(顶层回退机制) | ||
| 93 | + | ||
| 94 | +Validator 完成一轮验证后: | ||
| 95 | + | ||
| 96 | +- 若通过 → 本轮任务结束,等待/接收下一个外部 Goal。 | ||
| 97 | +- 若不通过 → 生成结构化 bug 任务,回传给 Planner。Planner 接到后需要判断: | ||
| 98 | + - 这是需求理解错误(PRD/SDD 需要修订)→ 走完整的 Planner Work Graph 修订相关 Artifact。 | ||
| 99 | + - 这是纯实现错误、需求本身没问题 → **快速通道(已确认,见 8.2)**:Org Graph 固定存在一条 Validator → Coder 的条件边,由 Validator 在 bug 记录中标注 `route: fast_lane | full_replan` 决定走向,Coder 直接消费 bug 记录中的修复指引,不需要 Planner 重新生成完整 BRD/PRD/SDD。 | ||
| 100 | +- **循环终止条件(已确认,见 8.3)**:同一任务的 Org-level Loop 最多 3 轮。第 3 轮仍未收敛 → 该任务标记为 `blocked_pending_human`,触发人工升级通知,同时 Org Graph 调度器跳过该任务,优先执行队列中其他可并行的任务,不阻塞整体产出。 | ||
| 101 | + | ||
| 102 | +## 5. Work Graph 层:动态生成与执行机制 | ||
| 103 | + | ||
| 104 | +每个 Org Agent 拥有一个 **Planning Skill**,在接到任务时先生成本轮的 Work Graph,再按图执行。Work Graph 的生成本身也是一次 Agent 调用(用规划能力"画图"),产出应是结构化的图描述(节点、边、节点的 skill 绑定、fan-out/fan-in 策略、合并/验证规则),而不是自然语言计划。 | ||
| 105 | + | ||
| 106 | +### 5.1 Work Graph 的图元素 | ||
| 107 | + | ||
| 108 | +- **串行边**:B 节点必须等 A 节点产出特定 Artifact 后才能开始。 | ||
| 109 | +- **Fan-out**:一个节点的产出被拆解为多个独立子任务,分派给多个 Subagent 并行执行(各自可能调用不同 skill)。 | ||
| 110 | +- **Fan-in**:多个并行分支的产出需要被合并/裁决为单一权威结果,需要定义合并策略(例如:加权综合、冲突时用哪个 Subagent 仲裁、或再起一个"合并 Agent")。 | ||
| 111 | +- **Loop**:节点或子图在未达标时重复执行,需要定义重试上限和"未达标"的判定条件。 | ||
| 112 | + | ||
| 113 | +### 5.2 Work Node 的构成 | ||
| 114 | + | ||
| 115 | +每个 Work Node = 1 个 Subagent + 1 个(或按需组合)Skill + 输入契约 + 输出契约 + 该节点自身的验证规则("这个节点怎么算做完/做对了")。 | ||
| 116 | + | ||
| 117 | +### 5.3 规划 → 执行 → 验证 → 交付 的通用生命周期 | ||
| 118 | + | ||
| 119 | +无论是 Planner、Coder 还是 Validator 的 Work Graph,都遵循同一个通用生命周期,这也是需要在 Graph Engine 中抽象出的公共能力: | ||
| 120 | + | ||
| 121 | +1. **Plan**:Org Agent 调用 Planning Skill,结合任务输入和历史经验,生成 Work Graph 定义(节点、边、fan-out/fan-in、验证规则)。 | ||
| 122 | +2. **Execute**:Graph Engine 按依赖关系调度 Work Node 执行,支持并行、支持节点内 reflection 重试。 | ||
| 123 | +3. **Merge/Verify**:fan-in 节点合并结果;每个关键节点/整图完成后有一个验证步骤,判定"这个 Org Agent 的任务是否算完成"。 | ||
| 124 | +4. **Handoff**:验证通过后,将最终 Artifact 交付给 Org Graph,触发下一个 Org 节点。若验证不通过且重试耗尽,升级(reflection 升级为 Org 级别的异常,可能触发人工介入护栏)。 | ||
| 125 | + | ||
| 126 | +## 6. 三个 Org 节点的 Work Graph 详细设计 | ||
| 127 | + | ||
| 128 | +### 6.1 Planner 的 Work Graph | ||
| 129 | + | ||
| 130 | +**输入**:Goal(或 Validator 回传的 bug 任务) | ||
| 131 | +**产出**:BRD → PRD → SDD → Benchmark & 测试用例集 | ||
| 132 | + | ||
| 133 | +- 串行主干:BRD → PRD → SDD → Benchmark/测试用例(后者依赖前者的确定版本)。 | ||
| 134 | +- Fan-out 候选点: | ||
| 135 | + - PRD 阶段:若目标涉及多个功能模块/多个用户场景,可 fan-out 让多个 Subagent 分别撰写各场景的 PRD 片段,再 fan-in 合并为完整 PRD,并做一致性检查(是否有相互矛盾的场景假设)。 | ||
| 136 | + - SDD 阶段:若涉及多个技术方案候选(如"用状态机实现 vs 用简单二维数组实现贪吃蛇"),可 fan-out 生成多个方案由一个评审节点做技术选型裁决,再 fan-in。 | ||
| 137 | + - Benchmark/测试用例阶段:功能测试、边界测试、性能 benchmark 可以分给不同 Subagent 并行生成,再 fan-in 汇总为统一测试集,并做覆盖率/冲突检查。 | ||
| 138 | +- 每一步的验证规则示例:BRD 是否包含可衡量的成功标准;PRD 是否覆盖 BRD 的每一条目标且无矛盾场景;SDD 是否对 PRD 每个功能点都有对应模块/接口;测试用例是否可执行、是否覆盖 PRD 的边界条件。 | ||
| 139 | +- Planner Work Graph 整体完成的判定:四类 Artifact 齐备且通过各自的验证规则,且相互之间的引用关系(血缘)完整可追溯。 | ||
| 140 | +- 完成后,Planner Agent 将这一组 Artifact 交付给 Org Graph,触发 Coder 节点。 | ||
| 141 | + | ||
| 142 | +### 6.2 Coder 的 Work Graph | ||
| 143 | + | ||
| 144 | +**输入**:BRD/PRD/SDD(+ 若是 bug 迭代任务,还包括 Validator 的 bug 记录) | ||
| 145 | +**产出**:代码变更、单元测试、自测报告 | ||
| 146 | + | ||
| 147 | +- Planning 阶段先生成 to-do list 并排序(依赖关系:哪些模块要先实现,哪些可以并行)。 | ||
| 148 | +- 每个 to-do 项对应一个或一组 Work Node,节点内部是一个"小闭环":编码 → reflection(自我审查是否符合 SDD/PRD)→ 本地验证(unit test,必要时 integration test)→ 若不通过则在节点内重试,重试耗尽则标记为阻塞并上报。 | ||
| 149 | +- Fan-out:相互无依赖的模块可并行分派给多个 Coder Subagent。 | ||
| 150 | +- **Supervisor(调度子 Agent)**:是 Coder Work Graph 的常驻节点,职责是监控所有并行/串行 Work Node 的执行状态,发现卡点(例如某节点反复 reflection 失败、依赖阻塞)后介入——可以是重新调度、调整任务边界、或将问题升级为"需要 Planner 澄清需求"的信号(这是一个 Work Graph 内部节点触发 Org Graph 级别信号的例外通路,需要专门定义)。 | ||
| 151 | +- Fan-in:各模块实现完成后,需要一个集成节点做整体 integration test,确认模块间接口按 SDD 约定对齐。 | ||
| 152 | +- Coder Work Graph 完成判定:to-do list 全部完成,integration test 通过,自测报告生成。完成后交付给 Validator 节点。 | ||
| 153 | + | ||
| 154 | +### 6.3 Validator 的 Work Graph | ||
| 155 | + | ||
| 156 | +**输入**:代码 + Benchmark/测试用例集 + PRD/SDD(用于溯源引用) | ||
| 157 | +**产出**:验收结论、结构化 bug list(含根因归属)、必要时的 Skill 迭代建议 | ||
| 158 | + | ||
| 159 | +- 主干节点:执行功能测试 → 执行 benchmark → 汇总结果。可 fan-out(功能测试、性能 benchmark、边界/异常测试并行执行)。 | ||
| 160 | +- **根因溯源节点(核心机制)**:出现失败项时,不止记录"哪里错了",还要往上游追溯: | ||
| 161 | + 1. 读取该功能点对应的 SDD/PRD/BRD 版本和 Coder 的实现记录(血缘追溯)。 | ||
| 162 | + 2. 判断失败属于哪一层:需求定义缺失/矛盾(Planning 问题)?规格清楚但实现不符(Coder 问题)?测试用例本身有误(Benchmark/测试用例问题)? | ||
| 163 | + 3. 进一步判断是"这次执行的偶发失误"还是"背后的 Skill 存在系统性缺陷"(例如:某个 Skill 反复在同类任务上出现相同类型的问题,说明 Skill 的提示词/流程本身有缺口)。 | ||
| 164 | + 4. 若判定为 Skill 缺陷,生成 Skill 迭代建议(更新对应 Skill 的定义),确保"从根上解决问题,下次不再复现",而不仅是修一次代码。 | ||
| 165 | +- Bug list 结构化输出:问题描述、复现方式、根因归属(Planning / Coder / Skill)、关联 Artifact 版本、建议的处理路径(回给 Planner 修 PRD/SDD,还是直接给 Coder 修代码,还是先迭代 Skill 再重跑)。 | ||
| 166 | +- Validator Work Graph 完成判定:所有测试/benchmark 项都有明确结论(通过或已归档为带根因的 bug),bug list 已结构化生成。完成后按 Org Graph 定义,将结果交付给 Org-level Loop(若不通过,回传给 Planner)。 | ||
| 167 | + | ||
| 168 | +## 7. 执行引擎与底层能力抽象 | ||
| 169 | + | ||
| 170 | +### 7.1 Graph Engine(自研) | ||
| 171 | + | ||
| 172 | +由于两层图(Org Graph、Work Graph)共享同一套图执行语义,建议自研一个统一的 Graph Engine,核心能力包括: | ||
| 173 | + | ||
| 174 | +- 图定义:节点、边、边的条件表达式、fan-out/fan-in 分组、loop 分组与终止条件。 | ||
| 175 | +- 状态机:每个节点具备 pending / running / reflecting / retrying / blocked / done / failed 等状态,图整体也有对应的聚合状态。 | ||
| 176 | +- 调度器:按依赖关系触发节点执行,支持并行节点的并发调度与资源限制(并发数、超时)。 | ||
| 177 | +- Fan-in 合并策略:可插拔的合并/裁决逻辑(如仲裁 Agent、规则合并、投票)。 | ||
| 178 | +- 断点续跑与持久化:图执行状态、节点产出、图定义本身都需要持久化,支持从任意断点恢复(对 7×24 自主运行是硬需求,不能因为进程重启就丢失进度)。 | ||
| 179 | +- 可观测性钩子:每个节点的输入/输出/耗时/重试次数/reflection 记录都要可查询,这是 Validator 做根因溯源、也是人工审计"黑灯工厂"运行状况的基础。 | ||
| 180 | +- Org 级别与 Work 级别复用同一套 Engine API,区别仅是节点内部绑定的执行体不同(Org 节点绑定 Org Agent,Work 节点绑定 Subagent+Skill)。 | ||
| 181 | + | ||
| 182 | +### 7.2 底层 Agent 执行器抽象层 | ||
| 183 | + | ||
| 184 | +节点的具体执行(调用 LLM、跑工具)需要通过一层执行器抽象(Executor Interface),Graph Engine 不直接绑定某个具体的底层 CLI/协议,而是面向接口编程。 | ||
| 185 | + | ||
| 186 | +**执行后端范围(决策,2026-07-29,取代此前"双后端"框架)**:MVP 阶段只实现并使用 **Claude Code ACP**(Agent Client Protocol)一种后端,不考虑 Hermes CLI 作为执行后端。此前设想的"Org Graph 层两后端一致、Work Graph 层两后端不对等"框架不再适用——不存在第二个后端,自然也不存在不对等问题。Executor Interface 的抽象层设计仍然保留(为未来接入其他 Agent 运行时留出扩展点),但 MVP 阶段只需要交付 Claude Code ACP 这一个实现,不需要再为"双后端一致性"做额外设计和测试。 | ||
| 187 | + | ||
| 188 | +**Org Graph 层的具体落地方式**:Planner/Coder/Validator 三个 Org Agent 直接实现为 Claude Code 的 **project-scoped subagent 定义文件**——目标项目仓库下的 `.claude/agents/{planner,coder,validator}.md`。这是 Claude Code 原生支持、每个 agent 独立配置的机制,完整覆盖 Hermes profile 原先的五个维度: | ||
| 189 | + | ||
| 190 | +- **身份(对应 SOUL.md)**:subagent 文件的 Markdown 正文就是系统提示词,是完全独立的人设定义(不叠加在主会话人设上) | ||
| 191 | +- **memory**:`memory: project` frontmatter 字段,对应目标项目 `.claude/agent-memory/{planner,coder,validator}/`,随项目 Git 历史版本化 | ||
| 192 | +- **skills**:`skills:` 字段预加载技能全文,运行时还能通过 Skill 工具按需调用其他技能 | ||
| 193 | +- **tools**:`tools`/`disallowedTools` 字段做精确白名单/黑名单,外加 `mcpServers` 字段挂载专属 MCP server | ||
| 194 | +- **model(LLM 配置的一部分)**:`model` 字段可按 agent 单独锁定,对应第 10 节"按节点分层选模型";但供应商/API key 这部分**不能**做到 per-agent 独立——整个 Claude Code 进程共享同一份认证,这是和 Hermes profile"每个 agent 独立 LLM API config"的唯一差距点,MVP 阶段接受这个限制 | ||
| 195 | + | ||
| 196 | +触发方式:`claude --agent planner -p "..."`,让整个会话以该 agent 身份运行,语义上对应 Org Graph"三个固定角色"的设计,和 Work Graph 层已验证的 `claude -p` 调用方式(见第 10.1 节、A-1 spike)是同一套机制,`c_org_node_trigger` 的实现可以直接复用。 | ||
| 197 | + | ||
| 198 | +### 7.3 数据与产物存储(已确认,见 8.1 / 8.4) | ||
| 199 | + | ||
| 200 | +Artifact、代码、Skill、Memory、Agent 人设文件全部纳入**同一 Git 仓库**管理,用目录区隔不同 Agent 的工作区和产物,所有变更都必须走 Git 提交(含系统自主变更),形成完整、可回滚的版本历史。参考目录结构: | ||
| 201 | + | ||
| 202 | +``` | ||
| 203 | +graphlemon/ | ||
| 204 | +├── org/ # Org Graph 定义(人工维护,变更需人工评审) | ||
| 205 | +│ └── dev-organizer.graph.yaml | ||
| 206 | +├── .claude/ | ||
| 207 | +│ ├── agents/ # 各 Org Agent 的 Claude Code subagent 定义(决策,2026-07-29,见 7.2 节) | ||
| 208 | +│ │ ├── planner.md # frontmatter: memory:project, skills:[...], tools:[...], model:...;正文迁移自 ~/.hermes/profiles/planning/SOUL.md | ||
| 209 | +│ │ ├── coder.md # 正文迁移自 ~/.hermes/profiles/code/SOUL.md | ||
| 210 | +│ │ └── validator.md # 正文迁移自 ~/.hermes/profiles/qa/SOUL.md + AGENTS.md(工作区权限契约折入 tools/disallowedTools) | ||
| 211 | +│ ├── agent-memory/ # 各 Agent 的持久化 memory(memory:project 字段自动写入,随本仓库 Git 历史版本化) | ||
| 212 | +│ │ ├── planner/ | ||
| 213 | +│ │ ├── coder/ | ||
| 214 | +│ │ └── validator/ | ||
| 215 | +│ └── workflows/ # Work Graph 执行层的动态工作流脚本(见第 10.1 节、03-C 3.5 节) | ||
| 216 | +├── skills/ # Skill 库(可被 Validator 触发的迭代改写,均走 git commit) | ||
| 217 | +│ ├── write-brd/ | ||
| 218 | +│ ├── write-prd/ | ||
| 219 | +│ ├── write-sdd/ | ||
| 220 | +│ ├── write-benchmark/ | ||
| 221 | +│ ├── code-implement/ | ||
| 222 | +│ ├── code-reflect/ | ||
| 223 | +│ ├── run-test/ | ||
| 224 | +│ ├── run-benchmark/ | ||
| 225 | +│ └── root-cause-trace/ | ||
| 226 | +├── runs/ # 每次任务的 Work Graph 执行记录与产物快照 | ||
| 227 | +│ └── <task_id>/ | ||
| 228 | +│ ├── brd.md / prd.md / sdd.md / benchmark/ | ||
| 229 | +│ ├── src/ # 本次任务对应的代码变更 | ||
| 230 | +│ ├── tests/ | ||
| 231 | +│ └── bugs/ | ||
| 232 | +└── src/ # 项目主代码(最终交付代码的主干) | ||
| 233 | +``` | ||
| 234 | + | ||
| 235 | +- **Skill 自主迭代(已确认,见 8.1)**:允许系统在 Validator 判定 Skill 存在缺陷后自主改写对应 `skills/*` 目录下的定义并立即生效,但每次改动必须产生一次独立 Git 提交(commit message 需包含触发该次迭代的 bug/任务 ID),保证可追溯、可回滚。这是黑灯工厂自主性与风险控制的平衡点:不设人工审批卡点,但设"事后可审计、可一键回退"的安全网。 | ||
| 236 | +- 执行记录(Graph Engine 的节点状态转移、reflection 记录)建议独立于 Git 管理的 Artifact,用结构化存储(数据库/日志系统)承载,Git 只保留每个任务的最终产物快照(`runs/<task_id>/`),避免高频状态变更污染提交历史。 | ||
| 237 | + | ||
| 238 | +### 7.4 任务队列与并发调度(已确认,见 8.3 / 8.5) | ||
| 239 | + | ||
| 240 | +- 系统需要维护一个**任务队列**而非单一任务指针:一个 Goal 分解出的多个可并行子任务、以及被 3 轮未收敛而"跳过"的任务,都需要作为队列中的独立条目被追踪,状态至少包括 `queued / running / blocked_pending_human / done`。 | ||
| 241 | +- 调度器在遇到 `blocked_pending_human` 的任务时不阻塞整体流水线,转而调度队列中其他 `queued` 且依赖已满足的任务继续执行。 | ||
| 242 | +- **多 Goal 并发(已确认,见 8.5)**:架构上支持同时运行多个独立 Goal(多个 Org Graph 实例),并发数是一个可配置参数,在管理后台设置。MVP 阶段该参数默认设为 `1`(单任务串行),后续可直接调大,无需改架构。 | ||
| 243 | + | ||
| 244 | +### 7.5 预算与成本护栏(已确认,见 8.6) | ||
| 245 | + | ||
| 246 | +- 护栏分两层: | ||
| 247 | + 1. **平台层限额**:依赖 Claude Code 自身的 coding plan 用量机制(5 小时滚动窗口 / 7 天窗口 / Opus 专属限额),执行器抽象层需要能识别额度耗尽的信号并向上传递。**这一层的处理方式不是"暂停等人工",而是"等待+自动重试"(决策,2026-07-29,详见 03-C 3.6/3.5 节、04-P)**:Claude Code plan 额度是一个会在固定时间窗口后自然恢复的资源,属于自愈型阻塞,不应该占用人工介入的通道;只有等待超过合理上限仍未恢复时才升级为人工介入,防止真正的账单/账号问题被无限期静默等待掩盖。 | ||
| 248 | + 2. **手工配置层**:管理后台允许人工设置额外的预算/额度上限(如按任务、按天、按 Goal 设置 token 或调用次数上限),超出后自动暂停对应任务并通知人工,行为与 Org-level Loop 的 `blocked_pending_human` 一致,复用同一套人工升级通知机制。这一层和上面的"平台层限额"是两回事:手工配置层是主动的预算控制(超限就该停下来问人),平台层限额是被动的资源约束(等它自己恢复就行),不能用同一套处理逻辑。 | ||
| 249 | + | ||
| 250 | +## 8. 已确认的架构决策 | ||
| 251 | + | ||
| 252 | +以下问题已与用户对齐,作为后续 PRD/SDD 的既定前提: | ||
| 253 | + | ||
| 254 | +1. **Skill 自主迭代边界(已确认)**:允许系统自主改写 Skill 定义并立即生效,前提是所有变更(Skill、代码、Work Graph 产物、Memory、`SOUL.md`/`AGENTS.md` 等人设与运行契约文件)都必须通过 Git 提交留痕,形成完整版本记录,可追溯、可回滚。详见 7.3。 | ||
| 255 | +2. **Org Graph 快速通道(已确认)**:允许存在 Validator → Coder 的快速通道,由 Validator 在 bug 记录中判断并标注是否为"纯实现问题"从而决定是否绕过 Planner 的完整重新规划。详见 4.3。 | ||
| 256 | +3. **循环终止与人工升级(已确认)**:轮次上限设为 3 轮,超过未收敛则标记 `blocked_pending_human`、通知人工介入,同时调度器跳过该任务、优先执行队列中其他可并行任务。详见 4.3 / 7.4。 | ||
| 257 | +4. **Artifact 与代码同仓库管理(已确认)**:BRD/PRD/SDD/bug list 等 Artifact 与代码纳入同一 Git 仓库,用目录区隔不同 Agent 的工作区和产物。详见 7.3 的目录结构。 | ||
| 258 | +5. **多任务并发(已确认)**:架构支持多 Goal 并发运行,MVP 阶段并发数参数设为 1,可在管理后台调整。详见 7.4。 | ||
| 259 | +6. **预算/额度护栏(已确认)**:结合 Claude Code 自身的 plan 额度限制(等待+自动重试,非人工介入),以及管理后台的手工配置上限(主动预算控制,超限转人工介入),双层护栏,两层处理逻辑不同。详见 7.5。 | ||
| 260 | +7. **Skill 库初始范围(已调研,方案见第 9 节)**:MVP 只做开发全流程的 Dev Organizer(Planner / Coder / Validator 三个 Org Agent)。已读取本地 Hermes profile(`~/.hermes/profiles/{code,planning,qa}`),三者已针对同一条"飞书 Base 驱动的软件研发流水线"做了扎实的角色分工与人设定制,可作为人设与方法论蓝本直接复用;但其任务队列/状态机强耦合飞书多维表格 + `lark-cli`,且是"轮询认领任务"模式而非"动态生成 Work Graph 执行"模式,需要改造。具体映射和缺口见第 9 节。 | ||
| 261 | + | ||
| 262 | +## 9. 现有 Hermes Profile 复用与改造方案 | ||
| 263 | + | ||
| 264 | +对 `~/.hermes/profiles/{code, planning, qa}` 的调研结论:三个 profile 不是换名字的同一个通用助理,而是已经围绕"飞书 Base 驱动的软件研发流水线"做了具体的角色分工,人设文件为 `SOUL.md`(qa 另有 `AGENTS.md` 补充运行契约),可直接作为 Planner/Coder/Validator 的人设与方法论蓝本。 | ||
| 265 | + | ||
| 266 | +### 9.1 角色映射与可直接复用的资产 | ||
| 267 | + | ||
| 268 | +| Org Agent | 对应本地 Profile | 可复用人设 | 可复用 Skill(举例) | 与本文档章节的对应关系 | | ||
| 269 | +|---|---|---|---|---| | ||
| 270 | +| **Planner** | `planning` | `SOUL.md`:16 条根本原则(先调查后规划、事实/决策/假设分层、纵向拆分、依赖显式无环、失败关闭、动态 Runtime Graph 等),明确"不写业务代码" | `planning-requirement-expansion`(主方法)、`project-brd-planning`、`project-prd-planning`、`project-sdd-planning`、`project-unit-decomposition`、`sdd-automation-runtime`、`sdd-generation-audit-hardening`、`review-blocker-verification` 等约 20 个规划子技能 | 直接对应第 6.1 节 BRD→PRD→SDD→测试用例四步串行主干,比本文档原先设想的"从零设计"更成熟,`project-unit-decomposition` 可直接用作 fan-out 拆分依据 | | ||
| 271 | +| **Coder** | `code` | `SOUL.md`:单任务开发主流程人设——每轮认领一条需求叶子任务或 Bug,唯一事实源、写单测、证据回写描述,禁止跳过测试或自称 QA 通过 | `code-task-development`(主方法:认领→实现→测单→证据回写)、`code-agent-engineering`、`debugging-toolkit`、`software-planning-execution` | 对应第 6.2 节的 to-do 闭环(编码→reflection→本地验证),"证据回写"机制可直接作为 Coder Work Graph 每个节点的验证规则 | | ||
| 272 | +| **Validator** | `qa` | `SOUL.md`:独立、真实、可复核的验收者,只出 PASS/FAIL/INCONCLUSIVE 三态,Dev 证据只是线索必须独立复核;`AGENTS.md`:工作区权限契约(`doc/` 只读、`source/` 只读审计+执行测试) | `qa-task-validation`(主方法:风险驱动测试、双向可追溯矩阵、严格放行门)、`unattended-agent-operations`、`software-quality` | 双向可追溯矩阵机制正好对应第 6.3 节的根因溯源节点,可直接复用作为 Validator 判定"Planning/Coder/Skill 归属"的方法论基础 | | ||
| 273 | + | ||
| 274 | +三个 profile 目前共享的记忆结构(`memories/MEMORY.md` + `USER.md`,`sessions/` 存原始请求日志)内容有重叠、未按角色隔离,不建议直接复用,需按第 7.3 节的 `memory/{planner,coder,validator}/` 目录重新组织。 | ||
| 275 | + | ||
| 276 | +三个 profile 的 `skills/` 顶层还挂载着大量与开发流程无关的通用/归档技能(`.archive/`、`migration/`、`openclaw-imports/` 以及 apple、creative、feishu-lark、social-media 等个人助理类技能),不纳入 graphlemon,避免噪音。 | ||
| 277 | + | ||
| 278 | +### 9.2 需要改造的三个缺口 | ||
| 279 | + | ||
| 280 | +1. **任务队列/状态机迁移(已确认,见 12.1)**:现有机制强耦合"飞书多维表格 + `lark-cli --as user`"作为需求表/Bug 表和任务认领的唯一事实源。**已确认完全替换为 Graph Engine 自研持久化**(Git + 结构化存储,见 7.3/7.4 节),不再依赖飞书 Base,迁移时需要另做管理后台承载原来飞书 Base 提供的人工可视化能力。 | ||
| 281 | +2. **从"轮询认领"改造为"图执行"**:现有模式是"每轮 cron 认领一条任务"的轮询循环,SOUL.md 里虽提到"动态执行图"(Runtime Graph)但目前只是文字描述的原则,没有第 7.1 节所需的可执行 Graph/Node 结构化定义(节点、边、fan-out/fan-in、验证规则)。需要把这些 prose 原则转译为结构化 Work Graph 模板。 | ||
| 282 | +3. **Skill 打包与去噪**:需要把上表列出的专属 skill 从各 profile 中单独抽取,迁移到第 7.3 节定义的 `graphlemon/skills/` 目录下,剥离掉归档/迁移/通用助理技能。 | ||
| 283 | + | ||
| 284 | +## 10. 对照《Graph Engineering with Claude: 14-Step roadmap》课程的映射与启发 | ||
| 285 | + | ||
| 286 | +用户提供的这份课程讲的是 Claude Code 原生的"动态工作流"(dynamic workflows)能力:Claude 把任务编排写成一段普通 JavaScript 脚本(`agent()` / `parallel()` / `pipeline()` 等原语),再生成一批子智能体去执行这段脚本,**编排本身不消耗 token,因为它是代码,不是对话**。这与我们对 Work Graph 的设想(动态生成、串行/并行/循环、节点=subagent+skill)高度同构,课程给出的是更具体、已经落地验证过的工程细节。逐条对照如下: | ||
| 287 | + | ||
| 288 | +| # | 课程技法 | 一句话要点 | 对 graphlemon 的启发 | | ||
| 289 | +|---|---|---|---| | ||
| 290 | +| 1 | Nodes are jobs, edges are what flows | 边只在数据真正流动时存在,"然后"不等于边 | 检查我们 Planner/Coder/Validator 的 Work Graph 里是否有"顺序上的然后"被误画成边——真正的边必须是下一节点读取上一节点的输出 | | ||
| 291 | +| 2 | 线性脚本是退化的图 | 对每条箭头问"下一步是否读取上一步的输出",能拆的链要拆宽 | 重新审视 6.1 节 BRD→PRD→SDD→Benchmark 这条串行主干,看是否有环节其实互不依赖、可以拆成并行节点 | | ||
| 292 | +| 3 | 给每个节点一个契约 | 有界输入、有界输出、恰好一个任务;用 JSON schema 强制校验,校验失败在工具调用层自动重试 | 直接补强第 5.2 节 Work Node 的"输入/输出契约":应落到具体的 schema 校验机制,而不只是文字约定,这也是 4.2 节 Artifact schema 的落地方式 | | ||
| 293 | +| 4 | 边是数据契约,且**零 token** | 简单的合并/去重/过滤是纯代码,不该派生一个 Agent 去做 | **重要修正**:我们 6.1/6.3 节设想的一些 fan-in 合并节点(PRD 片段合并、测试用例汇总)未必都需要"一个 Agent 仲裁",简单聚合应该是确定性代码边,只有真正需要判断力的合并才值得用 Agent | | ||
| 294 | +| 5 | Fan-out 用 `parallel()` | 是一个屏障;单个 thunk 失败 resolve 为 null 而不拖垮整批 | 呼应第 7.1 节需要的"节点失败隔离"机制,`.filter(Boolean)` 式的失败降级可以直接作为 Graph Engine 的默认行为 | | ||
| 295 | +| 6 | Fan-in 在屏障处 | 仅当某阶段真正需要全部结果时才用屏障 | 给第 5.1 节的 Fan-in 补充一条明确判断标准,避免不必要的屏障拖慢 Work Graph | | ||
| 296 | +| 7 | 菱形拓扑:split → work → merge | fan out → reduce(代码) → synthesize(Agent) 是标准形状 | 这正是我们 Planner/Coder/Validator 三者 Work Graph 里反复出现的模式,可以直接把"菱形"作为我们 Work Graph 模板的标准命名 | | ||
| 297 | +| 8 | 条件路由:判断用 Agent,路由用代码 | 分类交给智能体,走哪条边由确定性代码决定 | 我们的"快速通道"机制(Validator 打 `fast_lane`/`full_replan` 标签,Org Graph 用代码路由)已经隐含这个原则,应在文档里显式化 | | ||
| 298 | +| 9 | Edge 上挂 verifier:对抗式 / 多视角 / 评审团 | 三种具体的验证模式,比"验证"两个字可操作得多 | **直接可用于 6.3 节 Validator 的根因溯源节点**:多视角差异化校验(正确性/安全/可复现性)与我们"判断问题属于 Planning/Coder/Skill 哪一层"是同构的,可以把归因判断设计成一次多视角校验 | | ||
| 299 | +| 10 | 节点隔离:git worktree | 并行写文件的 Agent 用独立 git worktree 避免互相踩踏,再干净合并 | **6.2 节 Coder Work Graph 的遗漏点**:多个 Coder Subagent 并行改代码时需要 worktree 隔离,这也和我们 8.1 节"一切走 Git 提交"的决策直接衔接 | | ||
| 300 | +| 11 | 循环要收敛:loop-until-dry | 连续 K 轮无新发现才停止;去重要对比"全部见过的"而非"已确认的" | 对 4.3 节"3 轮上限"是一个有价值的补充方案:像 Validator 做"未知规模缺陷扫描"这类任务,用 loop-until-dry 比固定轮次更合理,值得作为新的开放点 | | ||
| 301 | +| 12 | 按节点分层选模型 | 有界重复的节点用便宜模型,综合/裁决节点用贵模型 | 应补进第 7.5 节预算护栏:Graph Engine 的 `agent()` 级别调用要支持按节点指定模型,这是控制成本的关键杠杆 | | ||
| 302 | +| 13 | 拓扑即成本延迟:`pipeline()` 优先于 `parallel()` | 默认流式无屏障,只有真正需要全量结果时才上屏障 | 补强第 7.1 节 Graph Engine 核心能力:明确区分"屏障并行"和"流式并行"两种原语,而不是笼统的"并行" | | ||
| 303 | +| 14 | 让 Claude 自己画图:动态工作流 / self-routing | 描述目标,Claude 自己生成编排脚本;可保存为 `.claude/workflows/` 里版本化、按名复用的脚本 | **对第 7.1 节"自研 Graph Engine"决策的重大启发,见下方新开放问题** | | ||
| 304 | + | ||
| 305 | +### 10.1 关键决策:Work Graph 执行层复用 Claude Code 原生动态工作流(已确认) | ||
| 306 | + | ||
| 307 | +第 14 条技法说明一个重要事实:**Claude Code 本身已经内置了一套 node/edge/state 的图执行机制**(`agent()`/`parallel()`/`pipeline()` 原语、schema 契约、条件路由、edge 上挂 verifier、loop-until-dry、按节点分层模型、`.claude/workflows/` 版本化脚本),这套东西和我们设想的"Org Agent 先 Planning 再生成 Work Graph 再执行"的核心机制高度重合,而且已经是 Claude Code 里可用的产品能力,不需要从零建。 | ||
| 308 | + | ||
| 309 | +**已确认:方案 A**——Work Graph 执行层直接复用 Claude Code 原生动态工作流(仅限 Claude Code ACP 后端)。graphlemon 自己的 Graph Engine 只负责 Org Graph 层的编排、跨 Org Agent 的持久化任务队列(第 7.4 节)、Git 版本化的 Skill 库(第 7.3 节)、7×24 断点恢复。Work Graph 内部的 fan-out/fan-in/loop/verifier 直接用 Claude Code 写的 JS 编排脚本执行,直接拿到"零 token 编排""按节点分层模型"等现成优化,开发量显著小于完全自研。 | ||
| 310 | + | ||
| 311 | +**由此产生的架构含义(已落到第 7.2 节执行器抽象层的设计里)**:最初设想这一段是在讨论"两个后端在 Work Graph 层不对等",但 2026-07-29 已决策 MVP 阶段只用 Claude Code ACP 一种后端(见 7.2 节),这个问题不再存在——Work Graph 的执行体就是 Claude Code 原生的 `agent()`/`parallel()`/`pipeline()` 编排脚本,Org Graph 层的执行体是 Claude Code 的 subagent 机制(`.claude/agents/*.md`),两层都在同一个后端上,不需要处理跨后端一致性问题。Executor Interface 的抽象设计仍然保留,是为未来扩展留的口子,不是当前要解决的问题。 | ||
| 312 | + | ||
| 313 | +第 10 节表格里那些具体设计准则(数据契约用 schema、简单聚合用零 token 代码边、菱形拓扑、loop-until-dry、按节点分层模型、pipeline 优先于 parallel、worktree 隔离并行写入)在方案 A 下大部分会直接由 Claude Code 的动态工作流原生提供,我们只需要在 Planning Skill 生成编排脚本时遵循这些准则即可,不需要自己实现底层机制。 | ||
| 314 | + | ||
| 315 | +**方案 A 的可行性已通过实测确认(2026-07-29)**:`agent()`/`pipeline()` 编排脚本可通过 `claude -p`(非交互式)调用,无人值守完成、返回结构化可解析结果,验证了本节决策的前提成立。详见 `spikes/a1-dynamic-workflows/` 与 SDD `03-C-控制与接口.md` 3.5 节的具体实现契约。 | ||
| 316 | + | ||
| 317 | +## 11. MVP 范围建议 | ||
| 318 | + | ||
| 319 | +- **Org Graph**:Planner → Coder → Validator 三节点 + Validator → Planner 回退边,固定实现。 | ||
| 320 | +- **场景**:以"贪吃蛇游戏"一类边界清晰、无外部依赖的小型任务作为打通全链路的验收场景。 | ||
| 321 | +- **验收标准**: | ||
| 322 | + - Planner 能自主产出结构化的 BRD/PRD/SDD/测试用例集,且四类 Artifact 通过一致性/覆盖率校验。 | ||
| 323 | + - Coder 能依据 SDD 自主完成编码,通过 Coder 自测(unit + integration test)。 | ||
| 324 | + - Validator 能自主运行 Benchmark/测试用例,对不通过项完成根因归属(Planning/Coder/Skill 三类之一),生成结构化 bug list。 | ||
| 325 | + - 完整跑通至少一次 Org-level Loop(引入一个人为植入的 bug,验证系统能自主定位根因并驱动 Planner/Coder 完成修复闭环)。 | ||
| 326 | + - Graph Engine 支持断点恢复:在执行中途中断进程后,能从持久化状态恢复继续执行。 | ||
| 327 | + - 执行器抽象层验证 Claude Code ACP 后端可用(MVP 阶段唯一后端,见 7.2 节 2026-07-29 决策),接口设计上预留未来接入其他底层引擎的能力,但不要求 MVP 阶段实现第二个后端。 | ||
| 328 | + - Claude Code plan 额度耗尽时,系统能自动等待并重试,不误判为需要人工介入的错误(见 7.5 节、03-C 3.6 节)。 | ||
| 329 | + | ||
| 330 | +## 12. 补充确认的决策 | ||
| 331 | + | ||
| 332 | +### 12.1 飞书 Base / `lark-cli` 任务队列的去留(已确认) | ||
| 333 | + | ||
| 334 | +本地 Hermes profile 现有的 code/planning/qa 三个 Agent 都以飞书多维表格为需求表/Bug 表的唯一事实源,用 `lark-cli --as user` 认领和回写任务状态。**已确认:完全替换为 Graph Engine 自研持久化**,不再依赖飞书 Base,任务队列、状态机、bug list 全部落在 Git 仓库 + 结构化存储(第 7.3/7.4 节)。原来飞书 Base 提供的人工可视化看板能力,需要在管理后台(第 8.5 节已提到的并发数配置界面)里一并补齐,作为后续 PRD 的一个明确子需求。 | ||
| 335 | + | ||
| 336 | +## 13. 人机交互控制台(Admin Console / Control Plane) | ||
| 337 | + | ||
| 338 | +之前第 8.5 节把这块简化成了"管理后台设置并发数",实际需求比这大得多:需要一个真正给人用的控制台,管理系统涉及的全部功能/数据/流程/图,提供实时看板看到任务动态运行过程,并且让人能选文件夹建项目、发起任务、反馈 bug、提出目标、检查执行过程。这是黑灯工厂的"驾驶舱",不是一个可有可无的配置页面,值得作为与 Org Graph Engine、Work Graph Engine、Skill/Artifact 存储并列的第四个核心子系统。 | ||
| 339 | + | ||
| 340 | +### 13.1 关键决策(已确认) | ||
| 341 | + | ||
| 342 | +1. **目标项目是独立代码空间**:graphlemon 从"只服务自己开发自己"升级为通用工厂平台——人选择一个本地文件夹(新建或已有 Git 仓库)作为"目标项目",Planner/Coder/Validator 在这个目标项目里工作,产出的代码直接落在该文件夹本身。graphlemon 平台自己的仓库只保留平台资产:Org/Work Graph 引擎、Skill 库、Agent 人设(`agents/`、`skills/`,见 7.3 节),不含任何目标项目的业务代码。 | ||
| 343 | +2. **MVP 只需单个目标项目**:数据模型里引入 `project` 实体(为将来多项目预留),但 MVP 阶段管理后台不需要做"项目列表/切换"这层完整交互,用户选一次文件夹跑起来即可,精力优先投入实时看板和人机交互本身。 | ||
| 344 | +3. **实时看板用 WebSocket 推送**:节点状态变化、Artifact 产出、bug 记录、Org-level Loop 路由决策、预算用量更新都需要真正的推送,不是轮询刷新。 | ||
| 345 | +4. **原始执行日志在控制台内直接可看,本地落盘 + 定时清理(已确认,修正原方案)**:原先设想的"日志不重复存储、深链回底层引擎会话记录"已被推翻。改为:Work Node 执行时的完整会话日志实时落盘到 graphlemon 平台自己的本地日志文件夹(不纳入 Git、不与目标项目仓库混在一起,因为需要定期清理),控制台可直接查看(运行中的节点支持实时滚动查看,已完成的节点支持静态查看),并提供可配置的保留天数,到期自动清理。 | ||
| 346 | +5. **控制台载体形态:本地 Web 应用 + 本地后端代拉系统对话框(已确认)**:控制台前端是普通网页,不做 Electron/Tauri 桌面应用。"选择文件夹"这类需要真实系统路径的操作,由本机常驻的后端进程(本来就要跑 `git`/执行器调用)代为拉起 OS 原生文件夹选择对话框(macOS 用 `osascript`、Windows/Linux 用对应平台的原生方案),把选中的绝对路径返回给前端,不依赖浏览器 File System Access API(该 API 在 Safari/Firefox 不支持,且拿到的是沙箱化的文件句柄而非可直接给后端 `git init`/`agent()` 调用使用的真实路径)。 | ||
| 347 | + | ||
| 348 | +### 13.2 目标项目内的目录约定 | ||
| 349 | + | ||
| 350 | +为了让目标项目的代码和 graphlemon 生成的规划类产物(BRD/PRD/SDD/Benchmark/Bug 记录)都能随目标项目自身的 Git 历史一起演进(呼应第 8 节决策 1"一切纳入 Git 版本化"),目标项目文件夹内新增一个隐藏子目录: | ||
| 351 | + | ||
| 352 | +``` | ||
| 353 | +<目标项目文件夹>/ | ||
| 354 | +├── .git/ | ||
| 355 | +├── .graphlemon/ # graphlemon 在该项目内生成的规划类产物,随项目自身仓库一起提交 | ||
| 356 | +│ └── runs/<task_id>/ | ||
| 357 | +│ ├── brd.md / prd.md / sdd.md / benchmark/ | ||
| 358 | +│ └── bugs/ | ||
| 359 | +├── src/ # 目标项目自己的正常源码目录(graphlemon 不规定具体结构,尊重目标项目原有约定) | ||
| 360 | +└── ...(目标项目原有的其他文件) | ||
| 361 | +``` | ||
| 362 | + | ||
| 363 | +graphlemon 平台自己的持久化(`task`/`work_graph_run`/`work_node_execution`/`execution_event` 等运行时状态,见后续 SDD)仍然是独立于目标项目 Git 历史的结构化存储,只是其中的字段会指向 `project.target_folder_path` 下的具体文件路径。 | ||
| 364 | + | ||
| 365 | +**原始执行日志的存放位置(第 13.1 节决策 4)**:与上面两类都不同,Work Node 执行时的原始会话日志既不进目标项目的 Git 仓库(会污染项目提交历史),也不进 graphlemon 平台仓库(体量大、需要定期清理,不适合 Git 版本化管理)。落在 graphlemon 平台自己的本地日志目录(如 `~/.graphlemon/logs/<project_id>/<task_id>/<run_id>/<node_exec_id>.jsonl`),由独立的保留策略(可配置天数)定时清理,与 Git 版本化的 Artifact/Skill/Agent 人设是两套不同生命周期的存储。 | ||
| 366 | + | ||
| 367 | +### 13.3 控制台核心能力(对应用户提出的诉求逐条落实) | ||
| 368 | + | ||
| 369 | +| 用户诉求 | 落地方式 | | ||
| 370 | +|---|---| | ||
| 371 | +| 选择文件夹、创建项目 | 项目创建页:本地文件夹选择器 + 校验(是否已是 Git 仓库,不是则询问是否初始化)→ 写入 `project` 实体 | | ||
| 372 | +| 开始任务 / 提出目标 | 目标提出入口:表单输入 Goal 描述,直接调用创建任务的接口,进入 Org Graph | | ||
| 373 | +| 反馈 bug | 独立于 Validator 自动生成的人工 bug 提交入口,产出的 bug 记录带 `source=human` 标记,进入 Validator 做归因确认后再路由 | | ||
| 374 | +| 实时看板,看到动态任务运行过程 | WebSocket 推送的 Org Graph 三节点实时状态 + Work Graph 节点级图可视化(按状态着色,随执行实时刷新) | | ||
| 375 | +| 检查过程 | 深入到单个 Work Node 的输入/输出契约、reflection 记录、重试次数、**原始执行会话日志**(本地落盘,控制台内直接查看,运行中实时滚动、已完成静态查看,定期清理不长期占用磁盘) | | ||
| 376 | +| 管理所有功能/数据/流程/图 | Artifact 浏览器(按项目/任务筛选查看 BRD/PRD/SDD/代码/测试报告/bug)、Skill 库浏览与审计、Org/Work Graph 结构可视化 | | ||
| 377 | + | ||
| 378 | +具体的页面清单、字段、接口、实时事件契约在 SDD 的 `02-V-界面与交互.md`、`03-C-控制与接口.md` 中细化(本次讨论后已同步更新)。 | ||
| 379 | + | ||
| 380 | +--- | ||
| 381 | + | ||
| 382 | +*本文档为讨论稿,尚未包含具体的接口定义、数据 schema 字段级设计和排期计划,待架构方向对齐后再展开细化。* |
docs/sdd/00-SDD-生成记录.md
0 → 100644
| 1 | +++ a/docs/sdd/00-SDD-生成记录.md | ||
| 1 | +# 00-SDD-生成记录 | ||
| 2 | + | ||
| 3 | +## 元信息 | ||
| 4 | + | ||
| 5 | +| 字段 | 值 | | ||
| 6 | +|---|---| | ||
| 7 | +| project | graphlemon | | ||
| 8 | +| 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`) | | ||
| 9 | +| owner | bboonnee | | ||
| 10 | +| effective_date | 2026-07-30 | | ||
| 11 | +| 方法论 | 复用本地 Hermes `planning` profile 的 `project-sdd-planning` skill(MVCP:Model/View/Controller/Process) | | ||
| 12 | + | ||
| 13 | +## 输入来源 | ||
| 14 | + | ||
| 15 | +本 SDD 的输入不是独立的 PRD 文件,而是 `docs/graphlemon_architecture.md`(v0.9,已与用户逐项对齐确认)。经用户确认,该文档作为 PRD 等价输入直接驱动本 SDD 编写(见架构文档第 8 / 9 / 10 / 12 / 13 节的全部已确认决策)。 | ||
| 16 | + | ||
| 17 | +## 产出清单 | ||
| 18 | + | ||
| 19 | +1. `01-M-模型设计.md` | ||
| 20 | +2. `02-V-界面与交互.md` | ||
| 21 | +3. `03-C-控制与接口.md` | ||
| 22 | +4. `04-P-流程与状态流转.md` | ||
| 23 | +5. `05-实施约束与依赖.md` | ||
| 24 | +6. `06-验收与测试锚点.md` | ||
| 25 | +7. `sdd-mvcp-contract.json`(Coder Agent 可直接消费的结构化约束包) | ||
| 26 | + | ||
| 27 | +## 范围声明 | ||
| 28 | + | ||
| 29 | +本 SDD 覆盖架构文档第 11 节定义的 MVP 范围:**单一 Dev Organizer 全链路**(Planner → Coder → Validator 三节点 Org Graph + Validator → Planner 回退边),以"贪吃蛇游戏"类边界清晰任务为验收场景。不覆盖:多 Goal 并发(架构已支持,但 MVP 并发数参数固定为 1,非本期开发重点)。 | ||
| 30 | + | ||
| 31 | +**执行后端范围收窄(决策,2026-07-29)**:MVP 阶段只实现 Claude Code ACP 一种执行后端,不考虑 Hermes CLI 后端。此前"双后端能力不对等"的框架(架构文档 10.1/7.2 节)不再适用——不是"两个后端能力不对等",而是"只有一个后端"。执行器抽象层(Executor Interface)的接口设计仍然保留,为未来接入其他底层引擎留出扩展点,但 MVP 阶段只需要有一个可用实现。Hermes profile 的内容复用(SOUL.md/skills,见架构文档第 9 节)不受影响——复用的是人设与方法论文本,落地形式是 Claude Code 的 project-scoped subagent 定义文件(`.claude/agents/{planner,coder,validator}.md`),不是运行 Hermes CLI 本身。 | ||
| 32 | + | ||
| 33 | +## 非目标(non-goals) | ||
| 34 | + | ||
| 35 | +- 不做飞书 Base 迁移的历史数据导入(历史 Hermes profile 的 `memories/`、`sessions/` 不做自动迁移,按架构文档 9.1 节人工筛选复用)。 | ||
| 36 | +- 不做多租户/多团队权限体系(MVP 单用户使用)。 | ||
| 37 | +- 不做 Hermes CLI 执行后端(决策,2026-07-29:MVP 阶段只用 Claude Code ACP 一种后端,见范围声明)。 | ||
| 38 | +- 不做 Skill 自主迭代的人工审批 UI(已确认 Skill 自主迭代立即生效,只需 Git 可追溯,见架构文档 8 节决策 1)。 | ||
| 39 | +- 不做完整多项目管理 UI(项目列表/切换),数据模型预留 `project` 但 MVP 只支持单个 active 项目。 | ||
| 40 | +- 不做日志清理周期的用户可配置(仅保留天数/总量上限可配,清理任务触发频率固定)。 | ||
| 41 | + | ||
| 42 | +## 假设(待验证,需标注 owner) | ||
| 43 | + | ||
| 44 | +| ID | 假设 | Owner | 验证计划 | 状态 | | ||
| 45 | +|---|---|---|---|---| | ||
| 46 | +| A-1 | Claude Code 的 `agent()`/`parallel()`/`pipeline()` 原语可通过 `claude -p` 非交互式调用,且能返回结构化可解析结果 | bboonnee | 已完成 spike 实测(`spikes/a1-dynamic-workflows/`),真实账号运行,成本 $0.46 | **confirmed(2026-07-29)**:`claude -p ... < /dev/null` 无交互提示、40s 完成、`structured_output` 精确匹配预期(`sum=29`)、`permission_denials` 为空;唯一限制是不能加 `--bare`(会跳过登录态发现),需用完整冷启动调用。具体实现契约见 03-C 3.5 节 | | ||
| 47 | +| A-2 | 本地 Hermes profile 的 `SOUL.md`/`AGENTS.md` 可以原样迁移到目标项目的 `.claude/agents/{planner,coder,validator}.md` 而不需要大改人设内容 | bboonnee | 实际迁移一次并跑通 MVP 场景验证 | pending | | ||
| 48 | +| A-3 | ~~Hermes CLI 后端在 MVP 阶段以"单点执行 Org Agent"方式接入即可满足验收~~ | bboonnee | 不适用(决策,2026-07-29:MVP 阶段不考虑 Hermes CLI 后端,见范围声明) | not_applicable | | ||
| 49 | +| A-4 | 控制台为本地 Web 应用,文件夹选择由本地后端代拉系统原生对话框(`c_pick_folder`)实现 | bboonnee | 已确认为产品决策(架构文档 13.1 节决策 5),实施时需按 macOS/Windows/Linux 分别验证原生对话框调用方式 | confirmed | |
docs/sdd/01-M-模型设计.md
0 → 100644
| 1 | +++ a/docs/sdd/01-M-模型设计.md | ||
| 1 | +# 01-M-模型设计 | ||
| 2 | + | ||
| 3 | +数据落地原则(继承架构文档 7.3 节):Artifact、代码、Skill、Memory、Agent 人设文件全部纳入 Git 仓库版本化;执行记录(状态转移、reflection 记录)独立于 Git,落结构化存储(MVP 阶段可用 SQLite/本地文件型数据库,不强制引入外部数据库服务)。 | ||
| 4 | + | ||
| 5 | +**新增原则(架构文档第 13 节)**:目标项目是独立于 graphlemon 平台自身的代码空间。`project.target_folder_path` 指向的文件夹拥有自己的 Git 仓库;graphlemon 平台自己的结构化存储(本文件描述的全部实体)与目标项目的 Git 历史是两套独立的持久化,前者只是引用后者的文件路径,不托管目标项目的代码本身。 | ||
| 6 | + | ||
| 7 | +## 1.0 实体:`project` | ||
| 8 | + | ||
| 9 | +**目的**:一个目标项目 = 一个独立代码空间(用户选择的本地文件夹)。MVP 阶段只有一个 `active` 项目,但数据模型按多项目设计以便后续扩展。 | ||
| 10 | + | ||
| 11 | +- `project_id` string,pk(UUID) | ||
| 12 | +- `name` string,not null | ||
| 13 | +- `target_folder_path` string,not null,unique —— 本地绝对路径 | ||
| 14 | +- `has_git_repo` boolean,not null —— 创建项目时校验;若原文件夹不是 Git 仓库,创建流程需询问是否代为初始化 | ||
| 15 | +- `graphlemon_subdir_initialized` boolean,not null,default false —— 是否已在目标文件夹内创建 `.graphlemon/` 子目录(见架构文档 13.2 节目录约定) | ||
| 16 | +- `status` enum,not null —— `active`\|`archived` | ||
| 17 | +- `created_at` timestamp | ||
| 18 | +- 唯一约束:`target_folder_path`(同一文件夹不能重复创建为两个项目) | ||
| 19 | +- 并发/幂等:创建项目时若路径已存在对应 `project` 记录,直接返回已有记录而非报错(幂等创建) | ||
| 20 | + | ||
| 21 | +## 1.1 实体:`org_graph_definition` | ||
| 22 | + | ||
| 23 | +**目的**:Org Graph 的静态定义(Planner→Coder→Validator 三节点 + 回退边),人工维护,变更需人工评审,不由系统自主修改。 | ||
| 24 | + | ||
| 25 | +- `graph_id` string,pk,如 `dev-organizer` | ||
| 26 | +- `version` string,not null(Git tag/commit 引用) | ||
| 27 | +- `nodes` json,not null —— 数组,每项 `{ node_id, role, agent_profile_ref }` | ||
| 28 | +- `edges` json,not null —— 数组,每项 `{ from, to, condition }`(如 `validator→coder` 的 `condition: route==fast_lane`) | ||
| 29 | +- `file_path` string,not null —— 指向 `org/dev-organizer.graph.yaml` 的相对路径 | ||
| 30 | +- 状态集合:`draft`、`active`、`deprecated` | ||
| 31 | +- 变更约束:仅人工提交 + 评审后的 Git commit 可以更新此实体,系统不得自主写入 | ||
| 32 | + | ||
| 33 | +## 1.2 实体:`task` | ||
| 34 | + | ||
| 35 | +**目的**:任务队列条目,替代原飞书多维表格的需求表/Bug 表(架构文档 12.1 节已确认)。 | ||
| 36 | + | ||
| 37 | +- `task_id` string,pk(UUID) | ||
| 38 | +- `project_id` string,not null,fk → `project.project_id` | ||
| 39 | +- `goal_text` string,not null —— 原始 Goal 描述或 Validator 回传的 bug 任务描述 | ||
| 40 | +- `task_type` enum,not null —— `goal` \| `bug_fix` | ||
| 41 | +- `origin_ref` string,nullable —— 若 `task_type=bug_fix`,指向来源 `bug_record.bug_id` | ||
| 42 | +- `created_via` enum,not null,default `api` —— `api`\|`human_console`(区分是控制台人工提出还是程序化创建,用于审计) | ||
| 43 | +- `status` string,not null —— 状态集合见 04-P 的 `p_org_task_lifecycle` | ||
| 44 | +- `route` enum,nullable —— `fast_lane` \| `full_replan`(仅 `bug_fix` 类型需要,由 Validator 判定写入) | ||
| 45 | +- `loop_round` int,not null,default 0 —— 当前 Org-level Loop 已执行轮次 | ||
| 46 | +- `blocked_reason` string,nullable —— 达到轮次上限或预算超限时的原因说明 | ||
| 47 | +- `assigned_org_node` string,not null,fk —— 当前所处 Org Graph 节点(`planner`\|`coder`\|`validator`) | ||
| 48 | +- `created_at` / `updated_at` timestamp | ||
| 49 | +- 索引:`status`、`assigned_org_node`、`project_id`(用于调度器扫描和控制台按项目筛选) | ||
| 50 | +- 唯一约束:无(同一 Goal 可能因 loop 产生多条 `bug_fix` 类型的派生 task,但需通过 `origin_ref` 追溯血缘) | ||
| 51 | +- 并发/幂等:调度器认领 task 需用乐观锁(`status` 字段的 CAS 更新),避免并发调度重复认领 | ||
| 52 | + | ||
| 53 | +## 1.3 实体:`work_graph_run` | ||
| 54 | + | ||
| 55 | +**目的**:某个 Org Agent 针对某个 `task` 的一次 Work Graph 执行实例(动态生成、每次任务重新规划)。 | ||
| 56 | + | ||
| 57 | +- `run_id` string,pk(UUID) | ||
| 58 | +- `task_id` string,not null,fk → `task.task_id` | ||
| 59 | +- `org_node` string,not null —— `planner`\|`coder`\|`validator` | ||
| 60 | +- `graph_manifest` json,not null —— 本次生成的 Work Graph 结构化定义(节点、边、fan-out/fan-in 分组、loop 规则),对应课程里的 `sdd_exec_graph_manifest.json` 风格 | ||
| 61 | + - **补充说明(2026-07-30,回应"task 层级关系看不出来"的核对)**:`org_node=coder` 的这份 `graph_manifest` 就是架构文档 §6.2 所说的"to-do list"本身——to-do 项 = `graph_manifest` 里的节点,to-do 项之间的先后/并行依赖 = 节点间的边和 fan-out/fan-in 分组。不新建独立的 `todolist` 实体或 `artifact_type`,避免同一份数据有两个来源。层级关系是:`project → task(一个 goal = 一个 task = 一个 Org Graph)→ work_graph_run(按 org_node 各一次)→ graph_manifest 定义的 to-do/节点树 → work_node_execution → execution_log_file`。控制台侧 `v_task_detail` 需要把这棵树画出来(见 02-V §2.6 更新),而不是平铺展示 Work Node。 | ||
| 62 | +- `orchestration_script_ref` string,nullable —— 若走 Claude Code 原生动态工作流(架构文档 10.1 节方案 A),指向保存的编排脚本路径(`.claude/workflows/<name>.js`) | ||
| 63 | +- `status` string,not null —— 状态集合见 04-P 的 `p_work_graph_execution` | ||
| 64 | +- `started_at` / `completed_at` timestamp | ||
| 65 | +- `result_artifact_refs` json,nullable —— 本次运行产出的 `artifact.artifact_id` 列表 | ||
| 66 | +- 生命周期:`planning → running → merging → verifying → done|failed|blocked` | ||
| 67 | +- 并发/幂等:同一 `task_id + org_node` 同一时刻只允许一个 `active` 状态的 `work_graph_run`(唯一约束:`(task_id, org_node) WHERE status IN ('planning','running','merging','verifying')`) | ||
| 68 | + | ||
| 69 | +## 1.4 实体:`work_node_execution` | ||
| 70 | + | ||
| 71 | +**目的**:`work_graph_run` 内每个 Work Node 的执行记录,对应课程"给每个节点一个契约"。 | ||
| 72 | + | ||
| 73 | +- `node_exec_id` string,pk | ||
| 74 | +- `run_id` string,not null,fk → `work_graph_run.run_id` | ||
| 75 | +- `node_id` string,not null —— 在 `graph_manifest` 中的节点标识 | ||
| 76 | +- `subagent_type` string,not null —— 绑定的 Subagent 类型 | ||
| 77 | +- `skill_ref` string,not null,fk → `skill.skill_id` | ||
| 78 | +- `input_schema_ref` / `output_schema_ref` string,not null —— JSON Schema 引用,强制校验(课程"给每个节点一个契约"的落地) | ||
| 79 | +- `input_payload` json,not null | ||
| 80 | +- `output_payload` json,nullable —— 校验失败时为 null,触发重试 | ||
| 81 | +- `model_tier` enum,not null,default `standard` —— `cheap`\|`standard`\|`premium`(对应课程"按节点分层选模型") | ||
| 82 | +- `status` string,not null —— `pending`\|`running`\|`reflecting`\|`retrying`\|`waiting_for_quota`\|`blocked`\|`done`\|`failed`(`waiting_for_quota` 新增,2026-07-29,见下方与 03-C 3.6/3.5 节) | ||
| 83 | +- `retry_count` int,not null,default 0 | ||
| 84 | +- `reflection_notes` json,nullable | ||
| 85 | +- `git_worktree_ref` string,nullable —— 若节点涉及并行写代码,记录隔离用的 git worktree 路径(对应课程"节点隔离") | ||
| 86 | +- `quota_wait_started_at` timestamp,nullable —— 首次检测到 Claude Code plan 额度耗尽的时间,仅 `status=waiting_for_quota` 时有值,用于计算是否超过等待上限 | ||
| 87 | +- `quota_limit_type` enum,nullable —— `session`\|`weekly`\|`opus`,从 `claude -p` 报错文本判断额度类型(新增,2026-07-29,见 03-C 3.6 节) | ||
| 88 | +- 并发/幂等:`output_payload` 写入前必须通过 `output_schema_ref` 校验;校验失败自动重试,`retry_count` 达上限后置为 `failed` 并冒泡到 `work_graph_run` 层 | ||
| 89 | +- **额度等待语义(新增,2026-07-29)**:`waiting_for_quota` 不是失败态,是自愈型阻塞——`retry_count` 不计入这类重试(避免和真实的 schema 校验失败重试混淆),退避轮询策略和超时兜底见 03-C 3.6 节 | ||
| 90 | + | ||
| 91 | +## 1.5 实体:`artifact` | ||
| 92 | + | ||
| 93 | +**目的**:BRD/PRD/SDD/Benchmark/测试用例集/Bug 记录等结构化产物,版本化、有血缘。 | ||
| 94 | + | ||
| 95 | +- `artifact_id` string,pk | ||
| 96 | +- `artifact_type` enum,not null —— `brd`\|`prd`\|`sdd`\|`benchmark`\|`bug_record`\|`code_diff`\|`test_report` | ||
| 97 | +- `task_id` string,not null,fk → `task.task_id` | ||
| 98 | +- `produced_by_run_id` string,not null,fk → `work_graph_run.run_id` | ||
| 99 | +- `file_path` string,not null —— 路径语义按 `artifact_type` 区分:`brd`/`prd`/`sdd`/`benchmark`/`bug_record` 落在目标项目 `<target_folder_path>/.graphlemon/runs/<task_id>/...`(架构文档 13.2 节);`code_diff`/`test_report` 落在目标项目自身源码目录内,路径由 Coder 按目标项目既有约定决定,不强制结构 | ||
| 100 | +- `git_commit_sha` string,not null —— 对应**目标项目自身仓库**的 Git 提交(不是 graphlemon 平台仓库的提交) | ||
| 101 | +- `depends_on_artifact_ids` json,nullable —— 血缘引用(如 `sdd` 依赖某版本 `prd`) | ||
| 102 | +- `validation_status` enum,not null,default `pending` —— `pending`\|`passed`\|`failed` | ||
| 103 | +- `created_at` timestamp | ||
| 104 | +- 唯一约束:`(task_id, artifact_type, git_commit_sha)` | ||
| 105 | +- 迁移/回滚约束:`artifact` 一旦写入不可变更(immutable),修订产生新记录并更新 `depends_on_artifact_ids` 血缘链,不做原地更新 | ||
| 106 | + | ||
| 107 | +## 1.6 实体:`bug_record` | ||
| 108 | + | ||
| 109 | +**目的**:结构化问题记录(`artifact_type=bug_record` 的强类型展开),来源可以是 Validator 自动生成,也可以是人在控制台手工反馈(架构文档第 13 节新增诉求)。 | ||
| 110 | + | ||
| 111 | +- `bug_id` string,pk | ||
| 112 | +- `task_id` string,nullable,fk —— 人工反馈时可能尚未关联具体 task(先建 bug_record,再由 Validator triage 后创建/关联 task) | ||
| 113 | +- `source` enum,not null —— `validator`\|`human` | ||
| 114 | +- `reported_by` string,nullable —— `source=human` 时记录操作人标识 | ||
| 115 | +- `description` string,not null | ||
| 116 | +- `repro_steps` string,nullable —— `source=human` 时允许先留空,Validator triage 阶段补齐 | ||
| 117 | +- `root_cause_category` enum,nullable —— `planning`\|`coder`\|`skill_defect`\|`core_engine`(新增,2026-07-30,见 05 节 §5.8:覆盖 `.claude/agents/*.md` 身份文件变更、Graph Engine 调度器/执行器抽象层核心逻辑变更、01-M 自身数据库 schema 变更三类 self-target 场景,命中时 `c_org_loop_route` 路由到 `p_deploy_supervisor_blue_green` 而非常规 `fast_lane`/`full_replan`,见 1.15/1.16 节、03-C、04-P);`source=human` 且尚未 triage 时为 null | ||
| 118 | +- `root_cause_detail` string,nullable —— 具体归因说明(引用具体 SDD 章节/Coder 实现记录/Skill ID);`source=validator` 时必填,`source=human` 待 triage 完成后必填 | ||
| 119 | +- `related_artifact_ids` json,nullable —— 关联的原始 Artifact 版本;`source=human` 提交时可能为空 | ||
| 120 | +- `suggested_route` enum,nullable —— `fast_lane`\|`full_replan`;triage 完成前为 null | ||
| 121 | +- `skill_iteration_suggestion_id` string,nullable,fk → `skill_iteration.iteration_id` | ||
| 122 | +- `status` enum,not null —— `open`\|`triaging`\|`routed`\|`resolved`(`source=human` 的记录初始态为 `open`,Validator 认领后转 `triaging`) | ||
| 123 | +- 并发/幂等:同一 `task_id` 允许多条 `bug_record`(一次验证可能发现多个问题),互不影响 | ||
| 124 | + | ||
| 125 | +## 1.7 实体:`skill` | ||
| 126 | + | ||
| 127 | +**目的**:Skill 库条目,Git 版本化,可被 Validator 触发自主迭代。 | ||
| 128 | + | ||
| 129 | +- `skill_id` string,pk(如 `write-sdd`、`code-task-development`、`qa-task-validation`) | ||
| 130 | +- `owner_org_node` string,not null —— `planner`\|`coder`\|`validator` | ||
| 131 | +- `file_path` string,not null —— `skills/<skill_id>/` 目录 | ||
| 132 | +- `current_git_commit_sha` string,not null | ||
| 133 | +- `source` enum,not null —— `migrated_from_hermes`\|`new`\|`auto_iterated` | ||
| 134 | +- `migrated_from_profile` string,nullable —— 若来自本地 Hermes profile 迁移,记录来源(`code`\|`planning`\|`qa`) | ||
| 135 | +- 状态集合:`active`\|`archived` | ||
| 136 | + | ||
| 137 | +## 1.8 实体:`skill_iteration` | ||
| 138 | + | ||
| 139 | +**目的**:Skill 自主迭代的变更记录(架构文档 8 节决策 1:允许自主改写并立即生效,但每次变更必须是独立 Git 提交)。 | ||
| 140 | + | ||
| 141 | +- `iteration_id` string,pk | ||
| 142 | +- `skill_id` string,not null,fk → `skill.skill_id` | ||
| 143 | +- `triggered_by_bug_id` string,not null,fk → `bug_record.bug_id` | ||
| 144 | +- `before_git_commit_sha` / `after_git_commit_sha` string,not null | ||
| 145 | +- `change_summary` string,not null | ||
| 146 | +- `applied_at` timestamp | ||
| 147 | +- `rollback_of_iteration_id` string,nullable —— 若本次是对某次迭代的回滚 | ||
| 148 | +- 约束:每条记录对应一次独立 Git commit,commit message 必须包含 `triggered_by_bug_id` | ||
| 149 | + | ||
| 150 | +## 1.9 实体:`agent_profile` | ||
| 151 | + | ||
| 152 | +**目的**:Org Agent 的人设与运行契约文件元数据(迁移自本地 Hermes profile 的 `SOUL.md`/`AGENTS.md`)。 | ||
| 153 | + | ||
| 154 | +- `org_node` string,pk —— `planner`\|`coder`\|`validator` | ||
| 155 | +- `soul_file_path` string,not null —— `agents/<org_node>/SOUL.md` | ||
| 156 | +- `agents_file_path` string,nullable —— 仅 `validator` 有 `AGENTS.md` | ||
| 157 | +- `migrated_from_profile` string,not null —— `planning`\|`code`\|`qa` | ||
| 158 | +- `current_git_commit_sha` string,not null | ||
| 159 | + | ||
| 160 | +## 1.10 实体:`budget_guardrail_config` | ||
| 161 | + | ||
| 162 | +**目的**:预算/额度护栏配置(架构文档 7.5 节,双层护栏之一:手工配置层)。 | ||
| 163 | + | ||
| 164 | +- `config_id` string,pk,default `default` | ||
| 165 | +- `scope` enum,not null —— `global`\|`per_task`\|`per_day` | ||
| 166 | +- `limit_type` enum,not null —— `token_count`\|`call_count` | ||
| 167 | +- `limit_value` int,not null | ||
| 168 | +- `current_usage` int,not null,default 0 | ||
| 169 | +- `on_exceed_action` enum,not null,default `pause_and_notify` | ||
| 170 | +- `updated_by` string,not null —— 管理后台操作人 | ||
| 171 | +- `updated_at` timestamp | ||
| 172 | + | ||
| 173 | +## 1.11 实体:`org_concurrency_config` | ||
| 174 | + | ||
| 175 | +**目的**:多 Goal 并发数配置(架构文档 8 节决策 5,MVP 默认 1,管理后台可调)。 | ||
| 176 | + | ||
| 177 | +- `config_id` string,pk,default `default` | ||
| 178 | +- `max_concurrent_goals` int,not null,default 1 | ||
| 179 | +- `updated_by` string,not null | ||
| 180 | +- `updated_at` timestamp | ||
| 181 | + | ||
| 182 | +## 1.12 实体:`execution_event` | ||
| 183 | + | ||
| 184 | +**目的**:架构文档第 13 节新增——控制台实时看板的唯一数据源。所有状态变化在写入各自实体的同时追加一条事件记录,既用于 WebSocket 推送,也用于"检查过程"页面回放历史时间线,避免另建一套单独的日志系统。 | ||
| 185 | + | ||
| 186 | +- `event_id` string,pk(单调递增或 ULID,保证可排序) | ||
| 187 | +- `project_id` string,not null,fk → `project.project_id` | ||
| 188 | +- `task_id` string,nullable,fk | ||
| 189 | +- `run_id` string,nullable,fk → `work_graph_run.run_id` | ||
| 190 | +- `node_exec_id` string,nullable,fk → `work_node_execution.node_exec_id` | ||
| 191 | +- `event_type` enum,not null —— `task_status_changed`\|`node_status_changed`\|`artifact_created`\|`bug_recorded`\|`loop_routed`\|`budget_updated`\|`skill_iterated` | ||
| 192 | +- `payload` json,not null —— 事件详情(如 `{from_status, to_status}`) | ||
| 193 | +- `created_at` timestamp,not null,index | ||
| 194 | +- 索引:`(project_id, created_at)`(看板按项目拉取时间线)、`(task_id, created_at)`(任务详情页时间线) | ||
| 195 | +- 约束:`execution_event` 只追加不更新不删除(append-only),是审计和实时推送的唯一事实源;写入 `execution_event` 与写入对应业务实体(如 `task.status`)需在同一事务内完成,避免看板显示与实际状态不一致 | ||
| 196 | +- 原始执行会话日志(Claude Code ACP 的完整对话记录)**不**存入本表,本表只记录结构化摘要事件;原始日志存储见 1.13 `execution_log_file`,`event_type` 新增 `log_appended` 用于驱动控制台实时滚动日志(见下方实体更新) | ||
| 197 | + | ||
| 198 | +(`event_type` 枚举补充:`task_status_changed`\|`node_status_changed`\|`artifact_created`\|`bug_recorded`\|`loop_routed`\|`budget_updated`\|`skill_iterated`\|`log_appended`\|`quota_wait_started`(新增,2026-07-29,见 03-C 3.6 节额度等待重试语义)) | ||
| 199 | + | ||
| 200 | +## 1.13 实体:`execution_log_file` | ||
| 201 | + | ||
| 202 | +**目的**:架构文档第 13.1 节决策 4——Work Node 执行的原始会话日志本地落盘,控制台内直接可看,与 Git 版本化的 Artifact/Skill 是两套不同生命周期的存储。 | ||
| 203 | + | ||
| 204 | +- `log_file_id` string,pk | ||
| 205 | +- `node_exec_id` string,not null,unique,fk → `work_node_execution.node_exec_id` —— 一个 Work Node 执行对应一个日志文件 | ||
| 206 | +- `project_id` string,not null,fk → `project.project_id` | ||
| 207 | +- `file_path` string,not null —— graphlemon 平台本地日志目录下的路径,如 `~/.graphlemon/logs/<project_id>/<task_id>/<run_id>/<node_exec_id>.jsonl`(不在任何 Git 仓库内) | ||
| 208 | +- `format` enum,not null —— `jsonl`\|`raw_text`(取决于底层执行引擎的会话记录格式) | ||
| 209 | +- `size_bytes` int,not null,default 0 —— 执行过程中增量更新 | ||
| 210 | +- `status` enum,not null —— `active`(写入中)\|`closed`(节点执行结束,日志文件不再追加)\|`purged`(已被清理任务删除) | ||
| 211 | +- `created_at` timestamp | ||
| 212 | +- `purged_at` timestamp,nullable | ||
| 213 | +- 索引:`node_exec_id`(唯一)、`project_id` | ||
| 214 | +- 约束:日志文件由执行器抽象层(架构文档 7.2 节)在 Work Node 执行期间流式追加写入,每次追加同时写一条 `execution_event(event_type=log_appended)` 驱动控制台实时滚动;`status=purged` 后 `file_path` 指向的物理文件已被删除,但记录本身保留作审计(不物理删除 `execution_log_file` 行,只标记状态),避免"日志曾经存在过"这件事本身也查不到 | ||
| 215 | + | ||
| 216 | +## 1.14 实体:`log_retention_config` | ||
| 217 | + | ||
| 218 | +**目的**:日志清理策略配置,对应用户诉求"定时清理就好"。 | ||
| 219 | + | ||
| 220 | +- `config_id` string,pk,default `default` | ||
| 221 | +- `retention_days` int,not null,default 14 —— 超过该天数的 `execution_log_file`(`status=closed`)会被清理任务标记为 `purged` 并删除物理文件 | ||
| 222 | +- `max_total_size_mb` int,nullable —— 可选的总大小上限,超过时按最旧优先清理(即使未到 `retention_days`) | ||
| 223 | +- `updated_by` string,not null | ||
| 224 | +- `updated_at` timestamp | ||
| 225 | + | ||
| 226 | +## 1.15 实体:`deployment`(PH5 新增,2026-07-30,见 `docs/supervisor-auto-rollback-design.md`) | ||
| 227 | + | ||
| 228 | +**目的**: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` 开头命名说明)。 | ||
| 229 | + | ||
| 230 | +- `deployment_id` string,pk(UUID) | ||
| 231 | +- `git_commit_hash` string,not null —— 本次候选版本对应的 graphlemon 自身仓库 commit | ||
| 232 | +- `previous_commit_hash` string,not null —— 回滚目标,取上一条 `status=finalized` 的 `deployment.git_commit_hash` | ||
| 233 | +- `status` enum,not null —— `staged`\|`candidate_starting`\|`health_check_running`\|`health_check_passed`\|`health_check_failed`\|`cutover_in_progress`\|`live`\|`monitoring`\|`anomaly_detected`\|`rollback_in_progress`\|`rolled_back`\|`rollback_failed`\|`blocked_pending_human`\|`monitoring_window_clean`\|`finalized`(完整状态转移见 04-P `p_deploy_supervisor_blue_green`) | ||
| 234 | +- `candidate_port` int,nullable —— 健康检查阶段候选实例监听的临时端口 | ||
| 235 | +- `candidate_db_path` string,nullable —— 一次性生产库副本路径,健康检查结束后清理 | ||
| 236 | +- `started_at` / `cutover_at` / `monitoring_closed_at` / `rollback_at` timestamp,nullable | ||
| 237 | +- `rollback_reason` string,nullable —— 触发回滚的具体信号快照(哪个 `signal_type`、哪次采样、什么值),或"回滚本身失败"的原因说明 | ||
| 238 | +- `previous_instance_dir` string,not null —— 上一个已知良好版本的 checkout 目录,在本次 deployment 监控窗口正常关闭(`finalized`)前不允许清理 | ||
| 239 | +- 约束:同一时刻 graphlemon 自身仓库只允许一条处于非终态(不在 `finalized`\|`rolled_back`\|`blocked_pending_human`)的 `deployment` 记录,避免并发蓝绿部署互相干扰 | ||
| 240 | + | ||
| 241 | +## 1.16 实体:`deploy_health_signal`(PH5 新增,2026-07-30,见 `docs/supervisor-auto-rollback-design.md`) | ||
| 242 | + | ||
| 243 | +**目的**:`deployment` 生命周期内每次健康检查/监控采样的记录,全部是确定性判断结果,不涉及任何 LLM 判断。 | ||
| 244 | + | ||
| 245 | +- `signal_id` string,pk | ||
| 246 | +- `deployment_id` string,not null,fk → `deployment.deployment_id` | ||
| 247 | +- `sampled_at` timestamp,not null | ||
| 248 | +- `signal_type` enum,not null —— `process_alive`\|`http_health`\|`canary_task`\|`validator_approval_rate` | ||
| 249 | +- `status` enum,not null —— `ok`\|`degraded`\|`fail` | ||
| 250 | +- `detail` string,nullable —— 供人工事后排查的具体数值/日志摘录,不参与自动判断逻辑 | ||
| 251 | +- 索引:`(deployment_id, sampled_at)`(按部署拉取信号时间线) | ||
| 252 | +- 约束:`deploy_health_signal` 只追加不更新不删除(append-only),与 `execution_event` 的审计哲学一致 |
docs/sdd/02-V-界面与交互.md
0 → 100644
| 1 | +++ a/docs/sdd/02-V-界面与交互.md | ||
| 1 | +# 02-V-界面与交互 | ||
| 2 | + | ||
| 3 | +管理后台是黑灯工厂的"驾驶舱"(架构文档第 13 节),不只是配置页面,而是覆盖项目创建、目标提出、bug 反馈、实时看板、过程检查、Artifact/Skill 管理的完整人机交互控制台。MVP 单用户,无角色权限矩阵;MVP 单项目,页面里预留项目筛选但不强制做完整的项目列表/切换体验。 | ||
| 4 | + | ||
| 5 | +实时性原则:凡标注"实时"的页面/组件,均通过 WebSocket 订阅 `execution_event` 流更新(架构文档 13.1 节决策 3),不做轮询。 | ||
| 6 | + | ||
| 7 | +## 2.0 导航壳层(新增,2026-07-30,源自 `docs/wireframes/` 连页原型) | ||
| 8 | + | ||
| 9 | +此前各页面只逐条描述字段/事件,没有定义跨页共享的导航结构,会导致同一个信息架构问题被不同实现者各自猜测。以下结构在制作 `docs/wireframes/*.html` 连页原型时确定,供实现时直接采用: | ||
| 10 | + | ||
| 11 | +- **常驻顶部导航**(除 `v_project_setup` 外全部页面共享):`仪表盘`(`v_dashboard`)→ `任务`(`v_task_board`)→ `产物`(`v_artifact_browser`)→ `技能库`(`v_skill_library`)→ `设置`(下拉菜单,含并发数/预算护栏/日志保留三个子项,分别指向 `v_concurrency_settings`/`v_budget_settings`/`v_log_retention_settings`) | ||
| 12 | +- **不进常驻导航的页面**: | ||
| 13 | + - `v_project_setup`——首次使用或点导航栏右上角"切换项目"链接进入,属于项目切换流程,不是常规导航项 | ||
| 14 | + - `v_goal_composer`/`v_bug_feedback`——通过 `v_dashboard` 的按钮进入(`click_propose_goal`/`click_report_bug`),原型阶段做成独立路由页面而非弹层(02-V 原描述"可作为弹层,也可独立路由"在此处收敛为"独立路由",弹层与否是纯 UI 实现细节,不影响信息结构,实现时如改用弹层不需要回来改本节) | ||
| 15 | + - `v_task_detail`/`v_skill_audit_log`——分别从 `v_task_board`/`v_skill_library` 的列表项点击进入,不是导航项 | ||
| 16 | +- **导航栏右侧**:当前项目名 + "切换项目"链接(指向 `v_project_setup`) | ||
| 17 | + | ||
| 18 | +参照实现(无构建步骤的静态 HTML,DOM/class 命名与本文档字段一一对应,可直接作为实现起点):`docs/wireframes/v_project_setup.html` 起,共 12 个路由页面 + `v_graph_visualizer.html` 内嵌组件示范。 | ||
| 19 | + | ||
| 20 | +## 2.1 页面:`v_project_setup` 项目创建/选择 | ||
| 21 | + | ||
| 22 | +- 参照原型:`docs/wireframes/v_project_setup.html` | ||
| 23 | +- 路由:`/setup`(首次使用或需要更换目标项目时进入) | ||
| 24 | +- 载体说明(架构文档 13.1 节决策 5):控制台是本地 Web 应用,不是桌面应用;点击"选择文件夹"时前端调用 `c_pick_folder`,由本机常驻的后端进程代为拉起 OS 原生文件夹选择对话框并返回真实绝对路径,不使用浏览器 File System Access API | ||
| 25 | +- 字段: | ||
| 26 | + - `folder_path` 默认只读文本展示(由 `c_pick_folder` 回填,避免手动输入拼错路径),必填;仅当 `c_pick_folder` 返回 `DIALOG_UNAVAILABLE` 时,字段切换为可编辑的手动输入兜底 | ||
| 27 | + - `project_name` 文本输入,非必填(留空则取文件夹名) | ||
| 28 | +- 交互事件:`click_pick_folder` → 触发 `c_pick_folder`,成功回填 `folder_path`;用户取消系统对话框则 `folder_path` 保持原值不变;返回 `DIALOG_UNAVAILABLE` 时提示"当前环境不支持弹窗选择,请手动输入路径"并解锁手动编辑 | ||
| 29 | +- 提交流程: | ||
| 30 | + 1. 选择文件夹后先校验 `has_git_repo`(探测 `.git` 是否存在) | ||
| 31 | + 2. 若不是 Git 仓库,弹出确认态"该文件夹还不是 Git 仓库,是否代为初始化(`git init`)?"——用户确认前不写入任何文件(对应护栏规则:`Prohibited`/`Explicit permission required` 类操作不能静默执行) | ||
| 32 | + 3. 确认后调用 `c_project_create`,创建 `.graphlemon/` 子目录 | ||
| 33 | + 4. 成功 → 跳转 `v_dashboard`;失败 → 展示具体错误(如路径不可写) | ||
| 34 | +- 异常态:文件夹不存在、无写权限、路径已被注册为其他项目(幂等命中,直接跳转已有项目看板而非报错) | ||
| 35 | + | ||
| 36 | +## 2.2 页面:`v_dashboard` 实时看板首页 | ||
| 37 | + | ||
| 38 | +- 参照原型:`docs/wireframes/v_dashboard.html` | ||
| 39 | +- 路由:`/`(默认首页) | ||
| 40 | +- 权限:`all` | ||
| 41 | +- 展示内容(全部实时刷新): | ||
| 42 | + - Org Graph 三节点状态卡片:`planner`/`coder`/`validator` 各自当前是否有 `active` 的 `work_graph_run`,高亮当前 `task.assigned_org_node` | ||
| 43 | + - 当前任务概要:`goal_text` 摘要、`loop_round`、`status` | ||
| 44 | + - 预算用量条:引用 `budget_guardrail_config.current_usage`/`limit_value`,实时更新 | ||
| 45 | + - 最近事件时间线:最近 N 条 `execution_event`,滚动追加(新事件从顶部滑入,对应 WebSocket 推送到达) | ||
| 46 | +- 交互事件: | ||
| 47 | + - `click_propose_goal` → 打开 `v_goal_composer` | ||
| 48 | + - `click_report_bug` → 打开 `v_bug_feedback` | ||
| 49 | + - `click_task_card` → 跳转 `v_task_detail` | ||
| 50 | +- 异常态:无任何任务时展示"暂无运行中任务"空态 + 醒目的"提出第一个目标"引导按钮;WebSocket 连接断开时展示"实时连接已断开,正在重连"提示条,不阻塞页面其余只读内容展示 | ||
| 51 | +- **信息态(新增,2026-07-29)**:有任务处于 `waiting_for_quota` 时展示"等待 Claude Code 额度恢复({session\|weekly\|opus}),预计 {剩余等待时长}"提示条——样式是信息性的浅色条,不是 `blocked_pending_human` 那种醒目告警样式,因为这是自愈型阻塞不需要人工操作;`click` 可展开跳转到对应 `v_task_detail` | ||
| 52 | + | ||
| 53 | +## 2.3 页面:`v_goal_composer` 提出目标 | ||
| 54 | + | ||
| 55 | +- 参照原型:`docs/wireframes/v_goal_composer.html` | ||
| 56 | +- 路由:`/goals/new`(可作为 `v_dashboard` 的弹层,也可独立路由) | ||
| 57 | +- 字段: | ||
| 58 | + - `goal_text` 多行文本输入,必填,校验 `maxLen:4000` | ||
| 59 | +- 提交流程:提交前校验非空 → 确认态展示即将创建的任务摘要 → 调用 `c_task_create`(`created_via=human_console`)→ 成功跳转 `v_task_detail` | ||
| 60 | +- 异常态:为空提交时内联校验提示,不发起请求 | ||
| 61 | + | ||
| 62 | +## 2.4 页面:`v_bug_feedback` 人工反馈 bug | ||
| 63 | + | ||
| 64 | +- 参照原型:`docs/wireframes/v_bug_feedback.html` | ||
| 65 | +- 路由:`/bugs/new` | ||
| 66 | +- 字段: | ||
| 67 | + - `description` 多行文本,必填 | ||
| 68 | + - `repro_steps` 多行文本,非必填(允许先反馈现象,复现步骤后补) | ||
| 69 | + - `related_task_id` 下拉选择器,非必填(关联到某个已有任务;留空则视为新发现的独立问题) | ||
| 70 | +- 提交流程:提交 → 调用 `c_bug_report_create`(创建 `bug_record(source=human, status=open)`)→ 成功提示"已提交,Validator 将进行归因确认" → 跳转到该 bug 的详情视图(复用 `v_task_detail` 的 bug 展示区,或独立 `v_bug_detail`,MVP 复用前者) | ||
| 71 | +- 异常态:`description` 为空时内联校验拦截 | ||
| 72 | + | ||
| 73 | +## 2.5 页面:`v_task_board` 任务看板 | ||
| 74 | + | ||
| 75 | +- 参照原型:`docs/wireframes/v_task_board.html` | ||
| 76 | +- 路由:`/tasks` | ||
| 77 | +- 字段: | ||
| 78 | + - `keyword` 搜索框,非必填,校验 `maxLen:128` | ||
| 79 | + - `status_filter` 下拉,选项对应 `task.status` 枚举 | ||
| 80 | + - `org_node_filter` 下拉,选项 `planner`\|`coder`\|`validator` | ||
| 81 | +- 展示列表项(实时):`task_id`、`goal_text` 摘要、`status`、`assigned_org_node`、`loop_round`、`updated_at`,`status` 变化时该行有动效高亮(对应收到 `task_status_changed` 事件) | ||
| 82 | +- 交互事件:`click_row` → 跳转 `v_task_detail`;`click_search` → 触发 `c_task_list` | ||
| 83 | +- 异常态:空态"暂无任务";加载失败态提示重试 | ||
| 84 | + | ||
| 85 | +## 2.6 页面:`v_task_detail` 任务详情 / 过程检查 | ||
| 86 | + | ||
| 87 | +- 参照原型:`docs/wireframes/v_task_detail.html` | ||
| 88 | +- 路由:`/tasks/:task_id` | ||
| 89 | +- 字段:只读展示 `task` 全部字段 + 关联 `artifact` 列表 + 关联 `bug_record` 列表 | ||
| 90 | +- 子模块: | ||
| 91 | + - 内嵌 `v_graph_visualizer`(该 task 当前 `work_graph_run` 的实时图结构) | ||
| 92 | + - **`org_node=coder` 的 to-do 树视图(新增,2026-07-30,回应"task 层级关系看不出来"的核对)**:把当前 `work_graph_run.graph_manifest`(即 to-do list 本身,见 01-M §1.3 补充说明)渲染为缩进树,而不只是 `v_graph_visualizer` 的按层 DAG 行;每个 to-do 节点标注对应 `work_node_execution.status`,点开能展开到该节点的执行详情/日志(复用下方"每个 work_node_execution 支持展开"能力)。体现的层级是 `project → task → work_graph_run → to-do 树(graph_manifest)→ work_node_execution → execution_log_file`,明确这是 task 内部单个 Coder Work Graph 的结构,不是多个 task 组成的树。 | ||
| 93 | + - 事件时间线:该 task 全部 `execution_event`,按时间正序展示,支持展开查看 `payload` 详情(这是"检查过程"诉求的核心落地) | ||
| 94 | + - 每个 `work_node_execution` 支持展开查看 `input_payload`/`output_payload`/`reflection_notes`/`retry_count` | ||
| 95 | + - 内嵌 `v_log_viewer`(该 `work_node_execution` 的原始执行日志,控制台内直接查看,见 2.7.1) | ||
| 96 | +- 交互事件: | ||
| 97 | + - `click_artifact_link` → 打开对应文件(目标项目内 `.graphlemon/runs/` 或源码路径) | ||
| 98 | + - `click_view_raw_log` → 展开 `v_log_viewer`(不再是外部深链,见架构文档 13.1 节决策 4 的修正) | ||
| 99 | + - `click_retry`(仅 `status=blocked_pending_human` 时可见)→ 触发人工干预(`manual_resume`) | ||
| 100 | +- 异常态:`blocked_pending_human` 状态需醒目告警样式 | ||
| 101 | + | ||
| 102 | +## 2.7 页面:`v_graph_visualizer` Org/Work Graph 实时可视化 | ||
| 103 | + | ||
| 104 | +- 参照原型:`docs/wireframes/v_graph_visualizer.html` | ||
| 105 | +- 内嵌组件,非独立路由 | ||
| 106 | +- 展示内容: | ||
| 107 | + - Org Graph 层:固定三节点图,高亮当前节点 | ||
| 108 | + - Work Graph 层:当前 `work_graph_run.graph_manifest` 渲染的节点/边图,节点按 `work_node_execution.status` 实时着色(`done`=绿、`running`=蓝、`reflecting`/`retrying`=黄、`waiting_for_quota`=浅蓝+时钟图标(信息性,非告警样式,见 03-C 3.6 节)、`blocked`/`failed`=红、`pending`=灰),状态变化时对应节点有过渡动效(对应收到 `node_status_changed` 事件) | ||
| 109 | +- 交互事件:`hover_node` → tooltip 展示 `input_schema_ref`/`output_schema_ref`/`retry_count` 摘要;`waiting_for_quota` 态额外展示 `quota_limit_type` 和已等待时长 | ||
| 110 | +- 无可执行变化时可回归条件:无 `work_graph_run` 记录时展示"尚未开始规划"占位态 | ||
| 111 | + | ||
| 112 | +## 2.7.1 组件:`v_log_viewer` 原始执行日志查看器(架构文档 13.1 节决策 4) | ||
| 113 | + | ||
| 114 | +- 内嵌组件,非独立路由,被 `v_task_detail` 按 `node_exec_id` 引用 | ||
| 115 | +- 字段:`node_exec_id`(必填,决定查看哪个 Work Node 的日志) | ||
| 116 | +- 展示内容: | ||
| 117 | + - `work_node_execution.status IN (pending, running, reflecting, retrying)` 时:实时滚动模式,通过 WebSocket 订阅该节点日志流,新内容持续追加到底部(对应 `execution_event(event_type=log_appended)`) | ||
| 118 | + - `status IN (done, blocked, failed)` 时:静态模式,一次性拉取 `execution_log_file` 全量内容(大文件分页加载,避免一次性渲染卡顿) | ||
| 119 | +- 交互事件: | ||
| 120 | + - `toggle_auto_scroll` → 前端本地状态,是否自动滚动到最新一行 | ||
| 121 | + - `click_download` → 下载完整日志文件(`c_execution_log_get` 的下载态) | ||
| 122 | +- 异常态: | ||
| 123 | + - `execution_log_file.status=purged`(已被定时清理)→ 展示"该日志已超过保留期限被清理,保留天数可在设置中调整",不报错 | ||
| 124 | + - 日志文件本身写入失败/丢失 → 展示"日志暂不可用"而非空白页面 | ||
| 125 | + | ||
| 126 | +## 2.8 页面:`v_artifact_browser` Artifact 浏览器 | ||
| 127 | + | ||
| 128 | +- 参照原型:`docs/wireframes/v_artifact_browser.html` | ||
| 129 | +- 路由:`/artifacts` | ||
| 130 | +- 字段:`artifact_type_filter` 下拉、`task_id_filter` 下拉 | ||
| 131 | +- 展示:列表 + 预览(Markdown 类型直接渲染,代码类型语法高亮,均为只读) | ||
| 132 | +- 交互事件:`click_row` → 展示 `depends_on_artifact_ids` 血缘链(图形化展示版本依赖关系) | ||
| 133 | +- 异常态:空态"暂无产物" | ||
| 134 | + | ||
| 135 | +## 2.9 页面:`v_skill_library` Skill 库管理 | ||
| 136 | + | ||
| 137 | +- 参照原型:`docs/wireframes/v_skill_library.html` | ||
| 138 | +- 路由:`/skills` | ||
| 139 | +- 展示:`skill` 列表(按 `owner_org_node` 分组:Planner/Coder/Validator),每项展示 `source`(是否迁移自 Hermes profile / 新建 / 自动迭代产生) | ||
| 140 | +- 交互事件:`click_skill` → 跳转 `v_skill_audit_log` | ||
| 141 | + | ||
| 142 | +## 2.10 页面:`v_skill_audit_log` Skill 变更审计日志 | ||
| 143 | + | ||
| 144 | +- 参照原型:`docs/wireframes/v_skill_audit_log.html` | ||
| 145 | +- 路由:`/skills/:skill_id/audit` | ||
| 146 | +- 字段:只读列表,展示 `skill_iteration` 记录:`applied_at`、`triggered_by_bug_id`、`change_summary`、`before/after_git_commit_sha` | ||
| 147 | +- 交互事件: | ||
| 148 | + - `click_diff` → 展示 Git diff | ||
| 149 | + - `click_rollback` → 触发 `c_skill_rollback`,生成新的 `skill_iteration` 记录 | ||
| 150 | +- 异常态:空态"该 Skill 尚无自主迭代记录" | ||
| 151 | + | ||
| 152 | +## 2.11 页面:`v_concurrency_settings` 并发数配置 | ||
| 153 | + | ||
| 154 | +- 参照原型:`docs/wireframes/v_concurrency_settings.html` | ||
| 155 | +- 路由:`/settings/concurrency` | ||
| 156 | +- 字段:`max_concurrent_goals` 数字输入,必填,校验 `range:1-50` | ||
| 157 | +- 交互事件:`submit_save` → 触发 `c_concurrency_config_update` | ||
| 158 | +- 提交流程:校验 → 确认态 → 成功提示"已更新,下一个新 Goal 起生效" | ||
| 159 | + | ||
| 160 | +## 2.12 页面:`v_budget_settings` 预算护栏配置 | ||
| 161 | + | ||
| 162 | +- 参照原型:`docs/wireframes/v_budget_settings.html` | ||
| 163 | +- 路由:`/settings/budget` | ||
| 164 | +- 字段:`scope` 下拉、`limit_type` 下拉、`limit_value` 数字输入,均必填 | ||
| 165 | +- 展示:当前用量进度条(实时) | ||
| 166 | +- 交互事件:`submit_save` → 触发 `c_budget_config_update` | ||
| 167 | +- 异常态:超限时展示告警条 + "查看已暂停任务"链接(跳转 `v_task_board` 预置 `status_filter=blocked_pending_human`) | ||
| 168 | + | ||
| 169 | +## 2.13 页面:`v_log_retention_settings` 日志保留策略配置(架构文档 13.1 节决策 4) | ||
| 170 | + | ||
| 171 | +- 参照原型:`docs/wireframes/v_log_retention_settings.html` | ||
| 172 | +- 路由:`/settings/log-retention` | ||
| 173 | +- 字段: | ||
| 174 | + - `retention_days` 数字输入,必填,校验 `>0` | ||
| 175 | + - `max_total_size_mb` 数字输入,非必填 | ||
| 176 | +- 展示:当前日志总占用大小(汇总所有 `status=active|closed` 的 `execution_log_file.size_bytes`) | ||
| 177 | +- 交互事件:`submit_save` → 触发 `c_log_retention_config_update` | ||
| 178 | +- 异常态:`retention_days` 填 0 或负数时内联校验拦截 |
docs/sdd/03-C-控制与接口.md
0 → 100644
| 1 | +++ a/docs/sdd/03-C-控制与接口.md | ||
| 1 | +# 03-C-控制与接口 | ||
| 2 | + | ||
| 3 | +接口分三层:项目层接口(第 13 节新增,对应控制台的项目创建/人工交互)、Org Graph 层接口(对两个后端一视同仁,架构文档 7.2 节)、Work Graph 层接口(MVP 阶段以 Claude Code 原生动态工作流为准,架构文档 10.1 节方案 A)。所有接口默认 `auth: system_internal`(MVP 单用户,无外部开放 API)。 | ||
| 4 | + | ||
| 5 | +## 3.-1 `c_pick_folder`(架构文档 13.1 节决策 5) | ||
| 6 | + | ||
| 7 | +- `method + path`:`POST /api/system/pick-folder` | ||
| 8 | +- 语义:控制台前端是本地 Web 应用,无法直接调用 OS 原生文件夹选择对话框;此接口由本机常驻的后端进程代为拉起系统对话框(macOS `osascript`、Windows/Linux 对应平台方案),返回用户选中的真实绝对路径 | ||
| 9 | +- `input_contract`:`{}`(无需参数,对话框本身是模态的,由后端进程所在操作系统渲染) | ||
| 10 | +- `output_contract`:成功 `{ selected_path: string }`;用户取消对话框 `{ selected_path: null }`(不算错误) | ||
| 11 | +- `idempotent`:否(每次调用弹出新的对话框交互) | ||
| 12 | +- `errors`: | ||
| 13 | + - `DIALOG_UNAVAILABLE`(不可重试):后端所在环境不支持拉起原生对话框(如无图形界面的纯服务器环境),控制台应提示改用手动路径输入作为兜底 | ||
| 14 | +- `owner_team`:control-plane | ||
| 15 | + | ||
| 16 | +## 3.0 `c_project_create` | ||
| 17 | + | ||
| 18 | +- `method + path`:`POST /api/projects` | ||
| 19 | +- `input_contract`:`{ target_folder_path: string(required), name: string(optional), init_git_if_missing: boolean(required) }` | ||
| 20 | +- `output_contract`:成功 `{ project_id, has_git_repo, graphlemon_subdir_initialized }` | ||
| 21 | +- `idempotent`:是——同一 `target_folder_path` 重复调用直接返回已有 `project` 记录(见 01-M 1.0 幂等约束) | ||
| 22 | +- `side_effects`:若 `has_git_repo=false` 且 `init_git_if_missing=true`,执行 `git init`;创建 `<target_folder_path>/.graphlemon/` 子目录 | ||
| 23 | +- `errors`: | ||
| 24 | + - `PATH_NOT_FOUND`(不可重试):文件夹不存在 | ||
| 25 | + - `PATH_NOT_WRITABLE`(不可重试):无写权限 | ||
| 26 | + - `GIT_INIT_DECLINED`(不可重试):非 Git 仓库且用户未确认初始化 | ||
| 27 | +- `owner_team`:control-plane | ||
| 28 | + | ||
| 29 | +## 3.0.1 `c_project_get` / `c_project_list` | ||
| 30 | + | ||
| 31 | +- `GET /api/projects/:project_id`、`GET /api/projects` | ||
| 32 | +- `idempotent`:是 | ||
| 33 | +- 说明:MVP 控制台 UI 不强制暴露完整项目列表交互(架构文档 13.1 节决策 2),但接口按多项目设计,供未来扩展 | ||
| 34 | + | ||
| 35 | +## 3.1 `c_task_create` | ||
| 36 | + | ||
| 37 | +- `method + path`:`POST /api/tasks` | ||
| 38 | +- `input_contract`:`{ project_id: string(required), goal_text: string(required), task_type: enum('goal'), created_via: enum('api','human_console', default='api') }` | ||
| 39 | +- `output_contract`:成功 `{ task_id, status: 'queued' }`;失败见 errors | ||
| 40 | +- `idempotent`:否(每次调用创建新任务) | ||
| 41 | +- `transaction`:单表插入 + 一条 `execution_event(event_type=task_status_changed)`,需同一事务 | ||
| 42 | +- `side_effects`:写入 `task` 表;若 `org_concurrency_config.max_concurrent_goals` 已达上限,任务落 `queued` 但不触发调度;写入 `execution_event` 供看板实时推送 | ||
| 43 | +- `errors`: | ||
| 44 | + - `INVALID_PARAM`(不可重试):`goal_text` 为空或 `project_id` 不存在 | ||
| 45 | +- `audit_trace`:`trace_id`、`actor=system|human`、`business_id=task_id` | ||
| 46 | +- `owner_team`:graph-engine | ||
| 47 | + | ||
| 48 | +## 3.1.1 `c_bug_report_create` | ||
| 49 | + | ||
| 50 | +- `method + path`:`POST /api/bug-reports` | ||
| 51 | +- 语义:控制台人工反馈 bug 的入口,区别于 Validator 自动生成的 `c_bug_record_create`(3.10 节),复用同一张 `bug_record` 表但 `source=human` | ||
| 52 | +- `input_contract`:`{ description: string(required), repro_steps: string(optional), related_task_id: string(optional) }` | ||
| 53 | +- `output_contract`:成功 `{ bug_id, status: 'open' }` | ||
| 54 | +- `idempotent`:否 | ||
| 55 | +- `side_effects`:写入 `bug_record(source=human, status=open)`;写入 `execution_event(event_type=bug_recorded)`;异步触发 Validator 认领该 bug 进行 triage(复用 04-P `p_validator_root_cause_trace`,见该节新增的人工接入分支) | ||
| 56 | +- `errors`: | ||
| 57 | + - `INVALID_PARAM`(不可重试):`description` 为空 | ||
| 58 | + | ||
| 59 | +## 3.2 `c_task_list` / `c_task_get` | ||
| 60 | + | ||
| 61 | +- `GET /api/tasks?status=&org_node=&keyword=`、`GET /api/tasks/:task_id` | ||
| 62 | +- `output_contract`:`c_task_list` 返回分页数组;`c_task_get` 返回单条 `task` + 关联 `artifact` 列表 + 关联 `bug_record` 列表 | ||
| 63 | +- `idempotent`:是 | ||
| 64 | +- `errors`:`NOT_FOUND`(不可重试,仅 `c_task_get`) | ||
| 65 | + | ||
| 66 | +## 3.3 `c_org_node_trigger` | ||
| 67 | + | ||
| 68 | +- `POST /api/org-nodes/:org_node/trigger` | ||
| 69 | +- `input_contract`:`{ task_id: string(required) }` | ||
| 70 | +- 语义:Org Graph 调度器将 `task.assigned_org_node` 指向的 Org Agent 唤起,Org Agent 内部先调用 `c_work_graph_plan` 再调用 `c_work_graph_execute` | ||
| 71 | +- **实现契约(架构文档 7.2 节决策,2026-07-29)**:底层是子进程调用 `claude --agent <planner|coder|validator> -p "..."`,让整个会话以目标项目 `.claude/agents/<org_node>.md` 定义的 subagent 身份运行。三个 Org Agent 的身份(对应 SOUL.md)、memory、skills、tools、model 全部由这个 subagent 定义文件承载,`c_org_node_trigger` 本身只负责拼装任务上下文 prompt 和解析返回结果,不需要额外的角色隔离逻辑 | ||
| 72 | +- `idempotent`:否(每次触发可能生成新的 `work_graph_run`) | ||
| 73 | +- `errors`: | ||
| 74 | + - `TASK_NOT_READY`(可重试):上游依赖 Artifact 未就绪 | ||
| 75 | + - `CONCURRENCY_LIMIT_REACHED`(可重试):超过 `max_concurrent_goals` | ||
| 76 | + - `USAGE_LIMIT_REACHED`(新增,2026-07-29):语义同 3.6 节,Org Agent 级别的调用同样可能撞到 plan 额度,处理方式一致(等待重试,不转人工介入) | ||
| 77 | +- `owner_team`:graph-engine | ||
| 78 | + | ||
| 79 | +## 3.4 `c_work_graph_plan` | ||
| 80 | + | ||
| 81 | +- `POST /api/work-graph/plan` | ||
| 82 | +- 语义:对应 Org Agent 的 Planning Skill 调用——输入任务上下文,输出结构化 `graph_manifest`(节点、边、fan-out/fan-in 分组、loop 规则),不执行,只规划 | ||
| 83 | +- `input_contract`:`{ task_id, org_node }` | ||
| 84 | +- `output_contract`:`{ run_id, graph_manifest }` | ||
| 85 | +- `idempotent`:否 | ||
| 86 | +- `errors`:`PLANNING_FAILED`(可重试,重试上限由调用方控制) | ||
| 87 | +- 约束(继承 10 节课程映射):`graph_manifest` 中每条边必须标注是否为"零 token 边"(纯代码聚合,如 flatten/dedupe)还是"需要 Agent 判断的边",规划阶段就要做这个区分,不能等执行时才发现 | ||
| 88 | + | ||
| 89 | +## 3.5 `c_work_graph_execute` | ||
| 90 | + | ||
| 91 | +- `POST /api/work-graph/:run_id/execute` | ||
| 92 | +- 语义:直接调用 Claude Code 原生动态工作流能力执行 `graph_manifest` 对应的编排脚本(`agent()`/`parallel()`/`pipeline()`),编排本身不消耗模型 token(MVP 阶段唯一执行后端,见架构文档 7.2 节 2026-07-29 决策——不再考虑 Hermes CLI 后端,此前的后端不对等声明已作废) | ||
| 93 | +- `input_contract`:`{ run_id }` | ||
| 94 | +- `output_contract`:`{ status, result_artifact_refs }` | ||
| 95 | +- `idempotent`:否 | ||
| 96 | +- `errors`: | ||
| 97 | + - `NODE_CONTRACT_VIOLATION`(可重试,触发对应 `work_node_execution` 重试):某节点输出未通过 `output_schema_ref` 校验 | ||
| 98 | + - `BUDGET_EXCEEDED`(不可重试,需人工介入):执行中触发 `c_budget_check` 拦截 | ||
| 99 | + - `CLASSIFIER_BLOCKED`(不可重试,需人工介入):见下方权限模式说明——auto mode 分类器连续拦截 3 次或累计 20 次,`-p` 下无人可确认,进程中止 | ||
| 100 | + | ||
| 101 | +**Claude Code ACP 后端的实现契约(A-1 已通过 spike 实测确认,见 `spikes/a1-dynamic-workflows/`,2026-07-29)**: | ||
| 102 | + | ||
| 103 | +调用是 **prompt-mediated** 的,不是对 Claude Code 的直接函数调用。`c_work_graph_execute` 的具体实现步骤: | ||
| 104 | + | ||
| 105 | +1. 将 `graph_manifest` 对应的编排脚本写入/更新目标项目仓库的 `.claude/workflows/<run_id>.js`(随目标项目自身 git 历史版本化,而非 graphlemon 平台仓库) | ||
| 106 | +2. 以子进程方式调用: | ||
| 107 | + ``` | ||
| 108 | + claude -p 'Run /<run_id> with args {...}' \ | ||
| 109 | + --output-format json \ | ||
| 110 | + --json-schema <output_schema_ref 对应的 JSON Schema> \ | ||
| 111 | + --permission-mode auto | ||
| 112 | + ``` | ||
| 113 | + **不加 `--bare`**:实测 `--bare` 会跳过凭据/登录态发现(报 `Not logged in`,与是否有 workflow 无关,是 `--bare` 本身的限制),必须用不带 `--bare` 的完整冷启动 | ||
| 114 | + **权限模式选定为 `auto`,而非 `bypassPermissions`/`--dangerously-skip-permissions`(决策,2026-07-29)**:官方文档明确要求 `bypassPermissions` 只应在隔离容器/VM+无网络访问环境下使用,且"对 prompt injection 和意外操作零防护";graphlemon 当前的节点隔离手段是 git worktree(同机目录级隔离,非容器/VM 级),达不到这个前提。`auto` mode 用一个分类器对每个动作实时把关,默认拦截一批高危操作(`curl|bash`、密钥外泄、生产部署、`force push`、批量删除、IAM 变更等),同样能在 `-p` 下零 prompt 跑完,且和架构已有的预算护栏/人工升级理念一致。**约束**:`auto` mode 要求会话模型是 Claude Sonnet 5 / Opus 4.7+ / Fable 5(Anthropic API 上);架构文档 10 节"按节点分层选模型"给便宜/有界重复节点选模型时,需要在这个集合内选,不能选更早/更小的模型 | ||
| 115 | +3. 解析返回 JSON 的 `structured_output` 字段作为 `output_contract` 的 `result_artifact_refs` 来源 | ||
| 116 | +4. 用返回 JSON 的 `total_cost_usd` / `usage` 字段回填 `budget_guardrail_config.current_usage`(对应 `c_budget_check`) | ||
| 117 | +5. 子进程需以无 stdin 方式启动(`< /dev/null` 或等效),避免任何交互式提示导致挂起;实测确认无 stdin 时不会挂起、不会插入确认提示 | ||
| 118 | +6. 若返回结果表明本轮运行被分类器连续拦截 3 次或累计 20 次(`-p` 下无人可确认,官方文档称此时 auto mode 会中止会话),映射为 `CLASSIFIER_BLOCKED` 错误,对应 04-P 状态机路由到 `blocked_pending_human`,`blocked_reason='classifier_blocked'`,并把分类器拦截的动作摘要写入 `v_task_detail`,供人工判断是补 `permissions.allow`/白名单信任规则还是这确实是个危险操作 | ||
| 119 | + | ||
| 120 | +## 3.6 `c_work_node_run` | ||
| 121 | + | ||
| 122 | +- 内部接口(不对外暴露 HTTP,供 `c_work_graph_execute` 调用),语义对应课程"给每个节点一个契约" | ||
| 123 | +- `input_contract`:`{ node_exec_id, input_payload, input_schema_ref }` —— 校验失败直接拒绝,不进入执行 | ||
| 124 | +- `output_contract`:`{ output_payload }` —— 必须通过 `output_schema_ref` 校验才算成功 | ||
| 125 | +- `idempotent`:否 | ||
| 126 | +- `errors`: | ||
| 127 | + - `SCHEMA_VALIDATION_FAILED`(可重试,重试即重新调用 Subagent,非幂等回放) | ||
| 128 | + - `USAGE_LIMIT_REACHED`(新增,2026-07-29,可重试但不是立即重试,见下方等待重试语义) | ||
| 129 | +- 约束:并行写代码的节点必须先申请 `git_worktree_ref`(见 01-M `work_node_execution.git_worktree_ref`),执行完成后清理 worktree 并合并回主分支 | ||
| 130 | +- **self-target 特例(新增,2026-07-30,见 05 节 §5.8)**:`task.project_id` 对应的 `project.target_folder_path == graphlemon 平台自身安装路径` 时,无论并行还是串行,一律强制申请 `git_worktree_ref`,不适用"仅并行时才强制"的一般规则;Coder/Validator 的自测/复核全部在 worktree 副本里跑,不碰主 checkout | ||
| 131 | +- **新增约束(架构文档 13.1 节决策 4)**:节点开始执行时必须先创建对应的 `execution_log_file` 记录(`status=active`),执行期间将 Claude Code ACP 的会话输出流式追加写入该文件,每次追加同时写一条 `execution_event(event_type=log_appended)`;节点执行结束(成功或失败)后将 `execution_log_file.status` 置为 `closed` | ||
| 132 | + | ||
| 133 | +**Claude Code plan 额度耗尽的等待重试语义(架构文档 7.5 节决策,2026-07-29)**: | ||
| 134 | + | ||
| 135 | +`claude -p` 的账号/plan 用量是一个会自然恢复的资源(5 小时滚动窗口 / 7 天窗口 / Opus 专属限额),耗尽时官方 CLI 直接终止请求并在文本里带上人类可读的恢复时间,例如 `You've hit your session limit · resets 3:45pm`、`You've hit your weekly limit · resets Mon 12:00am`。这类错误**不应该**路由到 `blocked_pending_human`(那是给需要人工判断的情况用的),而是自动等待+重试: | ||
| 136 | + | ||
| 137 | +1. 检测:`claude -p` 返回 `is_error=true` 且 `result` 文本匹配 `/hit your (session|weekly|Opus) limit/i` 时,判定为 `USAGE_LIMIT_REACHED`,从匹配的关键词提取 `quota_limit_type`(`session`\|`weekly`\|`opus`),写入 `work_node_execution.quota_limit_type` | ||
| 138 | +2. 状态转移:`work_node_execution.status → waiting_for_quota`,记录 `quota_wait_started_at`;不冒泡到 `task.status`,调度器继续跑队列里其他不依赖这个节点的工作(同 04-P `budget_exceeded` 的"跳过而非全局阻塞"模式) | ||
| 139 | +3. 退避轮询:**不解析"resets 3:45pm"这种脆弱的人类可读文本**(没有日期/时区锚点,跨天跨周边界容易解析错),改用容量更简单可靠的退避重试——等待间隔从 5 分钟起步,每次翻倍,上限 30 分钟(5/10/20/30/30/... 分钟),每次到点直接重新发起原本要跑的那次 `claude -p` 调用本身作为探测,成功即转入正常执行 | ||
| 140 | + - 例外:`quota_limit_type=opus` 时不用等待退避——按官方建议直接把该节点这次调用降级到非 Opus 模型重试(需符合 03-C 3.5 节 auto mode 的模型约束:Sonnet 5 / Opus 4.7+ / Fable 5) | ||
| 141 | +4. 超时兜底(防止真正的账号/计费问题被无限期静默等待掩盖):`quota_limit_type=session` 累计等待超过 8 小时、或 `quota_limit_type=weekly` 累计等待超过 9 天仍未恢复,转 `blocked_pending_human`,`blocked_reason='usage_limit_wait_timeout'` | ||
| 142 | +5. 可见性:进入 `waiting_for_quota` 时写一条 `execution_event(event_type=quota_wait_started)`,`v_dashboard`/`v_graph_visualizer` 用信息性样式(非告警)展示,见 02-V 2.2/2.7 节——这是自愈型等待,不需要人工介入,但应该在控制台可见 | ||
| 143 | + | ||
| 144 | +## 3.6.1 `c_execution_log_get` | ||
| 145 | + | ||
| 146 | +- `GET /api/work-nodes/:node_exec_id/log` | ||
| 147 | +- 语义:获取指定 Work Node 的原始执行日志(静态模式,用于已结束节点或首次加载) | ||
| 148 | +- `input_contract`:`{ path: { node_exec_id }, query: { offset: int(optional), limit: int(optional), download: boolean(optional) } }` —— 支持分页避免大文件一次性传输 | ||
| 149 | +- `output_contract`:`{ content: string, format, total_size_bytes, status }`;`download=true` 时返回文件流 | ||
| 150 | +- `idempotent`:是 | ||
| 151 | +- `errors`: | ||
| 152 | + - `LOG_NOT_FOUND`(不可重试):`node_exec_id` 不存在对应日志记录 | ||
| 153 | + - `LOG_PURGED`(不可重试):日志已被定时清理任务删除,`v_log_viewer` 据此展示对应异常态 | ||
| 154 | + | ||
| 155 | +## 3.6.2 `c_ws_log_tail`(实时日志订阅) | ||
| 156 | + | ||
| 157 | +- `protocol`:WebSocket,`WS /api/ws/work-nodes/:node_exec_id/log` | ||
| 158 | +- 语义:订阅指定 Work Node 的实时日志追加事件,用于运行中节点的滚动查看 | ||
| 159 | +- `input_contract`:`{ path: { node_exec_id } }` | ||
| 160 | +- `output_contract`(推送消息体):`{ node_exec_id, appended_content: string, created_at }` | ||
| 161 | +- `idempotent`:不适用(长连接) | ||
| 162 | +- `errors`:`NODE_EXEC_NOT_FOUND`(不可重试):连接建立时校验 | ||
| 163 | +- 说明:节点执行结束后(`work_node_execution.status` 转为终态)服务端主动关闭该连接,前端应据此切换到 `c_execution_log_get` 的静态模式 | ||
| 164 | + | ||
| 165 | +## 3.7 `c_artifact_write` | ||
| 166 | + | ||
| 167 | +- `POST /api/artifacts` | ||
| 168 | +- `input_contract`:`{ artifact_type, task_id, produced_by_run_id, file_path, content, depends_on_artifact_ids }` | ||
| 169 | +- `output_contract`:`{ artifact_id, git_commit_sha }` | ||
| 170 | +- `idempotent`:否(`artifact` 不可变更,见 01-M 迁移约束) | ||
| 171 | +- `side_effects`:写文件 + Git commit(commit message 包含 `task_id` 与 `artifact_type`) | ||
| 172 | +- `errors`:`GIT_COMMIT_FAILED`(可重试) | ||
| 173 | + | ||
| 174 | +## 3.8 `c_skill_update` | ||
| 175 | + | ||
| 176 | +- `POST /api/skills/:skill_id/update` | ||
| 177 | +- 语义:Skill 自主迭代的落地接口(架构文档 8 节决策 1)——允许 Validator 触发的自动改写立即生效,但必须产生独立 Git 提交 | ||
| 178 | +- `input_contract`:`{ skill_id, triggered_by_bug_id(required), new_content, change_summary(required) }` | ||
| 179 | +- `output_contract`:`{ iteration_id, after_git_commit_sha }` | ||
| 180 | +- `idempotent`:否 | ||
| 181 | +- `transaction`:文件写入 + Git commit 需原子完成(commit 失败则整体回滚,不留半成品 Skill 文件) | ||
| 182 | +- `errors`:`MISSING_TRIGGER_REF`(不可重试):`triggered_by_bug_id` 缺失时直接拒绝——不允许无来源的 Skill 变更 | ||
| 183 | + | ||
| 184 | +## 3.9 `c_skill_rollback` | ||
| 185 | + | ||
| 186 | +- `POST /api/skills/:skill_id/rollback` | ||
| 187 | +- `input_contract`:`{ iteration_id(required) }` —— 回滚到该次迭代之前的版本 | ||
| 188 | +- `output_contract`:`{ new_iteration_id }`(回滚本身也生成一条新的 `skill_iteration` 记录) | ||
| 189 | +- `idempotent`:否 | ||
| 190 | +- `errors`:`ITERATION_NOT_FOUND`(不可重试) | ||
| 191 | + | ||
| 192 | +## 3.10 `c_bug_record_create` | ||
| 193 | + | ||
| 194 | +- `POST /api/bug-records` | ||
| 195 | +- `input_contract`:`{ task_id, description, repro_steps, root_cause_category(required, enum), root_cause_detail(required), related_artifact_ids, suggested_route(required) }` | ||
| 196 | +- `output_contract`:`{ bug_id }` | ||
| 197 | +- `idempotent`:否 | ||
| 198 | +- `errors`:`INVALID_ROOT_CAUSE_CATEGORY`(不可重试):未落在 `planning`\|`coder`\|`skill_defect`\|`core_engine`(新增,2026-07-30,见 01-M 1.6/05 节 §5.8)四类之一 | ||
| 199 | +- 约束:`root_cause_category=skill_defect` 时必须同时创建 `skill_iteration_suggestion`,否则视为字段缺失(见 08 反模式门禁:不允许"空目标"式的归因);`root_cause_category=core_engine` 时不要求人工评审即可提交(用户明确否决"比照 `org_graph_definition` 强制人工 PR 评审"的原提案),但 `c_org_loop_route` 会将其路由到蓝绿部署+监控流程而非常规 `fast_lane`(见 3.11 节) | ||
| 200 | + | ||
| 201 | +## 3.11 `c_org_loop_route` | ||
| 202 | + | ||
| 203 | +- `POST /api/tasks/:task_id/route` | ||
| 204 | +- 语义:Org-level Loop 的路由决策落地——Validator 判定结果驱动任务回到 Planner(`full_replan`)或直达 Coder(`fast_lane`),决策本身由 Agent 判断(Validator 分类),路由本身是确定性代码(架构文档 10 节课程映射:判断用 Agent,路由用代码) | ||
| 205 | +- `input_contract`:`{ task_id, decided_route: enum('fast_lane','full_replan','manual_resume') }` | ||
| 206 | +- `output_contract`:`{ task_id, new_status, new_assigned_org_node }` | ||
| 207 | +- `idempotent`:否 | ||
| 208 | +- `errors`: | ||
| 209 | + - `LOOP_LIMIT_REACHED`(不可重试):`task.loop_round` 已达上限(3 轮),强制转 `blocked_pending_human`,不接受常规路由请求,只接受 `manual_resume` | ||
| 210 | +- **`core_engine` 路由分支(新增,2026-07-30,见 05 节 §5.8)**:关联 `bug_record.root_cause_category=core_engine` 的任务,无论 Validator 给出的 `decided_route` 是 `fast_lane` 还是 `full_replan`,`c_org_loop_route` 都不直接按常规路由处理,而是在 Coder 完成修复、Validator PASS、worktree 分支 merge 回 graphlemon 自身仓库主分支之后,转交 `p_deploy_supervisor_blue_green` 流程(04-P)——即触发 Deploy Supervisor 的蓝绿部署与自动回滚机制,不是"合并即生效" | ||
| 211 | + | ||
| 212 | +## 3.12 `c_budget_check` | ||
| 213 | + | ||
| 214 | +- 内部接口,`c_work_graph_execute` 执行前/中调用 | ||
| 215 | +- `input_contract`:`{ scope, estimated_usage }` | ||
| 216 | +- `output_contract`:`{ allowed: boolean, remaining_quota }` | ||
| 217 | +- `idempotent`:是 | ||
| 218 | +- `errors`:`BUDGET_EXCEEDED`(不可重试):触发后对应任务转 `blocked_pending_human`,并记录 `blocked_reason='budget_exceeded'` | ||
| 219 | + | ||
| 220 | +## 3.13 `c_concurrency_config_update` / `c_budget_config_update` | ||
| 221 | + | ||
| 222 | +- `PUT /api/settings/concurrency`、`PUT /api/settings/budget` | ||
| 223 | +- `input_contract`:对应 01-M `org_concurrency_config` / `budget_guardrail_config` 的可写字段 | ||
| 224 | +- `output_contract`:更新后的完整配置对象 | ||
| 225 | +- `idempotent`:是 | ||
| 226 | +- `errors`:`INVALID_PARAM`(不可重试):数值超出校验范围 | ||
| 227 | + | ||
| 228 | +## 3.13.1 `c_log_retention_config_update` | ||
| 229 | + | ||
| 230 | +- `PUT /api/settings/log-retention` | ||
| 231 | +- `input_contract`:`{ retention_days: int(required, >0), max_total_size_mb: int(optional) }` | ||
| 232 | +- `output_contract`:更新后的完整配置对象 | ||
| 233 | +- `idempotent`:是 | ||
| 234 | +- `errors`:`INVALID_PARAM`(不可重试):`retention_days <= 0` | ||
| 235 | + | ||
| 236 | +## 3.14 `c_ws_subscribe`(实时事件订阅,架构文档 13.1 节决策 3) | ||
| 237 | + | ||
| 238 | +- `protocol`:WebSocket,`WS /api/ws/projects/:project_id` | ||
| 239 | +- 语义:控制台建立连接后订阅指定 `project_id` 下的全部 `execution_event`;服务端在 `execution_event` 写入的同一事务提交后立即推送给已订阅的连接 | ||
| 240 | +- `input_contract`(连接建立时的订阅参数,query string):`{ project_id: string(required), task_id: string(optional) }` —— 传 `task_id` 时可选做服务端过滤,减少推送量 | ||
| 241 | +- `output_contract`(服务端推送的消息体):`{ event_id, event_type, task_id, run_id, node_exec_id, payload, created_at }`,字段与 `execution_event` 实体一一对应 | ||
| 242 | +- `idempotent`:不适用(长连接) | ||
| 243 | +- `errors`: | ||
| 244 | + - `PROJECT_NOT_FOUND`(不可重试):连接建立时校验 `project_id` 不存在,直接拒绝连接 | ||
| 245 | +- 断线重连约束:客户端重连后需带上 `last_event_id` 查询自该事件之后的遗漏事件(通过 `c_task_get` 系接口的时间线字段补齐,而非 WebSocket 层做消息重放缓冲,MVP 阶段简化处理) | ||
| 246 | +- `owner_team`:control-plane | ||
| 247 | + | ||
| 248 | +## 3.15 Deploy Supervisor 控制操作(PH5 新增,2026-07-30,见 `docs/supervisor-auto-rollback-design.md`) | ||
| 249 | + | ||
| 250 | +以下五个操作均由 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` 路由分支)。 | ||
| 251 | + | ||
| 252 | +### `c_deploy_supervisor_stage` | ||
| 253 | + | ||
| 254 | +- 内部接口,语义:拉取合并后的主分支到全新目录(非原地 `git pull`),生成 `deployment` 记录 | ||
| 255 | +- `input_contract`:`{ git_commit_hash }` | ||
| 256 | +- `output_contract`:`{ deployment_id, status: 'staged' }` | ||
| 257 | +- `idempotent`:否 | ||
| 258 | +- `side_effects`:复制 `PROD_DB_PATH` 到一次性 `candidate_db_path`;用 `supervisor/isolation/isolated_workspace.py` 同款隔离原则拉起候选进程(`subprocess.Popen`,`shell=False`),监听临时 `candidate_port` | ||
| 259 | +- `errors`:`CHECKOUT_FAILED`(不可重试,转 `blocked_pending_human`):全新目录 checkout 失败(如磁盘空间不足) | ||
| 260 | + | ||
| 261 | +### `c_deploy_supervisor_health_check` | ||
| 262 | + | ||
| 263 | +- 内部接口,语义:对候选实例执行 `process_alive`/`http_health`/`canary_task` 三项 pre-cutover 检查(见 `supervisor-auto-rollback-design.md` §6) | ||
| 264 | +- `input_contract`:`{ deployment_id }` | ||
| 265 | +- `output_contract`:`{ status: 'health_check_passed' | 'health_check_failed', signals: deploy_health_signal[] }` | ||
| 266 | +- `idempotent`:是(可重复查询当前检查状态,但不重新触发候选进程重启) | ||
| 267 | +- `errors`:`HEALTH_CHECK_TIMEOUT`(不可重试,直接判定该项 fail,进入回滚路径 A,不重试——重试属于"给坏版本第二次机会",不符合 fail-closed 原则) | ||
| 268 | +- 约束:任一检查项 fail 立即整体判定 `health_check_failed`,不等待其余检查项跑完 | ||
| 269 | + | ||
| 270 | +### `c_deploy_supervisor_cutover` | ||
| 271 | + | ||
| 272 | +- 内部接口,语义:蓝绿切换本体——仅在 `c_deploy_supervisor_health_check` 返回 `health_check_passed` 后才允许调用 | ||
| 273 | +- `input_contract`:`{ deployment_id }` | ||
| 274 | +- `output_contract`:`{ status: 'live', cutover_at }` | ||
| 275 | +- `idempotent`:否 | ||
| 276 | +- `side_effects`:Live 停止接受新的 `c_org_node_trigger` 调用(在途 `work_node_execution` 允许走到可安全中断的检查点,依赖既有持久化恢复机制,不强求优雅排空)→ Live 进程退出 → 候选进程改连 `PROD_DB_PATH`、改绑 `LIVE_PORT` → 候选成为新 Live;原 Live checkout 目录保留为 `previous_instance_dir`,不删除 | ||
| 277 | +- `errors`:`CUTOVER_FAILED`(不可重试,转 `blocked_pending_human`):切换过程中候选进程未能正确绑定生产端口/生产库 | ||
| 278 | + | ||
| 279 | +### `c_deploy_supervisor_monitor` | ||
| 280 | + | ||
| 281 | +- 内部接口,语义:post-cutover 监控窗口内周期性采样四类信号(见 `supervisor-auto-rollback-design.md` §6),窗口时长/任务数上限见 05 节 §5.9 | ||
| 282 | +- `input_contract`:`{ deployment_id }`(由 Deploy Supervisor 自身的调度循环周期性调用,不是一次性调用) | ||
| 283 | +- `output_contract`:`{ status: 'monitoring' | 'anomaly_detected' | 'monitoring_window_clean', latest_signal: deploy_health_signal }` | ||
| 284 | +- `idempotent`:是 | ||
| 285 | +- 约束:`validator_approval_rate` 信号从 `execution_event`/`work_node_execution` 按滚动窗口统计 Validator 节点 `approved` 占比,与该 `deployment` 之前的历史基线做纯算术比较,偏离 `VALIDATOR_RATE_DEVIATION_THRESHOLD`(05 节 §5.9)即判定 `anomaly_detected`——这是"改坏了的 validator"场景的核心检测点,判断全程不需要另一个 LLM 参与 | ||
| 286 | + | ||
| 287 | +### `c_deploy_supervisor_rollback` | ||
| 288 | + | ||
| 289 | +- 内部接口,语义:回滚路径 A(pre-cutover,健康检查未过)与路径 B(post-cutover,监控期 `anomaly_detected`)的统一入口,具体步骤见 `supervisor-auto-rollback-design.md` §7 | ||
| 290 | +- `input_contract`:`{ deployment_id, rollback_reason }` | ||
| 291 | +- `output_contract`:`{ status: 'rolled_back' | 'health_check_failed' | 'rollback_failed' }` | ||
| 292 | +- `idempotent`:否 | ||
| 293 | +- `side_effects`:路径 A——终止候选进程、删除一次性 DB 副本、`git revert` 本次合并 commit、创建 `bug_record(root_cause_category=core_engine)`;路径 B——用 `previous_instance_dir` 重新拉起旧版本、绑定 `LIVE_PORT`+`PROD_DB_PATH`、对复活的旧版本重跑一遍 pre-cutover 健康检查确认无误、`git revert`、创建 `bug_record` | ||
| 294 | +- `errors`: | ||
| 295 | + - `ROLLBACK_FAILED`(不可重试,转 `blocked_pending_human`,`blocked_reason='rollback_failed'`):连 `previous_instance_dir` 也起不来,或 `git revert` 冲突(见 `supervisor-auto-rollback-design.md` §10 开放问题 3)——这是本机制唯一保留的人工介入点,触发条件严格限定为"自动恢复机制本身失效",不是"代码判断谁对谁错" | ||
| 296 | +- `owner_team`:deploy-supervisor(独立于 `graph-engine`,进程边界见 01-M 1.15 节) |
docs/sdd/04-P-流程与状态流转.md
0 → 100644
| 1 | +++ a/docs/sdd/04-P-流程与状态流转.md | ||
| 1 | +# 04-P-流程与状态流转 | ||
| 2 | + | ||
| 3 | +## 4.1 流程:`p_org_task_lifecycle`(顶层任务生命周期) | ||
| 4 | + | ||
| 5 | +- `start_state`:`queued` | ||
| 6 | +- `end_state`:`done` | `blocked_pending_human`(终态之一,需人工介入才能继续) | ||
| 7 | + | ||
| 8 | +| from | to | trigger | action | guard | error_state | retry_max | | ||
| 9 | +|---|---|---|---|---|---|---| | ||
| 10 | +| `queued` | `planning_brd_prd_sdd` | `c_org_node_trigger(planner)` | `c_work_graph_plan` + `c_work_graph_execute` | `max_concurrent_goals` 未超限 | `blocked_pending_human` | 0 | | ||
| 11 | +| `planning_brd_prd_sdd` | `coding` | `planner_work_graph_done` | `c_org_node_trigger(coder)` | 四类 Artifact(BRD/PRD/SDD/Benchmark)通过一致性/覆盖率校验 | `planning_failed` | 2 | | ||
| 12 | +| `planning_failed` | `planning_brd_prd_sdd` | `retry` | 重新触发 Planner Work Graph | `retry_count < 2` | `blocked_pending_human` | - | | ||
| 13 | +| `coding` | `validating` | `coder_work_graph_done` | `c_org_node_trigger(validator)` | Coder 自测(unit+integration test)通过 | `coding_failed` | 2 | | ||
| 14 | +| `coding_failed` | `coding` | `retry` | 重新触发 Coder Work Graph | `retry_count < 2` | `blocked_pending_human` | - | | ||
| 15 | +| `validating` | `done` | `validator_pass` | 关闭任务 | 全部 Benchmark/测试用例通过 | - | 0 | | ||
| 16 | +| `validating` | `routed_fast_lane` | `validator_fail` | `c_bug_record_create` + `c_org_loop_route(fast_lane)` | `bug_record.suggested_route=fast_lane` 且 `task.loop_round < 3` | `blocked_pending_human` | - | | ||
| 17 | +| `validating` | `routed_full_replan` | `validator_fail` | `c_bug_record_create` + `c_org_loop_route(full_replan)` | `bug_record.suggested_route=full_replan` 且 `task.loop_round < 3` | `blocked_pending_human` | - | | ||
| 18 | +| `routed_fast_lane` | `coding` | `route_applied` | `task.loop_round += 1`,直达 Coder | - | - | - | | ||
| 19 | +| `routed_full_replan` | `planning_brd_prd_sdd` | `route_applied` | `task.loop_round += 1`,回到 Planner | - | - | - | | ||
| 20 | +| `*`(任意态) | `blocked_pending_human` | `loop_round >= 3` OR `budget_exceeded` | 记录 `blocked_reason`,调度器跳过该任务、执行队列中其他可并行任务(架构文档 4.3/7.4 节) | - | - | - | | ||
| 21 | +| `blocked_pending_human` | 恢复到触发前状态 | `c_org_loop_route(manual_resume)` | 人工在 `v_task_detail` 点击"人工干预" | 需人工在管理后台确认 | - | - | | ||
| 22 | + | ||
| 23 | +补充说明:`loop_round >= 3` 的判定复用架构文档第 10 节课程映射里 `loop-until-dry` 的思路作为**未来优化候选**——MVP 先用固定 3 轮上限(更简单、可预测),若后续 Validator 承担"未知规模缺陷扫描"类任务,再评估切换为连续 K 轮无新发现即停止的收敛判定(见 06 节验收锚点会标注为已知简化项,非本期强制要求)。 | ||
| 24 | + | ||
| 25 | +## 4.2 流程:`p_work_graph_execution`(单次 Work Graph 执行生命周期) | ||
| 26 | + | ||
| 27 | +- `start_state`:`planning` | ||
| 28 | +- `end_state`:`done` | `failed` | ||
| 29 | + | ||
| 30 | +| from | to | trigger | action | guard | error_state | retry_max | | ||
| 31 | +|---|---|---|---|---|---|---| | ||
| 32 | +| `planning` | `running` | `c_work_graph_plan` 完成 | 落 `graph_manifest`,调用 `c_work_graph_execute` | `graph_manifest` 通过反模式门禁(见 08 节) | `failed` | 0 | | ||
| 33 | +| `running` | `merging` | 全部 fan-out 节点 `done` 或 `filter(Boolean)` 后集合非空 | 执行 fan-in 边(零 token 代码边或 Agent 裁决边) | - | `failed` | 0 | | ||
| 34 | +| `merging` | `verifying` | fan-in 完成 | 触发本节点自身的验证规则(第 5.3 节生命周期"Merge/Verify"步骤) | - | `failed` | 0 | | ||
| 35 | +| `verifying` | `done` | 验证通过 | `c_artifact_write` 落最终产物 | - | - | - | | ||
| 36 | +| `verifying` | `reflecting` | 验证不通过 | 记录差距,决定重试路径 | `retry_count < node_level_max` | `failed` | 由具体 Work Node 定义 | | ||
| 37 | +| `reflecting` | `running` | reflection 决定重跑 | 回到受影响的最小子图(非整图重跑,对应课程 SDD 生成器"回灌策略:只回灌受影响子图") | - | - | - | | ||
| 38 | +| `running` | `blocked` | 节点失败且 `.filter(Boolean)` 后集合为空(全部并行分支失败) | 冒泡到 `p_org_task_lifecycle` 的 `*_failed` 态 | - | - | - | | ||
| 39 | + | ||
| 40 | +## 4.3 流程:`p_validator_root_cause_trace`(Validator 根因溯源) | ||
| 41 | + | ||
| 42 | +- `start_state`:`test_failed` \| `human_reported`(新增第二个起点,架构文档第 13 节人工反馈 bug 诉求) | ||
| 43 | +- `end_state`:`bug_recorded` | ||
| 44 | + | ||
| 45 | +| from | to | trigger | action | guard | error_state | retry_max | | ||
| 46 | +|---|---|---|---|---|---|---| | ||
| 47 | +| `human_reported` | `tracing_lineage` | `c_bug_report_create` 产生 `bug_record(source=human, status=open)` | Validator 认领该记录(`status: open→triaging`),若 `related_task_id` 为空则先反查最近相关 task | - | `tracing_failed` | 1 | | ||
| 48 | +| `test_failed` | `tracing_lineage` | 某功能点测试/benchmark 不通过 | 读取对应 SDD/PRD/BRD 版本 + Coder 实现记录(血缘追溯) | - | `tracing_failed` | 1 | | ||
| 49 | +| `tracing_lineage` | `judging_root_cause` | 血缘读取完成 | 多视角校验:正确性视角 / 需求完整性视角 / Skill 系统性缺陷视角(对应课程"多视角差异化校验") | 至少 2/3 视角一致才定案,否则升级人工 | `inconclusive_escalate` | - | | ||
| 50 | +| `judging_root_cause` | `bug_recorded` | 归因定案(`planning`\|`coder`\|`skill_defect` 三选一) | `c_bug_record_create` | `root_cause_category` 非空且有 `root_cause_detail` | - | - | | ||
| 51 | +| `judging_root_cause` | `inconclusive_escalate` | 多视角判断不一致 | 标记 `blocked_pending_human`,附带三视角原始判断供人工裁决 | - | - | - | | ||
| 52 | +| `bug_recorded` | `skill_iteration_triggered` | `root_cause_category=skill_defect` | `c_skill_update` | 必须提供 `triggered_by_bug_id` | `skill_update_failed` | 1 | | ||
| 53 | + | ||
| 54 | +补充说明:`judging_root_cause→bug_recorded` 的动作在两条起点路径下语义略有差异——`test_failed` 起点是全新创建 `bug_record`(`c_bug_record_create`);`human_reported` 起点是对已存在的 `bug_record` 做字段补全式更新(`root_cause_category`/`root_cause_detail`/`suggested_route` 从 null 补齐,`status: triaging→routed`),不产生重复记录。 | ||
| 55 | + | ||
| 56 | +## 4.4 流程:`p_budget_guardrail`(预算护栏) | ||
| 57 | + | ||
| 58 | +- `start_state`:`monitoring` | ||
| 59 | +- `end_state`:`paused` | ||
| 60 | + | ||
| 61 | +| from | to | trigger | action | guard | error_state | retry_max | | ||
| 62 | +|---|---|---|---|---|---|---| | ||
| 63 | +| `monitoring` | `monitoring` | 每次 `c_work_node_run` 前调用 `c_budget_check` | 累加 `current_usage` | `current_usage < limit_value` | - | - | | ||
| 64 | +| `monitoring` | `paused` | `c_budget_check` 返回 `allowed=false` | 对应 `task` 转 `blocked_pending_human`,`blocked_reason='budget_exceeded'` | - | - | - | | ||
| 65 | +| `paused` | `monitoring` | 人工在 `v_budget_settings` 调高额度或人工 `manual_resume` | 恢复调度 | - | - | - | | ||
| 66 | + | ||
| 67 | +补充:预算护栏和"平台层限额"(架构文档 7.5 节)是两条不同的路径,处理逻辑不同,不要合并成同一个 `blocked_reason`—— | ||
| 68 | + | ||
| 69 | +- **`budget_guardrail_config` 触发**(本节表格):这是人工主动配置的预算上限,超出后需要人工判断要不要提额,走 `blocked_pending_human`、`blocked_reason='budget_exceeded'` | ||
| 70 | +- **Claude Code plan 额度耗尽**(新增子流程 `p_usage_limit_wait`,见下方):这是账号/plan 自身的用量上限,会在固定时间窗口后自然恢复,不需要人工判断,走等待+自动重试,只有等待超时才升级到 `blocked_pending_human` | ||
| 71 | + | ||
| 72 | +### 4.4.1 子流程:`p_usage_limit_wait`(Claude Code plan 额度等待重试,架构文档 7.5 节决策,2026-07-29) | ||
| 73 | + | ||
| 74 | +- `start_state`:`running` | ||
| 75 | +- `end_state`:`done`(恢复正常执行)或 `blocked_pending_human`(超时兜底) | ||
| 76 | + | ||
| 77 | +| from | to | trigger | action | guard | error_state | retry_max | | ||
| 78 | +|---|---|---|---|---|---|---| | ||
| 79 | +| `running` | `waiting_for_quota` | `c_work_node_run`/`c_org_node_trigger` 返回 `USAGE_LIMIT_REACHED` | 记录 `quota_wait_started_at`/`quota_limit_type`,写 `execution_event(event_type=quota_wait_started)` | - | - | - | | ||
| 80 | +| `waiting_for_quota` | `waiting_for_quota` | 退避轮询到点(5→10→20→30 分钟,上限 30 分钟) | 重新发起原调用作为探测 | 距 `quota_wait_started_at` 未超时(`session`\|8h,`weekly`\|9d) | - | 不计入 `retry_count` | | ||
| 81 | +| `waiting_for_quota` | `running` | 探测调用成功 | 恢复正常执行流程 | - | - | - | | ||
| 82 | +| `waiting_for_quota` | `blocked_pending_human` | 距 `quota_wait_started_at` 超过对应超时上限 | `blocked_reason='usage_limit_wait_timeout'` | - | - | - | | ||
| 83 | + | ||
| 84 | +补充:`quota_limit_type=opus` 不走上面的退避轮询——直接把该节点这次调用降级到非 Opus 模型重试(见 03-C 3.6 节),因为 Opus 专属限额和会话/周额度是独立的资源池,切模型能立即恢复工作,没必要等待。 | ||
| 85 | + | ||
| 86 | +`waiting_for_quota` 期间调度器不阻塞整体流水线,继续执行队列中其他不依赖该节点的工作,和 `budget_exceeded` 的"跳过而非全局阻塞"是同一模式(见 7.4 节)。 | ||
| 87 | + | ||
| 88 | +补充:`c_work_graph_execute` 使用 `auto` 权限模式(03-C 3.5 节决策,2026-07-29)时,若返回 `CLASSIFIER_BLOCKED` 错误(分类器连续拦截 3 次或累计 20 次,`-p` 下无人可确认导致会话中止),同样路由到 `blocked_pending_human`,`blocked_reason='classifier_blocked'`,与 `budget_exceeded`/`platform_quota_exhausted` 在 `v_task_detail` 分别展示,避免人工误判为同一类问题;此类阻塞的恢复路径是人工复核分类器拦截的具体动作,判断是补充信任规则(架构文档需补充"受信任基础设施"配置项,MVP 阶段留空,先走人工 `manual_resume`)还是这确实是个应该被拦下的危险操作。 | ||
| 89 | + | ||
| 90 | +## 4.5 流程:`p_log_retention_cleanup`(日志定时清理,架构文档 13.1 节决策 4) | ||
| 91 | + | ||
| 92 | +- `start_state`:`idle` | ||
| 93 | +- `end_state`:`idle`(周期性流程,非一次性终结) | ||
| 94 | + | ||
| 95 | +| from | to | trigger | action | guard | error_state | retry_max | | ||
| 96 | +|---|---|---|---|---|---|---| | ||
| 97 | +| `idle` | `scanning` | 定时触发(默认每日一次,具体周期由平台调度配置,非用户可配) | 扫描 `execution_log_file WHERE status='closed'` | - | `scan_failed` | 1 | | ||
| 98 | +| `scanning` | `purging` | 扫描完成,得到候选集合 | 按 `log_retention_config.retention_days` 过滤过期项;若配置了 `max_total_size_mb` 且总量超限,追加按 `created_at` 最旧优先的候选,直到总量回落到上限内 | - | - | - | | ||
| 99 | +| `purging` | `idle` | 逐条删除候选的物理文件 | 删除成功则 `execution_log_file.status→purged`,`purged_at=now()`,写入 `execution_event`(可选,非高优先级事件,用于审计不用于实时推送) | - | `purge_partial_failed` | 0 | | ||
| 100 | +| `purge_partial_failed` | `idle` | 部分文件删除失败(如文件被占用) | 记录失败项,保留 `status=closed` 待下次周期重试,不阻塞其余成功项 | - | - | - | | ||
| 101 | + | ||
| 102 | +约束:清理过程只删除物理文件和更新 `execution_log_file` 状态,不删除 `execution_log_file` 记录本身、不删除任何 `execution_event`/`task`/`artifact` 等业务数据,保证"日志曾经存在过"这件事可审计(对应 01-M 1.13 节约束)。 | ||
| 103 | + | ||
| 104 | +## 4.6 流程:`p_deploy_supervisor_blue_green`(PH5 新增,2026-07-30,见 `docs/supervisor-auto-rollback-design.md`) | ||
| 105 | + | ||
| 106 | +- `start_state`:`staged` | ||
| 107 | +- `end_state`:`finalized`(安全终态)| `blocked_pending_human`(唯一需人工介入的终态,触发条件严格限定为"自动回滚机制本身失败",不是"代码写得不好") | ||
| 108 | + | ||
| 109 | +由 Deploy Supervisor(独立于 Graph Engine 之外的进程,见 01-M 1.15 节命名说明)驱动,触发前提:`bug_record.root_cause_category=core_engine` 的任务经 Coder 修复、Validator PASS、worktree 分支已 merge 回 graphlemon 自身仓库主分支(见 03-C 3.11 节 `core_engine` 路由分支)。全部状态转移均为确定性判断,不接入任何 LLM 判断。 | ||
| 110 | + | ||
| 111 | +| from | to | trigger | action | guard | error_state | retry_max | | ||
| 112 | +|---|---|---|---|---|---|---| | ||
| 113 | +| `staged` | `candidate_starting` | `c_deploy_supervisor_stage` 完成 | 全新目录 checkout(非原地 `git pull`),复制一次性 `candidate_db_path`,拉起候选进程 | - | `blocked_pending_human`(`CHECKOUT_FAILED`) | 0 | | ||
| 114 | +| `candidate_starting` | `health_check_running` | 候选进程启动完成 | 调用 `c_deploy_supervisor_health_check` | - | - | - | | ||
| 115 | +| `health_check_running` | `health_check_failed` | `process_alive`/`http_health`/`canary_task` 任一 fail 或超时 | 触发回滚路径 A(见下方) | - | - | 0(不重试,重试属于"给坏版本第二次机会",要重试整个自我修改需回到 Planner 重新出一版) | | ||
| 116 | +| `health_check_running` | `health_check_passed` | 三项检查全部通过 | 进入 cutover | - | - | - | | ||
| 117 | +| `health_check_passed` | `cutover_in_progress` | `c_deploy_supervisor_cutover` 调用 | Live 停止接受新 `c_org_node_trigger`,在途节点走到可恢复检查点 | - | `blocked_pending_human`(`CUTOVER_FAILED`) | 0 | | ||
| 118 | +| `cutover_in_progress` | `live` | Live 进程退出,候选切换到生产端口/生产库 | `cutover_at=now`,原 Live checkout 保留为 `previous_instance_dir` | - | - | - | | ||
| 119 | +| `live` | `monitoring` | cutover 完成 | `c_deploy_supervisor_monitor` 周期性采样四类信号 | - | - | - | | ||
| 120 | +| `monitoring` | `anomaly_detected` | 任一信号超出 05 节 §5.9 阈值(含 `validator_approval_rate` 偏离基线) | 记录信号快照到 `deployment.rollback_reason` | - | - | - | | ||
| 121 | +| `monitoring` | `monitoring_window_clean` | 监控窗口(`MONITORING_WINDOW_HOURS`/`MONITORING_MIN_TASK_COUNT` 先到者)内无 anomaly | - | - | - | - | | ||
| 122 | +| `monitoring_window_clean` | `finalized` | - | `previous_instance_dir` 可清理 | - | - | - | | ||
| 123 | +| `anomaly_detected` | `rollback_in_progress` | - | 触发回滚路径 B(见下方) | - | - | - | | ||
| 124 | +| `rollback_in_progress` | `rolled_back` | `previous_instance_dir` 重新拉起并通过健康检查 | 绑定 `LIVE_PORT`+`PROD_DB_PATH`,`git revert` 本次合并 commit,创建 `bug_record(root_cause_category=core_engine)` | - | - | - | | ||
| 125 | +| `rollback_in_progress` | `rollback_failed` | 旧版本也起不来,或 `git revert` 冲突 | - | - | `blocked_pending_human`(`blocked_reason='rollback_failed'`) | 0 | | ||
| 126 | +| `health_check_failed` | `rolled_back` | 回滚路径 A 执行完成 | 终止候选进程、删除一次性 DB 副本、`git revert`、创建 `bug_record(root_cause_category=core_engine)` | - | - | - | | ||
| 127 | + | ||
| 128 | +补充说明(回滚路径 A vs B,见 `supervisor-auto-rollback-design.md` §7): | ||
| 129 | + | ||
| 130 | +- **路径 A(`health_check_failed → rolled_back`)**:Live 从未被动过,回滚成本最低——终止候选、删除一次性 DB 副本、`git revert`、发 `bug_record`。整个过程 Live 全程未受影响,调度立即可以继续处理下一个任务。 | ||
| 131 | +- **路径 B(`anomaly_detected → rollback_in_progress → rolled_back`)**:Live 已经是候选版本,需要真正"换回去"——用 `previous_instance_dir` 重新拉起旧版本,绑定生产端口/生产库(生产库全程未被替换,只是换了跑在它前面的代码进程,数据不受影响),对复活的旧版本重跑一遍 pre-cutover 健康检查确认无误,才判定 `rolled_back`。 | ||
| 132 | +- **`rollback_failed → blocked_pending_human` 是本流程唯一保留的人工介入点**,且明确限定触发条件仅为"自动恢复机制本身失效"(旧版本也起不来、DB 不可写等基础设施问题),不是"代码判断谁对谁错"——这一区分需要在 `v_task_detail`/控制台清楚展示,避免用户误以为又回到了"需要人工评审代码改得好不好"的模式(该模式已被用户明确否决,见 05 节 §5.8)。 |
docs/sdd/05-实施约束与依赖.md
0 → 100644
| 1 | +++ a/docs/sdd/05-实施约束与依赖.md | ||
| 1 | +# 05-实施约束与依赖 | ||
| 2 | + | ||
| 3 | +## 5.1 现有资产复用清单(来自本地 Hermes profile 调研,架构文档第 9 节) | ||
| 4 | + | ||
| 5 | +| Org Agent | 复用来源 | 复用内容 | 迁移动作 | | ||
| 6 | +|---|---|---|---| | ||
| 7 | +| Planner | `~/.hermes/profiles/planning/` | `SOUL.md`(16 条根本原则);`planning-requirement-expansion`、`project-brd-planning`、`project-prd-planning`、`project-sdd-planning`(本 SDD 采用的方法论本身)、`project-unit-decomposition`、`review-blocker-verification` 等 skill | `SOUL.md` 正文迁移到目标项目 `.claude/agents/planner.md`(frontmatter 配 `skills:`/`memory: project`/`tools:`/`model:`,见架构文档 7.2 节),skill 迁移到 `graphlemon/skills/*` 供 `skills:` 字段引用 | | ||
| 8 | +| Coder | `~/.hermes/profiles/code/` | `SOUL.md`(认领→实现→测单→证据回写);`code-task-development`、`code-agent-engineering`、`debugging-toolkit`、`software-planning-execution` | 迁移到 `.claude/agents/coder.md` + `graphlemon/skills/*`,**需要改造**:原流程强绑定飞书 Base 认领机制,改为消费 `task` 表 + `c_org_node_trigger` | | ||
| 9 | +| Validator | `~/.hermes/profiles/qa/` | `SOUL.md` + `AGENTS.md`(三态判定、双向可追溯矩阵、工作区权限契约);`qa-task-validation`、`unattended-agent-operations` | 迁移到 `.claude/agents/validator.md`(`AGENTS.md` 的工作区权限契约折入 `tools`/`disallowedTools` frontmatter),双向可追溯矩阵直接对应 04-P `p_validator_root_cause_trace` 的血缘追溯步骤 | | ||
| 10 | + | ||
| 11 | +**不复用**:三个 profile 的 `memories/MEMORY.md`/`USER.md`(内容重叠、未按角色隔离);`.archive/`、`migration/`、`openclaw-imports/` 下的通用助理技能(apple、creative、feishu-lark 等)。 | ||
| 12 | + | ||
| 13 | +## 5.2 飞书 Base 替换计划(架构文档 12.1 节已确认) | ||
| 14 | + | ||
| 15 | +- 不做历史数据自动迁移,MVP 从空任务队列开始运行 | ||
| 16 | +- 原 `lark-cli --as user` 的认领/回写语义,改由 `c_org_node_trigger` + `task.status` 状态机承接 | ||
| 17 | +- 原飞书 Base 的人工可视化能力,由本 SDD 02-V 的 `v_task_board`/`v_task_detail`/`v_graph_visualizer` 承接 | ||
| 18 | + | ||
| 19 | +## 5.3 执行后端范围(架构文档 7.2 节决策,2026-07-29,取代此前"双后端不对等"框架) | ||
| 20 | + | ||
| 21 | +- MVP 阶段只实现并使用 **Claude Code ACP** 一种执行后端,不考虑 Hermes CLI 作为执行后端。此前"Org Graph 层两后端一致、Work Graph 层两后端不对等"的处理方案已作废——不存在第二个后端。 | ||
| 22 | +- Org Graph 层:`c_org_node_trigger` 底层是 `claude --agent <planner|coder|validator> -p "..."`,Org Agent 身份由目标项目 `.claude/agents/*.md` 承载(见 5.1 节迁移动作) | ||
| 23 | +- Work Graph 层:`c_work_graph_execute` 跑完整的 fan-out/fan-in/loop 能力(见 03-C 3.5 节),MVP 验收标准是这一条能跑通即可,不再需要"至少验证一种后端"这类留有余地的表述 | ||
| 24 | +- `v_graph_visualizer` 不再需要"简化模式"展示逻辑(02-V 2.7 节保持单一展示模式) | ||
| 25 | +- Executor Interface 的抽象层设计仍保留为未来扩展点,但不是当前实现范围 | ||
| 26 | + | ||
| 27 | +## 5.4 依赖的外部能力(对应 00 节 A-1,已通过 spike 实测确认) | ||
| 28 | + | ||
| 29 | +- Claude Code 动态工作流的 `agent()`/`parallel()`/`pipeline()` 原语已实测确认可通过 `claude -p`(非交互式)编程方式调用,方案 A 成立(`spikes/a1-dynamic-workflows/`,真实账号验证,2026-07-29,成本 $0.46): | ||
| 30 | + - `claude -p 'Run /<workflow> with args {...}' --output-format json --json-schema <schema> < /dev/null` 无交互提示、无挂起、40s 内完成,`structured_output` 精确匹配预期输出,`permission_denials` 为空 | ||
| 31 | + - **发现的约束**:不能加 `--bare`。`--bare` 会跳过登录态/凭据发现(报 `Not logged in`,与是否存在 workflow 无关),必须用不带 `--bare` 的完整冷启动调用,代价是启动稍慢,但可靠性优先 | ||
| 32 | + - 具体实现契约(写入 workflow 脚本、子进程调用、解析 `structured_output`、回填预算用量)已落地到 03-C 3.5 节 | ||
| 33 | +- 原风险已解除,不再需要在实施第一周单独排期做技术验证;架构文档 10.1 节的方案 B(完全自研 Graph Engine)作为长期备选保留,暂不启用 | ||
| 34 | + | ||
| 35 | +## 5.4.2 Claude Code plan 额度等待重试参数(架构文档 7.5 节决策,2026-07-29) | ||
| 36 | + | ||
| 37 | +- 退避轮询间隔(5→10→20→30 分钟,上限 30 分钟)、超时兜底阈值(session 8 小时、weekly 9 天)是 MVP 阶段的硬编码默认值,不在管理后台开放用户配置,和 `log_retention_config` 的"保留天数可配、清理频率不可配"是同一个 MVP 简化原则 | ||
| 38 | +- 这组参数的合理性依赖 Claude Code 官方文档给出的窗口周期(5 小时滚动 / 7 天)本身不变;若未来 Claude Code 调整了额度窗口周期,需要同步调整这里的超时阈值,不是一次性写死可以忽略的常量 | ||
| 39 | + | ||
| 40 | +## 5.3.1 `c_work_graph_execute` 权限模式决策(03-C 3.5 节,2026-07-29) | ||
| 41 | + | ||
| 42 | +- 选定 `--permission-mode auto`,而非 `--dangerously-skip-permissions`/`bypassPermissions`:后者官方文档明确要求只能在隔离容器/VM+无网络访问环境下使用;graphlemon 目前的节点隔离手段(git worktree)是同机目录级隔离,达不到这个前提,用 `bypassPermissions` 会暴露 prompt injection 风险 | ||
| 43 | +- **约束 1(模型选型)**:`auto` mode 要求会话模型为 Claude Sonnet 5 / Opus 4.7+ / Fable 5(Anthropic API 上),架构文档 10 节"按节点分层选模型"给 Coder/Validator 节点选便宜模型时不能选到这个集合以外 | ||
| 44 | +- **约束 2(失败模式)**:`-p` 非交互模式下,分类器连续拦截 3 次或累计 20 次会导致会话直接中止(无人可确认),对应 `c_work_graph_execute` 新增 `CLASSIFIER_BLOCKED` 错误码,路由到 `blocked_pending_human`(04-P 已补充) | ||
| 45 | +- **待办(非 MVP 阻塞项,值得后续排期)**:`auto` mode 支持通过"受信任基础设施"配置(`autoMode.environment`)减少误拦截,architecture.md 目前未定义 graphlemon 自己的可信基础设施清单(例如目标项目的 git remote、允许访问的包registry等),MVP 阶段先接受更高的误拦截率、靠人工在 `v_task_detail` 复核放行,后续可评估是否要把这份配置纳入 `org_graph_definition` 或新增专门的设置页 | ||
| 46 | + | ||
| 47 | +## 5.4.1 控制台载体形态的实施约束(架构文档 13.1 节决策 5,已确认) | ||
| 48 | + | ||
| 49 | +- 控制台是本地 Web 应用,后端是本机常驻进程(同时也承担 Graph Engine 的调度职责),前端是普通网页,两者本地通信(如 `localhost` 端口) | ||
| 50 | +- `c_pick_folder` 依赖后端所在操作系统的原生对话框能力:macOS 用 `osascript`,Windows/Linux 需要各自的原生方案(如 PowerShell 脚本 / `zenity`/`kdialog`),实施时需要按目标操作系统分别验证,不是一套代码天然跨平台 | ||
| 51 | +- 若后端运行在无图形界面的环境(纯服务器/容器),`c_pick_folder` 会返回 `DIALOG_UNAVAILABLE`,`v_project_setup` 需要有手动路径输入的兜底交互(MVP 阶段可以直接做成兜底默认项,不必等图形环境不可用时才现场适配) | ||
| 52 | + | ||
| 53 | +## 5.5 上线顺序与兼容性 | ||
| 54 | + | ||
| 55 | +1. 先落地 01-M 的数据模型(`task`/`artifact`/`skill` 等核心表)+ Git 仓库目录结构(架构文档 7.3 节) | ||
| 56 | +2. 再落地 Org Graph 层接口(3.1-3.3, 3.11-3.13)+ `p_org_task_lifecycle` 状态机,此时可先用手工调用验证 Planner/Coder/Validator 三个 Org Agent 分别可被触发 | ||
| 57 | +3. 最后落地 Work Graph 层(3.4-3.6)与 Claude Code 动态工作流的集成;A-1 已通过 spike 实测确认(见 5.4 节),可按 03-C 3.5 节的已验证实现契约直接开发,无需再单独排期做技术验证 | ||
| 58 | + | ||
| 59 | +## 5.6 迁移与回滚约束 | ||
| 60 | + | ||
| 61 | +- `org_graph_definition` 的变更(Org Graph 结构本身)走人工 PR 评审流程,不允许系统自主提交 | ||
| 62 | +- `skill` 的自主迭代(`c_skill_update`)允许系统自主提交,但必须可通过 `c_skill_rollback` 一键回滚到迭代前版本 | ||
| 63 | +- MVP 阶段数据库 schema 变更暂不考虑蓝绿发布/双写迁移(单用户本地运行,允许停机迁移)——**例外**:graphlemon 平台自身的 self-target 场景不适用这条"允许停机迁移"的简化假设,见 5.8 节 | ||
| 64 | + | ||
| 65 | +## 5.7 graphlemon 自身实现技术栈(决策,2026-07-30) | ||
| 66 | + | ||
| 67 | +- graphlemon 自身的后端(HTTP API + WebSocket/SSE server + Graph Engine 调度)与 PH5 的 Deploy Supervisor 进程,统一采用**纯 stdlib Python** 实现,不依赖任何第三方包(不用 Flask/FastAPI/Express/Node.js),对齐前身项目 graphloop 的既有约定(`graphloop/CLAUDE.md`原话:"Everything is stdlib: argparse, sqlite3, http.server, subprocess, threading, unittest, etc.") | ||
| 68 | +- 前端是本地 Web 应用(浏览器页面),后端用 `http.server`/`socketserver` 等 stdlib 原生能力提供 HTTP API;WebSocket 若 stdlib 支持不足,实施时(U-007)需要验证并可退化为 SSE/长轮询,但仍在 stdlib 范围内解决,不引入第三方包;前端不引入 npm/构建步骤,可以是原生 HTML/CSS/JS 或参考 graphloop `graph.html`/`project.html` 的单文件 no-build 页面模式 | ||
| 69 | +- **例外**:这条约束只针对 graphlemon 平台自身的实现语言,不代表 graphlemon 生成/审查的目标项目代码也必须是 Python——目标项目用什么语言由目标项目自己的技术栈决定 | ||
| 70 | +- **影响**:development-plan.md 的 U-007(本地后端骨架)、U-008(前端骨架)、U-036(Deploy Supervisor 骨架)的 Done 标准隐含"纯 stdlib"这条约束 | ||
| 71 | +- **源码目录约定(2026-07-30 补充)**:graphlemon 平台自身的后端源码(U-001 的 schema 实现、U-007 起的 HTTP/WebSocket 后端)落在 graphlemon 仓库根目录的 `src/`。这与 U-002 在**目标项目**仓库内生成的 `src/` 是两个完全不同的目录(分属两个物理仓库,见架构文档 §13.1"目标项目是独立代码空间"决策)——PH5 self-target 场景下二者会同时存在于同一台机器上,实现时不要混淆,必要时以绝对路径而非相对路径区分 | ||
| 72 | + | ||
| 73 | +## 5.8 self-target 场景治理规则(`self-development-feasibility.md` 缺口 1/3,2026-07-30 回填) | ||
| 74 | + | ||
| 75 | +- **worktree 无条件强制**:`target_folder_path == graphlemon 平台自身安装路径` 时,无论并行还是串行,Coder/Validator 一律强制使用 `git_worktree_ref`,不适用 03-C 3.6 节"仅并行写代码节点才强制申请 worktree"的一般规则——这是该规则的 self-target 特例 | ||
| 76 | +- **`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` | ||
| 77 | +- **不要求提交前人工评审**:`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 变更同样走蓝绿部署+监控流程,不是简单停机迁移 | ||
| 78 | + | ||
| 79 | +## 5.9 Deploy Supervisor MVP 固定参数(`docs/supervisor-auto-rollback-design.md` §9,2026-07-30 回填) | ||
| 80 | + | ||
| 81 | +| 参数 | 草案值 | 说明 | | ||
| 82 | +|---|---|---| | ||
| 83 | +| `HEALTH_CHECK_STARTUP_TIMEOUT` | 30s | Candidate 进程从拉起到能响应探活 | | ||
| 84 | +| `HEALTH_CHECK_HTTP_TIMEOUT` | 10s | 单次 `/api/health` 请求 | | ||
| 85 | +| `HEALTH_CHECK_CANARY_TIMEOUT` | 120s | 一次最小 canary 任务从提交到出结果 | | ||
| 86 | +| `MONITOR_SAMPLE_INTERVAL` | 5min | process/http 采样间隔 | | ||
| 87 | +| `CANARY_INTERVAL` | 1h | 监控期 canary 任务运行间隔 | | ||
| 88 | +| `MAX_CRASH_COUNT` / `CRASH_WINDOW` | 2 次 / 10min | 判定进程反复崩溃 | | ||
| 89 | +| `MAX_CONSECUTIVE_HTTP_FAIL` | 3 次 | 判定健康端点持续不可用 | | ||
| 90 | +| `VALIDATOR_RATE_DEVIATION_THRESHOLD` | ±30 个百分点 | 判定 Validator 行为异常(`validator_approval_rate` 信号阈值) | | ||
| 91 | +| `MONITORING_WINDOW_HOURS` | 24h | 监控窗口时长上限 | | ||
| 92 | +| `MONITORING_MIN_TASK_COUNT` | 10 个真实任务 | 监控窗口任务数上限,先到者结束 | | ||
| 93 | + | ||
| 94 | +**与 5.4.2 节额度等待参数的差异**:5.4.2 节的退避/超时参数依据是 Anthropic 平台文档定义的外部事实(5 小时/7 天窗口周期),可以合理标注为"MVP-fixed";本节这组阈值是纯粹的工程经验草案,没有类似的外部权威来源,标注为"可调,需在真实跑通几轮自我修改后实测校准",不建议实现时直接当成不可变常量硬编码进测试断言。 |
docs/sdd/06-验收与测试锚点.md
0 → 100644
| 1 | +++ a/docs/sdd/06-验收与测试锚点.md | ||
| 1 | +# 06-验收与测试锚点 | ||
| 2 | + | ||
| 3 | +对应架构文档第 11 节 MVP 验收标准,逐条映射到 requirement_id,并给出 M/V/C/P 覆盖和至少 3 类验收用例(happy / boundary / failure)。场景统一采用"贪吃蛇游戏"作为验收任务。 | ||
| 4 | + | ||
| 5 | +## REQ-01:Org Graph 三节点链路可触发 | ||
| 6 | + | ||
| 7 | +- 覆盖:M=`org_graph_definition`,`task`;C=`c_task_create`,`c_org_node_trigger`;P=`p_org_task_lifecycle` | ||
| 8 | +- 验收用例: | ||
| 9 | + - happy:创建 `goal_text="实现贪吃蛇游戏"` 的 task,依次触发 planner/coder/validator 三个 org_node,`task.status` 按状态机正确流转 | ||
| 10 | + - boundary:`max_concurrent_goals=1` 时,同时创建第二个 task,验证其落 `queued` 而不被立即调度 | ||
| 11 | + - failure:`goal_text` 为空时 `c_task_create` 返回 `INVALID_PARAM` | ||
| 12 | + | ||
| 13 | +## REQ-02:Planner 产出四类 Artifact 并通过校验 | ||
| 14 | + | ||
| 15 | +- 覆盖:M=`artifact`,`work_graph_run`;V=`v_task_detail`;C=`c_work_graph_plan`,`c_work_graph_execute`,`c_artifact_write`;P=`p_work_graph_execution` | ||
| 16 | +- 验收用例: | ||
| 17 | + - happy:Planner Work Graph 跑完后,`artifact` 表中存在 `brd`/`prd`/`sdd`/`benchmark` 四条记录,且 `depends_on_artifact_ids` 血缘链完整(`sdd`→`prd`→`brd`) | ||
| 18 | + - boundary:PRD 中出现互相矛盾的场景假设时,一致性校验应在 `verifying` 态被拦截,触发 `reflecting` 而非直接放行 | ||
| 19 | + - failure:`sdd` 生成时找不到对应 `prd` 版本引用,`c_artifact_write` 应拒绝写入(`depends_on_artifact_ids` 指向不存在的 `artifact_id`) | ||
| 20 | + | ||
| 21 | +## REQ-03:Coder 依据 SDD 完成编码并自测通过 | ||
| 22 | + | ||
| 23 | +- 覆盖:M=`work_node_execution`;C=`c_work_node_run`;P=`p_work_graph_execution` | ||
| 24 | +- 验收用例: | ||
| 25 | + - happy:Coder Work Graph 完成后产出 `code_diff` 与 `test_report` 两类 artifact,且 unit + integration test 全部通过 | ||
| 26 | + - boundary:两个无依赖的模块并行编码,验证各自 `git_worktree_ref` 不冲突,合并后代码无冲突残留 | ||
| 27 | + - failure:某模块单测失败,`work_node_execution.status` 应为 `reflecting`→`retrying`,达到节点级重试上限后冒泡为 `blocked` | ||
| 28 | + | ||
| 29 | +## REQ-04:Validator 完成测试/Benchmark + 根因归属 + 结构化 bug list | ||
| 30 | + | ||
| 31 | +- 覆盖:M=`bug_record`;C=`c_bug_record_create`;P=`p_validator_root_cause_trace` | ||
| 32 | +- 验收用例: | ||
| 33 | + - happy:人为植入一个"边界撞墙判定错误"的 bug,Validator 应判定 `root_cause_category=coder`(而非 planning 或 skill_defect),且 `root_cause_detail` 引用具体 SDD 章节和代码位置 | ||
| 34 | + - boundary:植入一个"PRD 未定义撞墙行为"的需求缺口型 bug,Validator 应判定 `root_cause_category=planning` | ||
| 35 | + - failure:三视角判断不一致时,流程应落在 `inconclusive_escalate` 而不是强行给出一个归因结论 | ||
| 36 | + | ||
| 37 | +## REQ-05:Org-level Loop 完整闭环 | ||
| 38 | + | ||
| 39 | +- 覆盖:C=`c_org_loop_route`;P=`p_org_task_lifecycle` | ||
| 40 | +- 验收用例: | ||
| 41 | + - happy:REQ-04 的 bug 记录经 `suggested_route=fast_lane` 路由后直达 Coder,修复后重新进入 Validator,最终 `task.status=done`,全程 `loop_round` 从 0 增至 1 | ||
| 42 | + - boundary:`suggested_route=full_replan` 时应回到 Planner 而非 Coder,验证路由分支正确 | ||
| 43 | + - failure:连续 3 轮同一 bug 未收敛,`task.status` 应转 `blocked_pending_human` 且调度器继续执行队列中其他任务(若有) | ||
| 44 | + | ||
| 45 | +## REQ-06:Graph Engine 断点恢复 | ||
| 46 | + | ||
| 47 | +- 覆盖:M=`work_graph_run`,`work_node_execution`;P=`p_work_graph_execution` | ||
| 48 | +- 验收用例: | ||
| 49 | + - happy:在 `work_graph_run.status=running` 时人为中断进程,重启后能从已持久化的 `work_node_execution` 状态继续,不重跑已 `done` 的节点 | ||
| 50 | + - boundary:中断发生在某节点 `retrying` 中途,重启后应恢复到该节点重试逻辑而非跳过 | ||
| 51 | + - failure:持久化数据损坏(模拟)时,系统应明确报错并转 `blocked_pending_human`,而不是静默丢失进度继续跑 | ||
| 52 | + | ||
| 53 | +## REQ-07:执行器抽象层(Claude Code ACP 后端)可用 | ||
| 54 | + | ||
| 55 | +- 覆盖:C=`c_work_graph_execute`;对应 05 节 5.3(架构文档 7.2 节决策,2026-07-29:MVP 阶段只用 Claude Code ACP 一种后端,不再考虑 Hermes CLI) | ||
| 56 | +- 验收用例: | ||
| 57 | + - happy:Claude Code ACP 后端下完整跑通 REQ-01~06 全部场景 | ||
| 58 | + - boundary:`claude -p` 调用不加 `--bare`(见 A-1 spike 结论),确认额外冷启动开销在可接受范围内 | ||
| 59 | + - failure:auto mode 分类器连续拦截 3 次或累计 20 次,返回 `CLASSIFIER_BLOCKED`,task 转 `blocked_pending_human`,`blocked_reason=classifier_blocked` | ||
| 60 | + | ||
| 61 | +## REQ-08:Skill 自主迭代 + Git 可追溯可回滚 | ||
| 62 | + | ||
| 63 | +- 覆盖:M=`skill`,`skill_iteration`;V=`v_skill_audit_log`;C=`c_skill_update`,`c_skill_rollback` | ||
| 64 | +- 验收用例: | ||
| 65 | + - happy:REQ-04 中判定 `root_cause_category=skill_defect` 的 bug 触发 `c_skill_update`,生成独立 Git commit,`v_skill_audit_log` 可查看该次变更 | ||
| 66 | + - boundary:`triggered_by_bug_id` 缺失时 `c_skill_update` 应拒绝(不允许无来源变更) | ||
| 67 | + - failure:`c_skill_rollback` 到不存在的 `iteration_id` 应返回 `ITERATION_NOT_FOUND` | ||
| 68 | + | ||
| 69 | +## REQ-09:循环终止与人工升级 | ||
| 70 | + | ||
| 71 | +- 覆盖:P=`p_org_task_lifecycle` 的 `blocked_pending_human` 分支 | ||
| 72 | +- 验收用例:(与 REQ-05 failure 用例共享验证场景) | ||
| 73 | + - happy:`blocked_pending_human` 后,人工在 `v_task_detail` 点击"人工干预"(`manual_resume`),任务能恢复到触发前状态继续执行 | ||
| 74 | + - boundary:多个任务同时有一个 `blocked_pending_human`、其余 `queued` 时,调度器应继续调度 `queued` 任务,不被阻塞任务卡住 | ||
| 75 | + - failure:`manual_resume` 在任务已经是 `done` 终态时调用,应返回明确错误而非误操作已完成任务 | ||
| 76 | + | ||
| 77 | +## REQ-10:预算护栏双层机制 | ||
| 78 | + | ||
| 79 | +- 覆盖:M=`budget_guardrail_config`;V=`v_budget_settings`;C=`c_budget_check`,`c_budget_config_update`;P=`p_budget_guardrail` | ||
| 80 | +- 验收用例: | ||
| 81 | + - happy:配置 `limit_type=token_count, limit_value=<小值>`,运行到超限后任务自动转 `blocked_pending_human`,`blocked_reason='budget_exceeded'` | ||
| 82 | + - boundary:调高额度后(`c_budget_config_update`),`paused` 态任务能恢复调度 | ||
| 83 | + - failure:`budget_guardrail_config` 触发的超限走 `blocked_pending_human`;Claude Code plan 额度耗尽走独立的 `p_usage_limit_wait` 子流程(见 REQ-14),两者不应共用同一个 `blocked_reason` | ||
| 84 | + | ||
| 85 | +## REQ-14:Claude Code plan 额度耗尽的等待重试(架构文档 7.5 节决策,2026-07-29) | ||
| 86 | + | ||
| 87 | +- 覆盖:M=`work_node_execution`(`status=waiting_for_quota`、`quota_wait_started_at`、`quota_limit_type`);V=`v_dashboard`,`v_graph_visualizer`;C=`c_work_node_run`,`c_org_node_trigger`;P=`p_usage_limit_wait` | ||
| 88 | +- 验收用例: | ||
| 89 | + - happy:模拟 `claude -p` 返回 `You've hit your session limit`,`work_node_execution.status` 转 `waiting_for_quota`,退避轮询后探测成功恢复正常执行,全程不产生 `blocked_pending_human` | ||
| 90 | + - boundary:`quota_limit_type=opus` 时不走退避轮询,直接降级到非 Opus 模型重试 | ||
| 91 | + - failure:模拟超过超时阈值(`session` 8 小时 / `weekly` 9 天)仍未恢复,转 `blocked_pending_human`,`blocked_reason='usage_limit_wait_timeout'` | ||
| 92 | + | ||
| 93 | +## REQ-11:项目创建与人机交互入口(架构文档第 13 节) | ||
| 94 | + | ||
| 95 | +- 覆盖:M=`project`,`bug_record`;V=`v_project_setup`,`v_goal_composer`,`v_bug_feedback`;C=`c_project_create`,`c_task_create`,`c_bug_report_create`;P=`p_validator_root_cause_trace`(human_reported 起点) | ||
| 96 | +- 验收用例: | ||
| 97 | + - happy:选择一个已有 Git 仓库的本地文件夹创建项目 → `.graphlemon/` 子目录被创建 → 通过 `v_goal_composer` 提出"实现贪吃蛇游戏"目标 → 成功创建 task 并进入 Org Graph | ||
| 98 | + - boundary:选择一个非 Git 仓库的文件夹,用户确认初始化后应执行 `git init` 并继续;用户不确认则整个创建流程中止,不留下任何写入 | ||
| 99 | + - failure:对同一 `target_folder_path` 重复调用 `c_project_create`,应幂等返回已有项目而非报错或创建重复记录;`v_bug_feedback` 提交空 `description` 应被前端和后端双重拦截 | ||
| 100 | + | ||
| 101 | +## REQ-12:实时看板(架构文档 13.1 节决策 3) | ||
| 102 | + | ||
| 103 | +- 覆盖:M=`execution_event`;V=`v_dashboard`,`v_graph_visualizer`,`v_task_detail`;C=`c_ws_subscribe` | ||
| 104 | +- 验收用例: | ||
| 105 | + - happy:某 `work_node_execution.status` 变化后,`v_graph_visualizer` 对应节点应在可感知的短时间内(作为非功能验收目标,建议 ≤2 秒)完成着色更新,无需人工刷新页面 | ||
| 106 | + - boundary:WebSocket 连接断开后自动重连,重连期间页面展示"实时连接已断开,正在重连"而不是冻结或报错崩溃;重连后能补齐断线期间遗漏的关键状态(通过重新拉取 `task`/`work_graph_run` 当前快照兜底,不要求完整补齐每一条中间事件) | ||
| 107 | + - failure:订阅一个不存在的 `project_id`,连接应被拒绝并返回 `PROJECT_NOT_FOUND`,而不是建立空连接后无声无息不推送任何数据 | ||
| 108 | + | ||
| 109 | +## REQ-13:原始日志控制台内查看 + 定时清理(架构文档 13.1 节决策 4) | ||
| 110 | + | ||
| 111 | +- 覆盖:M=`execution_log_file`,`log_retention_config`;V=`v_log_viewer`,`v_log_retention_settings`;C=`c_execution_log_get`,`c_ws_log_tail`,`c_log_retention_config_update`;P=`p_log_retention_cleanup` | ||
| 112 | +- 验收用例: | ||
| 113 | + - happy:某 Work Node 运行中,`v_log_viewer` 能实时滚动显示新追加的日志内容;节点结束后自动切换为静态模式,仍可完整查看 | ||
| 114 | + - boundary:配置 `retention_days=1`,构造一条 `created_at` 超过 1 天的 `closed` 状态日志,清理任务运行后应转为 `purged` 且物理文件被删除;`v_log_viewer` 打开该日志时展示"已被清理"提示而非报错 | ||
| 115 | + - failure:订阅一个不存在的 `node_exec_id` 的日志流,应返回 `NODE_EXEC_NOT_FOUND`;清理任务遇到某个文件删除失败(如被占用)时,应仅记录该项失败、不影响其余候选项的清理,且该条目下次周期继续重试 | ||
| 116 | + | ||
| 117 | +## REQ-15:Deploy Supervisor 蓝绿部署与自动回滚(PH5 新增,2026-07-30,见 `docs/supervisor-auto-rollback-design.md`) | ||
| 118 | + | ||
| 119 | +- 覆盖: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` | ||
| 120 | +- 验收用例: | ||
| 121 | + - happy(正常 cutover 到 finalize):graphlemon 自身仓库一处真实植入 bug(`self-development-feasibility.md` 第 3 节场景),标记 `root_cause_category=core_engine`,Coder 修复、Validator PASS、worktree 合并主分支后,Deploy Supervisor 依次完成 `staged→candidate_starting→health_check_running→health_check_passed→cutover_in_progress→live→monitoring`,监控窗口内四类信号均正常,最终 `monitoring_window_clean→finalized`,全程无需人工评审代码内容 | ||
| 122 | + - boundary(路径 A:健康检查失败):人为让候选实例的 `canary_task` 检查失败(如返回结果与预期不符),Deploy Supervisor 应立即判定 `health_check_failed`,不重试,直接走回滚路径 A(终止候选、`git revert`、发 `bug_record`),Live 全程未受影响,可通过对比 Live 进程 PID/端口在 cutover 前后未发生变化来验证 | ||
| 123 | + - boundary(路径 B:post-cutover 异常回滚):cutover 完成后,人为把 `validator_approval_rate` 信号推到偏离历史基线阈值(`VALIDATOR_RATE_DEVIATION_THRESHOLD`,05 节 §5.9)之外,Deploy Supervisor 应判定 `anomaly_detected`,走回滚路径 B,最终 `previous_instance_dir` 重新成为 Live,`rolled_back`,生产库数据在整个过程中未丢失(因为生产库本身没有被替换,只是替换了跑在它前面的代码进程) | ||
| 124 | + - failure(回滚本身失败):人为让 `previous_instance_dir` 也无法正常启动(如故意损坏该目录),路径 B 的回滚应终态到 `rollback_failed→blocked_pending_human`,`blocked_reason='rollback_failed'`,且控制台展示的原因应清楚区分"自动恢复机制本身失效"与"代码内容有问题"两种情况,不能让用户误以为又需要人工评审代码 |
docs/sdd/sdd-mvcp-contract.json
0 → 100644
| 1 | +++ a/docs/sdd/sdd-mvcp-contract.json | ||
| 1 | +{ | ||
| 2 | + "meta": { | ||
| 3 | + "project": "graphlemon", | ||
| 4 | + "version": "v1.7.0", | ||
| 5 | + "effective_date": "2026-07-30", | ||
| 6 | + "owner": "bboonnee", | ||
| 7 | + "changelog_v1.7.0": "新增 PH5 自举能力:deployment/deploy_health_signal 实体,bug_record.root_cause_category 新增 core_engine,c_deploy_supervisor_* 五个控制操作,c_org_loop_route 的 core_engine 路由分支,p_deploy_supervisor_blue_green 流程,REQ-15。Deploy Supervisor 与架构文档既有的 Supervisor(调度子 Agent)概念不同,见 docs/supervisor-auto-rollback-design.md 命名说明" | ||
| 8 | + }, | ||
| 9 | + "model_contract": [ | ||
| 10 | + { | ||
| 11 | + "entity": "project", | ||
| 12 | + "owner": "control-plane", | ||
| 13 | + "purpose": "目标项目=独立代码空间,架构文档第13节", | ||
| 14 | + "fields": [ | ||
| 15 | + {"name": "project_id", "type": "string", "nullable": false, "constraints": ["pk"]}, | ||
| 16 | + {"name": "name", "type": "string", "nullable": false, "constraints": []}, | ||
| 17 | + {"name": "target_folder_path", "type": "string", "nullable": false, "constraints": ["unique"], "index": "unique"}, | ||
| 18 | + {"name": "has_git_repo", "type": "boolean", "nullable": false, "constraints": []}, | ||
| 19 | + {"name": "graphlemon_subdir_initialized", "type": "boolean", "nullable": false, "constraints": []}, | ||
| 20 | + {"name": "status", "type": "varchar(20)", "nullable": false, "constraints": ["enum:active|archived"]}, | ||
| 21 | + {"name": "created_at", "type": "timestamp", "nullable": false, "constraints": []} | ||
| 22 | + ] | ||
| 23 | + }, | ||
| 24 | + { | ||
| 25 | + "entity": "org_graph_definition", | ||
| 26 | + "owner": "graph-engine", | ||
| 27 | + "purpose": "Org Graph 静态定义,人工维护", | ||
| 28 | + "fields": [ | ||
| 29 | + {"name": "graph_id", "type": "string", "nullable": false, "constraints": ["pk"]}, | ||
| 30 | + {"name": "version", "type": "string", "nullable": false, "constraints": ["git_ref"]}, | ||
| 31 | + {"name": "nodes", "type": "json", "nullable": false, "constraints": []}, | ||
| 32 | + {"name": "edges", "type": "json", "nullable": false, "constraints": []}, | ||
| 33 | + {"name": "file_path", "type": "string", "nullable": false, "constraints": []}, | ||
| 34 | + {"name": "status", "type": "varchar(20)", "nullable": false, "constraints": ["enum:draft|active|deprecated"]} | ||
| 35 | + ] | ||
| 36 | + }, | ||
| 37 | + { | ||
| 38 | + "entity": "task", | ||
| 39 | + "owner": "graph-engine", | ||
| 40 | + "purpose": "任务队列条目,替代飞书 Base", | ||
| 41 | + "fields": [ | ||
| 42 | + {"name": "task_id", "type": "string", "nullable": false, "constraints": ["pk"]}, | ||
| 43 | + {"name": "project_id", "type": "string", "nullable": false, "constraints": ["fk:project.project_id", "idx"], "index": "idx"}, | ||
| 44 | + {"name": "goal_text", "type": "text", "nullable": false, "constraints": []}, | ||
| 45 | + {"name": "task_type", "type": "varchar(20)", "nullable": false, "constraints": ["enum:goal|bug_fix"]}, | ||
| 46 | + {"name": "origin_ref", "type": "string", "nullable": true, "constraints": ["fk:bug_record.bug_id"]}, | ||
| 47 | + {"name": "created_via", "type": "varchar(20)", "nullable": false, "constraints": ["enum:api|human_console"], "default": "api"}, | ||
| 48 | + {"name": "status", "type": "varchar(30)", "nullable": false, "constraints": ["idx"], "index": "idx"}, | ||
| 49 | + {"name": "route", "type": "varchar(20)", "nullable": true, "constraints": ["enum:fast_lane|full_replan"]}, | ||
| 50 | + {"name": "loop_round", "type": "int", "nullable": false, "constraints": [">=0"], "default": 0}, | ||
| 51 | + {"name": "blocked_reason", "type": "string", "nullable": true, "constraints": []}, | ||
| 52 | + {"name": "assigned_org_node", "type": "varchar(20)", "nullable": false, "constraints": ["enum:planner|coder|validator", "idx"], "index": "idx"}, | ||
| 53 | + {"name": "created_at", "type": "timestamp", "nullable": false, "constraints": []}, | ||
| 54 | + {"name": "updated_at", "type": "timestamp", "nullable": false, "constraints": []} | ||
| 55 | + ] | ||
| 56 | + }, | ||
| 57 | + { | ||
| 58 | + "entity": "work_graph_run", | ||
| 59 | + "owner": "graph-engine", | ||
| 60 | + "purpose": "Org Agent 针对 task 的一次 Work Graph 执行实例", | ||
| 61 | + "fields": [ | ||
| 62 | + {"name": "run_id", "type": "string", "nullable": false, "constraints": ["pk"]}, | ||
| 63 | + {"name": "task_id", "type": "string", "nullable": false, "constraints": ["fk:task.task_id"]}, | ||
| 64 | + {"name": "org_node", "type": "varchar(20)", "nullable": false, "constraints": ["enum:planner|coder|validator"]}, | ||
| 65 | + {"name": "graph_manifest", "type": "json", "nullable": false, "constraints": []}, | ||
| 66 | + {"name": "orchestration_script_ref", "type": "string", "nullable": true, "constraints": []}, | ||
| 67 | + {"name": "status", "type": "varchar(20)", "nullable": false, "constraints": ["enum:planning|running|merging|verifying|reflecting|blocked|done|failed"]}, | ||
| 68 | + {"name": "started_at", "type": "timestamp", "nullable": true, "constraints": []}, | ||
| 69 | + {"name": "completed_at", "type": "timestamp", "nullable": true, "constraints": []}, | ||
| 70 | + {"name": "result_artifact_refs", "type": "json", "nullable": true, "constraints": []} | ||
| 71 | + ] | ||
| 72 | + }, | ||
| 73 | + { | ||
| 74 | + "entity": "work_node_execution", | ||
| 75 | + "owner": "graph-engine", | ||
| 76 | + "purpose": "Work Graph 内每个节点的执行记录与契约校验", | ||
| 77 | + "fields": [ | ||
| 78 | + {"name": "node_exec_id", "type": "string", "nullable": false, "constraints": ["pk"]}, | ||
| 79 | + {"name": "run_id", "type": "string", "nullable": false, "constraints": ["fk:work_graph_run.run_id"]}, | ||
| 80 | + {"name": "node_id", "type": "string", "nullable": false, "constraints": []}, | ||
| 81 | + {"name": "subagent_type", "type": "string", "nullable": false, "constraints": []}, | ||
| 82 | + {"name": "skill_ref", "type": "string", "nullable": false, "constraints": ["fk:skill.skill_id"]}, | ||
| 83 | + {"name": "input_schema_ref", "type": "string", "nullable": false, "constraints": []}, | ||
| 84 | + {"name": "output_schema_ref", "type": "string", "nullable": false, "constraints": []}, | ||
| 85 | + {"name": "input_payload", "type": "json", "nullable": false, "constraints": []}, | ||
| 86 | + {"name": "output_payload", "type": "json", "nullable": true, "constraints": []}, | ||
| 87 | + {"name": "model_tier", "type": "varchar(20)", "nullable": false, "constraints": ["enum:cheap|standard|premium"], "default": "standard"}, | ||
| 88 | + {"name": "status", "type": "varchar(20)", "nullable": false, "constraints": ["enum:pending|running|reflecting|retrying|waiting_for_quota|blocked|done|failed"]}, | ||
| 89 | + {"name": "retry_count", "type": "int", "nullable": false, "constraints": [">=0"], "default": 0}, | ||
| 90 | + {"name": "reflection_notes", "type": "json", "nullable": true, "constraints": []}, | ||
| 91 | + {"name": "git_worktree_ref", "type": "string", "nullable": true, "constraints": []}, | ||
| 92 | + {"name": "quota_wait_started_at", "type": "timestamp", "nullable": true, "constraints": []}, | ||
| 93 | + {"name": "quota_limit_type", "type": "varchar(10)", "nullable": true, "constraints": ["enum:session|weekly|opus"]} | ||
| 94 | + ] | ||
| 95 | + }, | ||
| 96 | + { | ||
| 97 | + "entity": "artifact", | ||
| 98 | + "owner": "graph-engine", | ||
| 99 | + "purpose": "BRD/PRD/SDD/Benchmark/BugRecord 等结构化产物,版本化不可变", | ||
| 100 | + "fields": [ | ||
| 101 | + {"name": "artifact_id", "type": "string", "nullable": false, "constraints": ["pk"]}, | ||
| 102 | + {"name": "artifact_type", "type": "varchar(20)", "nullable": false, "constraints": ["enum:brd|prd|sdd|benchmark|bug_record|code_diff|test_report"]}, | ||
| 103 | + {"name": "task_id", "type": "string", "nullable": false, "constraints": ["fk:task.task_id"]}, | ||
| 104 | + {"name": "produced_by_run_id", "type": "string", "nullable": false, "constraints": ["fk:work_graph_run.run_id"]}, | ||
| 105 | + {"name": "file_path", "type": "string", "nullable": false, "constraints": []}, | ||
| 106 | + {"name": "git_commit_sha", "type": "string", "nullable": false, "constraints": []}, | ||
| 107 | + {"name": "depends_on_artifact_ids", "type": "json", "nullable": true, "constraints": []}, | ||
| 108 | + {"name": "validation_status", "type": "varchar(20)", "nullable": false, "constraints": ["enum:pending|passed|failed"], "default": "pending"}, | ||
| 109 | + {"name": "created_at", "type": "timestamp", "nullable": false, "constraints": []} | ||
| 110 | + ] | ||
| 111 | + }, | ||
| 112 | + { | ||
| 113 | + "entity": "bug_record", | ||
| 114 | + "owner": "validator", | ||
| 115 | + "purpose": "结构化问题记录与根因归属,来源可为 Validator 自动生成或人工反馈", | ||
| 116 | + "fields": [ | ||
| 117 | + {"name": "bug_id", "type": "string", "nullable": false, "constraints": ["pk"]}, | ||
| 118 | + {"name": "task_id", "type": "string", "nullable": true, "constraints": ["fk:task.task_id"]}, | ||
| 119 | + {"name": "source", "type": "varchar(20)", "nullable": false, "constraints": ["enum:validator|human"]}, | ||
| 120 | + {"name": "reported_by", "type": "string", "nullable": true, "constraints": []}, | ||
| 121 | + {"name": "description", "type": "text", "nullable": false, "constraints": []}, | ||
| 122 | + {"name": "repro_steps", "type": "text", "nullable": true, "constraints": []}, | ||
| 123 | + {"name": "root_cause_category", "type": "varchar(20)", "nullable": true, "constraints": ["enum:planning|coder|skill_defect|core_engine"]}, | ||
| 124 | + {"name": "root_cause_detail", "type": "text", "nullable": true, "constraints": []}, | ||
| 125 | + {"name": "related_artifact_ids", "type": "json", "nullable": true, "constraints": []}, | ||
| 126 | + {"name": "suggested_route", "type": "varchar(20)", "nullable": true, "constraints": ["enum:fast_lane|full_replan"]}, | ||
| 127 | + {"name": "skill_iteration_suggestion_id", "type": "string", "nullable": true, "constraints": ["fk:skill_iteration.iteration_id"]}, | ||
| 128 | + {"name": "status", "type": "varchar(20)", "nullable": false, "constraints": ["enum:open|triaging|routed|resolved"]} | ||
| 129 | + ] | ||
| 130 | + }, | ||
| 131 | + { | ||
| 132 | + "entity": "skill", | ||
| 133 | + "owner": "graph-engine", | ||
| 134 | + "purpose": "Skill 库条目,Git 版本化", | ||
| 135 | + "fields": [ | ||
| 136 | + {"name": "skill_id", "type": "string", "nullable": false, "constraints": ["pk"]}, | ||
| 137 | + {"name": "owner_org_node", "type": "varchar(20)", "nullable": false, "constraints": ["enum:planner|coder|validator"]}, | ||
| 138 | + {"name": "file_path", "type": "string", "nullable": false, "constraints": []}, | ||
| 139 | + {"name": "current_git_commit_sha", "type": "string", "nullable": false, "constraints": []}, | ||
| 140 | + {"name": "source", "type": "varchar(20)", "nullable": false, "constraints": ["enum:migrated_from_hermes|new|auto_iterated"]}, | ||
| 141 | + {"name": "migrated_from_profile", "type": "varchar(20)", "nullable": true, "constraints": ["enum:code|planning|qa"]}, | ||
| 142 | + {"name": "status", "type": "varchar(20)", "nullable": false, "constraints": ["enum:active|archived"]} | ||
| 143 | + ] | ||
| 144 | + }, | ||
| 145 | + { | ||
| 146 | + "entity": "skill_iteration", | ||
| 147 | + "owner": "graph-engine", | ||
| 148 | + "purpose": "Skill 自主迭代的变更记录,每条对应一次独立 Git 提交", | ||
| 149 | + "fields": [ | ||
| 150 | + {"name": "iteration_id", "type": "string", "nullable": false, "constraints": ["pk"]}, | ||
| 151 | + {"name": "skill_id", "type": "string", "nullable": false, "constraints": ["fk:skill.skill_id"]}, | ||
| 152 | + {"name": "triggered_by_bug_id", "type": "string", "nullable": false, "constraints": ["fk:bug_record.bug_id"]}, | ||
| 153 | + {"name": "before_git_commit_sha", "type": "string", "nullable": false, "constraints": []}, | ||
| 154 | + {"name": "after_git_commit_sha", "type": "string", "nullable": false, "constraints": []}, | ||
| 155 | + {"name": "change_summary", "type": "text", "nullable": false, "constraints": []}, | ||
| 156 | + {"name": "applied_at", "type": "timestamp", "nullable": false, "constraints": []}, | ||
| 157 | + {"name": "rollback_of_iteration_id", "type": "string", "nullable": true, "constraints": ["fk:skill_iteration.iteration_id"]} | ||
| 158 | + ] | ||
| 159 | + }, | ||
| 160 | + { | ||
| 161 | + "entity": "agent_profile", | ||
| 162 | + "owner": "graph-engine", | ||
| 163 | + "purpose": "Org Agent 人设与运行契约文件元数据", | ||
| 164 | + "fields": [ | ||
| 165 | + {"name": "org_node", "type": "varchar(20)", "nullable": false, "constraints": ["pk", "enum:planner|coder|validator"]}, | ||
| 166 | + {"name": "soul_file_path", "type": "string", "nullable": false, "constraints": []}, | ||
| 167 | + {"name": "agents_file_path", "type": "string", "nullable": true, "constraints": []}, | ||
| 168 | + {"name": "migrated_from_profile", "type": "varchar(20)", "nullable": false, "constraints": ["enum:code|planning|qa"]}, | ||
| 169 | + {"name": "current_git_commit_sha", "type": "string", "nullable": false, "constraints": []} | ||
| 170 | + ] | ||
| 171 | + }, | ||
| 172 | + { | ||
| 173 | + "entity": "budget_guardrail_config", | ||
| 174 | + "owner": "graph-engine", | ||
| 175 | + "purpose": "预算/额度护栏配置,手工配置层", | ||
| 176 | + "fields": [ | ||
| 177 | + {"name": "config_id", "type": "string", "nullable": false, "constraints": ["pk"], "default": "default"}, | ||
| 178 | + {"name": "scope", "type": "varchar(20)", "nullable": false, "constraints": ["enum:global|per_task|per_day"]}, | ||
| 179 | + {"name": "limit_type", "type": "varchar(20)", "nullable": false, "constraints": ["enum:token_count|call_count"]}, | ||
| 180 | + {"name": "limit_value", "type": "int", "nullable": false, "constraints": [">0"]}, | ||
| 181 | + {"name": "current_usage", "type": "int", "nullable": false, "constraints": [">=0"], "default": 0}, | ||
| 182 | + {"name": "on_exceed_action", "type": "varchar(30)", "nullable": false, "constraints": [], "default": "pause_and_notify"}, | ||
| 183 | + {"name": "updated_by", "type": "string", "nullable": false, "constraints": []}, | ||
| 184 | + {"name": "updated_at", "type": "timestamp", "nullable": false, "constraints": []} | ||
| 185 | + ] | ||
| 186 | + }, | ||
| 187 | + { | ||
| 188 | + "entity": "org_concurrency_config", | ||
| 189 | + "owner": "graph-engine", | ||
| 190 | + "purpose": "多 Goal 并发数配置,MVP 默认 1", | ||
| 191 | + "fields": [ | ||
| 192 | + {"name": "config_id", "type": "string", "nullable": false, "constraints": ["pk"], "default": "default"}, | ||
| 193 | + {"name": "max_concurrent_goals", "type": "int", "nullable": false, "constraints": ["range:1-50"], "default": 1}, | ||
| 194 | + {"name": "updated_by", "type": "string", "nullable": false, "constraints": []}, | ||
| 195 | + {"name": "updated_at", "type": "timestamp", "nullable": false, "constraints": []} | ||
| 196 | + ] | ||
| 197 | + }, | ||
| 198 | + { | ||
| 199 | + "entity": "execution_event", | ||
| 200 | + "owner": "control-plane", | ||
| 201 | + "purpose": "实时看板的唯一数据源,append-only 事件流", | ||
| 202 | + "fields": [ | ||
| 203 | + {"name": "event_id", "type": "string", "nullable": false, "constraints": ["pk", "sortable"]}, | ||
| 204 | + {"name": "project_id", "type": "string", "nullable": false, "constraints": ["fk:project.project_id", "idx"], "index": "idx"}, | ||
| 205 | + {"name": "task_id", "type": "string", "nullable": true, "constraints": ["fk:task.task_id", "idx"], "index": "idx"}, | ||
| 206 | + {"name": "run_id", "type": "string", "nullable": true, "constraints": ["fk:work_graph_run.run_id"]}, | ||
| 207 | + {"name": "node_exec_id", "type": "string", "nullable": true, "constraints": ["fk:work_node_execution.node_exec_id"]}, | ||
| 208 | + {"name": "event_type", "type": "varchar(30)", "nullable": false, "constraints": ["enum:task_status_changed|node_status_changed|artifact_created|bug_recorded|loop_routed|budget_updated|skill_iterated|log_appended|quota_wait_started"]}, | ||
| 209 | + {"name": "payload", "type": "json", "nullable": false, "constraints": []}, | ||
| 210 | + {"name": "created_at", "type": "timestamp", "nullable": false, "constraints": ["idx"], "index": "idx"} | ||
| 211 | + ] | ||
| 212 | + }, | ||
| 213 | + { | ||
| 214 | + "entity": "execution_log_file", | ||
| 215 | + "owner": "control-plane", | ||
| 216 | + "purpose": "Work Node 原始执行日志本地落盘记录,架构文档13.1决策4", | ||
| 217 | + "fields": [ | ||
| 218 | + {"name": "log_file_id", "type": "string", "nullable": false, "constraints": ["pk"]}, | ||
| 219 | + {"name": "node_exec_id", "type": "string", "nullable": false, "constraints": ["fk:work_node_execution.node_exec_id", "unique"], "index": "unique"}, | ||
| 220 | + {"name": "project_id", "type": "string", "nullable": false, "constraints": ["fk:project.project_id", "idx"], "index": "idx"}, | ||
| 221 | + {"name": "file_path", "type": "string", "nullable": false, "constraints": []}, | ||
| 222 | + {"name": "format", "type": "varchar(20)", "nullable": false, "constraints": ["enum:jsonl|raw_text"]}, | ||
| 223 | + {"name": "size_bytes", "type": "int", "nullable": false, "constraints": [">=0"], "default": 0}, | ||
| 224 | + {"name": "status", "type": "varchar(20)", "nullable": false, "constraints": ["enum:active|closed|purged"]}, | ||
| 225 | + {"name": "created_at", "type": "timestamp", "nullable": false, "constraints": []}, | ||
| 226 | + {"name": "purged_at", "type": "timestamp", "nullable": true, "constraints": []} | ||
| 227 | + ] | ||
| 228 | + }, | ||
| 229 | + { | ||
| 230 | + "entity": "log_retention_config", | ||
| 231 | + "owner": "control-plane", | ||
| 232 | + "purpose": "日志清理策略配置", | ||
| 233 | + "fields": [ | ||
| 234 | + {"name": "config_id", "type": "string", "nullable": false, "constraints": ["pk"], "default": "default"}, | ||
| 235 | + {"name": "retention_days", "type": "int", "nullable": false, "constraints": [">0"], "default": 14}, | ||
| 236 | + {"name": "max_total_size_mb", "type": "int", "nullable": true, "constraints": [">0"]}, | ||
| 237 | + {"name": "updated_by", "type": "string", "nullable": false, "constraints": []}, | ||
| 238 | + {"name": "updated_at", "type": "timestamp", "nullable": false, "constraints": []} | ||
| 239 | + ] | ||
| 240 | + }, | ||
| 241 | + { | ||
| 242 | + "entity": "deployment", | ||
| 243 | + "owner": "deploy-supervisor", | ||
| 244 | + "purpose": "PH5 新增(2026-07-30):Deploy Supervisor 对 graphlemon 自身(self-target)执行一次蓝绿部署的记录,见 docs/supervisor-auto-rollback-design.md。不要与架构文档里的 Supervisor(调度子 Agent)概念混淆", | ||
| 245 | + "fields": [ | ||
| 246 | + {"name": "deployment_id", "type": "string", "nullable": false, "constraints": ["pk"]}, | ||
| 247 | + {"name": "git_commit_hash", "type": "string", "nullable": false, "constraints": []}, | ||
| 248 | + {"name": "previous_commit_hash", "type": "string", "nullable": false, "constraints": []}, | ||
| 249 | + {"name": "status", "type": "varchar(30)", "nullable": false, "constraints": ["enum:staged|candidate_starting|health_check_running|health_check_passed|health_check_failed|cutover_in_progress|live|monitoring|anomaly_detected|rollback_in_progress|rolled_back|rollback_failed|blocked_pending_human|monitoring_window_clean|finalized"]}, | ||
| 250 | + {"name": "candidate_port", "type": "int", "nullable": true, "constraints": []}, | ||
| 251 | + {"name": "candidate_db_path", "type": "string", "nullable": true, "constraints": []}, | ||
| 252 | + {"name": "started_at", "type": "timestamp", "nullable": true, "constraints": []}, | ||
| 253 | + {"name": "cutover_at", "type": "timestamp", "nullable": true, "constraints": []}, | ||
| 254 | + {"name": "monitoring_closed_at", "type": "timestamp", "nullable": true, "constraints": []}, | ||
| 255 | + {"name": "rollback_at", "type": "timestamp", "nullable": true, "constraints": []}, | ||
| 256 | + {"name": "rollback_reason", "type": "string", "nullable": true, "constraints": []}, | ||
| 257 | + {"name": "previous_instance_dir", "type": "string", "nullable": false, "constraints": []} | ||
| 258 | + ] | ||
| 259 | + }, | ||
| 260 | + { | ||
| 261 | + "entity": "deploy_health_signal", | ||
| 262 | + "owner": "deploy-supervisor", | ||
| 263 | + "purpose": "PH5 新增(2026-07-30):deployment 生命周期内每次健康检查/监控采样记录,全部确定性判断,不涉及 LLM", | ||
| 264 | + "fields": [ | ||
| 265 | + {"name": "signal_id", "type": "string", "nullable": false, "constraints": ["pk"]}, | ||
| 266 | + {"name": "deployment_id", "type": "string", "nullable": false, "constraints": ["fk:deployment.deployment_id"]}, | ||
| 267 | + {"name": "sampled_at", "type": "timestamp", "nullable": false, "constraints": []}, | ||
| 268 | + {"name": "signal_type", "type": "varchar(30)", "nullable": false, "constraints": ["enum:process_alive|http_health|canary_task|validator_approval_rate"]}, | ||
| 269 | + {"name": "status", "type": "varchar(10)", "nullable": false, "constraints": ["enum:ok|degraded|fail"]}, | ||
| 270 | + {"name": "detail", "type": "string", "nullable": true, "constraints": []} | ||
| 271 | + ] | ||
| 272 | + } | ||
| 273 | + ], | ||
| 274 | + "view_contract": [ | ||
| 275 | + { | ||
| 276 | + "view_id": "v_project_setup", | ||
| 277 | + "route": "/setup", | ||
| 278 | + "fields": [ | ||
| 279 | + {"name": "folder_path", "visible_when": "all", "validation": ["required"], "required": true}, | ||
| 280 | + {"name": "project_name", "visible_when": "all", "validation": [], "required": false} | ||
| 281 | + ], | ||
| 282 | + "events": [ | ||
| 283 | + {"event": "click_pick_folder", "target_controller_id": "c_pick_folder", "payload": {}}, | ||
| 284 | + {"event": "submit_create", "target_controller_id": "c_project_create", "payload": {"target_folder_path": "string", "name": "string", "init_git_if_missing": "boolean"}} | ||
| 285 | + ] | ||
| 286 | + }, | ||
| 287 | + { | ||
| 288 | + "view_id": "v_dashboard", | ||
| 289 | + "route": "/", | ||
| 290 | + "fields": [], | ||
| 291 | + "events": [ | ||
| 292 | + {"event": "click_propose_goal", "target_controller_id": "c_task_create", "payload": {}}, | ||
| 293 | + {"event": "click_report_bug", "target_controller_id": "c_bug_report_create", "payload": {}}, | ||
| 294 | + {"event": "click_task_card", "target_controller_id": "c_task_get", "payload": {"task_id": "string"}} | ||
| 295 | + ] | ||
| 296 | + }, | ||
| 297 | + { | ||
| 298 | + "view_id": "v_goal_composer", | ||
| 299 | + "route": "/goals/new", | ||
| 300 | + "fields": [ | ||
| 301 | + {"name": "goal_text", "visible_when": "all", "validation": ["required", "maxLen:4000"], "required": true} | ||
| 302 | + ], | ||
| 303 | + "events": [ | ||
| 304 | + {"event": "submit_create", "target_controller_id": "c_task_create", "payload": {"goal_text": "string", "created_via": "human_console"}} | ||
| 305 | + ] | ||
| 306 | + }, | ||
| 307 | + { | ||
| 308 | + "view_id": "v_bug_feedback", | ||
| 309 | + "route": "/bugs/new", | ||
| 310 | + "fields": [ | ||
| 311 | + {"name": "description", "visible_when": "all", "validation": ["required"], "required": true}, | ||
| 312 | + {"name": "repro_steps", "visible_when": "all", "validation": [], "required": false}, | ||
| 313 | + {"name": "related_task_id", "visible_when": "all", "validation": [], "required": false} | ||
| 314 | + ], | ||
| 315 | + "events": [ | ||
| 316 | + {"event": "submit_create", "target_controller_id": "c_bug_report_create", "payload": {"description": "string", "repro_steps": "string", "related_task_id": "string"}} | ||
| 317 | + ] | ||
| 318 | + }, | ||
| 319 | + { | ||
| 320 | + "view_id": "v_artifact_browser", | ||
| 321 | + "route": "/artifacts", | ||
| 322 | + "fields": [ | ||
| 323 | + {"name": "artifact_type_filter", "visible_when": "all", "validation": [], "required": false}, | ||
| 324 | + {"name": "task_id_filter", "visible_when": "all", "validation": [], "required": false} | ||
| 325 | + ], | ||
| 326 | + "events": [ | ||
| 327 | + {"event": "click_row", "target_controller_id": "c_artifact_get", "payload": {"artifact_id": "string"}} | ||
| 328 | + ] | ||
| 329 | + }, | ||
| 330 | + { | ||
| 331 | + "view_id": "v_skill_library", | ||
| 332 | + "route": "/skills", | ||
| 333 | + "fields": [], | ||
| 334 | + "events": [ | ||
| 335 | + {"event": "click_skill", "target_controller_id": "c_skill_diff", "payload": {"skill_id": "string"}} | ||
| 336 | + ] | ||
| 337 | + }, | ||
| 338 | + { | ||
| 339 | + "view_id": "v_task_board", | ||
| 340 | + "route": "/tasks", | ||
| 341 | + "fields": [ | ||
| 342 | + {"name": "keyword", "visible_when": "all", "validation": ["maxLen:128"], "required": false}, | ||
| 343 | + {"name": "status_filter", "visible_when": "all", "validation": [], "required": false}, | ||
| 344 | + {"name": "org_node_filter", "visible_when": "all", "validation": [], "required": false} | ||
| 345 | + ], | ||
| 346 | + "events": [ | ||
| 347 | + {"event": "click_row", "target_controller_id": "c_task_get", "payload": {"task_id": "string"}}, | ||
| 348 | + {"event": "click_search", "target_controller_id": "c_task_list", "payload": {"filters": ["keyword", "status_filter", "org_node_filter"]}} | ||
| 349 | + ] | ||
| 350 | + }, | ||
| 351 | + { | ||
| 352 | + "view_id": "v_task_detail", | ||
| 353 | + "route": "/tasks/:task_id", | ||
| 354 | + "fields": [ | ||
| 355 | + {"name": "task_id", "visible_when": "all", "validation": ["required"], "required": true} | ||
| 356 | + ], | ||
| 357 | + "events": [ | ||
| 358 | + {"event": "click_artifact_link", "target_controller_id": "c_artifact_get", "payload": {"artifact_id": "string"}}, | ||
| 359 | + {"event": "click_view_raw_log", "target_controller_id": "c_execution_log_get", "payload": {"node_exec_id": "string"}}, | ||
| 360 | + {"event": "click_retry", "target_controller_id": "c_org_loop_route", "payload": {"task_id": "string", "action": "manual_resume"}} | ||
| 361 | + ] | ||
| 362 | + }, | ||
| 363 | + { | ||
| 364 | + "view_id": "v_log_viewer", | ||
| 365 | + "route": "embedded", | ||
| 366 | + "fields": [ | ||
| 367 | + {"name": "node_exec_id", "visible_when": "all", "validation": ["required"], "required": true} | ||
| 368 | + ], | ||
| 369 | + "events": [ | ||
| 370 | + {"event": "toggle_auto_scroll", "target_controller_id": "none", "payload": {}}, | ||
| 371 | + {"event": "click_download", "target_controller_id": "c_execution_log_get", "payload": {"node_exec_id": "string", "download": true}} | ||
| 372 | + ] | ||
| 373 | + }, | ||
| 374 | + { | ||
| 375 | + "view_id": "v_log_retention_settings", | ||
| 376 | + "route": "/settings/log-retention", | ||
| 377 | + "fields": [ | ||
| 378 | + {"name": "retention_days", "visible_when": "all", "validation": [">0"], "required": true}, | ||
| 379 | + {"name": "max_total_size_mb", "visible_when": "all", "validation": [">0"], "required": false} | ||
| 380 | + ], | ||
| 381 | + "events": [ | ||
| 382 | + {"event": "submit_save", "target_controller_id": "c_log_retention_config_update", "payload": {"retention_days": "int", "max_total_size_mb": "int"}} | ||
| 383 | + ] | ||
| 384 | + }, | ||
| 385 | + { | ||
| 386 | + "view_id": "v_concurrency_settings", | ||
| 387 | + "route": "/settings/concurrency", | ||
| 388 | + "fields": [ | ||
| 389 | + {"name": "max_concurrent_goals", "visible_when": "all", "validation": ["range:1-50"], "required": true} | ||
| 390 | + ], | ||
| 391 | + "events": [ | ||
| 392 | + {"event": "submit_save", "target_controller_id": "c_concurrency_config_update", "payload": {"max_concurrent_goals": "int"}} | ||
| 393 | + ] | ||
| 394 | + }, | ||
| 395 | + { | ||
| 396 | + "view_id": "v_budget_settings", | ||
| 397 | + "route": "/settings/budget", | ||
| 398 | + "fields": [ | ||
| 399 | + {"name": "scope", "visible_when": "all", "validation": ["required"], "required": true}, | ||
| 400 | + {"name": "limit_type", "visible_when": "all", "validation": ["required"], "required": true}, | ||
| 401 | + {"name": "limit_value", "visible_when": "all", "validation": [">0"], "required": true} | ||
| 402 | + ], | ||
| 403 | + "events": [ | ||
| 404 | + {"event": "submit_save", "target_controller_id": "c_budget_config_update", "payload": {"scope": "string", "limit_type": "string", "limit_value": "int"}} | ||
| 405 | + ] | ||
| 406 | + }, | ||
| 407 | + { | ||
| 408 | + "view_id": "v_skill_audit_log", | ||
| 409 | + "route": "/skills/:skill_id/audit", | ||
| 410 | + "fields": [ | ||
| 411 | + {"name": "skill_id", "visible_when": "all", "validation": ["required"], "required": true} | ||
| 412 | + ], | ||
| 413 | + "events": [ | ||
| 414 | + {"event": "click_diff", "target_controller_id": "c_skill_diff", "payload": {"before": "string", "after": "string"}}, | ||
| 415 | + {"event": "click_rollback", "target_controller_id": "c_skill_rollback", "payload": {"iteration_id": "string"}} | ||
| 416 | + ] | ||
| 417 | + }, | ||
| 418 | + { | ||
| 419 | + "view_id": "v_graph_visualizer", | ||
| 420 | + "route": "embedded", | ||
| 421 | + "fields": [ | ||
| 422 | + {"name": "run_id", "visible_when": "task_has_active_run", "validation": [], "required": false} | ||
| 423 | + ], | ||
| 424 | + "events": [ | ||
| 425 | + {"event": "hover_node", "target_controller_id": "c_work_node_get", "payload": {"node_exec_id": "string"}} | ||
| 426 | + ] | ||
| 427 | + }, | ||
| 428 | + { | ||
| 429 | + "view_id": "v_deployment_status", | ||
| 430 | + "route": "embedded", | ||
| 431 | + "fields": [ | ||
| 432 | + {"name": "deployment_id", "visible_when": "self_target_deployment_active", "validation": [], "required": false} | ||
| 433 | + ], | ||
| 434 | + "events": [ | ||
| 435 | + {"event": "click_deployment_card", "target_controller_id": "c_deploy_supervisor_monitor", "payload": {"deployment_id": "string"}} | ||
| 436 | + ] | ||
| 437 | + } | ||
| 438 | + ], | ||
| 439 | + "controller_contract": [ | ||
| 440 | + { | ||
| 441 | + "operation_id": "c_pick_folder", | ||
| 442 | + "method": "POST", | ||
| 443 | + "path": "/api/system/pick-folder", | ||
| 444 | + "auth": "system_internal", | ||
| 445 | + "input": {}, | ||
| 446 | + "output": {"selected_path": "string|null"}, | ||
| 447 | + "idempotent": false, | ||
| 448 | + "errors": [ | ||
| 449 | + {"code": "DIALOG_UNAVAILABLE", "retryable": false, "description": "后端所在环境不支持原生文件夹对话框,前端应回退到手动路径输入"} | ||
| 450 | + ] | ||
| 451 | + }, | ||
| 452 | + { | ||
| 453 | + "operation_id": "c_project_create", | ||
| 454 | + "method": "POST", | ||
| 455 | + "path": "/api/projects", | ||
| 456 | + "auth": "system_internal", | ||
| 457 | + "input": {"target_folder_path": "string(required)", "name": "string(optional)", "init_git_if_missing": "boolean(required)"}, | ||
| 458 | + "output": {"project_id": "string", "has_git_repo": "boolean", "graphlemon_subdir_initialized": "boolean"}, | ||
| 459 | + "idempotent": true, | ||
| 460 | + "errors": [ | ||
| 461 | + {"code": "PATH_NOT_FOUND", "retryable": false, "description": "文件夹不存在"}, | ||
| 462 | + {"code": "PATH_NOT_WRITABLE", "retryable": false, "description": "无写权限"}, | ||
| 463 | + {"code": "GIT_INIT_DECLINED", "retryable": false, "description": "非 Git 仓库且用户未确认初始化"} | ||
| 464 | + ] | ||
| 465 | + }, | ||
| 466 | + { | ||
| 467 | + "operation_id": "c_project_get", | ||
| 468 | + "method": "GET", | ||
| 469 | + "path": "/api/projects/:project_id", | ||
| 470 | + "auth": "system_internal", | ||
| 471 | + "input": {"path": {"project_id": "string(required)"}}, | ||
| 472 | + "output": {"project": "object"}, | ||
| 473 | + "idempotent": true, | ||
| 474 | + "errors": [ | ||
| 475 | + {"code": "NOT_FOUND", "retryable": false, "description": "project_id 不存在"} | ||
| 476 | + ] | ||
| 477 | + }, | ||
| 478 | + { | ||
| 479 | + "operation_id": "c_project_list", | ||
| 480 | + "method": "GET", | ||
| 481 | + "path": "/api/projects", | ||
| 482 | + "auth": "system_internal", | ||
| 483 | + "input": {}, | ||
| 484 | + "output": {"items": "array"}, | ||
| 485 | + "idempotent": true, | ||
| 486 | + "errors": [] | ||
| 487 | + }, | ||
| 488 | + { | ||
| 489 | + "operation_id": "c_task_create", | ||
| 490 | + "method": "POST", | ||
| 491 | + "path": "/api/tasks", | ||
| 492 | + "auth": "system_internal", | ||
| 493 | + "input": {"project_id": "string(required)", "goal_text": "string(required)", "task_type": "enum(goal)", "created_via": "enum(api|human_console)"}, | ||
| 494 | + "output": {"task_id": "string", "status": "queued"}, | ||
| 495 | + "idempotent": false, | ||
| 496 | + "errors": [ | ||
| 497 | + {"code": "INVALID_PARAM", "retryable": false, "description": "goal_text 为空或 project_id 不存在"} | ||
| 498 | + ] | ||
| 499 | + }, | ||
| 500 | + { | ||
| 501 | + "operation_id": "c_bug_report_create", | ||
| 502 | + "method": "POST", | ||
| 503 | + "path": "/api/bug-reports", | ||
| 504 | + "auth": "system_internal", | ||
| 505 | + "input": {"description": "string(required)", "repro_steps": "string(optional)", "related_task_id": "string(optional)"}, | ||
| 506 | + "output": {"bug_id": "string", "status": "open"}, | ||
| 507 | + "idempotent": false, | ||
| 508 | + "errors": [ | ||
| 509 | + {"code": "INVALID_PARAM", "retryable": false, "description": "description 为空"} | ||
| 510 | + ] | ||
| 511 | + }, | ||
| 512 | + { | ||
| 513 | + "operation_id": "c_ws_subscribe", | ||
| 514 | + "method": "GET", | ||
| 515 | + "path": "/api/ws/projects/:project_id", | ||
| 516 | + "auth": "system_internal", | ||
| 517 | + "input": {"query": {"project_id": "string(required)", "task_id": "string(optional)"}}, | ||
| 518 | + "output": {"stream": "execution_event[]"}, | ||
| 519 | + "idempotent": false, | ||
| 520 | + "errors": [ | ||
| 521 | + {"code": "PROJECT_NOT_FOUND", "retryable": false, "description": "订阅的 project_id 不存在,拒绝建立连接"} | ||
| 522 | + ] | ||
| 523 | + }, | ||
| 524 | + { | ||
| 525 | + "operation_id": "c_task_list", | ||
| 526 | + "method": "GET", | ||
| 527 | + "path": "/api/tasks", | ||
| 528 | + "auth": "system_internal", | ||
| 529 | + "input": {"query": {"status": "string(optional)", "org_node": "string(optional)", "keyword": "string(optional)"}}, | ||
| 530 | + "output": {"items": "array", "total": "int"}, | ||
| 531 | + "idempotent": true, | ||
| 532 | + "errors": [] | ||
| 533 | + }, | ||
| 534 | + { | ||
| 535 | + "operation_id": "c_task_get", | ||
| 536 | + "method": "GET", | ||
| 537 | + "path": "/api/tasks/:task_id", | ||
| 538 | + "auth": "system_internal", | ||
| 539 | + "input": {"path": {"task_id": "string(required)"}}, | ||
| 540 | + "output": {"task": "object", "artifacts": "array", "bug_records": "array"}, | ||
| 541 | + "idempotent": true, | ||
| 542 | + "errors": [ | ||
| 543 | + {"code": "NOT_FOUND", "retryable": false, "description": "task_id 不存在"} | ||
| 544 | + ] | ||
| 545 | + }, | ||
| 546 | + { | ||
| 547 | + "operation_id": "c_org_node_trigger", | ||
| 548 | + "method": "POST", | ||
| 549 | + "path": "/api/org-nodes/:org_node/trigger", | ||
| 550 | + "auth": "system_internal", | ||
| 551 | + "input": {"task_id": "string(required)"}, | ||
| 552 | + "output": {"run_id": "string", "status": "string"}, | ||
| 553 | + "idempotent": false, | ||
| 554 | + "errors": [ | ||
| 555 | + {"code": "TASK_NOT_READY", "retryable": true, "description": "上游依赖 Artifact 未就绪"}, | ||
| 556 | + {"code": "CONCURRENCY_LIMIT_REACHED", "retryable": true, "description": "超过 max_concurrent_goals"}, | ||
| 557 | + {"code": "USAGE_LIMIT_REACHED", "retryable": true, "description": "Claude Code plan 额度耗尽(session/weekly/opus),走退避轮询等待重试,非立即重试,见 p_usage_limit_wait"} | ||
| 558 | + ] | ||
| 559 | + }, | ||
| 560 | + { | ||
| 561 | + "operation_id": "c_work_graph_plan", | ||
| 562 | + "method": "POST", | ||
| 563 | + "path": "/api/work-graph/plan", | ||
| 564 | + "auth": "system_internal", | ||
| 565 | + "input": {"task_id": "string(required)", "org_node": "string(required)"}, | ||
| 566 | + "output": {"run_id": "string", "graph_manifest": "object"}, | ||
| 567 | + "idempotent": false, | ||
| 568 | + "errors": [ | ||
| 569 | + {"code": "PLANNING_FAILED", "retryable": true, "description": "Work Graph 规划失败"} | ||
| 570 | + ] | ||
| 571 | + }, | ||
| 572 | + { | ||
| 573 | + "operation_id": "c_work_graph_execute", | ||
| 574 | + "method": "POST", | ||
| 575 | + "path": "/api/work-graph/:run_id/execute", | ||
| 576 | + "auth": "system_internal", | ||
| 577 | + "input": {"run_id": "string(required)"}, | ||
| 578 | + "output": {"status": "string", "result_artifact_refs": "array"}, | ||
| 579 | + "idempotent": false, | ||
| 580 | + "errors": [ | ||
| 581 | + {"code": "NODE_CONTRACT_VIOLATION", "retryable": true, "description": "节点输出未通过 schema 校验"}, | ||
| 582 | + {"code": "BUDGET_EXCEEDED", "retryable": false, "description": "预算护栏拦截"}, | ||
| 583 | + {"code": "CLASSIFIER_BLOCKED", "retryable": false, "description": "auto mode 分类器连续拦截 3 次或累计 20 次,-p 非交互模式下无人可确认,进程直接中止"} | ||
| 584 | + ] | ||
| 585 | + }, | ||
| 586 | + { | ||
| 587 | + "operation_id": "c_work_node_run", | ||
| 588 | + "method": "POST", | ||
| 589 | + "path": "internal://work-node/run", | ||
| 590 | + "auth": "system_internal", | ||
| 591 | + "input": {"node_exec_id": "string(required)", "input_payload": "object", "input_schema_ref": "string"}, | ||
| 592 | + "output": {"output_payload": "object"}, | ||
| 593 | + "idempotent": false, | ||
| 594 | + "errors": [ | ||
| 595 | + {"code": "SCHEMA_VALIDATION_FAILED", "retryable": true, "description": "输出未通过 output_schema_ref 校验"}, | ||
| 596 | + {"code": "USAGE_LIMIT_REACHED", "retryable": true, "description": "Claude Code plan 额度耗尽,退避轮询等待重试,见 p_usage_limit_wait"} | ||
| 597 | + ] | ||
| 598 | + }, | ||
| 599 | + { | ||
| 600 | + "operation_id": "c_execution_log_get", | ||
| 601 | + "method": "GET", | ||
| 602 | + "path": "/api/work-nodes/:node_exec_id/log", | ||
| 603 | + "auth": "system_internal", | ||
| 604 | + "input": {"path": {"node_exec_id": "string(required)"}, "query": {"offset": "int(optional)", "limit": "int(optional)", "download": "boolean(optional)"}}, | ||
| 605 | + "output": {"content": "string", "format": "string", "total_size_bytes": "int", "status": "string"}, | ||
| 606 | + "idempotent": true, | ||
| 607 | + "errors": [ | ||
| 608 | + {"code": "LOG_NOT_FOUND", "retryable": false, "description": "node_exec_id 不存在对应日志记录"}, | ||
| 609 | + {"code": "LOG_PURGED", "retryable": false, "description": "日志已被定时清理任务删除"} | ||
| 610 | + ] | ||
| 611 | + }, | ||
| 612 | + { | ||
| 613 | + "operation_id": "c_ws_log_tail", | ||
| 614 | + "method": "GET", | ||
| 615 | + "path": "/api/ws/work-nodes/:node_exec_id/log", | ||
| 616 | + "auth": "system_internal", | ||
| 617 | + "input": {"path": {"node_exec_id": "string(required)"}}, | ||
| 618 | + "output": {"stream": "log_chunk[]"}, | ||
| 619 | + "idempotent": false, | ||
| 620 | + "errors": [ | ||
| 621 | + {"code": "NODE_EXEC_NOT_FOUND", "retryable": false, "description": "node_exec_id 不存在"} | ||
| 622 | + ] | ||
| 623 | + }, | ||
| 624 | + { | ||
| 625 | + "operation_id": "c_log_retention_config_update", | ||
| 626 | + "method": "PUT", | ||
| 627 | + "path": "/api/settings/log-retention", | ||
| 628 | + "auth": "system_internal", | ||
| 629 | + "input": {"retention_days": "int(required, >0)", "max_total_size_mb": "int(optional)"}, | ||
| 630 | + "output": {"config": "object"}, | ||
| 631 | + "idempotent": true, | ||
| 632 | + "errors": [ | ||
| 633 | + {"code": "INVALID_PARAM", "retryable": false, "description": "retention_days<=0"} | ||
| 634 | + ] | ||
| 635 | + }, | ||
| 636 | + { | ||
| 637 | + "operation_id": "c_artifact_write", | ||
| 638 | + "method": "POST", | ||
| 639 | + "path": "/api/artifacts", | ||
| 640 | + "auth": "system_internal", | ||
| 641 | + "input": {"artifact_type": "string(required)", "task_id": "string(required)", "produced_by_run_id": "string(required)", "file_path": "string(required)", "content": "string(required)", "depends_on_artifact_ids": "array(optional)"}, | ||
| 642 | + "output": {"artifact_id": "string", "git_commit_sha": "string"}, | ||
| 643 | + "idempotent": false, | ||
| 644 | + "errors": [ | ||
| 645 | + {"code": "GIT_COMMIT_FAILED", "retryable": true, "description": "Git 提交失败"} | ||
| 646 | + ] | ||
| 647 | + }, | ||
| 648 | + { | ||
| 649 | + "operation_id": "c_artifact_get", | ||
| 650 | + "method": "GET", | ||
| 651 | + "path": "/api/artifacts/:artifact_id", | ||
| 652 | + "auth": "system_internal", | ||
| 653 | + "input": {"path": {"artifact_id": "string(required)"}}, | ||
| 654 | + "output": {"artifact": "object"}, | ||
| 655 | + "idempotent": true, | ||
| 656 | + "errors": [ | ||
| 657 | + {"code": "NOT_FOUND", "retryable": false, "description": "artifact_id 不存在"} | ||
| 658 | + ] | ||
| 659 | + }, | ||
| 660 | + { | ||
| 661 | + "operation_id": "c_skill_update", | ||
| 662 | + "method": "POST", | ||
| 663 | + "path": "/api/skills/:skill_id/update", | ||
| 664 | + "auth": "system_internal", | ||
| 665 | + "input": {"skill_id": "string(required)", "triggered_by_bug_id": "string(required)", "new_content": "string(required)", "change_summary": "string(required)"}, | ||
| 666 | + "output": {"iteration_id": "string", "after_git_commit_sha": "string"}, | ||
| 667 | + "idempotent": false, | ||
| 668 | + "errors": [ | ||
| 669 | + {"code": "MISSING_TRIGGER_REF", "retryable": false, "description": "triggered_by_bug_id 缺失,不允许无来源变更"} | ||
| 670 | + ] | ||
| 671 | + }, | ||
| 672 | + { | ||
| 673 | + "operation_id": "c_skill_rollback", | ||
| 674 | + "method": "POST", | ||
| 675 | + "path": "/api/skills/:skill_id/rollback", | ||
| 676 | + "auth": "system_internal", | ||
| 677 | + "input": {"iteration_id": "string(required)"}, | ||
| 678 | + "output": {"new_iteration_id": "string"}, | ||
| 679 | + "idempotent": false, | ||
| 680 | + "errors": [ | ||
| 681 | + {"code": "ITERATION_NOT_FOUND", "retryable": false, "description": "iteration_id 不存在"} | ||
| 682 | + ] | ||
| 683 | + }, | ||
| 684 | + { | ||
| 685 | + "operation_id": "c_skill_diff", | ||
| 686 | + "method": "GET", | ||
| 687 | + "path": "/api/skills/diff", | ||
| 688 | + "auth": "system_internal", | ||
| 689 | + "input": {"query": {"before": "string(required)", "after": "string(required)"}}, | ||
| 690 | + "output": {"diff_text": "string"}, | ||
| 691 | + "idempotent": true, | ||
| 692 | + "errors": [] | ||
| 693 | + }, | ||
| 694 | + { | ||
| 695 | + "operation_id": "c_bug_record_create", | ||
| 696 | + "method": "POST", | ||
| 697 | + "path": "/api/bug-records", | ||
| 698 | + "auth": "system_internal", | ||
| 699 | + "input": {"task_id": "string(required)", "description": "string(required)", "repro_steps": "string(required)", "root_cause_category": "enum(required)", "root_cause_detail": "string(required)", "related_artifact_ids": "array(required)", "suggested_route": "enum(required)"}, | ||
| 700 | + "output": {"bug_id": "string"}, | ||
| 701 | + "idempotent": false, | ||
| 702 | + "errors": [ | ||
| 703 | + {"code": "INVALID_ROOT_CAUSE_CATEGORY", "retryable": false, "description": "未落在 planning|coder|skill_defect|core_engine 四类之一(core_engine 为 2026-07-30 新增,见 01-M bug_record 实体)"} | ||
| 704 | + ] | ||
| 705 | + }, | ||
| 706 | + { | ||
| 707 | + "operation_id": "c_org_loop_route", | ||
| 708 | + "method": "POST", | ||
| 709 | + "path": "/api/tasks/:task_id/route", | ||
| 710 | + "auth": "system_internal", | ||
| 711 | + "input": {"task_id": "string(required)", "decided_route": "enum(fast_lane|full_replan|manual_resume)"}, | ||
| 712 | + "output": {"task_id": "string", "new_status": "string", "new_assigned_org_node": "string"}, | ||
| 713 | + "idempotent": false, | ||
| 714 | + "errors": [ | ||
| 715 | + {"code": "LOOP_LIMIT_REACHED", "retryable": false, "description": "loop_round 已达上限,仅接受 manual_resume"} | ||
| 716 | + ] | ||
| 717 | + }, | ||
| 718 | + { | ||
| 719 | + "operation_id": "c_work_node_get", | ||
| 720 | + "method": "GET", | ||
| 721 | + "path": "/api/work-nodes/:node_exec_id", | ||
| 722 | + "auth": "system_internal", | ||
| 723 | + "input": {"path": {"node_exec_id": "string(required)"}}, | ||
| 724 | + "output": {"node": "object"}, | ||
| 725 | + "idempotent": true, | ||
| 726 | + "errors": [ | ||
| 727 | + {"code": "NOT_FOUND", "retryable": false, "description": "node_exec_id 不存在"} | ||
| 728 | + ] | ||
| 729 | + }, | ||
| 730 | + { | ||
| 731 | + "operation_id": "c_budget_check", | ||
| 732 | + "method": "POST", | ||
| 733 | + "path": "internal://budget/check", | ||
| 734 | + "auth": "system_internal", | ||
| 735 | + "input": {"scope": "string(required)", "estimated_usage": "int(required)"}, | ||
| 736 | + "output": {"allowed": "boolean", "remaining_quota": "int"}, | ||
| 737 | + "idempotent": true, | ||
| 738 | + "errors": [ | ||
| 739 | + {"code": "BUDGET_EXCEEDED", "retryable": false, "description": "超出配置额度"} | ||
| 740 | + ] | ||
| 741 | + }, | ||
| 742 | + { | ||
| 743 | + "operation_id": "c_concurrency_config_update", | ||
| 744 | + "method": "PUT", | ||
| 745 | + "path": "/api/settings/concurrency", | ||
| 746 | + "auth": "system_internal", | ||
| 747 | + "input": {"max_concurrent_goals": "int(required, range:1-50)"}, | ||
| 748 | + "output": {"config": "object"}, | ||
| 749 | + "idempotent": true, | ||
| 750 | + "errors": [ | ||
| 751 | + {"code": "INVALID_PARAM", "retryable": false, "description": "数值超出范围"} | ||
| 752 | + ] | ||
| 753 | + }, | ||
| 754 | + { | ||
| 755 | + "operation_id": "c_budget_config_update", | ||
| 756 | + "method": "PUT", | ||
| 757 | + "path": "/api/settings/budget", | ||
| 758 | + "auth": "system_internal", | ||
| 759 | + "input": {"scope": "string(required)", "limit_type": "string(required)", "limit_value": "int(required, >0)"}, | ||
| 760 | + "output": {"config": "object"}, | ||
| 761 | + "idempotent": true, | ||
| 762 | + "errors": [ | ||
| 763 | + {"code": "INVALID_PARAM", "retryable": false, "description": "数值超出范围"} | ||
| 764 | + ] | ||
| 765 | + }, | ||
| 766 | + { | ||
| 767 | + "operation_id": "c_deploy_supervisor_stage", | ||
| 768 | + "method": "POST", | ||
| 769 | + "path": "internal://deploy-supervisor/stage", | ||
| 770 | + "auth": "system_internal", | ||
| 771 | + "input": {"git_commit_hash": "string(required)"}, | ||
| 772 | + "output": {"deployment_id": "string", "status": "staged"}, | ||
| 773 | + "idempotent": false, | ||
| 774 | + "errors": [ | ||
| 775 | + {"code": "CHECKOUT_FAILED", "retryable": false, "description": "全新目录 checkout 失败(如磁盘空间不足),转 blocked_pending_human"} | ||
| 776 | + ] | ||
| 777 | + }, | ||
| 778 | + { | ||
| 779 | + "operation_id": "c_deploy_supervisor_health_check", | ||
| 780 | + "method": "POST", | ||
| 781 | + "path": "internal://deploy-supervisor/health-check", | ||
| 782 | + "auth": "system_internal", | ||
| 783 | + "input": {"deployment_id": "string(required)"}, | ||
| 784 | + "output": {"status": "string", "signals": "deploy_health_signal[]"}, | ||
| 785 | + "idempotent": true, | ||
| 786 | + "errors": [ | ||
| 787 | + {"code": "HEALTH_CHECK_TIMEOUT", "retryable": false, "description": "process_alive/http_health/canary_task 任一超时,直接判定该项 fail,进入回滚路径 A,不重试"} | ||
| 788 | + ] | ||
| 789 | + }, | ||
| 790 | + { | ||
| 791 | + "operation_id": "c_deploy_supervisor_cutover", | ||
| 792 | + "method": "POST", | ||
| 793 | + "path": "internal://deploy-supervisor/cutover", | ||
| 794 | + "auth": "system_internal", | ||
| 795 | + "input": {"deployment_id": "string(required)"}, | ||
| 796 | + "output": {"status": "live", "cutover_at": "timestamp"}, | ||
| 797 | + "idempotent": false, | ||
| 798 | + "errors": [ | ||
| 799 | + {"code": "CUTOVER_FAILED", "retryable": false, "description": "候选进程未能正确绑定生产端口/生产库,转 blocked_pending_human"} | ||
| 800 | + ] | ||
| 801 | + }, | ||
| 802 | + { | ||
| 803 | + "operation_id": "c_deploy_supervisor_monitor", | ||
| 804 | + "method": "POST", | ||
| 805 | + "path": "internal://deploy-supervisor/monitor", | ||
| 806 | + "auth": "system_internal", | ||
| 807 | + "input": {"deployment_id": "string(required)"}, | ||
| 808 | + "output": {"status": "string", "latest_signal": "object"}, | ||
| 809 | + "idempotent": true, | ||
| 810 | + "errors": [] | ||
| 811 | + }, | ||
| 812 | + { | ||
| 813 | + "operation_id": "c_deploy_supervisor_rollback", | ||
| 814 | + "method": "POST", | ||
| 815 | + "path": "internal://deploy-supervisor/rollback", | ||
| 816 | + "auth": "system_internal", | ||
| 817 | + "input": {"deployment_id": "string(required)", "rollback_reason": "string(required)"}, | ||
| 818 | + "output": {"status": "string"}, | ||
| 819 | + "idempotent": false, | ||
| 820 | + "errors": [ | ||
| 821 | + {"code": "ROLLBACK_FAILED", "retryable": false, "description": "previous_instance_dir 也无法启动,或 git revert 冲突,转 blocked_pending_human,blocked_reason=rollback_failed;这是唯一保留的人工介入点,触发条件严格限定为自动恢复机制本身失效,不是代码判断谁对谁错"} | ||
| 822 | + ] | ||
| 823 | + } | ||
| 824 | + ], | ||
| 825 | + "process_contract": [ | ||
| 826 | + { | ||
| 827 | + "flow_id": "p_org_task_lifecycle", | ||
| 828 | + "start_state": "queued", | ||
| 829 | + "end_state": "done", | ||
| 830 | + "transitions": [ | ||
| 831 | + {"from": "queued", "to": "planning_brd_prd_sdd", "trigger": "c_org_node_trigger(planner)", "action": "c_work_graph_plan+c_work_graph_execute", "guard": "concurrency_ok", "error_state": "blocked_pending_human", "retry_max": 0}, | ||
| 832 | + {"from": "planning_brd_prd_sdd", "to": "coding", "trigger": "planner_work_graph_done", "action": "c_org_node_trigger(coder)", "guard": "artifacts_validated", "error_state": "planning_failed", "retry_max": 2}, | ||
| 833 | + {"from": "planning_failed", "to": "planning_brd_prd_sdd", "trigger": "retry", "action": "replan", "guard": "retry_count<2", "error_state": "blocked_pending_human"}, | ||
| 834 | + {"from": "coding", "to": "validating", "trigger": "coder_work_graph_done", "action": "c_org_node_trigger(validator)", "guard": "self_test_passed", "error_state": "coding_failed", "retry_max": 2}, | ||
| 835 | + {"from": "coding_failed", "to": "coding", "trigger": "retry", "action": "recode", "guard": "retry_count<2", "error_state": "blocked_pending_human"}, | ||
| 836 | + {"from": "validating", "to": "done", "trigger": "validator_pass", "action": "close_task", "error_state": ""}, | ||
| 837 | + {"from": "validating", "to": "routed_fast_lane", "trigger": "validator_fail", "action": "c_bug_record_create+c_org_loop_route(fast_lane)", "guard": "loop_round<3", "error_state": "blocked_pending_human"}, | ||
| 838 | + {"from": "validating", "to": "routed_full_replan", "trigger": "validator_fail", "action": "c_bug_record_create+c_org_loop_route(full_replan)", "guard": "loop_round<3", "error_state": "blocked_pending_human"}, | ||
| 839 | + {"from": "routed_fast_lane", "to": "coding", "trigger": "route_applied", "action": "loop_round+=1", "error_state": ""}, | ||
| 840 | + {"from": "routed_full_replan", "to": "planning_brd_prd_sdd", "trigger": "route_applied", "action": "loop_round+=1", "error_state": ""}, | ||
| 841 | + {"from": "blocked_pending_human", "to": "resumed_prior_state", "trigger": "c_org_loop_route(manual_resume)", "action": "manual_resume", "error_state": ""} | ||
| 842 | + ] | ||
| 843 | + }, | ||
| 844 | + { | ||
| 845 | + "flow_id": "p_work_graph_execution", | ||
| 846 | + "start_state": "planning", | ||
| 847 | + "end_state": "done", | ||
| 848 | + "transitions": [ | ||
| 849 | + {"from": "planning", "to": "running", "trigger": "plan_ready", "action": "c_work_graph_execute", "guard": "manifest_passes_antipattern_gate", "error_state": "failed"}, | ||
| 850 | + {"from": "running", "to": "merging", "trigger": "fanout_nodes_settled", "action": "fanin_edge", "error_state": "failed"}, | ||
| 851 | + {"from": "merging", "to": "verifying", "trigger": "fanin_done", "action": "node_verification", "error_state": "failed"}, | ||
| 852 | + {"from": "verifying", "to": "done", "trigger": "verify_passed", "action": "c_artifact_write", "error_state": ""}, | ||
| 853 | + {"from": "verifying", "to": "reflecting", "trigger": "verify_failed", "action": "record_gap", "guard": "retry_count<node_level_max", "error_state": "failed"}, | ||
| 854 | + {"from": "reflecting", "to": "running", "trigger": "reflection_retry", "action": "rerun_min_subgraph", "error_state": ""}, | ||
| 855 | + {"from": "running", "to": "blocked", "trigger": "all_parallel_branches_failed", "action": "bubble_up", "error_state": ""} | ||
| 856 | + ] | ||
| 857 | + }, | ||
| 858 | + { | ||
| 859 | + "flow_id": "p_validator_root_cause_trace", | ||
| 860 | + "start_state": "test_failed_or_human_reported", | ||
| 861 | + "end_state": "bug_recorded", | ||
| 862 | + "transitions": [ | ||
| 863 | + {"from": "human_reported", "to": "tracing_lineage", "trigger": "c_bug_report_create", "action": "validator_claims_open_bug_record", "error_state": "tracing_failed", "retry_max": 1}, | ||
| 864 | + {"from": "test_failed", "to": "tracing_lineage", "trigger": "test_or_benchmark_failed", "action": "read_lineage", "error_state": "tracing_failed", "retry_max": 1}, | ||
| 865 | + {"from": "tracing_lineage", "to": "judging_root_cause", "trigger": "lineage_ready", "action": "multi_perspective_verify", "guard": "2_of_3_perspectives_agree", "error_state": "inconclusive_escalate"}, | ||
| 866 | + {"from": "judging_root_cause", "to": "bug_recorded", "trigger": "root_cause_decided", "action": "c_bug_record_create", "guard": "root_cause_detail_present", "error_state": ""}, | ||
| 867 | + {"from": "judging_root_cause", "to": "inconclusive_escalate", "trigger": "perspectives_disagree", "action": "mark_blocked_pending_human", "error_state": ""}, | ||
| 868 | + {"from": "bug_recorded", "to": "skill_iteration_triggered", "trigger": "root_cause_category==skill_defect", "action": "c_skill_update", "guard": "triggered_by_bug_id_present", "error_state": "skill_update_failed", "retry_max": 1} | ||
| 869 | + ] | ||
| 870 | + }, | ||
| 871 | + { | ||
| 872 | + "flow_id": "p_budget_guardrail", | ||
| 873 | + "start_state": "monitoring", | ||
| 874 | + "end_state": "paused", | ||
| 875 | + "transitions": [ | ||
| 876 | + {"from": "monitoring", "to": "monitoring", "trigger": "c_budget_check_before_node_run", "action": "accumulate_usage", "guard": "current_usage<limit_value", "error_state": ""}, | ||
| 877 | + {"from": "monitoring", "to": "paused", "trigger": "budget_check_denied", "action": "mark_blocked_pending_human", "error_state": ""}, | ||
| 878 | + {"from": "paused", "to": "monitoring", "trigger": "manual_resume_or_limit_raised", "action": "resume_scheduling", "error_state": ""} | ||
| 879 | + ] | ||
| 880 | + }, | ||
| 881 | + { | ||
| 882 | + "flow_id": "p_usage_limit_wait", | ||
| 883 | + "start_state": "running", | ||
| 884 | + "end_state": "done", | ||
| 885 | + "transitions": [ | ||
| 886 | + {"from": "running", "to": "waiting_for_quota", "trigger": "USAGE_LIMIT_REACHED", "action": "record_quota_wait_started_at_and_type", "guard": "", "error_state": ""}, | ||
| 887 | + {"from": "waiting_for_quota", "to": "waiting_for_quota", "trigger": "backoff_interval_elapsed", "action": "retry_original_call_as_probe", "guard": "elapsed<timeout_threshold", "error_state": ""}, | ||
| 888 | + {"from": "waiting_for_quota", "to": "running", "trigger": "probe_succeeded", "action": "resume_normal_execution", "guard": "", "error_state": ""}, | ||
| 889 | + {"from": "waiting_for_quota", "to": "blocked_pending_human", "trigger": "timeout_threshold_exceeded", "action": "mark_blocked_pending_human", "guard": "", "error_state": "usage_limit_wait_timeout"} | ||
| 890 | + ] | ||
| 891 | + }, | ||
| 892 | + { | ||
| 893 | + "flow_id": "p_log_retention_cleanup", | ||
| 894 | + "start_state": "idle", | ||
| 895 | + "end_state": "idle", | ||
| 896 | + "transitions": [ | ||
| 897 | + {"from": "idle", "to": "scanning", "trigger": "scheduled_daily", "action": "scan_closed_log_files", "error_state": "scan_failed", "retry_max": 1}, | ||
| 898 | + {"from": "scanning", "to": "purging", "trigger": "scan_complete", "action": "filter_by_retention_and_size_policy", "error_state": ""}, | ||
| 899 | + {"from": "purging", "to": "idle", "trigger": "delete_success", "action": "mark_purged", "error_state": "purge_partial_failed"}, | ||
| 900 | + {"from": "purge_partial_failed", "to": "idle", "trigger": "partial_failure_recorded", "action": "keep_closed_for_next_cycle", "error_state": ""} | ||
| 901 | + ] | ||
| 902 | + }, | ||
| 903 | + { | ||
| 904 | + "flow_id": "p_deploy_supervisor_blue_green", | ||
| 905 | + "start_state": "staged", | ||
| 906 | + "end_state": "finalized", | ||
| 907 | + "transitions": [ | ||
| 908 | + {"from": "staged", "to": "candidate_starting", "trigger": "c_deploy_supervisor_stage_done", "action": "checkout_new_dir_and_start_candidate", "error_state": "blocked_pending_human", "retry_max": 0}, | ||
| 909 | + {"from": "candidate_starting", "to": "health_check_running", "trigger": "candidate_process_started", "action": "c_deploy_supervisor_health_check", "error_state": "", "retry_max": 0}, | ||
| 910 | + {"from": "health_check_running", "to": "health_check_failed", "trigger": "any_signal_fail_or_timeout", "action": "rollback_path_a", "error_state": "", "retry_max": 0}, | ||
| 911 | + {"from": "health_check_running", "to": "health_check_passed", "trigger": "all_signals_ok", "action": "proceed_to_cutover", "error_state": "", "retry_max": 0}, | ||
| 912 | + {"from": "health_check_passed", "to": "cutover_in_progress", "trigger": "c_deploy_supervisor_cutover", "action": "stop_live_accept_no_new_triggers", "error_state": "blocked_pending_human", "retry_max": 0}, | ||
| 913 | + {"from": "cutover_in_progress", "to": "live", "trigger": "candidate_bound_to_prod_port_and_db", "action": "mark_cutover_at", "error_state": "", "retry_max": 0}, | ||
| 914 | + {"from": "live", "to": "monitoring", "trigger": "cutover_done", "action": "c_deploy_supervisor_monitor_start", "error_state": "", "retry_max": 0}, | ||
| 915 | + {"from": "monitoring", "to": "anomaly_detected", "trigger": "signal_deviation_exceeds_threshold", "action": "record_rollback_reason_snapshot", "error_state": "", "retry_max": 0}, | ||
| 916 | + {"from": "monitoring", "to": "monitoring_window_clean", "trigger": "monitoring_window_elapsed_without_anomaly", "action": "", "error_state": "", "retry_max": 0}, | ||
| 917 | + {"from": "monitoring_window_clean", "to": "finalized", "trigger": "", "action": "allow_previous_instance_dir_cleanup", "error_state": "", "retry_max": 0}, | ||
| 918 | + {"from": "anomaly_detected", "to": "rollback_in_progress", "trigger": "c_deploy_supervisor_rollback", "action": "rollback_path_b", "error_state": "", "retry_max": 0}, | ||
| 919 | + {"from": "rollback_in_progress", "to": "rolled_back", "trigger": "previous_instance_dir_restarted_and_healthy", "action": "bind_live_port_and_prod_db_git_revert_bug_record", "error_state": "", "retry_max": 0}, | ||
| 920 | + {"from": "rollback_in_progress", "to": "rollback_failed", "trigger": "previous_instance_also_unhealthy_or_git_revert_conflict", "action": "", "error_state": "blocked_pending_human", "retry_max": 0}, | ||
| 921 | + {"from": "health_check_failed", "to": "rolled_back", "trigger": "rollback_path_a_complete", "action": "terminate_candidate_delete_db_copy_git_revert_bug_record", "error_state": "", "retry_max": 0} | ||
| 922 | + ] | ||
| 923 | + } | ||
| 924 | + ], | ||
| 925 | + "acceptance_matrix": [ | ||
| 926 | + { | ||
| 927 | + "requirement_id": "REQ-01", | ||
| 928 | + "coverage": {"model": ["org_graph_definition", "task"], "view": ["v_task_board"], "controller": ["c_task_create", "c_org_node_trigger"], "process": ["p_org_task_lifecycle"]}, | ||
| 929 | + "acceptance_cases": [ | ||
| 930 | + {"type": "happy", "description": "创建贪吃蛇任务并依次触发三个 org_node", "expected": "task.status 按状态机正确流转"}, | ||
| 931 | + {"type": "boundary", "description": "并发数=1 时创建第二个任务", "expected": "第二个任务落 queued 不被立即调度"}, | ||
| 932 | + {"type": "failure", "description": "goal_text 为空", "expected": "返回 INVALID_PARAM"} | ||
| 933 | + ] | ||
| 934 | + }, | ||
| 935 | + { | ||
| 936 | + "requirement_id": "REQ-02", | ||
| 937 | + "coverage": {"model": ["artifact", "work_graph_run"], "view": ["v_task_detail"], "controller": ["c_work_graph_plan", "c_work_graph_execute", "c_artifact_write"], "process": ["p_work_graph_execution"]}, | ||
| 938 | + "acceptance_cases": [ | ||
| 939 | + {"type": "happy", "description": "Planner 产出 BRD/PRD/SDD/Benchmark 四类 artifact 且血缘完整", "expected": "四条记录存在且 depends_on_artifact_ids 链完整"}, | ||
| 940 | + {"type": "boundary", "description": "PRD 出现矛盾场景假设", "expected": "verifying 态拦截,转 reflecting"}, | ||
| 941 | + {"type": "failure", "description": "SDD 引用不存在的 PRD 版本", "expected": "c_artifact_write 拒绝写入"} | ||
| 942 | + ] | ||
| 943 | + }, | ||
| 944 | + { | ||
| 945 | + "requirement_id": "REQ-03", | ||
| 946 | + "coverage": {"model": ["work_node_execution"], "view": [], "controller": ["c_work_node_run"], "process": ["p_work_graph_execution"]}, | ||
| 947 | + "acceptance_cases": [ | ||
| 948 | + {"type": "happy", "description": "Coder 完成编码并通过 unit+integration test", "expected": "code_diff 与 test_report 产出且测试全部通过"}, | ||
| 949 | + {"type": "boundary", "description": "两模块并行编码使用独立 git worktree", "expected": "合并后代码无冲突"}, | ||
| 950 | + {"type": "failure", "description": "单测失败达重试上限", "expected": "work_node_execution.status=blocked"} | ||
| 951 | + ] | ||
| 952 | + }, | ||
| 953 | + { | ||
| 954 | + "requirement_id": "REQ-04", | ||
| 955 | + "coverage": {"model": ["bug_record"], "view": [], "controller": ["c_bug_record_create"], "process": ["p_validator_root_cause_trace"]}, | ||
| 956 | + "acceptance_cases": [ | ||
| 957 | + {"type": "happy", "description": "植入 coder 类型 bug", "expected": "root_cause_category=coder 且 detail 引用具体位置"}, | ||
| 958 | + {"type": "boundary", "description": "植入 planning 类型 bug", "expected": "root_cause_category=planning"}, | ||
| 959 | + {"type": "failure", "description": "三视角判断不一致", "expected": "落在 inconclusive_escalate"} | ||
| 960 | + ] | ||
| 961 | + }, | ||
| 962 | + { | ||
| 963 | + "requirement_id": "REQ-05", | ||
| 964 | + "coverage": {"model": ["task"], "view": ["v_task_detail"], "controller": ["c_org_loop_route"], "process": ["p_org_task_lifecycle"]}, | ||
| 965 | + "acceptance_cases": [ | ||
| 966 | + {"type": "happy", "description": "fast_lane 路由修复后重新验证通过", "expected": "task.status=done,loop_round=1"}, | ||
| 967 | + {"type": "boundary", "description": "full_replan 路由", "expected": "回到 Planner 而非 Coder"}, | ||
| 968 | + {"type": "failure", "description": "连续 3 轮未收敛", "expected": "转 blocked_pending_human,调度器继续跑其他任务"} | ||
| 969 | + ] | ||
| 970 | + }, | ||
| 971 | + { | ||
| 972 | + "requirement_id": "REQ-06", | ||
| 973 | + "coverage": {"model": ["work_graph_run", "work_node_execution"], "view": [], "controller": [], "process": ["p_work_graph_execution"]}, | ||
| 974 | + "acceptance_cases": [ | ||
| 975 | + {"type": "happy", "description": "running 中人为中断进程后重启", "expected": "从持久化状态恢复,不重跑已完成节点"}, | ||
| 976 | + {"type": "boundary", "description": "中断发生在 retrying 中途", "expected": "恢复到重试逻辑而非跳过"}, | ||
| 977 | + {"type": "failure", "description": "持久化数据损坏", "expected": "明确报错并转 blocked_pending_human"} | ||
| 978 | + ] | ||
| 979 | + }, | ||
| 980 | + { | ||
| 981 | + "requirement_id": "REQ-07", | ||
| 982 | + "coverage": {"model": [], "view": [], "controller": ["c_work_graph_execute"], "process": []}, | ||
| 983 | + "acceptance_cases": [ | ||
| 984 | + {"type": "happy", "description": "Claude Code ACP 后端跑通 REQ-01~06", "expected": "全部通过"}, | ||
| 985 | + {"type": "boundary", "description": "claude -p 调用不加 --bare(A-1 spike 结论)", "expected": "冷启动开销在可接受范围内"}, | ||
| 986 | + {"type": "failure", "description": "auto mode 分类器连续拦截 3 次或累计 20 次", "expected": "返回 CLASSIFIER_BLOCKED,task 转 blocked_pending_human,blocked_reason=classifier_blocked"} | ||
| 987 | + ] | ||
| 988 | + }, | ||
| 989 | + { | ||
| 990 | + "requirement_id": "REQ-08", | ||
| 991 | + "coverage": {"model": ["skill", "skill_iteration"], "view": ["v_skill_audit_log"], "controller": ["c_skill_update", "c_skill_rollback"], "process": ["p_validator_root_cause_trace"]}, | ||
| 992 | + "acceptance_cases": [ | ||
| 993 | + {"type": "happy", "description": "skill_defect 类 bug 触发 skill 自主迭代", "expected": "生成独立 git commit,审计日志可查"}, | ||
| 994 | + {"type": "boundary", "description": "triggered_by_bug_id 缺失", "expected": "c_skill_update 拒绝"}, | ||
| 995 | + {"type": "failure", "description": "回滚到不存在的 iteration_id", "expected": "返回 ITERATION_NOT_FOUND"} | ||
| 996 | + ] | ||
| 997 | + }, | ||
| 998 | + { | ||
| 999 | + "requirement_id": "REQ-09", | ||
| 1000 | + "coverage": {"model": ["task"], "view": ["v_task_detail"], "controller": ["c_org_loop_route"], "process": ["p_org_task_lifecycle"]}, | ||
| 1001 | + "acceptance_cases": [ | ||
| 1002 | + {"type": "happy", "description": "人工 manual_resume 恢复任务", "expected": "恢复到触发前状态继续执行"}, | ||
| 1003 | + {"type": "boundary", "description": "一个任务 blocked 时其余 queued 任务", "expected": "调度器不被阻塞任务卡住"}, | ||
| 1004 | + {"type": "failure", "description": "对已 done 任务调用 manual_resume", "expected": "返回明确错误"} | ||
| 1005 | + ] | ||
| 1006 | + }, | ||
| 1007 | + { | ||
| 1008 | + "requirement_id": "REQ-10", | ||
| 1009 | + "coverage": {"model": ["budget_guardrail_config"], "view": ["v_budget_settings"], "controller": ["c_budget_check", "c_budget_config_update"], "process": ["p_budget_guardrail"]}, | ||
| 1010 | + "acceptance_cases": [ | ||
| 1011 | + {"type": "happy", "description": "超出配置额度", "expected": "任务自动转 blocked_pending_human,blocked_reason=budget_exceeded"}, | ||
| 1012 | + {"type": "boundary", "description": "调高额度后", "expected": "paused 态任务恢复调度"}, | ||
| 1013 | + {"type": "failure", "description": "平台层额度耗尽(非业务配置触发)", "expected": "blocked_reason=platform_quota_exhausted,UI 区分展示"} | ||
| 1014 | + ] | ||
| 1015 | + }, | ||
| 1016 | + { | ||
| 1017 | + "requirement_id": "REQ-11", | ||
| 1018 | + "coverage": {"model": ["project", "bug_record"], "view": ["v_project_setup", "v_goal_composer", "v_bug_feedback"], "controller": ["c_project_create", "c_task_create", "c_bug_report_create"], "process": ["p_validator_root_cause_trace"]}, | ||
| 1019 | + "acceptance_cases": [ | ||
| 1020 | + {"type": "happy", "description": "选择已有 Git 仓库文件夹创建项目并提出目标", "expected": ".graphlemon/ 子目录创建成功,task 成功进入 Org Graph"}, | ||
| 1021 | + {"type": "boundary", "description": "选择非 Git 仓库文件夹", "expected": "用户确认后 git init,不确认则中止且无任何写入"}, | ||
| 1022 | + {"type": "failure", "description": "重复创建同一 target_folder_path 的项目 / 提交空 description 的 bug 反馈", "expected": "前者幂等返回已有项目,后者被拦截"} | ||
| 1023 | + ] | ||
| 1024 | + }, | ||
| 1025 | + { | ||
| 1026 | + "requirement_id": "REQ-12", | ||
| 1027 | + "coverage": {"model": ["execution_event"], "view": ["v_dashboard", "v_graph_visualizer", "v_task_detail"], "controller": ["c_ws_subscribe"], "process": []}, | ||
| 1028 | + "acceptance_cases": [ | ||
| 1029 | + {"type": "happy", "description": "节点状态变化后图可视化实时着色更新", "expected": "≤2秒内更新,无需手动刷新"}, | ||
| 1030 | + {"type": "boundary", "description": "WebSocket 断线重连", "expected": "展示重连提示,重连后拉取当前快照兜底"}, | ||
| 1031 | + {"type": "failure", "description": "订阅不存在的 project_id", "expected": "连接被拒绝,返回 PROJECT_NOT_FOUND"} | ||
| 1032 | + ] | ||
| 1033 | + }, | ||
| 1034 | + { | ||
| 1035 | + "requirement_id": "REQ-13", | ||
| 1036 | + "coverage": {"model": ["execution_log_file", "log_retention_config"], "view": ["v_log_viewer", "v_log_retention_settings"], "controller": ["c_execution_log_get", "c_ws_log_tail", "c_log_retention_config_update"], "process": ["p_log_retention_cleanup"]}, | ||
| 1037 | + "acceptance_cases": [ | ||
| 1038 | + {"type": "happy", "description": "运行中节点实时滚动日志,结束后切换静态查看", "expected": "日志内容与实际执行过程一致,无缺失"}, | ||
| 1039 | + {"type": "boundary", "description": "retention_days=1 时清理超过1天的closed日志", "expected": "状态转purged,物理文件删除,UI展示已清理提示"}, | ||
| 1040 | + {"type": "failure", "description": "订阅不存在的 node_exec_id 日志流 / 清理时文件删除失败", "expected": "前者返回 NODE_EXEC_NOT_FOUND,后者仅记录失败不影响其余清理项"} | ||
| 1041 | + ] | ||
| 1042 | + }, | ||
| 1043 | + { | ||
| 1044 | + "requirement_id": "REQ-14", | ||
| 1045 | + "coverage": {"model": ["work_node_execution"], "view": ["v_dashboard", "v_graph_visualizer"], "controller": ["c_work_node_run", "c_org_node_trigger"], "process": ["p_usage_limit_wait"]}, | ||
| 1046 | + "acceptance_cases": [ | ||
| 1047 | + {"type": "happy", "description": "claude -p 返回 session limit 错误", "expected": "work_node_execution.status 转 waiting_for_quota,退避轮询后探测成功恢复正常执行,全程不产生 blocked_pending_human"}, | ||
| 1048 | + {"type": "boundary", "description": "quota_limit_type=opus", "expected": "不走退避轮询,直接降级到非 Opus 模型重试"}, | ||
| 1049 | + {"type": "failure", "description": "等待超过超时阈值(session 8小时/weekly 9天)仍未恢复", "expected": "转 blocked_pending_human,blocked_reason=usage_limit_wait_timeout"} | ||
| 1050 | + ] | ||
| 1051 | + }, | ||
| 1052 | + { | ||
| 1053 | + "requirement_id": "REQ-15", | ||
| 1054 | + "coverage": {"model": ["deployment", "deploy_health_signal", "bug_record"], "view": ["v_deployment_status"], "controller": ["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"], "process": ["p_deploy_supervisor_blue_green"]}, | ||
| 1055 | + "acceptance_cases": [ | ||
| 1056 | + {"type": "happy", "description": "graphlemon 自身真实植入 bug 修复后,root_cause_category=core_engine 触发蓝绿部署,健康检查全过,监控窗口无异常", "expected": "staged 到 finalized 全流程走完,全程无需人工评审代码内容"}, | ||
| 1057 | + {"type": "boundary", "description": "候选实例 canary_task 健康检查失败", "expected": "立即判定 health_check_failed,不重试,走回滚路径 A,Live 全程未受影响"}, | ||
| 1058 | + {"type": "boundary", "description": "cutover 完成后 validator_approval_rate 偏离历史基线阈值", "expected": "判定 anomaly_detected,走回滚路径 B,previous_instance_dir 重新成为 Live,生产库数据未丢失"}, | ||
| 1059 | + {"type": "failure", "description": "回滚路径 B 中 previous_instance_dir 也无法启动", "expected": "转 blocked_pending_human,blocked_reason=rollback_failed,且控制台展示的原因需清楚区分自动恢复机制失效与代码内容问题"} | ||
| 1060 | + ] | ||
| 1061 | + } | ||
| 1062 | + ], | ||
| 1063 | + "non_goals": [ | ||
| 1064 | + "不做飞书 Base 历史数据自动迁移", | ||
| 1065 | + "不做多租户/多团队权限体系", | ||
| 1066 | + "不考虑 Hermes CLI 执行后端(决策,2026-07-29:MVP 阶段只用 Claude Code ACP 一种后端)", | ||
| 1067 | + "不做 Skill 自主迭代的人工审批 UI(立即生效,仅需 Git 可追溯)", | ||
| 1068 | + "不做完整多项目管理 UI(项目列表/切换),数据模型预留 project 但 MVP 只支持单个 active 项目", | ||
| 1069 | + "不做日志清理周期的用户可配置(仅保留天数/总量上限可配,清理任务本身触发频率由平台固定,不开放给用户调整)" | ||
| 1070 | + ], | ||
| 1071 | + "assumptions": [ | ||
| 1072 | + {"id": "A-1", "statement": "Claude Code 的 agent()/parallel()/pipeline() 原语可通过 claude -p 非交互式调用并返回结构化可解析结果", "owner": "bboonnee", "verification_plan": "spike 实测(spikes/a1-dynamic-workflows/),真实账号运行验证", "status": "confirmed", "verified_at": "2026-07-29", "verification_note": "claude -p ... < /dev/null 无交互提示、40s 完成、structured_output 精确匹配预期(sum=29)、permission_denials 为空;限制:不能加 --bare(会跳过登录态发现),须用完整冷启动调用,见 03-C 3.5 节实现契约"}, | ||
| 1073 | + {"id": "A-2", "statement": "本地 Hermes profile 的 SOUL.md/AGENTS.md 可原样迁移到目标项目 .claude/agents/*.md 不需大改", "owner": "bboonnee", "verification_plan": "实际迁移一次并跑通 MVP 场景验证", "status": "pending"}, | ||
| 1074 | + {"id": "A-3", "statement": "Hermes CLI 后端 MVP 阶段以单点执行 Org Agent 方式接入即可满足验收", "owner": "bboonnee", "verification_plan": "不适用", "status": "not_applicable", "resolution_note": "决策,2026-07-29:MVP 阶段不考虑 Hermes CLI 执行后端,只用 Claude Code ACP,见架构文档 7.2 节"}, | ||
| 1075 | + {"id": "A-4", "statement": "控制台为本地 Web 应用,文件夹选择由本地后端代拉系统原生对话框(c_pick_folder)实现", "owner": "bboonnee", "verification_plan": "已确认为产品决策(架构文档13.1决策5),转为实施约束,需在实施时按 macOS/Windows/Linux 分别验证 c_pick_folder 的原生对话框调用方式", "status": "confirmed"} | ||
| 1076 | + ], | ||
| 1077 | + "risks": [ | ||
| 1078 | + {"id": "R-1", "description": "A-1 假设不成立,Claude Code 动态工作流无法编程式调用", "impact": "high", "mitigation": "回退到架构文档 10.1 节方案 B(完全自研 Work Graph 执行层),需重新评估工作量和排期", "status": "resolved", "resolution_note": "A-1 已于 2026-07-29 spike 实测确认成立,方案 A 按 03-C 3.5 节实现契约推进;方案 B 作为长期备选保留,未触发"}, | ||
| 1079 | + {"id": "R-2", "description": "双后端能力不对等导致后续开发中被误判为对等而产生缺陷", "impact": "medium", "mitigation": "架构文档 7.2 节 2026-07-29 决策:MVP 阶段不考虑 Hermes CLI 后端,只用 Claude Code ACP 一种后端,风险不再适用", "status": "not_applicable"}, | ||
| 1080 | + {"id": "R-3", "description": "Skill 自主迭代立即生效可能引入回归缺陷", "impact": "medium", "mitigation": "强制 Git 提交 + c_skill_rollback 一键回滚能力兜底", "status": "mitigated"}, | ||
| 1081 | + {"id": "R-4", "description": "WebSocket 长连接在弱网/断线场景下的一致性问题(推送丢失)", "impact": "medium", "mitigation": "MVP 用重连后拉取快照兜底,不做完整消息重放缓冲,见 03-C 3.14 节", "status": "mitigated"}, | ||
| 1082 | + {"id": "R-5", "description": "原始日志本地落盘在高频/长时间运行下可能造成磁盘占用持续增长", "impact": "medium", "mitigation": "log_retention_config 提供按天数和总大小双重上限的定时清理(p_log_retention_cleanup),MVP 默认 14 天", "status": "mitigated"}, | ||
| 1083 | + {"id": "R-6", "description": "auto mode 分类器误拦截合法操作,导致任务频繁转 blocked_pending_human,影响无人值守吞吐", "impact": "medium", "mitigation": "MVP 阶段接受较高误拦截率,靠人工在 v_task_detail 复核放行;03-C 3.5/04-P 已定义 CLASSIFIER_BLOCKED 到 blocked_pending_human 的路由;后续可评估是否引入 auto mode 的受信任基础设施配置(05 节 5.3.1 待办项)降低误拦截率", "status": "accepted"}, | ||
| 1084 | + {"id": "R-7", "description": "Claude Code plan 额度等待重试的超时阈值(session 8小时/weekly 9天)是基于当前官方文档窗口周期(5小时滚动/7天)估算的硬编码值,若额度窗口周期变化会导致阈值不再合理(过早误报或过晚才升级人工)", "impact": "low", "mitigation": "05 节 5.4.2 已注明需要跟踪官方文档变化同步调整;MVP 阶段接受这个假设固定的风险", "status": "accepted"}, | ||
| 1085 | + {"id": "R-8", "description": "PH5 Deploy Supervisor 的 MVP 固定参数(05 节 §5.9:健康检查超时、监控窗口时长、validator_approval_rate 偏离阈值等)是工程经验草案,没有类似 R-7 额度窗口那样的外部权威依据,实现前无法验证是否合理", "impact": "medium", "mitigation": "05 节 §5.9 已标注为需要在真实跑通几轮自我修改后实测校准,不建议实现时直接当成不可变常量写入测试断言;PH5 U-042 端到端验收本身就是校准这些参数的第一个真实数据点", "status": "accepted"}, | ||
| 1086 | + {"id": "R-9", "description": "PH5 Deploy Supervisor 的实现代码(c_deploy_supervisor_* 五个控制操作、p_deploy_supervisor_blue_green 状态机本体)尚未编写,仅 supervisor/isolation/ 四个隔离/manifest/apply 模块(从 graphloop 复制改造)已落地且经过一次手工冒烟测试,未有正式 unittest 覆盖", "impact": "medium", "mitigation": "development-plan.md PH5-S2 的 U-036 明确把补齐单元测试列为独立 Unit;PH5 依赖 PH4 全部收口才启动,不影响 PH0-PH4 主线进度", "status": "accepted"} | ||
| 1087 | + ] | ||
| 1088 | +} |
docs/sdd/unit_matrix.json
0 → 100644
| 1 | +++ a/docs/sdd/unit_matrix.json | ||
| 1 | +{ | ||
| 2 | + "meta": { | ||
| 3 | + "project": "graphlemon", | ||
| 4 | + "version": "v1.0", | ||
| 5 | + "generated_from": "docs/development-plan.md", | ||
| 6 | + "source_sdd_version": "v1.6.0", | ||
| 7 | + "methodology": "project-unit-decomposition + engineering-work-planning (local Hermes planning profile)", | ||
| 8 | + "date": "2026-07-29", | ||
| 9 | + "revisions": [ | ||
| 10 | + {"date": "2026-07-30", "note": "计划评审追加 U-004.5(RUN_REAL_AGENT_E2E harness 归属)与 U-024.5(U-024 依赖分级后的 REQ-12 完整验收);U-024 依赖从 U-009~U-021 收窄为 U-012/U-013;U-035 done 标准追加 REQ-03 回归要求"} | ||
| 11 | + ] | ||
| 12 | + }, | ||
| 13 | + "units": [ | ||
| 14 | + {"id": "U-001", "phase": "PH0", "sub_phase": "PH0-S1", "name": "01-M 全部 15 实体物理 schema", "classification": "Enabler", "depends_on": [], "coverage": {"model": ["project", "org_graph_definition", "task", "work_graph_run", "work_node_execution", "artifact", "bug_record", "skill", "skill_iteration", "agent_profile", "budget_guardrail_config", "org_concurrency_config", "execution_event", "execution_log_file", "log_retention_config"], "view": [], "controller": [], "process": [], "requirement": []}, "done": "15 张表全部建表成功,关键约束(唯一/外键/枚举)有单测覆盖"}, | ||
| 15 | + {"id": "U-002", "phase": "PH0", "sub_phase": "PH0-S1", "name": "目标项目 Git 仓库目录骨架初始化", "classification": "Enabler", "depends_on": ["U-001"], "coverage": {"model": ["project"], "view": [], "controller": ["c_project_create"], "process": [], "requirement": []}, "done": "给定空 git 仓库,能生成完整目录骨架并产生初始化 commit"}, | ||
| 16 | + {"id": "U-003", "phase": "PH0", "sub_phase": "PH0-S1", "name": "graphlemon 平台自身仓库/运行时目录结构", "classification": "Enabler", "depends_on": [], "coverage": {"model": [], "view": [], "controller": [], "process": [], "requirement": []}, "done": "平台目录与目标项目目录物理分离,互不干扰"}, | ||
| 17 | + {"id": "U-004", "phase": "PH0", "sub_phase": "PH0-S2", "name": "Hermes profile 迁移到 .claude/agents/*.md", "classification": "Risk Gate", "depends_on": ["U-002"], "coverage": {"model": [], "view": [], "controller": [], "process": [], "requirement": [], "assumption": ["A-2"]}, "done": "claude --agent planner -p 可正常启动且系统提示词正确;skills/memory/tools/model frontmatter 配置完整"}, | ||
| 18 | + {"id": "U-004.5", "phase": "PH0", "sub_phase": "PH0-S2", "name": "RUN_REAL_AGENT_E2E 门控测试 harness(2026-07-30 新增)", "classification": "Enabler", "depends_on": ["U-004"], "coverage": {"model": [], "view": [], "controller": [], "process": [], "requirement": []}, "done": "有可复用的 skip_unless_env(\"RUN_REAL_AGENT_E2E\") 装饰器/基类;默认 unittest 跳过真实 claude 子进程调用;显式开启环境变量时至少一个真实往返用例可跑通"}, | ||
| 19 | + {"id": "U-005", "phase": "PH0", "sub_phase": "PH0-S2", "name": "c_org_node_trigger 子进程调用封装(骨架)", "classification": "Enabler", "depends_on": ["U-004", "U-004.5"], "coverage": {"model": [], "view": [], "controller": ["c_org_node_trigger"], "process": [], "requirement": []}, "done": "手工调用能触发一个 Org Agent 并拿到文本响应"}, | ||
| 20 | + {"id": "U-006", "phase": "PH0", "sub_phase": "PH0-S2", "name": "Work Graph 调用封装(复用 A-1 spike 代码)", "classification": "Enabler", "depends_on": ["U-004", "U-004.5"], "coverage": {"model": [], "view": [], "controller": ["c_work_graph_execute"], "process": [], "requirement": [], "assumption": ["A-1"]}, "done": "封装为可调用接口,单测通过,--permission-mode auto 生效"}, | ||
| 21 | + {"id": "U-007", "phase": "PH0", "sub_phase": "PH0-S3", "name": "本地后端骨架 + c_pick_folder macOS 实现", "classification": "Enabler", "depends_on": [], "coverage": {"model": [], "view": [], "controller": ["c_pick_folder"], "process": [], "requirement": []}, "done": "后端可启停;macOS 上原生对话框返回真实路径"}, | ||
| 22 | + {"id": "U-008", "phase": "PH0", "sub_phase": "PH0-S3", "name": "前端骨架", "classification": "Enabler", "depends_on": ["U-007"], "coverage": {"model": [], "view": [], "controller": [], "process": [], "requirement": []}, "done": "本地 web 应用路由跳转正常"}, | ||
| 23 | + | ||
| 24 | + {"id": "U-009", "phase": "PH1", "sub_phase": "PH1-S1", "name": "c_project_create + c_task_create + task 状态机", "classification": "Value Path", "depends_on": ["U-001", "U-002"], "coverage": {"model": ["task"], "view": [], "controller": ["c_task_create"], "process": ["p_org_task_lifecycle"], "requirement": ["REQ-01"]}, "done": "REQ-01 happy/boundary/failure 全部通过"}, | ||
| 25 | + {"id": "U-010", "phase": "PH1", "sub_phase": "PH1-S1", "name": "c_org_node_trigger 完整实现", "classification": "Value Path", "depends_on": ["U-005", "U-009"], "coverage": {"model": [], "view": [], "controller": ["c_org_node_trigger"], "process": [], "requirement": []}, "done": "三个 Org Agent 依次可被触发,task.status 按状态机正确流转"}, | ||
| 26 | + {"id": "U-011", "phase": "PH1", "sub_phase": "PH1-S2", "name": "c_work_graph_plan 实现", "classification": "Value Path", "depends_on": ["U-004", "U-010"], "coverage": {"model": ["artifact", "work_graph_run"], "view": [], "controller": ["c_work_graph_plan"], "process": [], "requirement": ["REQ-02"]}, "done": "REQ-02 happy 用例通过"}, | ||
| 27 | + {"id": "U-012", "phase": "PH1", "sub_phase": "PH1-S2", "name": "c_work_graph_execute 完整实现", "classification": "Value Path", "depends_on": ["U-006", "U-011"], "coverage": {"model": [], "view": [], "controller": ["c_work_graph_execute"], "process": ["p_work_graph_execution", "p_usage_limit_wait"], "requirement": ["REQ-07", "REQ-14"]}, "done": "REQ-07 全部通过;REQ-14 happy 用例通过"}, | ||
| 28 | + {"id": "U-013", "phase": "PH1", "sub_phase": "PH1-S2", "name": "c_work_node_run + git worktree 隔离", "classification": "Value Path", "depends_on": ["U-012"], "coverage": {"model": ["work_node_execution"], "view": [], "controller": ["c_work_node_run"], "process": [], "requirement": ["REQ-03"]}, "done": "REQ-03 happy/boundary 通过"}, | ||
| 29 | + {"id": "U-014", "phase": "PH1", "sub_phase": "PH1-S3", "name": "贪吃蛇场景端到端验收", "classification": "Value Path", "depends_on": ["U-011", "U-012", "U-013"], "coverage": {"model": [], "view": [], "controller": [], "process": [], "requirement": []}, "done": "架构文档 §11 验收标准第 1-3 条全部达成"}, | ||
| 30 | + | ||
| 31 | + {"id": "U-015", "phase": "PH2", "sub_phase": "PH2-S1", "name": "p_validator_root_cause_trace 实现", "classification": "Risk Gate", "depends_on": ["U-013"], "coverage": {"model": ["bug_record"], "view": [], "controller": ["c_bug_record_create"], "process": ["p_validator_root_cause_trace"], "requirement": ["REQ-04"]}, "done": "REQ-04 happy/boundary/failure 通过"}, | ||
| 32 | + {"id": "U-016", "phase": "PH2", "sub_phase": "PH2-S1", "name": "c_org_loop_route + fast_lane/full_replan", "classification": "Risk Gate", "depends_on": ["U-015"], "coverage": {"model": [], "view": [], "controller": ["c_org_loop_route"], "process": [], "requirement": ["REQ-05", "REQ-09"]}, "done": "REQ-05/REQ-09 全部通过"}, | ||
| 33 | + {"id": "U-017", "phase": "PH2", "sub_phase": "PH2-S1", "name": "人为植入 bug 完整闭环验证", "classification": "Risk Gate", "depends_on": ["U-016"], "coverage": {"model": [], "view": [], "controller": [], "process": [], "requirement": []}, "done": "架构文档 §11 验收标准第 4 条达成"}, | ||
| 34 | + {"id": "U-018", "phase": "PH2", "sub_phase": "PH2-S2", "name": "c_skill_update + c_skill_rollback", "classification": "Risk Gate", "depends_on": ["U-015"], "coverage": {"model": ["skill", "skill_iteration"], "view": [], "controller": ["c_skill_update", "c_skill_rollback"], "process": [], "requirement": ["REQ-08"]}, "done": "REQ-08 全部通过"}, | ||
| 35 | + {"id": "U-019", "phase": "PH2", "sub_phase": "PH2-S3", "name": "budget_guardrail_config + c_budget_check", "classification": "Risk Gate", "depends_on": ["U-012"], "coverage": {"model": ["budget_guardrail_config"], "view": [], "controller": ["c_budget_check"], "process": ["p_budget_guardrail"], "requirement": ["REQ-10"]}, "done": "REQ-10 happy/boundary 通过"}, | ||
| 36 | + {"id": "U-020", "phase": "PH2", "sub_phase": "PH2-S3", "name": "p_usage_limit_wait 完整验收", "classification": "Risk Gate", "depends_on": ["U-012"], "coverage": {"model": [], "view": [], "controller": [], "process": ["p_usage_limit_wait"], "requirement": ["REQ-14"]}, "done": "REQ-14 全部通过(含超时兜底)"}, | ||
| 37 | + {"id": "U-021", "phase": "PH2", "sub_phase": "PH2-S4", "name": "Graph Engine 持久化 + 断点恢复", "classification": "Risk Gate", "depends_on": ["U-012", "U-013"], "coverage": {"model": ["work_graph_run", "work_node_execution"], "view": [], "controller": [], "process": ["p_work_graph_execution"], "requirement": ["REQ-06"]}, "done": "REQ-06 happy/boundary/failure 通过"}, | ||
| 38 | + | ||
| 39 | + {"id": "U-022", "phase": "PH3", "sub_phase": "PH3-S1", "name": "v_project_setup + c_pick_folder 全平台", "classification": "Value Path", "depends_on": ["U-007", "U-008"], "coverage": {"model": [], "view": ["v_project_setup"], "controller": ["c_pick_folder"], "process": [], "requirement": ["REQ-11"]}, "done": "REQ-11 全部通过"}, | ||
| 40 | + {"id": "U-023", "phase": "PH3", "sub_phase": "PH3-S1", "name": "v_goal_composer + v_bug_feedback", "classification": "Value Path", "depends_on": ["U-022"], "coverage": {"model": [], "view": ["v_goal_composer", "v_bug_feedback"], "controller": ["c_bug_report_create"], "process": [], "requirement": []}, "done": "能通过 UI 创建 task 和人工 bug_record"}, | ||
| 41 | + {"id": "U-024", "phase": "PH3", "sub_phase": "PH3-S2", "name": "c_ws_subscribe + execution_event 实时推送(最小连通版)", "classification": "Value Path", "depends_on": ["U-012", "U-013"], "coverage": {"model": ["execution_event"], "view": [], "controller": ["c_ws_subscribe"], "process": [], "requirement": ["REQ-12"]}, "done": "推送链路本身连通,≤2 秒延迟达成;REQ-12 happy 用例通过(boundary/failure 留给 U-024.5)"}, | ||
| 42 | + {"id": "U-024.5", "phase": "PH3", "sub_phase": "PH3-S2", "name": "REQ-12 完整验收:事件多样性联调(2026-07-30 新增)", "classification": "Value Path", "depends_on": ["U-024", "U-015", "U-016", "U-018", "U-019", "U-020", "U-021"], "coverage": {"model": [], "view": [], "controller": [], "process": [], "requirement": ["REQ-12"]}, "done": "REQ-12 全部 happy/boundary/failure 用例通过;PH3 Phase 验收里的 REQ-12 以本 Unit 为准"}, | ||
| 43 | + {"id": "U-025", "phase": "PH3", "sub_phase": "PH3-S2", "name": "v_dashboard + v_task_board + v_task_detail", "classification": "Value Path", "depends_on": ["U-024"], "coverage": {"model": [], "view": ["v_dashboard", "v_task_board", "v_task_detail"], "controller": [], "process": [], "requirement": []}, "done": "三个页面可正常展示实时数据"}, | ||
| 44 | + {"id": "U-026", "phase": "PH3", "sub_phase": "PH3-S2", "name": "v_graph_visualizer", "classification": "Value Path", "depends_on": ["U-024"], "coverage": {"model": [], "view": ["v_graph_visualizer"], "controller": [], "process": [], "requirement": []}, "done": "Org/Work Graph 双层可视化含 waiting_for_quota 信息态"}, | ||
| 45 | + {"id": "U-027", "phase": "PH3", "sub_phase": "PH3-S3", "name": "execution_log_file + 日志查看器", "classification": "Value Path", "depends_on": ["U-024"], "coverage": {"model": ["execution_log_file", "log_retention_config"], "view": ["v_log_viewer"], "controller": ["c_execution_log_get", "c_ws_log_tail"], "process": [], "requirement": ["REQ-13"]}, "done": "REQ-13 全部通过"}, | ||
| 46 | + {"id": "U-028", "phase": "PH3", "sub_phase": "PH3-S3", "name": "v_artifact_browser", "classification": "Value Path", "depends_on": ["U-011"], "coverage": {"model": [], "view": ["v_artifact_browser"], "controller": [], "process": [], "requirement": []}, "done": "Artifact 列表+预览+血缘链可用"}, | ||
| 47 | + {"id": "U-029", "phase": "PH3", "sub_phase": "PH3-S3", "name": "v_skill_library + v_skill_audit_log", "classification": "Value Path", "depends_on": ["U-018"], "coverage": {"model": [], "view": ["v_skill_library", "v_skill_audit_log"], "controller": [], "process": [], "requirement": []}, "done": "Skill 库与变更审计日志可查看"}, | ||
| 48 | + {"id": "U-030", "phase": "PH3", "sub_phase": "PH3-S4", "name": "并发/预算/日志保留配置页", "classification": "Value Path", "depends_on": ["U-019", "U-021"], "coverage": {"model": ["org_concurrency_config"], "view": ["v_concurrency_settings", "v_budget_settings", "v_log_retention_settings"], "controller": ["c_concurrency_config_update", "c_budget_config_update", "c_log_retention_config_update"], "process": [], "requirement": []}, "done": "三个配置页可用且改动生效"}, | ||
| 49 | + | ||
| 50 | + {"id": "U-031", "phase": "PH4", "sub_phase": "PH4-S1", "name": "REQ-01~14 全量自动化回归", "classification": "Risk Gate", "depends_on": ["U-014", "U-017", "U-020", "U-021", "U-022", "U-023", "U-024.5", "U-025", "U-026", "U-027", "U-028", "U-029", "U-030"], "coverage": {"model": [], "view": [], "controller": [], "process": [], "requirement": ["REQ-01", "REQ-02", "REQ-03", "REQ-04", "REQ-05", "REQ-06", "REQ-07", "REQ-08", "REQ-09", "REQ-10", "REQ-11", "REQ-12", "REQ-13", "REQ-14"]}, "done": "06 节全部 14 条 REQ 的全部用例通过一次完整回归"}, | ||
| 51 | + {"id": "U-032", "phase": "PH4", "sub_phase": "PH4-S2", "name": "多 Goal 并发 + 长时间运行稳定性压测", "classification": "Risk Gate", "depends_on": ["U-031"], "coverage": {"model": ["org_concurrency_config"], "view": [], "controller": [], "process": [], "requirement": []}, "done": "并发数调高后多个独立 Goal 可同时稳定运行"}, | ||
| 52 | + {"id": "U-033", "phase": "PH4", "sub_phase": "PH4-S3", "name": "A-2 假设收口", "classification": "Risk Gate", "depends_on": ["U-004"], "coverage": {"model": [], "view": [], "controller": [], "process": [], "requirement": [], "assumption": ["A-2"]}, "done": "A-2 状态确认为 confirmed 或记录实际偏差"}, | ||
| 53 | + | ||
| 54 | + {"id": "U-034", "phase": "PH5", "sub_phase": "PH5-S1", "name": "01-M 扩展:deployment/deploy_health_signal + core_engine 枚举", "classification": "Enabler", "depends_on": ["U-001"], "coverage": {"model": ["deployment", "deploy_health_signal", "bug_record"], "view": [], "controller": [], "process": [], "requirement": []}, "done": "两张新表建表成功;bug_record 新枚举值向后兼容 U-015/U-018 已有测试"}, | ||
| 55 | + {"id": "U-035", "phase": "PH5", "sub_phase": "PH5-S1", "name": "self-target worktree 无条件强制 + core_engine 路由到蓝绿部署", "classification": "Risk Gate", "depends_on": ["U-013", "U-016", "U-034"], "coverage": {"model": [], "view": [], "controller": ["c_org_loop_route"], "process": [], "requirement": ["REQ-03"]}, "done": "self-target 时无条件走 worktree;core_engine 分类的 bug 被路由到 PH5-S2/S3 而非 fast_lane;且改动落地后必须重跑 U-013/REQ-03(worktree 隔离)现有回归用例全部通过(2026-07-30 计划评审追加,确认未破坏 PH4 已建立的隔离保证)"}, | ||
| 56 | + {"id": "U-036", "phase": "PH5", "sub_phase": "PH5-S2", "name": "supervisor/isolation 补齐单测 + Deploy Supervisor 进程骨架", "classification": "Enabler", "depends_on": [], "coverage": {"model": [], "view": [], "controller": [], "process": [], "requirement": []}, "done": "四模块各有 test_*.py;Deploy Supervisor 骨架独立进程可启停,不受 Live/Candidate 崩溃影响"}, | ||
| 57 | + {"id": "U-037", "phase": "PH5", "sub_phase": "PH5-S2", "name": "c_deploy_supervisor_stage", "classification": "Risk Gate", "depends_on": ["U-034", "U-036"], "coverage": {"model": ["deployment"], "view": [], "controller": ["c_deploy_supervisor_stage"], "process": [], "requirement": []}, "done": "能生成 deployment 记录并拉起监听临时端口、连一次性 DB 副本的候选进程"}, | ||
| 58 | + {"id": "U-038", "phase": "PH5", "sub_phase": "PH5-S2", "name": "c_deploy_supervisor_health_check", "classification": "Risk Gate", "depends_on": ["U-037"], "coverage": {"model": [], "view": [], "controller": ["c_deploy_supervisor_health_check"], "process": [], "requirement": []}, "done": "process_alive/http_health/canary_task 三项检查全过 -> health_check_passed;任一失败 -> health_check_failed 且 Live 未受影响"}, | ||
| 59 | + {"id": "U-039", "phase": "PH5", "sub_phase": "PH5-S2", "name": "c_deploy_supervisor_cutover", "classification": "Risk Gate", "depends_on": ["U-038"], "coverage": {"model": ["deployment"], "view": [], "controller": ["c_deploy_supervisor_cutover"], "process": [], "requirement": []}, "done": "健康检查通过后完成真实切换;在途 work_node_execution 重启后可恢复;previous_instance_dir 保留至监控窗口结束"}, | ||
| 60 | + {"id": "U-040", "phase": "PH5", "sub_phase": "PH5-S3", "name": "c_deploy_supervisor_monitor", "classification": "Risk Gate", "depends_on": ["U-039"], "coverage": {"model": ["deploy_health_signal"], "view": [], "controller": ["c_deploy_supervisor_monitor"], "process": [], "requirement": []}, "done": "四类信号采样正常;validator_approval_rate 偏离基线阈值时触发 anomaly_detected"}, | ||
| 61 | + {"id": "U-041", "phase": "PH5", "sub_phase": "PH5-S3", "name": "c_deploy_supervisor_rollback", "classification": "Risk Gate", "depends_on": ["U-038", "U-040"], "coverage": {"model": ["deployment"], "view": [], "controller": ["c_deploy_supervisor_rollback"], "process": [], "requirement": []}, "done": "回滚路径 A/B 各跑通一次;旧版本也起不来时正确终态到 blocked_pending_human;git revert 冲突有明确处理"}, | ||
| 62 | + {"id": "U-042", "phase": "PH5", "sub_phase": "PH5-S3", "name": "自举闭环端到端验收", "classification": "Risk Gate", "depends_on": ["U-035", "U-041"], "coverage": {"model": [], "view": [], "controller": [], "process": [], "requirement": []}, "done": "真实复现 self-development-feasibility.md 9 步场景,全程无需人工评审代码内容;真实双实例 E2E 用例默认跳过,需显式环境变量开启"}, | ||
| 63 | + {"id": "U-043", "phase": "PH5", "sub_phase": "PH5-S4", "name": "控制台展示 deployment 状态与健康信号", "classification": "Value Path", "depends_on": ["U-024", "U-040"], "coverage": {"model": [], "view": ["v_deployment_status"], "controller": [], "process": [], "requirement": []}, "done": "能看到当前 deployment 状态机所处状态与最近一次健康信号采样结果"} | ||
| 64 | + ], | ||
| 65 | + "classification_summary": { | ||
| 66 | + "Value Path": 17, | ||
| 67 | + "Risk Gate": 18, | ||
| 68 | + "Enabler": 10 | ||
| 69 | + }, | ||
| 70 | + "critical_path": ["U-001", "U-002", "U-004", "U-004.5", "U-005", "U-006", "U-010", "U-011", "U-012", "U-013", "U-014"], | ||
| 71 | + "ph5_critical_path": ["U-034", "U-035", "U-036", "U-037", "U-038", "U-039", "U-040", "U-041", "U-042"] | ||
| 72 | +} |
docs/self-development-feasibility.md
0 → 100644
| 1 | +++ a/docs/self-development-feasibility.md | ||
| 1 | +# graphlemon 自举可行性分析:能不能自己开发自己 | ||
| 2 | + | ||
| 3 | +**日期**: 2026-07-29(2026-07-30 更新:缺口 2/3 已完成详细设计,见更新记录) | ||
| 4 | +**性质**: 探索性分析,不是已确认的架构决策,未写入 `sdd-mvcp-contract.json`。若决定采纳,需要走一次正式的 AskUserQuestion 决策流程,再回填进 05-实施约束与依赖.md 和 contract.json。 | ||
| 5 | +**结论先说**:机制上大部分已经具备(三个 Org Agent 落在目标项目 `.claude/agents/*.md`、`c_project_create` 对 `target_folder_path` 没有排除平台自身路径、git worktree 隔离已经是既定决策),但有三个真缺口会让"自己改自己"从"能跑"变成"跑了但可能很危险"。下面先列缺口,再用一个具体场景走一遍,标出每一步是"✅ 已具备"还是"⚠️ 缺口"。 | ||
| 6 | + | ||
| 7 | +**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 的原始描述保留作为问题记录,其"推荐方案"段落已更新为指向这两份新文档。 | ||
| 8 | + | ||
| 9 | +## 1. 前提:自举不是从零开始的 | ||
| 10 | + | ||
| 11 | +自我迭代能力只有在 PH0/PH1(development-plan.md)已经由人类(配合 Claude Code 交互式开发,就是我们现在这个对话在做的事)跑通一次之后才谈得上——这不是缺口,是任何自举系统的必然前提,就像第一版 C 编译器不可能用 C 自己写。development-plan.md 的关键路径已经隐含了这一点。 | ||
| 12 | + | ||
| 13 | +## 2. 三个真缺口 | ||
| 14 | + | ||
| 15 | +### 缺口 1:worktree 隔离目前只在"并行"时强制,自我修改时必须无条件强制 | ||
| 16 | + | ||
| 17 | +03-C 3.6 节现有约束是"**并行**写代码的节点必须先申请 `git_worktree_ref`"。改别的 target project 时,单个 Coder 节点串行工作直接改主 checkout 问题不大。但如果 target project 就是 graphlemon 自己,正在跑这个任务的后端进程本身就在读取这份代码——直接改主 checkout 有实时干扰正在运行的调度器的风险。 | ||
| 18 | + | ||
| 19 | +**需要新增规则**:`target_folder_path == graphlemon 平台自身安装路径` 时,无论是否并行,一律强制 `git_worktree_ref`,且 Coder/Validator 的自测/复核全部在 worktree 副本里跑,不碰主 checkout。 | ||
| 20 | + | ||
| 21 | +### 缺口 2:没有"应用变更 + 生效"这一步——这是最大的缺口 | ||
| 22 | + | ||
| 23 | +Validator 在 worktree 里 PASS 之后,现有机制(git_worktree_ref 执行完成后清理并合并回主分支)能把代码合并进 graphlemon 自己仓库的主分支。**但合并到主分支的代码不会被正在运行的那个 Node.js/Python 进程自动重新加载**。需要重启才能生效,而"正在执行调度逻辑的那个进程"不可能自己发指令重启自己——发出重启指令的那一刻,进程就要被杀掉了。 | ||
| 24 | + | ||
| 25 | +当前 SDD 里没有任何 controller/process 定义这一步。这不是简单补个接口能解决的,需要一个新的架构组件。 | ||
| 26 | + | ||
| 27 | +**推荐方案(蓝绿实例,而不是原地重启)**:不在主进程里做"重启自己",而是让 Validator 直接对着 worktree 里跑起来的一个**独立的第二份 graphlemon 实例**(换端口、换一份临时/只读数据库)做验证;通过后由一个**在 Graph Engine 控制范围之外**的独立 **Deploy Supervisor**(与架构文档里 Coder Work Graph 内部的"Supervisor(调度子 Agent)"是两个不同概念,注意区分)执行切换:停旧实例 → merge 分支到主分支 → 起新实例。这样"运行中的自己"永远不需要重启自己。 | ||
| 28 | + | ||
| 29 | +**(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`。 | ||
| 30 | + | ||
| 31 | +### 缺口 3:核心代码/身份文件的自我修改没有对应缺口 3.5 节 `org_graph_definition` 那样的强制人工评审 | ||
| 32 | + | ||
| 33 | +05 节 §5.6 已经规定 `org_graph_definition`(Org Graph 结构 YAML)的变更必须走人工 PR 评审,不允许系统自主提交;`skill`(Work Graph 技能定义)允许系统自主迭代但要求 Git 可回滚。但这两条规则都没覆盖: | ||
| 34 | + | ||
| 35 | +1. **`.claude/agents/{planner,coder,validator}.md`**(Org Agent 身份文件,对应 SOUL.md)——如果 Coder 被要求"优化一下 Validator 的判断逻辑",改的是判定裁决者本身的人设,一旦改坏(比如让它变得更宽松、更容易判 PASS),系统会在没有外部校验的情况下自我欺骗,而且没人会发现,因为发现问题的机制本身已经被改坏了。 | ||
| 36 | +2. **调度器/执行器抽象层等核心引擎代码**——和改一个 UI 组件的风险完全不是一个量级。 | ||
| 37 | + | ||
| 38 | +**原提案(已被用户否决,保留作记录)**:曾建议 `.claude/agents/*.md` 的变更、以及触碰 Graph Engine 调度器/执行器抽象层核心逻辑的变更一律强制 `blocked_pending_human`,走和 `org_graph_definition` 一样的人工评审通道。 | ||
| 39 | + | ||
| 40 | +**用户更正(2026-07-30,原话)**:"改坏了的 validator 没有人工评审倒不是问题,坏了由 supervisor 能够监督回滚即可。所以核心是需要一个三方的 agent 来作为补充 supervisor 接口。" —— 即:不要求核心文件改动必须先经人工评审才能生效,而是允许系统自主提交,由缺口 2 的同一个 Supervisor 在部署后持续监控行为信号(例如 Validator 的 approve 率是否偏离历史基线),一旦检测到异常自动回滚,不需要人在"改之前"把关。 | ||
| 41 | + | ||
| 42 | +**(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 节),而不是"代码判断谁对谁错"。 | ||
| 43 | + | ||
| 44 | +`bug_record.root_cause_category` 新增 `core_engine` 分类的建议仍然保留,但用途从"触发强制人工评审"改为"触发时跳过 `fast_lane`、走完整的蓝绿部署+监控流程"(普通 Skill 级别的自主迭代可能不需要这么重的验证)。 | ||
| 45 | + | ||
| 46 | +同理,如果这次自我修改涉及 01-M 自身的数据库 schema(比如给 `work_node_execution` 加字段),05 §5.6 现有的"允许停机迁移"假设是给人工运维用的,没覆盖"系统自主决定给自己数据库做 schema 变更"——这类改动同样纳入蓝绿部署+监控流程,而不是强制转人工,具体的 schema 迁移在蓝绿切换中的时序(迁移脚本什么时候跑、跑坏了怎么回滚)是 `supervisor-auto-rollback-design.md` 未覆盖的开放问题,需要后续补充。 | ||
| 47 | + | ||
| 48 | +## 3. 模拟走一遍:修复自己状态机里的一个边界 bug | ||
| 49 | + | ||
| 50 | +**场景**:人类发现 graphlemon 自己的 `p_org_task_lifecycle` 状态机实现里,`loop_round >= 3` 转 `blocked_pending_human` 的判断写成了 `loop_round > 3`(off-by-one,第 3 轮本该拦截却继续跑了)。这段逻辑就在 graphlemon 自己的调度器代码里——一个很好的自举测试用例,因为它既真实又刚好踩中"核心引擎代码"这条线。 | ||
| 51 | + | ||
| 52 | +| 步骤 | 操作 | 状态 | | ||
| 53 | +|---|---|---| | ||
| 54 | +| 1. 项目登记 | `c_pick_folder` 选中 graphlemon 自己的仓库路径,`c_project_create` 校验 `has_git_repo=true`(它自己确实是),创建 `.graphlemon/` 子目录 | ✅ 已具备,架构对 `target_folder_path` 没有排除平台自身路径 | | ||
| 55 | +| 2. 提出目标 | `c_task_create({goal_text: "修复 p_org_task_lifecycle 里 loop_round 边界判断的 off-by-one", project_id: <self>})` | ✅ 已具备 | | ||
| 56 | +| 3. Planner 触发 | `c_org_node_trigger(planner)`,Planner 读取的正是 graphlemon 自己的 04-P 文档 + 对应源码 | ⚠️ 轻微缺口:Planner 的 skill 是按"从 PRD 写新 SDD"设计的,这次是"改一处已有实现",`c_work_graph_plan` 目前没有区分这两种 `graph_manifest` 生成路径。大概率能工作(Planner 有能力判断这是小范围修复),但没有显式设计,建议补一条轻量修复路径的规则 | | ||
| 57 | +| 4. Coder 节点执行 | `c_work_node_run` 触发 Coder,按缺口 1 的新规则强制走 `git_worktree_ref`,在隔离副本里改代码、跑单测 | ⚠️ 命中缺口 1(需要新规则:self-target 场景无条件强制 worktree) | | ||
| 58 | +| 5. Validator 复核 | Validator 在 worktree 副本里复现原 bug 场景,确认修复且无回归 | ⚠️→✅ 命中缺口 3:这次改动是调度器核心代码,`root_cause_category` 应标记 `core_engine`,触发的是完整蓝绿部署+监控流程(而不是强制转人工),机制已设计(`supervisor-auto-rollback-design.md`) | | ||
| 59 | +| 6. 合并到主分支 | worktree 分支 merge 回 graphlemon 自己仓库的主分支 | ✅ 机制已具备(07 节"执行完成后清理 worktree 并合并回主分支") | | ||
| 60 | +| 7. 生效 | 需要让正在运行的后端进程加载新代码 | ⚠️→✅ 命中缺口 2(最大的缺口):需要蓝绿实例 + 独立于 Graph Engine 之外的 Deploy Supervisor 来完成切换,完整状态机/健康检查/回滚流程已设计并回填 SDD(`supervisor-auto-rollback-design.md`),尚未实现代码 | | ||
| 61 | +| 8. 数据库 schema | 本例的判断逻辑修复不涉及 01-M schema 变更,不触发 | (本例未命中,但换个例子——比如给 `work_node_execution` 加字段——会命中缺口 3 的 schema 部分,`supervisor-auto-rollback-design.md` 第 10 节已列为未覆盖的开放问题) | | ||
| 62 | +| 9. 完成 | 蓝绿切换完成(Deploy Supervisor 健康检查通过后自动完成,无需人工确认)后 `task.status=done` | 走到这一步,一次"系统被告知自己的缺陷 → 自己诊断根因 → 自己修复 → 自己验证 → 自动部署并被 Deploy Supervisor 持续监控(异常自动回滚)"的完整闭环就跑通了,且全程不需要人工介入代码内容评审 | | ||
| 63 | + | ||
| 64 | +## 4. 小结 | ||
| 65 | + | ||
| 66 | +9 步里 3 步直接可用,1 步大概率可用但没显式设计,剩下 5 步(含最关键的"合并后如何生效")命中三个真缺口。**2026-07-30 更新**:缺口 2(蓝绿实例生效)和缺口 3(核心文件自我修改的治理,按用户更正为"自动回滚而非强制人工评审")已完成详细设计,分别落在 `docs/supervisor-auto-rollback-design.md`(状态机/健康检查/回滚流程/MVP 参数)和 `docs/supervisor-design-comparison.md`(方案选型依据)。缺口 1(worktree 无条件强制)仍是待写入 05 节的一条治理规则,成本低。 | ||
| 67 | + | ||
| 68 | +如果要推进,建议顺序:先把缺口 1(一条治理规则)写进 05 节和 contract.json,成本低、马上能落地;缺口 2/3 的设计已经具备可实施粒度,可以作为 PH4 之后的独立 Phase(PH5:自举能力)排入 `development-plan.md`,不影响当前 PH0-PH4 主线——具体是现在就把 PH5 的 Unit 分解也做出来,还是先确认设计稿本身没问题再排,由用户决定。 |
docs/supervisor-auto-rollback-design.md
0 → 100644
| 1 | +++ a/docs/supervisor-auto-rollback-design.md | ||
| 1 | +# Deploy Supervisor 蓝绿实例自动回滚机制设计 | ||
| 2 | + | ||
| 3 | +**日期**: 2026-07-30(同日更新:完成 Supervisor 概念改名消歧,见下方命名说明) | ||
| 4 | +**性质**: 详细设计,回应用户"自动回滚机制现在就设计一下,省得到 PH5 阶段有那个阶段的问题"。落地 `self-development-feasibility.md` 缺口 2(应用变更+生效)与缺口 3(用户更正版:不要求人工评审,要求 supervisor 自动监督回滚)。 | ||
| 5 | +**依赖**: `docs/supervisor-design-comparison.md`(方案选型结论:复用 graphloop 隔离/manifest/apply 库,见 `supervisor/isolation/`;Deploy Supervisor 自身决策逻辑必须是确定性代码,不接入 LLM)。 | ||
| 6 | +**状态**: 设计已回填进正式 SDD(`01-M`/`03-C`/`04-P`/`05`/`06`/`sdd-mvcp-contract.json`),代码尚未实现(`supervisor/isolation/` 除外,那是通用库不是本机制专属)。 | ||
| 7 | + | ||
| 8 | +**命名说明(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 任务存在,一个是常驻的独立进程)。 | ||
| 9 | + | ||
| 10 | +## 1. 触发范围 | ||
| 11 | + | ||
| 12 | +本机制**只在** `target_folder_path == graphlemon 平台自身安装路径`(self-target 场景,`self-development-feasibility.md` 缺口 1 已定义的判断条件)且 Validator 对该次修改判定 PASS 之后触发。改别的 target project 时,走既有的"worktree 合并回主分支"流程即可,不涉及蓝绿实例——因为改的代码不是正在跑的这个进程本身。 | ||
| 13 | + | ||
| 14 | +触发时点:Validator PASS → worktree 分支 merge 回 graphlemon 自身仓库主分支(既有机制,`self-development-feasibility.md` 步骤 6)→ **Deploy Supervisor 从这里接管**,直到新代码安全生效或安全回滚为止。 | ||
| 15 | + | ||
| 16 | +## 2. 组件与进程边界 | ||
| 17 | + | ||
| 18 | +三个独立操作系统进程,任何一个崩溃不影响另外两个: | ||
| 19 | + | ||
| 20 | +1. **Live 实例**:当前正在服务的 graphlemon 后端(Graph Engine + 控制台 API),监听固定端口 `LIVE_PORT`,使用固定路径的生产 SQLite 库 `PROD_DB_PATH`。 | ||
| 21 | +2. **Candidate 实例**:本次自我修改合并后的新版本,Deploy Supervisor 拉起来做验证,监听临时端口,使用**一次性 DB 副本**,与生产 DB 完全隔离,验证期间无论 Candidate 干什么都不影响 Live。 | ||
| 22 | +3. **Deploy Supervisor 进程**:独立于以上两者的确定性 Python 脚本(stdlib only,无 LLM 调用),是本设计的主体。职责:拉起/健康检查/切换/监控/回滚 Candidate 与 Live,自身不做任何"业务判断"(比如不判断代码写得好不好),只做"这几个信号是否符合预设阈值"这种算术判断。 | ||
| 23 | + | ||
| 24 | +Deploy Supervisor 与 Live/Candidate 之间没有共享内存/共享失败域——这正是用户"第三方 agent"要求的字面落地:Deploy Supervisor 挂了不会拖死 Live,Live 挂了 Deploy Supervisor 还能观察到并处理。 | ||
| 25 | + | ||
| 26 | +## 3. 数据模型(已回填 `01-M-模型设计.md`) | ||
| 27 | + | ||
| 28 | +两个新实体: | ||
| 29 | + | ||
| 30 | +### `deployment` | ||
| 31 | + | ||
| 32 | +| 字段 | 类型 | 说明 | | ||
| 33 | +|---|---|---| | ||
| 34 | +| `deployment_id` | string, PK | | | ||
| 35 | +| `git_commit_hash` | string | 本次候选版本对应的 commit | | ||
| 36 | +| `previous_commit_hash` | string | 回滚目标(上一个 `status=finalized` 的 deployment 的 `git_commit_hash`) | | ||
| 37 | +| `status` | enum | 见第 5 节状态机 | | ||
| 38 | +| `candidate_port` | int, nullable | 健康检查阶段临时端口 | | ||
| 39 | +| `candidate_db_path` | string, nullable | 一次性 DB 副本路径,验证结束后清理 | | ||
| 40 | +| `started_at` / `cutover_at` / `monitoring_closed_at` / `rollback_at` | timestamp, nullable | | | ||
| 41 | +| `rollback_reason` | string, nullable | 见第 6 节信号类型 | | ||
| 42 | +| `previous_instance_dir` | string | 保留的上一个已知良好版本 checkout 目录,直到本次 deployment 监控窗口正常关闭才允许清理 | | ||
| 43 | + | ||
| 44 | +### `deploy_health_signal` | ||
| 45 | + | ||
| 46 | +| 字段 | 类型 | 说明 | | ||
| 47 | +|---|---|---| | ||
| 48 | +| `signal_id` | string, PK | | | ||
| 49 | +| `deployment_id` | string, FK | | | ||
| 50 | +| `sampled_at` | timestamp | | | ||
| 51 | +| `signal_type` | enum: `process_alive` \| `http_health` \| `canary_task` \| `validator_approval_rate` | | | ||
| 52 | +| `status` | enum: `ok` \| `degraded` \| `fail` | | | ||
| 53 | +| `detail` | string | 供人工事后排查,不参与自动判断 | | ||
| 54 | + | ||
| 55 | +## 4. Cutover 机制(部署新版本) | ||
| 56 | + | ||
| 57 | +1. Deploy Supervisor 拉取合并后的主分支到全新目录(不是原地 `git pull`,避免影响可能仍在读取源码的 Live 进程),生成 `deployment` 记录,`status=staged`。 | ||
| 58 | +2. 复制 `PROD_DB_PATH` 到 `candidate_db_path`(一次性副本,用完即删,绝不让 Candidate 直接碰生产库)。 | ||
| 59 | +3. 用 `supervisor/isolation/isolated_workspace.isolate_workspace()` 家族同款隔离原则拉起 Candidate 进程:`subprocess.Popen`,`shell=False`,固定 argv 形态,监听 `candidate_port`(生产端口 +1,或探测一个空闲端口),指向 `candidate_db_path`。`status=candidate_starting`。 | ||
| 60 | +4. 跑健康检查(见第 6 节),`status=health_check_running`。 | ||
| 61 | + - 全部通过 → `status=health_check_passed`,进入 5。 | ||
| 62 | + - 任一失败/超时 → `status=health_check_failed` → 走**回滚路径 A**(第 7.1 节)。 | ||
| 63 | +5. **Cutover 本体**(本地单用户桌面应用,不需要处理并发外部流量/负载均衡,这一条假设需要用户确认,见第 9 节开放问题): | ||
| 64 | + - Live 停止接受新的 Org Node 触发(不接受新 `c_org_node_trigger` 调用),但允许已在执行的 `work_node_execution` 走到可安全中断的检查点(已有的持久化恢复机制保证中断后可恢复,不强求"优雅排空")。 | ||
| 65 | + - Live 进程退出。 | ||
| 66 | + - Candidate 关闭一次性 DB 副本连接,改连 `PROD_DB_PATH`,改绑 `LIVE_PORT`。 | ||
| 67 | + - Candidate 成为新的 Live。`status=live`,`cutover_at=now`。 | ||
| 68 | + - 原来的 Live checkout 目录保留为 `previous_instance_dir`,**不删除**,直到监控窗口正常结束。 | ||
| 69 | + | ||
| 70 | +## 5. 完整状态机 | ||
| 71 | + | ||
| 72 | +``` | ||
| 73 | +staged | ||
| 74 | + -> candidate_starting | ||
| 75 | + -> health_check_running | ||
| 76 | + -> health_check_failed [回滚路径 A:见 7.1,不影响 Live,直接结束] | ||
| 77 | + -> health_check_passed | ||
| 78 | + -> cutover_in_progress | ||
| 79 | + -> live | ||
| 80 | + -> monitoring (进入监控窗口) | ||
| 81 | + -> anomaly_detected -> rollback_in_progress | ||
| 82 | + -> rolled_back [回滚路径 B:见 7.2,Live 换回旧版本] | ||
| 83 | + -> rollback_failed -> blocked_pending_human [兜底,见 7.3] | ||
| 84 | + -> monitoring_window_clean -> finalized (旧 previous_instance_dir 可清理) | ||
| 85 | +``` | ||
| 86 | + | ||
| 87 | +`finalized` 是唯一的"安全终态";`blocked_pending_human` 是唯一需要人工介入的终态,且触发条件严格限定为"自动回滚机制本身失败",不是"代码写得不好"——这与用户对缺口 3 的更正(不要求人工评审代码内容)不冲突,是不同层面的安全网。 | ||
| 88 | + | ||
| 89 | +## 6. 健康检查 / 监控信号(全部确定性,不接入任何 LLM 判断) | ||
| 90 | + | ||
| 91 | +### Pre-cutover 健康检查(`health_check_running`,有界超时,全部通过才能 cutover) | ||
| 92 | + | ||
| 93 | +| 检查项 | 方法 | 判定 | | ||
| 94 | +|---|---|---| | ||
| 95 | +| `process_alive` | 启动后轮询 `os.kill(pid, 0)`,超时 `HEALTH_CHECK_STARTUP_TIMEOUT` | 超时未存活 = fail | | ||
| 96 | +| `http_health` | 请求 Candidate 的 `/api/health`,超时 `HEALTH_CHECK_HTTP_TIMEOUT` | 非 200 或超时 = fail | | ||
| 97 | +| `canary_task` | 对 Candidate 跑一个固定的、结果可预期的最小任务(例如创建一个临时 project + 一个已知会通过的 trivial task,断言最终 `task.status` 与预期字段一致),超时 `HEALTH_CHECK_CANARY_TIMEOUT` | 结果不符/超时/异常 = fail | | ||
| 98 | + | ||
| 99 | +任一 fail 立即进入回滚路径 A,不重试(重试属于"给坏版本第二次机会",不符合 fail-closed 原则;要重试整个自我修改,走 Planner 重新出一版)。 | ||
| 100 | + | ||
| 101 | +### Post-cutover 监控(`monitoring`,窗口内持续采样) | ||
| 102 | + | ||
| 103 | +| 信号 | 方法 | 判定阈值(MVP 固定值,不做用户可配) | | ||
| 104 | +|---|---|---| | ||
| 105 | +| `process_alive` | 每 `MONITOR_SAMPLE_INTERVAL` 探活 | 连续崩溃/重启 ≥ `MAX_CRASH_COUNT` 次 / `CRASH_WINDOW` 内 = anomaly | | ||
| 106 | +| `http_health` | 同上频率请求 `/api/health` | 连续 `MAX_CONSECUTIVE_HTTP_FAIL` 次失败 = anomaly | | ||
| 107 | +| `canary_task` | 每 `CANARY_INTERVAL` 跑一次固定 canary 任务 | 单次失败即 anomaly(canary 本身就是"已知应该过"的任务,失败没有灰色地带) | | ||
| 108 | +| `validator_approval_rate` | 从 `execution_event`/`work_node_execution` 表按滚动窗口统计 Validator 节点 `approved` 占比,与 deployment 之前的历史基线比较 | 偏离基线超过 `VALIDATOR_RATE_DEVIATION_THRESHOLD`(绝对值,如 ±30 个百分点)= anomaly。**这一条直接对应用户点名的场景**:"改坏了的 validator"——如果新版本的 Validator 变得异常宽松(approve 率骤升)或异常严格(approve 率骤降到几乎全部拒绝),都会被这条信号捕获,且判断是纯算术,不需要另一个 LLM 来"评价 Validator 判得对不对" | | ||
| 109 | + | ||
| 110 | +监控窗口:`MONITORING_WINDOW_HOURS`(固定值,如 24h)或 `MONITORING_MIN_TASK_COUNT`(固定值,如 10 个真实任务)先到者为准,之后无 anomaly 则 `monitoring_window_clean → finalized`。 | ||
| 111 | + | ||
| 112 | +## 7. 回滚路径 | ||
| 113 | + | ||
| 114 | +### 7.1 路径 A:Pre-cutover 回滚(健康检查未过) | ||
| 115 | + | ||
| 116 | +Live 从未被动过,回滚成本最低: | ||
| 117 | + | ||
| 118 | +1. 终止 Candidate 进程(`SIGTERM`,参考 `hermes_watchdog.py` 的终止哲学)。 | ||
| 119 | +2. 删除 `candidate_db_path` 一次性副本。 | ||
| 120 | +3. `git revert` 本次合并 commit(不是 `reset`/force-push,保留历史可追溯,这一步复用 `workspace_security.validate_subprocess_command()` 的 argv 校验)。 | ||
| 121 | +4. 发一个 `bug_record`,`root_cause_category=core_engine`,把健康检查失败的具体信号/日志作为 evidence 挂上去,供下一轮 Planner/Coder 重新尝试时参考。 | ||
| 122 | +5. `status=health_check_failed`,记录结束,Live 全程未受影响,调度立即可以继续处理下一个任务。 | ||
| 123 | + | ||
| 124 | +### 7.2 路径 B:Post-cutover 回滚(监控期间检测到 anomaly) | ||
| 125 | + | ||
| 126 | +Live 已经是 Candidate,需要真正"换回去": | ||
| 127 | + | ||
| 128 | +1. 记录触发回滚的信号快照(哪个 signal_type、哪次采样、什么值)到 `deployment.rollback_reason`。 | ||
| 129 | +2. 当前 Live(即出问题的新版本)停止接受新触发,同样允许在途节点走到可恢复检查点。 | ||
| 130 | +3. 用 `previous_instance_dir`(第 4 步保留的上一版本 checkout)重新拉起进程,绑定 `LIVE_PORT`,连接 `PROD_DB_PATH`(生产库这次全程没有被换过,只是换了跑在它前面的代码进程,数据不受影响——这是 blue-green 相对"原地改代码"最大的安全收益)。 | ||
| 131 | +4. 对"复活的旧版本"跑一遍 7.1 同款的三项健康检查,确认旧版本本身没问题(正常情况下必过,因为它是上一次已经 `finalized` 的版本)。 | ||
| 132 | +5. 通过 → `status=rolled_back`,当前 Live = 旧版本,本次自我修改的 commit 在主分支上被 `git revert`,发 `bug_record`(同路径 A 第 4 步,evidence 换成监控期间的异常信号),调度恢复正常。 | ||
| 133 | +6. 不通过(极端情况:连旧版本都起不来,比如磁盘满了这类环境问题)→ 第 7.3 节。 | ||
| 134 | + | ||
| 135 | +### 7.3 兜底:回滚本身失败 | ||
| 136 | + | ||
| 137 | +`status=rollback_failed → blocked_pending_human`。这是本设计里唯一保留的人工介入点,且明确限定触发条件仅为"自动恢复机制本身失效"(旧版本也起不来、DB 不可写这类基础设施问题),不是"代码判断谁对谁错"。控制台需要能清楚展示这个区别,避免用户以为又回到了"需要人工评审代码改得好不好"的老路。 | ||
| 138 | + | ||
| 139 | +## 8. 与既有 SDD 的对接点(2026-07-30 已回填,见第二次更新记录) | ||
| 140 | + | ||
| 141 | +- `01-M-模型设计.md`:新增 `deployment`、`deploy_health_signal` 两个实体(第 3 节)。**已完成。** | ||
| 142 | +- `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` 的路由分支。**已完成。** | ||
| 143 | +- `04-P-流程与状态流转.md`:新增 `p_deploy_supervisor_blue_green` 流程,状态转移表照搬第 5 节。**已完成。** | ||
| 144 | +- `05-实施约束与依赖.md`:新增本机制的 MVP 固定参数表(第 9 节),比照现有 `p_usage_limit_wait` 参数(5→10→20→30min backoff、8h/9d 超时)"MVP 阶段硬编码、不做用户可配"的先例,登记同一张表里;新增 self-target worktree 无条件强制规则。**已完成。** | ||
| 145 | +- `06-验收与测试锚点.md`:新增 REQ-15,覆盖:健康检查全过正常 cutover、健康检查失败路径 A、监控期 anomaly 触发路径 B、回滚本身失败兜底到 `blocked_pending_human` 四类 happy/failure 用例。**已完成。** | ||
| 146 | +- `sdd-mvcp-contract.json`:同步以上四类变更,重新跑 `jsonschema.validate()`。**已完成。** | ||
| 147 | +- `self-development-feasibility.md`:缺口 2/3 的"推荐方案"段落指向本文档。**已完成(2026-07-30 更新记录)。** | ||
| 148 | + | ||
| 149 | +## 9. MVP 固定参数(草案值,需要在实现前用真实测量校准,参考 A-1 的验证方法论) | ||
| 150 | + | ||
| 151 | +| 参数 | 草案值 | 说明 | | ||
| 152 | +|---|---|---| | ||
| 153 | +| `HEALTH_CHECK_STARTUP_TIMEOUT` | 30s | Candidate 进程从拉起到能响应探活 | | ||
| 154 | +| `HEALTH_CHECK_HTTP_TIMEOUT` | 10s | 单次 `/api/health` 请求 | | ||
| 155 | +| `HEALTH_CHECK_CANARY_TIMEOUT` | 120s | 一次最小 canary 任务从提交到出结果 | | ||
| 156 | +| `MONITOR_SAMPLE_INTERVAL` | 5min | process/http 采样间隔 | | ||
| 157 | +| `CANARY_INTERVAL` | 1h | 监控期 canary 任务运行间隔 | | ||
| 158 | +| `MAX_CRASH_COUNT` / `CRASH_WINDOW` | 2 次 / 10min | 判定进程反复崩溃 | | ||
| 159 | +| `MAX_CONSECUTIVE_HTTP_FAIL` | 3 次 | 判定健康端点持续不可用 | | ||
| 160 | +| `VALIDATOR_RATE_DEVIATION_THRESHOLD` | ±30 个百分点 | 判定 Validator 行为异常 | | ||
| 161 | +| `MONITORING_WINDOW_HOURS` | 24h | 监控窗口时长上限 | | ||
| 162 | +| `MONITORING_MIN_TASK_COUNT` | 10 个真实任务 | 监控窗口任务数上限,先到者结束 | | ||
| 163 | + | ||
| 164 | +以上数值全部是工程经验草案,不是实测结果——和 `05-实施约束与依赖.md` §5.4.2 现有的额度等待退避参数一样,需要在真正实现并跑几轮真实自我修改后重新校准,不建议直接当最终值写进 contract.json 的强约束里(建议标注为"可调,需实测校准"而不是"硬编码不可变",这一点和额度等待参数刻意标注为"MVP-fixed"略有不同,因为额度等待的时间线是 Anthropic 平台文档定义的外部事实,而这里的阈值是纯粹的工程经验值)。 | ||
| 165 | + | ||
| 166 | +## 10. 未覆盖 / 开放问题(留给实现阶段决定,不阻塞本设计定稿) | ||
| 167 | + | ||
| 168 | +1. **单用户桌面场景假设**:第 4 节 cutover 假设不需要处理并发外部流量/连接排空,只有 Cowork 控制台这一个使用者,断线可重连。如果未来 graphlemon 要支持多用户/多终端同时连接,这里的"直接切端口"简化模型需要重新评估。 | ||
| 169 | +2. **`candidate_db_path` 一次性副本的体积/耗时**:如果生产 DB 随着长期使用变得很大,每次 cutover 前整库复制可能变慢,需要的话可以后续改成"复制 schema + 近期数据子集"而不是整库,MVP 阶段先用整库复制,简单可靠优先。 | ||
| 170 | +3. **`git revert` 冲突**:如果本次自我修改之后主分支又有新的(比如另一个并发自我修改任务的)提交,revert 可能冲突,需要 Deploy Supervisor 能识别 revert 失败并转 `blocked_pending_human`(不在当前状态机图里显式画出,应该并入 7.3 兜底路径,而不是新开一个分支——这样能保持状态机简单)。 |
docs/supervisor-design-comparison.md
0 → 100644
| 1 | +++ a/docs/supervisor-design-comparison.md | ||
| 1 | +# Deploy Supervisor 设计方案对比:自建 vs 复用 Hermes CLI | ||
| 2 | + | ||
| 3 | +**日期**: 2026-07-30(同日更新:完成命名消歧,见下方说明;结论已回填正式 SDD) | ||
| 4 | +**性质**: 方案选型分析,回应用户 "都不确定,先看两种方案的详细对比",已回填 SDD/contract.json(见 `supervisor-auto-rollback-design.md` §8)。 | ||
| 5 | +**输入**: 对 `graphloop`(同一项目前身,`/Users/yibo/Documents/lemonAI/graphloop`)的实地代码/任务记录调查。 | ||
| 6 | + | ||
| 7 | +**命名说明**:本文档讨论的"Deploy Supervisor"是独立于 Graph Engine 之外、负责 graphlemon 自我修改后蓝绿部署与自动回滚的进程,和架构文档里已有的"Supervisor(调度子 Agent,Coder Work Graph 内部监控节点)"是两个不同的概念,详见 `supervisor-auto-rollback-design.md` 开头的命名说明。本文档下文提到"supervisor"泛指这个待选型的组件本身,涉及 graphloop 自己代码里的 `hermes_watchdog.py`/`phase10_controller.py` 时沿用 graphloop 原始命名,不额外改名。 | ||
| 8 | + | ||
| 9 | +## 0. 结论先说 | ||
| 10 | + | ||
| 11 | +问题不是"自建 vs 用 Hermes CLI 当 Deploy Supervisor 的大脑"这个二选一——graphloop 自己的实现已经给出了答案,而且答案跟两个选项字面意思都不完全一样: | ||
| 12 | + | ||
| 13 | +- graphloop 的 supervisor 层(`hermes_watchdog.py` + `phase10_controller.py`)**自身的监督/裁决逻辑是纯 stdlib 确定性 Python,完全不调用任何 LLM**(Hermes 或 Claude 都不调)。LLM 只用来执行"被监督的那份工作",监督决策(存活判断、是否 blocked、evidence 是否齐全)是 if/else,不是 prompt。 | ||
| 14 | +- 但 graphloop 的监督哲学是 **"fail-closed 后停下等人工"**,不是用户要的 **"自动回滚后继续跑"**。这是个真实的哲学缺口,两个候选方案都没有现成覆盖。 | ||
| 15 | +- graphloop 已经有一套经过真实运行验证(含真实失败案例)的隔离/manifest/选择性写回机制,直接复用价值很高,且这部分代码同样是纯 stdlib、和"用不用 Hermes CLI"这个问题正交。 | ||
| 16 | + | ||
| 17 | +所以推荐路径不是"选 A 还是选 B",而是"复用 C(graphloop 的隔离/manifest/apply 库)+ 在其之上新写一个自动回滚层",详见第 4 节。 | ||
| 18 | + | ||
| 19 | +## 1. graphloop 里跟 supervisor 直接相关的既有实现 | ||
| 20 | + | ||
| 21 | +### 1.1 `hermes_watchdog.py`(109 行,已读全文) | ||
| 22 | + | ||
| 23 | +纯 stdlib,无 LLM 调用。核心逻辑: | ||
| 24 | + | ||
| 25 | +``` | ||
| 26 | +inspect(state_dir, stall_seconds=600.0, action="report"|"terminate"): | ||
| 27 | + 扫描 state_dir 下 status=="running" 的 *.json 状态文件 | ||
| 28 | + 用 last_output_at/updated_at 或日志 mtime 算 silent_for | ||
| 29 | + if stalled and action=="terminate": | ||
| 30 | + os.kill(pid, SIGTERM) | ||
| 31 | + 原子写 {"status": "blocked", "blocked_reason": "worker dead or silent beyond watchdog threshold"} | ||
| 32 | +``` | ||
| 33 | + | ||
| 34 | +模块 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 → 停下等人",不是"发现异常 → 自动回滚 → 继续跑"。 | ||
| 35 | + | ||
| 36 | +### 1.2 `phase10_controller.py`(自我演进调度器,已读核心结构) | ||
| 37 | + | ||
| 38 | +同样纯 stdlib,无 LLM 参与调度决策本身: | ||
| 39 | + | ||
| 40 | +``` | ||
| 41 | +main(): | ||
| 42 | + acquire_tick_lock() # 单飞锁,防止并发 tick | ||
| 43 | + 读 progress.json 找 current_task | ||
| 44 | + if 上一个任务的 worker PID 不存活: mark_stale() → blocked_stale,停 | ||
| 45 | + if task_id 在 blocked_steps 或历史记录里是 failed/blocked: blocked_task,停 | ||
| 46 | + completion_is_admissible(): 校验 evidence dict 是否包含必需 key(specialized_test/py_compile/full_regression), | ||
| 47 | + blockers 字段非空则直接判不可采信 | ||
| 48 | + 只有 evidence 完整且无 blocker 才推进 phase,否则原地 blocked,绝不自动重试/自动回滚 | ||
| 49 | +``` | ||
| 50 | + | ||
| 51 | +调度器本身只负责"要不要放行这一次 tick",真正干活的是它 shell 出去的 `hermes chat --yolo ...`。 | ||
| 52 | + | ||
| 53 | +### 1.3 隔离 / manifest / 选择性写回(`CLAUDE.md` 架构章节 + 已读代码路径) | ||
| 54 | + | ||
| 55 | +这是最值得直接复用的部分,和"用不用 Hermes CLI"完全无关,纯 stdlib: | ||
| 56 | + | ||
| 57 | +- `isolated_coding_workspace.isolate_workspace(source_dir)`:`tempfile.mkdtemp` 复制,源目录永不直接改动 | ||
| 58 | +- `workspace_security.validate_artifacts()`:拒绝绝对路径、`..`、逐段检查 symlink(防 TOCTOU 逃逸)、最终 `realpath`+`commonpath` 校验 | ||
| 59 | +- `workspace_security.validate_subprocess_command()`:只接受 `[python可执行文件, 已存在脚本, ...]`,拒绝 shell 元字符(`shell=False` 之外的纵深防御) | ||
| 60 | +- `change_manifest.py`:源 vs 隔离副本的 SHA-256 diff,生成 changed_files manifest | ||
| 61 | +- `selective_apply.apply_manifest()`:**唯一**写回真实源码的入口,校验每个 approved path、双向拒绝 symlink 逃逸、写入前先读全部字节(stage-before-commit)、`tempfile.mkstemp`+`os.replace` 原子写 | ||
| 62 | +- `run_graph_task.py` 的 `SafetyArgumentParser`:`--apply` 必须搭配显式 `--approve-path`/`--approve-all`,`--approve-all` 不带 `--apply` 直接拒绝——这是人工审批卡点,不是自动的 | ||
| 63 | + | ||
| 64 | +这套机制直接对应 graphlemon `self-development-feasibility.md` 缺口 1(worktree 隔离强制)的解法,而且是**已经在真实失败场景里验证过边界**的实现(见下一节)。 | ||
| 65 | + | ||
| 66 | +## 2. 真实失败证据(`tasks/phase10-progress.json`)及其含义 | ||
| 67 | + | ||
| 68 | +graphloop 自己跑 Phase 1-10 自我演进时卡在 Phase 2(`phase10_ready: false`),留下三个具体故障,每个都直接对应 graphlemon 架构里的一个假设: | ||
| 69 | + | ||
| 70 | +| 故障 | 现象 | 对 graphlemon 的启示 | | ||
| 71 | +|---|---|---| | ||
| 72 | +| `plan-schema-001` 超时 | Hermes worker 300s 未完成,判 failed | 03-C 里 `max_step_timeout`/`c_work_node_run` 超时参数不能拍脑袋定小了;graphloop 用的是 300s 默认值,实测不够 | | ||
| 73 | +| `step-fanout-001` **隔离被击穿** | Hermes worker 把实现文件直接写进了**源工作区**而不是隔离工作区;隔离 manifest 里没有任何变更,controller 判定"证据不可采信",清理了源目录里两个未授权文件,恢复快照 | **最关键的一条**:即使明确要求"在隔离工作区里改",执行 agent(无论 Claude 还是 Hermes)仍可能不遵守,隔离必须由 supervisor/controller 层做外部校验("我检查你改了哪些文件、改在哪"),不能只靠 prompt 里一句"请在隔离环境里改"。这直接印证了 `self-development-feasibility.md` 缺口 1 的判断,而且不是理论推测,是实测复现 | | ||
| 74 | +| worker 状态陈旧(PID 不存活但状态仍是 running) | `active_run.status="stale"`,controller 判定 `stale_reason="recorded worker PID is not alive"` | 印证了需要独立于被监督进程之外的存活探测(`os.kill(pid,0)` 这类外部判活),不能依赖被监督进程自己上报"我还活着" | | ||
| 75 | + | ||
| 76 | +## 3. 关键缺口:graphloop 的模型是"停下",graphlemon 要的是"回滚后继续" | ||
| 77 | + | ||
| 78 | +用户在缺口 3 的更正原话:"改坏了的 validator 没有人工评审倒不是问题,坏了由 supervisor 能够监督回滚即可"——这要求 Deploy Supervisor 具备**检测到问题后自动执行回滚动作、并让系统恢复到可继续工作的状态**的能力。 | ||
| 79 | + | ||
| 80 | +graphloop 现有两处监督实现(`hermes_watchdog.py`、`phase10_controller.py`)都止步于"标记 blocked,等人工",没有"自动回滚到上一个已知良好版本并恢复运行"这一步。这不是实现细节缺失,是设计哲学的差异: | ||
| 81 | + | ||
| 82 | +- graphloop:安全边界 = 停下(fail-closed to a halt) | ||
| 83 | +- graphlemon 要求:安全边界 = 回滚到已知良好状态后继续(fail-closed to a rollback, then resume) | ||
| 84 | + | ||
| 85 | +后者严格来说更难——"回滚"在 graphloop 的场景里相对简单(还没 apply,源码没动),但在 graphlemon 的蓝绿实例场景里,"回滚"意味着:停正在跑坏的新实例 → 把主分支 revert 或切回上一个 tag → 重新拉起旧版本实例 → 确认健康 → 恢复调度。这一段目前两个项目都没有代码。 | ||
| 86 | + | ||
| 87 | +## 4. 方案对比 | ||
| 88 | + | ||
| 89 | +| 维度 | 方案 A:全新自建 Deploy Supervisor | 方案 B:Hermes CLI 直接当"大脑" | 方案 C(推荐,已采纳):复用 graphloop 隔离/manifest/apply 库 + 新写确定性回滚层 | | ||
| 90 | +|---|---|---|---| | ||
| 91 | +| 隔离机制 | 从零写,风险是重新踩 graphloop 已经踩过的坑(symlink 逃逸、TOCTOU) | 同左,Hermes CLI 本身不提供这层,还是要自己写 | 直接复用 `isolated_coding_workspace.py`/`workspace_security.py`/`change_manifest.py`/`selective_apply.py`,已验证、纯 stdlib、零外部依赖 | | ||
| 92 | +| 存活/异常检测 | 从零写 | 同左 | 复用 `hermes_watchdog.py` 的判活逻辑(`os.kill(pid,0)` + staleness),改 action 分支 | | ||
| 93 | +| 决策逻辑是否含 LLM | 看怎么设计 | 是(如果真把 Hermes CLI 当决策者,等于让一个不确定性的 LLM 做安全关键判断) | 否——沿用 graphloop 的判断:安全关键的"要不要回滚"必须是确定性代码,不能是 LLM 判断 | | ||
| 94 | +| 自动回滚能力 | 需要新写(工作量:中,蓝绿切换脚本 + 健康检查 + revert 逻辑) | 需要新写,且如果决策链路里有 LLM 调用,回滚这种要求"确定性、可审计、不能出错"的动作反而更不适合让 LLM 参与 | 需要新写(同方案 A 工作量),但隔离/apply 部分零重复造轮子,净新增工作量最小 | | ||
| 95 | +| 与 Graph Engine 的独立性 | 完全独立,符合用户要求"第三方 agent" | 完全独立(Hermes CLI 本来就在另一套工具链) | 完全独立;graphloop 的 watchdog/controller 本来就是独立进程,架构上现成符合"third-party supervisor"要求 | | ||
| 96 | +| 已验证程度 | 零(全新代码) | 部分(graphloop 已用 Hermes CLI 跑了 Phase 1,但 Phase 2 卡住暴露隔离被击穿的问题——说明"用 Hermes CLI 执行任务"这条路径本身有已知未解决的可靠性问题) | 隔离/apply 部分已验证(含真实失败复现);回滚部分未验证(新代码,但范围小) | | ||
| 97 | +| 与既有决策的一致性 | 中性 | 有冲突:graphlemon 已明确决定 MVP 只用 Claude Code ACP 做 Org Agent 执行后端,不引入 Hermes CLI 执行任务;若 supervisor 内部又跑 Hermes CLI 去执行"诊断/回滚"这类任务,等于变相引入了第二个执行后端,需要重新论证 | 一致:只复用 graphloop 里**不调用任何 LLM 的确定性 Python 模块**(隔离/manifest/apply/watchdog 判活逻辑),不引入 Hermes CLI 作为执行后端,符合"只用 Claude Code ACP"的既有决策边界 | | ||
| 98 | + | ||
| 99 | +## 5. 推荐 | ||
| 100 | + | ||
| 101 | +**方案 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 的更正(自动回滚代替强制人工评审)。 | ||
| 102 | + | ||
| 103 | +不建议把 Hermes CLI 作为 supervisor 决策链路的一部分:graphloop 自己的实践已经证明——(a) 安全关键判断在其自身架构里也是纯确定性代码,从不假手 LLM;(b) 用 Hermes CLI 执行被监督的任务这条路径本身有实测的可靠性问题(隔离被击穿);(c) 引入 Hermes CLI 会和"MVP 只用 Claude Code ACP"这条已确认决策产生冲突,需要重新开一次论证。 | ||
| 104 | + | ||
| 105 | +## 6. 决策记录(2026-07-30,用户已逐条确认) | ||
| 106 | + | ||
| 107 | +1. **同意方案 C 方向**:"同意方案 c 方向,只在这一个环节复用,问题不大"——只复用隔离/apply 库这一部分,不引入 Hermes CLI 到决策链路。 | ||
| 108 | +2. **复制而非依赖**:"复制一份到 graphlemon 仓库改造,那边已经不维护了"——四个模块已复制到 `supervisor/isolation/` 并按 graphlemon 自身约定改造(详见该目录 `__init__.py` 的来源说明),不对 graphloop 仓库产生 import 依赖。 | ||
| 109 | +3. **自动回滚状态机现在就设计**:"自动回滚机制现在就设计一下,省得到 ph5 阶段有那个阶段的问题"——已完成,见 `supervisor-auto-rollback-design.md`,并已回填正式 SDD。 |
docs/wireframes/v_artifact_browser.html
0 → 100644
| 1 | +++ a/docs/wireframes/v_artifact_browser.html | ||
| 1 | +<!DOCTYPE html> | ||
| 2 | +<!-- v_artifact_browser — 静态原型,对应 02-V §2.8,路由 /artifacts --> | ||
| 3 | +<html lang="zh"> | ||
| 4 | +<head> | ||
| 5 | +<meta charset="UTF-8"> | ||
| 6 | +<title>graphlemon · Artifact 浏览器</title> | ||
| 7 | +<style> | ||
| 8 | +:root { --brand:#4f46e5; --border:#e5e7eb; --text:#111827; --text-muted:#6b7280; --bg:#fff; --bg-panel:#f9fafb; } | ||
| 9 | +* { box-sizing: border-box; } | ||
| 10 | +body { font-family:-apple-system,sans-serif; margin:0; color:var(--text); background:var(--bg); } | ||
| 11 | +.proto-banner { background:#fef3c7; border-bottom:1px solid #f59e0b; padding:6px 20px; font-size:12px; } | ||
| 12 | +.app-nav { display:flex; align-items:center; gap:4px; padding:0 20px; height:52px; border-bottom:1px solid var(--border); position:sticky; top:0; background:#fff; z-index:10;} | ||
| 13 | +.app-nav .brand { font-weight:700; margin-right:20px; color:var(--brand); } | ||
| 14 | +.app-nav a.nav-link { padding:8px 12px; border-radius:6px; color:var(--text); text-decoration:none; font-size:14px; } | ||
| 15 | +.app-nav a.nav-link:hover, .app-nav a.nav-link.active { background:var(--bg-panel); color:var(--brand); } | ||
| 16 | +.app-nav .nav-dropdown { position:relative; } | ||
| 17 | +.app-nav .nav-dropdown > span { padding:8px 12px; border-radius:6px; font-size:14px; cursor:pointer; display:inline-block; } | ||
| 18 | +.app-nav .dropdown-menu { display:none; position:absolute; top:100%; left:0; background:#fff; border:1px solid var(--border); border-radius:8px; padding:6px; min-width:160px; box-shadow:0 4px 12px rgba(0,0,0,.08); } | ||
| 19 | +.app-nav .nav-dropdown:hover .dropdown-menu { display:block; } | ||
| 20 | +.app-nav .dropdown-menu a { display:block; padding:6px 10px; border-radius:4px; color:var(--text); text-decoration:none; font-size:13px; } | ||
| 21 | +.app-nav .nav-project { margin-left:auto; font-size:13px; color:var(--text-muted); } | ||
| 22 | +.app-nav .nav-project a { color:var(--brand); text-decoration:none; margin-left:6px; } | ||
| 23 | +.page { padding:24px; max-width:1100px; margin:0 auto; display:grid; grid-template-columns: 320px 1fr; gap:20px; } | ||
| 24 | +h1 { font-size:20px; margin:0 0 16px; grid-column:1/-1; } | ||
| 25 | +.filters { display:flex; gap:10px; margin-bottom:14px; grid-column:1/-1; } | ||
| 26 | +.filters select { padding:8px 10px; border:1px solid var(--border); border-radius:6px; font-size:13px; } | ||
| 27 | +ul.artifact-list { list-style:none; margin:0; padding:0; border:1px solid var(--border); border-radius:10px; overflow:hidden; height:fit-content; } | ||
| 28 | +ul.artifact-list li { padding:10px 14px; border-bottom:1px solid var(--border); font-size:13px; cursor:pointer; } | ||
| 29 | +ul.artifact-list li:last-child { border-bottom:none; } | ||
| 30 | +ul.artifact-list li:hover, ul.artifact-list li.active { background:var(--bg-panel); } | ||
| 31 | +ul.artifact-list .type { font-size:11px; color:var(--text-muted); text-transform:uppercase; } | ||
| 32 | +.preview { border:1px solid var(--border); border-radius:10px; padding:20px; background:var(--bg-panel); } | ||
| 33 | +.lineage { display:flex; align-items:center; gap:8px; font-size:12px; color:var(--text-muted); margin-bottom:14px; } | ||
| 34 | +.lineage span.node { border:1px solid var(--border); border-radius:999px; padding:3px 10px; background:#fff; } | ||
| 35 | +pre.code { background:#0b1120; color:#d1d5db; padding:14px; border-radius:8px; font-size:12px; overflow:auto; } | ||
| 36 | +</style> | ||
| 37 | +</head> | ||
| 38 | +<body> | ||
| 39 | +<div class="proto-banner">静态原型(mock 数据)· 对应 02-V §2.8 · click_row → 展示 depends_on_artifact_ids 血缘链</div> | ||
| 40 | +<nav class="app-nav"> | ||
| 41 | + <div class="brand">graphlemon</div> | ||
| 42 | + <a href="v_dashboard.html" class="nav-link">仪表盘</a> | ||
| 43 | + <a href="v_task_board.html" class="nav-link">任务</a> | ||
| 44 | + <a href="v_artifact_browser.html" class="nav-link active">产物</a> | ||
| 45 | + <a href="v_skill_library.html" class="nav-link">技能库</a> | ||
| 46 | + <div class="nav-dropdown"><span>设置</span> | ||
| 47 | + <div class="dropdown-menu"> | ||
| 48 | + <a href="v_concurrency_settings.html">并发数</a> | ||
| 49 | + <a href="v_budget_settings.html">预算护栏</a> | ||
| 50 | + <a href="v_log_retention_settings.html">日志保留</a> | ||
| 51 | + </div> | ||
| 52 | + </div> | ||
| 53 | + <div class="nav-project">当前项目:snake-game <a href="v_project_setup.html">切换</a></div> | ||
| 54 | +</nav> | ||
| 55 | + | ||
| 56 | +<div class="page"> | ||
| 57 | + <h1>Artifact 浏览器</h1> | ||
| 58 | + <div class="filters"> | ||
| 59 | + <select><option>全部类型(artifact_type_filter)</option><option>sdd</option><option>code_diff</option></select> | ||
| 60 | + <select><option>全部任务(task_id_filter)</option><option>task-7a02</option></select> | ||
| 61 | + </div> | ||
| 62 | + | ||
| 63 | + <ul class="artifact-list"> | ||
| 64 | + <li class="active"><div class="type">sdd</div>sdd_v2.md</li> | ||
| 65 | + <li><div class="type">prd</div>prd_v1.md</li> | ||
| 66 | + <li><div class="type">code_diff</div>code_diff_collision.patch</li> | ||
| 67 | + <li><div class="type">test_report</div>test_report_r3.json</li> | ||
| 68 | + </ul> | ||
| 69 | + | ||
| 70 | + <div class="preview"> | ||
| 71 | + <div class="lineage">血缘链:<span class="node">brd_v1</span> → <span class="node">prd_v1</span> → <span class="node">sdd_v2(当前)</span></div> | ||
| 72 | + <pre class="code"># 4.2 撞墙判定规则 | ||
| 73 | + | ||
| 74 | +游戏区域为 20x20 网格,坐标从 0 开始。 | ||
| 75 | +撞墙判定:当蛇头坐标 x < 0 或 x >= 20(或 y 同理)时判负。 | ||
| 76 | +(注:Coder 在 code_diff_collision.patch 里最初把 >= 误写成 >, | ||
| 77 | + 是 task-7a02 这次 bug 的直接原因)</pre> | ||
| 78 | + </div> | ||
| 79 | +</div> | ||
| 80 | +</body> | ||
| 81 | +</html> |
docs/wireframes/v_budget_settings.html
0 → 100644
| 1 | +++ a/docs/wireframes/v_budget_settings.html | ||
| 1 | +<!DOCTYPE html> | ||
| 2 | +<!-- v_budget_settings — 静态原型,对应 02-V §2.12,路由 /settings/budget --> | ||
| 3 | +<html lang="zh"> | ||
| 4 | +<head> | ||
| 5 | +<meta charset="UTF-8"> | ||
| 6 | +<title>graphlemon · 预算护栏配置</title> | ||
| 7 | +<style> | ||
| 8 | +:root { --brand:#4f46e5; --border:#e5e7eb; --text:#111827; --text-muted:#6b7280; --bg:#fff; --bg-panel:#f9fafb; } | ||
| 9 | +* { box-sizing: border-box; } | ||
| 10 | +body { font-family:-apple-system,sans-serif; margin:0; color:var(--text); background:var(--bg); } | ||
| 11 | +.proto-banner { background:#fef3c7; border-bottom:1px solid #f59e0b; padding:6px 20px; font-size:12px; } | ||
| 12 | +.app-nav { display:flex; align-items:center; gap:4px; padding:0 20px; height:52px; border-bottom:1px solid var(--border); position:sticky; top:0; background:#fff; z-index:10;} | ||
| 13 | +.app-nav .brand { font-weight:700; margin-right:20px; color:var(--brand); } | ||
| 14 | +.app-nav a.nav-link { padding:8px 12px; border-radius:6px; color:var(--text); text-decoration:none; font-size:14px; } | ||
| 15 | +.app-nav a.nav-link:hover { background:var(--bg-panel); color:var(--brand); } | ||
| 16 | +.app-nav .nav-dropdown { position:relative; } | ||
| 17 | +.app-nav .nav-dropdown > span { padding:8px 12px; border-radius:6px; font-size:14px; cursor:pointer; display:inline-block; background:var(--bg-panel); color:var(--brand); } | ||
| 18 | +.app-nav .dropdown-menu { display:block; position:absolute; top:100%; left:0; background:#fff; border:1px solid var(--border); border-radius:8px; padding:6px; min-width:160px; box-shadow:0 4px 12px rgba(0,0,0,.08); } | ||
| 19 | +.app-nav .dropdown-menu a { display:block; padding:6px 10px; border-radius:4px; color:var(--text); text-decoration:none; font-size:13px; } | ||
| 20 | +.app-nav .dropdown-menu a.active { color:var(--brand); background:#eef2ff; } | ||
| 21 | +.app-nav .nav-project { margin-left:auto; font-size:13px; color:var(--text-muted); } | ||
| 22 | +.app-nav .nav-project a { color:var(--brand); text-decoration:none; margin-left:6px; } | ||
| 23 | +.page { padding:28px; max-width:520px; margin:20px auto 0; } | ||
| 24 | +h1 { font-size:20px; margin:0 0 4px; } | ||
| 25 | +.page-sub { color:var(--text-muted); font-size:13px; margin-bottom:24px; } | ||
| 26 | +.panel { border:1px solid var(--border); border-radius:10px; padding:20px; background:var(--bg-panel); } | ||
| 27 | +.field { margin-bottom:16px; } | ||
| 28 | +.field label { display:block; font-size:13px; font-weight:600; margin-bottom:6px; } | ||
| 29 | +.field input, .field select { width:100%; padding:8px 10px; border:1px solid var(--border); border-radius:6px; font-size:14px; } | ||
| 30 | +.progress { height:8px; border-radius:4px; background:#e5e7eb; overflow:hidden; margin-top:8px; } | ||
| 31 | +.progress .fill { height:100%; background:#f59e0b; width:78%; } | ||
| 32 | +.alert { background:#fef2f2; border:1px solid #fecaca; color:#991b1b; border-radius:8px; padding:10px 12px; font-size:12px; margin-top:14px; } | ||
| 33 | +.alert a { color:#991b1b; } | ||
| 34 | +.btn { display:inline-block; padding:9px 16px; border-radius:6px; border:1px solid var(--border); background:#fff; font-size:14px; cursor:pointer; margin-top:6px; } | ||
| 35 | +.btn-primary { background:var(--brand); border-color:var(--brand); color:#fff; } | ||
| 36 | +</style> | ||
| 37 | +</head> | ||
| 38 | +<body> | ||
| 39 | +<div class="proto-banner">静态原型(mock 数据)· 对应 02-V §2.12 · c_budget_config_update</div> | ||
| 40 | +<nav class="app-nav"> | ||
| 41 | + <div class="brand">graphlemon</div> | ||
| 42 | + <a href="v_dashboard.html" class="nav-link">仪表盘</a> | ||
| 43 | + <a href="v_task_board.html" class="nav-link">任务</a> | ||
| 44 | + <a href="v_artifact_browser.html" class="nav-link">产物</a> | ||
| 45 | + <a href="v_skill_library.html" class="nav-link">技能库</a> | ||
| 46 | + <div class="nav-dropdown"><span>设置</span> | ||
| 47 | + <div class="dropdown-menu"> | ||
| 48 | + <a href="v_concurrency_settings.html">并发数</a> | ||
| 49 | + <a href="v_budget_settings.html" class="active">预算护栏</a> | ||
| 50 | + <a href="v_log_retention_settings.html">日志保留</a> | ||
| 51 | + </div> | ||
| 52 | + </div> | ||
| 53 | + <div class="nav-project">当前项目:snake-game <a href="v_project_setup.html">切换</a></div> | ||
| 54 | +</nav> | ||
| 55 | + | ||
| 56 | +<div class="page"> | ||
| 57 | + <h1>预算护栏配置</h1> | ||
| 58 | + <div class="page-sub">/settings/budget · 人工配置层,与 Claude Code plan 额度耗尽是两套独立机制</div> | ||
| 59 | + <div class="panel"> | ||
| 60 | + <div class="field"> | ||
| 61 | + <label>作用范围(scope)</label> | ||
| 62 | + <select><option>global</option><option>per_task</option><option>per_day</option></select> | ||
| 63 | + </div> | ||
| 64 | + <div class="field"> | ||
| 65 | + <label>限额类型(limit_type)</label> | ||
| 66 | + <select><option>token_count</option><option>call_count</option></select> | ||
| 67 | + </div> | ||
| 68 | + <div class="field"> | ||
| 69 | + <label>限额值(limit_value)</label> | ||
| 70 | + <input type="number" value="100000"> | ||
| 71 | + </div> | ||
| 72 | + <div>当前用量:78,000 / 100,000</div> | ||
| 73 | + <div class="progress"><div class="fill"></div></div> | ||
| 74 | + <div class="alert">已有 1 个任务因超限转 blocked_pending_human · <a href="v_task_board.html">查看已暂停任务</a></div> | ||
| 75 | + <button class="btn btn-primary" style="margin-top:16px;">保存</button> | ||
| 76 | + </div> | ||
| 77 | +</div> | ||
| 78 | +</body> | ||
| 79 | +</html> |
docs/wireframes/v_bug_feedback.html
0 → 100644
| 1 | +++ a/docs/wireframes/v_bug_feedback.html | ||
| 1 | +<!DOCTYPE html> | ||
| 2 | +<!-- v_bug_feedback — 静态原型,对应 02-V §2.4,路由 /bugs/new --> | ||
| 3 | +<html lang="zh"> | ||
| 4 | +<head> | ||
| 5 | +<meta charset="UTF-8"> | ||
| 6 | +<title>graphlemon · 反馈 Bug</title> | ||
| 7 | +<style> | ||
| 8 | +:root { --brand:#4f46e5; --border:#e5e7eb; --text:#111827; --text-muted:#6b7280; --bg:#fff; --bg-panel:#f9fafb; } | ||
| 9 | +* { box-sizing: border-box; } | ||
| 10 | +body { font-family:-apple-system,sans-serif; margin:0; color:var(--text); background:var(--bg); } | ||
| 11 | +.proto-banner { background:#fef3c7; border-bottom:1px solid #f59e0b; padding:6px 20px; font-size:12px; } | ||
| 12 | +.app-nav { display:flex; align-items:center; gap:4px; padding:0 20px; height:52px; border-bottom:1px solid var(--border); position:sticky; top:0; background:#fff; z-index:10;} | ||
| 13 | +.app-nav .brand { font-weight:700; margin-right:20px; color:var(--brand); } | ||
| 14 | +.app-nav a.nav-link { padding:8px 12px; border-radius:6px; color:var(--text); text-decoration:none; font-size:14px; } | ||
| 15 | +.app-nav a.nav-link:hover { background:var(--bg-panel); color:var(--brand); } | ||
| 16 | +.app-nav .nav-dropdown { position:relative; } | ||
| 17 | +.app-nav .nav-dropdown > span { padding:8px 12px; border-radius:6px; font-size:14px; cursor:pointer; display:inline-block; } | ||
| 18 | +.app-nav .dropdown-menu { display:none; position:absolute; top:100%; left:0; background:#fff; border:1px solid var(--border); border-radius:8px; padding:6px; min-width:160px; box-shadow:0 4px 12px rgba(0,0,0,.08); } | ||
| 19 | +.app-nav .nav-dropdown:hover .dropdown-menu { display:block; } | ||
| 20 | +.app-nav .dropdown-menu a { display:block; padding:6px 10px; border-radius:4px; color:var(--text); text-decoration:none; font-size:13px; } | ||
| 21 | +.app-nav .nav-project { margin-left:auto; font-size:13px; color:var(--text-muted); } | ||
| 22 | +.app-nav .nav-project a { color:var(--brand); text-decoration:none; margin-left:6px; } | ||
| 23 | +.page { padding:28px; max-width:640px; margin:20px auto 0; } | ||
| 24 | +.page h1 { font-size:20px; margin:0 0 4px; } | ||
| 25 | +.page .page-sub { color:var(--text-muted); font-size:13px; margin-bottom:24px; } | ||
| 26 | +.panel { border:1px solid var(--border); border-radius:10px; padding:20px; background:var(--bg-panel); } | ||
| 27 | +.field { margin-bottom:16px; } | ||
| 28 | +.field label { display:block; font-size:13px; font-weight:600; margin-bottom:6px; } | ||
| 29 | +.field textarea, .field select { width:100%; padding:10px; border:1px solid var(--border); border-radius:6px; font-size:14px; font-family:inherit; } | ||
| 30 | +.field textarea { min-height:90px; resize:vertical; } | ||
| 31 | +.field .hint { font-size:12px; color:var(--text-muted); margin-top:5px; } | ||
| 32 | +.btn { display:inline-block; padding:9px 16px; border-radius:6px; border:1px solid var(--border); background:#fff; font-size:14px; cursor:pointer; } | ||
| 33 | +.btn-primary { background:var(--brand); border-color:var(--brand); color:#fff; } | ||
| 34 | +.btn-row { display:flex; gap:10px; margin-top:20px; justify-content:flex-end; } | ||
| 35 | +</style> | ||
| 36 | +</head> | ||
| 37 | +<body> | ||
| 38 | +<div class="proto-banner">静态原型(mock 数据)· 对应 02-V §2.4 · c_bug_report_create(source=human)</div> | ||
| 39 | +<nav class="app-nav"> | ||
| 40 | + <div class="brand">graphlemon</div> | ||
| 41 | + <a href="v_dashboard.html" class="nav-link">仪表盘</a> | ||
| 42 | + <a href="v_task_board.html" class="nav-link">任务</a> | ||
| 43 | + <a href="v_artifact_browser.html" class="nav-link">产物</a> | ||
| 44 | + <a href="v_skill_library.html" class="nav-link">技能库</a> | ||
| 45 | + <div class="nav-dropdown"><span>设置</span> | ||
| 46 | + <div class="dropdown-menu"> | ||
| 47 | + <a href="v_concurrency_settings.html">并发数</a> | ||
| 48 | + <a href="v_budget_settings.html">预算护栏</a> | ||
| 49 | + <a href="v_log_retention_settings.html">日志保留</a> | ||
| 50 | + </div> | ||
| 51 | + </div> | ||
| 52 | + <div class="nav-project">当前项目:snake-game <a href="v_project_setup.html">切换</a></div> | ||
| 53 | +</nav> | ||
| 54 | + | ||
| 55 | +<div class="page"> | ||
| 56 | + <h1>反馈 Bug</h1> | ||
| 57 | + <div class="page-sub">人工反馈,与 Validator 自动生成的 bug_record 复用同一张表(source=human)</div> | ||
| 58 | + <div class="panel"> | ||
| 59 | + <div class="field"> | ||
| 60 | + <label>问题描述(description,必填)</label> | ||
| 61 | + <textarea placeholder="描述你观察到的问题现象"></textarea> | ||
| 62 | + </div> | ||
| 63 | + <div class="field"> | ||
| 64 | + <label>复现步骤(repro_steps,非必填)</label> | ||
| 65 | + <textarea placeholder="可以先留空,Validator triage 阶段会补齐"></textarea> | ||
| 66 | + </div> | ||
| 67 | + <div class="field"> | ||
| 68 | + <label>关联任务(related_task_id,非必填)</label> | ||
| 69 | + <select> | ||
| 70 | + <option>— 不关联,视为新发现的独立问题 —</option> | ||
| 71 | + <option>实现贪吃蛇游戏(task-8f21)</option> | ||
| 72 | + </select> | ||
| 73 | + </div> | ||
| 74 | + <div class="btn-row"> | ||
| 75 | + <a class="btn" href="v_dashboard.html">取消</a> | ||
| 76 | + <a class="btn btn-primary" href="v_task_detail.html">提交反馈 →</a> | ||
| 77 | + </div> | ||
| 78 | + </div> | ||
| 79 | +</div> | ||
| 80 | +</body> | ||
| 81 | +</html> |
docs/wireframes/v_concurrency_settings.html
0 → 100644
| 1 | +++ a/docs/wireframes/v_concurrency_settings.html | ||
| 1 | +<!DOCTYPE html> | ||
| 2 | +<!-- v_concurrency_settings — 静态原型,对应 02-V §2.11,路由 /settings/concurrency --> | ||
| 3 | +<html lang="zh"> | ||
| 4 | +<head> | ||
| 5 | +<meta charset="UTF-8"> | ||
| 6 | +<title>graphlemon · 并发数配置</title> | ||
| 7 | +<style> | ||
| 8 | +:root { --brand:#4f46e5; --border:#e5e7eb; --text:#111827; --text-muted:#6b7280; --bg:#fff; --bg-panel:#f9fafb; } | ||
| 9 | +* { box-sizing: border-box; } | ||
| 10 | +body { font-family:-apple-system,sans-serif; margin:0; color:var(--text); background:var(--bg); } | ||
| 11 | +.proto-banner { background:#fef3c7; border-bottom:1px solid #f59e0b; padding:6px 20px; font-size:12px; } | ||
| 12 | +.app-nav { display:flex; align-items:center; gap:4px; padding:0 20px; height:52px; border-bottom:1px solid var(--border); position:sticky; top:0; background:#fff; z-index:10;} | ||
| 13 | +.app-nav .brand { font-weight:700; margin-right:20px; color:var(--brand); } | ||
| 14 | +.app-nav a.nav-link { padding:8px 12px; border-radius:6px; color:var(--text); text-decoration:none; font-size:14px; } | ||
| 15 | +.app-nav a.nav-link:hover { background:var(--bg-panel); color:var(--brand); } | ||
| 16 | +.app-nav .nav-dropdown { position:relative; } | ||
| 17 | +.app-nav .nav-dropdown > span { padding:8px 12px; border-radius:6px; font-size:14px; cursor:pointer; display:inline-block; background:var(--bg-panel); color:var(--brand); } | ||
| 18 | +.app-nav .dropdown-menu { display:block; position:absolute; top:100%; left:0; background:#fff; border:1px solid var(--border); border-radius:8px; padding:6px; min-width:160px; box-shadow:0 4px 12px rgba(0,0,0,.08); } | ||
| 19 | +.app-nav .dropdown-menu a { display:block; padding:6px 10px; border-radius:4px; color:var(--text); text-decoration:none; font-size:13px; } | ||
| 20 | +.app-nav .dropdown-menu a.active { color:var(--brand); background:#eef2ff; } | ||
| 21 | +.app-nav .nav-project { margin-left:auto; font-size:13px; color:var(--text-muted); } | ||
| 22 | +.app-nav .nav-project a { color:var(--brand); text-decoration:none; margin-left:6px; } | ||
| 23 | +.page { padding:28px; max-width:520px; margin:20px auto 0; } | ||
| 24 | +h1 { font-size:20px; margin:0 0 4px; } | ||
| 25 | +.page-sub { color:var(--text-muted); font-size:13px; margin-bottom:24px; } | ||
| 26 | +.panel { border:1px solid var(--border); border-radius:10px; padding:20px; background:var(--bg-panel); } | ||
| 27 | +.field label { display:block; font-size:13px; font-weight:600; margin-bottom:6px; } | ||
| 28 | +.field input { width:120px; padding:8px 10px; border:1px solid var(--border); border-radius:6px; font-size:14px; } | ||
| 29 | +.field .hint { font-size:12px; color:var(--text-muted); margin-top:5px; } | ||
| 30 | +.btn { display:inline-block; padding:9px 16px; border-radius:6px; border:1px solid var(--border); background:#fff; font-size:14px; cursor:pointer; margin-top:18px; } | ||
| 31 | +.btn-primary { background:var(--brand); border-color:var(--brand); color:#fff; } | ||
| 32 | +</style> | ||
| 33 | +</head> | ||
| 34 | +<body> | ||
| 35 | +<div class="proto-banner">静态原型(mock 数据)· 对应 02-V §2.11 · c_concurrency_config_update</div> | ||
| 36 | +<nav class="app-nav"> | ||
| 37 | + <div class="brand">graphlemon</div> | ||
| 38 | + <a href="v_dashboard.html" class="nav-link">仪表盘</a> | ||
| 39 | + <a href="v_task_board.html" class="nav-link">任务</a> | ||
| 40 | + <a href="v_artifact_browser.html" class="nav-link">产物</a> | ||
| 41 | + <a href="v_skill_library.html" class="nav-link">技能库</a> | ||
| 42 | + <div class="nav-dropdown"><span>设置</span> | ||
| 43 | + <div class="dropdown-menu"> | ||
| 44 | + <a href="v_concurrency_settings.html" class="active">并发数</a> | ||
| 45 | + <a href="v_budget_settings.html">预算护栏</a> | ||
| 46 | + <a href="v_log_retention_settings.html">日志保留</a> | ||
| 47 | + </div> | ||
| 48 | + </div> | ||
| 49 | + <div class="nav-project">当前项目:snake-game <a href="v_project_setup.html">切换</a></div> | ||
| 50 | +</nav> | ||
| 51 | + | ||
| 52 | +<div class="page"> | ||
| 53 | + <h1>并发数配置</h1> | ||
| 54 | + <div class="page-sub">/settings/concurrency</div> | ||
| 55 | + <div class="panel"> | ||
| 56 | + <div class="field"> | ||
| 57 | + <label>最大并发 Goal 数(max_concurrent_goals,1-50)</label> | ||
| 58 | + <input type="number" value="1" min="1" max="50"> | ||
| 59 | + <div class="hint">MVP 默认 1;调大后下一个新 Goal 起生效</div> | ||
| 60 | + </div> | ||
| 61 | + <button class="btn btn-primary">保存</button> | ||
| 62 | + </div> | ||
| 63 | +</div> | ||
| 64 | +</body> | ||
| 65 | +</html> |
docs/wireframes/v_dashboard.html
0 → 100644
| 1 | +++ a/docs/wireframes/v_dashboard.html | ||
| 1 | +<!DOCTYPE html> | ||
| 2 | +<!-- | ||
| 3 | + v_dashboard — 静态原型(mock 数据,无构建步骤) | ||
| 4 | + 对应:02-V-界面与交互.md §2.2,路由 /(默认首页) | ||
| 5 | + 这是本次连页原型里布局最复杂的页面之一(多面板实时看板),值得完整画出。 | ||
| 6 | +--> | ||
| 7 | +<html lang="zh"> | ||
| 8 | +<head> | ||
| 9 | +<meta charset="UTF-8"> | ||
| 10 | +<title>graphlemon · 仪表盘</title> | ||
| 11 | +<style> | ||
| 12 | +:root { --brand:#4f46e5; --border:#e5e7eb; --text:#111827; --text-muted:#6b7280; --bg:#fff; --bg-panel:#f9fafb; | ||
| 13 | + --status-pending:#9ca3af; --status-running:#3b82f6; --status-reflecting:#eab308; --status-waiting-quota:#7dd3fc; --status-blocked:#ef4444; --status-done:#22c55e; } | ||
| 14 | +* { box-sizing: border-box; } | ||
| 15 | +body { font-family:-apple-system,sans-serif; margin:0; color:var(--text); background:var(--bg); } | ||
| 16 | +.proto-banner { background:#fef3c7; border-bottom:1px solid #f59e0b; padding:6px 20px; font-size:12px; } | ||
| 17 | +.app-nav { display:flex; align-items:center; gap:4px; padding:0 20px; height:52px; border-bottom:1px solid var(--border); position:sticky; top:0; background:#fff; z-index:10;} | ||
| 18 | +.app-nav .brand { font-weight:700; margin-right:20px; color:var(--brand); } | ||
| 19 | +.app-nav a.nav-link { padding:8px 12px; border-radius:6px; color:var(--text); text-decoration:none; font-size:14px; } | ||
| 20 | +.app-nav a.nav-link:hover, .app-nav a.nav-link.active { background:var(--bg-panel); color:var(--brand); } | ||
| 21 | +.app-nav .nav-dropdown { position:relative; } | ||
| 22 | +.app-nav .nav-dropdown > span { padding:8px 12px; border-radius:6px; font-size:14px; cursor:pointer; display:inline-block; } | ||
| 23 | +.app-nav .nav-dropdown:hover > span { background:var(--bg-panel); color:var(--brand); } | ||
| 24 | +.app-nav .dropdown-menu { display:none; position:absolute; top:100%; left:0; background:#fff; border:1px solid var(--border); border-radius:8px; padding:6px; min-width:160px; box-shadow:0 4px 12px rgba(0,0,0,.08); } | ||
| 25 | +.app-nav .nav-dropdown:hover .dropdown-menu { display:block; } | ||
| 26 | +.app-nav .dropdown-menu a { display:block; padding:6px 10px; border-radius:4px; color:var(--text); text-decoration:none; font-size:13px; } | ||
| 27 | +.app-nav .dropdown-menu a:hover { background:var(--bg-panel); } | ||
| 28 | +.app-nav .nav-project { margin-left:auto; font-size:13px; color:var(--text-muted); } | ||
| 29 | +.app-nav .nav-project a { color:var(--brand); text-decoration:none; margin-left:6px; } | ||
| 30 | +.page { padding:24px; max-width:1200px; margin:0 auto; } | ||
| 31 | +.page h1 { font-size:20px; margin:0 0 16px; } | ||
| 32 | +.info-banner { background:#eff6ff; border:1px solid #bfdbfe; border-radius:8px; padding:10px 14px; font-size:13px; margin-bottom:18px; display:flex; align-items:center; gap:8px; } | ||
| 33 | +.grid { display:grid; grid-template-columns: 2fr 1fr; gap:20px; } | ||
| 34 | +.panel { border:1px solid var(--border); border-radius:10px; padding:18px; background:var(--bg-panel); margin-bottom:20px; } | ||
| 35 | +.panel h2 { font-size:13px; text-transform:uppercase; letter-spacing:.04em; color:var(--text-muted); margin:0 0 14px; } | ||
| 36 | +.org-strip { display:flex; align-items:center; gap:0; } | ||
| 37 | +.org-node { border:2px solid var(--border); border-radius:8px; padding:10px 18px; text-align:center; background:#fff; flex:1; } | ||
| 38 | +.org-node.active { border-color:var(--status-running); background:#eff6ff; } | ||
| 39 | +.org-node .role { font-weight:600; font-size:13px; } | ||
| 40 | +.org-node .hint { font-size:11px; color:var(--text-muted); margin-top:2px; } | ||
| 41 | +.org-arrow { flex:0 0 30px; text-align:center; color:var(--text-muted); } | ||
| 42 | +.task-summary .row { display:flex; justify-content:space-between; font-size:13px; padding:6px 0; border-bottom:1px solid var(--border); } | ||
| 43 | +.task-summary .row:last-child { border-bottom:none; } | ||
| 44 | +.progress { height:8px; border-radius:4px; background:#e5e7eb; overflow:hidden; margin-top:6px; } | ||
| 45 | +.progress .fill { height:100%; background:var(--brand); width:42%; } | ||
| 46 | +.event-item { font-size:12px; padding:8px 0; border-bottom:1px solid var(--border); } | ||
| 47 | +.event-item .t { color:var(--text-muted); } | ||
| 48 | +.actions { display:flex; gap:10px; margin-bottom:20px; } | ||
| 49 | +.btn { display:inline-block; padding:8px 16px; border-radius:6px; border:1px solid var(--border); background:#fff; font-size:14px; cursor:pointer; text-decoration:none; color:var(--text); } | ||
| 50 | +.btn-primary { background:var(--brand); border-color:var(--brand); color:#fff; } | ||
| 51 | +</style> | ||
| 52 | +</head> | ||
| 53 | +<body> | ||
| 54 | +<div class="proto-banner">静态原型(mock 数据)· 对应 02-V §2.2 · 全部内容通过 c_ws_subscribe 实时刷新</div> | ||
| 55 | + | ||
| 56 | +<nav class="app-nav"> | ||
| 57 | + <div class="brand">graphlemon</div> | ||
| 58 | + <a href="v_dashboard.html" class="nav-link active">仪表盘</a> | ||
| 59 | + <a href="v_task_board.html" class="nav-link">任务</a> | ||
| 60 | + <a href="v_artifact_browser.html" class="nav-link">产物</a> | ||
| 61 | + <a href="v_skill_library.html" class="nav-link">技能库</a> | ||
| 62 | + <div class="nav-dropdown"><span>设置</span> | ||
| 63 | + <div class="dropdown-menu"> | ||
| 64 | + <a href="v_concurrency_settings.html">并发数</a> | ||
| 65 | + <a href="v_budget_settings.html">预算护栏</a> | ||
| 66 | + <a href="v_log_retention_settings.html">日志保留</a> | ||
| 67 | + </div> | ||
| 68 | + </div> | ||
| 69 | + <div class="nav-project">当前项目:snake-game <a href="v_project_setup.html">切换</a></div> | ||
| 70 | +</nav> | ||
| 71 | + | ||
| 72 | +<div class="page"> | ||
| 73 | + <h1>仪表盘</h1> | ||
| 74 | + | ||
| 75 | + <!-- waiting_for_quota 信息态提示条,见 02-V §2.2 "信息态" --> | ||
| 76 | + <div class="info-banner">🕐 有任务正在等待 Claude Code 额度恢复(session),预计还需 8 分钟 · <a href="v_task_detail.html">查看详情</a></div> | ||
| 77 | + | ||
| 78 | + <div class="grid"> | ||
| 79 | + <div> | ||
| 80 | + <div class="panel"> | ||
| 81 | + <h2>Org Graph 三节点状态</h2> | ||
| 82 | + <div class="org-strip"> | ||
| 83 | + <div class="org-node"><div class="role">Planner</div><div class="hint">已完成</div></div> | ||
| 84 | + <div class="org-arrow">→</div> | ||
| 85 | + <div class="org-node active"><div class="role">Coder</div><div class="hint">当前节点</div></div> | ||
| 86 | + <div class="org-arrow">→</div> | ||
| 87 | + <div class="org-node"><div class="role">Validator</div><div class="hint">未开始</div></div> | ||
| 88 | + </div> | ||
| 89 | + </div> | ||
| 90 | + | ||
| 91 | + <div class="panel"> | ||
| 92 | + <h2>当前任务概要</h2> | ||
| 93 | + <div class="task-summary"> | ||
| 94 | + <div class="row"><span>Goal</span><span>实现贪吃蛇游戏</span></div> | ||
| 95 | + <div class="row"><span>Loop Round</span><span>0</span></div> | ||
| 96 | + <div class="row"><span>Status</span><span>coding</span></div> | ||
| 97 | + </div> | ||
| 98 | + </div> | ||
| 99 | + | ||
| 100 | + <div class="actions"> | ||
| 101 | + <a class="btn btn-primary" href="v_goal_composer.html">+ 提出新目标</a> | ||
| 102 | + <a class="btn" href="v_bug_feedback.html">反馈 Bug</a> | ||
| 103 | + </div> | ||
| 104 | + </div> | ||
| 105 | + | ||
| 106 | + <div> | ||
| 107 | + <div class="panel"> | ||
| 108 | + <h2>预算用量</h2> | ||
| 109 | + <div style="font-size:13px;">42,300 / 100,000 tokens</div> | ||
| 110 | + <div class="progress"><div class="fill"></div></div> | ||
| 111 | + </div> | ||
| 112 | + | ||
| 113 | + <div class="panel"> | ||
| 114 | + <h2>最近事件</h2> | ||
| 115 | + <div class="event-item"><div>Coder 节点 module-a 状态变更为 done</div><div class="t">刚刚</div></div> | ||
| 116 | + <div class="event-item"><div>Artifact code_diff 已生成</div><div class="t">2 分钟前</div></div> | ||
| 117 | + <div class="event-item"><div>Work Node module-c 进入 waiting_for_quota</div><div class="t">8 分钟前</div></div> | ||
| 118 | + </div> | ||
| 119 | + </div> | ||
| 120 | + </div> | ||
| 121 | +</div> | ||
| 122 | +</body> | ||
| 123 | +</html> |
docs/wireframes/v_goal_composer.html
0 → 100644
| 1 | +++ a/docs/wireframes/v_goal_composer.html | ||
| 1 | +<!DOCTYPE html> | ||
| 2 | +<!-- v_goal_composer — 静态原型,对应 02-V §2.3,路由 /goals/new --> | ||
| 3 | +<html lang="zh"> | ||
| 4 | +<head> | ||
| 5 | +<meta charset="UTF-8"> | ||
| 6 | +<title>graphlemon · 提出目标</title> | ||
| 7 | +<style> | ||
| 8 | +:root { --brand:#4f46e5; --border:#e5e7eb; --text:#111827; --text-muted:#6b7280; --bg:#fff; --bg-panel:#f9fafb; } | ||
| 9 | +* { box-sizing: border-box; } | ||
| 10 | +body { font-family:-apple-system,sans-serif; margin:0; color:var(--text); background:var(--bg); } | ||
| 11 | +.proto-banner { background:#fef3c7; border-bottom:1px solid #f59e0b; padding:6px 20px; font-size:12px; } | ||
| 12 | +.app-nav { display:flex; align-items:center; gap:4px; padding:0 20px; height:52px; border-bottom:1px solid var(--border); position:sticky; top:0; background:#fff; z-index:10;} | ||
| 13 | +.app-nav .brand { font-weight:700; margin-right:20px; color:var(--brand); } | ||
| 14 | +.app-nav a.nav-link { padding:8px 12px; border-radius:6px; color:var(--text); text-decoration:none; font-size:14px; } | ||
| 15 | +.app-nav a.nav-link:hover { background:var(--bg-panel); color:var(--brand); } | ||
| 16 | +.app-nav .nav-dropdown { position:relative; } | ||
| 17 | +.app-nav .nav-dropdown > span { padding:8px 12px; border-radius:6px; font-size:14px; cursor:pointer; display:inline-block; } | ||
| 18 | +.app-nav .dropdown-menu { display:none; position:absolute; top:100%; left:0; background:#fff; border:1px solid var(--border); border-radius:8px; padding:6px; min-width:160px; box-shadow:0 4px 12px rgba(0,0,0,.08); } | ||
| 19 | +.app-nav .nav-dropdown:hover .dropdown-menu { display:block; } | ||
| 20 | +.app-nav .dropdown-menu a { display:block; padding:6px 10px; border-radius:4px; color:var(--text); text-decoration:none; font-size:13px; } | ||
| 21 | +.app-nav .nav-project { margin-left:auto; font-size:13px; color:var(--text-muted); } | ||
| 22 | +.app-nav .nav-project a { color:var(--brand); text-decoration:none; margin-left:6px; } | ||
| 23 | +.page { padding:28px; max-width:640px; margin:20px auto 0; } | ||
| 24 | +.page h1 { font-size:20px; margin:0 0 4px; } | ||
| 25 | +.page .page-sub { color:var(--text-muted); font-size:13px; margin-bottom:24px; } | ||
| 26 | +.panel { border:1px solid var(--border); border-radius:10px; padding:20px; background:var(--bg-panel); } | ||
| 27 | +.field label { display:block; font-size:13px; font-weight:600; margin-bottom:6px; } | ||
| 28 | +.field textarea { width:100%; padding:10px; border:1px solid var(--border); border-radius:6px; font-size:14px; font-family:inherit; min-height:140px; resize:vertical; } | ||
| 29 | +.field .hint { font-size:12px; color:var(--text-muted); margin-top:5px; } | ||
| 30 | +.btn { display:inline-block; padding:9px 16px; border-radius:6px; border:1px solid var(--border); background:#fff; font-size:14px; cursor:pointer; } | ||
| 31 | +.btn-primary { background:var(--brand); border-color:var(--brand); color:#fff; } | ||
| 32 | +.btn-row { display:flex; gap:10px; margin-top:20px; justify-content:flex-end; } | ||
| 33 | +</style> | ||
| 34 | +</head> | ||
| 35 | +<body> | ||
| 36 | +<div class="proto-banner">静态原型(mock 数据)· 对应 02-V §2.3 · c_task_create(created_via=human_console)</div> | ||
| 37 | +<nav class="app-nav"> | ||
| 38 | + <div class="brand">graphlemon</div> | ||
| 39 | + <a href="v_dashboard.html" class="nav-link">仪表盘</a> | ||
| 40 | + <a href="v_task_board.html" class="nav-link">任务</a> | ||
| 41 | + <a href="v_artifact_browser.html" class="nav-link">产物</a> | ||
| 42 | + <a href="v_skill_library.html" class="nav-link">技能库</a> | ||
| 43 | + <div class="nav-dropdown"><span>设置</span> | ||
| 44 | + <div class="dropdown-menu"> | ||
| 45 | + <a href="v_concurrency_settings.html">并发数</a> | ||
| 46 | + <a href="v_budget_settings.html">预算护栏</a> | ||
| 47 | + <a href="v_log_retention_settings.html">日志保留</a> | ||
| 48 | + </div> | ||
| 49 | + </div> | ||
| 50 | + <div class="nav-project">当前项目:snake-game <a href="v_project_setup.html">切换</a></div> | ||
| 51 | +</nav> | ||
| 52 | + | ||
| 53 | +<div class="page"> | ||
| 54 | + <h1>提出新目标</h1> | ||
| 55 | + <div class="page-sub">从仪表盘"提出新目标"按钮进入,也可独立通过 /goals/new 访问</div> | ||
| 56 | + <div class="panel"> | ||
| 57 | + <div class="field"> | ||
| 58 | + <label>目标描述(goal_text,必填,maxLen 4000)</label> | ||
| 59 | + <textarea placeholder="用自然语言描述你想让 graphlemon 完成的目标,例如:实现一个可以在终端里玩的贪吃蛇游戏,支持方向键控制、撞墙/撞自身判负、显示当前得分">实现一个可以在终端里玩的贪吃蛇游戏,支持方向键控制、撞墙/撞自身判负、显示当前得分</textarea> | ||
| 60 | + <div class="hint">提交前会先展示确认态摘要,再调用 c_task_create</div> | ||
| 61 | + </div> | ||
| 62 | + <div class="btn-row"> | ||
| 63 | + <a class="btn" href="v_dashboard.html">取消</a> | ||
| 64 | + <a class="btn btn-primary" href="v_task_detail.html">确认创建 →</a> | ||
| 65 | + </div> | ||
| 66 | + </div> | ||
| 67 | +</div> | ||
| 68 | +</body> | ||
| 69 | +</html> |
docs/wireframes/v_graph_visualizer.html
0 → 100644
| 1 | +++ a/docs/wireframes/v_graph_visualizer.html | ||
| 1 | +<!DOCTYPE html> | ||
| 2 | +<!-- | ||
| 3 | + v_graph_visualizer — 静态原型(mock 数据,无构建步骤) | ||
| 4 | + | ||
| 5 | + 用途:给 Claude Code 在实现 PH3 U-026 时用作视觉+结构参照,不是最终代码, | ||
| 6 | + 但 DOM 结构 / class 命名 / 数据字段名刻意和真实实现保持一致,可以直接在这个 | ||
| 7 | + 基础上接入真实 c_ws_subscribe 推送数据,而不必重新设计布局。 | ||
| 8 | + | ||
| 9 | + 对应 SDD 条目: | ||
| 10 | + - 02-V-界面与交互.md §2.7(v_graph_visualizer)+ §2.7.1(v_log_viewer,本原型未展开, | ||
| 11 | + 真实页面里通过 click_view_raw_log 从 Work Node 卡片展开) | ||
| 12 | + - 03-C-控制与接口.md 3.6 节:work_node_execution.status 六态着色语义 | ||
| 13 | + done=绿 / running=蓝 / reflecting|retrying=黄 / waiting_for_quota=浅蓝+时钟图标(信息态,非告警) | ||
| 14 | + / blocked|failed=红 / pending=灰 | ||
| 15 | + - sdd-mvcp-contract.json view_contract.v_graph_visualizer: | ||
| 16 | + fields=[{name:"run_id", visible_when:"task_has_active_run"}] | ||
| 17 | + events=[{event:"hover_node", target_controller_id:"c_work_node_get", payload:{node_exec_id}}] | ||
| 18 | + | ||
| 19 | + DOM 约定(供 CC 直接复用): | ||
| 20 | + - 每个 Work Node 卡片:class="work-node status-<work_node_execution.status 枚举值>" | ||
| 21 | + - data-node-exec-id 属性对应真实的 node_exec_id,真实实现里 hover 事件从这里读取 | ||
| 22 | + - waiting_for_quota 状态额外带 data-quota-limit-type / data-quota-wait-since 两个属性, | ||
| 23 | + 对应 03-C 3.6 节退避重试语义里要展示的 quota_limit_type + 已等待时长 | ||
| 24 | +--> | ||
| 25 | +<html lang="zh"> | ||
| 26 | +<head> | ||
| 27 | +<meta charset="UTF-8"> | ||
| 28 | +<title>v_graph_visualizer 原型(mock 数据)</title> | ||
| 29 | +<style> | ||
| 30 | + :root { | ||
| 31 | + --status-pending: #9ca3af; | ||
| 32 | + --status-running: #3b82f6; | ||
| 33 | + --status-reflecting: #eab308; | ||
| 34 | + --status-waiting-quota: #7dd3fc; | ||
| 35 | + --status-blocked: #ef4444; | ||
| 36 | + --status-done: #22c55e; | ||
| 37 | + --border: #d1d5db; | ||
| 38 | + --text: #111827; | ||
| 39 | + --text-muted: #6b7280; | ||
| 40 | + --bg-panel: #f9fafb; | ||
| 41 | + } | ||
| 42 | + body { font-family: -apple-system, sans-serif; margin: 0; padding: 24px; background: #fff; color: var(--text); } | ||
| 43 | + .proto-banner { background: #fef3c7; border: 1px solid #f59e0b; border-radius: 6px; padding: 8px 12px; font-size: 13px; margin-bottom: 20px; } | ||
| 44 | + h2 { font-size: 15px; margin: 0 0 12px; color: var(--text-muted); text-transform: uppercase; letter-spacing: .04em; } | ||
| 45 | + section { margin-bottom: 32px; } | ||
| 46 | + | ||
| 47 | + /* ---- Org Graph:固定三节点 ---- */ | ||
| 48 | + .org-graph { display: flex; align-items: center; gap: 0; } | ||
| 49 | + .org-node { border: 2px solid var(--border); border-radius: 10px; padding: 14px 24px; text-align: center; background: var(--bg-panel); min-width: 120px; } | ||
| 50 | + .org-node.active { border-color: var(--status-running); background: #eff6ff; box-shadow: 0 0 0 3px rgba(59,130,246,.15); } | ||
| 51 | + .org-node .role { font-weight: 600; font-size: 14px; } | ||
| 52 | + .org-node .hint { font-size: 11px; color: var(--text-muted); margin-top: 4px; } | ||
| 53 | + .org-arrow { flex: 0 0 60px; text-align: center; color: var(--text-muted); font-size: 20px; } | ||
| 54 | + | ||
| 55 | + /* ---- Work Graph:动态节点/边 ---- */ | ||
| 56 | + .work-graph { position: relative; border: 1px dashed var(--border); border-radius: 8px; padding: 32px; background: var(--bg-panel); } | ||
| 57 | + .work-row { display: flex; gap: 24px; margin-bottom: 40px; justify-content: center; } | ||
| 58 | + .work-node { | ||
| 59 | + border: 2px solid var(--border); | ||
| 60 | + border-radius: 8px; | ||
| 61 | + padding: 10px 14px; | ||
| 62 | + min-width: 140px; | ||
| 63 | + background: #fff; | ||
| 64 | + cursor: pointer; | ||
| 65 | + position: relative; | ||
| 66 | + font-size: 13px; | ||
| 67 | + } | ||
| 68 | + .work-node .node-id { font-weight: 600; } | ||
| 69 | + .work-node .node-skill { color: var(--text-muted); font-size: 11px; margin-top: 2px; } | ||
| 70 | + .work-node.status-pending { border-color: var(--status-pending); } | ||
| 71 | + .work-node.status-running { border-color: var(--status-running); background: #eff6ff; } | ||
| 72 | + .work-node.status-reflecting, .work-node.status-retrying { border-color: var(--status-reflecting); background: #fefce8; } | ||
| 73 | + .work-node.status-waiting_for_quota { border-color: var(--status-waiting-quota); background: #f0f9ff; } | ||
| 74 | + .work-node.status-waiting_for_quota::after { content: "🕐"; position: absolute; top: -8px; right: -8px; font-size: 14px; } | ||
| 75 | + .work-node.status-blocked, .work-node.status-failed { border-color: var(--status-blocked); background: #fef2f2; } | ||
| 76 | + .work-node.status-done { border-color: var(--status-done); background: #f0fdf4; } | ||
| 77 | + | ||
| 78 | + /* ---- 图例 ---- */ | ||
| 79 | + .legend { display: flex; gap: 16px; flex-wrap: wrap; font-size: 12px; color: var(--text-muted); } | ||
| 80 | + .legend-item { display: flex; align-items: center; gap: 6px; } | ||
| 81 | + .legend-swatch { width: 14px; height: 14px; border-radius: 4px; border: 2px solid; } | ||
| 82 | + | ||
| 83 | + /* ---- hover tooltip(示意,真实实现走 c_work_node_get) ---- */ | ||
| 84 | + .tooltip { | ||
| 85 | + position: absolute; z-index: 10; background: #111827; color: #fff; font-size: 12px; | ||
| 86 | + padding: 8px 10px; border-radius: 6px; max-width: 260px; display: none; pointer-events: none; | ||
| 87 | + line-height: 1.5; | ||
| 88 | + } | ||
| 89 | + .tooltip.show { display: block; } | ||
| 90 | +</style> | ||
| 91 | +</head> | ||
| 92 | +<body> | ||
| 93 | + | ||
| 94 | +<div class="proto-banner"> | ||
| 95 | + 这是 v_graph_visualizer 的静态原型(mock 数据,不连真实后端)。用于 PH3 U-026 开发前的视觉核对—— | ||
| 96 | + DOM 结构 / class 命名 / data-* 属性均按 02-V §2.7、03-C 3.6 节约定设计,实现时可以直接复用这套结构接入 | ||
| 97 | + c_ws_subscribe 推送的真实数据,不需要重新排版。 | ||
| 98 | +</div> | ||
| 99 | + | ||
| 100 | +<section> | ||
| 101 | + <h2>Org Graph(固定三节点,02-V §2.7)</h2> | ||
| 102 | + <div class="org-graph"> | ||
| 103 | + <div class="org-node"> | ||
| 104 | + <div class="role">Planner</div> | ||
| 105 | + <div class="hint">已完成</div> | ||
| 106 | + </div> | ||
| 107 | + <div class="org-arrow">→</div> | ||
| 108 | + <div class="org-node active"> | ||
| 109 | + <div class="role">Coder</div> | ||
| 110 | + <div class="hint">当前节点(task.assigned_org_node)</div> | ||
| 111 | + </div> | ||
| 112 | + <div class="org-arrow">→</div> | ||
| 113 | + <div class="org-node"> | ||
| 114 | + <div class="role">Validator</div> | ||
| 115 | + <div class="hint">未开始</div> | ||
| 116 | + </div> | ||
| 117 | + </div> | ||
| 118 | +</section> | ||
| 119 | + | ||
| 120 | +<section> | ||
| 121 | + <h2>Work Graph(当前 work_graph_run.graph_manifest 渲染,节点按 status 着色)</h2> | ||
| 122 | + | ||
| 123 | + <div class="work-graph"> | ||
| 124 | + <div class="work-row"> | ||
| 125 | + <div class="work-node status-done" data-node-exec-id="node-001" | ||
| 126 | + onmouseover="showTip(event, 'node-001', 'skill: code-task-development', 'output_schema_ref: code_diff.schema.json', 'retry_count: 0')" | ||
| 127 | + onmouseout="hideTip()"> | ||
| 128 | + <div class="node-id">worker:module-a</div> | ||
| 129 | + <div class="node-skill">code-task-development</div> | ||
| 130 | + </div> | ||
| 131 | + <div class="work-node status-done" data-node-exec-id="node-002" | ||
| 132 | + onmouseover="showTip(event, 'node-002', 'skill: code-task-development', 'output_schema_ref: code_diff.schema.json', 'retry_count: 1')" | ||
| 133 | + onmouseout="hideTip()"> | ||
| 134 | + <div class="node-id">worker:module-b</div> | ||
| 135 | + <div class="node-skill">code-task-development</div> | ||
| 136 | + </div> | ||
| 137 | + </div> | ||
| 138 | + | ||
| 139 | + <div class="work-row"> | ||
| 140 | + <div class="work-node status-waiting_for_quota" data-node-exec-id="node-003" | ||
| 141 | + data-quota-limit-type="session" data-quota-wait-since="12 分钟前" | ||
| 142 | + onmouseover="showTip(event, 'node-003', 'skill: code-agent-engineering', 'quota_limit_type: session', '已等待 12 分钟,自愈型阻塞')" | ||
| 143 | + onmouseout="hideTip()"> | ||
| 144 | + <div class="node-id">worker:module-c</div> | ||
| 145 | + <div class="node-skill">code-agent-engineering</div> | ||
| 146 | + </div> | ||
| 147 | + <div class="work-node status-reflecting" data-node-exec-id="node-004" | ||
| 148 | + onmouseover="showTip(event, 'node-004', 'skill: debugging-toolkit', 'retry_count: 2', 'reflection_notes: unit test 断言不通过')" | ||
| 149 | + onmouseout="hideTip()"> | ||
| 150 | + <div class="node-id">worker:module-d</div> | ||
| 151 | + <div class="node-skill">debugging-toolkit</div> | ||
| 152 | + </div> | ||
| 153 | + </div> | ||
| 154 | + | ||
| 155 | + <div class="work-row"> | ||
| 156 | + <div class="work-node status-pending" data-node-exec-id="node-005" | ||
| 157 | + onmouseover="showTip(event, 'node-005', 'skill: software-planning-execution', 'output_schema_ref: test_report.schema.json', 'retry_count: 0')" | ||
| 158 | + onmouseout="hideTip()"> | ||
| 159 | + <div class="node-id">fan-in:merge</div> | ||
| 160 | + <div class="node-skill">等待上游节点全部完成</div> | ||
| 161 | + </div> | ||
| 162 | + </div> | ||
| 163 | + </div> | ||
| 164 | + | ||
| 165 | + <div class="tooltip" id="tooltip"></div> | ||
| 166 | + | ||
| 167 | + <div class="legend" style="margin-top:16px;"> | ||
| 168 | + <div class="legend-item"><span class="legend-swatch" style="border-color:var(--status-pending)"></span>pending</div> | ||
| 169 | + <div class="legend-item"><span class="legend-swatch" style="border-color:var(--status-running)"></span>running</div> | ||
| 170 | + <div class="legend-item"><span class="legend-swatch" style="border-color:var(--status-reflecting)"></span>reflecting / retrying</div> | ||
| 171 | + <div class="legend-item"><span class="legend-swatch" style="border-color:var(--status-waiting-quota)"></span>waiting_for_quota(信息态,非告警,见 03-C 3.6 节)</div> | ||
| 172 | + <div class="legend-item"><span class="legend-swatch" style="border-color:var(--status-blocked)"></span>blocked / failed</div> | ||
| 173 | + <div class="legend-item"><span class="legend-swatch" style="border-color:var(--status-done)"></span>done</div> | ||
| 174 | + </div> | ||
| 175 | +</section> | ||
| 176 | + | ||
| 177 | +<script> | ||
| 178 | + // 真实实现里这段逻辑对应 hover_node 事件 -> c_work_node_get(node_exec_id) | ||
| 179 | + // 这里用 data-* 属性里预置的 mock 文案代替真实接口返回 | ||
| 180 | + function showTip(evt, nodeId, ...lines) { | ||
| 181 | + var tip = document.getElementById('tooltip'); | ||
| 182 | + tip.innerHTML = '<b>' + nodeId + '</b><br>' + lines.join('<br>'); | ||
| 183 | + tip.style.left = (evt.pageX + 12) + 'px'; | ||
| 184 | + tip.style.top = (evt.pageY + 12) + 'px'; | ||
| 185 | + tip.classList.add('show'); | ||
| 186 | + } | ||
| 187 | + function hideTip() { | ||
| 188 | + document.getElementById('tooltip').classList.remove('show'); | ||
| 189 | + } | ||
| 190 | +</script> | ||
| 191 | + | ||
| 192 | +</body> | ||
| 193 | +</html> |
docs/wireframes/v_log_retention_settings.html
0 → 100644
| 1 | +++ a/docs/wireframes/v_log_retention_settings.html | ||
| 1 | +<!DOCTYPE html> | ||
| 2 | +<!-- v_log_retention_settings — 静态原型,对应 02-V §2.13,路由 /settings/log-retention --> | ||
| 3 | +<html lang="zh"> | ||
| 4 | +<head> | ||
| 5 | +<meta charset="UTF-8"> | ||
| 6 | +<title>graphlemon · 日志保留策略</title> | ||
| 7 | +<style> | ||
| 8 | +:root { --brand:#4f46e5; --border:#e5e7eb; --text:#111827; --text-muted:#6b7280; --bg:#fff; --bg-panel:#f9fafb; } | ||
| 9 | +* { box-sizing: border-box; } | ||
| 10 | +body { font-family:-apple-system,sans-serif; margin:0; color:var(--text); background:var(--bg); } | ||
| 11 | +.proto-banner { background:#fef3c7; border-bottom:1px solid #f59e0b; padding:6px 20px; font-size:12px; } | ||
| 12 | +.app-nav { display:flex; align-items:center; gap:4px; padding:0 20px; height:52px; border-bottom:1px solid var(--border); position:sticky; top:0; background:#fff; z-index:10;} | ||
| 13 | +.app-nav .brand { font-weight:700; margin-right:20px; color:var(--brand); } | ||
| 14 | +.app-nav a.nav-link { padding:8px 12px; border-radius:6px; color:var(--text); text-decoration:none; font-size:14px; } | ||
| 15 | +.app-nav a.nav-link:hover { background:var(--bg-panel); color:var(--brand); } | ||
| 16 | +.app-nav .nav-dropdown { position:relative; } | ||
| 17 | +.app-nav .nav-dropdown > span { padding:8px 12px; border-radius:6px; font-size:14px; cursor:pointer; display:inline-block; background:var(--bg-panel); color:var(--brand); } | ||
| 18 | +.app-nav .dropdown-menu { display:block; position:absolute; top:100%; left:0; background:#fff; border:1px solid var(--border); border-radius:8px; padding:6px; min-width:160px; box-shadow:0 4px 12px rgba(0,0,0,.08); } | ||
| 19 | +.app-nav .dropdown-menu a { display:block; padding:6px 10px; border-radius:4px; color:var(--text); text-decoration:none; font-size:13px; } | ||
| 20 | +.app-nav .dropdown-menu a.active { color:var(--brand); background:#eef2ff; } | ||
| 21 | +.app-nav .nav-project { margin-left:auto; font-size:13px; color:var(--text-muted); } | ||
| 22 | +.app-nav .nav-project a { color:var(--brand); text-decoration:none; margin-left:6px; } | ||
| 23 | +.page { padding:28px; max-width:520px; margin:20px auto 0; } | ||
| 24 | +h1 { font-size:20px; margin:0 0 4px; } | ||
| 25 | +.page-sub { color:var(--text-muted); font-size:13px; margin-bottom:24px; } | ||
| 26 | +.panel { border:1px solid var(--border); border-radius:10px; padding:20px; background:var(--bg-panel); } | ||
| 27 | +.field { margin-bottom:16px; } | ||
| 28 | +.field label { display:block; font-size:13px; font-weight:600; margin-bottom:6px; } | ||
| 29 | +.field input { width:160px; padding:8px 10px; border:1px solid var(--border); border-radius:6px; font-size:14px; } | ||
| 30 | +.field .hint { font-size:12px; color:var(--text-muted); margin-top:5px; } | ||
| 31 | +.usage { font-size:13px; margin-bottom:16px; } | ||
| 32 | +.btn { display:inline-block; padding:9px 16px; border-radius:6px; border:1px solid var(--border); background:#fff; font-size:14px; cursor:pointer; } | ||
| 33 | +.btn-primary { background:var(--brand); border-color:var(--brand); color:#fff; } | ||
| 34 | +</style> | ||
| 35 | +</head> | ||
| 36 | +<body> | ||
| 37 | +<div class="proto-banner">静态原型(mock 数据)· 对应 02-V §2.13 · c_log_retention_config_update</div> | ||
| 38 | +<nav class="app-nav"> | ||
| 39 | + <div class="brand">graphlemon</div> | ||
| 40 | + <a href="v_dashboard.html" class="nav-link">仪表盘</a> | ||
| 41 | + <a href="v_task_board.html" class="nav-link">任务</a> | ||
| 42 | + <a href="v_artifact_browser.html" class="nav-link">产物</a> | ||
| 43 | + <a href="v_skill_library.html" class="nav-link">技能库</a> | ||
| 44 | + <div class="nav-dropdown"><span>设置</span> | ||
| 45 | + <div class="dropdown-menu"> | ||
| 46 | + <a href="v_concurrency_settings.html">并发数</a> | ||
| 47 | + <a href="v_budget_settings.html">预算护栏</a> | ||
| 48 | + <a href="v_log_retention_settings.html" class="active">日志保留</a> | ||
| 49 | + </div> | ||
| 50 | + </div> | ||
| 51 | + <div class="nav-project">当前项目:snake-game <a href="v_project_setup.html">切换</a></div> | ||
| 52 | +</nav> | ||
| 53 | + | ||
| 54 | +<div class="page"> | ||
| 55 | + <h1>日志保留策略配置</h1> | ||
| 56 | + <div class="page-sub">/settings/log-retention</div> | ||
| 57 | + <div class="panel"> | ||
| 58 | + <div class="usage">当前日志总占用:1.8 GB</div> | ||
| 59 | + <div class="field"> | ||
| 60 | + <label>保留天数(retention_days,>0)</label> | ||
| 61 | + <input type="number" value="14"> | ||
| 62 | + </div> | ||
| 63 | + <div class="field"> | ||
| 64 | + <label>总大小上限(max_total_size_mb,非必填)</label> | ||
| 65 | + <input type="number" placeholder="不限制"> | ||
| 66 | + <div class="hint">超过该天数的 closed 状态日志会被清理任务标记 purged 并删除物理文件;清理任务触发频率本身不可配</div> | ||
| 67 | + </div> | ||
| 68 | + <button class="btn btn-primary">保存</button> | ||
| 69 | + </div> | ||
| 70 | +</div> | ||
| 71 | +</body> | ||
| 72 | +</html> |
docs/wireframes/v_project_setup.html
0 → 100644
| 1 | +++ a/docs/wireframes/v_project_setup.html | ||
| 1 | +<!DOCTYPE html> | ||
| 2 | +<!-- | ||
| 3 | + v_project_setup — 静态原型(mock 数据,无构建步骤) | ||
| 4 | + 对应:02-V-界面与交互.md §2.1,路由 /setup | ||
| 5 | + 说明:首次使用 / 切换项目的入口,不在常驻导航栏里,从任意页面右上角"切换项目"进入, | ||
| 6 | + 所以本页顶部不放完整导航,只放一个返回链接。 | ||
| 7 | +--> | ||
| 8 | +<html lang="zh"> | ||
| 9 | +<head> | ||
| 10 | +<meta charset="UTF-8"> | ||
| 11 | +<title>graphlemon · 项目创建</title> | ||
| 12 | +<style> | ||
| 13 | +:root { --brand:#4f46e5; --border:#e5e7eb; --text:#111827; --text-muted:#6b7280; --bg:#fff; --bg-panel:#f9fafb; } | ||
| 14 | +* { box-sizing: border-box; } | ||
| 15 | +body { font-family:-apple-system,sans-serif; margin:0; color:var(--text); background:var(--bg); } | ||
| 16 | +.proto-banner { background:#fef3c7; border-bottom:1px solid #f59e0b; padding:6px 20px; font-size:12px; } | ||
| 17 | +.top-back { padding:14px 20px; border-bottom:1px solid var(--border); font-size:13px; } | ||
| 18 | +.top-back a { color:var(--text-muted); text-decoration:none; } | ||
| 19 | +.page { padding:28px; max-width:560px; margin:60px auto 0; } | ||
| 20 | +.page h1 { font-size:22px; margin:0 0 4px; } | ||
| 21 | +.page .page-sub { color:var(--text-muted); font-size:13px; margin-bottom:28px; } | ||
| 22 | +.panel { border:1px solid var(--border); border-radius:10px; padding:24px; background:var(--bg-panel); } | ||
| 23 | +.field { margin-bottom:18px; } | ||
| 24 | +.field label { display:block; font-size:13px; font-weight:600; margin-bottom:6px; } | ||
| 25 | +.field input { width:100%; padding:9px 10px; border:1px solid var(--border); border-radius:6px; font-size:14px; } | ||
| 26 | +.field input[readonly] { background:#f3f4f6; color:var(--text-muted); } | ||
| 27 | +.field .hint { font-size:12px; color:var(--text-muted); margin-top:5px; } | ||
| 28 | +.btn { display:inline-block; padding:9px 16px; border-radius:6px; border:1px solid var(--border); background:#fff; font-size:14px; cursor:pointer; } | ||
| 29 | +.btn-primary { background:var(--brand); border-color:var(--brand); color:#fff; } | ||
| 30 | +.btn-row { display:flex; gap:10px; margin-top:22px; } | ||
| 31 | +.confirm-banner { background:#fffbeb; border:1px solid #f59e0b; border-radius:8px; padding:12px 14px; font-size:13px; margin-top:16px; } | ||
| 32 | +</style> | ||
| 33 | +</head> | ||
| 34 | +<body> | ||
| 35 | +<div class="proto-banner">静态原型(mock 数据)· 对应 02-V §2.1 · c_pick_folder / c_project_create</div> | ||
| 36 | +<div class="top-back"><a href="v_dashboard.html">← 返回仪表盘</a></div> | ||
| 37 | + | ||
| 38 | +<div class="page"> | ||
| 39 | + <h1>创建 / 切换项目</h1> | ||
| 40 | + <div class="page-sub">选择一个本地文件夹作为目标项目的代码空间</div> | ||
| 41 | + | ||
| 42 | + <div class="panel"> | ||
| 43 | + <div class="field"> | ||
| 44 | + <label>目标文件夹(folder_path,只读,由 c_pick_folder 回填)</label> | ||
| 45 | + <input type="text" readonly value="/Users/bboonnee/projects/snake-game"> | ||
| 46 | + <div class="hint">点击"选择文件夹"会由后端代为弹出系统原生对话框,不支持手动输入路径(除非返回 DIALOG_UNAVAILABLE)</div> | ||
| 47 | + </div> | ||
| 48 | + <button class="btn">📁 选择文件夹</button> | ||
| 49 | + | ||
| 50 | + <div class="field" style="margin-top:20px;"> | ||
| 51 | + <label>项目名称(project_name,非必填)</label> | ||
| 52 | + <input type="text" placeholder="留空则取文件夹名 snake-game"> | ||
| 53 | + </div> | ||
| 54 | + | ||
| 55 | + <!-- 非 Git 仓库时的确认态,见 02-V §2.1 提交流程第 2 步 --> | ||
| 56 | + <div class="confirm-banner"> | ||
| 57 | + 该文件夹还不是 Git 仓库,是否代为初始化(<code>git init</code>)?确认前不写入任何文件。 | ||
| 58 | + <div class="btn-row" style="margin-top:10px;"> | ||
| 59 | + <button class="btn">取消</button> | ||
| 60 | + <button class="btn btn-primary">确认初始化并创建项目</button> | ||
| 61 | + </div> | ||
| 62 | + </div> | ||
| 63 | + </div> | ||
| 64 | +</div> | ||
| 65 | +</body> | ||
| 66 | +</html> |
docs/wireframes/v_skill_audit_log.html
0 → 100644
| 1 | +++ a/docs/wireframes/v_skill_audit_log.html | ||
| 1 | +<!DOCTYPE html> | ||
| 2 | +<!-- v_skill_audit_log — 静态原型,对应 02-V §2.10,路由 /skills/:skill_id/audit --> | ||
| 3 | +<html lang="zh"> | ||
| 4 | +<head> | ||
| 5 | +<meta charset="UTF-8"> | ||
| 6 | +<title>graphlemon · Skill 审计日志</title> | ||
| 7 | +<style> | ||
| 8 | +:root { --brand:#4f46e5; --border:#e5e7eb; --text:#111827; --text-muted:#6b7280; --bg:#fff; --bg-panel:#f9fafb; } | ||
| 9 | +* { box-sizing: border-box; } | ||
| 10 | +body { font-family:-apple-system,sans-serif; margin:0; color:var(--text); background:var(--bg); } | ||
| 11 | +.proto-banner { background:#fef3c7; border-bottom:1px solid #f59e0b; padding:6px 20px; font-size:12px; } | ||
| 12 | +.app-nav { display:flex; align-items:center; gap:4px; padding:0 20px; height:52px; border-bottom:1px solid var(--border); position:sticky; top:0; background:#fff; z-index:10;} | ||
| 13 | +.app-nav .brand { font-weight:700; margin-right:20px; color:var(--brand); } | ||
| 14 | +.app-nav a.nav-link { padding:8px 12px; border-radius:6px; color:var(--text); text-decoration:none; font-size:14px; } | ||
| 15 | +.app-nav a.nav-link:hover, .app-nav a.nav-link.active { background:var(--bg-panel); color:var(--brand); } | ||
| 16 | +.app-nav .nav-dropdown { position:relative; } | ||
| 17 | +.app-nav .nav-dropdown > span { padding:8px 12px; border-radius:6px; font-size:14px; cursor:pointer; display:inline-block; } | ||
| 18 | +.app-nav .dropdown-menu { display:none; position:absolute; top:100%; left:0; background:#fff; border:1px solid var(--border); border-radius:8px; padding:6px; min-width:160px; box-shadow:0 4px 12px rgba(0,0,0,.08); } | ||
| 19 | +.app-nav .nav-dropdown:hover .dropdown-menu { display:block; } | ||
| 20 | +.app-nav .dropdown-menu a { display:block; padding:6px 10px; border-radius:4px; color:var(--text); text-decoration:none; font-size:13px; } | ||
| 21 | +.app-nav .nav-project { margin-left:auto; font-size:13px; color:var(--text-muted); } | ||
| 22 | +.app-nav .nav-project a { color:var(--brand); text-decoration:none; margin-left:6px; } | ||
| 23 | +.page { padding:24px; max-width:1000px; margin:0 auto; } | ||
| 24 | +.breadcrumb { font-size:13px; color:var(--text-muted); margin-bottom:6px; } | ||
| 25 | +.breadcrumb a { color:var(--text-muted); } | ||
| 26 | +h1 { font-size:20px; margin:0 0 16px; } | ||
| 27 | +.iteration { border:1px solid var(--border); border-radius:10px; padding:14px 16px; margin-bottom:12px; background:var(--bg-panel); } | ||
| 28 | +.iteration .top { display:flex; justify-content:space-between; font-size:13px; margin-bottom:6px; } | ||
| 29 | +.iteration .t { color:var(--text-muted); font-size:12px; } | ||
| 30 | +.iteration .summary { font-size:13px; margin-bottom:8px; } | ||
| 31 | +.iteration .actions { display:flex; gap:8px; } | ||
| 32 | +.btn { display:inline-block; padding:6px 12px; border-radius:6px; border:1px solid var(--border); background:#fff; font-size:12px; cursor:pointer; } | ||
| 33 | +</style> | ||
| 34 | +</head> | ||
| 35 | +<body> | ||
| 36 | +<div class="proto-banner">静态原型(mock 数据)· 对应 02-V §2.10 · click_diff / click_rollback → c_skill_rollback</div> | ||
| 37 | +<nav class="app-nav"> | ||
| 38 | + <div class="brand">graphlemon</div> | ||
| 39 | + <a href="v_dashboard.html" class="nav-link">仪表盘</a> | ||
| 40 | + <a href="v_task_board.html" class="nav-link">任务</a> | ||
| 41 | + <a href="v_artifact_browser.html" class="nav-link">产物</a> | ||
| 42 | + <a href="v_skill_library.html" class="nav-link active">技能库</a> | ||
| 43 | + <div class="nav-dropdown"><span>设置</span> | ||
| 44 | + <div class="dropdown-menu"> | ||
| 45 | + <a href="v_concurrency_settings.html">并发数</a> | ||
| 46 | + <a href="v_budget_settings.html">预算护栏</a> | ||
| 47 | + <a href="v_log_retention_settings.html">日志保留</a> | ||
| 48 | + </div> | ||
| 49 | + </div> | ||
| 50 | + <div class="nav-project">当前项目:snake-game <a href="v_project_setup.html">切换</a></div> | ||
| 51 | +</nav> | ||
| 52 | + | ||
| 53 | +<div class="page"> | ||
| 54 | + <div class="breadcrumb"><a href="v_skill_library.html">技能库</a> / debugging-toolkit</div> | ||
| 55 | + <h1>debugging-toolkit 变更审计</h1> | ||
| 56 | + | ||
| 57 | + <div class="iteration"> | ||
| 58 | + <div class="top"><b>迭代 #3</b><span class="t">applied_at: 2 天前</span></div> | ||
| 59 | + <div class="summary">triggered_by_bug_id: bug-2291 · 补充"栈溢出类异常"的定位步骤</div> | ||
| 60 | + <div class="actions"> | ||
| 61 | + <button class="btn">查看 diff</button> | ||
| 62 | + <button class="btn">回滚到此版本前</button> | ||
| 63 | + </div> | ||
| 64 | + </div> | ||
| 65 | + <div class="iteration"> | ||
| 66 | + <div class="top"><b>迭代 #2</b><span class="t">applied_at: 5 天前</span></div> | ||
| 67 | + <div class="summary">triggered_by_bug_id: bug-1987 · 修正 pytest 相关描述(本项目实际用 unittest)</div> | ||
| 68 | + <div class="actions"> | ||
| 69 | + <button class="btn">查看 diff</button> | ||
| 70 | + <button class="btn">回滚到此版本前</button> | ||
| 71 | + </div> | ||
| 72 | + </div> | ||
| 73 | +</div> | ||
| 74 | +</body> | ||
| 75 | +</html> |
docs/wireframes/v_skill_library.html
0 → 100644
| 1 | +++ a/docs/wireframes/v_skill_library.html | ||
| 1 | +<!DOCTYPE html> | ||
| 2 | +<!-- v_skill_library — 静态原型,对应 02-V §2.9,路由 /skills --> | ||
| 3 | +<html lang="zh"> | ||
| 4 | +<head> | ||
| 5 | +<meta charset="UTF-8"> | ||
| 6 | +<title>graphlemon · 技能库</title> | ||
| 7 | +<style> | ||
| 8 | +:root { --brand:#4f46e5; --border:#e5e7eb; --text:#111827; --text-muted:#6b7280; --bg:#fff; --bg-panel:#f9fafb; } | ||
| 9 | +* { box-sizing: border-box; } | ||
| 10 | +body { font-family:-apple-system,sans-serif; margin:0; color:var(--text); background:var(--bg); } | ||
| 11 | +.proto-banner { background:#fef3c7; border-bottom:1px solid #f59e0b; padding:6px 20px; font-size:12px; } | ||
| 12 | +.app-nav { display:flex; align-items:center; gap:4px; padding:0 20px; height:52px; border-bottom:1px solid var(--border); position:sticky; top:0; background:#fff; z-index:10;} | ||
| 13 | +.app-nav .brand { font-weight:700; margin-right:20px; color:var(--brand); } | ||
| 14 | +.app-nav a.nav-link { padding:8px 12px; border-radius:6px; color:var(--text); text-decoration:none; font-size:14px; } | ||
| 15 | +.app-nav a.nav-link:hover, .app-nav a.nav-link.active { background:var(--bg-panel); color:var(--brand); } | ||
| 16 | +.app-nav .nav-dropdown { position:relative; } | ||
| 17 | +.app-nav .nav-dropdown > span { padding:8px 12px; border-radius:6px; font-size:14px; cursor:pointer; display:inline-block; } | ||
| 18 | +.app-nav .dropdown-menu { display:none; position:absolute; top:100%; left:0; background:#fff; border:1px solid var(--border); border-radius:8px; padding:6px; min-width:160px; box-shadow:0 4px 12px rgba(0,0,0,.08); } | ||
| 19 | +.app-nav .nav-dropdown:hover .dropdown-menu { display:block; } | ||
| 20 | +.app-nav .dropdown-menu a { display:block; padding:6px 10px; border-radius:4px; color:var(--text); text-decoration:none; font-size:13px; } | ||
| 21 | +.app-nav .nav-project { margin-left:auto; font-size:13px; color:var(--text-muted); } | ||
| 22 | +.app-nav .nav-project a { color:var(--brand); text-decoration:none; margin-left:6px; } | ||
| 23 | +.page { padding:24px; max-width:1000px; margin:0 auto; } | ||
| 24 | +h1 { font-size:20px; margin:0 0 16px; } | ||
| 25 | +.group { margin-bottom:22px; } | ||
| 26 | +.group h2 { font-size:13px; text-transform:uppercase; letter-spacing:.04em; color:var(--text-muted); margin:0 0 10px; } | ||
| 27 | +.skill-list { border:1px solid var(--border); border-radius:10px; overflow:hidden; } | ||
| 28 | +.skill-row { display:flex; justify-content:space-between; align-items:center; padding:12px 16px; font-size:13px; border-bottom:1px solid var(--border); cursor:pointer; } | ||
| 29 | +.skill-row:last-child { border-bottom:none; } | ||
| 30 | +.skill-row:hover { background:var(--bg-panel); } | ||
| 31 | +.skill-row .name { font-weight:600; } | ||
| 32 | +.tag { font-size:11px; padding:2px 8px; border-radius:999px; background:#eef2ff; color:var(--brand); } | ||
| 33 | +.tag.auto { background:#fef3c7; color:#92400e; } | ||
| 34 | +</style> | ||
| 35 | +</head> | ||
| 36 | +<body> | ||
| 37 | +<div class="proto-banner">静态原型(mock 数据)· 对应 02-V §2.9 · click_skill → v_skill_audit_log</div> | ||
| 38 | +<nav class="app-nav"> | ||
| 39 | + <div class="brand">graphlemon</div> | ||
| 40 | + <a href="v_dashboard.html" class="nav-link">仪表盘</a> | ||
| 41 | + <a href="v_task_board.html" class="nav-link">任务</a> | ||
| 42 | + <a href="v_artifact_browser.html" class="nav-link">产物</a> | ||
| 43 | + <a href="v_skill_library.html" class="nav-link active">技能库</a> | ||
| 44 | + <div class="nav-dropdown"><span>设置</span> | ||
| 45 | + <div class="dropdown-menu"> | ||
| 46 | + <a href="v_concurrency_settings.html">并发数</a> | ||
| 47 | + <a href="v_budget_settings.html">预算护栏</a> | ||
| 48 | + <a href="v_log_retention_settings.html">日志保留</a> | ||
| 49 | + </div> | ||
| 50 | + </div> | ||
| 51 | + <div class="nav-project">当前项目:snake-game <a href="v_project_setup.html">切换</a></div> | ||
| 52 | +</nav> | ||
| 53 | + | ||
| 54 | +<div class="page"> | ||
| 55 | + <h1>技能库</h1> | ||
| 56 | + | ||
| 57 | + <div class="group"> | ||
| 58 | + <h2>Planner</h2> | ||
| 59 | + <div class="skill-list"> | ||
| 60 | + <div class="skill-row" onclick="location.href='v_skill_audit_log.html'"><span class="name">project-sdd-planning</span><span class="tag">migrated_from_hermes</span></div> | ||
| 61 | + </div> | ||
| 62 | + </div> | ||
| 63 | + | ||
| 64 | + <div class="group"> | ||
| 65 | + <h2>Coder</h2> | ||
| 66 | + <div class="skill-list"> | ||
| 67 | + <div class="skill-row" onclick="location.href='v_skill_audit_log.html'"><span class="name">code-task-development</span><span class="tag">migrated_from_hermes</span></div> | ||
| 68 | + <div class="skill-row" onclick="location.href='v_skill_audit_log.html'"><span class="name">debugging-toolkit</span><span class="tag auto">auto_iterated</span></div> | ||
| 69 | + </div> | ||
| 70 | + </div> | ||
| 71 | + | ||
| 72 | + <div class="group"> | ||
| 73 | + <h2>Validator</h2> | ||
| 74 | + <div class="skill-list"> | ||
| 75 | + <div class="skill-row" onclick="location.href='v_skill_audit_log.html'"><span class="name">qa-task-validation</span><span class="tag">migrated_from_hermes</span></div> | ||
| 76 | + </div> | ||
| 77 | + </div> | ||
| 78 | +</div> | ||
| 79 | +</body> | ||
| 80 | +</html> |
docs/wireframes/v_task_board.html
0 → 100644
| 1 | +++ a/docs/wireframes/v_task_board.html | ||
| 1 | +<!DOCTYPE html> | ||
| 2 | +<!-- v_task_board — 静态原型,对应 02-V §2.5,路由 /tasks --> | ||
| 3 | +<html lang="zh"> | ||
| 4 | +<head> | ||
| 5 | +<meta charset="UTF-8"> | ||
| 6 | +<title>graphlemon · 任务看板</title> | ||
| 7 | +<style> | ||
| 8 | +:root { --brand:#4f46e5; --border:#e5e7eb; --text:#111827; --text-muted:#6b7280; --bg:#fff; --bg-panel:#f9fafb; | ||
| 9 | + --status-pending:#9ca3af; --status-running:#3b82f6; --status-reflecting:#eab308; --status-waiting-quota:#7dd3fc; --status-blocked:#ef4444; --status-done:#22c55e; } | ||
| 10 | +* { box-sizing: border-box; } | ||
| 11 | +body { font-family:-apple-system,sans-serif; margin:0; color:var(--text); background:var(--bg); } | ||
| 12 | +.proto-banner { background:#fef3c7; border-bottom:1px solid #f59e0b; padding:6px 20px; font-size:12px; } | ||
| 13 | +.app-nav { display:flex; align-items:center; gap:4px; padding:0 20px; height:52px; border-bottom:1px solid var(--border); position:sticky; top:0; background:#fff; z-index:10;} | ||
| 14 | +.app-nav .brand { font-weight:700; margin-right:20px; color:var(--brand); } | ||
| 15 | +.app-nav a.nav-link { padding:8px 12px; border-radius:6px; color:var(--text); text-decoration:none; font-size:14px; } | ||
| 16 | +.app-nav a.nav-link:hover, .app-nav a.nav-link.active { background:var(--bg-panel); color:var(--brand); } | ||
| 17 | +.app-nav .nav-dropdown { position:relative; } | ||
| 18 | +.app-nav .nav-dropdown > span { padding:8px 12px; border-radius:6px; font-size:14px; cursor:pointer; display:inline-block; } | ||
| 19 | +.app-nav .dropdown-menu { display:none; position:absolute; top:100%; left:0; background:#fff; border:1px solid var(--border); border-radius:8px; padding:6px; min-width:160px; box-shadow:0 4px 12px rgba(0,0,0,.08); } | ||
| 20 | +.app-nav .nav-dropdown:hover .dropdown-menu { display:block; } | ||
| 21 | +.app-nav .dropdown-menu a { display:block; padding:6px 10px; border-radius:4px; color:var(--text); text-decoration:none; font-size:13px; } | ||
| 22 | +.app-nav .nav-project { margin-left:auto; font-size:13px; color:var(--text-muted); } | ||
| 23 | +.app-nav .nav-project a { color:var(--brand); text-decoration:none; margin-left:6px; } | ||
| 24 | +.page { padding:24px; max-width:1100px; margin:0 auto; } | ||
| 25 | +.page h1 { font-size:20px; margin:0 0 16px; } | ||
| 26 | +.filters { display:flex; gap:10px; margin-bottom:16px; } | ||
| 27 | +.filters input, .filters select { padding:8px 10px; border:1px solid var(--border); border-radius:6px; font-size:13px; } | ||
| 28 | +table.data-table { width:100%; border-collapse:collapse; font-size:13px; } | ||
| 29 | +table.data-table th { text-align:left; padding:8px 10px; border-bottom:2px solid var(--border); color:var(--text-muted); font-weight:600; } | ||
| 30 | +table.data-table td { padding:10px; border-bottom:1px solid var(--border); } | ||
| 31 | +table.data-table tr:hover td { background:var(--bg-panel); cursor:pointer; } | ||
| 32 | +.badge { display:inline-block; padding:2px 9px; border-radius:999px; font-size:11px; font-weight:600; color:#fff; } | ||
| 33 | +.badge.done { background:var(--status-done); } | ||
| 34 | +.badge.coding, .badge.validating { background:var(--status-running); } | ||
| 35 | +.badge.blocked_pending_human { background:var(--status-blocked); } | ||
| 36 | +.badge.queued { background:var(--status-pending); } | ||
| 37 | +a.row-link { color:inherit; text-decoration:none; display:contents; } | ||
| 38 | +</style> | ||
| 39 | +</head> | ||
| 40 | +<body> | ||
| 41 | +<div class="proto-banner">静态原型(mock 数据)· 对应 02-V §2.5 · c_task_list / click_row → v_task_detail</div> | ||
| 42 | +<nav class="app-nav"> | ||
| 43 | + <div class="brand">graphlemon</div> | ||
| 44 | + <a href="v_dashboard.html" class="nav-link">仪表盘</a> | ||
| 45 | + <a href="v_task_board.html" class="nav-link active">任务</a> | ||
| 46 | + <a href="v_artifact_browser.html" class="nav-link">产物</a> | ||
| 47 | + <a href="v_skill_library.html" class="nav-link">技能库</a> | ||
| 48 | + <div class="nav-dropdown"><span>设置</span> | ||
| 49 | + <div class="dropdown-menu"> | ||
| 50 | + <a href="v_concurrency_settings.html">并发数</a> | ||
| 51 | + <a href="v_budget_settings.html">预算护栏</a> | ||
| 52 | + <a href="v_log_retention_settings.html">日志保留</a> | ||
| 53 | + </div> | ||
| 54 | + </div> | ||
| 55 | + <div class="nav-project">当前项目:snake-game <a href="v_project_setup.html">切换</a></div> | ||
| 56 | +</nav> | ||
| 57 | + | ||
| 58 | +<div class="page"> | ||
| 59 | + <h1>任务看板</h1> | ||
| 60 | + <div class="filters"> | ||
| 61 | + <input type="text" placeholder="搜索 goal_text(keyword,maxLen 128)"> | ||
| 62 | + <select><option>全部状态(status_filter)</option><option>queued</option><option>coding</option><option>blocked_pending_human</option></select> | ||
| 63 | + <select><option>全部 Org 节点(org_node_filter)</option><option>planner</option><option>coder</option><option>validator</option></select> | ||
| 64 | + </div> | ||
| 65 | + <table class="data-table"> | ||
| 66 | + <thead><tr><th>task_id</th><th>goal_text</th><th>status</th><th>assigned_org_node</th><th>loop_round</th><th>updated_at</th></tr></thead> | ||
| 67 | + <tbody> | ||
| 68 | + <tr onclick="location.href='v_task_detail.html'"><td>task-8f21</td><td>实现贪吃蛇游戏</td><td><span class="badge coding">coding</span></td><td>coder</td><td>0</td><td>刚刚</td></tr> | ||
| 69 | + <tr onclick="location.href='v_task_detail.html'"><td>task-7a02</td><td>修复撞墙判定 off-by-one</td><td><span class="badge blocked_pending_human">blocked_pending_human</span></td><td>coder</td><td>3</td><td>1 小时前</td></tr> | ||
| 70 | + <tr onclick="location.href='v_task_detail.html'"><td>task-6c11</td><td>补充计分板 UI</td><td><span class="badge done">done</span></td><td>validator</td><td>0</td><td>昨天</td></tr> | ||
| 71 | + <tr onclick="location.href='v_task_detail.html'"><td>task-5b90</td><td>PRD 澄清:暂停/继续操作</td><td><span class="badge queued">queued</span></td><td>planner</td><td>0</td><td>2 天前</td></tr> | ||
| 72 | + </tbody> | ||
| 73 | + </table> | ||
| 74 | +</div> | ||
| 75 | +</body> | ||
| 76 | +</html> |
docs/wireframes/v_task_detail.html
0 → 100644
| 1 | +++ a/docs/wireframes/v_task_detail.html | ||
| 1 | +<!DOCTYPE html> | ||
| 2 | +<!-- | ||
| 3 | + v_task_detail — 静态原型,对应 02-V §2.6,路由 /tasks/:task_id | ||
| 4 | + 内嵌 v_graph_visualizer(§2.7,详细独立示范见 v_graph_visualizer.html)+ v_log_viewer(§2.7.1) | ||
| 5 | + 这是"检查过程"诉求的核心落地页面,多个子模块嵌套,值得完整画出。 | ||
| 6 | +--> | ||
| 7 | +<html lang="zh"> | ||
| 8 | +<head> | ||
| 9 | +<meta charset="UTF-8"> | ||
| 10 | +<title>graphlemon · 任务详情</title> | ||
| 11 | +<style> | ||
| 12 | +:root { --brand:#4f46e5; --border:#e5e7eb; --text:#111827; --text-muted:#6b7280; --bg:#fff; --bg-panel:#f9fafb; | ||
| 13 | + --status-pending:#9ca3af; --status-running:#3b82f6; --status-reflecting:#eab308; --status-waiting-quota:#7dd3fc; --status-blocked:#ef4444; --status-done:#22c55e; } | ||
| 14 | +* { box-sizing: border-box; } | ||
| 15 | +body { font-family:-apple-system,sans-serif; margin:0; color:var(--text); background:var(--bg); } | ||
| 16 | +.proto-banner { background:#fef3c7; border-bottom:1px solid #f59e0b; padding:6px 20px; font-size:12px; } | ||
| 17 | +.app-nav { display:flex; align-items:center; gap:4px; padding:0 20px; height:52px; border-bottom:1px solid var(--border); position:sticky; top:0; background:#fff; z-index:10;} | ||
| 18 | +.app-nav .brand { font-weight:700; margin-right:20px; color:var(--brand); } | ||
| 19 | +.app-nav a.nav-link { padding:8px 12px; border-radius:6px; color:var(--text); text-decoration:none; font-size:14px; } | ||
| 20 | +.app-nav a.nav-link:hover, .app-nav a.nav-link.active { background:var(--bg-panel); color:var(--brand); } | ||
| 21 | +.app-nav .nav-dropdown { position:relative; } | ||
| 22 | +.app-nav .nav-dropdown > span { padding:8px 12px; border-radius:6px; font-size:14px; cursor:pointer; display:inline-block; } | ||
| 23 | +.app-nav .dropdown-menu { display:none; position:absolute; top:100%; left:0; background:#fff; border:1px solid var(--border); border-radius:8px; padding:6px; min-width:160px; box-shadow:0 4px 12px rgba(0,0,0,.08); } | ||
| 24 | +.app-nav .nav-dropdown:hover .dropdown-menu { display:block; } | ||
| 25 | +.app-nav .dropdown-menu a { display:block; padding:6px 10px; border-radius:4px; color:var(--text); text-decoration:none; font-size:13px; } | ||
| 26 | +.app-nav .nav-project { margin-left:auto; font-size:13px; color:var(--text-muted); } | ||
| 27 | +.app-nav .nav-project a { color:var(--brand); text-decoration:none; margin-left:6px; } | ||
| 28 | +.page { padding:24px; max-width:1200px; margin:0 auto; } | ||
| 29 | +.breadcrumb { font-size:13px; color:var(--text-muted); margin-bottom:6px; } | ||
| 30 | +.breadcrumb a { color:var(--text-muted); } | ||
| 31 | +h1 { font-size:20px; margin:0 0 4px; } | ||
| 32 | +.alert-banner { background:#fef2f2; border:1px solid #fecaca; color:#991b1b; border-radius:8px; padding:12px 14px; font-size:13px; margin:14px 0; display:flex; justify-content:space-between; align-items:center; } | ||
| 33 | +.btn { display:inline-block; padding:7px 14px; border-radius:6px; border:1px solid var(--border); background:#fff; font-size:13px; cursor:pointer; text-decoration:none; color:var(--text); } | ||
| 34 | +.btn-danger { background:#dc2626; border-color:#dc2626; color:#fff; } | ||
| 35 | +.panel { border:1px solid var(--border); border-radius:10px; padding:18px; background:var(--bg-panel); margin:16px 0; } | ||
| 36 | +.panel h2 { font-size:13px; text-transform:uppercase; letter-spacing:.04em; color:var(--text-muted); margin:0 0 14px; } | ||
| 37 | +.meta-row { display:flex; gap:28px; font-size:13px; margin-bottom:6px; } | ||
| 38 | +.meta-row b { display:block; font-size:11px; color:var(--text-muted); font-weight:600; } | ||
| 39 | + | ||
| 40 | +/* Org Graph 条 */ | ||
| 41 | +.org-strip { display:flex; align-items:center; gap:0; margin-bottom:6px; } | ||
| 42 | +.org-node { border:2px solid var(--border); border-radius:8px; padding:8px 16px; text-align:center; background:#fff; flex:1; font-size:12px; } | ||
| 43 | +.org-node.active { border-color:var(--status-running); background:#eff6ff; } | ||
| 44 | +.org-arrow { flex:0 0 24px; text-align:center; color:var(--text-muted); } | ||
| 45 | + | ||
| 46 | +.see-more { font-size:12px; } | ||
| 47 | +.see-more a { color:var(--brand); } | ||
| 48 | + | ||
| 49 | +/* to-do 树(新增,2026-07-30,对应 01-M work_graph_run.graph_manifest 补充说明 + 02-V §2.6) | ||
| 50 | + graph_manifest 本身就是 to-do list:树的节点 = graph_manifest 的节点,缩进 = fan-out/fan-in 依赖分组。 | ||
| 51 | + 这棵树只属于「这一个 task 的这一次 coder work_graph_run」,不是多个 task 组成的树。 */ | ||
| 52 | +.todo-tree-label { font-size:11px; color:var(--text-muted); margin-bottom:8px; } | ||
| 53 | +.todo-tree { list-style:none; margin:0 0 4px; padding:0; font-size:13px; } | ||
| 54 | +.todo-tree li { position:relative; padding:6px 0 6px 22px; border-left:1px dashed var(--border); margin-left:8px; } | ||
| 55 | +.todo-tree li:last-child { border-left-color:transparent; } | ||
| 56 | +.todo-tree li::before { content:""; position:absolute; left:0; top:16px; width:14px; height:1px; background:var(--border); } | ||
| 57 | +.todo-tree .todo-row { display:flex; align-items:center; gap:8px; cursor:pointer; } | ||
| 58 | +.todo-tree .dot { width:9px; height:9px; border-radius:50%; border:2px solid var(--status-pending); flex:0 0 auto; } | ||
| 59 | +.todo-tree li.status-done .dot { border-color:var(--status-done); background:var(--status-done); } | ||
| 60 | +.todo-tree li.status-running .dot { border-color:var(--status-running); background:var(--status-running); } | ||
| 61 | +.todo-tree li.status-reflecting .dot { border-color:var(--status-reflecting); background:var(--status-reflecting); } | ||
| 62 | +.todo-tree li.status-waiting_for_quota .dot { border-color:var(--status-waiting-quota); background:var(--status-waiting-quota); } | ||
| 63 | +.todo-tree li.status-blocked .dot, .todo-tree li.status-failed .dot { border-color:var(--status-blocked); background:var(--status-blocked); } | ||
| 64 | +.todo-tree .todo-id { font-weight:600; } | ||
| 65 | +.todo-tree .todo-skill { color:var(--text-muted); font-size:11px; } | ||
| 66 | +.todo-tree .todo-status { margin-left:auto; font-size:11px; color:var(--text-muted); } | ||
| 67 | +.todo-tree ul { list-style:none; margin:2px 0 0; padding:0; } | ||
| 68 | +.todo-detail { display:none; margin:6px 0 0 17px; padding:8px 10px; background:#fff; border:1px solid var(--border); border-radius:6px; font-size:12px; color:var(--text-muted); } | ||
| 69 | +.todo-detail.open { display:block; } | ||
| 70 | +.todo-detail a { color:var(--brand); text-decoration:none; } | ||
| 71 | + | ||
| 72 | +/* Artifact / bug list */ | ||
| 73 | +ul.simple-list { list-style:none; margin:0; padding:0; font-size:13px; } | ||
| 74 | +ul.simple-list li { padding:8px 0; border-bottom:1px solid var(--border); display:flex; justify-content:space-between; } | ||
| 75 | +ul.simple-list li:last-child { border-bottom:none; } | ||
| 76 | +ul.simple-list a { color:var(--brand); text-decoration:none; } | ||
| 77 | + | ||
| 78 | +/* 日志查看器 v_log_viewer,§2.7.1 */ | ||
| 79 | +.log-viewer { background:#0b1120; color:#d1d5db; font-family:ui-monospace,monospace; font-size:12px; border-radius:8px; padding:14px; max-height:220px; overflow:auto; } | ||
| 80 | +.log-viewer .line { white-space:pre-wrap; line-height:1.6; } | ||
| 81 | +.log-toolbar { display:flex; justify-content:space-between; align-items:center; margin-bottom:8px; font-size:12px; } | ||
| 82 | +.log-toolbar label { display:flex; align-items:center; gap:6px; color:var(--text-muted); } | ||
| 83 | +</style> | ||
| 84 | +</head> | ||
| 85 | +<body> | ||
| 86 | +<div class="proto-banner">静态原型(mock 数据)· 对应 02-V §2.6/§2.7/§2.7.1 · 内嵌 v_graph_visualizer + v_log_viewer</div> | ||
| 87 | +<nav class="app-nav"> | ||
| 88 | + <div class="brand">graphlemon</div> | ||
| 89 | + <a href="v_dashboard.html" class="nav-link">仪表盘</a> | ||
| 90 | + <a href="v_task_board.html" class="nav-link active">任务</a> | ||
| 91 | + <a href="v_artifact_browser.html" class="nav-link">产物</a> | ||
| 92 | + <a href="v_skill_library.html" class="nav-link">技能库</a> | ||
| 93 | + <div class="nav-dropdown"><span>设置</span> | ||
| 94 | + <div class="dropdown-menu"> | ||
| 95 | + <a href="v_concurrency_settings.html">并发数</a> | ||
| 96 | + <a href="v_budget_settings.html">预算护栏</a> | ||
| 97 | + <a href="v_log_retention_settings.html">日志保留</a> | ||
| 98 | + </div> | ||
| 99 | + </div> | ||
| 100 | + <div class="nav-project">当前项目:snake-game <a href="v_project_setup.html">切换</a></div> | ||
| 101 | +</nav> | ||
| 102 | + | ||
| 103 | +<div class="page"> | ||
| 104 | + <div class="breadcrumb"><a href="v_task_board.html">任务</a> / task-7a02</div> | ||
| 105 | + <h1>修复撞墙判定 off-by-one</h1> | ||
| 106 | + | ||
| 107 | + <!-- blocked_pending_human 醒目告警样式,见 02-V §2.6 异常态 --> | ||
| 108 | + <div class="alert-banner"> | ||
| 109 | + ⚠ 已达 loop_round 上限(3 轮),转 blocked_pending_human,需要人工干预 | ||
| 110 | + <a class="btn btn-danger" href="v_task_board.html">人工干预(manual_resume)</a> | ||
| 111 | + </div> | ||
| 112 | + | ||
| 113 | + <div class="meta-row"> | ||
| 114 | + <div><b>STATUS</b>blocked_pending_human</div> | ||
| 115 | + <div><b>LOOP_ROUND</b>3</div> | ||
| 116 | + <div><b>ASSIGNED_ORG_NODE</b>coder</div> | ||
| 117 | + <div><b>CREATED_VIA</b>human_console</div> | ||
| 118 | + </div> | ||
| 119 | + | ||
| 120 | + <div class="panel"> | ||
| 121 | + <h2>Org Graph(3 个固定节点,高亮当前所处节点)</h2> | ||
| 122 | + <div class="org-strip"> | ||
| 123 | + <div class="org-node active">Planner</div><div class="org-arrow">→</div> | ||
| 124 | + <div class="org-node active">Coder</div><div class="org-arrow">→</div> | ||
| 125 | + <div class="org-node">Validator</div> | ||
| 126 | + </div> | ||
| 127 | + </div> | ||
| 128 | + | ||
| 129 | + <!-- | ||
| 130 | + to-do 树(新增,2026-07-30)—— 对应 01-M work_graph_run.graph_manifest 的补充说明 + 02-V §2.6: | ||
| 131 | + 这棵树渲染的就是 org_node=coder 这一次 work_graph_run.graph_manifest 本身(即 to-do list), | ||
| 132 | + 不是新的实体、也不是多个 task 组成的树。缩进 = fan-out/fan-in 依赖分组,圆点颜色 = work_node_execution.status。 | ||
| 133 | + 层级:project → task(本页) → work_graph_run(org_node=coder) → 下面这棵 to-do 树 → work_node_execution → execution_log_file | ||
| 134 | + --> | ||
| 135 | + <div class="panel"> | ||
| 136 | + <h2>Coder Work Graph 的 to-do 树(本次 work_graph_run.graph_manifest,graph_manifest = to-do list)</h2> | ||
| 137 | + <div class="todo-tree-label">来源:work_graph_run(org_node=coder,run_id=run-coder-014)· 点击节点展开 work_node_execution 详情</div> | ||
| 138 | + <ul class="todo-tree"> | ||
| 139 | + <li class="status-done"> | ||
| 140 | + <div class="todo-row" onclick="toggleTodo('t-collision')"> | ||
| 141 | + <span class="dot"></span> | ||
| 142 | + <span class="todo-id">to-do: collision-check</span> | ||
| 143 | + <span class="todo-skill">skill: code-task-development</span> | ||
| 144 | + <span class="todo-status">done · node_exec_id=node-001</span> | ||
| 145 | + </div> | ||
| 146 | + <div class="todo-detail" id="t-collision"> | ||
| 147 | + input_schema_ref: sdd_module.schema.json · output_schema_ref: code_diff.schema.json · retry_count: 0<br> | ||
| 148 | + <a href="#" onclick="event.stopPropagation()">展开 v_log_viewer 原始日志 →</a> | ||
| 149 | + </div> | ||
| 150 | + </li> | ||
| 151 | + <li class="status-reflecting"> | ||
| 152 | + <div class="todo-row" onclick="toggleTodo('t-score')"> | ||
| 153 | + <span class="dot"></span> | ||
| 154 | + <span class="todo-id">to-do: score-board</span> | ||
| 155 | + <span class="todo-skill">skill: debugging-toolkit</span> | ||
| 156 | + <span class="todo-status">reflecting · node_exec_id=node-002 · retry 3/5</span> | ||
| 157 | + </div> | ||
| 158 | + <div class="todo-detail open" id="t-score"> | ||
| 159 | + reflection_notes: 撞墙判定用 > 而不是 >=,unit test 断言未通过;准备第 4 轮重试<br> | ||
| 160 | + <a href="#" onclick="event.stopPropagation()">展开 v_log_viewer 原始日志 →</a>(见下方,已展开) | ||
| 161 | + </div> | ||
| 162 | + <ul> | ||
| 163 | + <li class="status-pending"> | ||
| 164 | + <div class="todo-row" onclick="toggleTodo('t-score-retry')"> | ||
| 165 | + <span class="dot"></span> | ||
| 166 | + <span class="todo-id">依赖:等待 score-board 修复后再跑 integration test</span> | ||
| 167 | + <span class="todo-status">fan-in 等待中</span> | ||
| 168 | + </div> | ||
| 169 | + <div class="todo-detail" id="t-score-retry">这一项是 fan-in:integration-test 节点,等待 score-board 和 input-handler 都到达 done 才会开始。</div> | ||
| 170 | + </li> | ||
| 171 | + </ul> | ||
| 172 | + </li> | ||
| 173 | + <li class="status-waiting_for_quota"> | ||
| 174 | + <div class="todo-row" onclick="toggleTodo('t-input')"> | ||
| 175 | + <span class="dot"></span> | ||
| 176 | + <span class="todo-id">to-do: input-handler</span> | ||
| 177 | + <span class="todo-skill">skill: code-agent-engineering</span> | ||
| 178 | + <span class="todo-status">waiting_for_quota(自愈型,非告警)· 已等待 12 分钟</span> | ||
| 179 | + </div> | ||
| 180 | + <div class="todo-detail" id="t-input"> | ||
| 181 | + quota_limit_type: session · 退避重试中,不计入 retry_count(03-C §3.6)<br> | ||
| 182 | + <a href="#" onclick="event.stopPropagation()">展开 v_log_viewer 原始日志 →</a> | ||
| 183 | + </div> | ||
| 184 | + </li> | ||
| 185 | + </ul> | ||
| 186 | + <div class="see-more"><a href="v_graph_visualizer.html">查看按执行层级排布的 DAG 视图(v_graph_visualizer,同一份 graph_manifest 的另一种呈现)→</a></div> | ||
| 187 | + </div> | ||
| 188 | + | ||
| 189 | + <div class="panel"> | ||
| 190 | + <h2>关联 Artifact</h2> | ||
| 191 | + <ul class="simple-list"> | ||
| 192 | + <li><a href="v_artifact_browser.html">sdd_v2.md</a><span>产出于 planner work_graph_run</span></li> | ||
| 193 | + <li><a href="v_artifact_browser.html">code_diff_collision.patch</a><span>产出于 coder work_graph_run</span></li> | ||
| 194 | + </ul> | ||
| 195 | + </div> | ||
| 196 | + | ||
| 197 | + <div class="panel"> | ||
| 198 | + <h2>关联 Bug 记录</h2> | ||
| 199 | + <ul class="simple-list"> | ||
| 200 | + <li>root_cause_category = coder:撞墙判定用 > 而不是 >=<span>root_cause_detail 链接</span></li> | ||
| 201 | + </ul> | ||
| 202 | + </div> | ||
| 203 | + | ||
| 204 | + <div class="panel"> | ||
| 205 | + <h2>原始执行日志(v_log_viewer,node_exec_id = node-002)</h2> | ||
| 206 | + <div class="log-toolbar"> | ||
| 207 | + <label><input type="checkbox" checked> 自动滚动到最新</label> | ||
| 208 | + <a class="btn" href="#">下载完整日志</a> | ||
| 209 | + </div> | ||
| 210 | + <div class="log-viewer"> | ||
| 211 | + <div class="line">[12:04:01] Coder 认领 task-7a02,读取 SDD §4.2 撞墙判定规则</div> | ||
| 212 | + <div class="line">[12:04:03] 修改 game_logic.py: is_collision() 边界判断</div> | ||
| 213 | + <div class="line">[12:04:20] 运行 python3 -m unittest test_game_logic.py</div> | ||
| 214 | + <div class="line">[12:04:21] FAILED: test_wall_collision_off_by_one (仍未修复)</div> | ||
| 215 | + <div class="line">[12:04:22] reflection: 差距未闭合,准备第 4 轮重试……</div> | ||
| 216 | + </div> | ||
| 217 | + </div> | ||
| 218 | +</div> | ||
| 219 | +<script> | ||
| 220 | + // 真实实现里点击 to-do 行 = click_view_raw_log / 展开 work_node_execution 详情(02-V §2.6) | ||
| 221 | + function toggleTodo(id) { | ||
| 222 | + document.getElementById(id).classList.toggle('open'); | ||
| 223 | + } | ||
| 224 | +</script> | ||
| 225 | +</body> | ||
| 226 | +</html> |
spikes/a1-dynamic-workflows/README.md
0 → 100644
| 1 | +++ a/spikes/a1-dynamic-workflows/README.md | ||
| 1 | +# A-1 Spike: Programmatic Invocation of Claude Code Dynamic Workflows | ||
| 2 | + | ||
| 3 | +Tests the highest-risk assumption blocking Work Graph execution design | ||
| 4 | +(`docs/sdd/00-SDD-生成记录.md` A-1; `docs/graphlemon_architecture.md` §10.1; | ||
| 5 | +`docs/sdd/05-实施约束与依赖.md` §5.4/5.5). | ||
| 6 | + | ||
| 7 | +## What's already confirmed by official docs (no need to re-derive) | ||
| 8 | + | ||
| 9 | +From `code.claude.com/docs/en/workflows` and `code.claude.com/docs/en/headless`: | ||
| 10 | + | ||
| 11 | +- Dynamic workflows are real: JS scripts using `agent()` / `pipeline()` / `parallel()`, saved under `.claude/workflows/*.js`, invoked as `/<name>` with an `args` object. | ||
| 12 | +- They explicitly run in `claude -p` (headless) and the Agent SDK, not just interactively. | ||
| 13 | +- Permission table states plainly: for `claude -p` and the Agent SDK, the run "starts immediately" — never prompted. | ||
| 14 | +- `--output-format json` + `--json-schema` returns parseable `structured_output`, plus `total_cost_usd` / token usage. | ||
| 15 | +- Requires Claude Code **v2.1.154+**; on Pro, "Dynamic workflows" must be turned on once via `/config` in an interactive session. | ||
| 16 | + | ||
| 17 | +This resolves the doc-level question. What's *not* confirmed is whether it actually behaves this way end-to-end on a real account, with a real fan-out, unattended — that's what this spike runs. | ||
| 18 | + | ||
| 19 | +## What this spike actually tests | ||
| 20 | + | ||
| 21 | +1. **Unattended execution** — `claude -p ... < /dev/null` (no stdin available at all): does it complete without hanging on a prompt? | ||
| 22 | +2. **Real fan-out** — the workflow computes squares of 3 numbers via 3 separate `agent()` calls, reduced in script code. Correct output (`sum == 29`) is only possible if the runtime actually executed the graph, not just echoed something. | ||
| 23 | +3. **Structured, parseable output** — can the result be consumed programmatically (`structured_output.sum`), which is what `c_work_graph_execute`'s implementation would need to do? | ||
| 24 | +4. **`--bare` mode compatibility** — docs recommend `--bare` for scripted/SDK calls, but `--bare` skips auto-discovery of things like skills/MCP/hooks; it's untested whether it still finds `.claude/workflows/`. | ||
| 25 | + | ||
| 26 | +Note: I checked this sandbox — `claude` CLI v2.1.219 is present (well above the version requirement) but **not logged in**, so I can't run this spike myself. It needs to run on your machine, where `claude` is authenticated against your actual plan. | ||
| 27 | + | ||
| 28 | +## Prerequisites | ||
| 29 | + | ||
| 30 | +- Claude Code CLI installed and logged in (`claude /login`), v2.1.154+ | ||
| 31 | +- A plan with dynamic workflows available (Pro/Max/Team/Enterprise) — on Pro, enable it once via `/config` in an interactive `claude` session before running this | ||
| 32 | +- `python3` on PATH | ||
| 33 | + | ||
| 34 | +## Run it | ||
| 35 | + | ||
| 36 | +```bash | ||
| 37 | +cd spikes/a1-dynamic-workflows | ||
| 38 | +./run-spike.sh /path/to/any/git/repo | ||
| 39 | +``` | ||
| 40 | + | ||
| 41 | +Any git repo works as the target — it doesn't need to be graphlemon itself. The script copies `workflow-under-test.js` into `<target>/.claude/workflows/graphlemon-a1-spike.js`, runs it twice (normal and `--bare`), saves raw JSON results, and prints a pass/fail verdict. | ||
| 42 | + | ||
| 43 | +## Interpreting the result | ||
| 44 | + | ||
| 45 | +- **All Test 1 checks pass** → A-1 confirmed. Proceed with architecture.md §10.1 Option A. `check-results.py` prints the exact refinement to fold into `03-C-控制与接口.md` for `c_work_graph_execute` (spawn `claude -p` as a subprocess, parse `structured_output`). | ||
| 46 | +- **Test 1 fails** → A-1 not confirmed as tested. Check the printed `result` field for the actual error (commonly: workflows not enabled on the plan, wrong CLI version, not logged in). If it can't be resolved, treat A-1 as rejected — fall back to architecture.md §10.1 Option B (fully custom Graph Engine), which cascades into rewriting SDD §3.4-3.6 and §5.4/5.5. | ||
| 47 | +- **Test 2 (`--bare`) fails independently of Test 1** → not fatal to A-1 itself, just means `c_work_graph_execute` must invoke `claude -p` without `--bare` (slower cold start, but reliable workflow discovery). | ||
| 48 | + | ||
| 49 | +## After running | ||
| 50 | + | ||
| 51 | +Report back the printed verdict (and the two `result-test*.json` files if anything looks ambiguous) so A-1's status in `docs/sdd/00-SDD-生成记录.md` and `docs/sdd/sdd-mvcp-contract.json` can be updated from `pending` to `confirmed` or rejected, and 03-C can be refined with the concrete invocation contract. |
spikes/a1-dynamic-workflows/RESULTS.md
0 → 100644
| 1 | +++ a/spikes/a1-dynamic-workflows/RESULTS.md | ||
| 1 | +# A-1 Spike Results — CONFIRMED | ||
| 2 | + | ||
| 3 | +**Date**: 2026-07-29 | ||
| 4 | +**Run by**: bboonnee, real authenticated account | ||
| 5 | +**Cost**: ~$0.46 total | ||
| 6 | + | ||
| 7 | +## Verdict | ||
| 8 | + | ||
| 9 | +`check-results.py` verdict: **A-1 CONFIRMED** (5/5 Test 1 checks PASS). | ||
| 10 | + | ||
| 11 | +## Test 1 — normal `claude -p ... < /dev/null` | ||
| 12 | + | ||
| 13 | +- exit 0, 40s wall time, no interactive prompt, no hang | ||
| 14 | +- `structured_output`: `{"input_numbers":[2,3,4],"squares":[4,9,16],"sum":29}` — exact match, meaning the runtime genuinely executed the 3-way `agent()` fan-out plus the script-level reduce, not a simulated/echoed result | ||
| 15 | +- `permission_denials`: empty | ||
| 16 | +- `total_cost_usd`: 0.3817 (this call alone) | ||
| 17 | + | ||
| 18 | +Raw output: `result-test1.json` | ||
| 19 | + | ||
| 20 | +## Test 2 — `--bare` mode | ||
| 21 | + | ||
| 22 | +Failed, but not for the reason the original README hypothesized (workflow-discovery skip). Root cause: `--bare` skips credential/login-state discovery entirely — `result: "Not logged in · Please run /login"`. Confirmed independently with a trivial non-workflow prompt under `--bare` (same error), ruling out a workflow-specific bug. | ||
| 23 | + | ||
| 24 | +**Implication**: `c_work_graph_execute` must invoke `claude -p` **without** `--bare`. Slower cold start, but reliable. This is now the documented contract in `03-C-控制与接口.md` §3.5. | ||
| 25 | + | ||
| 26 | +Raw output: `result-test2-bare.json` | ||
| 27 | + | ||
| 28 | +## What changed in the SDD as a result | ||
| 29 | + | ||
| 30 | +- `docs/sdd/00-SDD-生成记录.md`: A-1 status `pending` → `confirmed` | ||
| 31 | +- `docs/sdd/03-C-控制与接口.md` §3.5: added the concrete, verified invocation contract for `c_work_graph_execute` (write workflow to target repo's `.claude/workflows/`, subprocess `claude -p` without `--bare`, parse `structured_output`, map `total_cost_usd`/`usage` into budget guardrails) | ||
| 32 | +- `docs/sdd/05-实施约束与依赖.md` §5.4/§5.5: risk resolved, spike step no longer needed before Work Graph implementation | ||
| 33 | +- `docs/sdd/sdd-mvcp-contract.json`: `assumptions[A-1].status` → `confirmed`, `risks[R-1].status` → `resolved` (schema-revalidated, still `VALID`) | ||
| 34 | +- `docs/graphlemon_architecture.md` §10.1: added confirmation note, version bumped to v0.10 | ||
| 35 | + | ||
| 36 | +A-2 and A-3 remain `pending` — not in scope of this spike. |
spikes/a1-dynamic-workflows/check-results.py
0 → 100644
| 1 | +++ a/spikes/a1-dynamic-workflows/check-results.py | ||
| 1 | +import json | ||
| 2 | +import sys | ||
| 3 | + | ||
| 4 | + | ||
| 5 | +def load(path): | ||
| 6 | + try: | ||
| 7 | + with open(path) as f: | ||
| 8 | + return json.load(f) | ||
| 9 | + except Exception as e: | ||
| 10 | + return {"is_error": True, "result": f"could not parse {path}: {e}"} | ||
| 11 | + | ||
| 12 | + | ||
| 13 | +def check_test1(d): | ||
| 14 | + structured = d.get("structured_output") or {} | ||
| 15 | + checks = { | ||
| 16 | + "ran_without_error": not d.get("is_error", True), | ||
| 17 | + "has_structured_output": isinstance(structured, dict) and "sum" in structured, | ||
| 18 | + "sum_correct (2^2+3^2+4^2=29)": structured.get("sum") == 29, | ||
| 19 | + "squares_correct ([4,9,16])": structured.get("squares") == [4, 9, 16], | ||
| 20 | + "no_permission_denials": len(d.get("permission_denials", [])) == 0, | ||
| 21 | + } | ||
| 22 | + return checks | ||
| 23 | + | ||
| 24 | + | ||
| 25 | +def main(): | ||
| 26 | + t1 = load(sys.argv[1]) | ||
| 27 | + t2 = load(sys.argv[2]) | ||
| 28 | + | ||
| 29 | + print("--- Test 1: normal `claude -p`, structured output ---") | ||
| 30 | + c1 = check_test1(t1) | ||
| 31 | + for k, v in c1.items(): | ||
| 32 | + print(f" [{'PASS' if v else 'FAIL'}] {k}") | ||
| 33 | + a1_holds = all(c1.values()) | ||
| 34 | + if not a1_holds: | ||
| 35 | + print(f" raw result field: {t1.get('result')}") | ||
| 36 | + | ||
| 37 | + print() | ||
| 38 | + print("--- Test 2: `claude --bare -p` ---") | ||
| 39 | + bare_ran = not t2.get("is_error", True) | ||
| 40 | + bare_structured = (t2.get("structured_output") or {}).get("sum") == 29 | ||
| 41 | + if bare_ran and bare_structured: | ||
| 42 | + print(" [PASS] bare mode also discovers and runs .claude/workflows/") | ||
| 43 | + print(" -> Executor can use --bare for scripted invocation") | ||
| 44 | + else: | ||
| 45 | + print(" [NOTE] bare mode did NOT successfully run the workflow") | ||
| 46 | + print(f" raw result field: {t2.get('result')}") | ||
| 47 | + print(" -> this is not necessarily fatal to A-1; it just means the") | ||
| 48 | + print(" Executor's c_work_graph_execute must invoke `claude -p`") | ||
| 49 | + print(" WITHOUT --bare (slower cold start, but full context/discovery)") | ||
| 50 | + | ||
| 51 | + print() | ||
| 52 | + print("=== A-1 VERDICT ===") | ||
| 53 | + if a1_holds: | ||
| 54 | + print("A-1 CONFIRMED: dynamic workflows can be invoked headlessly via `claude -p`,") | ||
| 55 | + print("run unattended (no interactive prompt, no stdin needed), and return") | ||
| 56 | + print("machine-parseable structured output. Proceed with architecture.md") | ||
| 57 | + print("§10.1 Option A (reuse Claude Code native dynamic workflows).") | ||
| 58 | + print() | ||
| 59 | + print("Refinement to fold into 03-C-控制与接口.md: invocation is prompt-mediated") | ||
| 60 | + print('(`claude -p "Run /<workflow-name> with args {...}"`), not a raw function') | ||
| 61 | + print("call. c_work_graph_execute's implementation should:") | ||
| 62 | + print(" 1. write/update the workflow .js file under the target repo's") | ||
| 63 | + print(" .claude/workflows/ (versioned in the target repo's own git history)") | ||
| 64 | + print(" 2. spawn `claude -p 'Run /<name> with args {...}' --output-format json") | ||
| 65 | + print(" --json-schema <contract-schema>` as a subprocess") | ||
| 66 | + print(" 3. parse structured_output from the JSON result") | ||
| 67 | + print(" 4. map total_cost_usd / usage from the result into budget_guardrail_config") | ||
| 68 | + else: | ||
| 69 | + print("A-1 NOT CONFIRMED as tested. Check the FAIL lines above. Common causes:") | ||
| 70 | + print(" - dynamic workflows not enabled on this account/plan") | ||
| 71 | + print(" (interactive session -> /config -> enable 'Dynamic workflows')") | ||
| 72 | + print(" - claude CLI version < 2.1.154 (`claude --version`)") | ||
| 73 | + print(" - not logged in (`claude /login`)") | ||
| 74 | + print(" - workflow script syntax issue -- check the `result` field above") | ||
| 75 | + print(" for the actual error message") | ||
| 76 | + print("If unresolved after debugging, treat A-1 as rejected and fall back to") | ||
| 77 | + print("architecture.md §10.1 Option B (fully custom Graph Engine), which") | ||
| 78 | + print("cascades into rewriting 03-C §3.4-3.6 and 05 §5.4/5.5.") | ||
| 79 | + | ||
| 80 | + | ||
| 81 | +if __name__ == "__main__": | ||
| 82 | + main() |
spikes/a1-dynamic-workflows/result-test1.json
0 → 100644
| 1 | +++ a/spikes/a1-dynamic-workflows/result-test1.json | ||
| 1 | +{"is_error":false,"duration_api_ms":39220,"num_turns":7,"stop_reason":"tool_use","session_id":"9a6a6156-48f4-41f3-a691-e09d3130db4f","total_cost_usd":0.38173110000000005,"usage":{"input_tokens":145,"cache_creation_input_tokens":32531,"cache_read_input_tokens":88941,"output_tokens":2274,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":32531,"ephemeral_5m_input_tokens":0},"inference_geo":"not_available","iterations":[{"input_tokens":139,"output_tokens":140,"cache_read_input_tokens":30599,"cache_creation_input_tokens":1932,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":1932},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-sonnet-5":{"inputTokens":151,"outputTokens":2283,"cacheReadInputTokens":113607,"cacheCreationInputTokens":63935,"webSearchRequests":0,"costUSD":0.38173110000000005,"contextWindow":1000000,"maxOutputTokens":64000,"canonicalModel":"claude-sonnet-5","provider":"firstParty"}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","fast_mode_disabled_reason":"sdk_opt_in_required","subtype":"success","api_error_status":null,"result":"{\"input_numbers\":[2,3,4],\"squares\":[4,9,16],\"sum\":29}","structured_output":{"input_numbers":[2,3,4],"squares":[4,9,16],"sum":29},"ttft_ms":7673,"ttft_stream_ms":6208,"time_to_request_ms":3571,"type":"result","duration_ms":38203,"uuid":"85b2cc0e-4f6d-49e3-be17-35dfecea83aa"} |
spikes/a1-dynamic-workflows/result-test2-bare.json
0 → 100644
| 1 | +++ a/spikes/a1-dynamic-workflows/result-test2-bare.json | ||
| 1 | +{"is_error":true,"duration_api_ms":0,"num_turns":1,"stop_reason":"stop_sequence","session_id":"d0bd829f-4528-479c-8702-0fe96fccf715","total_cost_usd":0,"usage":{"input_tokens":0,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":0,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"},"modelUsage":{},"permission_denials":[],"terminal_reason":"api_error","fast_mode_state":"off","fast_mode_disabled_reason":"sdk_opt_in_required","subtype":"success","api_error_status":null,"result":"Not logged in · Please run /login","type":"result","duration_ms":35,"uuid":"c76497c9-66c4-4177-84c0-1cf6a4dc45c2"} |
spikes/a1-dynamic-workflows/run-spike.sh
0 → 100755
| 1 | +++ a/spikes/a1-dynamic-workflows/run-spike.sh | ||
| 1 | +#!/usr/bin/env bash | ||
| 2 | +set -euo pipefail | ||
| 3 | + | ||
| 4 | +# graphlemon A-1 spike driver. | ||
| 5 | +# Tests whether Claude Code dynamic workflows can be invoked programmatically | ||
| 6 | +# (headless, no human in the loop) with parseable structured output, per | ||
| 7 | +# architecture.md §10.1 and SDD 00-SDD-生成记录.md assumption A-1. | ||
| 8 | +# | ||
| 9 | +# Usage: ./run-spike.sh /path/to/some/git/repo | ||
| 10 | +# The target repo just needs to be *a* git repo -- it does not need to be | ||
| 11 | +# graphlemon itself. Do not point this at a repo you care about disturbing; | ||
| 12 | +# it only writes .claude/workflows/graphlemon-a1-spike.js into it. | ||
| 13 | + | ||
| 14 | +TARGET_REPO="${1:?Usage: ./run-spike.sh /path/to/target/git/repo}" | ||
| 15 | +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | ||
| 16 | + | ||
| 17 | +echo "== Pre-flight ==" | ||
| 18 | +command -v claude >/dev/null || { echo "FAIL: claude CLI not found on PATH"; exit 1; } | ||
| 19 | +claude --version | ||
| 20 | +echo "(dynamic workflows require Claude Code v2.1.154+, and on Pro require enabling" | ||
| 21 | +echo " the 'Dynamic workflows' row in /config from an interactive session first)" | ||
| 22 | + | ||
| 23 | +mkdir -p "$TARGET_REPO/.claude/workflows" | ||
| 24 | +cp "$SCRIPT_DIR/workflow-under-test.js" "$TARGET_REPO/.claude/workflows/graphlemon-a1-spike.js" | ||
| 25 | +echo "Installed workflow -> $TARGET_REPO/.claude/workflows/graphlemon-a1-spike.js" | ||
| 26 | + | ||
| 27 | +cd "$TARGET_REPO" | ||
| 28 | + | ||
| 29 | +SCHEMA='{"type":"object","required":["input_numbers","squares","sum"],"properties":{"input_numbers":{"type":"array"},"squares":{"type":"array"},"sum":{"type":"number"}}}' | ||
| 30 | + | ||
| 31 | +echo | ||
| 32 | +echo "== Test 1: normal -p invocation, no stdin available, expect NO interactive prompt ==" | ||
| 33 | +START=$(date +%s) | ||
| 34 | +set +e | ||
| 35 | +OUT1=$(claude -p 'Run /graphlemon-a1-spike with args {"numbers":[2,3,4]}' \ | ||
| 36 | + --output-format json \ | ||
| 37 | + --json-schema "$SCHEMA" \ | ||
| 38 | + --permission-mode bypassPermissions \ | ||
| 39 | + < /dev/null) | ||
| 40 | +RC1=$? | ||
| 41 | +set -e | ||
| 42 | +END=$(date +%s) | ||
| 43 | +echo "$OUT1" > "$SCRIPT_DIR/result-test1.json" | ||
| 44 | +echo "exit code: $RC1, elapsed: $((END-START))s" | ||
| 45 | +echo "$OUT1" | python3 -m json.tool 2>/dev/null | head -80 || echo "$OUT1" | ||
| 46 | + | ||
| 47 | +echo | ||
| 48 | +echo "== Test 2: --bare invocation (docs recommend --bare for scripted/SDK calls) ==" | ||
| 49 | +echo " checks whether bare mode still discovers .claude/workflows/" | ||
| 50 | +START=$(date +%s) | ||
| 51 | +set +e | ||
| 52 | +OUT2=$(claude --bare -p 'Run /graphlemon-a1-spike with args {"numbers":[2,3,4]}' \ | ||
| 53 | + --output-format json \ | ||
| 54 | + --json-schema "$SCHEMA" \ | ||
| 55 | + --permission-mode bypassPermissions \ | ||
| 56 | + < /dev/null) | ||
| 57 | +RC2=$? | ||
| 58 | +set -e | ||
| 59 | +END=$(date +%s) | ||
| 60 | +echo "$OUT2" > "$SCRIPT_DIR/result-test2-bare.json" | ||
| 61 | +echo "exit code: $RC2, elapsed: $((END-START))s" | ||
| 62 | +echo "$OUT2" | python3 -m json.tool 2>/dev/null | head -80 || echo "$OUT2" | ||
| 63 | + | ||
| 64 | +echo | ||
| 65 | +echo "== Verdict ==" | ||
| 66 | +python3 "$SCRIPT_DIR/check-results.py" "$SCRIPT_DIR/result-test1.json" "$SCRIPT_DIR/result-test2-bare.json" |
spikes/a1-dynamic-workflows/workflow-under-test.js
0 → 100644
| 1 | +++ a/spikes/a1-dynamic-workflows/workflow-under-test.js | ||
| 1 | +export const meta = { | ||
| 2 | + name: 'graphlemon-a1-spike', | ||
| 3 | + description: 'A-1 spike: minimal fan-out/fan-in workflow used to verify headless invocation and structured output.', | ||
| 4 | +} | ||
| 5 | + | ||
| 6 | +// args is injected by the runtime when invoked as: | ||
| 7 | +// claude -p 'Run /graphlemon-a1-spike with args {"numbers":[2,3,4]}' | ||
| 8 | +const numbers = (typeof args !== 'undefined' && args && args.numbers) || [2, 3, 4] | ||
| 9 | + | ||
| 10 | +// Fan-out: one agent per number, each computes a square in isolation. | ||
| 11 | +// This is the exact shape graphlemon's Work Graph "diamond" fan-out would use. | ||
| 12 | +const squares = await pipeline(numbers, n => | ||
| 13 | + agent(`Compute ${n} squared (${n} * ${n}). Reply with only the integer result, nothing else.`, { | ||
| 14 | + label: `square-${n}`, | ||
| 15 | + schema: { | ||
| 16 | + type: 'object', | ||
| 17 | + required: ['value'], | ||
| 18 | + properties: { value: { type: 'number' } }, | ||
| 19 | + }, | ||
| 20 | + }), | ||
| 21 | +) | ||
| 22 | + | ||
| 23 | +// Fan-in: reduce step, pure script code, zero extra tokens. | ||
| 24 | +const sum = squares.reduce((acc, s) => acc + (s && typeof s.value === 'number' ? s.value : 0), 0) | ||
| 25 | + | ||
| 26 | +return { | ||
| 27 | + input_numbers: numbers, | ||
| 28 | + squares: squares.map(s => (s ? s.value : null)), | ||
| 29 | + sum, | ||
| 30 | +} |
supervisor/isolation/__init__.py
0 → 100644
| 1 | +++ a/supervisor/isolation/__init__.py | ||
| 1 | +"""Isolation / change-manifest / selective-apply primitives for graphlemon's | ||
| 2 | +Deploy Supervisor (the independent process that handles blue-green deploy + | ||
| 3 | +automatic rollback for graphlemon's own self-modification -- not to be | ||
| 4 | +confused with the "Supervisor(调度子 Agent)" role documented in | ||
| 5 | +graphlemon_architecture.md, which is an in-process monitoring node inside | ||
| 6 | +the Coder Work Graph. See docs/supervisor-auto-rollback-design.md for the | ||
| 7 | +disambiguation. | ||
| 8 | + | ||
| 9 | +Adapted 2026-07-30 from graphloop (`/Users/yibo/Documents/lemonAI/graphloop`, | ||
| 10 | +an earlier attempt at the same graphlemon architecture, no longer | ||
| 11 | +maintained). These four modules were copied essentially verbatim because | ||
| 12 | +they are pure-stdlib, contain no Hermes/Claude CLI dependency, and were | ||
| 13 | +already exercised against a real failure mode in graphloop's own Phase-10 | ||
| 14 | +self-evolution run (a worker wrote implementation files directly into the | ||
| 15 | +source workspace instead of the isolated copy; `change_manifest`'s | ||
| 16 | +symlink-rejecting isolated-tree snapshot plus the caller's manifest check | ||
| 17 | +caught it before anything false was reported as evidence). | ||
| 18 | + | ||
| 19 | +See docs/supervisor-design-comparison.md and | ||
| 20 | +docs/supervisor-auto-rollback-design.md for why this code lives here and how | ||
| 21 | +it plugs into the blue-green deploy/rollback state machine. | ||
| 22 | + | ||
| 23 | +Module map (unchanged from graphloop): | ||
| 24 | +- isolated_workspace.py -- isolate_workspace(): disposable tempdir copy of | ||
| 25 | + a source directory; the source is never mutated directly. | ||
| 26 | +- workspace_security.py -- validate_artifacts() / validate_subprocess_command(): | ||
| 27 | + path-escape and shell-metacharacter guards, used at every trust boundary. | ||
| 28 | +- change_manifest.py -- snapshot_workspace() / build_change_manifest(): | ||
| 29 | + SHA-256 diff between a source snapshot and an isolated tree. | ||
| 30 | +- selective_apply.py -- apply_manifest(): the sole writer back into the | ||
| 31 | + real source tree; validates every approved path, stages bytes before any | ||
| 32 | + mutation, writes atomically via tempfile.mkstemp + os.replace. | ||
| 33 | + | ||
| 34 | +Not yet wired into any graphlemon controller/process -- that happens when | ||
| 35 | +the corresponding Enabler unit (see docs/development-plan.md) is scheduled. | ||
| 36 | +This directory is staged ahead of that so the design doc can reference | ||
| 37 | +concrete, already-adapted code rather than only prose. | ||
| 38 | +""" |
supervisor/isolation/change_manifest.py
0 → 100644
| 1 | +++ a/supervisor/isolation/change_manifest.py | ||
| 1 | +"""Workspace snapshotting and change-manifest/diff generation for isolated | ||
| 2 | +runs (Coder/Validator nodes, or a Supervisor candidate-instance checkout). | ||
| 3 | + | ||
| 4 | +Adapted verbatim from graphloop's change_manifest.py (2026-07-30) -- no | ||
| 5 | +functional changes. See test_change_manifest.py in graphloop for the | ||
| 6 | +original contract this was validated against; graphlemon should add an | ||
| 7 | +equivalent test module when this is wired into a real Enabler unit. | ||
| 8 | +""" | ||
| 9 | +import difflib | ||
| 10 | +import hashlib | ||
| 11 | +import os | ||
| 12 | +import posixpath | ||
| 13 | +import re | ||
| 14 | + | ||
| 15 | +IGNORED_DIRS = {".git", "__pycache__", ".pytest_cache"} | ||
| 16 | +IGNORED_FILES = {".DS_Store"} | ||
| 17 | + | ||
| 18 | + | ||
| 19 | +class ChangeManifestError(Exception): | ||
| 20 | + """Raised when build_change_manifest encounters untrusted input: | ||
| 21 | + a symlink under the isolated workspace, or a workspace-escaping path | ||
| 22 | + in the source snapshot.""" | ||
| 23 | + | ||
| 24 | + | ||
| 25 | +def _iter_files(root, reject_symlinks): | ||
| 26 | + for dirpath, dirnames, filenames in os.walk(root, followlinks=False): | ||
| 27 | + keep_dirs = [] | ||
| 28 | + for name in dirnames: | ||
| 29 | + if name in IGNORED_DIRS: | ||
| 30 | + continue | ||
| 31 | + full = os.path.join(dirpath, name) | ||
| 32 | + if os.path.islink(full): | ||
| 33 | + if reject_symlinks: | ||
| 34 | + raise ChangeManifestError(f"symlink encountered: {full}") | ||
| 35 | + continue | ||
| 36 | + keep_dirs.append(name) | ||
| 37 | + dirnames[:] = keep_dirs | ||
| 38 | + | ||
| 39 | + for name in filenames: | ||
| 40 | + if name in IGNORED_FILES: | ||
| 41 | + continue | ||
| 42 | + full = os.path.join(dirpath, name) | ||
| 43 | + if os.path.islink(full): | ||
| 44 | + if reject_symlinks: | ||
| 45 | + raise ChangeManifestError(f"symlink encountered: {full}") | ||
| 46 | + continue | ||
| 47 | + if not os.path.isfile(full): | ||
| 48 | + continue | ||
| 49 | + rel = os.path.relpath(full, root).replace(os.sep, "/") | ||
| 50 | + yield rel, full | ||
| 51 | + | ||
| 52 | + | ||
| 53 | +def _analyze_file(full): | ||
| 54 | + with open(full, "rb") as handle: | ||
| 55 | + data = handle.read() | ||
| 56 | + sha256 = hashlib.sha256(data).hexdigest() | ||
| 57 | + if b"\x00" in data: | ||
| 58 | + kind = "binary" | ||
| 59 | + else: | ||
| 60 | + try: | ||
| 61 | + data.decode("utf-8") | ||
| 62 | + kind = "text" | ||
| 63 | + except UnicodeDecodeError: | ||
| 64 | + kind = "binary" | ||
| 65 | + return sha256, len(data), kind | ||
| 66 | + | ||
| 67 | + | ||
| 68 | +def _snapshot_files(root, reject_symlinks): | ||
| 69 | + files = {} | ||
| 70 | + for rel, full in _iter_files(root, reject_symlinks): | ||
| 71 | + sha256, size, kind = _analyze_file(full) | ||
| 72 | + files[rel] = {"sha256": sha256, "size": size, "kind": kind} | ||
| 73 | + return files | ||
| 74 | + | ||
| 75 | + | ||
| 76 | +def _validate_relative_path(rel): | ||
| 77 | + if not rel or rel.startswith("/"): | ||
| 78 | + raise ChangeManifestError(f"unsafe path in snapshot: {rel!r}") | ||
| 79 | + normalized = posixpath.normpath(rel) | ||
| 80 | + if normalized == ".." or normalized.startswith("../"): | ||
| 81 | + raise ChangeManifestError(f"unsafe path in snapshot: {rel!r}") | ||
| 82 | + | ||
| 83 | + | ||
| 84 | +def _safe_join(root, rel): | ||
| 85 | + full = os.path.normpath(os.path.join(root, *rel.split("/"))) | ||
| 86 | + if os.path.commonpath([root, full]) != os.path.normpath(root): | ||
| 87 | + raise ChangeManifestError(f"path escapes root: {rel!r}") | ||
| 88 | + return full | ||
| 89 | + | ||
| 90 | + | ||
| 91 | +def snapshot_workspace(path): | ||
| 92 | + root = os.path.realpath(path) | ||
| 93 | + return {"root": root, "files": _snapshot_files(root, reject_symlinks=False)} | ||
| 94 | + | ||
| 95 | + | ||
| 96 | +def build_change_manifest(source_snapshot, isolated_path): | ||
| 97 | + source_files = source_snapshot["files"] | ||
| 98 | + for rel in source_files: | ||
| 99 | + _validate_relative_path(rel) | ||
| 100 | + | ||
| 101 | + isolated_root = os.path.realpath(isolated_path) | ||
| 102 | + isolated_files = _snapshot_files(isolated_root, reject_symlinks=True) | ||
| 103 | + | ||
| 104 | + changed_files = [] | ||
| 105 | + for rel in source_files.keys() | isolated_files.keys(): | ||
| 106 | + old = source_files.get(rel) | ||
| 107 | + new = isolated_files.get(rel) | ||
| 108 | + if old and new: | ||
| 109 | + if old["sha256"] == new["sha256"]: | ||
| 110 | + continue | ||
| 111 | + changed_files.append({ | ||
| 112 | + "path": rel, | ||
| 113 | + "status": "modified", | ||
| 114 | + "old_hash": old["sha256"], | ||
| 115 | + "new_hash": new["sha256"], | ||
| 116 | + "kind": new["kind"], | ||
| 117 | + }) | ||
| 118 | + elif new: | ||
| 119 | + changed_files.append({ | ||
| 120 | + "path": rel, | ||
| 121 | + "status": "added", | ||
| 122 | + "old_hash": None, | ||
| 123 | + "new_hash": new["sha256"], | ||
| 124 | + "kind": new["kind"], | ||
| 125 | + }) | ||
| 126 | + else: | ||
| 127 | + changed_files.append({ | ||
| 128 | + "path": rel, | ||
| 129 | + "status": "deleted", | ||
| 130 | + "old_hash": old["sha256"], | ||
| 131 | + "new_hash": None, | ||
| 132 | + "kind": old["kind"], | ||
| 133 | + }) | ||
| 134 | + | ||
| 135 | + changed_files.sort(key=lambda entry: entry["path"]) | ||
| 136 | + return { | ||
| 137 | + "source_root": source_snapshot["root"], | ||
| 138 | + "isolated_root": isolated_root, | ||
| 139 | + "changed_files": changed_files, | ||
| 140 | + } | ||
| 141 | + | ||
| 142 | + | ||
| 143 | +def _read_text_lines(full): | ||
| 144 | + with open(full, "rb") as handle: | ||
| 145 | + data = handle.read() | ||
| 146 | + return data.decode("utf-8").splitlines(keepends=True) | ||
| 147 | + | ||
| 148 | + | ||
| 149 | +def unified_diff(manifest): | ||
| 150 | + changed_files = manifest["changed_files"] | ||
| 151 | + if not changed_files: | ||
| 152 | + return "" | ||
| 153 | + | ||
| 154 | + source_root = manifest["source_root"] | ||
| 155 | + isolated_root = manifest["isolated_root"] | ||
| 156 | + sections = [] | ||
| 157 | + | ||
| 158 | + for entry in changed_files: | ||
| 159 | + rel = entry["path"] | ||
| 160 | + status = entry["status"] | ||
| 161 | + old_label = "/dev/null" if status == "added" else rel | ||
| 162 | + new_label = "/dev/null" if status == "deleted" else rel | ||
| 163 | + | ||
| 164 | + if entry["kind"] == "binary": | ||
| 165 | + sections.append(f"Binary files {old_label} and {new_label} differ\n") | ||
| 166 | + continue | ||
| 167 | + | ||
| 168 | + old_lines = ( | ||
| 169 | + _read_text_lines(_safe_join(source_root, rel)) | ||
| 170 | + if status != "added" else [] | ||
| 171 | + ) | ||
| 172 | + new_lines = ( | ||
| 173 | + _read_text_lines(_safe_join(isolated_root, rel)) | ||
| 174 | + if status != "deleted" else [] | ||
| 175 | + ) | ||
| 176 | + diff_lines = difflib.unified_diff( | ||
| 177 | + old_lines, new_lines, fromfile=old_label, tofile=new_label | ||
| 178 | + ) | ||
| 179 | + # Collapse "@@ -l,s +l,s @@" hunk headers to a single leading "@@" | ||
| 180 | + # marker so callers can count hunks via a plain substring count. | ||
| 181 | + diff_lines = ( | ||
| 182 | + re.sub(r" @@\n$", "\n", line) if line.startswith("@@") else line | ||
| 183 | + for line in diff_lines | ||
| 184 | + ) | ||
| 185 | + sections.append("".join(diff_lines)) | ||
| 186 | + | ||
| 187 | + return "".join(sections) |
supervisor/isolation/isolated_workspace.py
0 → 100644
| 1 | +++ a/supervisor/isolation/isolated_workspace.py | ||
| 1 | +"""Create an isolated, disposable copy of a workspace directory for a | ||
| 2 | +Coder/Validator node -- or, in the Supervisor's blue-green deploy flow, for | ||
| 3 | +a candidate instance -- to operate in. | ||
| 4 | + | ||
| 5 | +isolate_workspace(source_dir) never modifies `source_dir`; it copies it | ||
| 6 | +into a fresh temporary directory and returns the new path. | ||
| 7 | + | ||
| 8 | +Adapted from graphloop's isolated_coding_workspace.py (2026-07-30). Only | ||
| 9 | +change from the original: the default parent-dir constant and prefix now | ||
| 10 | +follow graphlemon's own `.graphlemon/` runtime-directory convention instead | ||
| 11 | +of graphloop's `.hermes/graphloop-isolated-workspaces`. Reconcile | ||
| 12 | +DEFAULT_ISOLATION_PARENT_DIR against the final runtime-directory layout in | ||
| 13 | +docs/sdd/05-实施约束与依赖.md when this is wired into a real Enabler unit. | ||
| 14 | +""" | ||
| 15 | +import os | ||
| 16 | +import shutil | ||
| 17 | +import tempfile | ||
| 18 | +from typing import Optional | ||
| 19 | + | ||
| 20 | +# Matches the graphlemon architecture doc's `.graphlemon/` convention for | ||
| 21 | +# platform-owned runtime state. Kept as a module constant (not hardcoded at | ||
| 22 | +# each call site) so a future SDD decision can change it in one place. | ||
| 23 | +DEFAULT_ISOLATION_PARENT_DIR = os.path.expanduser("~/.graphlemon/isolated-workspaces") | ||
| 24 | + | ||
| 25 | + | ||
| 26 | +class IsolatedWorkspaceError(Exception): | ||
| 27 | + """Raised when the source directory cannot be isolated.""" | ||
| 28 | + | ||
| 29 | + | ||
| 30 | +def isolate_workspace(source_dir: str, *, parent_dir: Optional[str] = None) -> str: | ||
| 31 | + """Copy `source_dir` into a fresh temporary directory and return its path. | ||
| 32 | + | ||
| 33 | + `parent_dir`, if given, is used as the parent of the fresh temp | ||
| 34 | + directory (created first if missing) instead of the OS default temp | ||
| 35 | + location. Some agent runtimes refuse to write under the OS default temp | ||
| 36 | + directory (e.g. a sandboxed CLI treating macOS's `/var/folders/.../T` as | ||
| 37 | + a sensitive system path and blocking writes to it even from a coding | ||
| 38 | + role) -- `parent_dir` lets callers that hit this route the isolated copy | ||
| 39 | + somewhere the worker/candidate-instance process can actually write to. | ||
| 40 | + Callers that don't pass `parent_dir` get plain `tempfile.mkdtemp()` | ||
| 41 | + behavior; callers that want the graphlemon-standard location should pass | ||
| 42 | + `DEFAULT_ISOLATION_PARENT_DIR` explicitly. | ||
| 43 | + """ | ||
| 44 | + if not source_dir or not os.path.isdir(source_dir): | ||
| 45 | + raise IsolatedWorkspaceError( | ||
| 46 | + f"source_dir is not a directory: {source_dir!r}" | ||
| 47 | + ) | ||
| 48 | + | ||
| 49 | + if parent_dir: | ||
| 50 | + os.makedirs(parent_dir, exist_ok=True) | ||
| 51 | + destination = tempfile.mkdtemp(prefix="graphlemon-isolated-", dir=parent_dir) | ||
| 52 | + shutil.copytree(source_dir, destination, dirs_exist_ok=True) | ||
| 53 | + return destination |
supervisor/isolation/selective_apply.py
0 → 100644
| 1 | +++ a/supervisor/isolation/selective_apply.py | ||
| 1 | +"""Selective, safe application of a change_manifest diff from an isolated | ||
| 2 | +workspace (or a Supervisor candidate checkout) back into its source | ||
| 3 | +workspace. | ||
| 4 | + | ||
| 5 | +Adapted from graphloop's selective_apply.py (2026-07-30). Only change from | ||
| 6 | +the original: `import change_manifest as cm` -> relative package import, | ||
| 7 | +since this now lives in the `supervisor.isolation` package alongside | ||
| 8 | +change_manifest.py instead of as a flat top-level module. Logic is | ||
| 9 | +otherwise unchanged. See test_selective_apply.py in graphloop for the | ||
| 10 | +original contract; graphlemon should add an equivalent test module when | ||
| 11 | +this is wired into a real Enabler unit. | ||
| 12 | +""" | ||
| 13 | +import hashlib | ||
| 14 | +import json | ||
| 15 | +import os | ||
| 16 | +import posixpath | ||
| 17 | +import tempfile | ||
| 18 | + | ||
| 19 | +from . import change_manifest as cm | ||
| 20 | + | ||
| 21 | + | ||
| 22 | +class SelectiveApplyError(Exception): | ||
| 23 | + """Raised when apply_manifest rejects a call before any mutation of | ||
| 24 | + source_root, or refuses to write through a symlink it discovers.""" | ||
| 25 | + | ||
| 26 | + | ||
| 27 | +def _normalize_relative_path(path): | ||
| 28 | + if not isinstance(path, str) or path == "": | ||
| 29 | + raise SelectiveApplyError(f"empty or invalid path: {path!r}") | ||
| 30 | + if posixpath.isabs(path): | ||
| 31 | + raise SelectiveApplyError(f"absolute path not allowed: {path!r}") | ||
| 32 | + normalized = posixpath.normpath(path) | ||
| 33 | + segments = normalized.split("/") | ||
| 34 | + if normalized in (".", "") or any(segment == ".." for segment in segments): | ||
| 35 | + raise SelectiveApplyError(f"path traversal not allowed: {path!r}") | ||
| 36 | + return normalized | ||
| 37 | + | ||
| 38 | + | ||
| 39 | +def _reject_symlink_components(root, rel_path): | ||
| 40 | + current = root | ||
| 41 | + for segment in rel_path.split("/"): | ||
| 42 | + current = os.path.join(current, segment) | ||
| 43 | + if os.path.islink(current): | ||
| 44 | + raise SelectiveApplyError(f"symlink encountered in path: {rel_path!r}") | ||
| 45 | + | ||
| 46 | + | ||
| 47 | +def _manifest_digest(manifest): | ||
| 48 | + payload = json.dumps(manifest, sort_keys=True).encode("utf-8") | ||
| 49 | + return hashlib.sha256(payload).hexdigest() | ||
| 50 | + | ||
| 51 | + | ||
| 52 | +def _hash_file(path): | ||
| 53 | + with open(path, "rb") as handle: | ||
| 54 | + return hashlib.sha256(handle.read()).hexdigest() | ||
| 55 | + | ||
| 56 | + | ||
| 57 | +def apply_manifest(manifest, source_root, isolated_root, approved_paths, | ||
| 58 | + expected_source_snapshot=None): | ||
| 59 | + if not isinstance(manifest, dict) or not isinstance(manifest.get("changed_files"), list): | ||
| 60 | + raise SelectiveApplyError("manifest must be a dict with a 'changed_files' list") | ||
| 61 | + if approved_paths is None: | ||
| 62 | + raise SelectiveApplyError("approved_paths is required and must not be None") | ||
| 63 | + if not os.path.isdir(source_root): | ||
| 64 | + raise SelectiveApplyError(f"source_root is not a directory: {source_root!r}") | ||
| 65 | + if not os.path.isdir(isolated_root): | ||
| 66 | + raise SelectiveApplyError(f"isolated_root is not a directory: {isolated_root!r}") | ||
| 67 | + | ||
| 68 | + source_root = os.path.realpath(source_root) | ||
| 69 | + isolated_root = os.path.realpath(isolated_root) | ||
| 70 | + | ||
| 71 | + entries_by_path = {entry["path"]: entry for entry in manifest["changed_files"]} | ||
| 72 | + | ||
| 73 | + normalized_approved = [] | ||
| 74 | + for raw_path in approved_paths: | ||
| 75 | + normalized = _normalize_relative_path(raw_path) | ||
| 76 | + if normalized not in entries_by_path: | ||
| 77 | + raise SelectiveApplyError(f"path not present in manifest: {raw_path!r}") | ||
| 78 | + normalized_approved.append(normalized) | ||
| 79 | + | ||
| 80 | + if expected_source_snapshot is not None: | ||
| 81 | + current_snapshot = cm.snapshot_workspace(source_root) | ||
| 82 | + if current_snapshot != expected_source_snapshot: | ||
| 83 | + raise SelectiveApplyError( | ||
| 84 | + "source_root has drifted from expected_source_snapshot" | ||
| 85 | + ) | ||
| 86 | + | ||
| 87 | + approved_entries = [entries_by_path[p] for p in normalized_approved] | ||
| 88 | + | ||
| 89 | + # Reject any destination that runs through a symlink (including | ||
| 90 | + # symlinked intermediate directories), and preload isolated bytes for | ||
| 91 | + # add/modify entries, so every possible read/validation failure surfaces | ||
| 92 | + # before source_root is touched. | ||
| 93 | + staged_data = {} | ||
| 94 | + for entry in approved_entries: | ||
| 95 | + rel = entry["path"] | ||
| 96 | + _reject_symlink_components(source_root, rel) | ||
| 97 | + | ||
| 98 | + dest = os.path.join(source_root, *rel.split("/")) | ||
| 99 | + if os.path.isdir(dest): | ||
| 100 | + raise SelectiveApplyError(f"destination is a directory: {rel!r}") | ||
| 101 | + | ||
| 102 | + if entry["status"] != "deleted": | ||
| 103 | + _reject_symlink_components(isolated_root, rel) | ||
| 104 | + src = os.path.join(isolated_root, *rel.split("/")) | ||
| 105 | + if not os.path.isfile(src): | ||
| 106 | + raise SelectiveApplyError(f"isolated source file missing: {rel!r}") | ||
| 107 | + with open(src, "rb") as handle: | ||
| 108 | + staged_data[rel] = handle.read() | ||
| 109 | + | ||
| 110 | + applied = [] | ||
| 111 | + already_applied = [] | ||
| 112 | + | ||
| 113 | + for entry in approved_entries: | ||
| 114 | + rel = entry["path"] | ||
| 115 | + dest = os.path.join(source_root, *rel.split("/")) | ||
| 116 | + | ||
| 117 | + if entry["status"] == "deleted": | ||
| 118 | + if os.path.exists(dest): | ||
| 119 | + os.remove(dest) | ||
| 120 | + applied.append(rel) | ||
| 121 | + else: | ||
| 122 | + already_applied.append(rel) | ||
| 123 | + continue | ||
| 124 | + | ||
| 125 | + data = staged_data[rel] | ||
| 126 | + if os.path.isfile(dest) and _hash_file(dest) == entry["new_hash"]: | ||
| 127 | + already_applied.append(rel) | ||
| 128 | + continue | ||
| 129 | + | ||
| 130 | + dest_dir = os.path.dirname(dest) | ||
| 131 | + os.makedirs(dest_dir, exist_ok=True) | ||
| 132 | + fd, tmp_path = tempfile.mkstemp(dir=dest_dir, prefix=".selective-apply-") | ||
| 133 | + try: | ||
| 134 | + with os.fdopen(fd, "wb") as tmp_handle: | ||
| 135 | + tmp_handle.write(data) | ||
| 136 | + os.replace(tmp_path, dest) | ||
| 137 | + except BaseException: | ||
| 138 | + if os.path.exists(tmp_path): | ||
| 139 | + os.remove(tmp_path) | ||
| 140 | + raise | ||
| 141 | + applied.append(rel) | ||
| 142 | + | ||
| 143 | + skipped = sorted(set(entries_by_path) - set(normalized_approved)) | ||
| 144 | + | ||
| 145 | + return { | ||
| 146 | + "applied": sorted(applied), | ||
| 147 | + "skipped": skipped, | ||
| 148 | + "already_applied": sorted(already_applied), | ||
| 149 | + "manifest_id": _manifest_digest(manifest), | ||
| 150 | + } |
supervisor/isolation/workspace_security.py
0 → 100644
| 1 | +++ a/supervisor/isolation/workspace_security.py | ||
| 1 | +"""Validation helpers that stand between an untrusted process (a Coder node, | ||
| 2 | +a Validator node, or a candidate deploy instance) and the things that trust | ||
| 3 | +it: the artifact paths it reports back, and the argv used to launch it as a | ||
| 4 | +subprocess. | ||
| 5 | + | ||
| 6 | +Both validators raise WorkspaceSecurityError on anything suspicious rather | ||
| 7 | +than silently coercing or ignoring it. | ||
| 8 | + | ||
| 9 | +Adapted verbatim from graphloop's workspace_security.py (2026-07-30) -- | ||
| 10 | +no functional changes. graphloop's own Phase-10 self-evolution run hit a | ||
| 11 | +real case this class of check is meant to catch (a worker writing files | ||
| 12 | +outside the isolated tree it was supposed to be confined to); keeping this | ||
| 13 | +module's logic unchanged preserves that validated behavior. | ||
| 14 | +""" | ||
| 15 | + | ||
| 16 | +from __future__ import annotations | ||
| 17 | + | ||
| 18 | +import os | ||
| 19 | +from typing import Any, List | ||
| 20 | + | ||
| 21 | + | ||
| 22 | +class WorkspaceSecurityError(Exception): | ||
| 23 | + """Raised by validate_artifacts/validate_subprocess_command on rejection.""" | ||
| 24 | + | ||
| 25 | + | ||
| 26 | +# Shell metacharacters/sequences that must never appear in a subprocess | ||
| 27 | +# argument, since these commands are meant to run with shell=False and any | ||
| 28 | +# of these would only matter if something later reinterpreted them via a | ||
| 29 | +# shell. | ||
| 30 | +_SHELL_METACHARACTERS = (";", "&&", "|", ">", "`", "$(") | ||
| 31 | + | ||
| 32 | + | ||
| 33 | +def _normalize_relative_posix_parts(artifact: Any) -> List[str]: | ||
| 34 | + if not isinstance(artifact, str): | ||
| 35 | + raise WorkspaceSecurityError(f"artifact path is not a string: {artifact!r}") | ||
| 36 | + if artifact == "": | ||
| 37 | + raise WorkspaceSecurityError("artifact path is empty") | ||
| 38 | + if os.path.isabs(artifact): | ||
| 39 | + raise WorkspaceSecurityError(f"artifact path is absolute: {artifact!r}") | ||
| 40 | + | ||
| 41 | + posix_path = artifact.replace(os.sep, "/") | ||
| 42 | + if os.altsep: | ||
| 43 | + posix_path = posix_path.replace(os.altsep, "/") | ||
| 44 | + | ||
| 45 | + parts = [part for part in posix_path.split("/") if part not in ("", ".")] | ||
| 46 | + if not parts: | ||
| 47 | + raise WorkspaceSecurityError(f"artifact path is empty: {artifact!r}") | ||
| 48 | + if any(part == ".." for part in parts): | ||
| 49 | + raise WorkspaceSecurityError( | ||
| 50 | + f"artifact path contains a parent traversal segment: {artifact!r}" | ||
| 51 | + ) | ||
| 52 | + return parts | ||
| 53 | + | ||
| 54 | + | ||
| 55 | +def validate_artifacts(artifacts: Any, workspace: str) -> List[str]: | ||
| 56 | + """Validate and normalize artifact paths reported by an untrusted | ||
| 57 | + child process, ensuring every one lives safely inside `workspace`.""" | ||
| 58 | + if not isinstance(artifacts, (list, tuple)): | ||
| 59 | + raise WorkspaceSecurityError( | ||
| 60 | + f"artifacts must be a list of strings, got {type(artifacts).__name__}" | ||
| 61 | + ) | ||
| 62 | + | ||
| 63 | + workspace_real = os.path.realpath(workspace) | ||
| 64 | + normalized: List[str] = [] | ||
| 65 | + | ||
| 66 | + for artifact in artifacts: | ||
| 67 | + parts = _normalize_relative_posix_parts(artifact) | ||
| 68 | + | ||
| 69 | + current = workspace | ||
| 70 | + for part in parts: | ||
| 71 | + current = os.path.join(current, part) | ||
| 72 | + if os.path.islink(current): | ||
| 73 | + raise WorkspaceSecurityError( | ||
| 74 | + f"artifact path contains a symlink component: {artifact!r}" | ||
| 75 | + ) | ||
| 76 | + | ||
| 77 | + if not os.path.exists(current): | ||
| 78 | + raise WorkspaceSecurityError( | ||
| 79 | + f"artifact does not exist under workspace: {artifact!r}" | ||
| 80 | + ) | ||
| 81 | + | ||
| 82 | + real_target = os.path.realpath(current) | ||
| 83 | + if os.path.commonpath([workspace_real, real_target]) != workspace_real: | ||
| 84 | + raise WorkspaceSecurityError( | ||
| 85 | + f"artifact resolves outside workspace: {artifact!r}" | ||
| 86 | + ) | ||
| 87 | + | ||
| 88 | + normalized.append("/".join(parts)) | ||
| 89 | + | ||
| 90 | + return normalized | ||
| 91 | + | ||
| 92 | + | ||
| 93 | +def validate_subprocess_command(command: Any) -> List[str]: | ||
| 94 | + """Validate an argv list about to be passed to subprocess.run(shell=False). | ||
| 95 | + | ||
| 96 | + Accepts the canonical [python_executable, existing_script.py, ...] shape | ||
| 97 | + only, with no shell metacharacters in any argument. | ||
| 98 | + """ | ||
| 99 | + if not isinstance(command, (list, tuple)): | ||
| 100 | + raise WorkspaceSecurityError( | ||
| 101 | + f"command must be a list of strings, got {type(command).__name__}" | ||
| 102 | + ) | ||
| 103 | + if len(command) == 0: | ||
| 104 | + raise WorkspaceSecurityError("command must not be empty") | ||
| 105 | + | ||
| 106 | + for argument in command: | ||
| 107 | + if not isinstance(argument, str): | ||
| 108 | + raise WorkspaceSecurityError( | ||
| 109 | + f"command argument is not a string: {argument!r}" | ||
| 110 | + ) | ||
| 111 | + if "\n" in argument or "\r" in argument: | ||
| 112 | + raise WorkspaceSecurityError( | ||
| 113 | + f"command argument contains a newline: {argument!r}" | ||
| 114 | + ) | ||
| 115 | + for metacharacter in _SHELL_METACHARACTERS: | ||
| 116 | + if metacharacter in argument: | ||
| 117 | + raise WorkspaceSecurityError( | ||
| 118 | + f"command argument contains shell metacharacter " | ||
| 119 | + f"{metacharacter!r}: {argument!r}" | ||
| 120 | + ) | ||
| 121 | + | ||
| 122 | + executable = command[0] | ||
| 123 | + if not os.path.isfile(executable): | ||
| 124 | + raise WorkspaceSecurityError( | ||
| 125 | + f"command executable does not exist: {executable!r}" | ||
| 126 | + ) | ||
| 127 | + | ||
| 128 | + if len(command) < 2: | ||
| 129 | + raise WorkspaceSecurityError( | ||
| 130 | + f"command is missing a script argument: {list(command)!r}" | ||
| 131 | + ) | ||
| 132 | + | ||
| 133 | + script = command[1] | ||
| 134 | + if not script.endswith(".py") or not os.path.isfile(script): | ||
| 135 | + raise WorkspaceSecurityError( | ||
| 136 | + f"command script argument is not an existing .py file: {script!r}" | ||
| 137 | + ) | ||
| 138 | + | ||
| 139 | + return list(command) |