From 1d4d77ef3d030a877c8e51806829c27fbbe75767 Mon Sep 17 00:00:00 2001 From: Linghong Date: Sat, 15 Aug 2026 05:17:39 +0000 Subject: [PATCH] =?UTF-8?q?=E7=AB=8B=E9=A1=B9=E7=AD=96=E5=88=92=EF=BC=9A?= =?UTF-8?q?=E8=90=BD=E5=9C=B0=20M1C-0=20=E5=90=88=E5=85=A5=E5=A4=8D?= =?UTF-8?q?=E6=A0=B8=E7=BB=93=E8=AE=BA=EF=BC=8C=E4=B8=89=E6=9D=A1=20M1C-1?= =?UTF-8?q?=20=E5=89=8D=E7=BD=AE=E4=B8=8E=E4=B8=80=E6=9D=A1=E5=93=A8?= =?UTF-8?q?=E5=85=B5=E8=AE=A2=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 生产代码零改动(delegation.rs 三个 hunk 全在 mod tests 内),本次只补回归与文档。 复核确认「今天行为零变化」成立且可证:contract_status 生产赋值点只有两处且都从 客观事实派生;StaticDelegateClaimRecord 全仓库唯一构造点是运行时构造,不是 agent 提交的 JSON;durable sidecar 在 .agent/runtime/** 下被 file_ops 与 filesystem 各 三处拒写;前端无 contractStatus 消费者。lineage 三个 fail-closed 出口全部返回 (u32::MAX, u32::MAX),新分支的哨兵检查接得住。 三条 M1C-1 前置写进技术方案第 23.7、23.8 节与 decision-log: ① UserRevisionRequested 不进 repair_required_count / user_input_required_count 任一 barrier,写入方落地即意味着 Supervisor run 可在用户修订未派出时完成; ② validate_static_delegate_structured_result 对该变体只有否定约束,缺正向一致性 分支,UserRevisionRequested + failed + 缺产物 能通过校验落盘; ③ 前向兼容失败粒度是整个子系统——list_static_delegate_deliveries_at 逐条 ? 上抛, 一条解析失败即整个目录枚举失败。bump schema version 救不了(版本校验在 parse 之后)。 文档订正:STATIC_DELEGATE_LINEAGE_MAX_HOPS = 32 限的是链上节点数不是跳数 (判据排在入链之前),真实跳数上限 31,第 23.7 节原写 32 跳已订正。 测试: - static_delegate_user_revision_preserves_existing_clarification_round 名不副实, 它把 UserRevisionRequested 放在目标位置,而计数循环只遍历父节点集合,新分支 从未被执行。改名为 ..._parent_hop_preserves_depth_and_clarification_round,补 一跳真正以用户修订为父的续跑并加反证;原断言留作对照组并注明性质。 - 新增 concurrent_user_revision_dispatch_creates_exactly_one_delivery,父节点为 UserRevisionRequested 且 depth 已为 1,两侧同时钉住并发下恰好放行一条。 - user_revision_continuation_... 补兄弟检查断言(depth 门对用户修订失效后,它是 该路径上唯一剩下的扇出约束)。 变异测试:摘掉 counters 分支 4 条变红(含新增反证 left (2,0) / right (1,1)); 摘掉 gate 分支 3 条变红(并发用例 left 0 / right 1)。两处守卫各自有回归覆盖。 验证:定向 41 passed / 0 failed;npm run check:encoding 通过(5374 files); git diff --check 干净。 Co-Authored-By: Claude Opus 5 --- .../src-tauri/src/delegation.rs | 44 +++- .../tests/collaboration/static_deliveries.rs | 208 +++++++++++++++++- .../shared-memory/decision-log.md | 13 ++ ...方案】立项策划Agent(Fast GDD)-2026-08-10.md | 9 +- 4 files changed, 265 insertions(+), 9 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/delegation.rs b/apps/ai-game-creator-shell/src-tauri/src/delegation.rs index 565421a7f..9fc62401a 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/delegation.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/delegation.rs @@ -2725,7 +2725,7 @@ mod tests { } #[test] - fn static_delegate_user_revision_preserves_existing_clarification_round() { + fn static_delegate_user_revision_parent_hop_preserves_depth_and_clarification_round() { let mut quality_root = new_static_delegate_delivery( GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, "m1c0-counter-session", @@ -2774,11 +2774,49 @@ mod tests { user_revision_result.contract_status = StaticDelegateContractStatus::UserRevisionRequested; user_revision.structured_result = Some(user_revision_result); - let deliveries = vec![quality_root, clarification, user_revision]; + let mut deliveries = vec![quality_root, clarification, user_revision]; + // 对照组,不是主张:计数循环只遍历 chain[..len-1](父节点集合),目标节点自身的 + // status 从不参与判定。所以「目标是 UserRevisionRequested」这条断言与新分支无关, + // 单靠它证明不了用户修订分类。 assert_eq!( static_delegate_lineage_counters(&deliveries, "m1c0-counter-d2"), (1, 1), - "用户修订必须继承此前的 depth=1 与 clarification_round=1" + "目标自身是用户修订时,两个计数只由其父链决定" + ); + + // 真正打到新分支的是「父节点为 UserRevisionRequested」的下一跳。 + let continuation = new_static_delegate_delivery_with_contract( + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + "m1c0-counter-session", + "m1c0-counter-run", + "m1c0-counter-d3-action", + "m1c0-counter-d3", + "design-director", + "m1c0-counter-d3-session", + "m1c0-counter-d3-run", + &[], + &[], + Some("m1c0-counter-d2"), + ); + deliveries.push(continuation); + assert_eq!( + static_delegate_lineage_counters(&deliveries, "m1c0-counter-d3"), + (1, 1), + "父节点是用户修订时,depth 与 clarification_round 都必须原样继承" + ); + + // 反证:同一条链只把父节点改回普通质量返工,上一条必须变成 (2, 0)。缺了这条, + // 上一条断言在新分支被删掉后依然成立(走 else 支得到 (2, 0) 才会失败), + // 但没有对照就看不出它究竟钉住了什么。 + deliveries[2] + .structured_result + .as_mut() + .expect("d2 structured result") + .contract_status = StaticDelegateContractStatus::NeedsRepair; + assert_eq!( + static_delegate_lineage_counters(&deliveries, "m1c0-counter-d3"), + (2, 0), + "父节点不是用户修订时必须回到质量返工分类" ); } diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/collaboration/static_deliveries.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/collaboration/static_deliveries.rs index e5f0f4829..8c4eb9e04 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/collaboration/static_deliveries.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/collaboration/static_deliveries.rs @@ -3801,8 +3801,7 @@ fn alternating_repair_and_clarification_hop_blocks_second_repair_at_depth_two() } #[test] -fn static_delegate_user_revision_requested_continuation_passes_real_delegate_gate_after_depth_one() -{ +fn user_revision_continuation_passes_real_gate_at_depth_one_but_stays_single_child() { // 这里手工把 delivery 状态切到 UserRevisionRequested,是对 M1C-1 未来审批写入的 // durable fixture;委派本身仍走真实 observe_agent_runtime_agent_delegate 生产路径。 let root = unique_project_path(); @@ -3942,6 +3941,211 @@ fn static_delegate_user_revision_requested_continuation_passes_real_delegate_gat Some(d1_id.as_str()) ); + // 新分支放行 depth 门之后,函数末尾的「同一静态委派最多允许一轮返工」兄弟检查必须 + // 仍然生效。这条是用户修订路径唯一剩下的扇出约束:depth 门对它不再适用,若兄弟检查 + // 也漏掉该状态,一个 UserRevisionRequested 父节点就能挂任意多个并存子委派。 + let d2b_observation = dispatch_static_delegate_plain_repair( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + run_id, + target_agent_id, + "对同一父节点再发一次用户修订", + &acceptance_criteria, + &expected_artifacts, + &d1_id, + "user-revision-real-gate-d2b-revision-action", + ); + assert_eq!( + d2b_observation.status, "failed", + "用户修订不得绕过兄弟检查扇出第二条并存子委派:{d2b_observation:?}" + ); + assert!( + d2b_observation.summary.contains("最多允许一轮返工"), + "拒绝理由必须是兄弟检查而不是别的门:{d2b_observation:?}" + ); + + drop(target_lock); + fs::remove_dir_all(root).ok(); +} + +/// 与 `project_supervisor_concurrent_repair_dispatch_creates_exactly_one_delivery` 同构, +/// 但父节点是 `UserRevisionRequested` 且链上 depth 已经是 1。两件事一起钉: +/// ① 并发下用户修订仍然只放行一条(兄弟检查在锁内有效,不因新分支失效); +/// ② 恰好放行「一条」而不是「零条」——新分支被删掉时两条都会被 depth 门拒,本用例变红。 +#[test] +fn concurrent_user_revision_dispatch_creates_exactly_one_delivery() { + let root = unique_project_path(); + init_local_game_project_at( + &root, + "project-concurrent-user-revision", + "用户修订并发派发测试", + ) + .expect("project init"); + let run_id = "project-supervisor-concurrent-user-revision-run"; + let state = start_game_creator_agent_runtime_task_at( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + "并发发起同一用户修订", + run_id, + "agent-chat", + "等待用户修订", + vec!["只允许一条用户修订续跑".to_string()], + ) + .expect("start concurrent user revision parent"); + let acceptance_criteria = vec!["必须交付策划产物".to_string()]; + let expected_artifacts = vec!["game/concurrent-user-revision.md".to_string()]; + let target_agent_id = "design-director"; + // 车道锁必须在任何真实派发之前拿:下面 D1 走的是真实 observe 路径,会占用 + // design-director 车道,之后再抢就拿不到了。 + let target_lock = try_acquire_game_creator_agent_runtime_task_lock(&root, target_agent_id) + .expect("acquire target lane") + .expect("target lane available"); + + let d0_id = "concurrent-user-revision-d0"; + create_dispatched_static_delegate_delivery( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + &state.session_id, + run_id, + "concurrent-user-revision-d0-action", + d0_id, + target_agent_id, + "concurrent-user-revision-d0-session", + "concurrent-user-revision-d0-run", + &acceptance_criteria, + &expected_artifacts, + None, + ); + mark_and_claim_static_delegate_needs_repair( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + run_id, + target_agent_id, + "concurrent-user-revision-d0-session", + "concurrent-user-revision-d0-run", + d0_id, + &expected_artifacts, + "concurrent-user-revision-d0-claim", + ); + + // 先用一次普通质量返工把链上 depth 顶到 1,之后才轮到新分支承重。 + let d1_action = "concurrent-user-revision-d1-repair-action"; + let d1_id = agent_runtime_delegation_id( + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + run_id, + target_agent_id, + d1_action, + ); + let d1_observation = dispatch_static_delegate_plain_repair( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + run_id, + target_agent_id, + "补齐用户修订前的策划产物", + &acceptance_criteria, + &expected_artifacts, + d0_id, + d1_action, + ); + assert_eq!( + d1_observation.status, "ok", + "首次质量返工必须放行:{d1_observation:?}" + ); + let d1_delivery = read_static_delegate_delivery_at(&root, &d1_id) + .expect("read D1") + .expect("D1 exists"); + mark_and_claim_static_delegate_needs_repair( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + run_id, + target_agent_id, + &d1_delivery.target_session_id, + &d1_delivery.target_run_id, + &d1_id, + &expected_artifacts, + "concurrent-user-revision-d1-claim", + ); + let mut d1_user_revision = read_static_delegate_delivery_at(&root, &d1_id) + .expect("read claimed D1") + .expect("claimed D1 exists"); + d1_user_revision + .structured_result + .as_mut() + .expect("D1 structured result") + .contract_status = StaticDelegateContractStatus::UserRevisionRequested; + write_static_delegate_delivery_at(&root, &d1_user_revision) + .expect("persist simulated approval revision status"); + + let start = Arc::new(Barrier::new(3)); + let mut workers = Vec::new(); + for action_id in [ + "concurrent-user-revision-action-a", + "concurrent-user-revision-action-b", + ] { + let worker_root = root.clone(); + let worker_start = Arc::clone(&start); + let worker_criteria = acceptance_criteria.clone(); + let worker_artifacts = expected_artifacts.clone(); + let worker_original = d1_id.clone(); + workers.push(std::thread::spawn(move || { + worker_start.wait(); + let observation = observe_agent_runtime_agent_delegate( + &worker_root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + run_id, + Some(action_id), + &serde_json::json!({ + "agentId": "design-director", + "task": "按用户审批修改方案", + "acceptanceCriteria": worker_criteria, + "expectedArtifacts": worker_artifacts, + "repairOfDelegationId": worker_original, + "runId": null + }), + ); + (action_id, observation) + })); + } + start.wait(); + let results = workers + .into_iter() + .map(|worker| worker.join().expect("join user revision worker")) + .collect::>(); + assert_eq!( + results + .iter() + .filter(|(_, observation)| observation.status == "ok") + .count(), + 1, + "并发用户修订必须恰好放行一条:{results:?}" + ); + assert!( + results.iter().any(|(_, observation)| { + observation.status == "failed" && observation.summary.contains("最多允许一轮返工") + }), + "被拒的那条必须止于兄弟检查:{results:?}" + ); + let persisted = results + .iter() + .filter_map(|(action_id, _)| { + read_static_delegate_delivery_at( + &root, + &agent_runtime_delegation_id( + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + run_id, + target_agent_id, + action_id, + ), + ) + .expect("read competing user revision delivery") + }) + .collect::>(); + assert_eq!(persisted.len(), 1, "只能落盘一条用户修订续跑"); + assert_eq!( + persisted[0].repair_of_delegation_id.as_deref(), + Some(d1_id.as_str()) + ); + drop(target_lock); fs::remove_dir_all(root).ok(); } diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 2a59a7f15..b1422e61f 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -1,5 +1,18 @@ # 决策记录 +## 2026-08-14 M1C-0 合入复核:三条 M1C-1 前置、一条文档订正、一条已排除假设 + +合入 `M1C-0`(见本文件同日条)后对该包做整组复核。**结论:包本身可合,「今天行为零变化」成立且可证**;但新变体在 `M1C-1` 写入之日会同时点亮三个默认值,而这三个默认值都不是裁决出来的,是「给用 `==` 比较(而非穷尽 `match`)的 durable enum 加变体,编译器不报,新变体静默落到作者没想过的那一侧」的结果——与本文件「修复 M1A-2 引入的回归」条同源,是同一失败模式的第二次出现。(复核完成后 `M1B-2` 已合入;其对 `delegation.rs` 的改动是 rustfmt 换行、无语义变化,本条结论不受影响。) + +- **不可达性已实证,不是靠注释。** `contract_status` 的生产赋值点只有两处(终态回执构造与 `build_static_delegate_structured_result_at`),都从客观事实派生(终态、缺失产物、verification、问题数),模型选不了;`StaticDelegateClaimRecord` 全仓库**唯一**构造点在 `delegation.rs` 的 claim 准备路径,是运行时构造而非 agent 提交的 JSON,所以「claim payload 自带 `contractStatus`」这条路不存在;durable sidecar 位于 `.agent/runtime/delegation-deliveries/`,被 `is_agent_runtime_private_control_path` 覆盖,`file_ops.rs` 与 `project/filesystem.rs` 各三处拒绝写入;前端无 `contractStatus` 消费者。哨兵侧同样成立:`static_delegate_lineage_counters` 的三个 fail-closed 出口(成环、超长、上游缺失)**全部**返回 `(u32::MAX, u32::MAX)`,两个分量都是 MAX,新分支的 `== u32::MAX` 检查全接得住;合法累加上限 31,不会误触。 +- **前置一:新变体不进任何 barrier。** `repair_required_count` 的判据是 `== NeedsRepair || (== NeedsUserInput && 有澄清答案)`,`user_input_required_count` 要求 `== NeedsUserInput`,`UserRevisionRequested` 两边都不落,因此 `barrier.is_clear()` 可能为真——`M1C-1` 写入方一落地就意味着 **Supervisor run 可以在用户修订尚未派出时完成**。同源的还有 `swarm_cli/terminal_classification.rs` 的 `static_delegate_delivery_has_repairable_contract`(`is_none_or(|r| == NeedsRepair)` → 新变体判为不可返工,影响面小但同样是默认选的)。`M1C-1` 须显式裁决补或不补。 +- **前置二:`validate_static_delegate_structured_result` 缺正向一致性分支。** `EvidenceReady` 有(终态/产物/verification 对账),`NeedsUserInput` 有(问题数 + 指纹),`UserRevisionRequested` 只继承了「不得携带 `user_input_questions`」这条否定约束,于是 `UserRevisionRequested + terminal_status="failed" + 缺产物` 能通过校验落盘。`M1C-0` 新增的 real-gate 回归恰好依赖这一点才能构造 fixture。 +- **前置三:前向兼容失败的粒度是整个子系统,不是单条记录。** `list_static_delegate_deliveries_at` 对每条 sidecar 用 `?` 上抛,一条解析失败即整个目录枚举失败,barrier、lineage、返工校验、run status 投影同时不可用。`M1C-0` 有意不 bump `STATIC_DELEGATE_DELIVERY_SCHEMA_VERSION` 且把「未知状态 serde 直接报错」记为安全属性——该性质成立,但**粒度当时没写**。注意 bump 版本号救不了:版本号校验排在 serde parse 之后。三条均已写进技术方案第 23.7 节「落地约束」与第 23.8 节 `M1C-1` 行。 +- **文档订正:`STATIC_DELEGATE_LINEAGE_MAX_HOPS = 32` 限的是链上节点数,不是跳数。** 判据 `chain.len() >= MAX_HOPS` 排在入链之前,故链最多 32 个节点 = **31 跳**。第 23.7 节原写「真实上限就是 32 跳」,已订正为 31。产品阈值 16 远低于两者,不受影响;`M1C-0` 自己的测试注释(「32 条 delivery(31 跳)」)一直是对的。之所以要订正:`M1C-0` 之前 `depth=1` 才是主刹车,用户修订路径上现在只剩这个哨兵,写错的数字从此是承重的。 +- **已排除的假设,后续不要重走。** 怀疑过「派一个子委派 → 抑制 → 再派一个」可绕开兄弟检查、在用户修订父节点下无限扩宽度。打不通:`suppress_static_delegate_delivery_at` 对 `ClaimedByParent` 直接原样返回、拒绝抑制,且生产侧唯一调用者是 `suppress_static_delegate_deliveries_for_parent_terminal_at`(父 run 终态清理),彼时同一 `parent_run_id` 已不能再派委派。另确认新分支**没有**跳过兄弟检查:if/else-if 链在前、兄弟检查在后且无提前返回,扇出仍是一条。 +- **测试侧本次一并处置。** ① `static_delegate_user_revision_preserves_existing_clarification_round` 名不副实:计数循环只遍历 `chain[..len-1]`(父节点集合),目标自身 status 从不参与判定,而该用例把 `UserRevisionRequested` 放在了**目标**位置,新分支根本没被执行。已改名为 `static_delegate_user_revision_parent_hop_preserves_depth_and_clarification_round`,补一跳真正以用户修订为父的续跑,并加反证(同一条链只把该父节点改回 `NeedsRepair`,结果必须变成 `(2, 0)`);原断言保留为对照组并注明其性质。② `user_revision_continuation_passes_real_gate_at_depth_one_but_stays_single_child`(原 `static_delegate_user_revision_requested_continuation_...`)补兄弟检查断言。③ 新增 `concurrent_user_revision_dispatch_creates_exactly_one_delivery`,与既有 `project_supervisor_concurrent_repair_dispatch_creates_exactly_one_delivery` 同构但父节点为 `UserRevisionRequested` 且链上 depth 已为 1,两侧同时钉住:并发下恰好放行一条,且不是零条(新分支被删则两条都会被 depth 门拒,用例变红)。 +- 教训(与 `M1A-2` 回归条合看):**给用 `==`/`!=` 比较的 durable enum 加变体,等于在每一个比较点上替作者做了一次没人复核的裁决。** 这类 PR 即使「无写入方、行为零变化」也必须逐个枚举比较点并写下每处落点,否则这些默认值会在写入方落地的那个 PR 里一次性生效,而那个 PR 的复核者只会看它自己的 diff。 + ## 2026-08-14 M1B-2 实现合同收口:Provider binding、结构化注入与提交恢复边界 - **状态与基线**:`M1B-1` 已通过门禁并合入,作为 `.agent/planning/` storage 基线;`M1B-2` 工作包已通过本包门禁并以 `27c3eb847` 合入本分支。当前包接 `plan.submit_gdd`、exact planning Provider 请求身份、提交点和恢复;`gdd-approval` planning pending、审批等待、receipt、审批命令与 UI 继续属于 `M1C-1` 及之后。本状态只表示 M1B-2 工作包完成,不表示完整产品可交付。 diff --git a/docs/technical/【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md b/docs/technical/【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md index da1e1ef24..2b86c5eb8 100644 --- a/docs/technical/【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md +++ b/docs/technical/【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md @@ -1979,17 +1979,18 @@ M0 完成不表示完整策划闭环已经上线。`M1A-1`~`M1A-4`、`M1B-1` | 硬边界 | 值 | 性质 | | --- | --- | --- | -| `STATIC_DELEGATE_LINEAGE_MAX_HOPS` | 32 | 链上推断每次走完整条链,超限 fail closed 返回 `(u32::MAX, u32::MAX)`;防环/防越界兜底,不是可调参数 | +| `STATIC_DELEGATE_LINEAGE_MAX_HOPS` | 32 | 链上推断每次走完整条链,超限 fail closed 返回 `(u32::MAX, u32::MAX)`;防环/防越界兜底,不是可调参数。**该常量限的是链上节点数,不是跳数**:判据 `chain.len() >= MAX_HOPS` 排在入链之前,故链最多 32 个节点 = **31 跳**(2026-08-14 订正,此前本节把它写成 32 跳) | | 单 lineage 版本数(第 8.8 节) | 128 | 版本链只增不改 | -只要修订挂在同一条链上,真实上限就是 **32 跳**。让修订不挂链(`repair_of=None` 另起)可绕开,但会丢失谱系关联并重置澄清预算,等于绕开整套约束,不采纳。产品阈值定为 **16 次修订且为软提示**:远低于机制边界、又高到用户实际碰不到;超过时不拒绝,提示「已修订 16 次,建议退回重做重新梳理需求」——每次修订都是一次完整 Provider 调用加一个新版本,改到十几次仍不满意时问题多半在需求本身,此时应走退回重做(新 lineage、重新问询)而非无限微调。 +只要修订挂在同一条链上,真实上限就是 **31 跳**。让修订不挂链(`repair_of=None` 另起)可绕开,但会丢失谱系关联并重置澄清预算,等于绕开整套约束,不采纳。产品阈值定为 **16 次修订且为软提示**:远低于机制边界、又高到用户实际碰不到;超过时不拒绝,提示「已修订 16 次,建议退回重做重新梳理需求」——每次修订都是一次完整 Provider 调用加一个新版本,改到十几次仍不满意时问题多半在需求本身,此时应走退回重做(新 lineage、重新问询)而非无限微调。 **落地约束**: - 拆包(2026-08-13 按第 23.8 节更正):**变体与分类分支在 `M1C-0`**——该 PR 无写入方、是惰性路径、行为零变化,便于把「做游戏链路返工额度未被误放宽」这条关键回归单独证明;**写入方与 receipt 在 `M1C-1` 同进同退**——只置 status 不写 receipt(或反之)会让链路计数与审批事实脱节。这样拆全程不产生半状态:`UserRevisionRequested` 直到 `M1C-1` 才可能被写出。 - `StaticDelegateContractStatus` 是 durable schema 的一部分。新增变体属**前向兼容**问题(旧代码读不了新记录),非后向兼容问题:只有做方案链路会写它,做游戏链路既有记录不受影响。反序列化必须 fail closed,**不得静默降级为 `NeedsRepair`**——那会让用户修订被误计成返工,正是本裁决要消除的行为。 +- **(2026-08-14 补,`M1C-0` 合入复核)前向兼容失败的粒度是整个子系统,不是单条记录。** `list_static_delegate_deliveries_at` 对每条 sidecar 用 `?` 上抛,一条解析失败即整个目录枚举失败,barrier 计算、lineage 重放、返工校验与 run status 投影同时不可用。因此 `M1C-1` 写出该状态后,旧版本 App(回滚、或多机版本不一致)打开同一项目会**整个静态委派面不可用**。注意 bump `STATIC_DELEGATE_DELIVERY_SCHEMA_VERSION` 救不了这一条:版本号校验排在 serde parse 之后,parse 已先失败——要处置只能改枚举粒度(单条损坏跳过并告警)。**`M1C-1` 上线前必须二选一并记录裁决:给枚举加单条容错,或明确接受回滚锁死项目。** - 本裁决改的是 master 已发布的静态委派机制,须在 `decision-log.md` 补 dated 记录,写明 `WP1` 的 depth≤1 结论未被推翻。 -- 回归必须覆盖:用户修订跳不增 `depth` 也不重置 `round`;连续多次修订均可通过;做游戏链路的返工仍在 `depth=1` 被拒;无该变体的历史记录分类行为不变;32 跳边界仍 fail closed。 +- 回归必须覆盖:用户修订跳不增 `depth` 也不重置 `round`;连续多次修订均可通过;做游戏链路的返工仍在 `depth=1` 被拒;无该变体的历史记录分类行为不变;链长边界仍 fail closed;**用户修订父节点下「同一静态委派最多允许一轮返工」的兄弟检查仍然生效(含并发)**——depth 门对用户修订不再适用后,兄弟检查是该路径上唯一剩下的扇出约束。 ### 23.8 M1 的 PR 结构与合入门禁(2026-08-13) @@ -2004,7 +2005,7 @@ M0 完成不表示完整策划闭环已经上线。`M1A-1`~`M1A-4`、`M1B-1` | `M1B-1` | `.agent/planning/` 存储层、strict schema、typed 指纹、版本链;含 `.agent/planning/**` 只挡写判据 | `M1A-2` | **2026-08-14 已通过门禁并合入本分支**:GDD / index / session strict DTO、canonical bytes、duplicate-key 与后缀门、typed 指纹、连续版本链、session 原子写入/恢复、Runtime writer identity 及 `game/fast_gdd.md` / `.agent/planning/**` 通用写入拒绝;第 9.1 节 golden vector(3857 bytes)与 11 个定向 storage 测试通过,writer 目标 schema 重验、index 权威对账与锁内 index recovery(缺失/损坏/陈旧从严格 GDD 链重建)、恢复故障矩阵、`cargo check --offline`、`npm run check:encoding` 与 `git diff --check` 均通过。合入时无 approval receipt schema,多版本 `statusCache` 只是无 receipt 的预审批投影;M1C-1 接入 receipt 后必须重建 `approved/revise/reject/superseded` 状态。create-only 与等前缀不可变 | | `M1B-2` | `plan.submit_gdd` 原生工具、exact planning Provider 请求绑定与 GDD 提交点 | `M1B-1` | **工作包已通过本包门禁并以 `27c3eb847` 合入本分支**。实现合同:四类 request kind 全部写 v3 lifecycle、required binding 与同一 dedicated structured-injection user message;只有 `tool-plan` 可生成 sole-submit v4 batch;提交点后只修复 index/Markdown/session successor、终止策划子 run,并保留 generic v5 standalone pending + v4 batch anchors,不创建 `gdd-approval` planning pending/审批等待。定向 Rust、`cargo check --offline`、格式、编码与 diff 门禁均已通过;完整审批链路与产品可交付仍留给后续工作包 | | `M1C-0` | 新增 `StaticDelegateContractStatus::UserRevisionRequested` 与分类分支 | `M1A-1` | **已落地**:无审批写入方、是惰性路径;用户修订跳不增 `repair_depth` 也不重置 `clarification_round`,连续修订可通过;做游戏链路返工仍在 `depth=1` 被拒;历史记录与未知状态均保持 fail-closed | -| `M1C-1` | `gdd-approval` pending、审批命令、receipt;receipt 写入上述 status | `M1B-2`、`M1C-0` | 三动作全通;版本+指纹竞态防护;两窗口并发;**连续多次修订均可通过且 `repair_depth` 不变** | +| `M1C-1` | `gdd-approval` pending、审批命令、receipt;receipt 写入上述 status | `M1B-2`、`M1C-0` | 三动作全通;版本+指纹竞态防护;两窗口并发;**连续多次修订均可通过且 `repair_depth` 不变**。**另须先裁决 `M1C-0` 合入复核留下的三条**(详见第 23.7 节「落地约束」与 `decision-log.md` 同名条):① `UserRevisionRequested` 目前不进 `repair_required_count` / `user_input_required_count` 任一 barrier,写入方一落地就意味着 Supervisor run 可在用户修订尚未派出时完成——要么补 barrier,要么明确裁决不补;② `validate_static_delegate_structured_result` 对该变体只有「不得携带 user_input_questions」这条否定约束,没有正向一致性分支,须补齐终态/产物口径;③ 前向兼容失败粒度为整个子系统,须按第 23.7 节二选一 | | `M1C-2a` | Goal Contract 接线:turn 1 冻结、固定验收图、审批前置门取证 | `M1C-1`、`M1A-3` | turn 1 只能是一个 `agent.goal_contract`;`acceptanceNodes` 不接受自定义;**第 13.0 节审批前置门:取证未通过时不出现审批卡、而是产生返工委派**(取证顺序是协议时序问题,归本包而非 `M1C-1`) | | `M1C-2b` | 澄清中转接线、轮次派生、预算注入 | `M1C-2a` | 3 轮上限;continuation 重放幂等不增加轮次;答案绑定冲突被拒 | | `M1D-1` | 前端 hydrate 与 GDD 审批卡 | `M1C-2b` | 前端只经 `hydrate_game_creator_plan_gdd_state` 读权威状态,不在页面侧合成批准事实 |