Commit b96e6895c3013eececdfe5c6aa0516c051f02175

Authored by bboonnee
1 parent c828c209

docs: reorganize docs/todo/ - delete superseded doc, split done/undone scheduler…

… work, compress bloated incident log

- Delete org-graph-as-work-graph-unified-loop-requirements.md: fully
  superseded by org-graph-unification-and-universal-self-planning-
  requirements.md; folded its one non-duplicated detail (the 2084590a
  benchmark-narration trigger incident) into that doc's background instead.
- Split scheduler-production-polling-loop-{requirements,design}.md by
  completion status: FR-2 (in-flight task reconciliation, shipped) moves to
  docs/done/scheduler-task-reconciliation-{requirements,design}.md; FR-1
  (new-goal auto-dispatch, still undecided) becomes a small standalone
  docs/todo/scheduler-new-goal-dispatch-requirements.md instead of staying
  bundled with a doc that's mostly done.
- Compress validator-test-script-narrative-output-issue.md's six
  ad-hoc "第N轮更新" paragraphs crammed into one status line into a scannable
  timeline table; moved the task 2084590a benchmark evidence into §1 where
  the other repro examples live instead of leaving it stranded in the header.
- implementation-gap-analysis.md: logged two new gaps found today (no
  pre-Coder review of Planner's output; node execution still depends on
  native Dynamic Workflow) plus the architecture.md §10.1 staleness itself,
  per that doc's own "edit in place" maintenance convention.
- Updated the doc-path references left in src/task_execution.py and
  src/task_reconciliation.py docstrings to point at the new locations.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
docs/todo/scheduler-production-polling-loop-design.md renamed to docs/done/scheduler-task-reconciliation-design.md
1   -# 生产环境调度/复原循环——详细设计(FR-2 部分)
  1 +# 在途任务复原循环——详细设计
2 2  
3 3 **日期**: 2026-08-06
4   -**性质**: SDD 层面设计文档,对应 `scheduler-production-polling-loop-requirements.md`。
5   -**范围**: 只覆盖该需求文档的 FR-2(在途任务复原)。FR-1(新 Goal 自动派发)按需求文档自身的
6   -非目标 #1 和风险章节,仍然是一个未决的、需要单独确认的自主权边界决策,本文档不涉及,需求文档
7   -本身也因此暂不移入 `docs/done/`。
8   -**状态**: 已实现(`src/task_reconciliation.py` + `src/backend_server.py::_start_reconciliation_loop`
9   -+ `src/task_execution.py` 的 in-flight 互斥锁),本文档是对已落地实现的设计记录,补齐
10   -`docs/todo/` 两段式流程里"确认后产出 SDD 设计文档"这一步。
  4 +**性质**: SDD 层面设计文档,对应 `docs/done/scheduler-task-reconciliation-requirements.md`。
  5 +**状态**: ✅ 已实现(`src/task_reconciliation.py` + `src/backend_server.py::_start_reconciliation_loop`
  6 ++ `src/task_execution.py` 的 in-flight 互斥锁)。**2026-08-08**:随需求文档一起从
  7 +`docs/todo/scheduler-production-polling-loop-design.md` 拆分移入 `docs/done/`(原文档同时覆盖已完成
  8 +的 FR-2 和仍是草案的 FR-1,拆分后本文档只保留 FR-2 部分,FR-1 独立见
  9 +`docs/todo/scheduler-new-goal-dispatch-requirements.md`)。
11 10  
12 11 ## 1. 整体方案
13 12  
14   -拆成三层,分别对应需求文档 §8 的五个开放问题:
  13 +拆成三层,分别对应需求文档"开放问题"里跟 FR-2 相关的几条:
15 14  
16 15 1. **候选发现**(`task_reconciliation.find_stale_resumable_tasks`):只读查询,扫描
17 16 `task.status IN task_execution._RESUMABLE_STATUSES` 且"最近一次活动早于阈值"的任务。
... ... @@ -24,13 +23,13 @@
24 23  
25 24 ## 2. 开放问题回答
26 25  
27   -| 需求文档 §8 的问题 | 本设计的答案 |
  26 +| 问题 | 本设计的答案 |
28 27 |---|---|
29   -| FR-2 轮询周期 | 300s(`_DEFAULT_RECONCILIATION_INTERVAL_SECONDS`),可通过 `--reconciliation-interval-seconds` 覆盖 |
  28 +| 轮询周期 | 300s(`_DEFAULT_RECONCILIATION_INTERVAL_SECONDS`),可通过 `--reconciliation-interval-seconds` 覆盖 |
30 29 | "一段时间没有新进展"的判定信号与阈值 | 用 `execution_event` 该 task 最近一条记录的 `created_at`(无记录则退回 `task.created_at`),**不用** `task.updated_at`——阶段执行期间 `task.updated_at` 本来就不变,这正是这次事故排查时的真实盲点。阈值统一 1800s,不区分阶段(见下方"已知简化") |
31   -| FR-2 载体形式 | `backend_server.py::main()` 内置守护线程,不是独立进程。理由:FR-2 只是"帮已存在的任务把该调用的函数再调用一次",复用的是 `task_execution` 现有的进程内 in-flight 互斥锁,做成独立进程反而需要额外的跨进程协调;这与 Deploy Supervisor"独立进程、无共享失败域"哲学的适用场景不同——Deploy Supervisor 独立是因为它需要在 `backend_server` 本身挂掉时还能做健康检查/回滚,FR-2 不需要这个属性 |
32   -| FR-2 互斥机制 | 进程内 `threading.Lock` + `set`(`task_execution._in_flight_task_ids`),不是数据库锁。有意选择:只需要防止**同一进程内**两个线程(控制台点 Start 的线程 vs 本循环)竞争同一个 `task_id`;不需要跨进程/跨重启存活——重启后这个 set 清空为空集合本身就是正确状态(意味着"这个进程里没有任何东西正在跑"),不是 bug |
33   -| 是否暴露为可配置项 | 是:`--reconciliation-interval-seconds`、`--disable-reconciliation-loop`。硬编码的是阈值本身(`_DEFAULT_STALE_AFTER_SECONDS=1800`),未暴露成 CLI 参数——v1 先用一个保守默认值,符合需求文档非目标 #4 |
  30 +| 载体形式 | `backend_server.py::main()` 内置守护线程,不是独立进程。理由:只是"帮已存在的任务把该调用的函数再调用一次",复用的是 `task_execution` 现有的进程内 in-flight 互斥锁,做成独立进程反而需要额外的跨进程协调;这与 Deploy Supervisor"独立进程、无共享失败域"哲学的适用场景不同——Deploy Supervisor 独立是因为它需要在 `backend_server` 本身挂掉时还能做健康检查/回滚,这里不需要这个属性 |
  31 +| 互斥机制 | 进程内 `threading.Lock` + `set`(`task_execution._in_flight_task_ids`),不是数据库锁。有意选择:只需要防止**同一进程内**两个线程(控制台点 Start 的线程 vs 本循环)竞争同一个 `task_id`;不需要跨进程/跨重启存活——重启后这个 set 清空为空集合本身就是正确状态(意味着"这个进程里没有任何东西正在跑"),不是 bug |
  32 +| 是否暴露为可配置项 | 是:`--reconciliation-interval-seconds`、`--disable-reconciliation-loop`。硬编码的是阈值本身(`_DEFAULT_STALE_AFTER_SECONDS=1800`),未暴露成 CLI 参数——v1 先用一个保守默认值 |
34 33  
35 34 ## 3. 竞态处理
36 35  
... ... @@ -42,14 +41,13 @@
42 41  
43 42 ## 4. 已知简化(v1 天花板,非 bug)
44 43  
45   -- **单一全局阈值,不分阶段**。需求文档 §8 提到"Coder 阶段可能比 Validator 阶段容忍更长空闲",
46   - v1 用统一的 1800s(比最长单次阻塞调用 Validator 自身 Work Graph 的 `timeout=1200s` 多
47   - 600s 余量)。升级路径:给 `_RESUMABLE_STATUSES` 里每个状态配一个独立阈值,按
  44 +- **单一全局阈值,不分阶段**。v1 用统一的 1800s(比最长单次阻塞调用 Validator 自身 Work Graph 的
  45 + `timeout=1200s` 多 600s 余量)。升级路径:给 `_RESUMABLE_STATUSES` 里每个状态配一个独立阈值,按
48 46 `task.assigned_org_node`/`task.status` 查表。
49 47 - **候选扫描全表查询,无索引优化**。当前任务量级下(个位数到几十个并发 task)不是问题;
50 48 升级路径:如果任务量上升,给 `task.status`、`execution_event.task_id` 加索引。
51 49  
52   -## 5. 可观测性修复(本次评审新增,2026-08-06)
  50 +## 5. 可观测性修复(2026-08-06)
