From 0f8230e8b929d576fb1c8765dfe33244832a58aa Mon Sep 17 00:00:00 2001 From: bboonnee Date: Wed, 12 Aug 2026 18:55:09 +0800 Subject: [PATCH] docs: Phase 1 landed (ea19c7d) - move design docs to done/, reconcile Phase 2 SDD --- agents/planner_reviewer/AGENTS.md | 2 +- agents/planner_reviewer/SOUL.md | 2 +- docs/done/org-graph-unification-phase1-review-gate-design.md | 467 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/done/task-execution-resume-assumes-planner-artifacts-exist.md | 37 +++++++++++++++++++++++++++++++++++++ docs/todo/org-graph-unification-and-universal-self-planning-requirements.md | 8 ++++---- docs/todo/org-graph-unification-phase1-review-gate-design.md | 467 ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- docs/todo/org-graph-unification-phase2-universal-self-planning-design.md | 12 ++++++------ docs/todo/task-execution-resume-assumes-planner-artifacts-exist.md | 37 ------------------------------------- docs/todo/validator-test-script-narrative-output-issue.md | 2 +- docs/todo/work-graph-node-execution-deblackbox-requirements.md | 2 +- src/schema.py | 4 ++-- 11 files changed, 520 insertions(+), 520 deletions(-) create mode 100644 docs/done/org-graph-unification-phase1-review-gate-design.md create mode 100644 docs/done/task-execution-resume-assumes-planner-artifacts-exist.md delete mode 100644 docs/todo/org-graph-unification-phase1-review-gate-design.md delete mode 100644 docs/todo/task-execution-resume-assumes-planner-artifacts-exist.md diff --git a/agents/planner_reviewer/AGENTS.md b/agents/planner_reviewer/AGENTS.md index 9edcc22..42d482f 100644 --- a/agents/planner_reviewer/AGENTS.md +++ b/agents/planner_reviewer/AGENTS.md @@ -1,5 +1,5 @@ diff --git a/agents/planner_reviewer/SOUL.md b/agents/planner_reviewer/SOUL.md index da8e8e6..1b730e0 100644 --- a/agents/planner_reviewer/SOUL.md +++ b/agents/planner_reviewer/SOUL.md @@ -1,5 +1,5 @@ planning_brd_prd_sdd + --[plan_documents() 真正规划]--> + --[trigger_org_node_full(planner_reviewer)]--> reviewing_planner_output + --[_review_planner_output_with_retries() 真正复核,内部最多重试 3 轮]--> + 通过 --> + --[trigger_org_node_full(coder)]--> coding + --[implement_code() 真正实现]--> + --[trigger_org_node_full(validator)]--> validating + --[validate_implementation_with_own_graph() 真正验收]--> + PASS --> done (close_task_done,不变) + FAIL/INCONCLUSIVE --> org_level_loop.run_fix_loop(不变,Phase 1 不碰这段) + 复核耗尽重试 --> blocked_pending_human(reason='planner_review_loop_limit_reached',新) +``` + +--- + +## 3. Schema 变更 + +### 3.1 `task.status` 新增枚举值 `reviewing_planner_output` + +命名理由:`_PRE_STATE`/`_POST_STATE` 现有值(`planning_brd_prd_sdd`/`coding`/`validating`)全部是"上一阶段刚握手完成"的记账态,不是字面意义的"正在进行中"(见 §2.2)。`validating` 已经是这种"其实已经握手完成、真正工作在这个状态名下面跑"的先例,`reviewing_planner_output` 沿用同一个动名词命名习惯,语义一致。 + +`schema.py:106-111` 的 CHECK 约束扩展为: + +```sql +status TEXT NOT NULL CHECK (status IN ( + 'queued', 'planning_brd_prd_sdd', 'reviewing_planner_output', 'planning_failed', + 'coding', 'coding_failed', 'validating', + 'routed_fast_lane', 'routed_full_replan', + 'blocked_pending_human', 'done' +)), +``` + +新增迁移函数 `migrate_add_reviewing_planner_output_status(conn)`,照抄 `migrate_expand_artifact_types`/`migrate_add_loop_back_event_type` 的 create-copy-drop-rename 惯用法(SQLite 无法 `ALTER` CHECK 约束):读 `sqlite_master` 里 `task` 表的建表 SQL,如果已经包含 `'reviewing_planner_output'` 就直接返回(幂等);否则 `CREATE TABLE task_new (...)` 用新枚举、`INSERT INTO task_new SELECT * FROM task`、`DROP TABLE task`、`ALTER TABLE task_new RENAME TO task`,随后重建 `idx_task_status` 索引。`backend_server.py` 里紧邻 `migrate_add_loop_back_event_type(conn)`(第 1040 行)新增一行调用。 + +### 3.2 `execution_event.event_type` 新增枚举值 `planner_review_verdict` + +用于持久化每一轮复核的 PASS/FAIL/INCONCLUSIVE 结果(`payload: {"attempt": N, "verdict": "...", "used_existing_artifacts": true/false}`)——不是给控制台展示用的(那是 Phase 3 的事),是给"崩溃后 resume 时能不能信任当前这批 doc artifact 已经被复核过"这件事留下审计证据;Phase 1 本身**不**依赖读这张表来做 resume 判断(见 §4.3 的简化说明),只是照 `loop_back_triggered` 的先例把决策记下来,供人工排查和未来 Phase 2 复用。 + +`schema.py:264-268` 的 CHECK 约束新增一个值;新增迁移函数 `migrate_add_planner_review_verdict_event_type(conn)`,逻辑与 `migrate_add_loop_back_event_type` 完全同构(复制粘贴改枚举值)。`backend_server.py` 同一处再加一行调用。 + +--- + +## 4. `org_node_lifecycle.py` 变更 + +```python +_PRE_STATE = { + "planner": "queued", + "planner_reviewer": "planning_brd_prd_sdd", + "coder": "reviewing_planner_output", # 原来是 "planning_brd_prd_sdd" + "validator": "coding", +} +_POST_STATE = { + "planner": "planning_brd_prd_sdd", + "planner_reviewer": "reviewing_planner_output", + "coder": "coding", + "validator": "validating", +} +_REQUIRED_ARTIFACTS = { + "coder": ("sdd",), # 原来是 ("brd", "prd", "sdd", "benchmark") + "validator": ("code_diff", "test_report"), +} +``` + +### 4.1 为什么把 `_REQUIRED_ARTIFACTS["coder"]` 从 4 个类型收窄到只剩 `sdd` + +调研 `artifact_write.py:8-14` 发现 `validation_status` **在写入时就无条件设成 `'passed'`**(该文件自己的 docstring 承认这是一个简化:真正的语义内容校验"还没有专属的 LLM-judgment 调用")。也就是说 `_REQUIRED_ARTIFACTS["coder"]` 今天检查的"`validation_status='passed'`"永远为真——这个 gate 从来没有真正拦住过任何东西,是一个失效的桩。**新增的 `planner_reviewer` 阶段才是第一个真正做语义校验的 gate**,`_REQUIRED_ARTIFACTS["coder"]` 里 `brd`/`prd`/`benchmark` 这三个失效检查应该删掉,不是保留一个"看起来有校验、实际上从不生效"的假象。 + +保留 `sdd`:这不是校验质量,是**存在性**兜底——`implement_code()`(`e2e_pipeline.py:617`)的函数签名要求一个真实的 `sdd_content` 位置参数,`sdd` artifact 不存在会导致调用方在更早、更难定位的地方崩(`KeyError`),保留这一条纯粹是防止"复核通过了,但 sdd 其实是空的"这种不可能但值得防御的情况提前报出清晰错误,而不是让它一路传到 `implement_code()` 内部再崩。 + +### 4.2 `planner` 分支不变 + +`_check_concurrency(conn)` 仍然只在 `org_node == "planner"` 时触发(`org_node_lifecycle.py:110-111`)——并发上限的语义是"同时有几个 goal 在跑",跟这次改动无关,不变。 + +--- + +## 5. `e2e_pipeline.py` 变更 + +### 5.1 新增:叙述文字启发式(复核阶段的辅助信号,非硬拦截) + +```python +# ponytail: 子串匹配,从 docs/todo/validator-test-script-narrative-output-issue.md +# 记录的真实复现文案里提炼——"以为在描述自己做了什么"的稳定措辞模式。只作为喂给 +# planner_reviewer 的提示信号,不是硬性机械拦截(跟 check_script_compiles 不同: +# 这里没有客观的"编译通过/失败"可判断,误判成本是把合法的、恰好提到这些词的文档 +# 打回重来)。上限:这份短语表穷举不完,Phase 2 的结构性修复(把"探索"和"最终作答" +# 拆成两次独立调用,见 validator-test-script-narrative-output-issue.md §8)才是 +# 真正的上游修复,这里只是先兜住已经复现过的具体案例。 +_NARRATION_PHRASES = ( + "no further action", "launched the", "workflow finished", "workflow completed", + "it produced a", "you'll be notified", "no files were changed", +) + + +def looks_like_narration(content): + lowered = (content or "").lower() + return any(phrase in lowered for phrase in _NARRATION_PHRASES) +``` + +### 5.2 新增:复核 verdict prompt + 调用函数 + +照抄 `_VALIDATOR_VERDICT_PROMPT`/`validate_implementation_with_own_graph()` 结尾那段"拼 prompt → `org_node_trigger.trigger_org_node()` → `_parse_verdict_line()`"的既有模式(`_parse_verdict_line` 本身不用改,直接复用): + +```python +_PLANNER_REVIEW_PROMPT = """... goal_text={goal_text} ... +BRD: {brd_content} +PRD: {prd_content} +SDD: {sdd_content} +Benchmark: {benchmark_content} +{narration_hint} +... 以 "VERDICT: PASS|FAIL|INCONCLUSIVE" 收尾,说明理由 ... +""" + + +def review_planner_output( + conn, task_id, goal_text, target_folder_path, + sdd_content, brd_content="N/A: not provided", + prd_content="N/A: not provided", benchmark_content="N/A: not provided", + timeout=300, +): + """真正的复核调用(planner_reviewer 角色),不是 trigger_org_node_full 的 ready + 握手。Returns {"verdict", "raw_response"}.""" + hints = [ + "Heuristic flag: {} content resembles a narrative description of an action " + "rather than structured document content -- verify carefully.".format(label) + for label, content in (("SDD", sdd_content), ("Benchmark", benchmark_content)) + if looks_like_narration(content) + ] + response = org_node_trigger.trigger_org_node( + "planner_reviewer", + _PLANNER_REVIEW_PROMPT.format( + goal_text=goal_text, brd_content=brd_content, prd_content=prd_content, + sdd_content=sdd_content, benchmark_content=benchmark_content, + narration_hint="\n".join(hints), + ), + target_folder_path, timeout=timeout, + ) + return {"verdict": _parse_verdict_line(response), "raw_response": response} +``` + +### 5.3 新增:`_existing_doc_artifact_contents` + 重试循环核心 `_review_planner_output_with_retries` + +`_existing_doc_artifact_contents` 是 fresh-run 和 resume 两条路径共用的"这个 task 现在已经有哪些 doc artifact"探测函数——**这正是修 P0 bug 的关键点**:resume 分支不再无条件假设 sdd/prd/benchmark 存在(旧代码 `task_execution.py:126-129` 的 `_artifact_content()` 硬失败调用),而是先探测存在性,"有就用、没有就重新规划",不再对"到底有没有"这件事打赌。 + +```python +_PLANNER_REVIEW_MAX_ATTEMPTS = 3 + + +class PlannerReviewLoopLimitReachedError(Exception): + """复核 _PLANNER_REVIEW_MAX_ATTEMPTS 轮都没拿到 PASS。""" + + +def _existing_doc_artifact_contents(conn, target_folder_path, task_id): + """{artifact_type: content},只包含真的存在的(brd/prd/sdd/benchmark 的子集, + 可能是空字典——比如 task-execution-resume-assumes-planner-artifacts-exist.md + 记录的那次真实复现,Planner 图从第一个节点就彻底失败,什么都没落盘)。""" + ids = task_execution._latest_artifact_ids(conn, task_id, task_execution._DOC_ARTIFACT_TYPES) + return {t: _read_artifact_content(conn, target_folder_path, aid) for t, aid in ids.items()} + + +def _review_planner_output_with_retries(conn, task_id, goal_text, target_folder_path, timeout=300): + """fresh-run(plan_and_review_documents)和 resume-from-reviewing_planner_output + (task_execution.py)共用的核心循环。第 1 轮优先复用已经存在的 doc artifact(resume + 场景,避免重复规划一次已经成功的产出);探测为空,或后续轮次,都重新调用 + plan_documents() 生成新的一批。每轮复核结果落一条 execution_event + (event_type='planner_review_verdict'),供审计/未来 Phase 2 复用,Phase 1 自己不读它。 + Returns plan_documents() 的返回形状(来自拿到 PASS 的那一轮)。 + ponytail: 每轮拒绝都是完整重新规划 BRD/PRD/SDD/Benchmark,不是针对性地只让 Planner + 改被拒的那一份文档——粗粒度但正确,跟 org_loop_route.py 现有 full_replan 的粒度一致。 + Phase 2 的节点级 loop_back(org-graph-unification-and-universal-self-planning- + requirements.md Phase 2)是精确重试的升级路径。 + """ + feedback_suffix = "" + verdict = None + for attempt in range(1, _PLANNER_REVIEW_MAX_ATTEMPTS + 1): + existing = _existing_doc_artifact_contents(conn, target_folder_path, task_id) if attempt == 1 else {} + if existing: + artifact_ids = task_execution._latest_artifact_ids( + conn, task_id, task_execution._DOC_ARTIFACT_TYPES + ) + plan_result = {"run_id": None, "artifact_ids": artifact_ids} + else: + plan_result = plan_documents(conn, task_id, goal_text + feedback_suffix, target_folder_path) + artifact_ids = plan_result["artifact_ids"] + + sdd_content = _read_artifact_content(conn, target_folder_path, artifact_ids["sdd"]) + brd_content = _read_artifact_content(conn, target_folder_path, artifact_ids["brd"]) if "brd" in artifact_ids else "N/A: not provided" + prd_content = _read_artifact_content(conn, target_folder_path, artifact_ids["prd"]) if "prd" in artifact_ids else "N/A: not provided" + benchmark_content = _read_artifact_content(conn, target_folder_path, artifact_ids["benchmark"]) if "benchmark" in artifact_ids else "N/A: not provided" + + review = review_planner_output( + conn, task_id, goal_text, target_folder_path, + sdd_content, brd_content, prd_content, benchmark_content, timeout=timeout, + ) + _record_planner_review_event(conn, task_id, attempt, review["verdict"], bool(existing)) + verdict = review["verdict"] + if verdict == "PASS": + return plan_result + feedback_suffix = "\n\n[Planner review attempt {} was rejected ({}): {}. Address this " \ + "before continuing.]".format(attempt, verdict, review["raw_response"]) + + raise PlannerReviewLoopLimitReachedError( + "task {} exhausted {} planner-review attempts, last verdict={}".format( + task_id, _PLANNER_REVIEW_MAX_ATTEMPTS, verdict + ) + ) + + +def plan_and_review_documents(conn, task_id, goal_text, target_folder_path, timeout=300): + """fresh-run 入口:两次握手(planner, planner_reviewer)+ 共用重试循环。取代 + run_task_lifecycle() 里原来的 trigger_org_node_full(planner) + plan_documents() + 两行。""" + org_node_lifecycle.trigger_org_node_full(conn, "planner", task_id, target_folder_path, _READY_PROMPT) + org_node_lifecycle.trigger_org_node_full(conn, "planner_reviewer", task_id, target_folder_path, _READY_PROMPT) + return _review_planner_output_with_retries(conn, task_id, goal_text, target_folder_path, timeout=timeout) +``` + +(`_record_planner_review_event` 是一个几行的 `execution_event` INSERT helper,形状照抄 `org_loop_route._write_status_event`。) + +### 5.4 `run_task_lifecycle()` 改动 + +```python +def run_task_lifecycle(conn, task_id, target_folder_path, goal_text): + plan_result = plan_and_review_documents(conn, task_id, goal_text, target_folder_path) + artifact_ids = plan_result["artifact_ids"] + sdd_content = _read_artifact_content(conn, target_folder_path, artifact_ids["sdd"]) + # FIX(本设计顺带修复,发现于本次调研,见 §6"额外发现"):prd/benchmark 原来是 + # artifact_ids["prd"] 无条件下标访问,自规划图省略 prd/benchmark 时会 KeyError + # 崩在这里——不是 resume 独有的问题,fresh-run 首次也会崩。改成跟 test_dataset/ + # test_script 一样的 "in" 判断 + 默认值。 + prd_content = _read_artifact_content(conn, target_folder_path, artifact_ids["prd"]) if "prd" in artifact_ids else "N/A: not provided" + benchmark_content = _read_artifact_content(conn, target_folder_path, artifact_ids["benchmark"]) if "benchmark" in artifact_ids else "N/A: not provided" + # test_dataset/test_script 部分不变(已有的 "in" 判断本来就是对的) + ... + org_node_lifecycle.trigger_org_node_full(conn, "coder", task_id, target_folder_path, _READY_PROMPT) + code_artifact_ids = implement_code(...) # 不变 + org_node_lifecycle.trigger_org_node_full(conn, "validator", task_id, target_folder_path, _READY_PROMPT) + verdict = validate_implementation_with_own_graph(...) # 不变 + ... +``` + +--- + +## 6. `task_execution.py` 变更 + +```python +_RESUMABLE_STATUSES = ("queued", "planning_brd_prd_sdd", "reviewing_planner_output", "coding", "validating") + + +def run_or_resume_task_lifecycle(conn, task_id, target_folder_path, goal_text): + status = ... + if status not in _RESUMABLE_STATUSES: + raise TaskExecutionError(...) + + if status == "queued": + return e2e_pipeline.run_task_lifecycle(conn, task_id, target_folder_path, goal_text) + + if status == "planning_brd_prd_sdd": + # planner 的 ready 握手已经成功过,但 plan_documents() 真正跑没跑完全未知 + # (P0 bug 场景:图第一个节点就彻底失败,什么都没落盘)。不再在这里对 + # sdd/prd/benchmark 做硬性 _artifact_content() 探测——把这件事完全交给 + # _review_planner_output_with_retries() 内部的存在性探测(§5.3),有就用, + # 没有就重新 plan_documents()。 + _check_not_paused(conn, task_id) + org_node_lifecycle.trigger_org_node_full( + conn, "planner_reviewer", task_id, target_folder_path, e2e_pipeline._READY_PROMPT + ) + status = "reviewing_planner_output" + + if status == "reviewing_planner_output": + _check_not_paused(conn, task_id) + plan_result = e2e_pipeline._review_planner_output_with_retries( + conn, task_id, goal_text, target_folder_path + ) + artifact_ids = plan_result["artifact_ids"] + sdd_content = e2e_pipeline._read_artifact_content(conn, target_folder_path, artifact_ids["sdd"]) + prd_content = _artifact_content_or_default(conn, target_folder_path, task_id, "prd", "N/A: not provided") + benchmark_content = _artifact_content_or_default(conn, target_folder_path, task_id, "benchmark", "N/A: not provided") + test_dataset_content = _artifact_content_or_default(conn, target_folder_path, task_id, "test_dataset", "N/A: not provided") + test_script_content = _artifact_content_or_default(conn, target_folder_path, task_id, "test_script", "N/A: not provided") + + _check_not_paused(conn, task_id) + org_node_lifecycle.trigger_org_node_full(conn, "coder", task_id, target_folder_path, e2e_pipeline._READY_PROMPT) + code_artifact_ids = e2e_pipeline.implement_code( + conn, task_id, sdd_content, goal_text, target_folder_path, + benchmark_content=benchmark_content, test_dataset_content=test_dataset_content, + test_script_content=test_script_content, + ) + else: # status in ("coding", "validating") -- Coder already ran, unchanged from today + # 这个分支也要读 sdd_content/prd_content 给 validator 用,逻辑跟今天一样 + # (原代码在函数顶部为所有 resumable 状态统一读取,这次改动后要挪到这个分支 + # 内部,改用 _artifact_content_or_default 而不是硬失败版本——见下方"顺带修复") + sdd_content = _artifact_content_or_default(conn, target_folder_path, task_id, "sdd", "N/A: not provided") + prd_content = _artifact_content_or_default(conn, target_folder_path, task_id, "prd", "N/A: not provided") + code_artifact_ids = _latest_artifact_ids(conn, task_id, _CODE_ARTIFACT_TYPES) + + _check_not_paused(conn, task_id) + if status != "validating": + org_node_lifecycle.trigger_org_node_full(conn, "validator", task_id, target_folder_path, e2e_pipeline._READY_PROMPT) + verdict = e2e_pipeline.validate_implementation_with_own_graph( + conn, task_id, sdd_content, prd_content, goal_text, target_folder_path, + ) + return {"task_id": task_id, "doc_artifact_ids": ..., "code_artifact_ids": code_artifact_ids, "verdict": verdict} +``` + +**注意**:原代码里 `sdd_content`/`prd_content`/`benchmark_content` 是在函数最开头、所有 resumable 状态共用地读取(`task_execution.py:126-137`),且 sdd/prd/benchmark 三个都是硬失败版本(`_artifact_content`,见 §7"P0 bug 修复点")——这正是 bug 本体所在的位置。本设计把这些读取挪到各自分支内部,且除 `sdd`(`reviewing_planner_output` 分支内,此时已经过复核确认存在)外全部改用软失败的 `_artifact_content_or_default`。`coding`/`validating` 分支里的 `sdd_content` 理论上不会真的缺失(复核通过才能到这两个状态),但仍用软失败版本兜底,防止一个理论上不该发生、但值得防御的边界状态导致跟今天完全一样的硬崩溃。 + +--- + +## 7. P0 bug(`task-execution-resume-assumes-planner-artifacts-exist.md`)的修复点小结 + +| 原代码位置 | 原问题 | 本设计的修复 | +|---|---|---| +| `task_execution.py:127-129`(旧) | `planning_brd_prd_sdd`/`coding`/`validating` 三个状态共用地、无条件调用 `_artifact_content()`(硬失败)读 sdd/prd/benchmark | 存在性探测下沉到 `_review_planner_output_with_retries()`(§5.3)内部,"没有就重新规划"而不是崩溃;`coding`/`validating` 分支改用软失败版本兜底(§6) | +| `e2e_pipeline.py:892-893`(旧,fresh-run 路径,本次调研新发现) | `artifact_ids["prd"]`/`artifact_ids["benchmark"]` 无条件下标访问,自规划图省略这两个类型时 `KeyError`,**不需要 resume 就能触发,首次运行就崩** | `run_task_lifecycle()` 改成跟 `test_dataset`/`test_script` 一样的 `"in" artifact_ids` 判断(§5.4) | + +--- + +## 8. 新角色 `planner_reviewer` + +### 8.1 `roles/planner_reviewer/role.json` + +照抄 `roles/validator/role.json` 的只读哲学(复核不应该有能力改任何文件): + +```json +{ + "role_id": "planner_reviewer", + "description": "graphlemon Planner Reviewer: independently judges whether Planner's BRD/PRD/SDD/Benchmark output is real structured content (not narrative prose) and consistent with the goal, before Coder is allowed to start. Read-only on source.", + "migrated_from_profile": null, + "identity": { + "soul_md_path": "agents/planner_reviewer/SOUL.md", + "agents_md_path": "agents/planner_reviewer/AGENTS.md" + }, + "context": { "memory": "project" }, + "model": "claude-sonnet-5", + "skills": ["planner-output-review", "unattended-agent-operations"], + "applications": { "mcp_servers": [] }, + "workflows": { "dynamic_planning": "none" }, + "permissions": { + "tools": ["Read", "Grep", "Glob"], + "disallowed_tools": ["Edit", "Write", "Bash"], + "notes": [ + { "constraint": "只读复核,不做任何文件/命令操作", "enforced": "tool_level", + "detail": "跟 validator 不同:validator 需要 Bash 去真的跑测试脚本,planner_reviewer 只判断文档内容质量,不需要执行任何东西,可以比 validator 更严格地在工具层面禁掉 Bash。" } + ] + }, + "acceptance_criteria": { "ref": "docs/sdd/06-验收与测试锚点.md" } +} +``` + +(`workflows.dynamic_planning: "none"`——Phase 1 的 `planner_reviewer` 是一次 flat `trigger_org_node` 调用,不是 Work Graph 节点,不需要自规划;Phase 2 会把它接进合图后的自规划体系,那时这个字段要改。) + +### 8.2 新增 skill:`skills/planner-output-review/SKILL.md` + +内容要点(供实现阶段撰写,本设计只定范围): +- 判断对象:BRD/PRD/SDD/Benchmark 是否为结构化文档内容,而不是"我做了什么"的叙述文字(直接引用 `docs/todo/validator-test-script-narrative-output-issue.md` 记录的真实案例作为反面教材)。 +- 判断对象二:产出是否跟 goal_text 语义一致(复用 `_prd_consistency_verify_fn` 已经建立的"内部矛盾检测"思路,但这次是跨文档一致性,不是单文档内部一致性)。 +- 输出格式:必须以 `VERDICT: PASS|FAIL|INCONCLUSIVE` 收尾(复用 `_parse_verdict_line` 的既有解析约定,不新发明格式)。 +- 明确"只读,不改文件"——跟 `qa-task-validation` 的既有措辞风格一致。 + +### 8.3 `agents/planner_reviewer/SOUL.md` + `AGENTS.md` + +照 `agents/validator/SOUL.md`(32 行)+ `AGENTS.md`(48 行)的规模和"独立验收、只读、不绕过发现的问题"基调迁移,替换掉"验收最终代码实现"为"验收 Planner 的文档产出"。具体文案是实现阶段的工作,不在本 SDD 展开。 + +--- + +## 9. `scheduler.py` 变更 + +`_BLOCKED_REASON_FOR_EXCEPTION`(`scheduler.py:79-84`)新增一行: + +```python +(e2e_pipeline.PlannerReviewLoopLimitReachedError, "planner_review_loop_limit_reached"), +``` + +这样复核耗尽重试时,`task_execution.py::_mark_blocked`/`scheduler.py` 的 `except _ROUTABLE_EXCEPTIONS` 分支能给出具体的 `blocked_reason`(而不是退化成通用的 `"execution_error"`),跟 `test_script_verification_failed`/`prd_consistency_check_failed` 的既有先例一致。 + +--- + +## 10. 测试方案 + +### 10.1 新增/改动的单测文件 + +- `src/test_org_node_lifecycle.py`:`_PRE_STATE`/`_POST_STATE`/`_REQUIRED_ARTIFACTS` 改动后的握手链测试(`planner_reviewer` 的 PRE/POST、`coder` 新 PRE_STATE、`_REQUIRED_ARTIFACTS["coder"]` 收窄后不再要求 brd/prd/benchmark)。 +- `src/test_e2e_pipeline.py`: + - `review_planner_output()`/`looks_like_narration()` 单测(含真实复现案例的原文措辞作为 fixture)。 + - `_review_planner_output_with_retries()`:PASS 直接返回;FAIL 重试到 `_PLANNER_REVIEW_MAX_ATTEMPTS` 后抛 `PlannerReviewLoopLimitReachedError`;第 1 轮存在既有 artifact 时跳过 `plan_documents()` 重新调用。 + - `run_task_lifecycle()`:`artifact_ids` 缺 `prd`/`benchmark` 时不再 `KeyError`(§7 第二行的回归测试)。 +- `src/test_task_execution.py`: + - 改写 `ResumeFromPlanningTest`(现在的 `status="planning_brd_prd_sdd"` 场景,`_REQUIRED_ARTIFACTS["coder"]` 收窄后这个测试原本"必须写全部 4 个 doc artifact"的注释/setup 要跟着简化)。 + - 新增 `ResumeFromReviewingTest`(`status="reviewing_planner_output"`,只跳到 Coder,不重新握手 planner/planner_reviewer)。 + - 新增 `ResumeFromPlanningWithZeroArtifactsTest`——**这是 P0 bug 的直接回归测试**:`status="planning_brd_prd_sdd"`、不写任何 doc artifact,断言 resume 不抛异常,而是走到重新 `plan_documents()` 的路径。 + - `_RESUMABLE_STATUSES` 新增值的 `ResumeUnresumableStatusTest` 边界检查照旧模式补一条。 +- `src/test_schema.py`:新增 `MigrateAddReviewingPlannerOutputStatusTest`/`MigrateAddPlannerReviewVerdictEventTypeTest`,结构照抄 `MigrateAddWorkGraphLoopLimitConfigTest`/等价的 `loop_back_triggered` 迁移测试(幂等性 + 旧数据库升级正确性)。 +- `src/test_scheduler.py`:`_BLOCKED_REASON_FOR_EXCEPTION` 新增映射的确认测试(照抄现有几行同类断言)。 + +### 10.2 验收标准(对应需求文档 Phase 1 的验收) + +1. **真实复现场景回归**:构造一个"benchmark 输出叙述文字"的假 mock 响应(复用 `validator-test-script-narrative-output-issue.md` 记录的原文),断言 `planner_reviewer` 给出 `FAIL`/`INCONCLUSIVE`,且 Coder(`implement_code`)**没有被调用**。 +2. **P0 bug 回归**:`status="planning_brd_prd_sdd"` + 零 doc artifact 的任务能被 resume,不抛 `TaskExecutionError`。 +3. **fresh-run KeyError 回归**:`plan_documents()` 返回的 `artifact_ids` 只有 `sdd`(没有 brd/prd/benchmark)时,`run_task_lifecycle()` 不崩。 +4. 全量回归:`python3 -m unittest discover -s src` + `discover -s supervisor -p "test_*.py"` 保持全绿。 + +--- + +## 11. 非目标 / 已知简化(Phase 1 范围内,明确不做) + +1. **复核失败的重试不是精确 `loop_back`**——`_review_planner_output_with_retries()` 每次拒绝都是完整重新跑一次 `plan_documents()`(全量重新生成 BRD/PRD/SDD/Benchmark),不是 Phase 2 要做的"精确跳回某个具体节点"。这是需求文档 Phase 1 明确允许的粗粒度过渡方案。 +2. **复核重试次数不持久化跨进程重启**——`_PLANNER_REVIEW_MAX_ATTEMPTS` 循环的 `attempt` 计数只在一次函数调用的 Python 栈里,进程崩溃后 resume 会让这个计数器归零重新开始(不会死循环,因为 `_review_planner_output_with_retries()` 每次被调用都有自己独立的 3 次上限,只是"总共实际尝试次数"跨多次 resume 可能超过 3)。`execution_event(event_type='planner_review_verdict')` 已经把每一轮的真实结果记下来,为将来需要跨进程持久化计数时提供数据基础,但 Phase 1 不读它做判断。 +3. **不接入 `org_level_loop.py`/`bug_record.py` 的根因路由**——复核失败走的是 `PlannerReviewLoopLimitReachedError` → `blocked_pending_human`,不产生 `bug_record` 行,不经过 `root_cause_trace` 的三视角陪审团判断。这套机制要不要接进来是 Phase 2(FR-4,节点级验证 skill 统一路由)的范围。 +4. **`looks_like_narration()` 是软信号,不是硬拦截**——见 §5.1 的 `ponytail` 注释,误判成本导致选择不做机械硬拦截,Phase 2 关联的结构性修复(`validator-test-script-narrative-output-issue.md` §8)才是真正的上游方案。 +5. **不改 Coder/Validator 的自规划**——`plan_and_review_documents()`/`review_planner_output()` 都是 flat 调用,不是 Work Graph 节点,不涉及 `plan_work_graph()` 的通用化(Phase 2 范围)。 + +--- + +## 12. 实施顺序建议 + +1. `schema.py` 两个迁移函数 + `backend_server.py` 接线 + `test_schema.py` 迁移测试(独立、无依赖,先落地)。 +2. `roles/planner_reviewer/role.json` + `agents/planner_reviewer/SOUL.md`/`AGENTS.md` + `skills/planner-output-review/SKILL.md`(角色发现是自动的,但没有这些文件真实调用会失败)。 +3. `org_node_lifecycle.py` 的 `_PRE_STATE`/`_POST_STATE`/`_REQUIRED_ARTIFACTS` 改动 + `test_org_node_lifecycle.py`。 +4. `e2e_pipeline.py` 新增函数(§5)+ `run_task_lifecycle()` 改动 + `test_e2e_pipeline.py`(这一步体量最大,建议拆两次提交:先加新函数+测试,再改 `run_task_lifecycle()` 接入)。 +5. `task_execution.py` 改动(§6)+ `test_task_execution.py`(依赖第 4 步的新函数已存在)。 +6. `scheduler.py` 的 `_BLOCKED_REASON_FOR_EXCEPTION` 一行 + `test_scheduler.py`。 +7. 全量回归 `discover -s src` + `discover -s supervisor`,确认零回归。 +8. (可选,需要真实 API 额度,用户决定是否现在跑)`RUN_REAL_AGENT_E2E=1` 针对新流程的一次真实端到端验证。 diff --git a/docs/done/task-execution-resume-assumes-planner-artifacts-exist.md b/docs/done/task-execution-resume-assumes-planner-artifacts-exist.md new file mode 100644 index 0000000..86fc8ce --- /dev/null +++ b/docs/done/task-execution-resume-assumes-planner-artifacts-exist.md @@ -0,0 +1,37 @@ +# `task_execution.py` 恢复逻辑假设 Planner 一定已产出 sdd/prd/benchmark artifact——问题记录 + +**日期**: 2026-08-08 +**性质**: 问题记录(真实复现,根因已定位,未修复)。发现于 `docs/done/work-graph-self-planning-and-loop-back-requirements.md`(Planner 自规划)上线后的第一次真实端到端验证。 +**状态**: ✅ 已修复(commit `ea19c7d`,2026-08-08,随 Phase 1 一并落地)。修复方式与 §4"可能的修复方向"一致:sdd/prd/benchmark 的存在性探测下沉到新增的 `_review_planner_output_with_retries()` 内部(有就用,没有就重新规划),不再在 resume 分支顶部无条件硬读;同一次实现还修复了 fresh-run 路径(`run_task_lifecycle()`)里同类但更早触发的 `KeyError`。详细设计见 `docs/done/org-graph-unification-phase1-review-gate-design.md` §6-7,`python3 -m unittest discover -s src` 全绿(2026-08-12 复核)。 + +## 1. 现象(真实复现) + +任务 `2084590a`(目标:修复 `work_node_run.py` 的 `execution_log_file` UNIQUE 约束 bug): + +1. 第一次真实调用:Planner 自规划出 `sdd → benchmark → test_script` 三节点图(没有 brd/prd——goal 是个小 bug 修复,Planner 判断不需要),`sdd` 节点的 `build_prompt()` 因为 `_DOC_PROMPTS["sdd"]` 硬编码的 `{prd_content}` 占位符找不到值而抛 `KeyError('prd_content')`(已修复,见 `e2e_pipeline.py::_MissingContentDefault`)。此时 `sdd` 节点本身从未成功过,`artifact` 表里这个 task 一行都没有。 +2. 通过 `/route` 的 `manual_resume` 恢复:`task.status` 被设置回 `planning_brd_prd_sdd`。 +3. 重新执行时走的是 `task_execution.py::run_or_resume_task_lifecycle()` 的"从 `planning_brd_prd_sdd` 恢复"分支,而不是从头重跑 `plan_documents()`——这个分支无条件调用 `_artifact_content(conn, ..., "sdd")`/`"prd"`/`"benchmark"`,读不到就直接 `TaskExecutionError`。因为这个 task 的 Planner 图从未成功过一个节点,三个 artifact 全部不存在,报错为 `"task ... has no sdd artifact to resume from"`,任务再次卡住。 + +## 2. 根因 + +`org_node_lifecycle.trigger_org_node_full(conn, "planner", ...)` 在 `run_task_lifecycle()` 里**先于** `plan_documents()` 执行,并把 `task.status` 设成 `_POST_STATE["planner"] = "planning_brd_prd_sdd"`(`org_node_lifecycle.py:32`)——这个状态转移只代表"Planner 的 ready 确认调用成功了",不代表"Planner 的真实规划(`plan_documents()`)已经产出了任何东西"。 + +`task_execution.py::run_or_resume_task_lifecycle()` 里 `planning_brd_prd_sdd` 分支(`task_execution.py:126-129`)的注释写的是"Planner done, Coder not yet triggered"——这个假设在 Work Graph 自规划(`docs/done/work-graph-self-planning-and-loop-back-requirements.md`)上线前基本成立:固定链的第一个节点是 brd,几乎不可能在第一个节点就失败到零产出。自规划之后这个假设不再可靠——自规划出的图可能把一个更容易失败的节点(比如需要更复杂 judgment 的 sdd)排在第一位,如果它在第一次尝试就彻底失败(耗尽 loop_back/retry 上限,或者像这次一样是一个跟自规划本身相关的新 bug),Planner 的 `work_graph_run` 会整个失败,不会有任何 artifact 落盘,但 `task.status` 已经先一步被置成了 `planning_brd_prd_sdd`。 + +`test_dataset_content`/`test_script_content` 已经用 `_artifact_content_or_default()`(读不到就给默认值,不硬失败)处理过同一类问题(2026-08-06,那次是"这两个 artifact 类型本来就允许不存在"),但 `sdd`/`prd`/`benchmark` 三个仍然用会硬失败的 `_artifact_content()`——这个区分在固定链时代是对的(sdd/prd/benchmark 曾经是保证存在的),自规划之后不再成立。 + +## 3. 跟已实现的两个相关修复的区别 + +- 不是 `work-graph-partial-artifact-persistence-requirements.md` 那次修复能覆盖的场景——那次解决的是"图跑到一半、后面节点失败,前面成功节点的产出要保留",这次是"图的第一个节点就彻底失败,压根没有'前面成功节点'可保留",`_write_completed_node_artifacts` 在这种情况下正确地什么都不落盘(因为真的什么都没成功),不是它的 bug。 +- 不是这次 session 里刚修的 `_MissingContentDefault`(`e2e_pipeline.py::_planner_prompt_builder`)能覆盖的——那个修的是"build_prompt 拼提示词时崩溃",这次是崩溃之后**恢复逻辑**本身的假设过时了,是下游的另一层问题。 + +## 4. 可能的修复方向(未验证,仅供后续参考) + +在 `run_or_resume_task_lifecycle()` 的 `planning_brd_prd_sdd` 分支里,先检查这个 task 是否**真的有任何 doc 类 artifact**(`_latest_artifact_ids(conn, task_id, _DOC_ARTIFACT_TYPES)` 是否为空): + +- 如果有(哪怕只有部分,比如这次自规划场景下可能只有 sdd 没有 prd/benchmark),按现有逻辑继续,但 `prd`/`benchmark` 也应该跟 `test_dataset`/`test_script` 一样改用 `_artifact_content_or_default`,不再假设它们一定存在(自规划场景下 prd/benchmark 本来就可能被 Planner 判断为不需要,不是"缺失的异常情况")。 +- 如果一个都没有(这次真实复现的情况——Planner 的图从第一个节点就彻底失败,什么都没留下),说明"从这个状态恢复"这个假设本身不成立,应该等价于从 `queued` 重新完整跑一次 `run_task_lifecycle()`(即重新调用 `plan_documents()`),而不是尝试读取不存在的 artifact。 + +## 5. 临时规避(已用于解决这次真实卡住的任务) + +直接把 `task.status` 手动改回 `queued`(`UPDATE task SET status='queued', blocked_reason=NULL WHERE task_id=...`),让下一次执行走 `run_task_lifecycle()` 的"queued"分支、重新完整调用一次 `plan_documents()`——不是通用修复,只是这一次真实卡住的任务的人工解法。 diff --git a/docs/todo/org-graph-unification-and-universal-self-planning-requirements.md b/docs/todo/org-graph-unification-and-universal-self-planning-requirements.md index 7ba62b1..92624f5 100644 --- a/docs/todo/org-graph-unification-and-universal-self-planning-requirements.md +++ b/docs/todo/org-graph-unification-and-universal-self-planning-requirements.md @@ -3,7 +3,7 @@ **日期**: 2026-08-08 **性质**: 需求文档(BRD/PRD 层面),不含详细技术设计。融合并统领以下既有 `docs/todo/` 文档的相关内容: - `docs/todo/org-graph-as-work-graph-unified-loop-requirements.md`(已删除,内容 100% 并入本文档)——本文档是它的展开与拍板;该文档提出的"跨 Org Agent 精确 loop_back"诉求是本文档 FR-4 + §4 关键架构决策的正文。 -- `docs/todo/task-execution-resume-assumes-planner-artifacts-exist.md`——它记录的 bug 与本文档 FR-1 改动的是**同一段** `run_or_resume_task_lifecycle()` 分支逻辑(见 §2.1 调研结论),折入 Phase 1 一并修,不再单独排期。 +- `docs/done/task-execution-resume-assumes-planner-artifacts-exist.md`——它记录的 bug 与本文档 FR-1 改动的是**同一段** `run_or_resume_task_lifecycle()` 分支逻辑(见 §2.1 调研结论),折入 Phase 1 一并修,不再单独排期。 - `docs/todo/validator-test-script-narrative-output-issue.md` 第 8 节提到的"另一份尚未撰写、关于给 Coder agent 加入 test_script 模块的文档",即本文档 Phase 2(FR-2/FR-3)。 **触发场景**:任务 `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 规划。 @@ -110,8 +110,8 @@ ## 6. Phase 规划 -### Phase 1 — Org 主流程改造 + resume bug 一并修复 -- **详细设计**:`docs/todo/org-graph-unification-phase1-review-gate-design.md`(2026-08-08,可直接实施)。 +### Phase 1 — Org 主流程改造 + resume bug 一并修复 ✅ 已实现(commit `ea19c7d`,2026-08-08) +- **详细设计**:`docs/done/org-graph-unification-phase1-review-gate-design.md`(已随实现移入 `docs/done/`,与实现逐条吻合,见该文档更新后的状态行)。 - **范围**:FR-1(含 `task-execution-resume-assumes-planner-artifacts-exist.md` 的修复)。 - **产出**:`task.status`/`execution_event` 两处 schema 迁移、新增独立 org role `planner_reviewer`、`org_node_lifecycle.py` 的 `_PRE_STATE`/`_POST_STATE`/`_REQUIRED_ARTIFACTS` 扩展、`e2e_pipeline.py` 新增复核+重试循环、`run_task_lifecycle()`/`run_or_resume_task_lifecycle()` 新分支。复核失败的重试是任务内部的粗粒度循环(完整重新规划,不接 `org_level_loop.py`),不是"打回整个任务重来"。 - **额外发现**(设计阶段调研中定位,一并修复):`e2e_pipeline.py::run_task_lifecycle()` 对 `artifact_ids["prd"]`/`["benchmark"]` 的无条件下标访问,在自规划图省略这两个类型时会 `KeyError`——这个崩溃在 fresh-run 首次执行就会触发,不需要等到 resume,是比 P0 bug 更早触发的同类问题。 @@ -142,7 +142,7 @@ ## 7. 待确认的问题(讨论后逐条勾掉,参照 `graph-engineering-open-items.md` 的状态图例) ✅ §4 关键架构决策:方案 A(合图)。2026-08-08 用户拍板。 -✅ 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。 +✅ FR-1 新增的 `task.status` 枚举值定为 `reviewing_planner_output`。2026-08-08,随 Phase 1 SDD(`docs/done/org-graph-unification-phase1-review-gate-design.md`)一起拍板,理由见该文档 §3.1。 ✅ FR-1 的"复核 Validator"用新增独立 org role `planner_reviewer`,不复用 `validator`。2026-08-08 用户拍板(`AskUserQuestion` 确认),理由见 Phase 1 SDD §2.1——`_PRE_STATE`/`_POST_STATE` 是按 org_node 名字一对一映射,同一个 key 没法挂两组状态转移。 ✅ 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,但不复用文件本身。 ✅ 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 文档链那种固定顺序的强约束。 diff --git a/docs/todo/org-graph-unification-phase1-review-gate-design.md b/docs/todo/org-graph-unification-phase1-review-gate-design.md deleted file mode 100644 index 8307157..0000000 --- a/docs/todo/org-graph-unification-phase1-review-gate-design.md +++ /dev/null @@ -1,467 +0,0 @@ -# Phase 1 — Planner 产出复核阶段 + resume/fresh-run artifact 假设修复——详细设计(SDD) - -**日期**: 2026-08-08 -**性质**: 详细设计,对应 `docs/todo/org-graph-unification-and-universal-self-planning-requirements.md` 的 Phase 1(FR-1,含 `task-execution-resume-assumes-planner-artifacts-exist.md` 的修复)。给出可直接实施的 schema/接口/算法/测试方案。 -**依赖代码**:`src/org_node_lifecycle.py`(`_PRE_STATE`/`_POST_STATE`/`_REQUIRED_ARTIFACTS`/`trigger_org_node_full`)、`src/e2e_pipeline.py`(`run_task_lifecycle`/`plan_documents`/`_parse_verdict_line`/`_VALIDATOR_VERDICT_PROMPT` 先例)、`src/task_execution.py`(`run_or_resume_task_lifecycle`/`_RESUMABLE_STATUSES`)、`src/scheduler.py`(`_BLOCKED_REASON_FOR_EXCEPTION`)、`src/schema.py`(`migrate_add_loop_back_event_type` 的 create-copy-drop-rename 先例)、`src/agent_profile.py`(`_discover_role_ids`,角色自动发现)、`roles/validator/role.json`(新角色的模板)。 -**状态**: 设计草案,待实施。 - ---- - -## 1. 前置决策回顾 - -`org-graph-unification-and-universal-self-planning-requirements.md` §7 留了两个 Phase 1 相关的开放问题,本设计做出以下拍板(理由见对应小节): - -1. **"复核 Validator"用新角色,不复用 `validator`**——已通过 `AskUserQuestion` 与用户确认(2026-08-08),理由见 §2。 -2. **新 `task.status` 枚举值定为 `reviewing_planner_output`**——本设计新拍板,理由见 §3.1。 - ---- - -## 2. 设计概览 - -### 2.1 为什么必须是新角色,不能复用 `validator` - -`org_node_lifecycle.py:31-32` 的 `_PRE_STATE`/`_POST_STATE` 是按 `org_node` 名字的**一对一**字典:`trigger_org_node_full()` 内部 `task["status"] != _PRE_STATE[org_node]`(`org_node_lifecycle.py:102`)这一行假设同一个 `org_node` 在整个任务生命周期里只对应一组固定的前置/后置状态。如果"复核"和"终审"都叫 `validator`,这个字典没法给同一个 key 挂两组状态转移。 - -新角色 `planner_reviewer` 走 Role Package 抽象(`roles/planner_reviewer/role.json`)——`agent_profile.py:72` 的 `ORG_NODES = _discover_role_ids()` 已经是扫描 `roles/` 目录动态发现的,新增一个角色只需要新增一个 `role.json` + `agents/planner_reviewer/SOUL.md`/`AGENTS.md`,不需要改 `agent_profile.py`/`org_node_trigger.py` 任何一行代码(`org_node_trigger.py:30` 的 `if org_node not in ORG_NODES` 检查复用同一个动态发现的集合)。 - -### 2.2 一个关键的既有语义,决定了整个设计怎么做(必须先理解,否则会重复 P0 bug 的错误) - -调研 `run_task_lifecycle()`(`e2e_pipeline.py:878-920`)发现:`trigger_org_node_full()` 对每个 org_node 的调用顺序是**先做一次轻量"ready"握手 + 立即把 `task.status` 推进到 POST_STATE,然后才执行真正的实质性工作**(`plan_documents()`/`implement_code()`/`validate_implementation_with_own_graph()`)。也就是说: - -``` -trigger_org_node_full(planner, _READY_PROMPT) # 握手成功 -> status 立即变成 planning_brd_prd_sdd -plan_result = plan_documents(...) # 真正的规划工作,此时 status 已经是"下一阶段的样子" -``` - -`task.status` 从来不是"这个阶段的产出是否合格"的信号,只是"轮到谁"的记账 token——真正的产出校验(如果有的话)由**下游独立的一次调用**完成(Validator precedent:`validate_implementation_with_own_graph()` 产出 verdict 后,`route_org_loop`/`close_task_done` 才是真正决定任务命运的地方;`e2e_pipeline.py:891-893` 目前对 Planner 的产出**完全没有**类似的 verdict 步骤,这正是本次要补的缺口)。 - -**设计推论**:新增的 `planner_reviewer` 阶段必须照抄 Validator 这个"先握手推进状态、真正校验单独一次调用"的模式,**不能**在校验通过之后才推进 `task.status`——否则会破坏 `trigger_org_node_full` 现有的统一契约(03-C 的 `c_org_node_trigger` 就是"握手+状态推进"这一个原子操作,不应该在这次改动里对 Planner Reviewer 单独发明"校验通过才推进"的第二套语义)。真正的把关,落在"任务从 `reviewing_planner_output` 恢复时必须重新调用一次真正的校验函数,不能假设状态名等于结果"——这跟 Validator 的 `validating` resume 分支已经是同一个模式(`task_execution.py:102-118` 的 docstring 原话:"this is NOT a between-phases resume... the actual gap is narrower")。 - -### 2.3 新流程总览 - -``` -queued - --[trigger_org_node_full(planner)]--> planning_brd_prd_sdd - --[plan_documents() 真正规划]--> - --[trigger_org_node_full(planner_reviewer)]--> reviewing_planner_output - --[_review_planner_output_with_retries() 真正复核,内部最多重试 3 轮]--> - 通过 --> - --[trigger_org_node_full(coder)]--> coding - --[implement_code() 真正实现]--> - --[trigger_org_node_full(validator)]--> validating - --[validate_implementation_with_own_graph() 真正验收]--> - PASS --> done (close_task_done,不变) - FAIL/INCONCLUSIVE --> org_level_loop.run_fix_loop(不变,Phase 1 不碰这段) - 复核耗尽重试 --> blocked_pending_human(reason='planner_review_loop_limit_reached',新) -``` - ---- - -## 3. Schema 变更 - -### 3.1 `task.status` 新增枚举值 `reviewing_planner_output` - -命名理由:`_PRE_STATE`/`_POST_STATE` 现有值(`planning_brd_prd_sdd`/`coding`/`validating`)全部是"上一阶段刚握手完成"的记账态,不是字面意义的"正在进行中"(见 §2.2)。`validating` 已经是这种"其实已经握手完成、真正工作在这个状态名下面跑"的先例,`reviewing_planner_output` 沿用同一个动名词命名习惯,语义一致。 - -`schema.py:106-111` 的 CHECK 约束扩展为: - -```sql -status TEXT NOT NULL CHECK (status IN ( - 'queued', 'planning_brd_prd_sdd', 'reviewing_planner_output', 'planning_failed', - 'coding', 'coding_failed', 'validating', - 'routed_fast_lane', 'routed_full_replan', - 'blocked_pending_human', 'done' -)), -``` - -新增迁移函数 `migrate_add_reviewing_planner_output_status(conn)`,照抄 `migrate_expand_artifact_types`/`migrate_add_loop_back_event_type` 的 create-copy-drop-rename 惯用法(SQLite 无法 `ALTER` CHECK 约束):读 `sqlite_master` 里 `task` 表的建表 SQL,如果已经包含 `'reviewing_planner_output'` 就直接返回(幂等);否则 `CREATE TABLE task_new (...)` 用新枚举、`INSERT INTO task_new SELECT * FROM task`、`DROP TABLE task`、`ALTER TABLE task_new RENAME TO task`,随后重建 `idx_task_status` 索引。`backend_server.py` 里紧邻 `migrate_add_loop_back_event_type(conn)`(第 1040 行)新增一行调用。 - -### 3.2 `execution_event.event_type` 新增枚举值 `planner_review_verdict` - -用于持久化每一轮复核的 PASS/FAIL/INCONCLUSIVE 结果(`payload: {"attempt": N, "verdict": "...", "used_existing_artifacts": true/false}`)——不是给控制台展示用的(那是 Phase 3 的事),是给"崩溃后 resume 时能不能信任当前这批 doc artifact 已经被复核过"这件事留下审计证据;Phase 1 本身**不**依赖读这张表来做 resume 判断(见 §4.3 的简化说明),只是照 `loop_back_triggered` 的先例把决策记下来,供人工排查和未来 Phase 2 复用。 - -`schema.py:264-268` 的 CHECK 约束新增一个值;新增迁移函数 `migrate_add_planner_review_verdict_event_type(conn)`,逻辑与 `migrate_add_loop_back_event_type` 完全同构(复制粘贴改枚举值)。`backend_server.py` 同一处再加一行调用。 - ---- - -## 4. `org_node_lifecycle.py` 变更 - -```python -_PRE_STATE = { - "planner": "queued", - "planner_reviewer": "planning_brd_prd_sdd", - "coder": "reviewing_planner_output", # 原来是 "planning_brd_prd_sdd" - "validator": "coding", -} -_POST_STATE = { - "planner": "planning_brd_prd_sdd", - "planner_reviewer": "reviewing_planner_output", - "coder": "coding", - "validator": "validating", -} -_REQUIRED_ARTIFACTS = { - "coder": ("sdd",), # 原来是 ("brd", "prd", "sdd", "benchmark") - "validator": ("code_diff", "test_report"), -} -``` - -### 4.1 为什么把 `_REQUIRED_ARTIFACTS["coder"]` 从 4 个类型收窄到只剩 `sdd` - -调研 `artifact_write.py:8-14` 发现 `validation_status` **在写入时就无条件设成 `'passed'`**(该文件自己的 docstring 承认这是一个简化:真正的语义内容校验"还没有专属的 LLM-judgment 调用")。也就是说 `_REQUIRED_ARTIFACTS["coder"]` 今天检查的"`validation_status='passed'`"永远为真——这个 gate 从来没有真正拦住过任何东西,是一个失效的桩。**新增的 `planner_reviewer` 阶段才是第一个真正做语义校验的 gate**,`_REQUIRED_ARTIFACTS["coder"]` 里 `brd`/`prd`/`benchmark` 这三个失效检查应该删掉,不是保留一个"看起来有校验、实际上从不生效"的假象。 - -保留 `sdd`:这不是校验质量,是**存在性**兜底——`implement_code()`(`e2e_pipeline.py:617`)的函数签名要求一个真实的 `sdd_content` 位置参数,`sdd` artifact 不存在会导致调用方在更早、更难定位的地方崩(`KeyError`),保留这一条纯粹是防止"复核通过了,但 sdd 其实是空的"这种不可能但值得防御的情况提前报出清晰错误,而不是让它一路传到 `implement_code()` 内部再崩。 - -### 4.2 `planner` 分支不变 - -`_check_concurrency(conn)` 仍然只在 `org_node == "planner"` 时触发(`org_node_lifecycle.py:110-111`)——并发上限的语义是"同时有几个 goal 在跑",跟这次改动无关,不变。 - ---- - -## 5. `e2e_pipeline.py` 变更 - -### 5.1 新增:叙述文字启发式(复核阶段的辅助信号,非硬拦截) - -```python -# ponytail: 子串匹配,从 docs/todo/validator-test-script-narrative-output-issue.md -# 记录的真实复现文案里提炼——"以为在描述自己做了什么"的稳定措辞模式。只作为喂给 -# planner_reviewer 的提示信号,不是硬性机械拦截(跟 check_script_compiles 不同: -# 这里没有客观的"编译通过/失败"可判断,误判成本是把合法的、恰好提到这些词的文档 -# 打回重来)。上限:这份短语表穷举不完,Phase 2 的结构性修复(把"探索"和"最终作答" -# 拆成两次独立调用,见 validator-test-script-narrative-output-issue.md §8)才是 -# 真正的上游修复,这里只是先兜住已经复现过的具体案例。 -_NARRATION_PHRASES = ( - "no further action", "launched the", "workflow finished", "workflow completed", - "it produced a", "you'll be notified", "no files were changed", -) - - -def looks_like_narration(content): - lowered = (content or "").lower() - return any(phrase in lowered for phrase in _NARRATION_PHRASES) -``` - -### 5.2 新增:复核 verdict prompt + 调用函数 - -照抄 `_VALIDATOR_VERDICT_PROMPT`/`validate_implementation_with_own_graph()` 结尾那段"拼 prompt → `org_node_trigger.trigger_org_node()` → `_parse_verdict_line()`"的既有模式(`_parse_verdict_line` 本身不用改,直接复用): - -```python -_PLANNER_REVIEW_PROMPT = """... goal_text={goal_text} ... -BRD: {brd_content} -PRD: {prd_content} -SDD: {sdd_content} -Benchmark: {benchmark_content} -{narration_hint} -... 以 "VERDICT: PASS|FAIL|INCONCLUSIVE" 收尾,说明理由 ... -""" - - -def review_planner_output( - conn, task_id, goal_text, target_folder_path, - sdd_content, brd_content="N/A: not provided", - prd_content="N/A: not provided", benchmark_content="N/A: not provided", - timeout=300, -): - """真正的复核调用(planner_reviewer 角色),不是 trigger_org_node_full 的 ready - 握手。Returns {"verdict", "raw_response"}.""" - hints = [ - "Heuristic flag: {} content resembles a narrative description of an action " - "rather than structured document content -- verify carefully.".format(label) - for label, content in (("SDD", sdd_content), ("Benchmark", benchmark_content)) - if looks_like_narration(content) - ] - response = org_node_trigger.trigger_org_node( - "planner_reviewer", - _PLANNER_REVIEW_PROMPT.format( - goal_text=goal_text, brd_content=brd_content, prd_content=prd_content, - sdd_content=sdd_content, benchmark_content=benchmark_content, - narration_hint="\n".join(hints), - ), - target_folder_path, timeout=timeout, - ) - return {"verdict": _parse_verdict_line(response), "raw_response": response} -``` - -### 5.3 新增:`_existing_doc_artifact_contents` + 重试循环核心 `_review_planner_output_with_retries` - -`_existing_doc_artifact_contents` 是 fresh-run 和 resume 两条路径共用的"这个 task 现在已经有哪些 doc artifact"探测函数——**这正是修 P0 bug 的关键点**:resume 分支不再无条件假设 sdd/prd/benchmark 存在(旧代码 `task_execution.py:126-129` 的 `_artifact_content()` 硬失败调用),而是先探测存在性,"有就用、没有就重新规划",不再对"到底有没有"这件事打赌。 - -```python -_PLANNER_REVIEW_MAX_ATTEMPTS = 3 - - -class PlannerReviewLoopLimitReachedError(Exception): - """复核 _PLANNER_REVIEW_MAX_ATTEMPTS 轮都没拿到 PASS。""" - - -def _existing_doc_artifact_contents(conn, target_folder_path, task_id): - """{artifact_type: content},只包含真的存在的(brd/prd/sdd/benchmark 的子集, - 可能是空字典——比如 task-execution-resume-assumes-planner-artifacts-exist.md - 记录的那次真实复现,Planner 图从第一个节点就彻底失败,什么都没落盘)。""" - ids = task_execution._latest_artifact_ids(conn, task_id, task_execution._DOC_ARTIFACT_TYPES) - return {t: _read_artifact_content(conn, target_folder_path, aid) for t, aid in ids.items()} - - -def _review_planner_output_with_retries(conn, task_id, goal_text, target_folder_path, timeout=300): - """fresh-run(plan_and_review_documents)和 resume-from-reviewing_planner_output - (task_execution.py)共用的核心循环。第 1 轮优先复用已经存在的 doc artifact(resume - 场景,避免重复规划一次已经成功的产出);探测为空,或后续轮次,都重新调用 - plan_documents() 生成新的一批。每轮复核结果落一条 execution_event - (event_type='planner_review_verdict'),供审计/未来 Phase 2 复用,Phase 1 自己不读它。 - Returns plan_documents() 的返回形状(来自拿到 PASS 的那一轮)。 - ponytail: 每轮拒绝都是完整重新规划 BRD/PRD/SDD/Benchmark,不是针对性地只让 Planner - 改被拒的那一份文档——粗粒度但正确,跟 org_loop_route.py 现有 full_replan 的粒度一致。 - Phase 2 的节点级 loop_back(org-graph-unification-and-universal-self-planning- - requirements.md Phase 2)是精确重试的升级路径。 - """ - feedback_suffix = "" - verdict = None - for attempt in range(1, _PLANNER_REVIEW_MAX_ATTEMPTS + 1): - existing = _existing_doc_artifact_contents(conn, target_folder_path, task_id) if attempt == 1 else {} - if existing: - artifact_ids = task_execution._latest_artifact_ids( - conn, task_id, task_execution._DOC_ARTIFACT_TYPES - ) - plan_result = {"run_id": None, "artifact_ids": artifact_ids} - else: - plan_result = plan_documents(conn, task_id, goal_text + feedback_suffix, target_folder_path) - artifact_ids = plan_result["artifact_ids"] - - sdd_content = _read_artifact_content(conn, target_folder_path, artifact_ids["sdd"]) - brd_content = _read_artifact_content(conn, target_folder_path, artifact_ids["brd"]) if "brd" in artifact_ids else "N/A: not provided" - prd_content = _read_artifact_content(conn, target_folder_path, artifact_ids["prd"]) if "prd" in artifact_ids else "N/A: not provided" - benchmark_content = _read_artifact_content(conn, target_folder_path, artifact_ids["benchmark"]) if "benchmark" in artifact_ids else "N/A: not provided" - - review = review_planner_output( - conn, task_id, goal_text, target_folder_path, - sdd_content, brd_content, prd_content, benchmark_content, timeout=timeout, - ) - _record_planner_review_event(conn, task_id, attempt, review["verdict"], bool(existing)) - verdict = review["verdict"] - if verdict == "PASS": - return plan_result - feedback_suffix = "\n\n[Planner review attempt {} was rejected ({}): {}. Address this " \ - "before continuing.]".format(attempt, verdict, review["raw_response"]) - - raise PlannerReviewLoopLimitReachedError( - "task {} exhausted {} planner-review attempts, last verdict={}".format( - task_id, _PLANNER_REVIEW_MAX_ATTEMPTS, verdict - ) - ) - - -def plan_and_review_documents(conn, task_id, goal_text, target_folder_path, timeout=300): - """fresh-run 入口:两次握手(planner, planner_reviewer)+ 共用重试循环。取代 - run_task_lifecycle() 里原来的 trigger_org_node_full(planner) + plan_documents() - 两行。""" - org_node_lifecycle.trigger_org_node_full(conn, "planner", task_id, target_folder_path, _READY_PROMPT) - org_node_lifecycle.trigger_org_node_full(conn, "planner_reviewer", task_id, target_folder_path, _READY_PROMPT) - return _review_planner_output_with_retries(conn, task_id, goal_text, target_folder_path, timeout=timeout) -``` - -(`_record_planner_review_event` 是一个几行的 `execution_event` INSERT helper,形状照抄 `org_loop_route._write_status_event`。) - -### 5.4 `run_task_lifecycle()` 改动 - -```python -def run_task_lifecycle(conn, task_id, target_folder_path, goal_text): - plan_result = plan_and_review_documents(conn, task_id, goal_text, target_folder_path) - artifact_ids = plan_result["artifact_ids"] - sdd_content = _read_artifact_content(conn, target_folder_path, artifact_ids["sdd"]) - # FIX(本设计顺带修复,发现于本次调研,见 §6"额外发现"):prd/benchmark 原来是 - # artifact_ids["prd"] 无条件下标访问,自规划图省略 prd/benchmark 时会 KeyError - # 崩在这里——不是 resume 独有的问题,fresh-run 首次也会崩。改成跟 test_dataset/ - # test_script 一样的 "in" 判断 + 默认值。 - prd_content = _read_artifact_content(conn, target_folder_path, artifact_ids["prd"]) if "prd" in artifact_ids else "N/A: not provided" - benchmark_content = _read_artifact_content(conn, target_folder_path, artifact_ids["benchmark"]) if "benchmark" in artifact_ids else "N/A: not provided" - # test_dataset/test_script 部分不变(已有的 "in" 判断本来就是对的) - ... - org_node_lifecycle.trigger_org_node_full(conn, "coder", task_id, target_folder_path, _READY_PROMPT) - code_artifact_ids = implement_code(...) # 不变 - org_node_lifecycle.trigger_org_node_full(conn, "validator", task_id, target_folder_path, _READY_PROMPT) - verdict = validate_implementation_with_own_graph(...) # 不变 - ... -``` - ---- - -## 6. `task_execution.py` 变更 - -```python -_RESUMABLE_STATUSES = ("queued", "planning_brd_prd_sdd", "reviewing_planner_output", "coding", "validating") - - -def run_or_resume_task_lifecycle(conn, task_id, target_folder_path, goal_text): - status = ... - if status not in _RESUMABLE_STATUSES: - raise TaskExecutionError(...) - - if status == "queued": - return e2e_pipeline.run_task_lifecycle(conn, task_id, target_folder_path, goal_text) - - if status == "planning_brd_prd_sdd": - # planner 的 ready 握手已经成功过,但 plan_documents() 真正跑没跑完全未知 - # (P0 bug 场景:图第一个节点就彻底失败,什么都没落盘)。不再在这里对 - # sdd/prd/benchmark 做硬性 _artifact_content() 探测——把这件事完全交给 - # _review_planner_output_with_retries() 内部的存在性探测(§5.3),有就用, - # 没有就重新 plan_documents()。 - _check_not_paused(conn, task_id) - org_node_lifecycle.trigger_org_node_full( - conn, "planner_reviewer", task_id, target_folder_path, e2e_pipeline._READY_PROMPT - ) - status = "reviewing_planner_output" - - if status == "reviewing_planner_output": - _check_not_paused(conn, task_id) - plan_result = e2e_pipeline._review_planner_output_with_retries( - conn, task_id, goal_text, target_folder_path - ) - artifact_ids = plan_result["artifact_ids"] - sdd_content = e2e_pipeline._read_artifact_content(conn, target_folder_path, artifact_ids["sdd"]) - prd_content = _artifact_content_or_default(conn, target_folder_path, task_id, "prd", "N/A: not provided") - benchmark_content = _artifact_content_or_default(conn, target_folder_path, task_id, "benchmark", "N/A: not provided") - test_dataset_content = _artifact_content_or_default(conn, target_folder_path, task_id, "test_dataset", "N/A: not provided") - test_script_content = _artifact_content_or_default(conn, target_folder_path, task_id, "test_script", "N/A: not provided") - - _check_not_paused(conn, task_id) - org_node_lifecycle.trigger_org_node_full(conn, "coder", task_id, target_folder_path, e2e_pipeline._READY_PROMPT) - code_artifact_ids = e2e_pipeline.implement_code( - conn, task_id, sdd_content, goal_text, target_folder_path, - benchmark_content=benchmark_content, test_dataset_content=test_dataset_content, - test_script_content=test_script_content, - ) - else: # status in ("coding", "validating") -- Coder already ran, unchanged from today - # 这个分支也要读 sdd_content/prd_content 给 validator 用,逻辑跟今天一样 - # (原代码在函数顶部为所有 resumable 状态统一读取,这次改动后要挪到这个分支 - # 内部,改用 _artifact_content_or_default 而不是硬失败版本——见下方"顺带修复") - sdd_content = _artifact_content_or_default(conn, target_folder_path, task_id, "sdd", "N/A: not provided") - prd_content = _artifact_content_or_default(conn, target_folder_path, task_id, "prd", "N/A: not provided") - code_artifact_ids = _latest_artifact_ids(conn, task_id, _CODE_ARTIFACT_TYPES) - - _check_not_paused(conn, task_id) - if status != "validating": - org_node_lifecycle.trigger_org_node_full(conn, "validator", task_id, target_folder_path, e2e_pipeline._READY_PROMPT) - verdict = e2e_pipeline.validate_implementation_with_own_graph( - conn, task_id, sdd_content, prd_content, goal_text, target_folder_path, - ) - return {"task_id": task_id, "doc_artifact_ids": ..., "code_artifact_ids": code_artifact_ids, "verdict": verdict} -``` - -**注意**:原代码里 `sdd_content`/`prd_content`/`benchmark_content` 是在函数最开头、所有 resumable 状态共用地读取(`task_execution.py:126-137`),且 sdd/prd/benchmark 三个都是硬失败版本(`_artifact_content`,见 §7"P0 bug 修复点")——这正是 bug 本体所在的位置。本设计把这些读取挪到各自分支内部,且除 `sdd`(`reviewing_planner_output` 分支内,此时已经过复核确认存在)外全部改用软失败的 `_artifact_content_or_default`。`coding`/`validating` 分支里的 `sdd_content` 理论上不会真的缺失(复核通过才能到这两个状态),但仍用软失败版本兜底,防止一个理论上不该发生、但值得防御的边界状态导致跟今天完全一样的硬崩溃。 - ---- - -## 7. P0 bug(`task-execution-resume-assumes-planner-artifacts-exist.md`)的修复点小结 - -| 原代码位置 | 原问题 | 本设计的修复 | -|---|---|---| -| `task_execution.py:127-129`(旧) | `planning_brd_prd_sdd`/`coding`/`validating` 三个状态共用地、无条件调用 `_artifact_content()`(硬失败)读 sdd/prd/benchmark | 存在性探测下沉到 `_review_planner_output_with_retries()`(§5.3)内部,"没有就重新规划"而不是崩溃;`coding`/`validating` 分支改用软失败版本兜底(§6) | -| `e2e_pipeline.py:892-893`(旧,fresh-run 路径,本次调研新发现) | `artifact_ids["prd"]`/`artifact_ids["benchmark"]` 无条件下标访问,自规划图省略这两个类型时 `KeyError`,**不需要 resume 就能触发,首次运行就崩** | `run_task_lifecycle()` 改成跟 `test_dataset`/`test_script` 一样的 `"in" artifact_ids` 判断(§5.4) | - ---- - -## 8. 新角色 `planner_reviewer` - -### 8.1 `roles/planner_reviewer/role.json` - -照抄 `roles/validator/role.json` 的只读哲学(复核不应该有能力改任何文件): - -```json -{ - "role_id": "planner_reviewer", - "description": "graphlemon Planner Reviewer: independently judges whether Planner's BRD/PRD/SDD/Benchmark output is real structured content (not narrative prose) and consistent with the goal, before Coder is allowed to start. Read-only on source.", - "migrated_from_profile": null, - "identity": { - "soul_md_path": "agents/planner_reviewer/SOUL.md", - "agents_md_path": "agents/planner_reviewer/AGENTS.md" - }, - "context": { "memory": "project" }, - "model": "claude-sonnet-5", - "skills": ["planner-output-review", "unattended-agent-operations"], - "applications": { "mcp_servers": [] }, - "workflows": { "dynamic_planning": "none" }, - "permissions": { - "tools": ["Read", "Grep", "Glob"], - "disallowed_tools": ["Edit", "Write", "Bash"], - "notes": [ - { "constraint": "只读复核,不做任何文件/命令操作", "enforced": "tool_level", - "detail": "跟 validator 不同:validator 需要 Bash 去真的跑测试脚本,planner_reviewer 只判断文档内容质量,不需要执行任何东西,可以比 validator 更严格地在工具层面禁掉 Bash。" } - ] - }, - "acceptance_criteria": { "ref": "docs/sdd/06-验收与测试锚点.md" } -} -``` - -(`workflows.dynamic_planning: "none"`——Phase 1 的 `planner_reviewer` 是一次 flat `trigger_org_node` 调用,不是 Work Graph 节点,不需要自规划;Phase 2 会把它接进合图后的自规划体系,那时这个字段要改。) - -### 8.2 新增 skill:`skills/planner-output-review/SKILL.md` - -内容要点(供实现阶段撰写,本设计只定范围): -- 判断对象:BRD/PRD/SDD/Benchmark 是否为结构化文档内容,而不是"我做了什么"的叙述文字(直接引用 `docs/todo/validator-test-script-narrative-output-issue.md` 记录的真实案例作为反面教材)。 -- 判断对象二:产出是否跟 goal_text 语义一致(复用 `_prd_consistency_verify_fn` 已经建立的"内部矛盾检测"思路,但这次是跨文档一致性,不是单文档内部一致性)。 -- 输出格式:必须以 `VERDICT: PASS|FAIL|INCONCLUSIVE` 收尾(复用 `_parse_verdict_line` 的既有解析约定,不新发明格式)。 -- 明确"只读,不改文件"——跟 `qa-task-validation` 的既有措辞风格一致。 - -### 8.3 `agents/planner_reviewer/SOUL.md` + `AGENTS.md` - -照 `agents/validator/SOUL.md`(32 行)+ `AGENTS.md`(48 行)的规模和"独立验收、只读、不绕过发现的问题"基调迁移,替换掉"验收最终代码实现"为"验收 Planner 的文档产出"。具体文案是实现阶段的工作,不在本 SDD 展开。 - ---- - -## 9. `scheduler.py` 变更 - -`_BLOCKED_REASON_FOR_EXCEPTION`(`scheduler.py:79-84`)新增一行: - -```python -(e2e_pipeline.PlannerReviewLoopLimitReachedError, "planner_review_loop_limit_reached"), -``` - -这样复核耗尽重试时,`task_execution.py::_mark_blocked`/`scheduler.py` 的 `except _ROUTABLE_EXCEPTIONS` 分支能给出具体的 `blocked_reason`(而不是退化成通用的 `"execution_error"`),跟 `test_script_verification_failed`/`prd_consistency_check_failed` 的既有先例一致。 - ---- - -## 10. 测试方案 - -### 10.1 新增/改动的单测文件 - -- `src/test_org_node_lifecycle.py`:`_PRE_STATE`/`_POST_STATE`/`_REQUIRED_ARTIFACTS` 改动后的握手链测试(`planner_reviewer` 的 PRE/POST、`coder` 新 PRE_STATE、`_REQUIRED_ARTIFACTS["coder"]` 收窄后不再要求 brd/prd/benchmark)。 -- `src/test_e2e_pipeline.py`: - - `review_planner_output()`/`looks_like_narration()` 单测(含真实复现案例的原文措辞作为 fixture)。 - - `_review_planner_output_with_retries()`:PASS 直接返回;FAIL 重试到 `_PLANNER_REVIEW_MAX_ATTEMPTS` 后抛 `PlannerReviewLoopLimitReachedError`;第 1 轮存在既有 artifact 时跳过 `plan_documents()` 重新调用。 - - `run_task_lifecycle()`:`artifact_ids` 缺 `prd`/`benchmark` 时不再 `KeyError`(§7 第二行的回归测试)。 -- `src/test_task_execution.py`: - - 改写 `ResumeFromPlanningTest`(现在的 `status="planning_brd_prd_sdd"` 场景,`_REQUIRED_ARTIFACTS["coder"]` 收窄后这个测试原本"必须写全部 4 个 doc artifact"的注释/setup 要跟着简化)。 - - 新增 `ResumeFromReviewingTest`(`status="reviewing_planner_output"`,只跳到 Coder,不重新握手 planner/planner_reviewer)。 - - 新增 `ResumeFromPlanningWithZeroArtifactsTest`——**这是 P0 bug 的直接回归测试**:`status="planning_brd_prd_sdd"`、不写任何 doc artifact,断言 resume 不抛异常,而是走到重新 `plan_documents()` 的路径。 - - `_RESUMABLE_STATUSES` 新增值的 `ResumeUnresumableStatusTest` 边界检查照旧模式补一条。 -- `src/test_schema.py`:新增 `MigrateAddReviewingPlannerOutputStatusTest`/`MigrateAddPlannerReviewVerdictEventTypeTest`,结构照抄 `MigrateAddWorkGraphLoopLimitConfigTest`/等价的 `loop_back_triggered` 迁移测试(幂等性 + 旧数据库升级正确性)。 -- `src/test_scheduler.py`:`_BLOCKED_REASON_FOR_EXCEPTION` 新增映射的确认测试(照抄现有几行同类断言)。 - -### 10.2 验收标准(对应需求文档 Phase 1 的验收) - -1. **真实复现场景回归**:构造一个"benchmark 输出叙述文字"的假 mock 响应(复用 `validator-test-script-narrative-output-issue.md` 记录的原文),断言 `planner_reviewer` 给出 `FAIL`/`INCONCLUSIVE`,且 Coder(`implement_code`)**没有被调用**。 -2. **P0 bug 回归**:`status="planning_brd_prd_sdd"` + 零 doc artifact 的任务能被 resume,不抛 `TaskExecutionError`。 -3. **fresh-run KeyError 回归**:`plan_documents()` 返回的 `artifact_ids` 只有 `sdd`(没有 brd/prd/benchmark)时,`run_task_lifecycle()` 不崩。 -4. 全量回归:`python3 -m unittest discover -s src` + `discover -s supervisor -p "test_*.py"` 保持全绿。 - ---- - -## 11. 非目标 / 已知简化(Phase 1 范围内,明确不做) - -1. **复核失败的重试不是精确 `loop_back`**——`_review_planner_output_with_retries()` 每次拒绝都是完整重新跑一次 `plan_documents()`(全量重新生成 BRD/PRD/SDD/Benchmark),不是 Phase 2 要做的"精确跳回某个具体节点"。这是需求文档 Phase 1 明确允许的粗粒度过渡方案。 -2. **复核重试次数不持久化跨进程重启**——`_PLANNER_REVIEW_MAX_ATTEMPTS` 循环的 `attempt` 计数只在一次函数调用的 Python 栈里,进程崩溃后 resume 会让这个计数器归零重新开始(不会死循环,因为 `_review_planner_output_with_retries()` 每次被调用都有自己独立的 3 次上限,只是"总共实际尝试次数"跨多次 resume 可能超过 3)。`execution_event(event_type='planner_review_verdict')` 已经把每一轮的真实结果记下来,为将来需要跨进程持久化计数时提供数据基础,但 Phase 1 不读它做判断。 -3. **不接入 `org_level_loop.py`/`bug_record.py` 的根因路由**——复核失败走的是 `PlannerReviewLoopLimitReachedError` → `blocked_pending_human`,不产生 `bug_record` 行,不经过 `root_cause_trace` 的三视角陪审团判断。这套机制要不要接进来是 Phase 2(FR-4,节点级验证 skill 统一路由)的范围。 -4. **`looks_like_narration()` 是软信号,不是硬拦截**——见 §5.1 的 `ponytail` 注释,误判成本导致选择不做机械硬拦截,Phase 2 关联的结构性修复(`validator-test-script-narrative-output-issue.md` §8)才是真正的上游方案。 -5. **不改 Coder/Validator 的自规划**——`plan_and_review_documents()`/`review_planner_output()` 都是 flat 调用,不是 Work Graph 节点,不涉及 `plan_work_graph()` 的通用化(Phase 2 范围)。 - ---- - -## 12. 实施顺序建议 - -1. `schema.py` 两个迁移函数 + `backend_server.py` 接线 + `test_schema.py` 迁移测试(独立、无依赖,先落地)。 -2. `roles/planner_reviewer/role.json` + `agents/planner_reviewer/SOUL.md`/`AGENTS.md` + `skills/planner-output-review/SKILL.md`(角色发现是自动的,但没有这些文件真实调用会失败)。 -3. `org_node_lifecycle.py` 的 `_PRE_STATE`/`_POST_STATE`/`_REQUIRED_ARTIFACTS` 改动 + `test_org_node_lifecycle.py`。 -4. `e2e_pipeline.py` 新增函数(§5)+ `run_task_lifecycle()` 改动 + `test_e2e_pipeline.py`(这一步体量最大,建议拆两次提交:先加新函数+测试,再改 `run_task_lifecycle()` 接入)。 -5. `task_execution.py` 改动(§6)+ `test_task_execution.py`(依赖第 4 步的新函数已存在)。 -6. `scheduler.py` 的 `_BLOCKED_REASON_FOR_EXCEPTION` 一行 + `test_scheduler.py`。 -7. 全量回归 `discover -s src` + `discover -s supervisor`,确认零回归。 -8. (可选,需要真实 API 额度,用户决定是否现在跑)`RUN_REAL_AGENT_E2E=1` 针对新流程的一次真实端到端验证。 diff --git a/docs/todo/org-graph-unification-phase2-universal-self-planning-design.md b/docs/todo/org-graph-unification-phase2-universal-self-planning-design.md index c031d03..8331e32 100644 --- a/docs/todo/org-graph-unification-phase2-universal-self-planning-design.md +++ b/docs/todo/org-graph-unification-phase2-universal-self-planning-design.md @@ -2,8 +2,8 @@ **日期**: 2026-08-08 **性质**: 详细设计,对应 `docs/todo/org-graph-unification-and-universal-self-planning-requirements.md` 的 Phase 2(FR-2/FR-3/FR-4),按 §4/§4.1 已拍板的合图(方案 A)路径设计。 -**依赖**: 硬依赖 Phase 1(`docs/todo/org-graph-unification-phase1-review-gate-design.md`)先落地——Phase 2 是在 Phase 1 建立的 `planner_reviewer` 阶段基础上继续合图,Phase 1 未完成前本文档的调用点改动无法接入。Phase 1 目前由另一个进程在实施中,本文档的具体代码位置引用以 Phase 1 SDD 的设计为准,Phase 1 真正落地后需要一次快速核对(见 §9)。 -**状态**: 设计草案。**2026-08-08 修订**:写作过程中,需求文档 §7 的四个开放问题已被用户直接拍板(详见该文档 §7 对应条目),其中两条推翻了本文档最初的设计判断——Coder/Validator 自规划改为"建议节点清单"而非完全自由(§3),`work_graph_run.org_node` 改为彻底移除而非保留(§2.1)——本版本已按拍板结果改写,不是原始草稿。 +**依赖**: 硬依赖 Phase 1(`docs/done/org-graph-unification-phase1-review-gate-design.md`)——已实现(commit `ea19c7d`,2026-08-08)。§9 接口核对清单已于 2026-08-12 逐条核对完成:三项全部吻合(`plan_and_review_documents()` 经 `_review_planner_output_with_retries()` 透传 `plan_documents()` 的 `{"run_id", "artifact_ids"}` 返回形状,含 `run_id`;`task.status` 新值确认是 `reviewing_planner_output`;`roles/planner_reviewer/role.json` 目前 `skills: ["planner-output-review", "unattended-agent-operations"]`,未预留 §7.1 新 skill 位置,Phase 2 实施时新增一行即可,不是不兼容)。 +**状态**: 设计草案,Phase 1 前置条件已满足,可以开始实施。**2026-08-08 修订**:写作过程中,需求文档 §7 的四个开放问题已被用户直接拍板(详见该文档 §7 对应条目),其中两条推翻了本文档最初的设计判断——Coder/Validator 自规划改为"建议节点清单"而非完全自由(§3),`work_graph_run.org_node` 改为彻底移除而非保留(§2.1)——本版本已按拍板结果改写,不是原始草稿。**2026-08-08 追加**(同日,见需求文档新增 §3.1):FR-4 的目标被明确为"失败即进化"完整方法论(判断失败 → Reflection-review 动态诊断 → 系统性缺陷则先 Fix-Skill → 跳回执行),本文档 §5 设计的 `dynamic_loop_back` 只覆盖其中第 1/2/4 步,第 3 步(Fix-Skill)是已知缺口,见 §5 开头说明和 §8 第 5 条。 --- @@ -397,11 +397,11 @@ Phase 1 阶段"复核失败先用现有 `org_level_loop.py` 粗粒度路由兜åº --- -## 9. 跟正在实施的 Phase 1 的接口核对清单(Phase 1 落地后立即执行) +## 9. 跟 Phase 1 的接口核对清单——✅ 全部核对完成(2026-08-12) -- [ ] `plan_and_review_documents()` 的返回形状是否包含 `run_id`(§6.2 依赖)。 -- [ ] Phase 1 实际引入的新 `task.status` 值是不是本文档假设的名字(Phase 1 SDD 定的是 `reviewing_planner_output`,Phase 1 由另一进程实施,实际落地值需要核对一次)。 -- [ ] `roles/planner_reviewer/role.json` 的 `skills` 列表是否已经预留位置给 §7.1 新增的 `work-graph-node-review`(如果 Phase 1 实施时已经写了别的占位 skill 名字,需要统一)。 +- [x] `plan_and_review_documents()` 的返回形状是否包含 `run_id`——**是**:`plan_and_review_documents()` → `_review_planner_output_with_retries()` → 透传拿到 PASS 那一轮 `plan_documents()` 的原始返回值 `{"run_id", "artifact_ids"}`,`run_id` 齐全,§6.2 的假设成立,`implement_code()`/`validate_implementation_with_own_graph()` 可以直接拿这个 `run_id` 用作 `append_to_run_id`。 +- [x] Phase 1 实际引入的新 `task.status` 值是不是 `reviewing_planner_output`——**是**,`src/schema.py`/`src/org_node_lifecycle.py` 均按此实现,与本文档假设完全一致。 +- [x] `roles/planner_reviewer/role.json` 的 `skills` 列表是否已经预留位置给 §7.1 新增的 `work-graph-node-review`——**没有预留**,当前是 `["planner-output-review", "unattended-agent-operations"]`。不是问题:Phase 2 实施 §7.1 时直接追加一行 `"work-graph-node-review"` 即可,`roles/coder`/`roles/validator` 的 `role.json` 同理需要各追加一行——这是 Phase 2c 实施清单里的一步,不是需要提前协调的接口不兼容。 --- diff --git a/docs/todo/task-execution-resume-assumes-planner-artifacts-exist.md b/docs/todo/task-execution-resume-assumes-planner-artifacts-exist.md deleted file mode 100644 index 32398d7..0000000 --- a/docs/todo/task-execution-resume-assumes-planner-artifacts-exist.md +++ /dev/null @@ -1,37 +0,0 @@ -# `task_execution.py` 恢复逻辑假设 Planner 一定已产出 sdd/prd/benchmark artifact——问题记录 - -**日期**: 2026-08-08 -**性质**: 问题记录(真实复现,根因已定位,未修复)。发现于 `docs/done/work-graph-self-planning-and-loop-back-requirements.md`(Planner 自规划)上线后的第一次真实端到端验证。 -**状态**: 待修复,未立项。**2026-08-08 更新**:已折入 `docs/todo/org-graph-unification-and-universal-self-planning-requirements.md` 的 Phase 1(FR-1)——插入"Planner 产出复核"新阶段本来就要重写这段 `run_or_resume_task_lifecycle()` 分支逻辑,这个 bug 跟那次改动是同一段代码路径,不再单独排期,随 Phase 1 一并修。**详细设计已发布**:`docs/todo/org-graph-unification-phase1-review-gate-design.md` §6-7——修复方式是把 sdd/prd/benchmark 的存在性探测下沉到新增的 `_review_planner_output_with_retries()` 内部(有就用,没有就重新规划),不再在 resume 分支顶部无条件硬读;同一次调研还发现 fresh-run 路径(`run_task_lifecycle()`)有同类但更早触发的 `KeyError`,一并修复。 - -## 1. 现象(真实复现) - -任务 `2084590a`(目标:修复 `work_node_run.py` 的 `execution_log_file` UNIQUE 约束 bug): - -1. 第一次真实调用:Planner 自规划出 `sdd → benchmark → test_script` 三节点图(没有 brd/prd——goal 是个小 bug 修复,Planner 判断不需要),`sdd` 节点的 `build_prompt()` 因为 `_DOC_PROMPTS["sdd"]` 硬编码的 `{prd_content}` 占位符找不到值而抛 `KeyError('prd_content')`(已修复,见 `e2e_pipeline.py::_MissingContentDefault`)。此时 `sdd` 节点本身从未成功过,`artifact` 表里这个 task 一行都没有。 -2. 通过 `/route` 的 `manual_resume` 恢复:`task.status` 被设置回 `planning_brd_prd_sdd`。 -3. 重新执行时走的是 `task_execution.py::run_or_resume_task_lifecycle()` 的"从 `planning_brd_prd_sdd` 恢复"分支,而不是从头重跑 `plan_documents()`——这个分支无条件调用 `_artifact_content(conn, ..., "sdd")`/`"prd"`/`"benchmark"`,读不到就直接 `TaskExecutionError`。因为这个 task 的 Planner 图从未成功过一个节点,三个 artifact 全部不存在,报错为 `"task ... has no sdd artifact to resume from"`,任务再次卡住。 - -## 2. 根因 - -`org_node_lifecycle.trigger_org_node_full(conn, "planner", ...)` 在 `run_task_lifecycle()` 里**先于** `plan_documents()` 执行,并把 `task.status` 设成 `_POST_STATE["planner"] = "planning_brd_prd_sdd"`(`org_node_lifecycle.py:32`)——这个状态转移只代表"Planner 的 ready 确认调用成功了",不代表"Planner 的真实规划(`plan_documents()`)已经产出了任何东西"。 - -`task_execution.py::run_or_resume_task_lifecycle()` 里 `planning_brd_prd_sdd` 分支(`task_execution.py:126-129`)的注释写的是"Planner done, Coder not yet triggered"——这个假设在 Work Graph 自规划(`docs/done/work-graph-self-planning-and-loop-back-requirements.md`)上线前基本成立:固定链的第一个节点是 brd,几乎不可能在第一个节点就失败到零产出。自规划之后这个假设不再可靠——自规划出的图可能把一个更容易失败的节点(比如需要更复杂 judgment 的 sdd)排在第一位,如果它在第一次尝试就彻底失败(耗尽 loop_back/retry 上限,或者像这次一样是一个跟自规划本身相关的新 bug),Planner 的 `work_graph_run` 会整个失败,不会有任何 artifact 落盘,但 `task.status` 已经先一步被置成了 `planning_brd_prd_sdd`。 - -`test_dataset_content`/`test_script_content` 已经用 `_artifact_content_or_default()`(读不到就给默认值,不硬失败)处理过同一类问题(2026-08-06,那次是"这两个 artifact 类型本来就允许不存在"),但 `sdd`/`prd`/`benchmark` 三个仍然用会硬失败的 `_artifact_content()`——这个区分在固定链时代是对的(sdd/prd/benchmark 曾经是保证存在的),自规划之后不再成立。 - -## 3. 跟已实现的两个相关修复的区别 - -- 不是 `work-graph-partial-artifact-persistence-requirements.md` 那次修复能覆盖的场景——那次解决的是"图跑到一半、后面节点失败,前面成功节点的产出要保留",这次是"图的第一个节点就彻底失败,压根没有'前面成功节点'可保留",`_write_completed_node_artifacts` 在这种情况下正确地什么都不落盘(因为真的什么都没成功),不是它的 bug。 -- 不是这次 session 里刚修的 `_MissingContentDefault`(`e2e_pipeline.py::_planner_prompt_builder`)能覆盖的——那个修的是"build_prompt 拼提示词时崩溃",这次是崩溃之后**恢复逻辑**本身的假设过时了,是下游的另一层问题。 - -## 4. 可能的修复方向(未验证,仅供后续参考) - -在 `run_or_resume_task_lifecycle()` 的 `planning_brd_prd_sdd` 分支里,先检查这个 task 是否**真的有任何 doc 类 artifact**(`_latest_artifact_ids(conn, task_id, _DOC_ARTIFACT_TYPES)` 是否为空): - -- 如果有(哪怕只有部分,比如这次自规划场景下可能只有 sdd 没有 prd/benchmark),按现有逻辑继续,但 `prd`/`benchmark` 也应该跟 `test_dataset`/`test_script` 一样改用 `_artifact_content_or_default`,不再假设它们一定存在(自规划场景下 prd/benchmark 本来就可能被 Planner 判断为不需要,不是"缺失的异常情况")。 -- 如果一个都没有(这次真实复现的情况——Planner 的图从第一个节点就彻底失败,什么都没留下),说明"从这个状态恢复"这个假设本身不成立,应该等价于从 `queued` 重新完整跑一次 `run_task_lifecycle()`(即重新调用 `plan_documents()`),而不是尝试读取不存在的 artifact。 - -## 5. 临时规避(已用于解决这次真实卡住的任务) - -直接把 `task.status` 手动改回 `queued`(`UPDATE task SET status='queued', blocked_reason=NULL WHERE task_id=...`),让下一次执行走 `run_task_lifecycle()` 的"queued"分支、重新完整调用一次 `plan_documents()`——不是通用修复,只是这一次真实卡住的任务的人工解法。 diff --git a/docs/todo/validator-test-script-narrative-output-issue.md b/docs/todo/validator-test-script-narrative-output-issue.md index 33a35ba..5a77480 100644 --- a/docs/todo/validator-test-script-narrative-output-issue.md +++ b/docs/todo/validator-test-script-narrative-output-issue.md @@ -15,7 +15,7 @@ | 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 自己识别,识别到不等于能强制拦住。 | | 6 | 08-08 | 结构性修复的落脚点确定为 `org-graph-unification-and-universal-self-planning-requirements.md` Phase 2——该文档非目标 §5.2 已记录这条已知风险,要求 Phase 2 落地时专门验证是否复现。 | -后续讨论方向记在 `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` 落地为具体设计)。 +后续讨论方向记在 `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/done/org-graph-unification-phase1-review-gate-design.md` 落地为具体设计)。 ## 1. 现象 diff --git a/docs/todo/work-graph-node-execution-deblackbox-requirements.md b/docs/todo/work-graph-node-execution-deblackbox-requirements.md index 2afe815..46ca78a 100644 --- a/docs/todo/work-graph-node-execution-deblackbox-requirements.md +++ b/docs/todo/work-graph-node-execution-deblackbox-requirements.md @@ -195,4 +195,4 @@ def run_fn(cwd, model=None): ## 7. 跟 Org-Graph-Unification 系列文档的关系 -正交但有先后关系:这次改动只影响**单节点执行机制**,不影响 Org Graph 流程形状或自规划范围,`docs/todo/org-graph-unification-phase1-review-gate-design.md`(Phase 1)不依赖这次改动,可以并行/先后独立推进。但 `docs/todo/org-graph-unification-and-universal-self-planning-requirements.md` 的 **Phase 2**(FR-4,节点级验证 skill)会大量新增 `judgment.kind="llm"` 判断调用——如果这次去黑盒化工作能在 Phase 2 动工前完成,Phase 2 就不需要在两套机制(Workflow 和新的 flat 调用)之间选一套来复用,直接基于 `trigger_org_node_structured()` 设计,减少后续返工。建议顺序:这份文档 → Phase 2,不是必须但值得。 +正交但有先后关系:这次改动只影响**单节点执行机制**,不影响 Org Graph 流程形状或自规划范围,`docs/done/org-graph-unification-phase1-review-gate-design.md`(Phase 1)不依赖这次改动,可以并行/先后独立推进。但 `docs/todo/org-graph-unification-and-universal-self-planning-requirements.md` 的 **Phase 2**(FR-4,节点级验证 skill)会大量新增 `judgment.kind="llm"` 判断调用——如果这次去黑盒化工作能在 Phase 2 动工前完成,Phase 2 就不需要在两套机制(Workflow 和新的 flat 调用)之间选一套来复用,直接基于 `trigger_org_node_structured()` 设计,减少后续返工。建议顺序:这份文档 → Phase 2,不是必须但值得。 diff --git a/src/schema.py b/src/schema.py index 2603edd..deb8658 100644 --- a/src/schema.py +++ b/src/schema.py @@ -697,7 +697,7 @@ def migrate_add_loop_back_event_type(conn): def migrate_add_reviewing_planner_output_status(conn): """2026-08-08 (Org Graph unification Phase 1, - docs/todo/org-graph-unification-phase1-review-gate-design.md §3.1): adds + docs/done/org-graph-unification-phase1-review-gate-design.md §3.1): adds 'reviewing_planner_output' to task.status's CHECK enum on an ALREADY-POPULATED database -- same create-copy-drop-rename dance as migrate_expand_artifact_types/migrate_add_loop_back_event_type. Idempotent @@ -741,7 +741,7 @@ def migrate_add_reviewing_planner_output_status(conn): def migrate_add_planner_review_verdict_event_type(conn): """2026-08-08 (Org Graph unification Phase 1, - docs/todo/org-graph-unification-phase1-review-gate-design.md §3.2): adds + docs/done/org-graph-unification-phase1-review-gate-design.md §3.2): adds 'planner_review_verdict' to execution_event.event_type's CHECK enum on an ALREADY-POPULATED database -- same create-copy-drop-rename dance as migrate_add_loop_back_event_type. Idempotent and safe to call -- libgit2 0.21.4