development-plan.md 134 KB

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-M project.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-C c_project_create
  • 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.md Testing 约束里点名的 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.md Testing 约束
  • 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-C c_task_create;04-P p_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 围栏代码块
    • 真实 bug(2026-08-03,U-042 第六次真实自举压测发现):前五次真实压测各自卡在别的更早环节,这是第一次真正走到"用真实 graph_manifest 跑完整个 4 节点文档链"这一步就出问题。这次真实调用返回的 graph_manifest 里,四个节点连 edges 都没给(比 2026-08-01 那次"字段名不对但至少有 edges"更彻底),_normalize_graph_manifest 原来的"从 edges 反推 depends_on"逻辑无从反推——topological_order() 在完全没有依赖信息时按 node_id 字母序处理零入度节点,"benchmark"排在"sdd"前面,于是 e2e_pipeline.build_prompt() 试图给 benchmark 的 prompt 模板填 {sdd_content} 时,sdd 节点根本还没跑,直接 KeyError。这次没有再指望 LLM 每次都能把这个结构给对:_PROMPT_TEMPLATE 本来就是让 Planner 每次都产出完全固定的 brd→prd→sdd→benchmark 线性四节点,这是个不会变的架构事实,不是每次都要重新推断的东西——_normalize_graph_manifest 现在多一步:只要四个规范 artifact_type(brd/prd/sdd/benchmark)齐全,不管 LLM 给没给 depends_on/edges,直接按已知的固定线性顺序强制补上依赖链,和 ensure_worktree_committed 一样是"机制用代码"而不是祈祷 Agent 每次都按契约给结构。补了三个回归测试:完全没有依赖信息时依然能正确补链、node_id 和 artifact_type 不同名时按真实 node_id 补(不是瞎猜字符串)、四个规范类型不全时不强行拼凑。
    • 覆盖:01-M artifact/work_graph_run;03-C c_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-P p_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-C c_work_node_run;REQ-03(boundary)
    • 真实 bug(2026-08-02/03,U-042 第一次真实自举压测时发现):RealGitWorktreeIsolationTest 从 U-013 建成那天起,run_fn 一直是测试自己写的、写完文件后主动 git add + git commit 的假实现——这个盲区从没被真实 Coder 会话踩过,因为 PH1~PH4 所有真实调用都没启用 needs_worktree=True(development-plan.md U-031 的实现补充里已经记过这条),直到 U-035 让 self-target 场景无条件强制 worktree,U-042 第一次真实自举跑起来后才第一次真正踩中。真实现象:Coder 在 worktree 里确实编辑了文件,自己也验证过(git status/git diff --stat 都在它自己的 test_report 里回显了),但从未 git commit——Coder 人设里没有任何一处要求它提交自己的改动。merge_worktree() 只会 git merge 已提交的分支,worktree 分支上如果压根没有新提交,merge 就是个真正的空操作("Already up to date",不产生新 commit),紧接着 remove_worktree() 把这个还带着未提交改动的目录直接删掉——一次真实、已验证过的 Coder 改动就这样被无声吞掉,implement_code() 算出来的 code_diff 自然是空的。修复:新增 work_node_run.ensure_worktree_committed(),在 merge_worktree() 之前检查 worktree 是否有未提交改动(git status --porcelain),有则自动 git add -A + 提交一条 graphlemon: auto-commit uncommitted worktree changes for <node_exec_id>,worktree 本来就干净(比如纯排查、没有真的改代码)时什么也不做。新增两个回归测试:test_uncommitted_worktree_changes_are_auto_committed_before_merge(复现"改了但没提交"场景,断言改动最终真的落到主分支)、test_clean_worktree_with_no_changes_is_still_a_valid_merge(确认"确实没什么好改"这种合法结果不会被误伤)。这是"判断用 Agent,机制用代码"原则的又一次体现——不指望靠给人设加一条"记得 commit"的提示词来保证正确性,而是在系统层面兜底,人设漂移/遗忘不应该导致真实工作成果被无声丢弃。
    • 真实 bug 的第二层(2026-08-03,U-042 第八/九次真实压测发现,比上面这条更深一层):ensure_worktree_committed() 只兜住了 worktree 自己的未提交改动,但 git worktree 的设计本身是和主仓库共享同一个项目身份(同一个 .git,只是另一份检出)——一次真实压测发现,claude -p 的 memory: project 写入并不总是遵守调用时传入的 cwd,而是可能直接落在主仓库自己的检出目录上,和当时具体是哪个节点、哪次调用完全无关(不是 _check_prd_consistency 一个函数的问题,把它单独包一层 worktree 只是挪动了"这次踩中的是哪次调用",没有从根上解决)。后果:主仓库工作区多出一处未提交的 memory 文件改动,下一个节点自己的、原本完全正常的 worktree merge 会因为"主分支有本地改动会被覆盖"直接报错退出(git merge exit 2)。修复:ensure_worktree_committed() 从只处理 worktree 自身,扩展成同时也对 repo_path(主仓库检出目录)本身跑一遍同样的"有脏改动就自动提交"逻辑,在每次 merge_worktree() 之前调用两次——不管是哪次真实调用造成的漂移、原因是什么,统一兜底,而不是针对某一个特定调用点单独打补丁。修复过程中顺带发现并解决了这条兜底自己引入的一个新问题:对主仓库跑 git add -A 时,如果某个节点的 worktree 检出目录(.graphlemon-worktrees/<node_exec_id>/)当时还没被 remove_worktree() 清理,会被当成一个类似 git link 的条目一并加进去,紧接着目录被删掉后 git status 就会显示成一条诡异的"D"(删除)——两处修复:.graphlemon-worktrees/ 加进 .gitignore(这是长期正确性),ensure_worktree_committed() 自己的 git add -A/git status --porcelain 也显式排除这个路径(-- . ':!.graphlemon-worktrees'),不依赖调用方仓库一定配置了对的 .gitignore。新增回归测试 test_drift_on_main_checkout_is_auto_committed_before_merge,模拟"改动落在主仓库而不是 worktree"这个具体场景,并验证接下来一个节点的合并不受影响。
    • 上面这条修复自己的一个真 bug(2026-08-03,U-042 第十次真实压测立刻发现):-- . ':!.graphlemon-worktrees' 这个显式排除 pathspec,一旦该路径同时已经在 .gitignore 里(这次修复自己刚加的),git add -A 会直接报错退出("paths are ignored by one of your .gitignore files",即使配置 advice.addIgnoredFile=false 抑制提示文案,退出码依然是 1,不是单纯的提示)——本地复现过,是真的会失败,不是巧合。既然 .gitignore 已经能让普通的 git add -A(不带任何显式 pathspec)正确跳过这个目录,显式排除反而是多余且互相冲突的,直接删掉,ensure_worktree_committed() 恢复成最简单的 git add -A。同时把 test_work_node_run.py 的 _init_git_repo() 测试夹具也补上同一条 .gitignore 规则,让所有真实用到 git worktree add 的测试仓库和生产环境配置保持一致,而不是靠巧合"测试仓库正好没触发这条冲突"。

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-validation skill + 迁移后的 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-C c_bug_record_create;04-P p_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-C c_skill_update/c_skill_rollback;REQ-08

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-C c_budget_check;04-P p_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() 把 SQLite CURRENT_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-P p_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 全平台补齐(Windows PowerShell/Linux zenity/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(PowerShell FolderBrowserDialog 脚本)/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-C c_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_create HTTP 接线、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-C c_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-C c_ws_subscribe;REQ-12(部分)
  • 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-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-C c_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-V v_log_viewer;03-C c_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-C c_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-C c_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-C c_concurrency_config_update/c_budget_config_update/c_log_retention_config_update

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(用 sqlite trace_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 断言。
    • 真实 bug(2026-08-03,U-042 第四次真实自举压测发现):_check_prd_consistency() 直接对 target_folder_path 调用 execute_work_graph(),完全绕开了 work_node_run.run_work_node() 的 worktree 隔离机制——之前每一次真实调用这个函数都是对着一次性 target project 克隆做的,从没自我 targeting 过,所以从没暴露过问题。这次真实自举压测里,这个调用用 agentType:'planner' + memory:project,会真的往调用时的 cwd 写 .claude/agent-memory/planner/MEMORY.md——self-target 场景下 cwd 就是 target_folder_path 本身(主仓库),于是这次真实写入变成了主分支工作区里一处未提交的本地改动;紧接着 SDD 节点自己的 worktree 合并回主分支时,git merge 因为主分支工作区有未提交改动会覆盖同一个文件而直接报错退出(exit 2),SDD 节点的真实产出被卡在一个从未合并成功的 worktree 分支里。修复:_check_prd_consistency() 现在按 05 §5.8 同样的"self-target 无条件强制 worktree"规则,复用 work_node_run.create_worktree/ensure_worktree_committed/merge_worktree/remove_worktree 把这次调用也隔离到自己的 worktree 里,不再直接碰主仓库工作区;paths.is_self_target() 为 False 时(普通 target project)保持原样直接调用,不引入不必要的开销。新增两个回归测试锁死这个行为分支(self-target 走 worktree、非 self-target 不走)。清理过程中还顺手发现并合并了一个被卡住的真实 worktree 分支(SDD 节点当时的真实产出,合并冲突只是两条几乎一样的 memory 文案措辞不同,手工解决),以及三个此前测试遗留下来从未清理的孤儿 worktree(git worktree remove)。
    • 全部 5 处修复后,python3 -m unittest discover -s src 380 个测试全绿(含新增的);REQ-01~14 现在逐条都有真实覆盖,PH4 可以视为真正收口,PH5 排期不再依赖对这份回归的信任假设。
    • 覆盖:01-M work_graph_run/work_node_execution(真正被生产代码写入,不再只是测试 fixture);03-C c_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(agentType spike 里模型自称"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-生成记录.md A-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_category CHECK 约束加了 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.py docstring 里记录。
    • 覆盖: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.md 9 步走查场景本身(一个普通的自我改进 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_diff artifact 内容里解析 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-037 c_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 分类,触发条件已按用户决策订正)

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/状态文件约定参考 graphloop hermes_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(参照 graphloop hermes_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-C output_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-M deployment.candidate_instance_dir(补字段);backend_server.py 新增可独立运行的 CLI 入口
    • 真实 bug(2026-08-03,U-042 第三次真实自举压测才第一次真正跑到这一步):前两次真实压测分别卡在"人设文件没部署"和"worktree 改动没提交"这两个更早的环节,这是第一次真正执行到 stage_deployment() 本体。stage_deployment() 自己已经正确处理了"没有上一条 finalized deployment"这个引导期边界情况(previous_commit_hash/previous_instance_dir 回退到当前正在跑的 commit/checkout),但 _copy_prod_db() 完全没有对应处理——它假设 prod_db_path 永远已经存在,直接 shutil.copy2(),而 graphlemon 从未真正作为一个长期运行的服务跑过,~/.graphlemon/graphlemon.db 根本不存在,第一次真实调用直接抛出未捕获的 FileNotFoundError。本文件 test_deploy_stage.py 里每一个既有测试的 setUp() 都会先建好一份 prod_db_path,所以这个真实场景之前从未被测过。修复:prod_db_path 不存在时,不再尝试复制,改为直接在候选副本的位置新建一份带完整 schema 的空库(schema.connect() + schema.create_schema())——语义上是对的:第一次部署本来就没有生产数据可继承,候选实例应该从一份干净但可用的库开始,这份库后续 cutover 成功后就会真正变成生产库。新增回归测试 BootstrapProdDbTest,验证 prod_db_path 不存在时不再崩溃,且生成的候选库真的可用(能查询 task 表)。
  • 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-036 deploy_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(哪怕后续项因为短路没跑,已跑的那几条也留痕)。
    • 真实 bug(2026-08-03,U-042 第七/十一次真实压测都在这里失败):候选进程真实拉起后连续两次都在同一秒内卡在 http_health 检查——process_alive 已经判定"活着",但 urlopen 立刻拿到 Connection refused。真实原因:process_alive 判定的是"进程存在",不是"HTTP 服务已经真正绑定端口开始监听"——候选进程从 Popen 到 backend_server.py 完成 import、真正调用 serve_forever() 之间有一段真实存在的启动延迟,check_http_health 原来的实现是一次性 urlopen(timeout=X),这个 timeout 只控制"连上之后等响应等多久",对"端口压根还没开始监听"这种情况完全不起作用——ECONNREFUSED 是操作系统立刻拒绝的,不会等 timeout。这条修复本身涉及一处 03-C 明确写的设计决策("不重试——重试属于给坏版本第二次机会"),没有直接动手改,先跟用户确认了这算不算违反那条策略:结论是"容忍启动延迟"和"给一个已经失败的检查第二次机会"是两回事——check_process_alive 自己早就在做前者(轮询到 30s 上限),只是 http_health 没跟上同一个模式而已;一次真实的非 2xx 响应仍然是"服务已经起来但判定为不健康",依旧立刻失败不重试,这条界线没有被打破。check_http_health 现在改成和 check_process_alive 一样在自己的 timeout 窗口内轮询,只对连接类错误(URLError/OSError,即"还没开始监听")重试,真实收到的非 2xx 响应仍然立即失败。补了两个真实回归测试:test_connection_refused_polls_until_server_actually_listening(真实起一个延迟 0.3 秒才开始监听的服务器,验证轮询最终能等到它)、test_real_http_error_response_fails_immediately_without_polling(确认真实的 500 响应不会被误当成"还没起来"而重试)。
    • 覆盖:03-C c_deploy_supervisor_health_check;backend_server.py 新增 GET /api/health(U-037 留空的口子,这里补上);01-M deploy_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-M deploy_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 唯一遗留的未定义行为
    • 实现补充(2026-08-02)——本 Unit 是 PH5 关键路径上明确标注返工风险最高的一环,按计划要求真的用人为制造的失败场景验证了兜底路径,没有假设"大概率不会发生"就跳过:新增 supervisor/deploy_rollback.py。设计上两处判断,都是按已有约定处理,不是新拍的:① 没有一个统一的 rollback(conn, deployment_id) 总入口——deployment 表存了 candidate_instance_dir(U-037)但从没存过任何 PID 列,deploy_stage.stage_deployment()/deploy_cutover.cutover() 早就要求调用方直接把 pid 传进来(这些 pid 只在同一个 Deploy Supervisor 进程的生命周期内才有意义),rollback_path_a/rollback_path_b 延续同一约定,没有另造一套。② 03-C 的 output_contract 只写了 {status: 'rolled_back'|'health_check_failed'|'rollback_failed'},从没出现过 blocked_pending_human,但 04-P 状态转移表的 error_state 列和 03-C 的 errors 描述都明确写"转 blocked_pending_human"——读作"rollback_failed 是错误名/返回值,真正持久化的 deployment.status 是 blocked_pending_human",和 deploy_stage.CheckoutFailedError/deploy_cutover.CutoverFailedError 已经在用的同一个模式完全一致,不是新发明。git revert 冲突(本 Unit 自己点名要求处理的场景,design doc §10 开放问题 3)不开新分支,直接并入两条路径共用的"回滚本身失败"兜底,git revert --abort 收尾避免仓库卡在冲突中间态。
    • 真实 bug(2026-08-03,U-042 第七次真实自举压测第一次真正走到 health_check 环节):这是六次前置 bug 修完之后第一次真正让 stage_deployment→run_health_check→rollback_path_a 这条链路对着真实候选进程走一遍——候选健康检查真实失败,rollback_path_a 真实终止了候选进程、真实 git revert 了合并提交,全部正确;但候选自己的 git worktree add --detach 检出目录(candidate_instance_dir,U-037 就是为了追踪它才补的字段)从始至终没人清理,永久留在磁盘上、还继续注册在 git worktree list 里——一个确认失败的候选之后不会再被用到,路径 A 理应清掉它,和成功 cutover 后候选变成 previous_instance_dir 需要保留到监控窗口结束是两回事,不能用同一套"先留着"逻辑。修复:rollback_path_a 新增可选参数 candidate_instance_dir(默认 None,不影响任何未使用真实 worktree 检出的既有测试),提供时在终止进程之后、git revert 之前先 git worktree remove --force;self_hosting_e2e.py 把 stage_deployment() 自己返回值里早就有的 candidate_instance_dir 传过去。补了一个用真实 git worktree add --detach 检出目录的回归测试,验证目录和 git worktree list 注册都被清理干净。清理过程中顺带发现并杀掉了三个更早的 test_deploy_rollback.py 真实进程测试遗留下来、backing 目录早已不存在的孤儿进程。
    • 真实验证,四个场景全部对着真实子进程/真实 git 仓库跑过,没有 mock 关键路径:路径 A happy(真进程 sleep 300 模拟候选、真的删一次性 DB 文件、真实 git revert 落地、真的写一条 bug_record);路径 A 失败(构造一个真实会冲突的 git 历史——v2 是待回滚的自我修改,v3 是之后touch 同一行的后续提交——git revert 真的冲突,验证 blocked_pending_human + 仓库被 --abort 清理干净、不留冲突中间态);路径 B happy(真实 git worktree add 出一份"上一个已知良好版本"的检出目录,真实拉起一个"坏 Live"进程,真实停掉+复活旧版本+真实三项复核+真实 revert,复活的进程最后真的在验证用的端口上对外提供服务,不只是返回值对);路径 B 失败(本 Unit 最该较真的场景):previous_instance_dir 指向一个真实存在但完全没有 Python 包内容的空目录,真实拉起会真的失败/绑不上端口,验证最终落到 blocked_pending_human 而不是卡死重试或悄悄放过,且确认这次真的没有执行 git revert(提交内容原样保留,因为回滚在到达那一步之前就已经失败)。
    • 覆盖:03-C c_deploy_supervisor_rollback;git revert 冲突场景(design doc §10 开放问题 3,已有明确处理)
  • 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 版本
    • 真实压测(2026-08-03,累计 13 次真实尝试,用户明确授权、逐次确认后执行):从 U-042 建成到本次压测之前,这条真实自举闭环从未真正跑过一次。逐次真实跑之后依次发现并修复了 9 个此前完全未知的真实缺口,每一个都只有真跑起来才会暴露:① graphlemon 自己的 .claude/agents/*.md 从未真正部署过(每个之前的真实测试都是对着一次性 target project 克隆做的,从没有自我 targeting 过);② work_node_run.ensure_worktree_committed() 缺失——Coder 编辑 worktree 后从不 commit,merge_worktree 空操作,真实改动被 remove_worktree 静默吞掉;③ deploy_stage._copy_prod_db() 没处理"第一次部署、prod db 压根不存在"这个引导期场景;④ _check_prd_consistency() 没做 self-target worktree 隔离,真实 memory 写入落在主仓库触发下一个节点合并失败;⑤ _normalize_graph_manifest() 没处理"LLM 完全不给 depends_on/edges"这种比已知情况更彻底的偏差;⑥ ensure_worktree_committed() 兜底只覆盖了 worktree 自身,没覆盖主仓库检出目录本身的漂移;⑦ 修⑥引入的新 bug——排除 pathspec 和 .gitignore 同时命中同一路径时 git add -A 直接报错;⑧ deploy_rollback.rollback_path_a() 从不清理候选的 worktree 检出目录,永久留在磁盘上;⑨ check_http_health() 单次尝试不轮询,候选进程刚 spawn、端口还没真正监听时立刻误判为不健康(这条修复涉及 03-C"不重试"策略的解释,跟用户确认后再动手,结论是"容忍启动延迟"和"给失败检查第二次机会"是两回事)。第 13 次真实压测第一次真正走完了完整链路(task→plan→code→merge→classify→stage→health_check→真实 cutover),只在 monitor 阶段因为真实生产参数是 24 小时或 10 个真实任务才关闭监控窗口(deploy_monitor.MONITORING_WINDOW_HOURS/MONITORING_MIN_TASK_COUNT)而没能在交互式会话里等到 finalized——不是 bug,是真实生产参数本来就不该被一次交互式验收跑等到;run_self_hosting_closed_loop() 补了 monitor_window_hours/monitor_min_task_count 两个可覆盖参数(和已有的 monitor_timeout_cycles 同一类"给验收跑用的旋钮",不影响真实生产默认值),使真正拿到一次 finalized 终态在下一次压测里变得可行。
    • 实现补充(2026-08-02):新增 supervisor/self_hosting_e2e.py:run_self_hosting_closed_loop() 把这次会话建好的全部 PH5 模块(U-034~U-041)+ Graph Engine 自己的 scheduler.run_goal_to_completion(U-032)串成一条真实可调用的链路——真实 self-target Goal → Planner/Coder/Validator(真实 claude 调用,scheduler 内部已经走 U-035 的 self-target 强制 worktree)→ PASS 后走 U-035 记的 core_engine 审计 bug_record → deploy_stage.stage_deployment → deploy_health_check.run_health_check → 通过则 deploy_cutover.cutover,不通过直接 deploy_rollback.rollback_path_a 停在这一步 → 循环 deploy_monitor.run_monitor_cycle 直到 monitoring_window_clean(finalized)或 anomaly_detected(deploy_rollback.rollback_path_b)——这正是本 Unit 要求的"两条路径至少走一条",实现上两条路径都是同一次真实调用可能走到的真实分支,不是分别写死两个脚本。明确不是生产级 Deploy Supervisor 的最终 poll_fn 实现——真正独立轮询"有没有新的 self-target 合并等待部署"的发现机制是 U-036 留的钩子,本 Unit 只提供验收场景自己的编排入口,不代表以后正式的常驻发现循环长这样,避免把验收脚本和生产实现搞混。
    • Mock 版本(日常回归):test_self_hosting_e2e.py 前 6 个测试类在每个已经独立测试过真实机制的模块边界上打桩(scheduler.run_goal_to_completion/deploy_stage.stage_deployment/deploy_health_check.run_health_check/deploy_cutover.cutover/deploy_monitor.run_monitor_cycle/deploy_rollback.rollback_path_a/rollback_path_b),只验证编排本身的分支/顺序逻辑对不对(任务没到 done 提前返回、不是 core_engine 提前返回、健康检查失败走 A 不碰 cutover、监控干净走 finalized、监控异常走 B),不重新证明任何一个模块自己的真实机制——那些已经在各自的测试文件里用真实进程/真实 git 仓库验证过了。
    • 真实版本,已建好但没有执行:RealSelfHostingClosedLoopTest,字面复现 self-development-feasibility.md 例子本身("loop_round > 3 应该是 loop_round >= 3")。用了一个和项目一贯的 RUN_REAL_AGENT_E2E 不同、单独命名的环境变量 RUN_REAL_SELF_HOSTING_E2E——这是本项目目前为止影响面最大的一个动作,不只是花真钱调用 claude(那是 RUN_REAL_AGENT_E2E 覆盖的常规风险),而是真的可能替换掉正在跑的 graphlemon 进程本身、真的会在这个仓库自己的 git 历史上跑 git revert——用不同名字的环境变量,是为了不让"为了跑别的真实测试而设的 RUN_REAL_AGENT_E2E=1"意外带着触发这个。没有替用户执行这个测试:按本 Unit 开工前收到的明确指示,这类"影响范围超出本进程/本会话"的动作,写好、gate 好就停,真正执行前需要用 AskUserQuestion 问过用户——这次实现结束后立刻会问。
    • 覆盖:self_hosting_e2e.py(编排层,串联 U-032/034~041 全部真实模块);真实验收场景已建好、默认不跑

PH5-S4 可观测性(非阻塞,可延后)

  • U-043 [Value Path] 控制台展示 deployment 状态与健康信号(02-V 新增视图)
    • 依赖:U-024, U-040
    • Done:能在控制台看到当前是否有 deployment 在进行中、处于状态机的哪个状态、最近一次健康信号采样结果
    • 实现补充(2026-08-02):02-V 从没写过这个视图(PH5 是后加的 Phase,02-V 原文只覆盖 PH0-PH4),DOM/class 命名沿用现有页面(v_skill_library 等)的既有约定,没有 wireframe 可以照抄。新增 src/deployment_query.py(list_deployments/get_current_deployment/get_deployment,deployment 表没有 project_id——graphlemon 自己只有一份 self-target,不是每项目一份,所以读接口不需要 project_id 过滤,和 v_skill_library 的"平台级资产"是同一类情况)+ backend_server.py 三个新路由(GET /api/deployments、GET /api/deployments/current、GET /api/deployments/:id,后者带最近 50 条 deploy_health_signal)。前端 v_deployment_status:deployment.status 变化不走 c_ws_subscribe(Deploy Supervisor 直接写 deployment/deploy_health_signal 表,不产生自己的 execution_event),页面改用定时轮询(5s 间隔)而不是复用 WS 连接,路由切换时清理 interval 的方式和其余页面清理 WS 连接是同一个 registerRouteCleanup 约定。验证方式:9 个 deployment_query 单测 + 5 个真实 HTTP 路由测试(test_backend_server.py 新增 DeploymentRouteTest)+ 对着真实跑起来的服务器用 urllib 走了一遍全部三条路由确认真实响应结构正确(浏览器扩展本次不可用,node --check 过语法)。
    • 提交时机的一个插曲,记录一下:新增 deployment_query.py 后,supervisor/test_deploy_cutover.py/test_deploy_rollback.py/test_deploy_stage.py 里那些用 git stash create 模拟"对着当前未提交工作区做真实 candidate 拉起"的测试集体失败——根因是 git stash create 不会捕获未跟踪的新文件(这条限制 U-037 当时已经在自己的测试注释里写明过),deployment_query.py 是全新文件,没被 stash 快照捕捉到,检出出来的候选进程 import 它时直接失败,"候选进程起不来"。这不是这次改动本身有 bug(真正提交之后,HEAD 天然包含这份新文件,同一批测试会恢复通过),是这批测试自己的固有前提("只覆盖已跟踪文件的改动")第一次被一次"新增文件"的改动踩到;提交后立即重跑过一次完整套件确认确实恢复全绿,不是只凭猜测判断"这样应该没事"。
    • 覆盖:src/deployment_query.py;backend_server.py 新增 GET /api/deployments/GET /api/deployments/current/GET /api/deployments/:id;frontend/app.js/index.html 新增 v_deployment_status

PH5 已知交叉缺口(不阻塞设计定稿,实现前需要留意)

  1. supervisor/isolation/ 现有代码目前只做过一次手工冒烟测试(本次会话里跑的 isolate → diff → apply 往返验证),不是正式 unittest 套件——U-036 里已列为待办,暂不算"已完成"。
  2. supervisor/ 目录归属 U-002 还是 U-003 的问题目前只在 U-036 里留了一个临时决定,还没有真正的架构决策支撑,PH0 真正实现 U-002/U-003 时应该回头确认这个临时决定是否仍然成立。
  3. U-001 的"15 张表"这个数字会在 PH5 落地后变成 17 张(deployment/deploy_health_signal),后续如果有人只看 PH0 的验收标准会觉得和实际情况对不上,属于文档层面需要注意的地方,不是设计缺陷。
  4. 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 标准需要同步更新,避免两份文档各说各话。


5. PH0-PH5 之外:用户直接请求的控制台功能扩展

PH0-PH5(45 个 Unit)全部完成、U-042 真实自举压测进行中期间,用户直接向 Claude Code(不经过 graphlemon 自己的 Planner/Coder/Validator 闭环,原因见下)提出了两个新的控制台页面需求,是对 02-V 原有非目标"不做完整多项目管理 UI(项目列表/切换)"的明确、有意突破,不是发现遗漏后的补充。

为什么不走 graphlemon 自己的自举闭环去实现这个需求:讨论过程中确认了当前 implement_code()(Coder 阶段)永远用一个写死的单节点 graph_manifest,从不调用 plan_work_graph() 规划自己的 Work Graph——不管 SDD 多详细,整个实现都只交给一次 Coder 会话,没有按视图/模块拆分执行的能力(U-031 当时明确把"造一个 Coder 图规划器"排除在集成范围之外)。这个需求涉及两个新页面、6+ 个新后端接口,用一次不可拆分的 Coder 会话去做风险高、不可控,所以直接由 Claude Code 实现,不走自举闭环。

  • v_project_list(新增):项目列表 + 增删改查。"新建/导入项目"复用已有的 v_project_setup/c_project_create 流程——project_scaffold.create_project() 原本就只在目标目录下新增 .graphlemon/,不碰任何已有代码,"导入一个已有代码的目录"这条诉求不需要新代码。真正新增的是读/改半边:src/project_query.py(list_projects/get_project/update_project),新增 HTTP GET /api/projects、GET /api/projects/:id、PUT /api/projects/:id、DELETE /api/projects/:id(DELETE 语义是把 status 置为 archived,不是物理删除行——project_id 被 task/artifact/bug_record 等大量外键引用,物理删除要么违反约束要么需要级联删掉这些本该保留的审计历史,01-M 自己的 active/archived 枚举本来就是软删除的设计)。
  • v_goal_list(对已有 v_task_board/v_task_detail 的扩展,非全新页面):搜索/筛选已有,新增排序(task_query.list_tasks() 加 sort/order 参数,白名单校验列名防注入);v_task_detail 新增"编辑目标"/"删除任务",仅在 status='queued' 时出现——一旦 Planner/Coder 已经基于这个 goal_text 产出过真实 artifact/work_graph_run,改写或删除就会让审计记录指向一个不存在的目标,这正是这个代码库一直在避免的不可追溯漂移,所以 task_query.update_task_goal_text()/delete_task() 都在非 queued 状态下拒绝(TaskNotEditableError,HTTP 409)。
  • 真实验证时发现并修复一个真 bug:delete_task() 第一版对一个真实创建的(走 task_create.create_task(),不是测试自己手写的 INSERT)queued 任务执行 DELETE FROM task 时,真实抛出 sqlite3.IntegrityError——task_create.create_task() 自己在同一个事务里总会写一条 execution_event 行(01-M 自己的外键,没有 ON DELETE CASCADE),哪怕这个任务从没被 Planner/Coder/Validator 碰过,也已经至少有一条自己创建时留下的事件。补上了同事务内先删这条任务自己的 execution_event(queued 态下不可能有真正的 artifact/bug_record/work_graph_run,所以这是唯一可能挡路的外键)。这次 bug 是测试自己的 _insert_task 辅助函数(手写 INSERT,从不写 execution_event)从没暴露过的——单测通过不代表真的能用,加了一个走真实 task_create.create_task() 路径的回归测试锁死。
  • 验证方式:src/test_project_query.py(12 个用例)、src/test_task_query.py 新增的排序/编辑/删除用例(含上面这条真实路径回归)、src/test_backend_server.py 新增两个真实 HTTP 路由测试类(ProjectListAndCrudRouteTest/TaskCrudRouteTest,真实起服务器+真实请求,不 mock)。浏览器扩展本次仍不可用,额外对着真实运行的服务器用 curl 走了一遍全部新增接口(含前面提到的真删除 bug 就是这一步抓到的)。474 个测试全绿。