53 51  
54 52 原实现里 `_start_reconciliation_loop` 的 `except Exception: pass` 会让"每一轮都失败"和
55 53 "每一轮都没有候选"从外部看起来完全一样——对一个设计目标就是无人值守 7x24 运行的循环来说,
... ... @@ -57,7 +55,7 @@
57 55 是真实 CLI 入口,stderr 在部署环境下通常会被进程管理器捕获落盘)。这不引入新依赖,也不改变
58 56 "一次失败不杀死循环"的既有语义。
59 57  
60   -## 6. 验收标准核对(对照需求文档 §5)
  58 +## 6. 验收标准核对
61 59  
62 60 1. ✅ `test_finds_task_with_no_activity_at_all` / `test_stale_event_still_flagged` 覆盖"卡住的
63 61 任务被重新捡起"
... ... @@ -66,9 +64,7 @@
66 64 单独构造"一次长达接近 1200s 的阻塞调用期间完全没有任何新事件"的场景。这属于用阈值余量
67 65 (1800 vs 1200)做保证、而非用事件覆盖做保证,测试没有专门验证这条时间余量假设本身——留作
68 66 后续如果要收紧阈值时的一个真实回归风险点。
69   -3. FR-1(新建 `queued` Goal 自动派发)不适用——本设计明确不覆盖 FR-1。
70   -4. ✅ `--disable-reconciliation-loop` 提供独立开关;FR-2 与 FR-1 天然独立(FR-1 尚未实现,
71   - 两者不存在耦合)。
  67 +3. ✅ `--disable-reconciliation-loop` 提供独立开关。
72 68  
73 69 ## 6.1 部署候选进程的联动修复(2026-08-06,与本设计同批)
74 70  
... ... @@ -82,8 +78,9 @@
82 78 调用点(Live/回滚路径)**不**需要这个 flag——它们启动的是真正要接管服务的实例,本来就应该有
83 79 复原循环。
84 80  
85   -## 7. 后续(不在本次范围内)
  81 +## 7. 关系
86 82  
87   -FR-1(新 Goal 自动派发)仍然是需求文档标注的"改变系统自主权边界"的决策,按该文档非目标 #1
88   -的要求,需要单独确认是否要做、什么时候做,不与 FR-2 混跑。在那之前,`queued` 状态的新 Goal
89   -仍然需要人工触发 `run_goal_to_completion`/`scheduler.run_scheduler_once()`。
  83 +跟"新 Goal 自动派发"(`docs/todo/scheduler-new-goal-dispatch-requirements.md`)是独立、正交的
  84 +两个机制——前者只是让已经存在的任务继续跑完,后者是让还没跑过的 `queued` Goal 自动开始跑,两者
  85 +不共享循环、不共享判断逻辑,也不应该被合并成一个"万能调度循环"。在后者被单独评审确认前,
  86 +`queued` 状态的新 Goal 仍然需要人工触发 `run_goal_to_completion`/`scheduler.run_scheduler_once()`。
... ...
docs/todo/scheduler-production-polling-loop-requirements.md renamed to docs/done/scheduler-task-reconciliation-requirements.md
1   -# 生产环境调度/复原循环——需求文档
  1 +# 在途任务复原循环——需求文档
2 2  
3 3 **日期**: 2026-08-06
4 4 **性质**: 需求文档(BRD/PRD 层面),不含详细技术设计。回应一次真实事故的根因排查——任务 `3605272e-fe33-48cc-b3f3-e0c0ba26f970` 在 `validating` 状态卡死 15+ 小时,`execution_event` 表在此期间零记录,`work_graph_run` 里从未出现 Validator 的运行记录。
5   -**状态**: FR-2(在途任务复原)✅ 已实现(2026-08-06)。FR-1(新 Goal 自动派发)仍是草案,按第 3 节非目标/第 7 节风险的既定判断,未与 FR-2 一起做——它改变系统自主权边界,需要单独评审决策。
  5 +**状态**: ✅ 已实现(2026-08-06)。**2026-08-08 拆分**:原文档 `scheduler-production-polling-loop-requirements.md` 同时覆盖 FR-1(新 Goal 自动派发,仍是草案)和 FR-2(本文档,已实现),两者状态不同、混在一份文档里不利于归档——本文档是拆分后只保留 FR-2(已完成部分)的版本,移入 `docs/done/`;FR-1 独立留在 `docs/todo/scheduler-new-goal-dispatch-requirements.md`。设计文档见 `docs/done/scheduler-task-reconciliation-design.md`。
6 6  
7   -## 0. FR-2 实现记录(2026-08-06)
  7 +## 0. 实现记录(2026-08-06)
8 8  
9 9 - `src/task_reconciliation.py`(新增):`find_stale_resumable_tasks()`(纯查询,判定信号是 `execution_event` 里该任务最近一条记录的 `MAX(created_at)`,没有则退回 `task.created_at`——不是 `task.updated_at`,理由见模块 docstring 和 FR-3)+ `reconcile_once()`(扫描后逐个 dispatch,单个候选 dispatch 失败不影响其余候选,呼应 04-P"调度器继续执行队列中其他任务")。
10 10 - `src/task_execution.py`:新增进程内 `_in_flight_task_ids`(`threading.Lock` 保护的内存 set)+ `is_in_flight()`/`_try_claim()`/`_release()`,`start_task_execution_in_background()` 接线为"已在途则立即 `TaskExecutionError`,不重复派发"——这正是 FR-5 之外一条隐含但必须有的约束(防止复原循环和人工点 Start 竞争同一个任务)。
... ... @@ -20,56 +20,38 @@
20 20 排查这次真实事故发现两层原因:
21 21  
22 22 1. **生产后端进程从来没有接入过任何轮询循环**。`docs/implementation-gap-analysis.md` 第 6 条早就记录了这个缺口:"给某个常驻进程(`backend_server.py` 或独立进程)接入 `scheduler.run_scheduler_once()` 的轮询循环",评级"高",并且明确建议"改变系统自己的自主权边界,建议单独一批、额外人工审查 diff,不要和其他 gap 混跑"。这次事故是这条缺口第一次以真实用户可见的方式发作。
23   -2. **即使真的接上 `scheduler.run_scheduler_once()`,也不会自动修好这次事故这类问题**——这是排查过程中新发现的、比原缺口描述更精确的一点。`run_scheduler_once(db_path, goals, max_workers=None)` 的入参 `goals` 是 `[(target_folder_path, goal_text), ...]`,语义是"这一轮要**新建**并跑完这些 Goal",内部通过 `task_create.create_task()` 每次都新建一个 `task` 行。它解决的是"新 Goal 怎么被自动拿去跑",**不是**"一个已经存在、卡在某个中间状态的 `task_id` 怎么被自动接回去继续跑"——后者需要的是 `task_execution.run_or_resume_task_lifecycle`(按 `task_id` 恢复),是完全不同的另一个函数、另一条代码路径。这次事故属于第二类:任务本身已经存在、状态是 `validating`(`task_execution._RESUMABLE_STATUSES` 里合法的可恢复状态之一),只是没有任何东西在合适的时机去调用恢复逻辑。
  23 +2. **即使真的接上 `scheduler.run_scheduler_once()`,也不会自动修好这次事故这类问题**——这是排查过程中新发现的、比原缺口描述更精确的一点。`run_scheduler_once(db_path, goals, max_workers=None)` 的入参 `goals` 是 `[(target_folder_path, goal_text), ...]`,语义是"这一轮要**新建**并跑完这些 Goal",内部通过 `task_create.create_task()` 每次都新建一个 `task` 行。它解决的是"新 Goal 怎么被自动拿去跑"(这是 FR-1 的范围,独立文档跟踪),**不是**"一个已经存在、卡在某个中间状态的 `task_id` 怎么被自动接回去继续跑"——后者需要的是 `task_execution.run_or_resume_task_lifecycle`(按 `task_id` 恢复),是完全不同的另一个函数、另一条代码路径。这次事故属于第二类:任务本身已经存在、状态是 `validating`(`task_execution._RESUMABLE_STATUSES` 里合法的可恢复状态之一),只是没有任何东西在合适的时机去调用恢复逻辑。
24 24  
25 25 这次事故本身的具体触发点(`validating` 状态本来就无法通过 `run_or_resume_task_lifecycle` 恢复)已经单独修复(见 `src/task_execution.py` 的 `_RESUMABLE_STATUSES`/`request_pause` 改动,2026-08-06)——这是"人工点 Resume 现在能生效了",不等于"以后不会再有任务卡死没人管"。本需求要解决的是后者。
26 26  
27 27 ## 2. 目标
28 28  
29   -把生产环境需要的"持续推进任务"能力,拆成两个明确区分、可以分开排期的子问题:
30   -
31   -1. **新 Goal 派发**:`queued` 状态的任务(或者说,还没被任何调用触发过 `run_goal_to_completion`/`run_or_resume_task_lifecycle` 的新 Goal)需要被自动拿去执行,不需要人工每次手动点 Start。
32   -2. **在途任务复原(这次事故的真正根因)**:任何已经开始执行、但因为驱动它的那次调用提前退出(进程重启、脚本运行完毕、连接断开等任何原因)而卡在某个可恢复中间状态(`task_execution._RESUMABLE_STATUSES`:`queued`/`planning_brd_prd_sdd`/`coding`/`validating`)、且已经**一段时间没有新进展**的任务,需要被自动重新接回去继续跑,而不是无限期地停在原地、既不算 `blocked_pending_human`(触发不了人工介入通知),也没有任何东西在真的干活。
  29 +**在途任务复原(这次事故的真正根因)**:任何已经开始执行、但因为驱动它的那次调用提前退出(进程重启、脚本运行完毕、连接断开等任何原因)而卡在某个可恢复中间状态(`task_execution._RESUMABLE_STATUSES`:`queued`/`planning_brd_prd_sdd`/`coding`/`validating`)、且已经**一段时间没有新进展**的任务,需要被自动重新接回去继续跑,而不是无限期地停在原地、既不算 `blocked_pending_human`(触发不了人工介入通知),也没有任何东西在真的干活。
