From b47e4364bb166f49c0394eb673190183799d0879 Mon Sep 17 00:00:00 2001 From: bboonnee Date: Thu, 13 Aug 2026 14:44:24 +0800 Subject: [PATCH] test_script re-enabled: each org agent self-plans whether to include it --- docs/done/org-graph-unification-and-universal-self-planning-requirements.md | 2 +- docs/done/org-graph-unification-phase1-review-gate-design.md | 4 ++-- docs/done/org-graph-unification-phase2-universal-self-planning-design.md | 2 +- docs/done/validator-test-script-narrative-output-issue.md | 120 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/done/work-graph-self-planning-and-loop-back-requirements.md | 6 +++--- docs/todo/validator-test-script-narrative-output-issue.md | 112 ---------------------------------------------------------------------------------------------------------------- skills/planner-output-review/SKILL.md | 2 +- skills/skill-defect-remediation/SKILL.md | 2 +- skills/work-graph-node-review/SKILL.md | 2 +- src/e2e_pipeline.py | 4 ++-- src/scheduler.py | 2 +- src/test_e2e_pipeline.py | 2 +- src/work_graph_plan.py | 10 +++++++++- 13 files changed, 143 insertions(+), 127 deletions(-) create mode 100644 docs/done/validator-test-script-narrative-output-issue.md delete mode 100644 docs/todo/validator-test-script-narrative-output-issue.md diff --git a/docs/done/org-graph-unification-and-universal-self-planning-requirements.md b/docs/done/org-graph-unification-and-universal-self-planning-requirements.md index 1627d53..becf665 100644 --- a/docs/done/org-graph-unification-and-universal-self-planning-requirements.md +++ b/docs/done/org-graph-unification-and-universal-self-planning-requirements.md @@ -4,7 +4,7 @@ **性质**: 需求文档(BRD/PRD 层面),不含详细技术设计。融合并统领以下既有 `docs/todo/` 文档的相关内容: - `docs/todo/org-graph-as-work-graph-unified-loop-requirements.md`(已删除,内容 100% 并入本文档)——本文档是它的展开与拍板;该文档提出的"跨 Org Agent 精确 loop_back"诉求是本文档 FR-4 + §4 关键架构决策的正文。 - `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)。 +- `docs/done/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 规划。 diff --git a/docs/done/org-graph-unification-phase1-review-gate-design.md b/docs/done/org-graph-unification-phase1-review-gate-design.md index 585b832..2df5592 100644 --- a/docs/done/org-graph-unification-phase1-review-gate-design.md +++ b/docs/done/org-graph-unification-phase1-review-gate-design.md @@ -122,7 +122,7 @@ _REQUIRED_ARTIFACTS = { ### 5.1 新增:叙述文字启发式(复核阶段的辅助信号,非硬拦截) ```python -# ponytail: 子串匹配,从 docs/todo/validator-test-script-narrative-output-issue.md +# ponytail: 子串匹配,从 docs/done/validator-test-script-narrative-output-issue.md # 记录的真实复现文案里提炼——"以为在描述自己做了什么"的稳定措辞模式。只作为喂给 # planner_reviewer 的提示信号,不是硬性机械拦截(跟 check_script_compiles 不同: # 这里没有客观的"编译通过/失败"可判断,误判成本是把合法的、恰好提到这些词的文档 @@ -396,7 +396,7 @@ def run_or_resume_task_lifecycle(conn, task_id, target_folder_path, goal_text): ### 8.2 新增 skill:`skills/planner-output-review/SKILL.md` 内容要点(供实现阶段撰写,本设计只定范围): -- 判断对象:BRD/PRD/SDD/Benchmark 是否为结构化文档内容,而不是"我做了什么"的叙述文字(直接引用 `docs/todo/validator-test-script-narrative-output-issue.md` 记录的真实案例作为反面教材)。 +- 判断对象:BRD/PRD/SDD/Benchmark 是否为结构化文档内容,而不是"我做了什么"的叙述文字(直接引用 `docs/done/validator-test-script-narrative-output-issue.md` 记录的真实案例作为反面教材)。 - 判断对象二:产出是否跟 goal_text 语义一致(复用 `_prd_consistency_verify_fn` 已经建立的"内部矛盾检测"思路,但这次是跨文档一致性,不是单文档内部一致性)。 - 输出格式:必须以 `VERDICT: PASS|FAIL|INCONCLUSIVE` 收尾(复用 `_parse_verdict_line` 的既有解析约定,不新发明格式)。 - 明确"只读,不改文件"——跟 `qa-task-validation` 的既有措辞风格一致。 diff --git a/docs/done/org-graph-unification-phase2-universal-self-planning-design.md b/docs/done/org-graph-unification-phase2-universal-self-planning-design.md index e47831c..dbd9b49 100644 --- a/docs/done/org-graph-unification-phase2-universal-self-planning-design.md +++ b/docs/done/org-graph-unification-phase2-universal-self-planning-design.md @@ -7,7 +7,7 @@ **2026-08-13 §2.1/§2.2 已实施**:`work_graph_run.org_node` 列彻底删除,`ux_work_graph_run_active` 收紧为 `UNIQUE(task_id) WHERE status NOT IN ('done', 'failed')`('blocked' 故意算作活跃——恢复一个 blocked 的 run 现在总是走 `append_to_run_id` 追加进同一行,不会再 INSERT 新行)。新迁移函数 `schema.migrate_drop_work_graph_run_org_node()`(内部先幂等调用 `migrate_relax_org_node_constraints()`,再做 create-copy-drop-rename,`PRAGMA foreign_keys=OFF` 全程包裹,理由同 `migrate_add_reviewing_planner_output_status` 的真实重启事故记录——`work_graph_run` 是被多张表 REFERENCES 的父表)已接入 `backend_server.py::_ensure_schema()`。下游改动:`work_graph_plan.py`/`work_graph_fork.py`(含 `create_fork()`/`_reconstruct_build_prompt()`/`execute_fork()`,`fork_info["org_node"]` 改名 `fork_info["subagent_type"]`,源节点自己的执行身份)/`root_cause_trace.py` 的三处 INSERT 全部去掉 `org_node` 列;`work_graph_run_query.py`(`list_work_graph_runs`/`get_work_graph_run`)与 `task_query.py::get_task` 的返回形状从单值 `"org_node"` 改成 `"org_nodes_involved"`(从 `work_node_execution.subagent_type` 去重派生的列表,`org_node=` 查询参数语义从等值匹配改成 EXISTS 子查询),前端 `frontend/app.js` 对应改造(`getLatestRunForOrgNode`/`orgNodesLabel`/`renderOrgGraphTopology` 等消费点全部改用列表语义)。发现一处设计文档本节 §2.1.3 没预料到的遗漏点:`task_query.py::get_task` 也有同样的单值 `org_node` 读取,一并修完。`root_cause_trace.py::judge_root_cause_via_work_graph` 的 INSERT 有一个已知、记录在代码注释里的边界情况:索引收紧后,若这个函数被调到时对应 task 已有一个仍处于活跃/blocked 状态的共享 run,会撞索引——目前不是真实风险(这个函数唯一的调用方 `org_level_loop.run_fix_loop` 只在一个已经不可达的分支里被调用,且它自己的测试夹具里对应 task 从未预先插入过 work_graph_run 行),但如果未来重新接上这条路径需要重新评估。`python3 -m unittest discover -s src`(768 tests)+ `discover -s supervisor`(126 tests)+ 4 个 frontend smoke test 全绿。 -**2026-08-13 §7.1 已实施**:新增 `skills/work-graph-node-review/SKILL.md`——判断"自规划 Work Graph 节点(Coder/Validator/planner_reviewer 自己规划出的、声明 `judgment.kind="llm"` 的节点)的产出是不是真的、结构化的",不是叙述文字冒充产出;吸收了 `docs/todo/validator-test-script-narrative-output-issue.md` §6 的根因分析和 `_NARRATION_RETRY_HINT`(条件句而非断言句)的措辞教训,并明确跟 `planner-output-review`(Planner 固定文档链、任务级、Coder 开始前把关)、`qa-task-validation`(任务级整体验收)、`skill-defect-remediation`(改 Skill 文件本身)三者的边界——判断粒度是"这一个自规划节点",不是文档链/整任务/Skill 文件。已挂到 `roles/planner_reviewer`、`roles/coder`、`roles/validator` 三个 `role.json` 的 `skills` 列表(各追加一行,如 §9 核对清单当时预判)。未修改 `.claude/agents/*.md`——那是 `agent_profile.deploy_agent_definitions()` 从 `role.json` 编译出的部署产物,由 Deploy Supervisor 在真实 cutover 时重新生成,不是手动跟 `role.json` 同步维护的(观察到现有 `.claude/agents/coder.md` 已经缺了更早前 Phase 2a 加的 `design-dynamic-work-graph`,印证这个文件本来就不是手动保持同步的)。`python3 -m unittest discover -s src`(768 tests,含 `test_agent_profile.py`)全绿。 +**2026-08-13 §7.1 已实施**:新增 `skills/work-graph-node-review/SKILL.md`——判断"自规划 Work Graph 节点(Coder/Validator/planner_reviewer 自己规划出的、声明 `judgment.kind="llm"` 的节点)的产出是不是真的、结构化的",不是叙述文字冒充产出;吸收了 `docs/done/validator-test-script-narrative-output-issue.md` §6 的根因分析和 `_NARRATION_RETRY_HINT`(条件句而非断言句)的措辞教训,并明确跟 `planner-output-review`(Planner 固定文档链、任务级、Coder 开始前把关)、`qa-task-validation`(任务级整体验收)、`skill-defect-remediation`(改 Skill 文件本身)三者的边界——判断粒度是"这一个自规划节点",不是文档链/整任务/Skill 文件。已挂到 `roles/planner_reviewer`、`roles/coder`、`roles/validator` 三个 `role.json` 的 `skills` 列表(各追加一行,如 §9 核对清单当时预判)。未修改 `.claude/agents/*.md`——那是 `agent_profile.deploy_agent_definitions()` 从 `role.json` 编译出的部署产物,由 Deploy Supervisor 在真实 cutover 时重新生成,不是手动跟 `role.json` 同步维护的(观察到现有 `.claude/agents/coder.md` 已经缺了更早前 Phase 2a 加的 `design-dynamic-work-graph`,印证这个文件本来就不是手动保持同步的)。`python3 -m unittest discover -s src`(768 tests,含 `test_agent_profile.py`)全绿。 **Phase 2c 至此全部完成**(§2.1/§2.2/§6 Validator 自规划改造/§7.1 均已实施;§7.2 收口分析已完成,决定不改代码,见上文)。**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)是已知缺口。**2026-08-12 补齐**:§5 已扩写为完整四步设计(§5.4 Reflection-review 现在同时判断目标节点和缺陷性质,§5.5 新增 Fix-Skill,真实调用既有但从未真正投入使用的 `skill_update.py::update_skill()`),过程中发现两个既有事实(`skills:` frontmatter 只按名字动态加载内容、不需要重新部署;`org_level_loop.py` 的 `_ROUTE_FOR_CATEGORY` 从来没有 `skill_defect` 这个 key,`update_skill()` 在这个代码库里投产以来从未被真正调用过)——详见 §5 开头。由哪个身份执笔改 Skill 已通过 `AskUserQuestion` 确认(2026-08-12):统一用 Validator,见 §5.5。 diff --git a/docs/done/validator-test-script-narrative-output-issue.md b/docs/done/validator-test-script-narrative-output-issue.md new file mode 100644 index 0000000..0c82ac3 --- /dev/null +++ b/docs/done/validator-test-script-narrative-output-issue.md @@ -0,0 +1,120 @@ +# test_script 节点偶发输出叙述文字而非代码——问题记录(Planner + Validator 都会中招) + +**日期**: 2026-08-07(最后更新 2026-08-13) +**性质**: 问题记录——源于真实生产复现,根因已定位(§6),已落地两轮缓解(§7/§8),结构性修复推迟到另一份文档(见时间线第 6 轮)。 +**状态**: ✅ 根因已定位、结构性修复已落地(2026-08-13)。**§8 记录的"直接禁用"已被 2026-08-08 自规划改造 + Phase 2 自规划改造整体取代,不再是当前生产状态**——test_script 是否需要这个节点,现在是 Planner(`_PLANNER_NODE_CATALOG`)、Coder(`_CODER_NODE_CATALOG`)、Validator(`_VALIDATOR_NODE_CATALOG`)三个 org agent 各自在自规划阶段按 goal 自行判断的建议项之一(LLM 决定要不要规划出这个节点,不是固定必须、也不是硬编码禁用),不再有任何"这个节点类型被整体关闭"的机制——`check_script_compiles()` 机械拦截网 + `_NARRATION_RETRY_HINT` 纠偏提示对三边都生效。`_NARRATION_RETRY_HINT` 缓解代码见 §7。**2026-08 Coder 自规划场景专项复核:未复现**(时间线第 8 轮)——真实非 self-target 任务,Coder 自规划出的"先探索、再产出审计报告"内容型节点,产出是真实、具体、逐行引用证据的审计发现,不是空洞叙述;细节见第 8 轮。 + +**更新时间线**(压缩自原先六轮分散更新,2026-08-08): + +| 轮次 | 日期 | 要点 | +|---|---|---| +| 1 | 08-07 | 首次记录:Validator `validator_test_script` 节点连续两次真实生产运行输出叙述文字而非代码,`check_script_compiles()` 正确拦截,重试仍失败(详见 §1)。 | +| 2 | 08-07 | `911861fe` 任务证明 Planner 侧 `test_script` 节点同样复现——不是 Validator 专属。累计 3 个任务、6 次到达 test_script 类节点,6/6 复现。 | +| 3 | 08-07 | 根因排查(§6):是 08-06"Benchmark 叙述事故"(commit `07a0a27`)同类复发。根因不是 self-hosting 词汇污染,而是"先工具探索、再产出内容"这类节点的模型收尾话术天然是"汇报做了什么";self-hosting 只是让叙述读起来更精确的放大器,不是必要条件。 | +| 4 | 08-08 | 用户决策:`work-graph-self-planning-and-loop-back` 上线后 test_script 重新开放给 Planner 自由规划,**不等结构性修复**——复发风险已知且被接受。 | +| 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 落地时专门验证是否复现。 | +| 7 | 08-12 | **Phase 1 真实端到端验证期间三次独立复现,进一步坐实第 5 轮的判断**:验证 `org-graph-unification-phase1-review-gate-design.md` 落地效果时,`run_end_to_end()` 走完整 Planner 自规划链路,连续 3 次真实运行、3 个不同 goal,全部撞上同一类失败——(a) "add(a,b) 求和" goal:`benchmark_add` 节点 `loop_back` 耗尽 `max_attempts=2`;(b) "shout(s) 转大写" goal:`test_script_shout` 节点同样耗尽 `max_attempts=2`;(c) 接入真实生产 backend_server(重启后,见 `bd1404d` 的 FK 迁移修复)、走真实 API 创建的 "reverse_words(s)" goal(task `b4892aae-8a44-4a33-a53e-7521838baef5`):`benchmark` 节点连续两轮被 judge 拒绝("material given is a narrative summary of scenario categories and counts... rather than actual scenario content with real input/expected-output pairs"),第二轮的 judge 反馈甚至精确指出"this same narrative-summary content class was already rejected once before for this goal",`loop_back` 仍然原样复现,最终 `task.status` correctly 落到 `blocked_pending_human`(`blocked_reason='prd_consistency_check_failed'`,走的是既有 `ConsistencyCheckFailedError` 路由,不是 Phase 1 新增的 `planner_review_loop_limit_reached`——因为失败发生在 `plan_documents()` 内部,`planner_reviewer` 本身根本没被触发到)。三次复现全部跟 Phase 1 本身的改动无关(`plan_documents()` 是既有代码,未改动),但确认了第 5 轮"触发条件是'任务需不需要模型先给出可判定的具体场景',不是'节点类型是不是 test_script'"的判断——即使是"实现一个纯函数+单测"这种几乎不需要探索真实代码的极简 goal,也会被诱导成"叙述场景类别和数量的总结"而不是"具体输入输出对"。3 个新任务里,(a)(b) 是本地临时仓库,(c) 是真实生产 task(保留在 `~/.graphlemon/graphlemon.db` 里未清理,供后续复核)。 | +| 8 | 08-13 | **Coder 自规划场景专项复核(Org Graph unification Phase 2 落地后),真实非 self-target 任务,未复现**:为隔离"self-hosting 词汇污染"这个混淆变量,专门构造了一个全新的 tempfile 临时 git 仓库(不是 graphlemon 自己),种入一个真实的、带一个精心设计的浮点截断 bug 的 `token_bucket.py`(`_refill()` 用 `int(elapsed * refill_rate)` 截断小数 token 且把丢弃的小数余量算进 `_last_refill = now`,导致小数 `refill_rate` 场景系统性少发 token)。goal 明确要求"先读懂已有代码、再产出一份纯审计报告"(`docs/token_bucket_audit.md`,含设计说明/逐条问题证据/修复建议三段),是 §6.2 根因结论里"先探索、再产出内容"这个触发条件的直接构造。直接调用生产代码路径 `work_graph_plan.plan_work_graph(conn, "coder", ...)` + `work_graph_orchestrate.run_graph()`(跟 `e2e_pipeline.implement_code()` 内部调用序列完全一致,只是省略了它外层的 code_diff/test_report 包装,便于直接读每个自规划节点的原始 `content`)。**结果**:Coder 自规划出单节点图 `audit_token_bucket`(`node_type="impl"`)——即使 `_CODER_NODE_CATALOG` 的 `impl` 目录条目本身没有 `suggested_judgment`,规划 LLM 仍然自己给这个节点声明了 `judgment.kind="llm"`,`judge_prompt` 文字明确针对"narrative description of an action taken...standing in for the real content itself"(跟 `_NODE_REVIEW_JUDGE_PROMPT`/`work-graph-node-review` skill 的方法论高度一致,说明该 skill 挂载到 coder 角色后确实在影响自规划行为),外加 `on_fail: retry_self`。真实执行后(`work_node_execution` 表只有一行 `status='done'`,没有 `-reflect` 重试行,说明第一次尝试就通过了这个自加的 LLM judge):`content` 字段是"The graphlemon coder workflow finished — it audited `token_bucket.py`...`"开头(这句开场白措辞跟历史叙述事故的"The ... workflow completed"高度相似,是一个残留的文体尾巴,值得记录),但紧跟着是 5 条具体到行号(如"L21-27")、带可运行复现脚本证据(如"`refill_rate=2.5` 时理论应发 25 个 token,实测仅 20 个")、逐条给出修复建议的真实发现——不是空洞叙述冒充产出。磁盘上的真实交付物 `docs/token_bucket_audit.md`(1.5K+ 字)同样是结构完整(三段式)、逐条引用真实代码行/片段作为证据的审计报告,精确命中了预先埋入的截断 bug,另外还发现了 4 个未预设的真实问题(负数 `n` 绕过容量上限、构造函数无参数校验、非单调时钟回拨冻结刷新、公开属性无并发保护)。**结论**:这次真实非 self-target 复核里,`content` 字段的"体裁"仍有历史叙述事故的措辞痕迹(开场白),但没有复现"内容整体被叙述文字替代、真实交付物缺失"这个核心失败模式——真实交付物存在、具体、可验证;`_CODER_NODE_CATALOG` 的 `impl` 条目虽然本身缺 `suggested_judgment`,但 Phase 2c 新增的 `work-graph-node-review` skill + `_NODE_REVIEW_JUDGE_PROMPT` 的方法论已经外溢到自由规划阶段,规划 LLM 会自发给内容型节点补一道等价的反叙述 judgment,不是完全没有防线。样本量为 1 次真实运行(用户仅明确同意"跑一次"),结论置信度有限,不排除低概率仍会复现——如果后续生产真实任务里再看到这类节点复现叙述性 `content`,应重新评估要不要把 `_NODE_REVIEW_JUDGE_PROMPT` 直接接到 `impl` 目录条目的 `suggested_judgment` 上(而不是完全依赖规划 LLM 自愿声明)。复核脚本未入库(一次性验证,未固化为回归测试),本轮未修改任何生产代码。 | +| 9 | 08-13 | **用户指出 §8 的"整体禁用"框架已经不准确,要求去掉**:核实后确认 Planner(`_PLANNER_NODE_CATALOG`)、Validator(`_VALIDATOR_NODE_CATALOG`)的自规划目录里 test_script 早已是建议项(分别是 08-08 自规划改造、Phase 2c 落地时加回去的),唯独 Coder 的 `_CODER_NODE_CATALOG` 一直没有这个选项——不是"故意跳过",是 Phase 2b 当初只给了 impl/unit_test/integration_check 三项,没人再补。**修复**:给 `_CODER_NODE_CATALOG` 新增 `test_script` 建议条目(`suggested_judgment: {"kind": "mechanical", "check_ref": "check_script_compiles"}`,跟 Validator 那份同款机械拦截网),至此三个 org agent 的自规划目录里 test_script 都是可选建议项,不再有任何"这个节点类型被整体关闭"的机制。同步更新本文档顶部状态行和 §8,标注"整体禁用"状态已被取代,不再是当前生产行为(保留 §8 原文作为决策链路的真实记录)。 | + +后续讨论方向记在 `docs/done/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. 现象 + +`validate_implementation_with_own_graph()`(Validator 自己的 benchmark→test_dataset→test_script 验证图)里的 `validator_test_script` 节点,连续两次真实生产运行中都输出了**叙述性文字**而不是可执行代码,导致 `check_script_compiles()` 正确拦截(py_compile/node --check 都报语法错误),`verify_fn` 判定失败,重试一次仍然失败,最终 `ConsistencyCheckFailedError`。 + +两次真实发生,内容形态高度相似,都是"在描述某个 Work Node/workflow 的执行情况",不是在写测试脚本本身: + +- 任务 `3605272e`(2026-08-07 03:10 UTC 附近):`"Launched the 'graphlemon-node-f9cefb87af4c492aac738b9736d41e4e' workflow (Work Node validator) in the background, run ID wf_93345ca8-8c6. It's running asynchronously — I'll report results once it completes; no further action is needed right now."` +- 任务 `bfd3ee82`(2026-08-07 03:13 UTC 附近):`"The graphlemon Work Node (validator) workflow completed. It authored \`test_nav_project_context_benchmark.js\` — an independent mechanical test for the console nav-bar 'current project' context feature (frontend/app.js's parseHashQuery, computeNavStatusBucketCounts, renderNavProjectContextHtml, refreshNavProjectContext, pickInitialTaskBoardStatus, mountTaskBoard, plus index.html's #nav_project_context element and style.css's .nav-count-* rules). It loads the real app.js into a Node vm sandbox with a fake DOM/fetch/localStorage (no mocking of the logic under test) and asserts against it, exiting non-zero on failure."` +- 任务 `bfd3ee82`,**同一个任务重试后第三次复现**(2026-08-07 05:35 UTC 附近):`"It produced a runnable Node.js test script (\`nav_project_context_benchmark.js\`) validating U-047/U-048 (console nav-bar 'current project' context). It loads the real \`frontend/app.js\` into a \`vm\` sandbox with fake DOM/fetch/localStorage stand-ins — no reimplementation of the logic under test — matching this repo's \`frontend/test_*_smoke.js\` convention."` ——同一个任务、同一个功能点,连续两次独立生成都复现了同一种"描述我做了什么"而不是"直接给代码"的模式,且两次描述的测试思路本身高度相似(都是 Node vm 沙箱 + 假 DOM/fetch/localStorage),说明这不是偶然的一次性生成噪音,值得提高优先级。 +- 任务 `bfd3ee82`,**第四次复现**(2026-08-07 06:03 UTC):`"Launched the graphlemon Work Node (validator) workflow in the background (Task ID: wrux6aaat, Run ID: wf_201fcc6d-59f). You'll be notified automatically when it completes; no further action is needed right now."` ——跟 `3605272e` 第一次的措辞几乎一字不差("Launched ... workflow in the background ... You'll be notified ... no further action is needed"),进一步印证这是一种稳定的、可复现的措辞模式,不是随机生成噪音。 +- 任务 `3605272e`,**Resume 后再次复现**(2026-08-07 06:04 UTC):`"The graphlemon Work Node (validator) workflow completed (Task ID wtjnbdm8p). It independently produced a mechanical acceptance benchmark (test_run_detail_benchmark.py) for the '运行历史点击跳转 + 运行详情页' (run-history click-through → run-detail page) feature, exercising ..."` ——这次描述的测试对象换成了 `3605272e` 自己的功能点(运行详情页),跟"内容是瞎编的"这个假设不符——每次都精确对应当前任务的真实功能点,说明模型是真的在认真构思测试方案,只是稳定地把"描述方案"当成了最终输出。 + +- 任务 `911861fe`(纯后端任务——修 `supervisor/deploy_stage.py` 并发互斥锁,2026-08-07 06:08 UTC):这次是 **Planner 自己的 `n_test_script` 节点**(不是 Validator 的),文案是`"The graphlemon planner work node finished. It generated a Benchmark ..."`——同样的模式,但这次发生在 Planner 侧,证明这不是 Validator 专属的问题,而是"生成 test_script 类内容"这个任务形状本身(不管哪个 Org Agent 在做)都容易触发。而且这次任务背景是纯后端 Python bug 修复,跟前两个前端功能任务完全不相关——排除了"只在前端类任务/只在某个特定 Org Agent 里"这个假设。 + +**截至目前的复现统计**:3 个任务、累计 6 次到达 test_script 类节点(Planner 的 `test_script` + Validator 的 `validator_test_script`),6 次全部复现(100%)。不再是"可能是偶然"的阶段,也不是 Validator 专属或前端任务专属。 + +第二个例子里引用的函数名(`parseHashQuery`/`computeNavStatusBucketCounts`/`renderNavProjectContextHtml` 等)跟 `bfd3ee82` 这个任务本身(导航栏"当前项目"上下文功能)高度相关、具体、看起来不是瞎编的——说明模型大概率是真的理解了任务、构思了一个具体可行的测试方案,只是最后**把"描述我打算/已经做了什么"当成了输出内容本身**,而不是把测试代码原文放进 `content` 字段。 + +- **任务 `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 次证明了),但识别到不等于能强制拦住,下一轮又原样复现。 + +## 2. 和已修复的三个 bug 的区别(避免误认为同一件事) + +- 不是 `join()` 逐字符拆解那个 bug(那个已修复,产出的是"c; o; n; t; e; n; t"这种逐字符加分隔符的乱码,肉眼一看就是格式损坏;这次是完整、通顺、有实际内容的一段英文叙述,不是格式损坏)。 +- 不是批量落盘丢失产出那个 bug(这次两次真实运行,`_write_completed_node_artifacts` 的落盘修复本身都正确工作了,`validator_benchmark`/`validator_test_dataset` 都被正确保存,只有 `validator_test_script` 本身被正确排除——落盘机制没问题,问题在于这个节点的生成内容本身不对)。 +- 不是 API 层面的瞬时错误(529 过载、SSL 证书问题)——那些是连接都没建立成功;这次是模型正常回复了,只是回复内容的"体裁"不对。 + +## 3. 一个可能的根因方向(未验证,只是观察到的线索) + +两次叙述内容都在讲"某个 Work Node/workflow 在跑/跑完了"——这类措辞在 graphlemon 自举场景下(`target_folder_path` 是 graphlemon 自己的仓库)非常眼熟,因为 Validator 自己就活在一套"Work Node/workflow"运行时里,它的系统提示词、可用工具、上下文里大概率充斥着这类"Work Node 执行/workflow 运行"相关的表述。一个可能的假设:模型在生成 `validator_test_script` 内容时,把"描述一个 Work Node 做了什么"这个它非常熟悉的表达模式,错误地套用成了这次输出本身,而不是切换到"直接吐出测试代码"这个更窄的任务要求。 + +**这只是一个待验证的猜测**,不是结论——但现在有一点新证据支持它:三个任务全部是 self-target 任务(`target_folder_path` 是 graphlemon 自己的仓库,这三个任务本身就是 graphlemon 在修自己),不是针对其他目标项目的任务。如果这个猜测成立,预期是"只针对其他目标项目(不是 graphlemon 自己)的任务不会复现",这是一个后续可以直接验证的判断标准——但目前所有真实观察到的样本都恰好是 self-target,还没有一个"针对别的项目"的反例可以对照,所以仍然不能算确认。 + +## 4. 非目标(暂定,待调查结果出来后可能调整) + +- 不在没有更多样本/更明确根因之前贸然改 `_VALIDATOR_DOC_PROMPTS`/相关提示词——两次样本不足以定位到底是哪句话引导偏了,盲改可能没用甚至帮倒忙。 +- 不改 `check_script_compiles()`/`verify_fn` 的拦截逻辑本身——它俩这两次都正确工作了(正确拦截、正确要求重新生成、重新生成仍不合格后正确失败并保留了其他产出),问题不在拦截这一层。 + +## 5. 待确认的问题 + +- [x] 需要更多样本才能判断这是不是真的"高频"问题——**已有答案**:三次独立生成里出现了三次(`3605272e` 一次、`bfd3ee82` 重试后又出现两次,且两次描述的测试思路高度相似),不是偶然的一次性生成噪音,值得提高优先级尽快排查,不用再等更多样本。 +- [x] 如果确认是高频问题,根因排查方向:是不是 `validator_test_script` 节点的提示词本身对"describe vs. output the actual script"这件事表述不够明确?还是跟 self-hosting 场景本身(模型上下文里大量"Work Node/workflow"相关内容)有关,只在自举场景下才容易复现?——**见第 6 节,2026-08-07 根因排查结论**。 +- [ ] 要不要在 `_test_script_verify_fn` 的失败反馈里,除了编译报错本身,再显式加一句"不要描述你做了什么,直接输出脚本原文"这类更直接的纠偏提示,看能不能提高第二次重试的成功率(这是一个相对低成本、可以先试的缓解手段,不需要先confirm根因;`_test_script_verify_fn` 是 Planner/Validator 共用的同一个函数(`script_artifact_type` 参数区分两边),改一处两边都受益)。——**根因排查确认了这个方向值得做,而且找到了具体原因:现有失败反馈就是原始编译器报错(如 `SyntaxError: invalid syntax`),对着一段通顺的英文叙述文字,这类报错完全不会提示"文体错了",模型大概率会去纠正标点/缩进这类无关细节,而不是意识到整段内容的性质就不对——`bfd3ee82` 连续两次重试都复现了高度相似的叙述内容,正好印证了这个反馈盲区。尚未实现,等用户确认。** +- [x] `911861fe` 这次证明了不是 Validator 专属,但目前全部样本都是 self-target 任务——找一个真实的、非 self-target(面向其他目标项目)的任务观察一下会不会复现,能进一步验证或排除"self-hosting 场景本身"这个根因方向。——**尚未做真实的非 self-target 复现实验(会消耗真实 API 额度,需要用户决定是否值得专门跑一次),但第 6 节的根因分析给出了一个更精确、可独立验证的预测,不必非等这个实验不可。** + +## 6. 根因排查结论(2026-08-07) + +### 6.1 这不是第一次——是 8 月 6 日"Benchmark 叙述事故"的同类复发 + +`check_script_compiles()` 自己的 docstring 直接引用了 `docs/implementation-gap-analysis.md` 的"Benchmark-narration incident",commit `07a0a27`(2026-08-06,"Give Planner/Coder/Validator real, machine-checkable acceptance criteria")记录了几乎一模一样的现象,**只是发生在 Benchmark 节点**: + +> "The planner workflow completed and produced a Benchmark defining 18 scenarios..." + +当时的修复是两件事一起做的:① 给 BRD/PRD/SDD/Benchmark/test_dataset/test_script 全部 6 个文档提示词加上 `_NO_NARRATION`(本文档第 1 节引用的两处提示词,`test_script`/`validator_test_script`,**从一开始就带着这个修复**,不是缺失);② 专门给 `test_script`/`validator_test_script` 加了 `check_script_compiles()` 这个机械(非 LLM 判断)编译检查兜底,因为这是唯一能客观核实"是不是真代码"的节点类型。 + +**关键点**:test_script 类节点是当时**唯一同时拿到"提示词纠偏 + 机械拦截网"双重防护**的节点类型,而且拦截网本身完全正确工作(6/6 次都被正确拦截、正确触发重试)——但纠偏仍然 6/6 全部失败,包括重试后依然叙述。这说明问题不是"忘了加防护",而是这两层防护对这一类节点都不够。 + +### 6.2 为什么偏偏是 test_script,不是 BRD/PRD/SDD/Benchmark(这几个自 08-06 后再未复现) + +四份文档提示词(BRD/PRD/SDD/Benchmark)本质是"综合已给定的上下文,写一篇文档"——纯组合任务,不需要调用工具做探索。`test_script`/`validator_test_script` 的提示词不一样,明确要求"mechanically checks...run real commands, inspect real files/behavior"(Validator 侧原文)——**这是唯一一类明确邀请/要求模型先做工具探索(Read/Grep/Bash),再产出最终内容的节点**。 + +模型在用 Bash/Read/Grep 做了几轮探索性工具调用之后,其"收尾话术"的训练分布是"总结/汇报刚才做了什么或启动了什么",而不是"切换成纯文本吐出模式"——这是一个跟这次任务无关的、更底层的行为习惯,`_NO_NARRATION` 这句附加在提示词末尾的格式规则,不足以覆盖这种在多轮工具调用之后触发的收尾习惯。 + +self-hosting 场景(`target_folder_path` 是 graphlemon 自己)在这个机制上不是必要条件,而是**放大器**:当模型探索的对象就是 graphlemon 自己的仓库时,它读到的代码/表结构里到处都是"Work Node"/"workflow"/`wf_*`/`graphlemon-node-*` 这类词汇(`work_graph_orchestrate.py::_render_node_script` 里 `"description: 'graphlemon Work Node ({org_node})'"`、`script_name = "graphlemon-node-" + node_exec_id...` 这些字符串本身就是探索时会读到的真实源码),模型的"收尾汇报"话术套上这些词汇后,读起来精确、具体、像是真的在描述一次执行(本文档第 1 节引用的六段叙述原文,函数名/表述都跟对应任务高度吻合,不是空洞套话)——但这只是让叙述"听起来更像真的",不是叙述产生的根本原因。 + +**可独立验证的预测**(修正后,比原假设更精确):换一个不需要"先探索再产出"的文档类节点(BRD/PRD/SDD/Benchmark/test_dataset),即使是 self-target 任务也不会复现(这已经是现有观测——自 08-06 后这几类节点未见复现);换一个非 self-target 任务的 `test_script` 节点,预期仍会偶发叙述(因为触发条件是"探索后收尾"这个行为模式本身),但描述内容会更泛化、不会精确命中 graphlemon 自己的表/字段名——如果做非 self-target 复现实验,应该重点看叙述"具体程度"而不是"有没有叙述"。 + +### 6.3 现有重试反馈的具体盲区 + +`check_script_compiles()`/`_detect_script_language()` 的 `detail` 字符串本身其实已经带了一句"not a narrative description"(`"content does not compile as Python or Node/JS -- must be a real, runnable script, not a narrative description.\npython3 -m py_compile:\n...\nnode --check:\n..."`),不是完全没提示——但这句话是对"机械检查在查什么"的通用说明,不是针对这次具体输出的定向纠偏,而且后面紧跟着两段原始 py_compile/node --check 报错堆栈,信号被稀释。`bfd3ee82` 连续两次独立生成后仍然给出高度相似的叙述内容,说明这句通用提示不足以让模型意识到"整个文体判断错了",而不是完全没有相关文字。 + +## 7. 已实现(2026-08-07) + +在 `_test_script_verify_fn` 里新增 `_NARRATION_RETRY_HINT` 常量,作为 reflect feedback 列表的第二项(跟 `check_script_compiles()` 的原始 detail 一起拼进 `"; ".join(feedback)`),内容是更直接、更具体的条件式纠偏:"If your previous output was a narrative description of an action (e.g. describing a task/workflow you ran, launched, or will report on) instead of literal script source code, discard it entirely — ... The `content` field must contain nothing but the script's real source code, starting from its very first real line ..."。 + +设计上刻意用条件句("If your previous output was...")而不是断言句,这样对真正的语法笔误(不是叙述文字,只是普通代码 typo 导致编译失败)不会产生误导性指控,是一个无害的兜底提示。`_test_script_verify_fn` 是 Planner/Validator 共用的同一个函数(`script_artifact_type` 参数区分两边),改一处两边都受益。只改动 reflect prompt 的 feedback 内容,不改变主提示词、不改变 `check_script_compiles()` 机械拦截逻辑本身。 + +后续如果这个缓解仍不够(比如下次真实自举任务再次复现),下一步该看第 6.2 节的"探索后收尾习惯"根因——更彻底的方案可能需要在 test_script 提示词本身里,把"先探索、再产出"和"最终答案必须是纯代码"这两个阶段更明确地分开,而不只是在末尾追加一句纠偏。 + +## 8. 已实现(2026-08-07,第二次决策):直接禁用 test_script/validator_test_script 节点 + +**⚠️ 本节记录的"整体禁用"状态已于 2026-08-13 被自规划改造取代,不代表当前生产行为**——保留本节是为了留下决策 +链路的真实记录,不是因为这个禁用状态还生效。当时禁用的对象(`_PROMPT_TEMPLATE` 固定要求的文档链、 +`_CANONICAL_DOC_CHAIN`、`e2e_pipeline._VALIDATOR_MANIFEST` 硬编码 manifest)本身在后续的 2026-08-08 自规划改造 +(本节下方就已经提到"真正 LLM 自己决定要几个节点的能力")和 Org Graph unification Phase 2 里被整体替换掉了—— +`_CANONICAL_DOC_CHAIN`/`_VALIDATOR_MANIFEST` 现在已经不存在于生产代码路径(后者保留一份未使用的历史代码, +仅供参考)。当前状态见文档顶部状态行和第 9 节。 + +用户决策(原始记录,2026-08-07):与其继续在提示词层面打补丁(第 7 节的缓解已经证明有效性无法在下次真实复现前确认),不如先把这两个节点从图里直接去掉,后续真的有需求再重新设计,而不是"所有 goal 任务都固定有这个节点"。这也牵出一个更大的架构问题:Planner 自己的文档链(`work_graph_plan._PROMPT_TEMPLATE`)虽然过了一层真实的 LLM 规划调用,但这次调用的提示词硬编码要求"必须为 brd/prd/sdd/benchmark/test_dataset/test_script 各出一个节点"——是一个固定架构常量(2026-08-06 的既有设计决定,见 `docs/done/dynamic-work-graph-design-skill-design.md §3`),不是真的由 LLM 按 goal 自由决定节点。Validator 自己的验证链(`_VALIDATOR_MANIFEST`)更彻底,连"过一层 LLM 规划"都没有,是硬编码的 Python 字面量。真正"LLM 自己决定要几个节点"的能力已经存在于 `_GENERIC_PROMPT_TEMPLATE`(给 Coder/Validator 其他子任务用),但没有用在这两条文档链上——是否要把这条能力也用到 Planner/Validator 自己的文档链上,是一个更大的、需要单独决策的后续方向,不在本次改动范围内。 + +**具体改动**: +- `src/work_graph_plan.py::_PROMPT_TEMPLATE`:不再要求 test_script 节点(只要求 brd/prd/sdd/benchmark/test_dataset 5 个)。 +- `src/work_graph_plan.py::_CANONICAL_DOC_CHAIN`:从 6 元组缩成 5 元组,同步去掉了原本专门给 test_script 节点注入"额外依赖 benchmark"的那段代码(`_normalize_graph_manifest` 里)。 +- `src/e2e_pipeline.py::_VALIDATOR_MANIFEST`:从 3 节点(validator_benchmark→validator_test_dataset→validator_test_script)缩成 2 节点。 +- `validate_implementation_with_own_graph()`:`outputs["validator_test_script"]` 的访问加了 `in` 判断,不存在时 `script_execution` 走"N/A: 节点已禁用"的兜底分支,verdict 完全由 LLM 自己的判断决定(不再有脚本硬门禁)。 +- 下游消费者(`run_task_lifecycle`/`task_execution.py` 里读 test_script artifact 内容喂给 Coder 的逻辑)本来就已经用 `"test_script" in artifact_ids`/`_artifact_content_or_default` 做了容错(这是 2026-08-06 test_script 刚上线时,为了兼容"这个节点之前不存在的旧任务"而写的防御性代码),这次改动不需要再碰这两处。 +- `_DOC_PROMPTS["test_script"]`/`_VALIDATOR_DOC_PROMPTS["validator_test_script"]`/`check_script_compiles()`/`_test_script_verify_fn`/`_NARRATION_RETRY_HINT` 等下游代码全部原样保留,只是不会再被触发——如果以后要重新启用,只需要把这两处节点定义加回去。 + +**测试**:`src/test_work_graph_plan.py`/`src/test_e2e_pipeline.py` 里原本依赖 6/3 节点固定结构的用例做了相应调整;其中"晚失败节点不连累早节点产出"(partial-artifact-persistence 的回归用例)和"脚本真失败强制 FAIL"(hard-gate 用例)改成显式用一个临时补丁把 test_script/validator_test_script 节点加回manifest 来测试——因为这两块机制本身仍是真实代码,只是默认不会被规划出来,不应该因为默认禁用就失去回归覆盖。`python3 -m unittest discover -s src`(643 tests)+ `discover -s supervisor -p "test_*.py"`(126 tests)全绿。 diff --git a/docs/done/work-graph-self-planning-and-loop-back-requirements.md b/docs/done/work-graph-self-planning-and-loop-back-requirements.md index 3434448..d00c000 100644 --- a/docs/done/work-graph-self-planning-and-loop-back-requirements.md +++ b/docs/done/work-graph-self-planning-and-loop-back-requirements.md @@ -8,7 +8,7 @@ ### 1.1 现状:三条独立的、能力有限的机制 -- **Planner 自己的文档链**(`work_graph_plan.py::_PROMPT_TEMPLATE`):过一层真实的 LLM 规划调用,但提示词硬编码要求"必须为 brd/prd/sdd/benchmark/test_dataset 各出一个节点"(test_script 已于 2026-08-07 从这个固定清单里移除,见 `docs/todo/validator-test-script-narrative-output-issue.md`)——节点集合本质是固定的,规划这一步只是在确认依赖关系,不是真的自由决策"要不要有某个节点"。这是 2026-08-06 的一次明确设计决定(`docs/done/dynamic-work-graph-design-skill-design.md` §3 原话:"不是让 Claude 完全自由决定 Planner 要生成哪些节点——Planner 那条链是'架构常量'")。 +- **Planner 自己的文档链**(`work_graph_plan.py::_PROMPT_TEMPLATE`):过一层真实的 LLM 规划调用,但提示词硬编码要求"必须为 brd/prd/sdd/benchmark/test_dataset 各出一个节点"(test_script 已于 2026-08-07 从这个固定清单里移除,见 `docs/done/validator-test-script-narrative-output-issue.md`)——节点集合本质是固定的,规划这一步只是在确认依赖关系,不是真的自由决策"要不要有某个节点"。这是 2026-08-06 的一次明确设计决定(`docs/done/dynamic-work-graph-design-skill-design.md` §3 原话:"不是让 Claude 完全自由决定 Planner 要生成哪些节点——Planner 那条链是'架构常量'")。 - **`_GENERIC_PROMPT_TEMPLATE`**(同文件,服务 Coder/Validator 的子任务图):真正让 LLM 自由规划 `{node_id, depends_on, task_prompt}`,但目前只有 `work_graph_fork.py`(人工发起的 time-travel 调试功能)在用,没有接入任何生产路径的自动执行;且和 Planner 的固定链一样,只是一张纯 DAG,没有判断/回路概念。 - **`work_graph_orchestrate.py::run_graph()` 的验证-重试机制**:`_verify_and_maybe_regenerate()` 只支持"某节点验证失败 → 原地重新生成同一个节点一次 → 还失败就整体 `ConsistencyCheckFailedError`"。这不是回路——失败结果不会被送回更早的节点,历史反馈也只保留最近一轮(`"; ".join(feedback)` 直接拼进 reflect prompt,不会累积多轮)。 @@ -38,7 +38,7 @@ 2. **不回退到 Claude Code 原生 Dynamic Workflow 原语**(见 1.3)。继续在 Python 层的 `work_graph_orchestrate.py` 里扩展执行引擎,保住 REQ-06 断点续跑。 3. **不追求完全通用的图执行框架**。回路的触发方式限定为"某节点判断失败 → 跳回自己声明的 `on_fail.target_node_id`",不支持任意节点间的条件分支、多目标跳转、或图结构在运行时被重新规划——这些是更大的能力,不在这轮范围内。 4. **不重新设计跨 Org 的既有 loop 机制**(`org_level_loop.py`/`org_loop_route.py`:Validator FAIL/INCONCLUSIVE → 判根因 → 路由回 Planner/Coder,`loop_round` 封顶 3 轮)。本需求的回路是"同一个 Org Agent 自己的 Work Graph 内部"这一层,比跨 Org 的 loop 粒度更细,两者独立计数、不共享额度、不互相替代——内层耗尽自己的上限后照旧抛 `ConsistencyCheckFailedError`,外层机制不变地接管。 -5. **不在本需求里解决 test_script/validator_test_script 节点"叙述文字而非代码"的结构性根因**。这个节点已于 2026-08-07 因该问题禁用(`docs/todo/validator-test-script-narrative-output-issue.md`),根因是"探索后收尾习惯",跟本需求要解决的问题(节点集合是否可以自由规划、失败能否形成回路)完全正交。用户已确认(见「8. 已确认的决策」第 7 条):Part B 上线后,test_script 可以直接作为可选节点提供给 Planner 自由规划选择,不需要等这个结构性问题修复;该结构性问题的真正修复安排在另一份文档里,跟"给 Coder agent 加入 test_script 模块"一起解决——本需求只负责不阻塞它,不负责修它。 +5. **不在本需求里解决 test_script/validator_test_script 节点"叙述文字而非代码"的结构性根因**。这个节点已于 2026-08-07 因该问题禁用(`docs/done/validator-test-script-narrative-output-issue.md`),根因是"探索后收尾习惯",跟本需求要解决的问题(节点集合是否可以自由规划、失败能否形成回路)完全正交。用户已确认(见「8. 已确认的决策」第 7 条):Part B 上线后,test_script 可以直接作为可选节点提供给 Planner 自由规划选择,不需要等这个结构性问题修复;该结构性问题的真正修复安排在另一份文档里,跟"给 Coder agent 加入 test_script 模块"一起解决——本需求只负责不阻塞它,不负责修它。 6. **不修改现有 `_GENERIC_PROMPT_TEMPLATE` 路径本身**(Coder/Validator 子任务图、`work_graph_fork.py` 用的那条)。如果这轮的 judgment/on_fail 字段设计得足够通用,未来可以顺带让这条路径也受益,但不是本轮的验收范围。 ## 4. 功能需求 @@ -90,7 +90,7 @@ ## 7. 风险与已知权衡 -- **用户已知情并接受:Part B 上线后 test_script 类节点可能被自由规划回来,narrative-output 问题可能重现**。`docs/todo/validator-test-script-narrative-output-issue.md` 记录的根因是"探索后收尾习惯",跟本需求解决的问题(节点集合能不能自由规划、失败能否形成回路)完全正交,本需求不修它。用户决策(「8. 已确认的决策」第 7 条):不等结构性修复,真正的修复安排在"给 Coder agent 加入 test_script 模块"那份文档里一起解决——本需求上线后,如果自举/真实任务里又复现了叙述文字问题,这是已知的、被接受的风险,不是本需求的回归。 +- **用户已知情并接受:Part B 上线后 test_script 类节点可能被自由规划回来,narrative-output 问题可能重现**。`docs/done/validator-test-script-narrative-output-issue.md` 记录的根因是"探索后收尾习惯",跟本需求解决的问题(节点集合能不能自由规划、失败能否形成回路)完全正交,本需求不修它。用户决策(「8. 已确认的决策」第 7 条):不等结构性修复,真正的修复安排在"给 Coder agent 加入 test_script 模块"那份文档里一起解决——本需求上线后,如果自举/真实任务里又复现了叙述文字问题,这是已知的、被接受的风险,不是本需求的回归。 - **回路能力本身可能被滥用成隐藏的高成本重试**:一个设计不当的 `judgment`(例如过于严格的 LLM 判断标准)配合 `loop_back`,可能导致某个节点反复被打回、消耗大量 token 才勉强通过上限。FR-A5 的按 `(org_node, node 类型)` 可配置上限缓解了"一刀切"的问题,但初始默认值仍需要设计阶段给出具体理由,不能只是"抄一个数字";上线后应该根据真实调用数据回头调整(这正是选择"DB 配置表"而不是"硬编码常量"的原因)。 - **Part B 是对已有架构决定的正式推翻**,不是渐进式增强。`docs/done/dynamic-work-graph-design-skill-design.md` §3 需要在 Part B 实施后同步更新,避免文档和代码再次出现本 session 里多次遇到的"文档记录的意图与实际代码不一致"的问题。 - **Part A + Part B 一并实施(已确认,见「8. 已确认的决策」第 5 条),没有中间的"仅 Part A 先跑一段时间验证稳定性"的缓冲阶段**——意味着 Part A 的回路引擎第一次被真实使用,就是在"节点集合本身也是自由规划、不受固定链条约束"这个更不可预测的场景下,两个新变量同时引入,出问题时定位"是回路引擎的 bug 还是自由规划出了一个奇怪的图结构"会比分阶段更难。这是用户已知情的权衡,不是本需求遗漏。 diff --git a/docs/todo/validator-test-script-narrative-output-issue.md b/docs/todo/validator-test-script-narrative-output-issue.md deleted file mode 100644 index b2e1534..0000000 --- a/docs/todo/validator-test-script-narrative-output-issue.md +++ /dev/null @@ -1,112 +0,0 @@ -# test_script 节点偶发输出叙述文字而非代码——问题记录(Planner + Validator 都会中招) - -**日期**: 2026-08-07(最后更新 2026-08-13) -**性质**: 问题记录——源于真实生产复现,根因已定位(§6),已落地两轮缓解(§7/§8),结构性修复推迟到另一份文档(见时间线第 6 轮)。 -**状态**: 根因已定位。当前生产状态:test_script/validator_test_script 节点已禁用(§8),`_NARRATION_RETRY_HINT` 缓解代码保留但当前不会触发(§7)。**结构性修复不在本文档范围**——已并入 `docs/done/org-graph-unification-and-universal-self-planning-requirements.md` Phase 2(FR-2/FR-3,Coder 接入自规划时一并处理,该文档非目标 §5.2 记录了这条已知复发风险)。**2026-08 Coder 自规划场景专项复核:未复现**(时间线第 8 轮)——真实非 self-target 任务,Coder 自规划出的"先探索、再产出审计报告"内容型节点,产出是真实、具体、逐行引用证据的审计发现,不是空洞叙述;细节见第 8 轮。 - -**更新时间线**(压缩自原先六轮分散更新,2026-08-08): - -| 轮次 | 日期 | 要点 | -|---|---|---| -| 1 | 08-07 | 首次记录:Validator `validator_test_script` 节点连续两次真实生产运行输出叙述文字而非代码,`check_script_compiles()` 正确拦截,重试仍失败(详见 §1)。 | -| 2 | 08-07 | `911861fe` 任务证明 Planner 侧 `test_script` 节点同样复现——不是 Validator 专属。累计 3 个任务、6 次到达 test_script 类节点,6/6 复现。 | -| 3 | 08-07 | 根因排查(§6):是 08-06"Benchmark 叙述事故"(commit `07a0a27`)同类复发。根因不是 self-hosting 词汇污染,而是"先工具探索、再产出内容"这类节点的模型收尾话术天然是"汇报做了什么";self-hosting 只是让叙述读起来更精确的放大器,不是必要条件。 | -| 4 | 08-08 | 用户决策:`work-graph-self-planning-and-loop-back` 上线后 test_script 重新开放给 Planner 自由规划,**不等结构性修复**——复发风险已知且被接受。 | -| 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 落地时专门验证是否复现。 | -| 7 | 08-12 | **Phase 1 真实端到端验证期间三次独立复现,进一步坐实第 5 轮的判断**:验证 `org-graph-unification-phase1-review-gate-design.md` 落地效果时,`run_end_to_end()` 走完整 Planner 自规划链路,连续 3 次真实运行、3 个不同 goal,全部撞上同一类失败——(a) "add(a,b) 求和" goal:`benchmark_add` 节点 `loop_back` 耗尽 `max_attempts=2`;(b) "shout(s) 转大写" goal:`test_script_shout` 节点同样耗尽 `max_attempts=2`;(c) 接入真实生产 backend_server(重启后,见 `bd1404d` 的 FK 迁移修复)、走真实 API 创建的 "reverse_words(s)" goal(task `b4892aae-8a44-4a33-a53e-7521838baef5`):`benchmark` 节点连续两轮被 judge 拒绝("material given is a narrative summary of scenario categories and counts... rather than actual scenario content with real input/expected-output pairs"),第二轮的 judge 反馈甚至精确指出"this same narrative-summary content class was already rejected once before for this goal",`loop_back` 仍然原样复现,最终 `task.status` correctly 落到 `blocked_pending_human`(`blocked_reason='prd_consistency_check_failed'`,走的是既有 `ConsistencyCheckFailedError` 路由,不是 Phase 1 新增的 `planner_review_loop_limit_reached`——因为失败发生在 `plan_documents()` 内部,`planner_reviewer` 本身根本没被触发到)。三次复现全部跟 Phase 1 本身的改动无关(`plan_documents()` 是既有代码,未改动),但确认了第 5 轮"触发条件是'任务需不需要模型先给出可判定的具体场景',不是'节点类型是不是 test_script'"的判断——即使是"实现一个纯函数+单测"这种几乎不需要探索真实代码的极简 goal,也会被诱导成"叙述场景类别和数量的总结"而不是"具体输入输出对"。3 个新任务里,(a)(b) 是本地临时仓库,(c) 是真实生产 task(保留在 `~/.graphlemon/graphlemon.db` 里未清理,供后续复核)。 | -| 8 | 08-13 | **Coder 自规划场景专项复核(Org Graph unification Phase 2 落地后),真实非 self-target 任务,未复现**:为隔离"self-hosting 词汇污染"这个混淆变量,专门构造了一个全新的 tempfile 临时 git 仓库(不是 graphlemon 自己),种入一个真实的、带一个精心设计的浮点截断 bug 的 `token_bucket.py`(`_refill()` 用 `int(elapsed * refill_rate)` 截断小数 token 且把丢弃的小数余量算进 `_last_refill = now`,导致小数 `refill_rate` 场景系统性少发 token)。goal 明确要求"先读懂已有代码、再产出一份纯审计报告"(`docs/token_bucket_audit.md`,含设计说明/逐条问题证据/修复建议三段),是 §6.2 根因结论里"先探索、再产出内容"这个触发条件的直接构造。直接调用生产代码路径 `work_graph_plan.plan_work_graph(conn, "coder", ...)` + `work_graph_orchestrate.run_graph()`(跟 `e2e_pipeline.implement_code()` 内部调用序列完全一致,只是省略了它外层的 code_diff/test_report 包装,便于直接读每个自规划节点的原始 `content`)。**结果**:Coder 自规划出单节点图 `audit_token_bucket`(`node_type="impl"`)——即使 `_CODER_NODE_CATALOG` 的 `impl` 目录条目本身没有 `suggested_judgment`,规划 LLM 仍然自己给这个节点声明了 `judgment.kind="llm"`,`judge_prompt` 文字明确针对"narrative description of an action taken...standing in for the real content itself"(跟 `_NODE_REVIEW_JUDGE_PROMPT`/`work-graph-node-review` skill 的方法论高度一致,说明该 skill 挂载到 coder 角色后确实在影响自规划行为),外加 `on_fail: retry_self`。真实执行后(`work_node_execution` 表只有一行 `status='done'`,没有 `-reflect` 重试行,说明第一次尝试就通过了这个自加的 LLM judge):`content` 字段是"The graphlemon coder workflow finished — it audited `token_bucket.py`...`"开头(这句开场白措辞跟历史叙述事故的"The ... workflow completed"高度相似,是一个残留的文体尾巴,值得记录),但紧跟着是 5 条具体到行号(如"L21-27")、带可运行复现脚本证据(如"`refill_rate=2.5` 时理论应发 25 个 token,实测仅 20 个")、逐条给出修复建议的真实发现——不是空洞叙述冒充产出。磁盘上的真实交付物 `docs/token_bucket_audit.md`(1.5K+ 字)同样是结构完整(三段式)、逐条引用真实代码行/片段作为证据的审计报告,精确命中了预先埋入的截断 bug,另外还发现了 4 个未预设的真实问题(负数 `n` 绕过容量上限、构造函数无参数校验、非单调时钟回拨冻结刷新、公开属性无并发保护)。**结论**:这次真实非 self-target 复核里,`content` 字段的"体裁"仍有历史叙述事故的措辞痕迹(开场白),但没有复现"内容整体被叙述文字替代、真实交付物缺失"这个核心失败模式——真实交付物存在、具体、可验证;`_CODER_NODE_CATALOG` 的 `impl` 条目虽然本身缺 `suggested_judgment`,但 Phase 2c 新增的 `work-graph-node-review` skill + `_NODE_REVIEW_JUDGE_PROMPT` 的方法论已经外溢到自由规划阶段,规划 LLM 会自发给内容型节点补一道等价的反叙述 judgment,不是完全没有防线。样本量为 1 次真实运行(用户仅明确同意"跑一次"),结论置信度有限,不排除低概率仍会复现——如果后续生产真实任务里再看到这类节点复现叙述性 `content`,应重新评估要不要把 `_NODE_REVIEW_JUDGE_PROMPT` 直接接到 `impl` 目录条目的 `suggested_judgment` 上(而不是完全依赖规划 LLM 自愿声明)。复核脚本未入库(一次性验证,未固化为回归测试),本轮未修改任何生产代码。 | - -后续讨论方向记在 `docs/done/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. 现象 - -`validate_implementation_with_own_graph()`(Validator 自己的 benchmark→test_dataset→test_script 验证图)里的 `validator_test_script` 节点,连续两次真实生产运行中都输出了**叙述性文字**而不是可执行代码,导致 `check_script_compiles()` 正确拦截(py_compile/node --check 都报语法错误),`verify_fn` 判定失败,重试一次仍然失败,最终 `ConsistencyCheckFailedError`。 - -两次真实发生,内容形态高度相似,都是"在描述某个 Work Node/workflow 的执行情况",不是在写测试脚本本身: - -- 任务 `3605272e`(2026-08-07 03:10 UTC 附近):`"Launched the 'graphlemon-node-f9cefb87af4c492aac738b9736d41e4e' workflow (Work Node validator) in the background, run ID wf_93345ca8-8c6. It's running asynchronously — I'll report results once it completes; no further action is needed right now."` -- 任务 `bfd3ee82`(2026-08-07 03:13 UTC 附近):`"The graphlemon Work Node (validator) workflow completed. It authored \`test_nav_project_context_benchmark.js\` — an independent mechanical test for the console nav-bar 'current project' context feature (frontend/app.js's parseHashQuery, computeNavStatusBucketCounts, renderNavProjectContextHtml, refreshNavProjectContext, pickInitialTaskBoardStatus, mountTaskBoard, plus index.html's #nav_project_context element and style.css's .nav-count-* rules). It loads the real app.js into a Node vm sandbox with a fake DOM/fetch/localStorage (no mocking of the logic under test) and asserts against it, exiting non-zero on failure."` -- 任务 `bfd3ee82`,**同一个任务重试后第三次复现**(2026-08-07 05:35 UTC 附近):`"It produced a runnable Node.js test script (\`nav_project_context_benchmark.js\`) validating U-047/U-048 (console nav-bar 'current project' context). It loads the real \`frontend/app.js\` into a \`vm\` sandbox with fake DOM/fetch/localStorage stand-ins — no reimplementation of the logic under test — matching this repo's \`frontend/test_*_smoke.js\` convention."` ——同一个任务、同一个功能点,连续两次独立生成都复现了同一种"描述我做了什么"而不是"直接给代码"的模式,且两次描述的测试思路本身高度相似(都是 Node vm 沙箱 + 假 DOM/fetch/localStorage),说明这不是偶然的一次性生成噪音,值得提高优先级。 -- 任务 `bfd3ee82`,**第四次复现**(2026-08-07 06:03 UTC):`"Launched the graphlemon Work Node (validator) workflow in the background (Task ID: wrux6aaat, Run ID: wf_201fcc6d-59f). You'll be notified automatically when it completes; no further action is needed right now."` ——跟 `3605272e` 第一次的措辞几乎一字不差("Launched ... workflow in the background ... You'll be notified ... no further action is needed"),进一步印证这是一种稳定的、可复现的措辞模式,不是随机生成噪音。 -- 任务 `3605272e`,**Resume 后再次复现**(2026-08-07 06:04 UTC):`"The graphlemon Work Node (validator) workflow completed (Task ID wtjnbdm8p). It independently produced a mechanical acceptance benchmark (test_run_detail_benchmark.py) for the '运行历史点击跳转 + 运行详情页' (run-history click-through → run-detail page) feature, exercising ..."` ——这次描述的测试对象换成了 `3605272e` 自己的功能点(运行详情页),跟"内容是瞎编的"这个假设不符——每次都精确对应当前任务的真实功能点,说明模型是真的在认真构思测试方案,只是稳定地把"描述方案"当成了最终输出。 - -- 任务 `911861fe`(纯后端任务——修 `supervisor/deploy_stage.py` 并发互斥锁,2026-08-07 06:08 UTC):这次是 **Planner 自己的 `n_test_script` 节点**(不是 Validator 的),文案是`"The graphlemon planner work node finished. It generated a Benchmark ..."`——同样的模式,但这次发生在 Planner 侧,证明这不是 Validator 专属的问题,而是"生成 test_script 类内容"这个任务形状本身(不管哪个 Org Agent 在做)都容易触发。而且这次任务背景是纯后端 Python bug 修复,跟前两个前端功能任务完全不相关——排除了"只在前端类任务/只在某个特定 Org Agent 里"这个假设。 - -**截至目前的复现统计**:3 个任务、累计 6 次到达 test_script 类节点(Planner 的 `test_script` + Validator 的 `validator_test_script`),6 次全部复现(100%)。不再是"可能是偶然"的阶段,也不是 Validator 专属或前端任务专属。 - -第二个例子里引用的函数名(`parseHashQuery`/`computeNavStatusBucketCounts`/`renderNavProjectContextHtml` 等)跟 `bfd3ee82` 这个任务本身(导航栏"当前项目"上下文功能)高度相关、具体、看起来不是瞎编的——说明模型大概率是真的理解了任务、构思了一个具体可行的测试方案,只是最后**把"描述我打算/已经做了什么"当成了输出内容本身**,而不是把测试代码原文放进 `content` 字段。 - -- **任务 `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 次证明了),但识别到不等于能强制拦住,下一轮又原样复现。 - -## 2. 和已修复的三个 bug 的区别(避免误认为同一件事) - -- 不是 `join()` 逐字符拆解那个 bug(那个已修复,产出的是"c; o; n; t; e; n; t"这种逐字符加分隔符的乱码,肉眼一看就是格式损坏;这次是完整、通顺、有实际内容的一段英文叙述,不是格式损坏)。 -- 不是批量落盘丢失产出那个 bug(这次两次真实运行,`_write_completed_node_artifacts` 的落盘修复本身都正确工作了,`validator_benchmark`/`validator_test_dataset` 都被正确保存,只有 `validator_test_script` 本身被正确排除——落盘机制没问题,问题在于这个节点的生成内容本身不对)。 -- 不是 API 层面的瞬时错误(529 过载、SSL 证书问题)——那些是连接都没建立成功;这次是模型正常回复了,只是回复内容的"体裁"不对。 - -## 3. 一个可能的根因方向(未验证,只是观察到的线索) - -两次叙述内容都在讲"某个 Work Node/workflow 在跑/跑完了"——这类措辞在 graphlemon 自举场景下(`target_folder_path` 是 graphlemon 自己的仓库)非常眼熟,因为 Validator 自己就活在一套"Work Node/workflow"运行时里,它的系统提示词、可用工具、上下文里大概率充斥着这类"Work Node 执行/workflow 运行"相关的表述。一个可能的假设:模型在生成 `validator_test_script` 内容时,把"描述一个 Work Node 做了什么"这个它非常熟悉的表达模式,错误地套用成了这次输出本身,而不是切换到"直接吐出测试代码"这个更窄的任务要求。 - -**这只是一个待验证的猜测**,不是结论——但现在有一点新证据支持它:三个任务全部是 self-target 任务(`target_folder_path` 是 graphlemon 自己的仓库,这三个任务本身就是 graphlemon 在修自己),不是针对其他目标项目的任务。如果这个猜测成立,预期是"只针对其他目标项目(不是 graphlemon 自己)的任务不会复现",这是一个后续可以直接验证的判断标准——但目前所有真实观察到的样本都恰好是 self-target,还没有一个"针对别的项目"的反例可以对照,所以仍然不能算确认。 - -## 4. 非目标(暂定,待调查结果出来后可能调整) - -- 不在没有更多样本/更明确根因之前贸然改 `_VALIDATOR_DOC_PROMPTS`/相关提示词——两次样本不足以定位到底是哪句话引导偏了,盲改可能没用甚至帮倒忙。 -- 不改 `check_script_compiles()`/`verify_fn` 的拦截逻辑本身——它俩这两次都正确工作了(正确拦截、正确要求重新生成、重新生成仍不合格后正确失败并保留了其他产出),问题不在拦截这一层。 - -## 5. 待确认的问题 - -- [x] 需要更多样本才能判断这是不是真的"高频"问题——**已有答案**:三次独立生成里出现了三次(`3605272e` 一次、`bfd3ee82` 重试后又出现两次,且两次描述的测试思路高度相似),不是偶然的一次性生成噪音,值得提高优先级尽快排查,不用再等更多样本。 -- [x] 如果确认是高频问题,根因排查方向:是不是 `validator_test_script` 节点的提示词本身对"describe vs. output the actual script"这件事表述不够明确?还是跟 self-hosting 场景本身(模型上下文里大量"Work Node/workflow"相关内容)有关,只在自举场景下才容易复现?——**见第 6 节,2026-08-07 根因排查结论**。 -- [ ] 要不要在 `_test_script_verify_fn` 的失败反馈里,除了编译报错本身,再显式加一句"不要描述你做了什么,直接输出脚本原文"这类更直接的纠偏提示,看能不能提高第二次重试的成功率(这是一个相对低成本、可以先试的缓解手段,不需要先confirm根因;`_test_script_verify_fn` 是 Planner/Validator 共用的同一个函数(`script_artifact_type` 参数区分两边),改一处两边都受益)。——**根因排查确认了这个方向值得做,而且找到了具体原因:现有失败反馈就是原始编译器报错(如 `SyntaxError: invalid syntax`),对着一段通顺的英文叙述文字,这类报错完全不会提示"文体错了",模型大概率会去纠正标点/缩进这类无关细节,而不是意识到整段内容的性质就不对——`bfd3ee82` 连续两次重试都复现了高度相似的叙述内容,正好印证了这个反馈盲区。尚未实现,等用户确认。** -- [x] `911861fe` 这次证明了不是 Validator 专属,但目前全部样本都是 self-target 任务——找一个真实的、非 self-target(面向其他目标项目)的任务观察一下会不会复现,能进一步验证或排除"self-hosting 场景本身"这个根因方向。——**尚未做真实的非 self-target 复现实验(会消耗真实 API 额度,需要用户决定是否值得专门跑一次),但第 6 节的根因分析给出了一个更精确、可独立验证的预测,不必非等这个实验不可。** - -## 6. 根因排查结论(2026-08-07) - -### 6.1 这不是第一次——是 8 月 6 日"Benchmark 叙述事故"的同类复发 - -`check_script_compiles()` 自己的 docstring 直接引用了 `docs/implementation-gap-analysis.md` 的"Benchmark-narration incident",commit `07a0a27`(2026-08-06,"Give Planner/Coder/Validator real, machine-checkable acceptance criteria")记录了几乎一模一样的现象,**只是发生在 Benchmark 节点**: - -> "The planner workflow completed and produced a Benchmark defining 18 scenarios..." - -当时的修复是两件事一起做的:① 给 BRD/PRD/SDD/Benchmark/test_dataset/test_script 全部 6 个文档提示词加上 `_NO_NARRATION`(本文档第 1 节引用的两处提示词,`test_script`/`validator_test_script`,**从一开始就带着这个修复**,不是缺失);② 专门给 `test_script`/`validator_test_script` 加了 `check_script_compiles()` 这个机械(非 LLM 判断)编译检查兜底,因为这是唯一能客观核实"是不是真代码"的节点类型。 - -**关键点**:test_script 类节点是当时**唯一同时拿到"提示词纠偏 + 机械拦截网"双重防护**的节点类型,而且拦截网本身完全正确工作(6/6 次都被正确拦截、正确触发重试)——但纠偏仍然 6/6 全部失败,包括重试后依然叙述。这说明问题不是"忘了加防护",而是这两层防护对这一类节点都不够。 - -### 6.2 为什么偏偏是 test_script,不是 BRD/PRD/SDD/Benchmark(这几个自 08-06 后再未复现) - -四份文档提示词(BRD/PRD/SDD/Benchmark)本质是"综合已给定的上下文,写一篇文档"——纯组合任务,不需要调用工具做探索。`test_script`/`validator_test_script` 的提示词不一样,明确要求"mechanically checks...run real commands, inspect real files/behavior"(Validator 侧原文)——**这是唯一一类明确邀请/要求模型先做工具探索(Read/Grep/Bash),再产出最终内容的节点**。 - -模型在用 Bash/Read/Grep 做了几轮探索性工具调用之后,其"收尾话术"的训练分布是"总结/汇报刚才做了什么或启动了什么",而不是"切换成纯文本吐出模式"——这是一个跟这次任务无关的、更底层的行为习惯,`_NO_NARRATION` 这句附加在提示词末尾的格式规则,不足以覆盖这种在多轮工具调用之后触发的收尾习惯。 - -self-hosting 场景(`target_folder_path` 是 graphlemon 自己)在这个机制上不是必要条件,而是**放大器**:当模型探索的对象就是 graphlemon 自己的仓库时,它读到的代码/表结构里到处都是"Work Node"/"workflow"/`wf_*`/`graphlemon-node-*` 这类词汇(`work_graph_orchestrate.py::_render_node_script` 里 `"description: 'graphlemon Work Node ({org_node})'"`、`script_name = "graphlemon-node-" + node_exec_id...` 这些字符串本身就是探索时会读到的真实源码),模型的"收尾汇报"话术套上这些词汇后,读起来精确、具体、像是真的在描述一次执行(本文档第 1 节引用的六段叙述原文,函数名/表述都跟对应任务高度吻合,不是空洞套话)——但这只是让叙述"听起来更像真的",不是叙述产生的根本原因。 - -**可独立验证的预测**(修正后,比原假设更精确):换一个不需要"先探索再产出"的文档类节点(BRD/PRD/SDD/Benchmark/test_dataset),即使是 self-target 任务也不会复现(这已经是现有观测——自 08-06 后这几类节点未见复现);换一个非 self-target 任务的 `test_script` 节点,预期仍会偶发叙述(因为触发条件是"探索后收尾"这个行为模式本身),但描述内容会更泛化、不会精确命中 graphlemon 自己的表/字段名——如果做非 self-target 复现实验,应该重点看叙述"具体程度"而不是"有没有叙述"。 - -### 6.3 现有重试反馈的具体盲区 - -`check_script_compiles()`/`_detect_script_language()` 的 `detail` 字符串本身其实已经带了一句"not a narrative description"(`"content does not compile as Python or Node/JS -- must be a real, runnable script, not a narrative description.\npython3 -m py_compile:\n...\nnode --check:\n..."`),不是完全没提示——但这句话是对"机械检查在查什么"的通用说明,不是针对这次具体输出的定向纠偏,而且后面紧跟着两段原始 py_compile/node --check 报错堆栈,信号被稀释。`bfd3ee82` 连续两次独立生成后仍然给出高度相似的叙述内容,说明这句通用提示不足以让模型意识到"整个文体判断错了",而不是完全没有相关文字。 - -## 7. 已实现(2026-08-07) - -在 `_test_script_verify_fn` 里新增 `_NARRATION_RETRY_HINT` 常量,作为 reflect feedback 列表的第二项(跟 `check_script_compiles()` 的原始 detail 一起拼进 `"; ".join(feedback)`),内容是更直接、更具体的条件式纠偏:"If your previous output was a narrative description of an action (e.g. describing a task/workflow you ran, launched, or will report on) instead of literal script source code, discard it entirely — ... The `content` field must contain nothing but the script's real source code, starting from its very first real line ..."。 - -设计上刻意用条件句("If your previous output was...")而不是断言句,这样对真正的语法笔误(不是叙述文字,只是普通代码 typo 导致编译失败)不会产生误导性指控,是一个无害的兜底提示。`_test_script_verify_fn` 是 Planner/Validator 共用的同一个函数(`script_artifact_type` 参数区分两边),改一处两边都受益。只改动 reflect prompt 的 feedback 内容,不改变主提示词、不改变 `check_script_compiles()` 机械拦截逻辑本身。 - -后续如果这个缓解仍不够(比如下次真实自举任务再次复现),下一步该看第 6.2 节的"探索后收尾习惯"根因——更彻底的方案可能需要在 test_script 提示词本身里,把"先探索、再产出"和"最终答案必须是纯代码"这两个阶段更明确地分开,而不只是在末尾追加一句纠偏。 - -## 8. 已实现(2026-08-07,第二次决策):直接禁用 test_script/validator_test_script 节点 - -用户决策:与其继续在提示词层面打补丁(第 7 节的缓解已经证明有效性无法在下次真实复现前确认),不如先把这两个节点从图里直接去掉,后续真的有需求再重新设计,而不是"所有 goal 任务都固定有这个节点"。这也牵出一个更大的架构问题:Planner 自己的文档链(`work_graph_plan._PROMPT_TEMPLATE`)虽然过了一层真实的 LLM 规划调用,但这次调用的提示词硬编码要求"必须为 brd/prd/sdd/benchmark/test_dataset/test_script 各出一个节点"——是一个固定架构常量(2026-08-06 的既有设计决定,见 `docs/done/dynamic-work-graph-design-skill-design.md §3`),不是真的由 LLM 按 goal 自由决定节点。Validator 自己的验证链(`_VALIDATOR_MANIFEST`)更彻底,连"过一层 LLM 规划"都没有,是硬编码的 Python 字面量。真正"LLM 自己决定要几个节点"的能力已经存在于 `_GENERIC_PROMPT_TEMPLATE`(给 Coder/Validator 其他子任务用),但没有用在这两条文档链上——是否要把这条能力也用到 Planner/Validator 自己的文档链上,是一个更大的、需要单独决策的后续方向,不在本次改动范围内。 - -**具体改动**: -- `src/work_graph_plan.py::_PROMPT_TEMPLATE`:不再要求 test_script 节点(只要求 brd/prd/sdd/benchmark/test_dataset 5 个)。 -- `src/work_graph_plan.py::_CANONICAL_DOC_CHAIN`:从 6 元组缩成 5 元组,同步去掉了原本专门给 test_script 节点注入"额外依赖 benchmark"的那段代码(`_normalize_graph_manifest` 里)。 -- `src/e2e_pipeline.py::_VALIDATOR_MANIFEST`:从 3 节点(validator_benchmark→validator_test_dataset→validator_test_script)缩成 2 节点。 -- `validate_implementation_with_own_graph()`:`outputs["validator_test_script"]` 的访问加了 `in` 判断,不存在时 `script_execution` 走"N/A: 节点已禁用"的兜底分支,verdict 完全由 LLM 自己的判断决定(不再有脚本硬门禁)。 -- 下游消费者(`run_task_lifecycle`/`task_execution.py` 里读 test_script artifact 内容喂给 Coder 的逻辑)本来就已经用 `"test_script" in artifact_ids`/`_artifact_content_or_default` 做了容错(这是 2026-08-06 test_script 刚上线时,为了兼容"这个节点之前不存在的旧任务"而写的防御性代码),这次改动不需要再碰这两处。 -- `_DOC_PROMPTS["test_script"]`/`_VALIDATOR_DOC_PROMPTS["validator_test_script"]`/`check_script_compiles()`/`_test_script_verify_fn`/`_NARRATION_RETRY_HINT` 等下游代码全部原样保留,只是不会再被触发——如果以后要重新启用,只需要把这两处节点定义加回去。 - -**测试**:`src/test_work_graph_plan.py`/`src/test_e2e_pipeline.py` 里原本依赖 6/3 节点固定结构的用例做了相应调整;其中"晚失败节点不连累早节点产出"(partial-artifact-persistence 的回归用例)和"脚本真失败强制 FAIL"(hard-gate 用例)改成显式用一个临时补丁把 test_script/validator_test_script 节点加回manifest 来测试——因为这两块机制本身仍是真实代码,只是默认不会被规划出来,不应该因为默认禁用就失去回归覆盖。`python3 -m unittest discover -s src`(643 tests)+ `discover -s supervisor -p "test_*.py"`(126 tests)全绿。 diff --git a/skills/planner-output-review/SKILL.md b/skills/planner-output-review/SKILL.md index efb7fe1..1ed3809 100644 --- a/skills/planner-output-review/SKILL.md +++ b/skills/planner-output-review/SKILL.md @@ -21,7 +21,7 @@ tags: ## 判断对象一:是否为真实结构化内容,而不是叙述文字冒充产出 -**真实反面案例**(`docs/todo/validator-test-script-narrative-output-issue.md` 记录的实际复现,同样的措辞模式在多个任务、多次独立触发中稳定重现): +**真实反面案例**(`docs/done/validator-test-script-narrative-output-issue.md` 记录的实际复现,同样的措辞模式在多个任务、多次独立触发中稳定重现): - `"Launched the 'graphlemon-node-...' workflow (Work Node validator) in the background, run ID wf_.... It's running asynchronously — I'll report results once it completes; no further action is needed right now."` - `"The graphlemon Work Node (validator) workflow completed. It authored \`test_....js\` — an independent mechanical test for ...`(描述"我做了什么",不是文档/脚本本身的内容) diff --git a/skills/skill-defect-remediation/SKILL.md b/skills/skill-defect-remediation/SKILL.md index d31a063..b993819 100644 --- a/skills/skill-defect-remediation/SKILL.md +++ b/skills/skill-defect-remediation/SKILL.md @@ -28,7 +28,7 @@ tags: ## 一个真实的参考案例:叙述文字冒充结构化产出 -这个代码库真实反复复现过的一类系统性 Skill 缺陷(`docs/todo/validator-test-script-narrative-output-issue.md` §6 完整根因记录):要求"先用工具探索、再产出最终内容"的节点(比如需要先读真实代码才能写 SDD/Benchmark 的场景),模型探索完之后的收尾话术天然倾向"汇报我刚才做了/启动了什么",而不是切换成"直接吐出最终内容"模式——即使 Skill 提示词已经写了"不要写叙述文字",这句话附加在提示词末尾的格式规则,不足以覆盖"多轮工具调用之后的收尾习惯"这个更底层的行为模式。 +这个代码库真实反复复现过的一类系统性 Skill 缺陷(`docs/done/validator-test-script-narrative-output-issue.md` §6 完整根因记录):要求"先用工具探索、再产出最终内容"的节点(比如需要先读真实代码才能写 SDD/Benchmark 的场景),模型探索完之后的收尾话术天然倾向"汇报我刚才做了/启动了什么",而不是切换成"直接吐出最终内容"模式——即使 Skill 提示词已经写了"不要写叙述文字",这句话附加在提示词末尾的格式规则,不足以覆盖"多轮工具调用之后的收尾习惯"这个更底层的行为模式。 如果这次要修的 Skill 也有类似"先探索、再产出"的结构,而失败症状也是"产出的是进度汇报式散文,不是结构化内容",可以直接参考这个案例的诊断方式,但**不要机械照抄"加一句不要写叙述文字"这种已经被证明不够的修法**——真正管用的方向是把"探索阶段"和"最终作答阶段"在提示词结构上明确分开(比如要求模型先完成探索、再有一个清晰的"现在开始,你的下一段输出必须且只能是最终内容本身"的转折),而不是在末尾追加一句格式提醒。 diff --git a/skills/work-graph-node-review/SKILL.md b/skills/work-graph-node-review/SKILL.md index 9e73647..9c68076 100644 --- a/skills/work-graph-node-review/SKILL.md +++ b/skills/work-graph-node-review/SKILL.md @@ -30,7 +30,7 @@ Coder 开始之前一次性把关);我也不判断"整个任务是否验收通è¿ ## 判断标准:是不是叙述文字冒充产出 -**真实反复复现过的一类系统性失败**(`docs/todo/validator-test-script-narrative-output-issue.md` +**真实反复复现过的一类系统性失败**(`docs/done/validator-test-script-narrative-output-issue.md` §6 的完整根因记录):要求"先用工具探索、再产出最终内容"的节点,模型探索完之后的收尾话术天然 倾向"汇报我刚才做了/启动了什么",而不是切换成"直接吐出最终内容"模式——即使节点自己的提示词 已经写了"不要写叙述文字",这句话作为附加在提示词末尾的格式规则,不足以覆盖"多轮工具调用之后 diff --git a/src/e2e_pipeline.py b/src/e2e_pipeline.py index d58ef16..41b0be6 100644 --- a/src/e2e_pipeline.py +++ b/src/e2e_pipeline.py @@ -475,7 +475,7 @@ def _test_script_verify_fn(node_artifact_types, script_artifact_type="test_scrip parameterized so this same function backs both the Planner's 'test_script' node and the Validator's own 'validator_test_script' node. - 2026-08-07 (docs/todo/validator-test-script-narrative-output-issue.md): + 2026-08-07 (docs/done/validator-test-script-narrative-output-issue.md): `check_script_compiles`'s own detail string already says "not a narrative description", but that's a generic explanation of what the mechanical check looks for, buried ahead of two raw compiler @@ -696,7 +696,7 @@ def plan_documents(conn, task_id, goal_text, target_folder_path, timeout=300): return {"run_id": run_id, "artifact_ids": artifact_ids} -# ponytail: 子串匹配,从 docs/todo/validator-test-script-narrative-output-issue.md +# ponytail: 子串匹配,从 docs/done/validator-test-script-narrative-output-issue.md # 记录的真实复现文案里提炼——"以为在描述自己做了什么"的稳定措辞模式。只作为喂给 # planner_reviewer 的提示信号,不是硬性机械拦截(跟 check_script_compiles 不同: # 这里没有客观的"编译通过/失败"可判断,误判成本是把合法的、恰好提到这些词的文档 diff --git a/src/scheduler.py b/src/scheduler.py index a0602a4..cb557de 100644 --- a/src/scheduler.py +++ b/src/scheduler.py @@ -95,7 +95,7 @@ def _blocked_reason_for(exc): generalized the same exception/retry machinery to the test_script mechanical compile-check too -- without this, both show up in the console as the same misleading 'prd_consistency_check_failed' label - (see docs/todo/validator-test-script-narrative-output-issue.md), even + (see docs/done/validator-test-script-narrative-output-issue.md), even when the real failure has nothing to do with the PRD. ponytail: distinguishes by substring-matching the failing node's LLM- chosen node_id (e.g. 'n_test_script'/'validator_test_script') rather diff --git a/src/test_e2e_pipeline.py b/src/test_e2e_pipeline.py index 5aa4d1f..b06b51e 100644 --- a/src/test_e2e_pipeline.py +++ b/src/test_e2e_pipeline.py @@ -344,7 +344,7 @@ class PlanDocumentsTest(PipelineTestBase): # must be persisted as artifacts, not silently discarded. # # test_script itself is disabled by default as of the same date - # (docs/todo/validator-test-script-narrative-output-issue.md) -- this + # (docs/done/validator-test-script-narrative-output-issue.md) -- this # test uses _fake_plan_subprocess_run_with_test_script to keep # exercising the salvage mechanism itself (generic, not specific to # test_script) rather than losing this coverage entirely. diff --git a/src/work_graph_plan.py b/src/work_graph_plan.py index ad523ca..b8fa866 100644 --- a/src/work_graph_plan.py +++ b/src/work_graph_plan.py @@ -116,7 +116,7 @@ GRAPH_MANIFEST_SCHEMA = { # what order, and how each one's judgment/on_fail should work. This # catalog is reference DATA the prompt hands the LLM, not a requirement # that every entry appears -- FR-B2. test_script (disabled 2026-08-07, -# docs/todo/validator-test-script-narrative-output-issue.md) is back in +# docs/done/validator-test-script-narrative-output-issue.md) is back in # this catalog per that decision's §8 item 7: Part B doesn't wait for the # narrative-output structural fix, that fix is tracked separately # alongside a future Coder-side test_script module. @@ -227,6 +227,14 @@ _CODER_NODE_CATALOG = [ "suggested_judgment": {"kind": "llm", "judge_prompt": _NODE_REVIEW_JUDGE_PROMPT}, "suggested_promotes_to_artifact": {"artifact_type": "test_report"}, }, + { + "node_type": "test_script", + "description": "A real, runnable script that mechanically checks the SDD's acceptance scenarios --" + "only include this if a genuine mechanical check beyond unit_test/integration_check " + "is useful for this goal (e.g. a standalone repro/benchmark script this task's " + "deliverable should ship with).", + "suggested_judgment": {"kind": "mechanical", "check_ref": "check_script_compiles"}, + }, ] _VALIDATOR_NODE_CATALOG = [ -- libgit2 0.21.4