graphlemon 开发计划
版本: v1.0
日期: 2026-07-29
输入来源: graphlemon_architecture.md(v0.11)+ docs/sdd/(00-06 + sdd-mvcp-contract.json v1.6.0)
方法论: 复用本地 Hermes planning profile 的 project-unit-decomposition(Development Unit 分解 + 依赖 DAG)+ engineering-work-planning(Phase/Sub-phase/Unit 三层分类:Epic 级/Feature 级/Task 级,垂直切片优先于技术分层)
0. 怎么读这份文档
- Phase(大阶段,对应 Epic 级):有自己的验收标准和"完成"定义,多个 Sub-phase 组成。
- Sub-phase(小阶段,对应 Feature 级):一个可独立验收的能力切片,多个 Unit 组成。
- Unit(子任务,对应 Task 级,即 Development Unit):一个明确的工程产物 + 完成标准,映射到至少一条 SDD 条目(M/V/C/P/REQ),标注风险分类:
- [Value Path] 主线价值切片,直接对应用户/系统可感知的能力
- [Risk Gate] 高风险校验/回路/护栏,不做会在生产阶段暴露风险
- [Enabler] 使能型基础设施,本身不直接产出用户可见价值,但阻塞其他 Unit
- 每个 Unit 标注
依赖(DAG 上游)和Done 标准(可观察、可验证,优先复用 06-验收与测试锚点.md 已有的 REQ 用例,不重新发明验收口径)。 - 机器可读版本见
docs/sdd/unit_matrix.json(Unit ID、依赖、REQ/M/V/C/P 覆盖、风险分类,供后续 Coder Agent 或调度器直接消费)。
关于阶段排序的说明:Phase 0 是纯 Enabler(无独立用户价值),Phase 1 起才是垂直切片(贪吃蛇场景端到端跑通)。这不违反"避免按技术分层切故事"的原则——Phase 0 内部的确按技术层(数据模型→后端集成→控制台骨架)排列,但这是因为它们是共同阻塞后续所有价值切片的地基工作,不是把一个本该完整的价值切片强行拆成"先做数据库、再做 API、再做前端"三个假装独立的"Phase"。真正的价值切片从 Phase 1 开始,一路贯穿 Org Graph→Work Graph→端到端验收,不按技术层拆分。
1. Phase 总览
| Phase | 目标 | 类型 | 验收锚点 |
|---|---|---|---|
| PH0 | 基础设施与脚手架就绪 | Enabler | 能创建目标项目、Git 仓库结构符合约定、15 个实体可建表 |
| PH1 | MVP 全链路闭环跑通(贪吃蛇场景) | Value Path | 架构文档 §11 验收标准第 1-3 条;REQ-01/02/03/07 |
| PH2 | 质量闭环:回路、Skill 迭代、护栏、断点恢复 | Risk Gate | 架构文档 §11 验收标准第 4-5 条;REQ-04/05/06/08/10/14 |
| PH3 | 人机交互控制台 | Value Path | REQ-11/12/13 |
| PH4 | 全量验收与固化 | Risk Gate | 06 节全部 14 条 REQ 完整回归通过一次 |
| PH5(2026-07-30 新增) | 自举能力:Deploy Supervisor + 蓝绿部署自动回滚 | Risk Gate | docs/supervisor-auto-rollback-design.md 状态机全部路径(health_check_failed / rolled_back / rollback_failed / finalized)至少各复现一次;self-development-feasibility.md 9 步走查场景真实跑通一次 |
依赖顺序: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 开始被允许对自己的代码做自主修改"这个新增能力上生效。
PH0:基础设施与脚手架(Enabler)
Goal:为后续所有开发建立可靠地基,本身不产出用户可见能力。
Phase 验收:给定一个空 git 仓库路径,能自动初始化出完整的 graphlemon 目录骨架;15 个数据实体全部建表成功并通过约束校验;claude --agent planner 之类的 subagent 调用能跑通最简单的一次往返。
PH0-S1 数据模型与仓库骨架
可与 PH0-S3 并行。
U-001 [Enabler] 落地 01-M 全部 15 个实体的物理数据库 schema(MVP 单机场景可用 SQLite),含全部枚举约束、外键、唯一索引
- 依赖:无(起点)
- Done:15 张表全部建表成功;
project.target_folder_path唯一约束、work_node_execution.status枚举约束等关键约束有单测覆盖 - 覆盖:01-M 全部实体
- 源码位置:graphlemon 仓库根目录
src/(见 05 节 §5.7"源码目录约定",与 U-002 在目标项目里生成的src/是两回事)
U-002 [Enabler] 目标项目 Git 仓库目录骨架初始化逻辑:只生成
.graphlemon/子目录(含runs/<task_id>/,架构文档 §13.2——01-Mproject.graphlemon_subdir_initialized字段本身指向的就是这节,不是 §7.3)- 2026-07-31 订正:development-plan.md 这行原先误引用了架构文档 §7.3 的旧目录树(
.claude/agents/、.claude/agent-memory/、.claude/workflows/、skills/、顶层runs/)。但 §13.1("已确认"决策,晚于 §7.3)明确写着这些是 graphlemon 平台自己仓库的资产("graphlemon 平台自己的仓库只保留平台资产:Org/Work Graph 引擎、Skill 库、Agent 人设...不含任何目标项目的业务代码"),属于 U-003 范围,不是目标项目、不是 U-002。§7.3 是"graphlemon 只服务自己开发自己"阶段(自举模式)的旧设计,已被 §13"目标项目是独立代码空间"这个决策取代。目标项目自己的src/不需要 graphlemon 创建或规定结构("目标项目自己原有的源码目录,graphlemon 不规定具体结构,尊重目标项目原有约定")。用户确认,2026-07-31。 - 依赖:U-001(
project.graphlemon_subdir_initialized字段需要有实体支撑) - Done:给定一个空 git 仓库,能生成
.graphlemon/子目录骨架并产生一次初始化 commit;非 git 仓库时按 02-V 2.1 节的确认态流程走(不静默git init) - 覆盖:01-M
project;03-Cc_project_create
- 2026-07-31 订正:development-plan.md 这行原先误引用了架构文档 §7.3 的旧目录树(
U-003 [Enabler] graphlemon 平台自身的仓库/运行时目录结构(与目标项目仓库物理分离;本地非 Git 的原始日志目录,见架构文档 §13.2)
- 依赖:无
- Done:平台目录和任意目标项目目录可以是完全独立的两个路径,互不干扰;日志目录不纳入平台自身 Git 历史
PH0-S2 Claude Code 后端集成
依赖 PH0-S1。这是全项目风险最集中的 Enabler 子阶段,直接决定 A-1(已 confirmed)之后的下一个高风险假设 A-2 能否成立。
U-004 [Risk Gate] Hermes profile 内容迁移到
.claude/agents/{planner,coder,validator}.md(对应 SDD 00 节 A-2 假设的实际验证动作)- 依赖:U-002
- Done:三个 subagent 定义文件可被 Claude Code 发现,
claude --agent planner -p "..."能正常启动且系统提示词内容正确;skills:/memory: project/tools:/model:frontmatter 字段配置完整(见 05-实施约束与依赖.md §5.1) - 覆盖:架构文档 §7.2/§9;05 节 §5.1;A-2 假设
- 注意(2026-07-31 已验证,结果:部分成立):Planner/Validator 的 SOUL.md 基本原样迁移;Coder 的 SOUL.md 需要实质性重写(原文件硬编码了一个真实飞书 Base API token、整段认领机制强绑定
lark-cli/Base 状态机、且指示自己再跑一次claude --dangerously-skip-permissions,与 05 §5.3.1 的auto权限模式直接冲突)。真实claude --agent planner -p调用(--permission-mode auto,成本 $0.05)验证通过,返回内容准确复述了迁移后的身份与红线。A-2 记为部分成立,按本条预先注册的判定标准处理,不是 confirmed;详见 00-SDD-生成记录.md 假设表和agents/{planner,coder,validator}/SOUL.md各自文件头的迁移说明。U-005/U-011 沿用现有.claude/agents/*.md内容即可,不需要因为这条重新做 A-2 spike
U-004.5(2026-07-30 新增)[Enabler] 搭建
RUN_REAL_AGENT_E2E门控测试 harness(真实 shell out 到claude的端到端测试默认跳过,需显式环境变量开启;镜像CLAUDE.mdTesting 约束里点名的 graphloop 先例,避免 U-005/U-006/U-010 等后续 Unit 各自发明一套)- 依赖:U-004
- Done:仓库里有一个可复用的 skip 装饰器/测试基类(例如
skip_unless_env("RUN_REAL_AGENT_E2E"));日常python3 -m unittest默认跳过任何真实调用claude子进程的测试;显式设置该环境变量时,对 U-004 落地的某个 subagent 做一次真实-p往返调用能跑通 - 覆盖:
CLAUDE.mdTesting 约束
U-005 [Enabler]
c_org_node_trigger的claude --agent <name> -p子进程调用封装(骨架,不含完整状态机)- 依赖:U-004, U-004.5
- Done:手工调用能触发三个 Org Agent 之一并拿到文本响应
U-006 [Enabler] Work Graph 执行层调用封装(workflow 脚本写入 +
claude -p调用,直接复用spikes/a1-dynamic-workflows/已验证的代码路径,见 03-C 3.5 节实现契约)- 依赖:U-004, U-004.5
- Done:封装成可被服务端函数调用的接口,单测通过,
--permission-mode auto生效(不是--bare,不是bypassPermissions,见 05 节 §5.3.1)
PH0-S3 控制台骨架
可与 PH0-S1/S2 并行,无相互依赖。
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 补齐)- 依赖:无
- Done:后端进程可启停;macOS 上
/api/system/pick-folder能真实弹出系统对话框并返回路径;后端代码零第三方依赖(pip list/import检查可验证) - 覆盖:03-C
c_pick_folder;05 节 §5.4.1, §5.7
U-008 [Enabler] 前端骨架(本地 Web 应用,原生 HTML/CSS/JS,无构建步骤,无 npm 依赖,见 05 节 §5.7)
- 依赖:U-007
- Done:浏览器访问
localhost能看到路由跳转正常的空壳应用;前端不依赖 npm install/构建工具链
PH1:MVP 全链路闭环(Value Path,贪吃蛇场景)
Goal:端到端跑通一次从"提出目标"到"任务完成"的完整闭环,不要求 UI,可以用手工 API 调用驱动。 Phase 验收:REQ-01/02/03/07 的 happy path 全部通过;架构文档 §11 验收标准第 1-3 条达成。
PH1-S1 Org Graph 层单点触发
U-009 [Value Path]
c_task_create实现,task状态机(p_org_task_lifecycle)落地到queued态(c_project_create已经在 U-002 完整实现,这里不重做)- 依赖:U-001, U-002
- Done(2026-07-31 订正:原来写"REQ-01 happy/boundary/failure 三个用例全部通过"不准确——happy 用例("依次触发 planner/coder/validator 三个 org_node,task.status 按状态机正确流转")需要
c_org_node_trigger完整实现,U-009 本身不碰这个,做不到;U-010 的 Done 标准原话几乎就是这句 happy 用例。boundary(并发上限不阻塞任务创建本身)和 failure(INVALID_PARAM)两条在 U-009 范围内是真实可测的):boundary/failure 两个用例通过;happy 用例随 U-010 落地后累计满足,不在本 Unit 单独打勾 - 覆盖:01-M
task;03-Cc_task_create;04-Pp_org_task_lifecycle;REQ-01(boundary/failure)
U-010 [Value Path]
c_org_node_trigger完整实现(含TASK_NOT_READY/CONCURRENCY_LIMIT_REACHED/USAGE_LIMIT_REACHED错误处理)- 依赖:U-005, U-009
- Done:手工依次触发 planner/coder/validator 三个 Org Agent,
task.status按状态机正确流转(2026-07-31 订正引用:原来写"对应架构文档 §11'手工调用验证'里程碑",但架构文档 §11 里根本没有这个短语;真正出处是 05 节 §5.5"上线顺序与兼容性"第 2 步:"再落地 Org Graph 层接口...此时可先用手工调用验证 Planner/Coder/Validator 三个 Org Agent 分别可被触发"——注意是"分别",即逐个验证能被触发,不是要求一次串联跑通带真实 Artifact 校验的完整链路,那是 05 节 §5.5 第 3 步"Work Graph 层集成"之后的事,此处不做) - 范围说明(2026-07-31,实现后补充):本 Unit 只落地
p_org_task_lifecycle(04-P)里由单次c_org_node_trigger调用直接触发的三段前向状态迁移——queued→planning_brd_prd_sdd(触发 planner)、planning_brd_prd_sdd→coding(触发 coder,需要 BRD/PRD/SDD/Benchmark 四类 Artifact 均validation_status=passed)、coding→validating(触发 validator,需要code_diff/test_report均validation_status=passed)。planning_failed/coding_failed重试、Validator PASS/FAIL 判定与bug_record/c_org_loop_route路由、blocked_pending_human升级,都需要真实 Work Graph 产出(U-011 起)或 Org-level Loop(U-015/U-016,PH2)才能实现,本 Unit 不做,也做不到——由于没有真实 Work Graph 产出 Artifact,测试里的 Artifact 校验前置条件是直接 seed 数据进去的,不是跑出来的,这是意料之中的(上游契约还没实现,消费方先按契约测) - 覆盖:03-C
c_org_node_trigger
PH1-S2 Work Graph 层集成(本项目风险最高的 Sub-phase)
U-011 [Value Path]
c_work_graph_plan实现(Planning Skill 调用,输出graph_manifest)- 依赖:U-004, U-010
- Done(2026-07-31 订正:原来写"REQ-02 happy 用例通过(Planner 产出 BRD/PRD/SDD/Benchmark 四类 artifact 且血缘完整)",但 03-C §3.4 里
c_work_graph_plan自己的契约明确写着"不执行,只规划"——它只产出graph_manifest(结构/计划),不产出真实 Artifact 内容;REQ-02 happy 用例要求的"四类 artifact 且血缘完整"需要真正执行(U-012/U-013)加上c_artifact_write(见下方缺口),单靠 U-011 做不到,也不该做,这是同一类 Done 标准过头的问题,U-009/U-010 已经出现过两次):c_work_graph_plan真实调用claude --agent planner -p产出一个通过结构校验的graph_manifest(四个节点对应 brd/prd/sdd/benchmark,依赖链 sdd→prd→brd 正确),落一条work_graph_run(status='planning')记录;REQ-02 happy 用例整体随 U-012/U-013 落地后累计满足 - 已知缺口(2026-07-31,实现时发现):
c_artifact_write(03-C §3.7)是 REQ-02 happy/failure 两个用例都直接依赖的控制器(failure 用例原话:"c_artifact_write应拒绝写入"),但翻遍 development-plan.md 全部 43 个 Unit,没有任何一个 Unit 的"覆盖"字段提到c_artifact_write——不是分散在别处,是真的没人认领。等做到 U-012/U-013(或不管哪个 Unit 第一个需要"把 Agent 写的文件登记成 artifact 行")时需要显式补上,不要假设它已经存在 - 技术发现(2026-07-31):
--agent和--json-schema同时使用时,--json-schema会被静默忽略(structured_output完全不出现),已记录到 05 节 §5.4.1.1,plan_work_graph改成从result里解析```json围栏代码块 - 覆盖:01-M
artifact/work_graph_run;03-Cc_work_graph_plan;REQ-02(happy 用例的"计划"部分,非全部)
U-012 [Value Path]
c_work_graph_execute完整实现(auto权限模式、USAGE_LIMIT_REACHED退避重试p_usage_limit_wait、CLASSIFIER_BLOCKED处理)- 依赖:U-006, U-011
- Done(2026-07-31 订正:原来写"REQ-07 全部用例通过",但 REQ-07 happy 用例原话是"Claude Code ACP 后端下完整跑通 REQ-01~06 全部场景"——这需要 REQ-04/05/06(Validator 根因归属、Org-level Loop、断点恢复)全部就绪,那是 PH2 的事,U-012 单靠 U-006+U-011 做不到,是同一类 Done 标准过头问题第四次出现):REQ-07 boundary(不加
--bare,冷启动开销可接受——U-006 已满足,本 Unit 未改变这点)和 failure(CLASSIFIER_BLOCKED检测正确路由)通过;happy 用例随 PH2 收口后累计满足。REQ-14 happy 用例通过(boundary/failure 场景在 PH2-S3/U-020 补齐完整覆盖)——这条原来就写对了,不用改 - 范围说明(2026-07-31,实现后补充):
p_usage_limit_wait退避重试落在整个execute_work_graph调用级别,不是逐个work_node_execution记录级别——U-013(c_work_node_run,真正拥有work_node_execution行)还没做,节点级别的额度等待追踪要等 U-013 才有意义。CLASSIFIER_BLOCKED判定用单次-p调用自带的permission_denials数组长度 ≥3 做近似(03-C 原话"连续 3 次或累计 20 次",跨调用的真正累计计数需要持久化,也是等 U-013) - 覆盖:03-C
c_work_graph_execute;04-Pp_work_graph_execution/p_usage_limit_wait;REQ-07(boundary/failure)/REQ-14(happy)
U-013 [Value Path]
c_work_node_run+git_worktree隔离(并行写代码节点)- 依赖:U-012
- Done(2026-07-31 订正:原来写"REQ-03 happy/boundary 通过",但 happy 用例("产出 code_diff 与 test_report...unit+integration test 全部通过")需要真实 Coder 会话写代码 +
c_artifact_write(U-011 已记录的缺口,至今没有 Unit 认领),U-013 单靠 U-012 做不到——第五次出现同一类问题):REQ-03 boundary 通过(两个独立节点各自用git worktree add隔离,run_fn各写一个文件并 commit,merge --no-ff合并回主分支,git log确认两次提交都在,且work_node_execution.git_worktree_ref各不相同);happy 用例随c_artifact_write落地后累计满足 - 精度提升(2026-07-31,实现后补充):03-C §3.6 给出了
USAGE_LIMIT_REACHED的精确探测正则/hit your (session|weekly|Opus) limit/i,比 U-010/U-012 当时用的短语近似匹配更准——U-013 按这条正则实现,且真正写回work_node_execution.quota_wait_started_at/quota_limit_type(U-010/U-012 阶段这两张表字段还没有真实归属)。c_work_node_run是内部接口(03-C 原话"不对外暴露 HTTP,供 c_work_graph_execute 调用"),退避重试从 U-012 的整图级别下沉到这里的单节点级别 - 覆盖:01-M
work_node_execution;03-Cc_work_node_run;REQ-03(boundary)
PH1-S3 端到端验收(收口)
- U-014 [Value Path] 完整跑通"实现贪吃蛇游戏"验收场景,从
goal_text到task.status=done- 依赖:U-011, U-012, U-013
- Done:架构文档 §11 MVP 验收标准第 1-3 条全部达成(Planner 出四件套,Coder 自测通过,Validator 归因产出 bug list)
- 实现补充(2026-07-31):这是前面 U-009~U-013 五次"Done 标准过头"里被推迟的 happy 用例真正兑现的地方。落地时顺手把 U-011 记录的
c_artifact_write(03-C §3.7)缺口补上了——src/artifact_write.py,之前没有任何 Unit 认领。编排代码是src/e2e_pipeline.py:plan_documents(真实调用 planner 依次产出 BRD→PRD→SDD→Benchmark,各自用c_artifact_write落盘+提交+建血缘)、implement_code(真实调用 coder,用会话前后的git diff作为code_diff、coder 自己的回复文本作为test_report)、validate_implementation(真实调用 validator,解析约定的VERDICT: PASS|FAIL|INCONCLUSIVE行)、run_end_to_end(串联全部四步) - 真实验证的范围(2026-07-31):
plan_documents这一段(4 次真实 planner 调用产出真实 BRD/PRD/SDD/Benchmark,血缘链验证)成本和之前几个 Unit 同量级,已经真实跑过。run_end_to_end的 Coder/Validator 那一半——真实写代码、真实跑测试——量级完全不同(可能是分钟级、真金白银的开销,而且结果不确定),没有不请示直接跑,等用户决定要不要真的跑一次完整闭环
PH2:质量闭环(Risk Gate,回路与护栏)
Goal:系统出错时能自主定位、自主修复,或安全地转人工,不会无限空转也不会裸奔超支。 Phase 验收:完整跑通一次 Org-level Loop(人为植入 bug → 定位 → 修复);REQ-04/05/06/08/09/10/14 全部通过。
PH2-S1/S2/S3/S4 之间除标注的依赖外相互独立,可并行推进。
PH2-S1 Validator 回路与根因溯源
U-015 [Risk Gate]
p_validator_root_cause_trace实现(三态判定 PASS/FAIL/INCONCLUSIVE,双向可追溯矩阵)- 依赖:U-013
- Done:REQ-04 happy/boundary/failure 通过
- 实现补充(2026-07-31):这是这几轮里少有的一次不用订正 Done 标准的 Unit——REQ-04 的三个用例都只依赖"给定 lineage 上下文后的多视角判定",没有牵扯到 U-015 自身依赖之外的能力。"血缘读取"(读 SDD/PRD/BRD/Coder 实现记录)刻意设计成调用方职责(
perspective_fn闭包自带上下文),judge_root_cause本身只管三视角≥2/3 多数表决。"双向可追溯矩阵"是 Validator 人设自己的内部推理框架(qa-task-validationskill + 迁移后的 AGENTS.md "claim—风险—实现—测试—证据矩阵",U-004 时已经迁移),不是本 Unit 需要单独建的后端能力 - 已知缺口,2026-07-31 U-018 已解决:
root_cause_category=skill_defect时 03-C §3.10 要求"必须同时创建 skill_iteration_suggestion",当时skill_iteration表要求的before_git_commit_sha/after_git_commit_sha还没有真实来源。做 U-018 时按 04-P §4.3 本身的状态表重新确认:bug_recorded(c_bug_record_create单独达成)和skill_iteration_triggered(触发c_skill_update)是两个先后独立的状态迁移,不是同一次调用——"必须同时创建"读成"同一条流程线里最终要有"而不是"同一次 API 调用里同步造出来",bug_record.skill_iteration_suggestion_id从 null 起步,c_skill_update跑完后回填。已放开bug_record.py接受skill_defect - 覆盖:01-M
bug_record;03-Cc_bug_record_create;04-Pp_validator_root_cause_trace;REQ-04
U-016 [Risk Gate]
c_org_loop_route+fast_lane/full_replan快速通道- 依赖:U-015
- Done:REQ-05/REQ-09 全部用例通过(含 3 轮上限 →
blocked_pending_human) - 实现补充(2026-07-31):顺手补上了
validating→done(validator_pass 分支,04-P 原话动作是"关闭任务",没有挂具体c_*控制器名字,之前也没有任何代码把task.status真正写成过done)——close_task_done(),和c_org_loop_route是同一个"validating 状态怎么退出"决策的另一半,REQ-05 happy 用例要求"最终 task.status=done",不补这个到不了终点 - 简化,如实标注(2026-07-31):04-P 表格把 fast_lane/full_replan 拆成两跳(
validating→routed_fast_lane→coding),但c_org_loop_route自己的 03-C 契约直接输出最终态{new_status, new_assigned_org_node},且没有第二跳的独立控制器名字——实现上把两跳合并成一次原子调用,routed_fast_lane/routed_full_replan这两个task.status枚举值实际不会被落库,改成写一条execution_event(event_type=loop_routed)记录路由决策本身。REQ-05/REQ-09 的验收文字都只看最终落点,不检查这个中间态,不影响 Done;以后 PH3 看板如果要真的展示这个瞬时状态,需要回头改 - 覆盖:03-C
c_org_loop_route;REQ-05/REQ-09
U-017 [Risk Gate] 人为植入 bug 完整闭环验证(架构文档 §11 验收标准第 4 条)
- 依赖:U-016
- Done:引入一个人为植入的 bug,系统自主定位根因并驱动 Planner/Coder 完成修复闭环
- 实现补充(2026-07-31):
src/org_level_loop.py的run_fix_loop把 U-015(三视角判定)+ U-016(路由)串成一个闭环:判定→建bug_record→路由→真实触发 Coder/Planner 修复→真实触发 Validator 复核→复核通过则close_task_done。category=coder→fast_lane、category=planning→full_replan是这里定的固定映射规则(bug_record.suggested_route本来是 Validator 独立判定的字段,但 REQ-05/架构文档 §11 的验收场景里两者天然对应,没有再额外调一次 Agent 单独决定 route)。真实验证复用了 U-015 那个撞墙误判场景,但这次真的让 Coder 编辑仓库里的collision.py文件、真的让 Validator 读文件复核,不是只判定不动手
PH2-S2 Skill 自主迭代
- U-018 [Risk Gate]
c_skill_update+c_skill_rollback+ Git commit 留痕- 依赖:U-015(需要
root_cause_category=skill_defect触发) - Done:REQ-08 全部用例通过
- 实现补充(2026-07-31):Skill 文件物理落在 graphlemon 平台自己仓库(
skills/<skill_id>/,架构文档 §13.1 平台资产,和 U-004 时agents/的归属判断一致),不是目标项目——repo_path参数没有默认值,强制调用方每次显式传,避免测试不小心忘记覆盖就误提交进 graphlemon 自己的真实仓库(写代码时先犯过这个错,当场改掉了)。c_skill_rollback不是历史重写,是"读回滚前那个 commit 的内容,作为一次新的 commit + 新的skill_iteration记录"提交,回滚记录的triggered_by_bug_id复用被回滚那次迭代原本的 bug_id(c_skill_rollback输入契约本来就没有单独给 bug_id 的位置,skill_iteration.triggered_by_bug_id又是非空 FK,只有这一个来源可用) - 已知缺口,不是本 Unit 的事:Hermes 迁移过来的 Skill 内容本身还没有真正搬过来(05§5.1 只列了要迁移的技能名字,
agent_profile.py的skills:frontmatter 目前只是引用名字,skills/<skill_id>/在仓库里还没有真实文件)——测试里跟 U-010/U-011 对没建成的上游产物的处理方式一样,自己 seed 一个占位 Skill 文件 - 覆盖:01-M
skill/skill_iteration;03-Cc_skill_update/c_skill_rollback;REQ-08
- 依赖:U-015(需要
PH2-S3 护栏双层机制
U-019 [Risk Gate]
budget_guardrail_config+c_budget_check(人工配置层)- 依赖:U-012
- Done:REQ-10 happy/boundary 通过
- 实现补充(2026-07-31):boundary 用例(调高额度后
paused态任务恢复调度)需要的"改配置+恢复调度"能力,本 Unit 只建了最小可用版本update_budget_limit_and_resume——完整设置页控制器c_budget_config_update(含完整字段校验、UI)是 U-030 的事;这里直接复用 U-016 的manual_resume(org_loop_route.route_org_loop)做实际的状态恢复,没有重新实现一遍。c_budget_check超限时直接抛BudgetExceededError并把blocked_reason设成budget_exceeded(不是返回allowed=false让调用方自己处理),对齐 03-C 原文"触发后对应任务转 blocked_pending_human"这句话描述的是c_budget_check自己的副作用 - 覆盖:01-M
budget_guardrail_config;03-Cc_budget_check;04-Pp_budget_guardrail;REQ-10
U-020 [Risk Gate]
p_usage_limit_wait完整验收(session/weekly/opus三种quota_limit_type+ 超时兜底)- 依赖:U-012(骨架已在 U-012 实现,本 Unit 补齐边界/失败场景)
- Done:REQ-14 全部用例通过(含超时转
blocked_pending_human, blocked_reason='usage_limit_wait_timeout') - 实现补充(2026-07-31):这次不只是补测试——重新对照 REQ-14 boundary 用例原文发现,U-013 时
work_node_run.py的 opus 处理只做到"不退避",没做到"直接降级到非 Opus 模型重试"(原来是探测到 opus 额度直接抛错,从没真正重试过)。补上了:run_fn签名改成run_fn(cwd, model=None),探测到 opus 信号后立即(不等待)用opus_downgrade_model(默认claude-sonnet-5)重试一次;如果降级后还是收到 opus 信号(不合理,非 opus 模型不该撞 opus 专属额度),直接判超时,不会无限重试。顺手把之前漏测的weekly额度类型(探测恢复 + 9 天超时)也补齐了。work_graph_execute.py(U-012)里同名的 opus 立即失败逻辑没有跟着改——REQ-14 覆盖列表只写了c_work_node_run/c_org_node_trigger,没有c_work_graph_execute,而且那条路径是多 Agent 动态工作流,没有一个单一的"the model"可以整体降级,属于更深的 A-1 动态工作流问题,本 Unit 不动 - 覆盖:04-P
p_usage_limit_wait;REQ-14
PH2-S4 断点恢复
- U-021 [Risk Gate] Graph Engine 持久化状态 + 断点恢复
- 依赖:U-012, U-013
- Done:REQ-06 happy/boundary/failure 通过(执行中途中断进程后从持久化状态恢复,不重跑已完成节点)
- 实现补充(2026-07-31):核心问题是 U-013
run_work_node的now默认值是time.monotonic——单调时钟在进程重启后没有固定的墙钟含义,REQ-06 恰恰要求"进程重启后继续计算 quota 等待已经过去多久",两者根本不兼容。改法:now默认改回time.time(墙钟),新增_parse_sqlite_timestamp()把 SQLiteCURRENT_TIMESTAMP的 UTC 字符串转成可比的 epoch 值;run_work_node一开始就读出该 node_exec_id 已持久化的status/retry_count/quota_wait_started_at/git_worktree_ref,status='done'直接返回存量output_payload(不重跑run_fn),retrying/waiting_for_quota从持久化的retry_count/quota_wait_started_at续算而不是清零重来,needs_worktree=True时若已存在对应 worktree 目录则复用而不是重新git worktree add(会直接失败)。新增resume_work_graph_run(conn, run_id):返回一次work_graph_run里还没done的node_exec_id列表,供调用方决定重新调度谁;对内部不一致的持久化状态(无效的work_graph_run.status/节点status,status='done'但output_payload为空,status='waiting_for_quota'但quota_wait_started_at为空)统一抛CorruptPersistedStateError,不会返回一份看似正常但实际有问题的恢复计划。这几类"状态不一致"用正常写入路径其实到不了(各字段都有CHECK约束),测试里用PRAGMA ignore_check_constraints=ON模拟外部损坏的行来触发。 - 覆盖:01-M
work_graph_run/work_node_execution;04-Pp_work_graph_execution;REQ-06
PH3:人机交互控制台(Value Path,可观测性与操作入口)
Goal:人可以通过浏览器创建项目、提出目标、反馈 bug、实时看到运行过程、管理 Skill 库。
Phase 验收:REQ-11/12/13 全部通过。
并行关系:可在 PH1 完成后与 PH2 并行开工(控制台是对已产生数据的可视化/操作层,不阻塞回路/护栏本身的开发逻辑)。PH3-S2(实时看板)分两级:U-024(推送链路本身)只需 U-012/U-013 落地即可开工,比原计划更早;REQ-12 的完整验收挪到 U-024.5,需要排在 PH2 大部分 Unit 完成、execution_event 各类场景都已产生真实数据之后再联调。
PH3-S1 项目与目标入口
U-022 [Value Path]
v_project_setup+c_pick_folder全平台补齐(WindowsPowerShell/Linuxzenity/kdialog)- 依赖:U-007, U-008
- Done:REQ-11 happy/boundary/failure 通过
- 实现补充(2026-07-31):Done 行字面写的是"REQ-11 全部通过",但 REQ-11 happy 用例完整走完还需要
v_goal_composer/c_task_create创建 task 那一步——那是 U-023 的范围,本 Unit 依赖只到 U-007/U-008,不可能单独满足。本 Unit 实际完成的是 REQ-11 里v_project_setup/c_pick_folder/c_project_create那部分:①pick_folder()补齐 Windows(PowerShellFolderBrowserDialog脚本)/Linux(zenity优先,缺失则退回kdialog,都不在则DIALOG_UNAVAILABLE);②c_project_create(POST /api/projects)此前只有 U-002 写好的project_scaffold.create_project()函数本体、从未接到 HTTP 层(backend_server.py因此第一次需要持有一个 sqlite 连接——ThreadingHTTPServer按请求起线程,给schema.connect()加了check_same_thread=False参数 + 一把server.conn_lock避免并发请求踩到同一个连接);③frontend/app.js补上#/setup真实交互(选文件夹 → 校验 Git 仓库 → 非 Git 仓库时二次确认 → 创建 → 跳转仪表盘),DOM class 命名对齐docs/wireframes/v_project_setup.html。REQ-11 的完整 happy 用例(含创建 task 那一步)等 U-023 落地后再验收。浏览器扩展在本次会话不可用,UI 部分改为对着真实运行的后端做完整 HTTP 级冒烟(curl走一遍选文件夹之外的全部接口调用序列,node --check过一遍app.js语法),不是真人点击浏览器验证过的。 - 覆盖:02-V
v_project_setup;03-Cc_pick_folder;05 节 §5.4.1;REQ-11
U-023 [Value Path]
v_goal_composer+v_bug_feedback- 依赖:U-022
- Done:能通过 UI 创建
task和人工发起的bug_record(c_bug_report_create) - 实现补充(2026-07-31):①
c_task_create(U-009 已实现的task_create.create_task())第一次接上POST /api/tasks;②c_bug_report_create(03-C §3.1.1)此前完全没有实现,本 Unit 新增bug_record.create_bug_report()+POST /api/bug-reports。发现并解决一个 03-C 内部小缺口:c_bug_report_create的input_contract只列了{description, repro_steps, related_task_id},没有project_id;但execution_event.project_id是 NOT NULL(01-M 1.9),bug_record表自己又没有project_id列可以在related_task_id留空时兜底推导——"留空则视为新发现的独立问题"(02-V §2.4)这条路径字面上无法满足execution_event的约束。判断这是文档遗漏而非架构决策点(同类判断依据见 U-022 的c_project_createHTTP 接线、U-014 的c_artifact_write),直接让project_id和c_task_create一样成为必填参数,未停下来单独确认。③ 控制台目前没有"当前项目"这个概念的载体(v_dashboard是 U-025 才做),v_goal_composer/v_bug_feedback需要的project_id暂存到localStorage(v_project_setup创建成功时写入),没有选中项目时两个页面直接引导回#/setup。④related_task_id在 02-V 原型里是下拉选择器,但c_task_list(列出当前项目下的任务供选择)没有被任何 Unit 认领,本 Unit 简化成纯文本 task_id 输入框,避免顺手做一个不属于自己范围的接口;等 U-025 补上任务列表后再换成真下拉。⑤ 提交成功后 02-V 写的是"跳转v_task_detail",但那个页面是 U-025 才建,本 Unit 改成在原页面展示一条带 task_id/bug_id 的确认信息 + 返回仪表盘链接,等 U-025 落地后再切回真跳转。⑥ 对着真实运行的后端做端到端冒烟时发现一个真 bug:create_bug_report()一开始没校验project_id/related_task_id是否存在,传入不存在的project_id会在execution_event插入时触发未捕获的sqlite3.IntegrityError,请求直接 500(无干净 JSON 错误体)——补了存在性校验,改为清晰的INVALID_PARAM/400,并加了回归测试。 - 覆盖:02-V
v_goal_composer/v_bug_feedback;03-Cc_task_create/c_bug_report_create
PH3-S2 实时看板
U-024 [Value Path]
c_ws_subscribe+execution_event实时推送(最小连通版)- 依赖:U-012, U-013(work graph 执行、work node run 是最早、最稳定产出
execution_event的两个来源,足够验证推送链路本身是否通) - Done:
c_ws_subscribe对真实execution_event做实时推送,≤2 秒延迟达成;REQ-12 happy 用例通过(boundary/failure——尤其是断线重连拉快照在多样事件场景下的表现——留给 U-024.5 补齐完整覆盖) - 实现补充(2026-07-31):Python stdlib 没有 WebSocket 支持,
src/ws_subscribe.py手写了 RFC 6455 握手(Sec-WebSocket-Accept用 hashlib+base64 算)+ 帧编解码(socket原始读写,服务端帧不加掩码,客户端帧强制解掩码),没有引入websockets之类的第三方库(05 §5.7)。推送机制是轮询,不是写路径 hook——execution_event的写入分散在org_node_trigger/work_graph_execute/work_node_run/task_create/bug_record等十几个调用点,没有单一写入路径可以插桩,Unit 名字里的"最小连通版"本身也在暗示这个方向;_handle_ws_subscribe用ThreadingHTTPServer劫持连接后每 0.5 秒查一次execution_event(按rowid增量,project_id/可选task_id过滤),轻松满足 REQ-12 的 ≤2 秒目标(该数字本身写的是"建议"而非硬 SLA)。03-C §3.14 自己写明断线重连走 REST 快照补齐、不在 WebSocket 层做消息重放缓冲,所以新连接只看"连接那一刻起"的新事件,不做历史重放。给ThreadingHTTPServer加了daemon_threads = True,避免长连接的 WS 线程阻塞进程退出。端到端验证:src/test_backend_server.py用原始 socket 走真实握手+推送延迟断言;另外用 Node 原生WebSocket客户端(跨实现的真实互操作性验证,不是同一套代码自己测自己)连真实运行的服务器,从建连到收到c_task_create触发的真实task_status_changed推送全程走通。 - 覆盖:01-M
execution_event;03-Cc_ws_subscribe;REQ-12(部分)
- 依赖:U-012, U-013(work graph 执行、work node run 是最早、最稳定产出
U-024.5(2026-07-30 新增)[Value Path] REQ-12 完整验收(事件多样性联调)
- 依赖:U-024, U-015, U-016, U-018, U-019, U-020, U-021(需要 PH2 大部分 Unit 已上线,事件类型覆盖足够丰富才能真正验证 boundary/failure 场景——例如 usage-limit 等待、护栏拦截、Skill 回滚各自产生的
execution_event都要真实出现过) - Done:REQ-12 全部 happy/boundary/failure 用例通过;PH3 Phase 验收里的 REQ-12 以本 Unit 为准,U-024 本身只解决"链路通不通"
- 实现补充(2026-08-01)——U-031 回归审计中发现的真实缺口:核对本 Unit 是否真的 Done 时,重新对照 06 节 REQ-12 的 boundary 用例原文——"重连后能补齐断线期间遗漏的关键状态(通过重新拉取 task/work_graph_run 当前快照兜底)"——grep 了
frontend/app.js里所有onStatusChange用法,发现只有v_dashboard做了一点反应(重连时把断线提示 banner 隐藏),而且连它自己也没有真的重新拉取数据;v_task_board/v_task_detail(含内嵌的v_graph_visualizer)的onStatusChange全是空函数() => {}。也就是说这条 boundary 用例此前从未被真正实现过,U-024/U-024.5 之前的"REQ-12 完整通过"是不准确的。修复:mountDashboard/mountTaskBoard/mountTaskDetail三处的connectEventSocket都加了wasDisconnected标志,在状态从disconnected转回connected时调用各自页面已有的刷新函数(refreshTasksAndOrgGraph+refreshBudget/refresh/refresh)重新拉取当前快照。v_artifact_browser的onStatusChange保持空——REQ-12 命名的验收范围只到v_dashboard/v_graph_visualizer/v_task_detail,artifact 浏览器不在其中,不属于这次修复范围。浏览器扩展本次会话不可用,验证方式:node --check过语法 + 全量unittest回归(361 tests);WS 重连本身的行为逻辑此前已被src/test_backend_server.py/Node 原生 WebSocket 端到端验证过(U-024/U-027),这次只是前端消费侧新增的刷新调用,未改动服务端重连协议本身。 - 覆盖:REQ-12(完整)
- 依赖:U-024, U-015, U-016, U-018, U-019, U-020, U-021(需要 PH2 大部分 Unit 已上线,事件类型覆盖足够丰富才能真正验证 boundary/failure 场景——例如 usage-limit 等待、护栏拦截、Skill 回滚各自产生的
U-025 [Value Path]
v_dashboard+v_task_board+v_task_detail- 依赖:U-024
- Done:三个页面可正常展示实时数据(原文档缺 Done/覆盖行,2026-07-31 补上)
- 实现补充(2026-07-31):这三个页面需要的读接口此前完全不存在,本 Unit 顺带把它们建了:①
c_task_list/c_task_get(03-C §3.2,此前无 Unit 认领)新增src/task_query.py,c_task_get的返回比 03-C 字面写的"task + artifact 列表 + bug_record 列表"更完整——额外带上work_graph_run(含解析后的graph_manifest)+ 每个 run 的work_node_execution列表,因为v_task_detail的 to-do 树(01-M 1.3 补充说明、02-V §2.6 2026-07-30 新增)就是要渲染这份数据,字面契约低估了这个视图真正需要的东西;②c_org_loop_route(03-C §3.11,U-016 已实现route_org_loop()但从未接 HTTP)第一次接上POST /api/tasks/:task_id/route,v_task_detail的"人工干预(manual_resume)"按钮需要它;③ 预算护栏没有任何读接口(03-C §3.13 只写了 PUT,读是 U-030 配置页的隐含前提但没人建),新增budget_guardrail.list_budget_configs()+GET /api/settings/budget给仪表盘的预算条用。前端:frontend/app.js补上真实的#/(org graph 三节点卡片、当前任务概要、预算条、事件时间线,全部通过c_ws_subscribe实时刷新)、#/tasks(关键字/状态/org_node 过滤 + 点击行跳详情)、#/tasks/:task_id(task 字段、to-do 树、关联 artifact/bug_record、blocked_pending_human告警条 + manual_resume 按钮);路由改成对象形式支持挂载态清理(registerRouteCleanup),每次切页面先关掉上一个页面开的 WebSocket,避免连接堆积。v_task_detail里v_graph_visualizer(DAG 视图)和v_log_viewer(原始日志)两个子模块明确标成"随 U-026/U-027 补上"的占位提示,不是假造内容。顺带把 U-023 里因为c_task_list当时还不存在而简化成纯文本输入的related_task_id换回了真下拉(v_goal_composer/v_bug_feedback提交成功后也从"页面内展示确认信息"改回 02-V 原文写的"跳转v_task_detail",这个页面现在是真的了)。浏览器扩展本次会话不可用,验证方式:全部新增 HTTP 路由的真实请求/响应断言(src/test_backend_server.py)+ 对着真实运行的服务器用curl/Node 原生WebSocket/fetch走一遍数据形状(尤其是 to-do 树需要的嵌套 JSON 结构,逐字段核对过 JS 渲染逻辑会怎么消费它)+node --check过语法,不是真人点击浏览器验证过的。 - 覆盖:02-V
v_dashboard/v_task_board/v_task_detail;03-Cc_task_list/c_task_get/c_org_loop_route(HTTP 接线)
U-026 [Value Path]
v_graph_visualizer(Org Graph + Work Graph 双层可视化,含waiting_for_quota信息态展示)- 依赖:U-024
- Done:Org/Work Graph 双层可视化含
waiting_for_quota信息态(原文档缺 Done/覆盖行,2026-07-31 补上) - 实现补充(2026-07-31):纯前端 Unit,不需要新接口——数据已经在 U-025 的
c_task_get里(work_graph_run.graph_manifest+work_node_execution列表),只补了两个 hover tooltip 要用但 U-025 当时没取的字段(input_schema_ref/output_schema_ref/quota_limit_type/quota_wait_started_at,见task_query._work_nodes_for_run)。frontend/app.js新增computeGraphLayers():按graph_manifest节点的depends_on边做拓扑分层(没有依赖的节点在第 0 层,每个节点的层号 = 1 + 依赖节点里最大层号),渲染成按执行层级排布的行(work-row),fan-out/fan-in 都能正确分层——用真实运行的服务器 + 一个 fan-out/fan-in 混合的graph_manifest(n1→[n2,n3]→n4)验证过,直接从app.js里eval出computeGraphLayers本体(不是重新实现一份逻辑再测)跑真实拿到的 JSON,分层结果[["n1"],["n2","n3"],["n4"]]完全正确。节点按 03-C §3.6 的六态配色(done/running/reflecting|retrying/waiting_for_quota/blocked|failed/pending),waiting_for_quota额外挂data-quota-limit-type/data-quota-wait-since,hover tooltip 展示 skill/quota_limit_type/已等待时长(客户端用quota_wait_started_at和Date.now()算,服务端只存原始时间戳不算相对时长)。02-V §2.7 原描述还包含一份 Org Graph 层,但v_task_detail(U-025)页面顶部已经有等价的org-strip面板(同款 class/data 结构),紧接着 to-do 树下面再画一份完全一样的东西纯属重复滚动,所以这里只做了 Work Graph DAG 层,Org Graph 复用页面已有的那个——不是漏做,是判断为无意义重复后跳过,写在这里说明。嵌入位置:v_task_detail里原来"随 U-026 补上"的占位提示换成了真实的 DAG 面板。浏览器扩展本次会话不可用,验证方式同 U-025:真实服务器 + 真实数据 + 直接跑生产代码里的分层函数,不是真人点击浏览器验证过的。 - 覆盖:02-V
v_graph_visualizer;03-C 3.6 节六态配色语义
PH3-S3 日志与 Artifact/Skill 管理
U-027 [Value Path]
execution_log_file+c_execution_log_get+c_ws_log_tail+v_log_viewer- 依赖:U-024
- Done:REQ-13 happy/boundary/failure 通过
- 实现补充(2026-07-31):
execution_log_file的行生命周期(active→closed)U-013 早就管好了,但从来没有代码真的往日志文件里写过内容——src/execution_log.py新增append_log()补上这条缺失的写路径,设计成run_fn(真正干活的那个函数)在执行过程中随时可调用,不关心节点在做什么,和work_node_run.py自己对run_fn划的边界一致。get_log()按 offset/limit 分页读,download=true时走原始文件流。c_ws_log_tail(WS /api/ws/work-nodes/:node_exec_id/log)复用 U-024 手写的 RFC 6455 机制,同样是轮询execution_event(event_type=log_appended);按 03-C §3.6.2 原文要求,节点一旦转终态(done/blocked/failed)服务端主动发关闭帧断开连接,不是让客户端一直轮询下去。p_log_retention_cleanup(04-P §4.5)实现成一个纯函数run_log_retention_cleanup()——过期扫描 +max_total_size_mb超限时按最旧优先追加候选 + 单个文件删除失败只记录不阻塞其余项,全部按 04-P 原文逻辑写;没有配调度器/cron(那不在本 Unit 覆盖范围内,c_execution_log_get/c_ws_log_tail才是),留给未来的调度进程周期调用。过程中意外抓到一个真的并发 bug:backend_server.py的_Server.server_close()关闭共享 sqlite 连接时原来没拿conn_lock,如果这时候正好有个 WS 轮询线程在另一边持锁执行 SQL,两个线程无同步地并发操作同一个 C 级 sqlite3 连接对象,实测直接把解释器崩了(exit code 139,SIGSEGV)——不是新代码引入的,U-024 就已经有这个隐患,只是这次高频率的 WS 测试把它的时序窗口撞开了;修好后连续跑几十次全部稳定通过。前端:frontend/app.js的v_log_viewer挂在v_task_detail每个 to-do 节点的展开详情里(不是页面底部一个孤立面板,node_exec_id对应哪个节点这份日志就该长在哪个节点旁边),首次展开才建连接(懒加载),运行中态走c_ws_log_tail实时滚动、终态走c_execution_log_get一次性静态拉取;活跃的 log-tail 连接会在任务级刷新/离开页面时统一关闭,避免连接泄漏。真实服务器 + Node 原生WebSocket/fetch端到端验证过实时追加与终态自动断开的完整链路(静态回填先拿到已有内容,WS 只推增量,二者拼起来等于最终静态内容)。 - 覆盖:01-M
execution_log_file/log_retention_config;02-Vv_log_viewer;03-Cc_execution_log_get/c_ws_log_tail;REQ-13
U-028 [Value Path]
v_artifact_browser- 依赖:U-011
- Done:Artifact 列表+预览+血缘链可用(原文档缺 Done/覆盖行,2026-07-31 补上)
- 实现补充(2026-07-31):
c_artifact_get在机器可读的sdd-mvcp-contract.json里其实定义过(GET /api/artifacts/:artifact_id),只是 03-C 的正文一直没写出来;c_artifact_list连 MVCP JSON 里都没有 operation 定义,纯粹是v_artifact_browser自己的过滤字段(artifact_type_filter/task_id_filter)倒逼出来的缺口,和 U-025 补c_task_list是同一类"读接口没人建"问题。新增src/artifact_query.py:list_artifacts()返回裸数组(跟list_tasks()保持同款风格,不跟 MVCP JSON 里c_task_list的{items,total}包一层——本地单项目 MVP 不需要分页,跟自己代码库已有约定保持一致比跟一份机器生成的镜像文件保持一致更重要);get_artifact()顺带把血缘链(lineage_chain(),按depends_on_artifact_ids往回走,多个依赖时只走第一个——原型图和这条流水线的实际产出都只有线性 BRD→PRD→SDD 链,没有真正的多依赖分叉场景)和文件内容(artifact表本身没有 content 列,内容活在磁盘上,走project.target_folder_path+artifact.file_path拼出绝对路径现读)一起返回,预览面板一次请求就够。前端v_artifact_browser的预览统一用<pre>纯文本展示,不做 Markdown 渲染/代码高亮——02-V 原文想要那个效果,但这需要引入第一个前端第三方库(图表/高亮/markdown 解析器),和这个控制台"纯 vanilla JS、无构建步骤"的一贯做法(05 §5.7 精神延伸到前端)冲突,判断为有意简化而非漏做。顺手把frontend/index.html的常驻导航栏加上了"产物"链接(之前只有 Dashboard/Tasks/Settings 三个占位项)。真实服务器验证过三级血缘链(BRD→PRD→SDD)返回顺序正确。 - 覆盖:02-V
v_artifact_browser;03-Cc_artifact_get(正文订正补全)+ 新增c_artifact_list
U-029 [Value Path]
v_skill_library+v_skill_audit_log- 依赖:U-018
- Done:Skill 库与变更审计日志可查看(原文档缺 Done/覆盖行,2026-07-31 补上)
- 实现补充(2026-07-31):
c_skill_diff和 U-028 的c_artifact_get一样,只存在于机器可读的sdd-mvcp-contract.json(GET /api/skills/diff?before=&after=),03-C 正文从未写出来,本 Unit 订正补全;list/get 系读接口(c_skill_list/c_skill_get)两份文档都没定义,跟c_task_list/c_artifact_list是同一类"视图倒逼出来的缺口"。新增src/skill_query.py:list_skills()、get_skill_audit_log()、get_diff()(git diff <before> <after>——skill_update.py的_write_and_commit把每次迭代做成原子的单文件 commit,所以整仓库范围的 diff 天然就只涉及这一个 Skill 文件,不需要额外传 path 过滤)。关键安全考量:Skill 库活在 graphlemon 自己的仓库里(不是某个 target project 的),c_skill_rollback会真的在这个仓库里跑git commit——如果 HTTP 层写死指向真实仓库根目录,任何针对这条路由的测试都会在正在跑的这个 graphlemon 仓库里留下真实提交。参照db_path已有的做法,给create_server()加了skill_repo_path参数(默认skill_query._REPO_ROOT,即 graphlemon 真实仓库根,生产环境下这就是对的行为;测试传一个 tempfile 建的临时仓库,绝不碰真实历史),不通过 HTTP 暴露这个参数(只在进程/测试构造时可设,避免变成一个可以让客户端指定任意写入目标的口子)。真实服务器 + 临时仓库端到端验证过完整的 update→diff→rollback 闭环,并且确认过跑完之后 graphlemon 自己仓库的 git log 完全没变化。前端v_skill_library/v_skill_audit_log两个页面不检查"当前项目"(Skill 是平台级资产,跟项目无关,是这个控制台里唯一不需要getCurrentProjectId()守卫的真实页面)。 - 覆盖:02-V
v_skill_library/v_skill_audit_log;03-Cc_skill_diff(正文订正补全)+ 新增c_skill_list/c_skill_get;c_skill_rollback(U-018 已实现rollback_skill())首次接上 HTTP
PH3-S4 配置页
- U-030 [Value Path]
v_concurrency_settings+v_budget_settings+v_log_retention_settings- 依赖:U-019(预算配置)、U-021 涉及的
log_retention_config(U-001 已建表,此处只是配置页 UI) - Done:三个配置页可用且改动生效(原文档缺 Done/覆盖行,2026-07-31 补上)
- 实现补充(2026-07-31):三个都是
config_id='default'单例配置,首次使用即创建、之后原地更新(upsert),不需要单独的"初始化"流程。①c_concurrency_config_update:新增src/settings_config.py(org_concurrency_config之前完全没人碰过),校验max_concurrent_goals落在 1-50;②c_budget_config_update:budget_guardrail.py里新增upsert_budget_config(),跟 U-019 已有的update_budget_limit_and_resume()(只更新已存在的行,不建新的)不是一回事——upsert_budget_config()是真正完整的设置页控制器,支持首次创建,而且沿用了同一套"额度调高后自动恢复budget_exceeded阻塞任务"的副作用(04-P §4.4),复用了同一段恢复逻辑(提取成_resume_budget_blocked_tasks()共享);③c_log_retention_config_update:同样在settings_config.py里,校验retention_days > 0。三处的数值/枚举校验都在业务层做完整检查(而不是让请求体里的None/超范围值直接怼到 SQL 层,被动等CHECK约束或TypeError崩掉),HTTP 层额外兜了一层except (InvalidParamError, TypeError)——payload 里数值字段缺失时None <= 0这类比较会抛TypeError而不是业务异常,两个都要接住才能保证返回干净的 400 而不是裸 500。backend_server.py 第一次加了do_PUT(之前只有 GET/POST)。前端三个页面共享一个renderSettingsSubnav()小组件模拟 02-V 原型里的"设置"下拉菜单(这个控制台的单层#nav顶栏放不下嵌套下拉,属于纯 UI 简化,不影响信息结构)。真实服务器端到端验证过三个 PUT/GET 的完整读写循环。 - 覆盖:02-V
v_concurrency_settings/v_budget_settings/v_log_retention_settings;03-Cc_concurrency_config_update/c_budget_config_update/c_log_retention_config_update
- 依赖:U-019(预算配置)、U-021 涉及的
PH4:全量验收与固化(Risk Gate,收口)
Goal:确认 MVP 验收标准(架构文档 §11)全部达成,系统可以长期无人值守运行。 Phase 验收:06-验收与测试锚点.md 全部 14 条 REQ 的全部用例(happy/boundary/failure)完整回归通过一次。
U-031 [Risk Gate] REQ-01~14 全量自动化回归套件
- 依赖:PH1/PH2/PH3 全部 Unit
- Done:06 节全部 14 条 REQ 的全部用例通过一次完整回归(原文档缺 Done/覆盖行,2026-08-01 补上)
- 实现补充(2026-08-01)——发现并修复一个结构性集成缺口,这是本 Unit 目前为止最重要的产出:逐条核对 REQ-01~14 验收用例对应的测试覆盖时,发现 REQ-02/REQ-03 验收锚点明确列出的控制器覆盖(
c_work_graph_plan/c_work_graph_execute/c_work_node_run)——也就是"Org Graph 触发 → Work Graph 规划 → Work Graph 执行 → 每个 Work Node 独立运行"这套两层架构——虽然在 U-011/U-012/U-013/U-020/U-021 各自都建好、也各自做过完整单元测试(U-013/U-021 甚至做过真实 git worktree 验证),但plan_work_graph()/execute_work_graph()/run_work_node()这三个函数彼此之间、以及和唯一真正跑通验证过的闭环(U-014 的e2e_pipeline.py,snake-game 那次真实生成)之间,从未被真正接起来过——e2e_pipeline.py走的是完全独立、更简单的路径(对 Planner/Coder/Validator 各打一次org_node_trigger.trigger_org_node()直连调用),完全绕开了这套两层架构。这个决策已跟用户确认(AskUserQuestion),选择立即补上集成而不是把现状订正进文档了事,因为 REQ-06(断点恢复)的验收要求"重启后不重跑已完成节点",只有work_node_run.py的按节点 Python 层持久化状态能做到,这和"整个 Work Graph 包在一次不透明的claude -p调用里"(execute_work_graph()原本设想的用法)根本互斥,所以这个缺口不是"锦上添花",是 REQ-06 能不能算真正满足的前提。 - 设计:新增
src/work_graph_orchestrate.py,每个 Work Node 的真实执行走它自己单独的一次动态工作流调用(write_workflow_script+execute_work_graph),不是把整张图塞进一次调用——这是刻意的取舍,牺牲了 03-C §3.5 原本"多节点编排摊薄到一次调用里,编排本身不消耗 token"的效率立意,换来 REQ-06 要求的按节点级持久化/断点恢复。topological_order()对graph_manifest做拓扑排序,同时读depends_on和edges并合并(原因见下一条发现)。build_prompt(node, outputs)在拓扑序上逐节点调用,outputs里已经有该节点全部上游依赖的真实产出内容,天然支持"prd 的 prompt 需要嵌入 brd 的真实内容"这种链式依赖,也天然支持断点恢复(run_work_node对已done节点直接返回持久化结果,恢复执行时上游内容照样能取到)。 work_node_run.py补了一个真实 bug:原来只捕获_UsageLimitSignal,任何其他异常(比如新集成会遇到的ClassifierBlockedError)会直接从run_work_node()冒泡出去,work_node_execution.status永远停在running,没有任何"为什么卡住了"的记录。改成任何非 usage-limit 异常都先把节点标记blocked再重新抛出。- 真实调用验证时连续发现两个集成 bug,都已修复:① 第一次真实跑
plan_work_graph()时,模型返回的graph_manifest每个节点用的是id而不是文档要求的node_id,而且完全没填任何节点的depends_on(依赖关系只体现在edges里)——plan_work_graph()本身从来没做过 schema 校验(--json-schema配--agent不生效,05 §5.4.1.1 早就记录过),这是真实模型输出会偏离文档契约的活例子。新增work_graph_plan._normalize_graph_manifest(),在 parse 完、持久化前统一把id→node_id、从edges反推缺失的depends_on——这样不只是我这个新集成受益,v_task_detail的 to-do 树、v_graph_visualizer的 DAG 分层以后遇到真实(而不是测试里手写整齐的)manifest 时也不会因为字段名对不上而显示错误。② 修好第一个 bug 后,第二次真实跑又炸了:work_graph_orchestrate.run_graph()调run_work_node()时忘了传repo_path,cwd解析成None,写工作流脚本直接崩——两次真实调用暴露的都是"单元测试全绿但没人真正把这几个函数接起来跑过"的典型症状,修完重跑,真实 4 篇文档(BRD→PRD→SDD→Benchmark)全部生成成功、血缘链正确、内容长度合理。 agentType+schema能否在动态工作流的agent()调用里正确共存,是这次集成最大的未知数,做了两次独立真实验证:先用一个最小 spike(一个只有一次agent({agentType:'planner', schema:{...}})调用的一节点工作流)验证——不仅structured_output正确返回,追问角色身份时模型明确自称"Planning Agent (PL)",说明迁移后的 SOUL.md 人设真的通过agentType正确加载了(不是走到某个通用 agent);后续完整plan_documents()真实跑通又在真实的四节点场景下再次确认。记录在 05 §5.4.1.5。- Coder 阶段的范围决策:REQ-03 的验收锚点只写了
c_work_node_run,没写c_work_graph_plan/c_work_graph_execute——因为"规划一个真正的多模块代码图"是另一个完全不同的 LLM 能力,这次没有人要求建,也没有 SDD 规定怎么建,属于超出这次集成范围的新能力,不是"补上就完事"的小活。implement_code()因此只用一个合成的单节点 manifest 走work_graph_orchestrate.run_graph(),让c_work_node_run对 Coder 也真正生效,但没有尝试造一个"Coder 图规划器"。needs_worktree参数留在run_graph()里但暂时两处调用都没启用——worktree 隔离要求.claude/agents/<org_node>.md已经提交到分支上(git worktree add只看已提交内容,deploy_agent_definitions()目前只写工作区文件不 commit),这个"部署后要不要顺手提交"的问题这次没有一并解决,留作后续发现,不是遗漏是有意不在这次范围内展开。真实验证也只覆盖了 Planner 全流程(Coder/Validator 走同一套机制,机制本身已经被 Planner 的真实跑通证明,没有再花真钱跑一次完整 Coder 会话,跟test_e2e_pipeline_e2e.py一贯的成本节制原则一致)。 - Validator 阶段未改动:REQ-04 的覆盖从来没提过 Work Graph 控制器(只有
c_bug_record_create/p_validator_root_cause_trace),validate_implementation()原来的直连调用已经如实对应它自己的 REQ,不需要动。 - 实现补充(2026-08-02)——PH5 开工前对 U-031 自己的 Done 标准("06 节全部 14 条 REQ 的全部用例通过一次完整回归")做逐条复核,而不是只信任"全部单测通过":CLAUDE.md 明确写着 PH5 依赖 PH4 真正收口,逐条核对 06-验收与测试锚点.md 每条 REQ 的 happy/boundary/failure 用例(不含 PH5 自己的 REQ-15)后,找到并修复了 5 个真实缺口(其中一个是本 Unit 自己就该覆盖但漏掉的更大缺口):
- REQ-03 failure:
work_node_execution.status一直是直接从running跳到retrying,reflecting这个状态值虽然在 01-M 的枚举里,但从没被任何代码真正写过。work_node_run.py的 schema 校验失败分支现在先写一次reflecting再决定重试/放弃,新增test_schema_failure_passes_through_reflecting_before_retrying(用 sqlitetrace_callback侦测真实执行过的 SQL,不是靠时序猜测)。 - REQ-05 failure / REQ-09 boundary("多个任务同时有一个 blocked_pending_human、其余 queued 时,调度器应继续调度 queued 任务"):
test_org_loop_route.py里对应这条的测试自己写着"no scheduler exists yet to test against directly"——U-032 建好调度器后这句注释已经过期,但一直没人回头补一个真正对着调度器跑的测试。新增test_scheduler.SchedulerContinuesPastBlockedTaskTest:真实跑run_scheduler_once,一个 Goal 的任务预置loop_round=3,其真实触发org_loop_route.LoopLimitReachedError(真实的blocked_pending_human写入,不是 mock 出来的),其余 Goal 正常跑完,验证调度器没有被卡住。 - REQ-06 / REQ-07 / REQ-14 三条 failure 用例共享的同一个真实缺口,是这次审计里最重要的发现:
CorruptPersistedStateError(REQ-06)、ClassifierBlockedError(REQ-07)、UsageLimitWaitTimeoutError(REQ-14)都被正确地抛出,也都正确地把各自的work_node_execution.status标成了blocked(后者是 U-031 自己加的 catch-all),但从来没有任何代码把这个后果真正传导到task.status='blocked_pending_human'——work_node_run.py自己的 docstring 甚至早就写明"callers should route it",但没有一个调用方真的这么做。这正是 U-032 的调度器该管的事:e2e_pipeline.py拆出run_task_lifecycle()(run_end_to_end()变成"建 task + 调它"的两行包装,签名/行为不变),scheduler.py新增_block_task()(照抄budget_guardrail.check_budget()已有的 UPDATE +task_status_changed事件写法,不是发明新模式),run_goal_to_completion()自己建 task 后捕获这三种异常并正确路由;其余异常仍然原样冒泡,不吞掉。三个新测试类(CorruptPersistedStateRoutingTest/ClassifierBlockedRoutingTest/UsageLimitWaitTimeoutRoutingTest)逐一验证。 - REQ-02 boundary(PRD 出现自相矛盾的场景假设应被拦截,触发
reflecting而非直接放行):这条此前完全没有实现——artifact_write.write_artifact()一直无条件把validation_status写成'passed',artifact_write.py自己的 docstring 也如实记录了这个缺口。因为这是全新的 LLM 判断能力而不是接线修复,就此询问用户是否现在补上,用户选择"现在就建"。新增work_graph_orchestrate.run_graph()的可选verify_fn钩子(每个节点跑完、下游节点开始前调用,命中失败时把work_graph_run.status写成reflecting、对同一节点重新生成一次并再次校验,仍失败则写blocked并抛ConsistencyCheckFailedError——work_graph_run.status在此之前从未被任何代码真正写过,一直停在初始的'planning',这次是它第一次被真正用起来,但只做了verifying/reflecting/blocked三个和这条用例直接相关的状态,没有借机把running/merging那一整套图级状态机也补全——04-P 没具体描述过那部分,属于超出这次范围的新设计);e2e_pipeline._check_prd_consistency()是新增的一次真实agent(agentType:'planner', schema:...)调用,只挂在 PRD 节点上(_prd_consistency_verify_fn,其余节点直接放行不调用)。真实验证:test_e2e_pipeline_e2e.RealPrdConsistencyCheckTest用一段刻意自相矛盾的撞墙规则("必须立即结束" vs "可以穿墙不结束")+一段真实一致的 PRD 各跑一次真实调用。第一次真实跑就抓到一个真 bug:模型把consistent字段返回成 JSON 字符串"false"而不是布尔值false(即使 schema 里已经写了"consistent": {"type": "boolean"}——05 §5.4.1.1 早就说过不能盲目信任 schema 真的会被强制),Python 里if "false":是真值,直接把"发现矛盾"误判成"通过",是全场景里最隐蔽的一类 bug(mock 测试永远返回真的 Python bool,只有真实调用能暴露)。新增_coerce_bool()做防御性转换(bool 原样、字符串按大小写不敏感比较 "true"、其余走bool()),修复后重跑真实测试通过;同时补了两个 mock 层回归测试锁死这个具体行为(test_string_false_from_a_real_call_is_not_treated_as_truthy/test_string_true_from_a_real_call_is_treated_as_consistent)。 - REQ-13 failure:一个次要的测试质量问题(不是功能缺口)——
test_ws_nonexistent_node_rejected_before_upgrade只检查了 HTTP 状态行是 404,没检查响应体里的NODE_EXEC_NOT_FOUND(生产代码本身是对的)。顺手加了_read_http_response_with_json_body辅助函数,把这条和同类的PROJECT_NOT_FOUND(REQ-12 failure)测试都补上了 body 断言。 - 全部 5 处修复后,
python3 -m unittest discover -s src380 个测试全绿(含新增的);REQ-01~14 现在逐条都有真实覆盖,PH4 可以视为真正收口,PH5 排期不再依赖对这份回归的信任假设。 - 覆盖:01-M
work_graph_run/work_node_execution(真正被生产代码写入,不再只是测试 fixture);03-Cc_work_graph_plan/c_work_graph_execute/c_work_node_run(首次真正接通);REQ-01~14 全部
U-032 [Risk Gate] 多 Goal 并发(
org_concurrency_config)+ 长时间运行稳定性压测- 依赖:U-031
- Done:并发数调高后多个独立 Goal 可同时稳定运行(原文档缺 Done 行,2026-08-02 补上)
- 实现补充(2026-08-02)——同一类"文档假设了、代码从没建过"的结构性缺口:开始压测前核对"并发跑多个 Goal"具体由谁负责调度时,发现 04-P(
blocked_pending_human分支"调度器跳过该任务、执行队列中其他可并行任务")、01-M(task表索引设计理由直接写"用于调度器扫描")、06 节 REQ-09 boundary 用例("多个任务同时有一个blocked_pending_human、其余queued时,调度器应继续调度queued任务")三处都在预设一个自动轮询/派发多个 Goal 的调度器存在,但翻遍代码库找不到它——org_node_lifecycle._check_concurrency()(U-010)只是给单次trigger_org_node_full调用挡一道并发上限闸门,从未被循环调用过;e2e_pipeline.run_end_to_end()(U-014)是唯一真正跑通过的闭环,但它是同步驱动一个任务、且只被测试调用过,没有任何生产代码会自己发起它。REQ-09 boundary 这条用例实测确实从未被真正验证过(test_backend_server.py里同名测试只测了manual_resume这个 HTTP 端点本身,不测调度器绕过阻塞任务继续跑队列)。跟用户确认后(AskUserQuestion,选择"现在就建这个调度器"),新增src/scheduler.py: - 设计:
run_scheduler_once(db_path, goals, max_workers=None)用concurrent.futures.ThreadPoolExecutor派发多个 Goal,max_workers不传时读org_concurrency_config.max_concurrent_goals(和_check_concurrency同一个配置项);每个 worker 线程各自schema.connect(db_path)开自己独立的 sqlite 连接(不共享一个conn对象跨线程),因为 U-027 已经用一次真实 SIGSEGV 证明了共享连接对象需要手动加锁包住每一次操作(含close()),照搬那套锁只会把所有 DB 操作重新串行化,等于白建了并发;每个连接设busy_timeout让真实写竞争时重试而不是直接抛 "database is locked"。某个 Goal 抛异常(含LoopLimitReachedError触发的blocked_pending_human,这本身是预期终态不是 bug)不影响其余 Goal 继续跑完,呼应 04-P"调度器继续执行队列中其他任务"这条原文。 - 顺带补上
run_end_to_end()自己早就写明的缺口:U-014 的文档一直如实写着"Criteria 4-7(Org-level Loop 等)不在本 Unit 范围"——run_end_to_end()拿到 Validator 的 verdict 后就直接返回,从未调用过org_loop_route.close_task_done()(PASS)或org_level_loop.run_fix_loop()(FAIL,U-017 已经建好且真实验证过的根因判断+路由+修复+复验闭环),任务永远卡在validating。调度器的职责是"把 Goal 真正跑到底",不能留着这个洞,所以在scheduler.finish_task_lifecycle()里补上了这一步,没有改动run_end_to_end()自身的行为和既有测试(test_e2e_pipeline.py断言 PASS 后停在validating依然成立,这仍然如实描述 U-014 自己的范围)。 - 验证方式,未额外花真钱:
test_scheduler.py用 mock 验证finish_task_lifecycle的 PASS/FAIL 分支各自调对了函数;另外三个测试用真实ThreadPoolExecutor(不 mock 线程调度本身)验证并发数上限确实生效(6 个 Goal、max_workers=2时用锁+计数器实测峰值并发正好是 2,不是"表面并发实际串行")、单个 Goal 抛异常不拖垮其余、max_workers未传时正确读到org_concurrency_config。这次没有另外跑一轮真实多 Goal 并发的RUN_REAL_AGENT_E2E——判断依据:这次新增的唯一真正未验证过的风险面是"多线程各自开独立 sqlite 连接、并发写同一个文件",ConcurrentSqliteWritesStressTest(10 个 Goal、max_workers=5,真实文件数据库、真实线程、真实close_task_done()UPDATE,只是把e2e_pipeline.run_end_to_end换成一个不调用真实 API 但真的写 DB 的假函数)已经真实验证过这一风险面,不需要真花 API 调用去验证;而run_end_to_end本身的真实调用链路已经在 U-031 三次真实验证过,run_fix_loop(FAIL 分支)也在 U-017 自己的真实 e2e 测试里验证过,调度器只是把这些已验证过的部分用线程+独立连接组合起来,不是重新发明一条未知路径。 - 覆盖:01-M
org_concurrency_config;架构文档 §7.4"多任务并发"决策;REQ-09(boundary 用例首次被真正实现和验证,而不只是停留在 HTTP 端点测试);新增src/scheduler.py(04-P/01-M 假设存在但此前从未建过的调度器)
U-033 [Risk Gate] A-2 假设收口:Hermes SOUL.md 迁移效果复核,确认
confirmed或记录偏差- 依赖:U-004(此时已有足够真实运行数据判断人设迁移是否真的"不需要大改")
- Done:A-2 状态确认为 confirmed 或记录实际偏差(原文档缺 Done 行,2026-08-02 补上)
- 实现补充(2026-08-02):复核范围是"PH1~PH4 期间积累的真实运行数据,有没有暴露出 U-004(2026-07-31)当时没发现的新偏差",不是重新跑一次 U-004 已经做过的验证。梳理了 U-004 之后每一次真实调用迁移后人设的记录:U-011(
plan_work_graph真实调用,回复引用"SOUL 原则 #10")、U-014("贪吃蛇游戏"真实完整闭环,Coder/Validator 人设都真实跑过)、U-031(agentTypespike 里模型自称"Planning Agent (PL)",以及完整plan_documents()四节点真实调用)——全部指向同一个结论:迁移后的人设内容本身没有再出问题,U-004 当时发现的三处 Coder 偏差(飞书 Base token/lark-cli/--dangerously-skip-permissions)已经是偏差的全部范围。期间另外发现的 05 §5.4.1.2(disallowedTools子命令限定 bug,一度让 Validator 没有 Bash 权限)判断为工具权限层的 Claude Code 平台行为,不算 A-2 问的"人设内容需要大改",不计入偏差范围(A-2 的验证计划字面问的是SOUL.md/AGENTS.md内容能否原样迁移,不是工具层配置语法)。最终结论:维持"部分成立",不改判为 confirmed——U-004 时已发生的大改是既成事实,不会因为后续没再出新问题就被追溯为"没有大改";U-033 的收口价值在于确认这个偏差范围到此为止、没有持续扩大,PH5 排期不需要为 A-2 预留额外风险缓冲。详见00-SDD-生成记录.mdA-2 行的最终状态记录。 - 覆盖:00 节 A-2 假设(最终状态:部分成立,已收口)
PH5:自举能力(Risk Gate,Deploy Supervisor + 蓝绿部署自动回滚)
命名说明:本 Phase 的"Deploy Supervisor"和架构文档里"Supervisor(调度子 Agent,Coder Work Graph 内部监控节点)"是两个不同概念,详见 supervisor-auto-rollback-design.md 开头的命名说明,不要混用。
Goal:graphlemon 具备安全地自主修改自己代码(含 Org Agent 身份文件、调度器/执行器核心逻辑)的能力——出问题时自动回滚,不需要人工评审代码内容把关。
Phase 验收:docs/supervisor-auto-rollback-design.md 状态机的四个终态(health_check_failed、rolled_back、rollback_failed → blocked_pending_human、finalized)都能真实触发一次;self-development-feasibility.md 第 3 节的 9 步走查场景改成真实执行(不是模拟),从"人类发现 bug"到"Deploy Supervisor 自动 finalize"全程无需人工介入代码评审。
设计依据:docs/supervisor-design-comparison.md(方案选型)+ docs/supervisor-auto-rollback-design.md(状态机/数据模型/健康检查/回滚流程/MVP 参数)。
PH5-S1 数据模型与治理规则
U-034 [Enabler] 01-M 扩展:新增
deployment、deploy_health_signal两个实体建表;bug_record.root_cause_category新增core_engine枚举值- 依赖:U-001
- Done:两张新表建表成功并通过约束校验;
bug_record现有记录/查询逻辑对新枚举值向后兼容(不破坏 U-015/U-018 已有测试) - 实现补充(2026-08-02):按 01-M §1.15/1.16(比
supervisor-auto-rollback-design.md§3 的概要表更完整,以 01-M 为准)在src/schema.py追加两张表;deployment.status用 01-M 原文的完整 15 值状态机枚举(不是 §3 表格里省略过的简化版)。ux_deployment_active用表达式索引CREATE UNIQUE INDEX ... ON deployment((1)) WHERE status NOT IN (...)实现"同一时刻只允许一条非终态 deployment"的全局单例约束——和work_graph_run现有的按(task_id, org_node)分组的 partial unique index 是同一个思路,只是这里没有天然的分组键(graphlemon 自己就一份,不是"每个 X 一条"),用常量表达式当分组键达到"全局只有一条"的效果,本地 sqlite3 验证过真实生效。bug_record.root_cause_categoryCHECK 约束加了core_engine,但bug_record.py自己的应用层_VALID_ROOT_CAUSE_CATEGORIES白名单刻意没有跟着放开——"什么时候一个 bug 该归为 core_engine"是 05 §5.8 定义的按文件路径判断的规则(.claude/agents/*.md/Graph Engine 核心逻辑/01-M 自身 schema 变更),不是 Validator 现有三视角judge_root_cause能判断的东西(那三个视角硬编码只产出 planning/coder/skill_defect),这条分类逻辑连同c_org_loop_route的路由扩展一起留给 U-035,不是这个 Unit 该做的。previous_commit_hash/previous_instance_dir在 01-M 里标了 not null,但第一次真正跑 Deploy Supervisor 时没有"上一条 finalized deployment"可取——这个引导期边界情况留给 U-037 的c_deploy_supervisor_stage处理(第一次调用时把当前正在跑的 commit/checkout 目录当成"previous"传进去),不是 schema 层该解决的,已在schema.pydocstring 里记录。 - 覆盖:
supervisor-auto-rollback-design.md§3;self-development-feasibility.md缺口 3
U-035 [Risk Gate] self-target 场景 worktree 无条件强制规则(缺口 1)+
root_cause_category=core_engine命中时路由到蓝绿部署流程而非fast_lane(c_org_loop_route扩展)- 依赖:U-013(worktree 机制)、U-016(
c_org_loop_route)、U-034(core_engine枚举) - 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 刚建立起来的信任前提 - 实现补充(2026-08-02)——发现并解决一处真实 SDD 内部冲突(已跟用户确认):动手前核对"core_engine 命中时路由到蓝绿部署流程"具体怎么触发时,发现
supervisor-auto-rollback-design.md§1("触发时点:Validator PASS → merge → Deploy Supervisor 接管",完全不提 bug_record)和 03-C §3.11/04-P §4.6(字面上把触发门槛定义为"已存在一条root_cause_category=core_engine的 bug_record")互相矛盾。核实task_create.py后确认:c_task_create只会创建task_type='goal'的任务,bug_record只在 Validator FAIL 归因或人工报告两条路径上产生——也就是说,self-development-feasibility.md9 步走查场景本身(一个普通的自我改进 goal 任务,Planner→Coder→Validator 一次性 PASS,从未进入过 FAIL 循环)根本不会产生任何 bug_record,按 03-C/04-P 字面文本,Deploy Supervisor 永远不会为这整类任务触发——这会悄悄废掉 PH5 对"goal 型自我改进任务"这整条路径的意义。用 AskUserQuestion 向用户说明冲突后,用户选择"按 §1 字面设计:仅凭 self-target+PASS 触发,bug_record 是触发后的旁路产物而非前提条件"。 - 设计:新增
src/paths.repo_root()(graphlemon 自身 checkout 根目录,和已有的platform_home()——~/.graphlemon运行时目录——是两个不同概念,别混)+paths.is_self_target(target_folder_path)(真实路径比较,支持测试注入repo_root_path覆盖,避免测试真的对着当前这份 checkout 跑 worktree 副作用)。work_graph_orchestrate.run_graph()内部计算effective_needs_worktree = needs_worktree or paths.is_self_target(target_folder_path),不管调用方传了什么都会在 self-target 时强制为真——两个真实调用点(e2e_pipeline.plan_documents/implement_code)都不用改,改动集中在唯一的真正入口。新增src/core_engine_classify.py:classify_core_engine_change(changed_files)是纯路径规则(.claude/agents/*.md/src/*/supervisor/*命中即 core_engine,frontend//docs/不命中——05 §5.8 原文明确点名前两类+schema,schema 本身就在src/下天然覆盖,没有单独点名前端console,判断为有意排除);parse_changed_files_from_diff()直接从已有的code_diffartifact 内容里解析diff --git a/X b/Y头,不需要额外一次git diff --name-only子进程调用。bug_record.py新增record_core_engine_change()——和已有的create_bug_record()(FAIL 路径的根因归因,suggested_route必填)是两个不同场景:这是 PASS 路径的审计留痕,没有"路由决策"可言,suggested_route留空、status='resolved'(因为验证已经通过,不是待处理的开放问题);_VALID_ROOT_CAUSE_CATEGORIES顺带放开core_engine(U-034 当时故意没放开,因为分类机制还不存在,现在存在了)。scheduler.finish_task_lifecycle()的 PASS 分支里,close_task_done()之后附加调用_record_core_engine_audit_if_self_target()——是附加副作用,不是替代:p_deploy_supervisor_blue_green(04-P §4.6)整套状态机只写deployment.status,从头到尾没有一条转移碰task.status,这与 §2"Deploy Supervisor 和 Live/Candidate 之间没有共享失败域"的原则完全一致——Coder 的工作本身确实已经完成且已合并,task.status='done'不应该被 Deploy Supervisor 是否还在跑而卡住。 - 范围边界(刻意不做的部分):这个 Unit 只到"检测 self-target + 记一条 core_engine 审计 bug_record"为止,不创建
deployment行——deployment记录的生成明确是 U-037c_deploy_supervisor_stage自己的 Done 标准("给定一个已合并到主分支的 commit,能生成 deployment 记录"),Deploy Supervisor 的守护进程(U-036 建骨架)今后靠轮询"self-target 项目里状态为 done、且没有对应 deployment 行的任务"来发现待部署的改动,这个发现机制本身也是 U-036/037 的范围,这里不提前实现。org_loop_route.route_org_loop()的fast_lane/full_replan分支本身完全没有改动——那是 Validator FAIL 后的重试路由决策,和这里"PASS 后是否需要蓝绿部署"是两个正交的问题,development-plan.md 原文里"c_org_loop_route扩展"这行描述在这次实现后证明是不准确的说法,实际改动点在scheduler.py的任务收尾逻辑,不在org_loop_route.py。 - 验证:全量 417 个测试通过;
test_work_node_run.RealGitWorktreeIsolationTest(真实 git worktree 隔离+merge,U-013 的 REQ-03 boundary 用例)单独重跑确认无回归,如 Done 标准要求。 - 覆盖:REQ-03(回归);05 §5.8(self-target worktree 强制 + core_engine 分类,触发条件已按用户决策订正)
- 依赖:U-013(worktree 机制)、U-016(
PH5-S2 Deploy Supervisor 核心机制
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/状态文件约定参考 graphloophermes_watchdog.py的原子写模式)- 依赖:无(代码已存在,是补测试和补骨架,不是从零开发)
- Done:四模块各自有对应
test_*.py(可直接照搬/改造 graphloop 的test_change_manifest.py/test_selective_apply.py/test_workspace_security.py等,逻辑未改动,测试也基本不用改);Deploy Supervisor 骨架能独立启停,不受 Live/Candidate 进程崩溃影响 - 待定(2026-07-31 订正):此前这里写的是"
supervisor/目录该归入 U-002 的目标项目树(.claude/、skills/、src/那套)还是 U-003 的平台目录"——但 U-002 的范围已经订正为只创建.graphlemon/(见该 Unit 2026-07-31 订正说明),.claude/、skills/从来就不是目标项目的东西,是 graphlemon 平台自己仓库的资产。所以这个选择题已经不成立,supervisor/只可能归属"platform 自身工具"(当前的实际存放位置,supervisor/isolation/),不存在真正的"归入目标项目"选项;保留这条记录是为了让后来者理解为什么这里曾经有过一次误判 - 实现补充(2026-08-02):四模块的单测直接照搬 graphloop 对应文件(
test_isolated_coding_workspace.py/test_workspace_security.py/test_change_manifest.py/test_selective_apply.py),逐条核对后确认 API 与行为完全匹配(graphlemon 这四个模块本来就是"adapted verbatim"),只改了 import 路径(from supervisor.isolation import X)+ 补了几条 graphlemon 自己模块文档里提到但原测试没覆盖的小 case(isolated_workspace的空/不存在 source_dir 拒绝、workspace_security的换行符注入拒绝);新增supervisor/__init__.py(原本靠 Python 3 namespace package 也能跑,但补一个空的显式__init__.py和src/__init__.py保持同一约定,不依赖隐式行为)。52 个测试全绿,放在supervisor/isolation/test_*.py(和被测模块同目录,比照src/test_*.py的既有约定),运行方式python3 -m unittest discover -s supervisor/isolation -p "test_*.py"(不在python3 -m unittest discover -s src覆盖范围内,是新增的第二条独立测试入口,README/CLAUDE.md 后续需要记一笔)。 Deploy Supervisor 骨架落在新文件supervisor/deploy_supervisor.py:PID 文件+状态文件走paths.platform_home()/deploy_supervisor/下(复用 U-003 已有的运行时目录约定,不新建一套),原子写沿用tempfile+os.replace(参照 graphloophermes_watchdog.py的_atomic_save,未直接 import,逻辑重写了一遍)。run_foreground(poll_fn, poll_interval, iterations)是骨架的核心循环——poll_fn是 U-037+ 才会真正实现的"检查是否有待部署的 commit 并行动"逻辑,本 Unit 只负责按周期调用它、写 PID/状态文件、处理SIGTERM(收到后跳出循环,finally块无条件写status='stopped'并删 PID 文件,包括poll_fn自己抛异常的情况——fail-closed,崩溃了就该看起来像崩溃了,不能让 PID 文件继续指向一个已经不存在的循环)。start_detached()/stop()是给上层(未来某个"启动 graphlemon 平台"脚本,或人工)用的真实进程管理入口,start_detached用subprocess.Popen拉起真实独立进程(不是线程,故意如此——两个进程之间不共享失败域是这整个设计的前提),stop()发SIGTERM并轮询等它自己清理干净,超时不 SIGKILL(留给调用方判断,不擅自做主)。验证方式:RunForegroundTest系列在同一个测试进程内直接验证循环/状态写入/SIGTERM处理(SIGTERM测试用poll_fn内部对自己进程os.kill(os.getpid(), SIGTERM)模拟外部信号,不需要真的起第二个进程);RealSubprocessLifecycleTest是真实的(不 mock)python3 -m supervisor.deploy_supervisor子进程拉起+stop()往返,确认"能独立启停"这条 Done 标准不是靠单元测试逻辑自证的,是真的对着一个真实操作系统进程验证过的。全程用GRAPHLEMON_HOME环境变量覆盖到临时目录,不碰真实用户的~/.graphlemon。73 个测试(含四个 isolation 模块)全绿。 - 覆盖:
supervisor/isolation/四模块(真实单测覆盖,不再只是"一次性手工冒烟");Deploy Supervisor 进程骨架(supervisor/deploy_supervisor.py,尚不含任何真实部署决策逻辑)
U-037 [Risk Gate]
c_deploy_supervisor_stage:候选实例拉起(全新目录 checkout,不git pull原地改)+ 一次性 DB 副本机制(候选实例验证期间绝不接触生产库)- 依赖:U-034, U-036
- Done:给定一个已合并到主分支的 commit,能生成
deployment记录(status=staged)并拉起一个监听临时端口、连接一次性 DB 副本的候选进程 - 实现补充(2026-08-02):新增
supervisor/deploy_stage.py:stage_deployment()建deployment行、git worktree add --detach(不是-b <branch>,候选检出是只读验证用途,不打算再接受提交,和work_node_run.create_worktree()的编码用 worktree 是两回事)到全新目录、复制生产 DB 到一次性副本、subprocess.Popen拉起候选进程。返回值订正(低风险,按判断处理未升级询问):03-Coutput_contract字面写返回{deployment_id, status:'staged'},但 04-P §4.6 状态转移表明确写"c_deploy_supervisor_stage完成"这个触发条件本身就是把deployment.status从staged推进到candidate_starting——两处矛盾,按"更详细的 04-P 状态机表为准"处理,函数完成时返回并持久化的都是candidate_starting,不是字面上的staged。发现并补上两个真实缺口:①deployment表 01-M 字段清单里完全没有"候选检出目录存在哪"这一列——candidate_port/candidate_db_path都有,唯独检出目录没地方存,Deploy Supervisor 若在 staging 和 cutover 之间重启就会彻底丢失候选目录位置;schema.py补了candidate_instance_dir列(同一次 finalized 部署的这个字段,就是下一次 staging 时previous_instance_dir的来源——candidate 一旦顺利转正就是新 Live,回滚要回退到的正是它)。②backend_server.py从 U-007 建骨架以来就没有任何 CLI 入口——create_server()只在测试里以进程内方式实例化过,从没人真正把它作为独立 OS 进程启动过;候选进程恰好需要这个能力,顺手补了main()(python3 -m src.backend_server --host --port --db-path --skill-repo-path,SIGTERM走server.shutdown()——必须在另一个线程调用,同线程调用serve_forever()期间调shutdown()会按http.server自己的文档死锁)。/api/health端点本身留给 U-038 自己去加,不在这里超范围实现。 - 验证方式:
BootstrapPreviousTest/StageDeploymentHappyPathTest/CheckoutFailedTest用真实 sqlite + 真实 git worktree 操作(只 mock 掉真正拉候选进程这一步,验证stage_deployment()自己的逻辑);RealCandidateLaunchTest完全不 mock——对着 graphlemon 自己仓库真实git worktree add --detach检出 + 真实python3 -m src.backend_server子进程拉起 + 真实 HTTP 请求验证候选进程确实在真实监听、真实响应,验证完SIGTERM(超时兜底SIGKILL)+git worktree remove清理干净。这个真实测试意外命中一个开发时序问题:git worktree add只看已提交内容,而这次实现是先写代码再测试再提交(标准流程),测试跑的时候main()还没提交,导致 checkout 出来的backend_server.py是没有main()的旧版本,候选进程"启动"后什么也不做直接退出——用git stash create(不碰工作区/HEAD,只生成一个代表当前工作区状态的 commit 对象)替代直接HEAD,让测试始终对着"当前工作区实际内容"验证,而不是"上一次提交的内容";提交后本地工作区变干净时git stash create打印空字符串,自动回退到普通HEAD,两种时序都覆盖。 - 覆盖:03-C
c_deploy_supervisor_stage;01-Mdeployment.candidate_instance_dir(补字段);backend_server.py新增可独立运行的 CLI 入口
U-038 [Risk Gate]
c_deploy_supervisor_health_check:process_alive/http_health/canary_task三项 pre-cutover 检查- 依赖:U-037
- Done:三项检查全部通过 →
status=health_check_passed;任一失败/超时 →status=health_check_failed,Live 全程未受影响(对应回滚路径 A) - 实现补充(2026-08-02):新增
supervisor/deploy_health_check.py。process_alive复用 U-036deploy_supervisor.is_alive()(os.kill(pid,0)),按HEALTH_CHECK_STARTUP_TIMEOUT轮询;http_health请求候选的/api/health——这个端点 U-037 当时特意留空,这里补上:backend_server.py新增GET /api/health,不只是"进程活着就 200",还真的执行一次SELECT 1确认 sqlite 连接本身没卡死(连接卡死但进程还活着的场景,只测process_alive会漏检,这正是"health check"要抓的那类问题);canary_task是design doc §6"结果可预期的最小任务"字面实现——对候选发起一次真实的"建临时 project(临时目录git init+ 一个空提交)+ 建一个 goal 任务 + 读回确认status=queued/goal_text匹配",不跑真实 Org Graph(不产生真实claude调用/花费),因为c_task_create本身落queued就是同步、确定性、"已知会通过"的最小验证,够用。三项检查按 03-C 原文"任一 fail 立即整体判定,不等其余项跑完"实现,run_health_check()用 tuple-of-lambda 顺序短路,每项检查结果都真实写一条deploy_health_signal(哪怕后续项因为短路没跑,已跑的那几条也留痕)。 - 覆盖:03-C
c_deploy_supervisor_health_check;backend_server.py新增GET /api/health(U-037 留空的口子,这里补上);01-Mdeploy_health_signal(首次被真正写入)
U-039 [Risk Gate]
c_deploy_supervisor_cutover:蓝绿切换本体(Live 停止接受新触发 → 候选切换到生产端口/生产库 → 旧 Live checkout 保留为previous_instance_dir)- 依赖:U-038
- Done:健康检查通过后能完成真实切换,切换过程中在途
work_node_execution能在重启后从持久化状态恢复(复用 U-021 断点恢复机制),previous_instance_dir在监控窗口结束前不被清理 - 实现补充(2026-08-02):新增
supervisor/deploy_cutover.py。发现并补上一个真实缺口:05 §5.9 的 MVP 固定参数表把健康检查/监控相关的时间阈值都列全了,唯独没有一个"生产端口"常量——这是合理的(控制台前端app.js全是同源相对路径fetch('/api/...'),运行时从不需要知道自己监听在哪个端口),但 cutover 场景确实需要:连续几次自我修改必须落在同一个端口上,人工书签/固定入口才有意义。新增LIVE_PORT = 8420常量,和 §5.9 表里其余参数同一性质(工程草案值,非用户可配)。Rebind 策略是标准库约束下的判断处理,不是 SDD 里有歧义要停下来问的地方:http.server没有"给已监听的 socket 换端口/换 DB"这种原地操作,"候选切换到生产端口/生产库"落地成"停掉健康检查用的候选进程,从同一个(已经过验证的)候选检出目录,用生产端口+生产 DB 重新拉起一个新进程"——对外效果等价(同一份已验证代码现在服务生产端口),是 stdlib 唯一提供的机制,不是设计妥协。"在途work_node_execution能在重启后恢复"这条 Done 标准完全靠复用已有的 REQ-06 机制(resume_work_graph_run/run_work_node),本 Unit 不需要为此新写任何逻辑——cutover 造成的中断和其他任何中断(进程崩溃、机器重启)对 REQ-06 机制而言没有区别。顺带记录一个相邻但不在本 Unit 范围内的缺口:backend_server.py目前完全没有调用scheduler.run_scheduler_once()的地方,"Live 进程里有在途工作"目前更多是理论场景而非真实运行时状态——这是调度器接线的缺口,不是 cutover 该修的,留给后续发现。 - 真实测试意外抓到一个真 bug,两处都修了:
deploy_supervisor.is_alive()(U-036)用os.kill(pid,0)判活,这个调用无法区分"真的还在跑"和"已经退出但父进程从没wait()过、还是个僵尸"——僵尸进程的 PID 槽位在被回收之前kill(pid,0)一直成功。U-036 自己的真实子进程测试之所以没暴露这个问题,是因为 Deploy Supervisor 骨架自己在退出前会删掉 PID 文件,is_running()靠文件消失就判定"没在跑",从来没真的走到is_alive()这条判活分支上验证过。这次deploy_cutover.py的真实测试第一次用"发SIGTERM后一直轮询直到进程真的消失"这种更严格的验证方式,一测就卡住——旧 Live 进程收到SIGTERM后真的退出了,但因为父进程(Deploy Supervisor 自己)从没调用过os.waitpid,它变成僵尸,is_alive()永远报告"还活着"。修法:deploy_supervisor.py新增reap_if_exited(pid)——是自己subprocess.Popen出来的子进程就用os.waitpid(pid, WNOHANG)真的回收(顺带拿到准确的"已退出"判定),不是自己子进程(比如上一次 Deploy Supervisor 会话遗留下来的live_pid)就os.waitpid会抛ChildProcessError,退化回is_alive()的尽力而为判断——两种场景都要处理,因为 cutover 完全可能在 Deploy Supervisor 自己重启之后被调用,此时手上的live_pid不是这次进程真正 fork 出来的。deploy_cutover._stop_process()和deploy_health_check.check_process_alive()都改用reap_if_exited,deploy_supervisor.stop()顺手也补了一次回收(不影响它自己已有的返回值语义,纯粹清理僵尸)。这是这次会话里又一次"mock 测试全绿、真实测试才炸出真 bug"的例子,和 PH4/U-031 那几次是同一个模式。 - 覆盖:03-C
c_deploy_supervisor_cutover;05 §5.9 新增LIVE_PORT参数;deploy_supervisor.is_alive()的僵尸进程判活缺陷(真实测试发现,非 mock)
PH5-S3 监控与回滚
U-040 [Risk Gate]
c_deploy_supervisor_monitor:post-cutover 监控窗口内四类信号采样(process_alive/http_health/canary_task/validator_approval_rate)- 依赖:U-039
- Done:
validator_approval_rate信号能从execution_event/work_node_execution正确计算滚动窗口内 Validator 的 approve 占比,人为把该占比推到偏离基线阈值之外时能触发anomaly_detected - 实现补充(2026-08-02):新增
supervisor/deploy_monitor.py,run_monitor_cycle()是"一次采样"的最小单元,由 Deploy Supervisor 自己的调度循环反复调用(03-C 原文就是这么写的,不是一次性调用)。validator_approval_rate的"approve 占比"具体怎么算,01-M/03-C/04-P 都没有字面定义(execution_event.event_type枚举里根本没有"validator_approved"这类事件)——核对了scheduler.finish_task_lifecycle()的真实代码后确认:Validator 判 PASS 时直接close_task_done(),从不产生bug_record;判 FAIL/INCONCLUSIVE 时org_level_loop.run_fix_loop()必然会通过root_cause_trace.judge_root_cause()产生一条bug_record(source='validator')。所以"这个任务 Validator 是否一次通过"可以从"这个任务有没有对应的source='validator'bug_record"精确推导,不是猜的,是核对真实代码逻辑后得出的结构性事实。基线("与该 deployment 之前的历史基线比较")不额外建表存,直接用同一个函数在 cutover 时间点往前回溯MONITORING_WINDOW_HOURS小时现算,baseline_rate=None(没有足够历史数据)时约定为"不判定异常"而不是当成 0,避免冷启动阶段被误判。崩溃计数/连续失败窗口按 05 §5.9 字面是"时间窗口内计数",这里简化成"按最近 N 次采样计数"(N = CRASH_WINDOW / MONITOR_SAMPLE_INTERVAL),前提是采样节奏基本均匀——这是有意简化,注释里写清楚了,不是漏做。process_alive 信号在监控阶段的语义和 U-038 健康检查阶段不同:健康检查阶段任何一次不活着就直接 fail;监控阶段按设计文档字面"连续崩溃 ≥ MAX_CRASH_COUNT 次才算 anomaly",单次挂掉只记录不触发——第一版测试写反了这条语义(以为单次挂掉就该立刻 anomaly),跑起来才意识到这是"崩溃循环检测"不是"瞬时存活检测",改正后测试反而更准确地锁定了这条容易读错的规则。 - 覆盖:03-C
c_deploy_supervisor_monitor;01-Mdeploy_health_signal(monitoring 阶段持续写入)
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- 依赖:U-038, U-040
- Done:两条回滚路径各自跑通一次;人为制造"旧版本也起不来"的场景能正确终态到
blocked_pending_human而不是无限重试或静默失败;git revert冲突场景(supervisor-auto-rollback-design.md§10 开放问题 3)有明确处理,不能是本 Unit 唯一遗留的未定义行为
U-042 [Risk Gate] 自举闭环端到端验收:真实复现
self-development-feasibility.md第 3 节的 9 步场景(修复 graphlemon 自己p_org_task_lifecycle状态机里的一个真实植入 bug),走完整"部署→健康检查→cutover→监控→finalize"或"检测异常→自动回滚"两条路径中至少一条- 依赖:U-035, U-041
- Done:全程无需人工评审代码内容;参照 graphloop
RUN_REAL_AGENT_E2E=1的先例,真实拉起两个 graphlemon 实例的端到端用例默认跳过,需要显式环境变量开启,日常回归只跑 mock 版本
PH5-S4 可观测性(非阻塞,可延后)
- U-043 [Value Path] 控制台展示
deployment状态与健康信号(02-V 新增视图)- 依赖:U-024, U-040
- Done:能在控制台看到当前是否有 deployment 在进行中、处于状态机的哪个状态、最近一次健康信号采样结果
PH5 已知交叉缺口(不阻塞设计定稿,实现前需要留意)
supervisor/isolation/现有代码目前只做过一次手工冒烟测试(本次会话里跑的 isolate → diff → apply 往返验证),不是正式unittest套件——U-036 里已列为待办,暂不算"已完成"。supervisor/目录归属 U-002 还是 U-003 的问题目前只在 U-036 里留了一个临时决定,还没有真正的架构决策支撑,PH0 真正实现 U-002/U-003 时应该回头确认这个临时决定是否仍然成立。- U-001 的"15 张表"这个数字会在 PH5 落地后变成 17 张(
deployment/deploy_health_signal),后续如果有人只看 PH0 的验收标准会觉得和实际情况对不上,属于文档层面需要注意的地方,不是设计缺陷。 supervisor-auto-rollback-design.md里的参数草案值(健康检查超时、监控窗口时长等)明确标注"工程经验值,需要实测校准",U-036~U-042 实现时不要把这些数字当成已经验证过的常量直接硬编码进测试断言里,要留出可调整的空间。
2. Unit 分类汇总
| 分类 | Unit 数量 | Unit 列表 |
|---|---|---|
| Value Path(主线) | 17 | U-009~U-014, U-022~U-030, U-024.5, U-043 |
| Risk Gate(高风险校验/护栏/收口) | 18 | U-004, U-015~U-021, U-031~U-033, U-035, U-037~U-042 |
| Enabler(使能基础设施) | 10 | U-001~U-003, U-004.5, U-005~U-008, U-034, U-036 |
(PH0-PH4 原有 33 个 Unit 分类不变,PH5 新增 10 个:U-034~U-043,其中 Value Path +1、Risk Gate +7、Enabler +2。
2026-07-30 计划评审追加 2 个 Unit:U-004.5[Enabler](RUN_REAL_AGENT_E2E harness 归属)、U-024.5[Value Path](U-024 依赖分级后的 REQ-12 完整验收),不改变原有编号顺序,只插入在对应 Unit 之后)
3. 关键路径(Critical Path)
风险最高、返工代价最大的一条依赖链(PH0-PH4 主线):
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 收口)
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 各自解决的隐性工作,显式化成一个前置节点,避免重复发明。
PH5 关键路径(与上面这条主线不合并——PH5 依赖 PH4 全部收口,是完全独立的第二条链):
U-034 → U-035 → U-036 → U-037 → U-038 → U-039 → U-040 → U-041 → U-042(PH5 收口)
U-041(回滚路径实现,含 §10 提到的 git revert 冲突处理)是 PH5 里返工风险最集中的一环,因为"回滚"本身失败没有再下一层的自动兜底,只能转人工——建议 U-041 完成后立即用人为制造的"旧版本也起不来"场景验证兜底路径,而不是假设它"大概率不会发生"就跳过测试。
4. 与 06-验收与测试锚点.md 的对应关系
本文档不重新定义验收标准,Unit 的 Done 标准直接引用 06 节已有的 REQ-01~14 用例(happy/boundary/failure)。如果后续 06 节的验收用例发生变更,本文档对应 Unit 的 Done 标准需要同步更新,避免两份文档各说各话。