33 30  
34 31 ## 3. 非目标
35 32  
36   -1. **不在本文档里裁定"要不要做""什么时候做"这个更大的问题**。`docs/implementation-gap-analysis.md` 已经把这条标注为"改变系统自己的自主权边界"——是否现在就投入、要不要额外的人工评审门槛,是需要单独确认的决策,本文档只负责把需求本身讲清楚,供决策时参考。
37   -2. **不要求解决方案是常驻进程/独立进程/`backend_server.py` 内置线程三选一中的哪一个**。具体载体形式留给设计阶段,本文档只描述行为需求。
38   -3. **不重新设计 `run_scheduler_once`/`run_goal_to_completion`/`run_or_resume_task_lifecycle` 这三个已经存在且经过测试的函数本身**。它们的行为已经是对的,本需求要解决的是"谁、在什么时机去调用它们"这一层,不是重写它们的内部逻辑。
39   -4. **不要求第一版就有一套精细的"任务卡死"判定算法**(比如区分"真的卡死"和"合理地在等真实的 `claude -p` 长耗时调用")。第一版可以用一个保守、简单的阈值(比如"`updated_at`/最近一条 `execution_event` 距今超过 N 分钟,且没有已知的进行中标记"),不需要一次做到完美。
  33 +1. **不要求解决方案是常驻进程/独立进程/`backend_server.py` 内置线程三选一中的哪一个**。具体载体形式留给设计阶段,本文档只描述行为需求。
  34 +2. **不重新设计 `run_or_resume_task_lifecycle` 本身**。它的行为已经是对的,本需求要解决的是"谁、在什么时机去调用它"这一层,不是重写它的内部逻辑。
  35 +3. **不要求第一版就有一套精细的"任务卡死"判定算法**(比如区分"真的卡死"和"合理地在等真实的 `claude -p` 长耗时调用")。第一版可以用一个保守、简单的阈值(比如"最近一条 `execution_event` 距今超过 N 分钟,且没有已知的进行中标记"),不需要一次做到完美。
40 36  
41 37 ## 4. 功能需求
42 38  
43 39 | # | 需求描述 | 优先级 |
44 40 |---|---|---|
45   -| FR-1 | 需要一个机制,定期(具体周期留待设计阶段)调用 `scheduler.run_scheduler_once()` 派发新 Goal——对应「目标 1」。 | 必须 |
46   -| FR-2 | 需要一个独立的机制,定期扫描 `task` 表里状态属于 `task_execution._RESUMABLE_STATUSES` 且"已经一段时间没有新进展"的任务,对每一个调用 `task_execution.run_or_resume_task_lifecycle`(或 `start_task_execution_in_background`)——对应「目标 2」,是这次事故的直接对应修复。 | 必须 |
47   -| FR-3 | FR-2 的"已经一段时间没有新进展"判定,需要能区分"真的没人管"和"确实在真实执行中,只是这一步本来就耗时"——不能简单靠 `task.status` 停留时间判断(比如 Validator 自己的 Work Graph 跑一次可能就要接近 `timeout=1200` 秒),需要参考更细粒度的信号(比如该任务关联的 `work_node_execution`/`execution_event` 最近一次活动时间,而不是 `task.updated_at`——`task.updated_at` 在阶段内部执行期间根本不会变,这正是这次事故排查时的真实盲点)。 | 必须 |
48   -| FR-4 | FR-1、FR-2 两个机制必须能独立开关/独立排期——不能因为"要接 FR-1 需要走完整的人工评审流程"就连带阻塞了 FR-2(这次事故的直接修复)。 | 必须 |
49   -| FR-5 | 无论 FR-1/FR-2 最终选择常驻进程还是独立进程实现,都不能引入新的"共享一个 sqlite 连接跨线程"风险——复用 `scheduler.py` 模块文档里已经验证过的模式(每个 worker 各自开自己的连接 + `busy_timeout`),不要重新踩 U-027 已经用真实 SIGSEGV 验证过的坑。 | 必须 |
  41 +| FR-2 | 需要一个独立的机制,定期扫描 `task` 表里状态属于 `task_execution._RESUMABLE_STATUSES` 且"已经一段时间没有新进展"的任务,对每一个调用 `task_execution.run_or_resume_task_lifecycle`(或 `start_task_execution_in_background`)——是这次事故的直接对应修复。 | 必须 |
  42 +| FR-3 | "已经一段时间没有新进展"的判定,需要能区分"真的没人管"和"确实在真实执行中,只是这一步本来就耗时"——不能简单靠 `task.status` 停留时间判断(比如 Validator 自己的 Work Graph 跑一次可能就要接近 `timeout=1200` 秒),需要参考更细粒度的信号(比如该任务关联的 `work_node_execution`/`execution_event` 最近一次活动时间,而不是 `task.updated_at`——`task.updated_at` 在阶段内部执行期间根本不会变,这正是这次事故排查时的真实盲点)。 | 必须 |
  43 +| FR-5 | 不能引入新的"共享一个 sqlite 连接跨线程"风险——复用 `scheduler.py` 模块文档里已经验证过的模式(每个 worker 各自开自己的连接 + `busy_timeout`),不要重新踩 U-027 已经用真实 SIGSEGV 验证过的坑。 | 必须 |
50 44  
51 45 ## 5. 验收标准
52 46  
53   -1. 人为构造一个"任务卡在 `validating`,很久没有新进展"的场景(复现这次事故的条件),验证 FR-2 的机制能在设定的检测周期内把它重新接回去执行,不需要人工干预。
54   -2. 人为构造一个"任务正常在跑,Validator 的 Work Graph 正在执行中,还没超过合理耗时"的场景,验证 FR-2 的机制**不会**误判成"卡死"、不会对一个正常在跑的任务重复触发 `run_or_resume_task_lifecycle`(这会导致同一个任务被并发跑两次,需要明确防止)。
55   -3. 新建一个 `queued` 状态的 Goal,不手动点 Start,验证 FR-1 的机制能在设定周期内自动派发执行。
56   -4. FR-1 和 FR-2 分别单独关闭时,各自的行为符合预期(关 FR-1 不影响 FR-2 正常工作,反之亦然)。
  47 +1. 人为构造一个"任务卡在 `validating`,很久没有新进展"的场景(复现这次事故的条件),验证机制能在设定的检测周期内把它重新接回去执行,不需要人工干预。
  48 +2. 人为构造一个"任务正常在跑,Validator 的 Work Graph 正在执行中,还没超过合理耗时"的场景,验证机制**不会**误判成"卡死"、不会对一个正常在跑的任务重复触发 `run_or_resume_task_lifecycle`(这会导致同一个任务被并发跑两次,需要明确防止)。
57 49  
58 50 ## 6. 约束与假设
59 51  
60   -- 复用现有的 `task_execution._RESUMABLE_STATUSES`(已扩展为包含 `validating`)作为 FR-2 判断"这个任务原则上可以恢复"的依据,不重新定义一套判断标准。
61   -- FR-2 的具体实现要避免和「人工点 Resume」竞争——如果一个任务已经在被 `start_task_execution_in_background` 的某个后台线程处理,FR-2 的扫描不能对同一个 `task_id` 重复触发第二次执行。具体的互斥机制(数据库标记、内存锁、还是别的方式)留给设计阶段。
  52 +- 复用现有的 `task_execution._RESUMABLE_STATUSES`(已扩展为包含 `validating`)作为判断"这个任务原则上可以恢复"的依据,不重新定义一套判断标准。
  53 +- 要避免和「人工点 Resume」竞争——如果一个任务已经在被 `start_task_execution_in_background` 的某个后台线程处理,扫描不能对同一个 `task_id` 重复触发第二次执行。具体的互斥机制(数据库标记、内存锁、还是别的方式)留给设计阶段。
62 54  
63 55 ## 7. 风险
64 56  
65   -- **FR-1(新 Goal 自动派发)是这次事故排查之前就已经被 `implementation-gap-analysis.md` 标注为"改变系统自主权边界"的决策**,本文档不试图淡化这一点——它意味着系统会在没有人显式点击"Start"的情况下自主开始消耗真实 API 调用、真实修改代码。这条风险原样保留,需要在决策阶段被认真对待。
66   -- **FR-2(在途任务复原)看起来风险更小(只是让已经存在的任务继续跑完,不是凭空发起新工作),但仍然是自主行为的扩展**——需要在设计阶段想清楚"防误判"(FR-3/验收标准 2)没做好的后果:一个只是耗时较长、并没有真的卡死的任务被提前打断重跑,可能造成重复的 API 花费或者更糟的状态不一致。
67   -- **两个机制如果实现成同一个循环、共享同一套判断逻辑,容易在后续维护中被混为一谈**——按第 2 节"目标"明确拆开成两个独立问题,是为了避免这种混淆,设计阶段应该保持这个拆分,不要图省事合并成一个"万能调度循环"。
68   -
69   -## 8. 开放问题(留给详细设计阶段回答)
70   -
71   -- [ ] FR-1/FR-2 各自的轮询周期定多少。
72   -- [ ] FR-3"已经一段时间没有新进展"的具体判定信号和阈值——参考哪张表的哪个时间戳字段,阈值怎么定(是否要按 `task` 当前所处的具体阶段给不同阈值,比如 Coder 阶段允许的空闲时间可能比 Validator 阶段更长)。
73   -- [ ] FR-1/FR-2 的载体形式:`backend_server.py` 内置后台线程,还是独立进程(类似 Deploy Supervisor 的"独立进程、无共享失败域"哲学)。
74   -- [ ] FR-2 的互斥机制具体怎么实现,防止同一个任务被并发重复触发。
75   -- [ ] 这两个机制要不要暴露成可配置项(管理后台开关/环境变量),还是先硬编码一个保守的默认行为。
  57 +- **看起来风险较小(只是让已经存在的任务继续跑完,不是凭空发起新工作),但仍然是自主行为的扩展**——需要在设计阶段想清楚"防误判"(FR-3/验收标准 2)没做好的后果:一个只是耗时较长、并没有真的卡死的任务被提前打断重跑,可能造成重复的 API 花费或者更糟的状态不一致。
... ...
docs/implementation-gap-analysis.md
... ... @@ -72,6 +72,18 @@
72 72 场景仍未专门测试过,不算完全清零。
73 73 - `v_artifact_browser`(U-028)有意不做 Markdown 渲染/代码高亮(02-V 原型期望有),因为要避免引入第一个前端
74 74 第三方库,判断为有意简化而非漏做。
  75 +- **Coder 开始实现前,Planner 自己的产出从未被独立复核过**(2026-08-08 新发现,真实复现见
  76 + `docs/todo/validator-test-script-narrative-output-issue.md` 时间线第 5 轮,任务 `2084590a`):`org_node_lifecycle.py`
  77 + 的固定流程是 `planner → coder → validator`,`artifact_write.py` 的 `validation_status` 写入时无条件设成
  78 + `'passed'`(自己承认"语义内容校验还没有专属 LLM-judgment 调用"),实际上没有任何机制拦住"Planner 产出叙述文字
  79 + 冒充 benchmark"这类内容直接流入 Coder。**设计已完成,待实施**:`docs/todo/org-graph-unification-phase1-review-
  80 + gate-design.md`(新增 `planner_reviewer` org role + `reviewing_planner_output` 状态,插在 Planner/Coder 之间)。
  81 +- **Work Graph 单节点执行层仍依赖 Claude Code 原生 Dynamic Workflow,不是自研**(2026-08-08 新发现):图编排层
  82 + (`work_graph_orchestrate.py`)早已自研(REQ-06 断点续跑驱动),但 `_render_node_script()`/`_llm_judgment_
  83 + verify_fn`/`e2e_pipeline._check_prd_consistency` 三处仍在生成一次性 `.claude/workflows/*.js` 脚本、经
  84 + `claude -p 'Run /<name>...'` 执行——`plan_work_graph()` 已经证明同一个"`--agent` 身份 + 结构化输出"需求可以
  85 + 不经 Workflow、靠 flat `claude --agent` 调用 + JSON 围栏提取解决。**设计已完成,待实施**:
  86 + `docs/todo/work-graph-node-execution-deblackbox-requirements.md`。
75 87  
76 88 ## 4. 文档本身滞后于实现(这本身就是一种"差距")
77 89  
... ... @@ -81,6 +93,12 @@
81 93 经过时——`development-plan.md` U-042 条目下 2026-08-03 的记录显示之后确实做了 13 次真实自举压测(`git log`
82 94 里那一长串 `graphlemon: auto-commit`/`merge`/artifact 提交就是证据),只是仍未到达 `finalized`。连"总结性
83 95 收口文档"都会在自举持续运行的过程中被甩在后面——这是个会持续放大的风险,不是一次性的疏漏。
  96 +- **✅ 已修复(2026-08-08)`graphlemon_architecture.md` §10.1"方案 A"从未同步实际背离**:该节"已确认:Work
  97 + Graph 执行层复用 Claude Code 原生 Dynamic Workflow"从 2026-07-29 写下后就没再改过,但图编排层
  98 + (`work_graph_orchestrate.py`)早在 U-031(2026-08-01)就已经自研,正文完全没提这个背离——直到用户 2026-08-08
  99 + 追问"这个描述改了吗"才发现。**修复**:新增 §10.2 决策修订,记录图编排层的既有背离 + 单节点执行层这次也要
  100 + 跟进去黑盒化(见上方第 3 节两条新 gap)。教训:架构文档的"已确认"决策段落不会随实现自动更新,需要真实实现
  101 + 背离时主动回来补一笔,不能假设"写过一次就不用管了"。
84 102  
85 103 ## 5. 验证方法上的缺口
86 104  
... ...
docs/todo/org-graph-as-work-graph-unified-loop-requirements.md deleted
1   -# Org Graph 本身也变成一个 Work Graph(Planner/Coder 之间加判断+精确回路)——需求文档
2   -
3   -**日期**: 2026-08-08
4   -**性质**: 需求文档草稿(BRD/PRD 层面),尚未逐条讨论确认——跟 `docs/done/work-graph-self-planning-and-loop-back-requirements.md` 不同,那份是走完"逐条开放问题讨论→用户拍板"流程后才写的,这份是用户提出想法后直接要求"先记到 todo 里",还没有走那个流程。「6. 待确认的问题」列的都是需要讨论、不是已经拍板的。
5   -**触发场景(真实复现)**:任务 `2084590a`(修复 `work_node_run.py` 的 `execution_log_file` bug)里,Planner 自规划出的 `benchmark` 节点连续 4 次产出的都是"叙述我做了什么"的散文(跟 `docs/todo/validator-test-script-narrative-output-issue.md` 同一类问题,但这次发生在 benchmark 上,不是 test_script),LLM judge 的 `on_fail=retry_self` 循环耗尽 `max_attempts=3` 后任务直接 `blocked_pending_human`。用户追问引出两个观察:① Validator 从来没有机会在 Coder 开始实现之前复核 Planner 自己的产出;② 现有的 `org_level_loop.py`/`org_loop_route.py` 只在 Coder 完整实现之后才起作用,而且只能粗粒度地把整个任务打回"Planner 阶段"或"Coder 阶段"重来,不能精确跳到某个具体 Work Graph 节点。
6   -**状态**: 草稿,待讨论。**2026-08-08 更新**:用户提出更完整的四点架构改动设想(新 Org 固定流程、全员自规划、前端动态/历史图谱、节点级验证 skill),本文档已被展开合并进 `docs/todo/org-graph-unification-and-universal-self-planning-requirements.md`——后续讨论请统一在那份文档进行,本文档保留作为触发场景/根因排查的原始记录,不再单独跟进。
7   -
8   -## 1. 背景:现在有两套独立的、粒度不同的循环机制
9   -
10   -- **`work-graph-self planning-and-loop-back`(今天刚做完)**:一个 Org Agent **自己的** Work Graph 内部,节点声明 `judgment`/`on_fail`,失败可以 `loop_back` 跳回同一张图里更早的节点,历史记录在 `execution_event`,上限可配置。只在**单个 Org Agent 的图内部**生效。
11   -- **`org_level_loop.py`/`org_loop_route.py`(已实现,更早)**:Validator 对 Coder 的最终实现给出 FAIL/INCONCLUSIVE 判决后,`p_validator_root_cause_trace` 判根因(planning/coder/skill_defect),路由到 `fast_lane`(回 Coder)或 `full_replan`(回 Planner),`task.loop_round` 封顶 3 轮。这是**跨 Org Agent 边界**的循环,但粒度是"整个阶段重来",不是"跳到具体哪个节点";而且触发条件是"Coder 已经完整实现完,Validator 也已经判过"——Planner 自己的产出(BRD/PRD/SDD/Benchmark)在被 Coder 使用之前,从来没有任何独立角色复核过。
12   -
13   -这次真实复现暴露的具体问题:Planner 自己的 Work Graph 内部循环耗尽后,直接 `blocked_pending_human`,整个任务卡死——既没有像 `org_level_loop` 那样有根因判断和路由,也没有人能在 Coder 动手之前发现"这个 Benchmark 其实是叙述文字,根本不能用"。
14   -
15   -## 2. 用户提出的方向
16   -
17   -把 Planner → Coder → Validator 这条 Org Graph 本身,也按照今天 Work Graph 那套"节点 + judgment + on_fail + loop_back"机制来跑:
18   -
19   -1. Planner 完成后,先过一道 judgment(可以是 Validator,也可以是别的判断机制),不通过就 `loop_back` 回 Planner(或者回 Planner 自己 Work Graph 里更精确的某个节点,比如就回那个 benchmark 节点,而不是"重新触发整个 Planner")。
20   -2. 通过了才进入 Coder。
21   -3. Coder 完成后过 judgment(现有 Validator 逻辑),不通过时要先判断根因是 Planner 的问题还是 Coder 的问题,再精确 `loop_back` 到对应阶段的具体节点,而不是现在这种"笼统打回整个阶段"。
22   -
23   -本质上是把"Org Graph 本身"也建模成一张 Work Graph(Planner/Coder/Validator 是三个"节点",每个节点内部又是一张自己的、可能自规划的子图),对失败的处理机制上下两层统一。
24   -
25   -## 3. 目标(草案,待讨论后细化为正式 FR)
26   -
27   -1. Coder 开始实现之前,Planner 自己的产出(至少是 Benchmark,可能还有 SDD)要经过一次独立判断,能拦住这次真实复现的那种"叙述文字冒充 benchmark"的情况,不必等到 Coder 把错误的实现做完才被 Validator 发现。
28   -2. 失败时的路由要能精确到**具体是哪个节点出的问题**(比如"benchmark 节点内容不对"),而不是只知道"Planner 阶段"或"Coder 阶段"这种粗粒度。
29   -3. 跨 Org Agent 边界的循环(现有 `org_level_loop.py` 覆盖的场景)和单个 Org Agent 内部的循环(今天刚做的 Work Graph loop_back)用同一套机制表达,不是两套平行的、概念不同的循环实现。
30   -4. 循环上限、历史记录复用今天已经建立的模式(`settings_config.py` 风格的可配置上限、`execution_event` 记录历史),不重新发明。
31   -
32   -## 4. 一个明显更难的地方:跨 Org Agent 的"节点"地址空间不一样
33   -
34   -今天的 `loop_back` 是在**同一个 `work_graph_run`**(同一张图)内部跳转——`_on_failure_target`/`_transitive_dependents` 全部假设 `target_node_id` 和当前失败节点在同一个 `graph_manifest["nodes"]` 里。但 Planner 和 Coder 是**两次独立的 `plan_work_graph()` 调用**,各自产出各自的 `work_graph_run`/`graph_manifest`,节点 id 空间互不相通——"Coder 阶段发现问题,要跳回 Planner 阶段的 benchmark 节点",这个"benchmark 节点"活在另一个、可能早就标记为 `done` 的 `work_graph_run` 里,不是当前正在跑的这张图的一部分。今天的 `_LoopBackRequested`/`run_graph()` 主循环完全没有考虑"目标节点在另一张图里"这种情况。
35   -
36   -这不是一个小的实现细节——这是这次需求跟今天那次最大的架构差异,需要先想清楚:是把 Planner 和 Coder 的图**合并成同一张跨阶段的大图**(这样 `loop_back` 天然能跳),还是**保留现在"每个 Org Agent 各自一张图"的边界**、在图与图之间另设计一层专门的跨图路由(更接近现有 `org_loop_route.py` 的做法,只是把"回 Planner"精确到"回 Planner 图里的哪个节点")。
37   -
38   -## 5. 非目标(草案)
39   -
40   -1. 不在这份需求里改变 `task.status` 现有的 `queued/planning_brd_prd_sdd/coding/validating` 状态机本身的字面值——如果要做,这套状态机可能也需要重新设计("Planner 阶段内部又在 loop_back"这种状态目前完全没有对应的 task.status 值),但那是另一个需要谨慎讨论的问题,不在这次草稿里预先决定。
41   -2. 不假定"给 Coder 开始前加一道判断"用的一定是 Validator——可能是 Validator 的一个轻量子集,也可能是完全独立的判断机制,待讨论。
42   -
43   -## 6. 待确认的问题(这份文档的主要目的——先把问题摆出来)
44   -
45   -- [ ] **要不要完全取代 `org_level_loop.py`/`org_loop_route.py`,还是新机制只是给现有机制"打补丁"(比如只加"Coder 开始前先判断 Planner 产出"这一段,Coder 完成后的现有路由逻辑不动)?**
46   -- [ ] **§4 提到的跨图节点寻址问题:合并成一张大图,还是图与图之间单独设计一层路由?** 这个决定几乎决定了整个实现方式,建议第一个讨论。
47   -- [ ] **"Coder 开始前判断 Planner 产出"这一步谁来做?** 复用 Validator(但 Validator 现在的定位是"独立验收最终实现",拿它来判断"BRD/PRD/SDD/Benchmark 写得好不好"是不是错位?)、还是让 Planner 自己的 Work Graph 内部多一个"自我复核"节点(这样其实不需要跨 Org Agent,本质是今天 Part A 机制的延伸,不需要碰 Org Graph)?
48   -- [ ] **根因判断("这次失败该回 Planner 还是回 Coder")复用现有 `p_validator_root_cause_trace` 的三选一分类,还是需要更细粒度(精确到某个 Work Graph 节点)的判断机制?**
49   -- [ ] **`task.loop_round`/`_LOOP_LIMIT=3` 这个既有的、粗粒度的计数器要不要保留?** 如果循环变成能精确到节点级别,是不是也需要参考今天 `settings_config.py` 风格的按 org_node/节点类型可配置上限,而不是一个全局固定的 3?
50   -- [ ] **这次真实触发这个讨论的具体症状(benchmark 叙述文字),本身要不要单独先用更小的手段缓解**(比如给 benchmark 也加一个类似 `check_script_compiles` 的机械启发式检查,判断"这段内容像不像散文而不是结构化 benchmark"),**而不必等这个更大的 Org Graph 改造做完?** 这个可以先记到 `docs/todo/validator-test-script-narrative-output-issue.md`(已经在追踪同一类问题,只是要把范围从 test_script 扩到 benchmark)。
51   -
52   -## 7. 相关文档
53   -
54   -- `docs/done/work-graph-self-planning-and-loop-back-requirements.md`/`-design.md`:今天实现的 Work-Graph-内部判断+回路机制,这份需求本质是把它推广到 Org Graph 层面。
55   -- `docs/todo/validator-test-script-narrative-output-issue.md`:叙述文字问题的原始记录(目前只覆盖 test_script),这次真实复现证明同一个根因也会发生在 benchmark 上,需要更新范围。
docs/todo/org-graph-unification-and-universal-self-planning-requirements.md
1 1 # 统一 Org Graph 主流程 + 全体 Org Agent 自规划 Work Graph + 节点级验证回路 + 前端动态/历史图谱——需求文档(融合版)
2 2  
3 3 **日期**: 2026-08-08
4   -**性质**: 需求文档(BRD/PRD 层面),不含详细技术设计。融合并统领以下三份既有 `docs/todo/` 文档的相关内容,讨论请统一在本文档继续,原文档保留作为触发场景/根因排查的原始记录,不删除:
5   -- `docs/todo/org-graph-as-work-graph-unified-loop-requirements.md`——本文档是它的展开与拍板;该文档提出的"跨 Org Agent 精确 loop_back"诉求是本文档 FR-4 + §4 关键架构决策的正文。
  4 +**性质**: 需求文档(BRD/PRD 层面),不含详细技术设计。融合并统领以下既有 `docs/todo/` 文档的相关内容:
  5 +- `docs/todo/org-graph-as-work-graph-unified-loop-requirements.md`(已删除,内容 100% 并入本文档)——本文档是它的展开与拍板;该文档提出的"跨 Org Agent 精确 loop_back"诉求是本文档 FR-4 + §4 关键架构决策的正文。
6 6 - `docs/todo/task-execution-resume-assumes-planner-artifacts-exist.md`——它记录的 bug 与本文档 FR-1 改动的是**同一段** `run_or_resume_task_lifecycle()` 分支逻辑(见 §2.1 调研结论),折入 Phase 1 一并修,不再单独排期。
7 7 - `docs/todo/validator-test-script-narrative-output-issue.md` 第 8 节提到的"另一份尚未撰写、关于给 Coder agent 加入 test_script 模块的文档",即本文档 Phase 2(FR-2/FR-3)。
8 8  
9   -**触发场景**:用户 2026-08-08 提出四点架构改动设想(见 §1),与当天早些时候整理的 `docs/todo/` 待办优先级建议(P0~P3)高度交叉,要求重新盘点、融合、给出 phase 规划。
  9 +**触发场景**:任务 `2084590a`(修复 `work_node_run.py` 的 `execution_log_file` bug)里,Planner 自规划出的 `benchmark` 节点连续 4 次产出的都是"叙述我做了什么"的散文(跟 `validator-test-script-narrative-output-issue.md` 同一类问题,这次发生在 benchmark 上),LLM judge 的 `on_fail=retry_self` 循环耗尽 `max_attempts=3` 后任务直接 `blocked_pending_human`——暴露出 Validator 从来没有机会在 Coder 开始实现之前复核 Planner 自己的产出。用户随后 2026-08-08 提出四点架构改动设想(见 §1),与当天早些时候整理的 `docs/todo/` 待办优先级建议(P0~P3)高度交叉,要求重新盘点、融合、给出 phase 规划。
10 10  
11 11 **状态**: 草稿,讨论中——§4 关键架构决策已拍板(方案 A,合图,2026-08-08),其余 §7 列出的问题仍待确认。沿用 `org-graph-as-work-graph-unified-loop-requirements.md` 的"逐条开放问题讨论 → 用户拍板 → 展开设计文档"两段式流程,本文档本身不是最终设计。
12 12  
... ... @@ -86,7 +86,7 @@
86 86  
87 87 ## 5. 非目标
88 88  
89   -1. 不动 `scheduler-production-polling-loop-requirements.md` 的 FR-1(新 Goal 自动派发)——该文档已明确这是独立的自主权边界决策,与本次 Org Graph 主流程改动正交,不合并排期。
  89 +1. 不动 `scheduler-new-goal-dispatch-requirements.md`(FR-1,新 Goal 自动派发)——该文档已明确这是独立的自主权边界决策,与本次 Org Graph 主流程改动正交,不合并排期。
90 90 2. 不要求第一版就把 `validator-test-script-narrative-output-issue.md` 的根因彻底修完——FR-2/FR-3 落地后 Coder 也会产出 test_script 类内容节点,叙述文字复发风险依然存在(该文档已记录用户接受此已知风险);本文档只是把"给 Coder 加 test_script 模块"接进 FR-2/FR-3 范围,Phase 2 落地时需要专门验证这条风险有没有复现,不单独立项新对策。
91 91 3. 不重写 `work_graph_orchestrate.py` 的 `judgment`/`on_fail`/`loop_back` 引擎本身——它已经是通用的,本次只新增调用方(FR-2)和一个 skill(FR-4),不改引擎代码。
92 92 4. 不重写前端已有的 SVG DAG 渲染器——FR-5 是在现有 `renderGraphVisualizerPanel`/`mountGraphVisualizerEdges` 基础上新增元图和回放时间轴,不是推倒重来。
... ... @@ -119,7 +119,7 @@
119 119 - **依赖说明**:FR-5b 只依赖 `loop_back_events` 数据形状,今天已存在,不需要等 Phase 2 完成;FR-5a 的"复核"这一段需要 Phase 1 新增的 `task.status` 值才有东西可渲染,其余三段可以先做。
120 120  
121 121 ### 明确不排期
122   -`scheduler-production-polling-loop-requirements.md` 的 FR-1(新 Goal 自动派发)——保持独立评审,不合并进本文档的任何 phase。
  122 +`scheduler-new-goal-dispatch-requirements.md`(FR-1,新 Goal 自动派发)——保持独立评审,不合并进本文档的任何 phase。
123 123  
124 124 ---
125 125  
... ... @@ -128,7 +128,7 @@
128 128 ✅ §4 关键架构决策:方案 A(合图)。2026-08-08 用户拍板。
129 129 ✅ FR-1 新增的 `task.status` 枚举值定为 `reviewing_planner_output`。2026-08-08,随 Phase 1 SDD(`docs/todo/org-graph-unification-phase1-review-gate-design.md`)一起拍板,理由见该文档 §3.1。
130 130 ✅ FR-1 的"复核 Validator"用新增独立 org role `planner_reviewer`,不复用 `validator`。2026-08-08 用户拍板(`AskUserQuestion` 确认),理由见 Phase 1 SDD §2.1——`_PRE_STATE`/`_POST_STATE` 是按 org_node 名字一对一映射,同一个 key 没法挂两组状态转移。
131   -🔲 FR-4 的节点级验证 skill 是在 `qa-task-validation` 基础上改造,还是新开一个 skill。
132   -🔲 Phase 2 的 Coder/Validator 自规划,要不要像 Planner 一样有一份"允许的节点类型清单"约束,还是完全自由(对应 `_PROMPT_TEMPLATE` vs `_GENERIC_PROMPT_TEMPLATE` 的既有分野)。
133   -🔲 §4.1 第 1 点:`org_node` 下沉到节点级别后,`work_graph_run.org_node`(run 级)这一列要不要保留(比如作为"发起该 run 的原始 org_node"这种冗余但方便查询的字段),还是彻底移除、查询时改成从节点聚合。
134   -🔲 §4.1 第 2 点:"增量追加规划"的具体接口形态——是 `plan_work_graph()` 新增一个"追加模式"参数直接 `UPDATE` 已有 `run_id` 的 `graph_manifest`,还是由调用方(`e2e_pipeline.py`)读出旧 `graph_manifest`、拼接新节点、再整体写回。
  131 +✅ FR-4 的节点级验证 skill:新开一个 skill,不在 `qa-task-validation` 基础上改造。2026-08-08 用户拍板。理由:`qa-task-validation` 整份内容绑定飞书多维表格语义(硬编码 `tblXXX` 表 ID、状态机是"待测试→已完成"这类 Base 记录写回),判定粒度是整个任务的 PASS/FAIL/INCONCLUSIVE;FR-4 要的是单个 Work Graph 节点的 `judgment.kind="llm"` 回调,输入是一个节点的输出,输出要精确指向 `loop_back` 的目标 `node_id`,和 Base 完全无关,两种用途混进同一份 skill 容易互相污染。新 skill 可以借鉴其判定原则(三态结论、独立复核、真实路径优先)写进 prose,但不复用文件本身。
  132 +✅ Phase 2 的 Coder/Validator 自规划:加一份建议清单(类似 `_PLANNER_NODE_CATALOG`),不是完全自由。2026-08-08 用户拍板——推翻了 `work_graph_plan.py:161-170` 2026-08-06 注释里"Coder/Validator's sub-tasks have no such fixed small vocabulary of node types"的原判断。Phase 2 设计阶段需要给 Coder/Validator 各自定义一份"常见节点类型"参考目录(如 impl/unit_test/integration_check),每项可以像 `_PLANNER_NODE_CATALOG` 一样标注 `suggested_judgment`,但不强制——沿用 Planner 目录"建议不强制"的既有约定,而不是 Planner 文档链那种固定顺序的强约束。
  133 +✅ §4.1 第 1 点:`work_graph_run.org_node`(run 级单值列)彻底移除,查询改为从节点级聚合。2026-08-08 用户拍板——推翻了"保留作冗余字段"的备选项。Phase 2 设计阶段需要解决两处下游改造:`work_graph_run_query.py:62` 的 `WHERE wgr.org_node = ?` 过滤要重写成 JOIN 节点表再过滤;`work_graph_fork.py` 的 fork 谱系展示要重新定义"一个横跨四段的合图 run 展示哪个/哪些 org_node"这个尚未存在的聚合语义(例如展示节点级 org_node 的去重列表,而不是单值)——这是本次决策遗留的新开放问题,不是照抄旧列就能解决的,需要在 Phase 2 SDD 里明确定义。
  134 +✅ §4.1 第 2 点:"增量追加规划"接口:`plan_work_graph()` 新增一个"追加模式"参数(如 `append_to_run_id`),内部完成"读现有 `graph_manifest` → 合并新规划出的节点/边(含 `node_id` 冲突检测) → `UPDATE` 写回",不下放给调用方各自实现。2026-08-08 用户拍板。理由:合并/冲突检测逻辑集中在 `work_graph_plan.py` 一处——该模块本来就是 `_normalize_graph_manifest`/`_validate_loop_back_targets` 等 `graph_manifest` 结构性操作的归属地——避免 Coder、Validator 两个调用点各自实现一遍合并逻辑,未来出现两处不一致的风险。
... ...
docs/todo/scheduler-new-goal-dispatch-requirements.md 0 → 100644
  1 +# 新 Goal 自动派发——需求文档
  2 +
  3 +**日期**: 2026-08-06
  4 +**性质**: 需求文档(BRD/PRD 层面),不含详细技术设计。**2026-08-08 拆分**:原文档
  5 +`docs/todo/scheduler-production-polling-loop-requirements.md`(FR-1/FR-2 合并)已按完成状态拆分——
  6 +FR-2(在途任务复原)已实现,移入 `docs/done/scheduler-task-reconciliation-requirements.md`/
  7 +`-design.md`;本文档只保留仍是草案的 FR-1。
  8 +**状态**: 草稿,未立项。`docs/implementation-gap-analysis.md` 第 6 条已把这条标注为"改变系统自己的
  9 +自主权边界",需要单独评审决策,不与其他 gap 混跑。
  10 +
  11 +## 1. 背景
  12 +
  13 +`run_scheduler_once(db_path, goals, max_workers=None)` 的入参 `goals` 是 `[(target_folder_path,
  14 +goal_text), ...]`,语义是"这一轮要**新建**并跑完这些 Goal",内部通过 `task_create.create_task()`
  15 +每次都新建一个 `task` 行。这个函数存在且经过测试,但从未被任何常驻进程接入——`queued` 状态的新
  16 +Goal(还没被任何调用触发过 `run_goal_to_completion`)目前只能靠人工手动点击控制台的 Start 按钮
  17 +执行,不会被自动拿去跑。
  18 +
  19 +跟"在途任务复原"(`docs/done/scheduler-task-reconciliation-requirements.md`,已实现)是两个独立
  20 +问题:那个是"已经开始执行的任务卡住了怎么自动接回去",这个是"还没开始执行的新 Goal 怎么自动开始"。
  21 +
  22 +## 2. 目标
  23 +
  24 +`queued` 状态的任务(或者说,还没被任何调用触发过 `run_goal_to_completion`/
  25 +`run_or_resume_task_lifecycle` 的新 Goal)需要被自动拿去执行,不需要人工每次手动点 Start。
  26 +
  27 +## 3. 非目标
  28 +
  29 +1. **不在本文档里裁定"要不要做""什么时候做"这个更大的问题**。是否现在就投入、要不要额外的
  30 + 人工评审门槛,是需要单独确认的决策,本文档只负责把需求本身讲清楚,供决策时参考。
  31 +2. **不要求解决方案是常驻进程/独立进程/`backend_server.py` 内置线程三选一中的哪一个**。具体载体
  32 + 形式留给设计阶段。
  33 +3. **不重新设计 `run_scheduler_once` 本身**。它的行为已经是对的,本需求要解决的是"谁、在什么
  34 + 时机去调用它"这一层。
  35 +
  36 +## 4. 功能需求
  37 +
  38 +| # | 需求描述 | 优先级 |
  39 +|---|---|---|
  40 +| FR-1 | 需要一个机制,定期(具体周期留待设计阶段)调用 `scheduler.run_scheduler_once()` 派发新 Goal。 | 必须 |
  41 +
  42 +## 5. 验收标准
  43 +
  44 +新建一个 `queued` 状态的 Goal,不手动点 Start,验证机制能在设定周期内自动派发执行。
  45 +
  46 +## 6. 风险
  47 +
  48 +**这是改变系统自主权边界的决策**——意味着系统会在没有人显式点击"Start"的情况下自主开始消耗
  49 +真实 API 调用、真实修改代码。这条风险不因为实现简单而减轻,需要在决策阶段被认真对待。
  50 +
  51 +## 7. 开放问题(留给详细设计阶段回答,需要先拍板"要不要做"再讨论这些)
  52 +
  53 +- [ ] 轮询周期定多少。
  54 +- [ ] 载体形式:`backend_server.py` 内置后台线程,还是独立进程。
  55 +- [ ] 要不要暴露成可配置项(管理后台开关/环境变量),还是先硬编码一个保守的默认行为。
  56 +- [ ] 要不要跟已实现的在途任务复原循环(`docs/done/scheduler-task-reconciliation-design.md`)
  57 + 共用同一个后台线程/轮询周期,还是保持两个独立循环——两者已实现的那部分刻意没有合并
  58 + 成一个"万能调度循环",这个原则是否延续到 FR-1 需要在设计阶段确认。
... ...
docs/todo/validator-test-script-narrative-output-issue.md
1 1 # test_script 节点偶发输出叙述文字而非代码——问题记录(Planner + Validator 都会中招)
2 2  
3   -**日期**: 2026-08-07
4   -**性质**: 问题记录 + 待调查,不是已确认根因的需求文档(区别于同目录下其他几份——那几份是已经定位到具体代码 bug 并修复过的,这份目前还只是"观察到的现象",根因未确定)。
5   -**状态**: 根因已定位(见第 6 节)。用户决策(2026-08-07):不再继续在提示词/重试反馈上打补丁,**先直接禁用 test_script/validator_test_script 节点**(Planner 的 `work_graph_plan._PROMPT_TEMPLATE` 不再要求这个节点,Validator 的 `e2e_pipeline._VALIDATOR_MANIFEST` 从 3 节点缩成 2 节点),后续如果真的需要再重新设计(见第 8 节的结构性修复方向:把"探索"和"最终作答"拆成两次独立调用)。`_NARRATION_RETRY_HINT`(第 7 节)代码仍保留在 `_test_script_verify_fn` 里,只是当前这条路径不会被触发——如果以后重新启用这个节点,这个缓解依然生效。**2026-08-08 更新(第四轮,与自规划 Work Graph 需求的交叉决策)**:`docs/done/work-graph-self-planning-and-loop-back-requirements.md`(Planner 自规划节点集合 + 失败回路需求)上线后,test_script 会重新作为可选节点开放给 Planner 自由规划——用户明确决定**不等本文档的结构性修复**,narrative-output 问题可能因此重现,这是已知且被接受的风险。本文档记录的根因(第 6 节)的真正结构性修复,安排在**另一份尚未撰写、关于"给 Coder agent 加入 test_script 模块"的文档**里一起解决,不是本文档单独继续跟进的范围。**2026-08-08 更新(第六轮)**:那份"尚未撰写的文档"现在是 `docs/todo/org-graph-unification-and-universal-self-planning-requirements.md` 的 Phase 2(FR-2/FR-3:Coder 接入自规划)——该文档非目标 §5.2 明确记录了这条已知风险,要求 Phase 2 落地时专门验证是否复现,不是事后打补丁。**2026-08-07 更新(第二轮):不只是 Validator 的问题——`911861fe` 这个纯后端任务在 Planner 自己的 `n_test_script` 节点上也复现了同一模式。累计 3 个任务、6 次到达 test_script 类节点,6 次全部复现(100%),横跨 Planner 和 Validator 两种场景。建议提高优先级,不要再靠重试撞运气。** **2026-08-07 更新(第三轮,根因排查):这是 08-06 "Benchmark 叙述事故"(commit `07a0a27`)的同类复发——test_script 类节点当时已经同时拿到提示词纠偏和机械拦截网双重防护,仍然 6/6 复现。根因不是"self-hosting 词汇污染"这个泛泛的猜测,而是 test_script 提示词本身要求"先做工具探索再产出内容",模型探索后的收尾话术天然是"汇报我做了/启动了什么"而不是切换成纯文本吐出模式;self-hosting 只是放大器,让叙述读起来更精确(因为探索对象就是 graphlemon 自己,词汇表精确对应),不是必要条件。BRD/PRD/SDD/Benchmark/test_dataset 这几个不需要先探索的纯组合型节点,自 08-06 后再未复现,与此吻合。**
  3 +**日期**: 2026-08-07(最后更新 2026-08-08)
  4 +**性质**: 问题记录——源于真实生产复现,根因已定位(§6),已落地两轮缓解(§7/§8),结构性修复推迟到另一份文档(见时间线第 6 轮)。
  5 +**状态**: 根因已定位。当前生产状态:test_script/validator_test_script 节点已禁用(§8),`_NARRATION_RETRY_HINT` 缓解代码保留但当前不会触发(§7)。**结构性修复不在本文档范围**——已并入 `docs/todo/org-graph-unification-and-universal-self-planning-requirements.md` Phase 2(FR-2/FR-3,Coder 接入自规划时一并处理,该文档非目标 §5.2 记录了这条已知复发风险)。
6 6  
7   -**2026-08-08 更新(第五轮,真实复现,推翻了第三轮"Benchmark 不需要先探索、所以不会复现"这个判断)**:任务 `2084590a`(Work Graph 自规划 + 失败回路上线后的第一次真实端到端验证)里,Planner 自规划出的 `benchmark` 节点连续 4 次真实产出全部是叙述文字,例如:"The planner Work Node finished. It produced a Benchmark artifact covering a real bug in `src/work_node_run.py::run_work_node()`... **No further action taken unless the user wants this handed off to the Coder**",以及 "**No files were changed in this checkout (git status clean) — this run only generated the benchmark text; it did not apply the fix or commit anything.**"——都是"我做了什么"的散文,不是真实的 benchmark 场景+断言内容。这次是自规划场景下 `judgment.kind="llm"` 的 LLM judge(不是机械检查)在把关,4 次里第 3 次的 judge 反馈甚至精确诊断出了这一点:"the text only asserts scenarios exist, without stating them... No benchmark.md matching this content was found on disk anywhere in the repo... recommend surfacing the actual artifact file/text... rather than a prose summary of it"——但这个诊断只是混在其他反馈条目里的一条,没有变成能强制拦住的机制,第 4 次原样复现,最终 `loop_back` 耗尽 `max_attempts=3`,任务 `blocked_pending_human`。
  7 +**更新时间线**(压缩自原先六轮分散更新,2026-08-08):
8 8  
9   -这推翻了第三轮"BRD/PRD/SDD/Benchmark/test_dataset 不需要先探索,所以不会复现"的判断——这次的 goal 是一个真实 bug 修复(不是新功能),SDD 节点的 judge_prompt 里精确引用了 `work_node_run.py` 第 272/274-278/420-424 行的真实代码事实,说明模型确实主动去读了真实代码来支撑 SDD/Benchmark 的内容——**触发条件看起来不是"节点类型是不是 test_script",而是"这次任务本身需不需要模型先探索真实代码才能写出扎实的内容"**,跟 test_script 一样都会诱导"先探索、再收尾汇报"的模式,只是 benchmark 类型的节点被诱导的条件更宽(只要目标是修复已有真实代码,不需要是"要不要写可执行脚本"这么窄的条件)。另外一个新发现:**benchmark 没有像 test_script 的 `check_script_compiles` 那样的机械拦截网**,只能靠 LLM judge 自己识别"这是散文还是真 benchmark"——judge 有能力识别(第 3 次证明了),但识别到不等于能强制拦住,下一轮又原样复现。
  9 +| 轮次 | 日期 | 要点 |
  10 +|---|---|---|
  11 +| 1 | 08-07 | 首次记录:Validator `validator_test_script` 节点连续两次真实生产运行输出叙述文字而非代码,`check_script_compiles()` 正确拦截,重试仍失败(详见 §1)。 |
  12 +| 2 | 08-07 | `911861fe` 任务证明 Planner 侧 `test_script` 节点同样复现——不是 Validator 专属。累计 3 个任务、6 次到达 test_script 类节点,6/6 复现。 |
  13 +| 3 | 08-07 | 根因排查(§6):是 08-06"Benchmark 叙述事故"(commit `07a0a27`)同类复发。根因不是 self-hosting 词汇污染,而是"先工具探索、再产出内容"这类节点的模型收尾话术天然是"汇报做了什么";self-hosting 只是让叙述读起来更精确的放大器,不是必要条件。 |
  14 +| 4 | 08-08 | 用户决策:`work-graph-self-planning-and-loop-back` 上线后 test_script 重新开放给 Planner 自由规划,**不等结构性修复**——复发风险已知且被接受。 |
  15 +| 5 | 08-08 | **真实复现,推翻第 3 轮判断**:任务 `2084590a` 的 `benchmark` 节点(不是 test_script)连续 4 次输出叙述文字(详见 §1 末尾),LLM judge 第 3 次已精确诊断出问题但未能强制拦住,第 4 次原样复现,`loop_back` 耗尽后 `blocked_pending_human`。触发条件改判为"这次任务需不需要模型先探索真实代码",不是"节点类型是不是 test_script"——benchmark 也没有类似 `check_script_compiles` 的机械拦截网,只能靠 LLM judge 自己识别,识别到不等于能强制拦住。 |
  16 +| 6 | 08-08 | 结构性修复的落脚点确定为 `org-graph-unification-and-universal-self-planning-requirements.md` Phase 2——该文档非目标 §5.2 已记录这条已知风险,要求 Phase 2 落地时专门验证是否复现。 |
10 17  
11   -后续讨论方向记在 `docs/todo/org-graph-as-work-graph-unified-loop-requirements.md`(草案,待讨论)——包括要不要给 benchmark 也加一个类似的启发式机械检查、以及要不要在 Coder 开始实现之前对 Planner 产出多一道独立复核。
  18 +后续讨论方向记在 `docs/todo/org-graph-unification-and-universal-self-planning-requirements.md`(原草案 `org-graph-as-work-graph-unified-loop-requirements.md` 已并入此文档并删除)——包括要不要给 benchmark 也加一个类似的启发式机械检查、以及要不要在 Coder 开始实现之前对 Planner 产出多一道独立复核(后者已在该文档 Phase 1 SDD `docs/todo/org-graph-unification-phase1-review-gate-design.md` 落地为具体设计)。
12 19  
13 20 ## 1. 现象
14 21  
... ... @@ -28,6 +35,8 @@
28 35  
29 36 第二个例子里引用的函数名(`parseHashQuery`/`computeNavStatusBucketCounts`/`renderNavProjectContextHtml` 等)跟 `bfd3ee82` 这个任务本身(导航栏"当前项目"上下文功能)高度相关、具体、看起来不是瞎编的——说明模型大概率是真的理解了任务、构思了一个具体可行的测试方案,只是最后**把"描述我打算/已经做了什么"当成了输出内容本身**,而不是把测试代码原文放进 `content` 字段。
30 37  
  38 +- **任务 `2084590a`(2026-08-08,时间线第 5 轮,推翻"BRD/PRD/SDD/Benchmark 不会复现"的判断)**:Work Graph 自规划 + 失败回路上线后的第一次真实端到端验证里,Planner 自规划出的 `benchmark` 节点(不是 test_script)连续 4 次真实产出全部是叙述文字,例如:`"The planner Work Node finished. It produced a Benchmark artifact covering a real bug in src/work_node_run.py::run_work_node()... No further action taken unless the user wants this handed off to the Coder"`,以及 `"No files were changed in this checkout (git status clean) — this run only generated the benchmark text; it did not apply the fix or commit anything."`——都是"我做了什么"的散文,不是真实的 benchmark 场景+断言内容。这次是自规划场景下 `judgment.kind="llm"` 的 LLM judge(不是机械检查)在把关,4 次里第 3 次的 judge 反馈甚至精确诊断出了这一点:"the text only asserts scenarios exist, without stating them... No benchmark.md matching this content was found on disk anywhere in the repo... recommend surfacing the actual artifact file/text... rather than a prose summary of it"——但这个诊断只是混在其他反馈条目里的一条,没有变成能强制拦住的机制,第 4 次原样复现,最终 `loop_back` 耗尽 `max_attempts=3`,任务 `blocked_pending_human`。这次的 goal 是一个真实 bug 修复(不是新功能),SDD 节点的 judge_prompt 里精确引用了 `work_node_run.py` 第 272/274-278/420-424 行的真实代码事实,说明模型确实主动去读了真实代码来支撑 SDD/Benchmark 的内容——**触发条件不是"节点类型是不是 test_script",而是"这次任务本身需不需要模型先探索真实代码才能写出扎实的内容"**,跟 test_script 一样都会诱导"先探索、再收尾汇报"的模式,只是 benchmark 类型的节点被诱导的条件更宽(只要目标是修复已有真实代码,不需要是"要不要写可执行脚本"这么窄的条件)。另一个新发现:**benchmark 没有像 test_script 的 `check_script_compiles` 那样的机械拦截网**,只能靠 LLM judge 自己识别"这是散文还是真 benchmark"——judge 有能力识别(第 3 次证明了),但识别到不等于能强制拦住,下一轮又原样复现。
  39 +
31 40 ## 2. 和已修复的三个 bug 的区别(避免误认为同一件事)
32 41  
33 42 - 不是 `join()` 逐字符拆解那个 bug(那个已修复,产出的是"c; o; n; t; e; n; t"这种逐字符加分隔符的乱码,肉眼一看就是格式损坏;这次是完整、通顺、有实际内容的一段英文叙述,不是格式损坏)。
... ...
src/task_execution.py
... ... @@ -113,9 +113,9 @@ def run_or_resume_task_lifecycle(conn, task_id, target_folder_path, goal_text):
113 113 hours with zero `execution_event` rows and no `validator`-org_node
114 114 `work_graph_run` ever created, because nothing in production actually
115 115 polls `scheduler.run_scheduler_once()` (a separate, larger, tracked gap
116   - -- see `docs/todo/scheduler-production-polling-loop-requirements.md`)
117   - and whatever one-off call had driven this task that far had already
118   - exited by the time Coder finished."""
  116 + -- see `docs/done/scheduler-task-reconciliation-requirements.md`, the
  117 + fix that incident drove) and whatever one-off call had driven this task
  118 + that far had already exited by the time Coder finished."""
119 119 status = conn.execute("SELECT status FROM task WHERE task_id = ?", (task_id,)).fetchone()[0]
120 120 if status not in _RESUMABLE_STATUSES:
121 121 raise TaskExecutionError("task {} is status={}, not resumable from here".format(task_id, status))
... ...
src/task_reconciliation.py
... ... @@ -4,10 +4,11 @@ incident: a task sat in status=&#39;validating&#39; for 15+ hours with zero
4 4 whatever one-off call had driven it that far had already exited and
5 5 nothing in production polls for orphaned resumable tasks).
6 6  
7   -Scope: FR-2 only from `docs/todo/scheduler-production-polling-loop-
8   -requirements.md` (in-flight task reconciliation) -- deliberately NOT FR-1
9   -(new-goal auto-dispatch via `scheduler.run_scheduler_once()`), which is a
10   -separate, larger, deliberately-not-bundled decision (see that doc's
  7 +Scope: `docs/done/scheduler-task-reconciliation-requirements.md` (in-flight
  8 +task reconciliation) -- deliberately NOT new-goal auto-dispatch via
  9 +`scheduler.run_scheduler_once()`, which is a separate, larger,
  10 +deliberately-not-bundled decision tracked in
  11 +`docs/todo/scheduler-new-goal-dispatch-requirements.md` (see that doc's
11 12 non-goals/risks: "改变系统自主权边界"). This module only ever resumes a
12 13 task that already exists and was already in progress; it never creates a
13 14 new task or starts work nobody asked for.
... ...