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 9b2d2370d..857ad465e 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/delegation.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/delegation.rs @@ -37,6 +37,7 @@ pub(crate) enum StaticDelegateContractStatus { EvidenceReady, NeedsRepair, NeedsUserInput, + UserRevisionRequested, } #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] @@ -1090,7 +1091,7 @@ pub(crate) fn game_chat_art_delivery_gap_at( /// 下一跳(若存在)应当被归类为澄清 continuation,而不是质量返工。 /// `validate_static_delegate_repair_request_at` 与 /// `validate_static_delegate_clarification_continuation_at` 共用这一个判据源, -/// 避免两处口径漂移。 +/// 避免两处口径漂移;用户修订状态另由下方判据单独识别。 fn static_delegate_original_is_awaiting_clarification( delivery: &StaticDelegateDeliveryRecord, ) -> bool { @@ -1099,12 +1100,26 @@ fn static_delegate_original_is_awaiting_clarification( }) } +/// 唯一权威判据:某条 delivery 是否由用户审批的「修改」动作标记为待修订。 +/// +/// 该状态只由后续审批工作包写入;本包只让 lineage 重放认识它,不能自行生成或 +/// 把其它状态静默映射成它。 +fn static_delegate_original_is_user_revision_requested( + delivery: &StaticDelegateDeliveryRecord, +) -> bool { + delivery.structured_result.as_ref().is_some_and(|result| { + result.contract_status == StaticDelegateContractStatus::UserRevisionRequested + }) +} + /// 沿 repair_of_delegation_id 反向重放整条链,现算目标 delivery 的 /// (repair_depth, clarification_round)。两个维度都是运行时派生值,故意不落盘: -/// - R3(链根,repair_of_delegation_id 为 None):depth = 0,round = 0。 -/// - R1(澄清跳:父节点处于 awaiting-clarification):round = parent.round + 1, +/// - 链根(repair_of_delegation_id 为 None):depth = 0,round = 0。 +/// - 澄清跳(父节点处于 awaiting-clarification):round = parent.round + 1, /// depth 原样继承(不重置),否则可以插一次澄清洗掉返工深度,变成无限返工。 -/// - R2(返工跳:父节点不处于 awaiting-clarification):depth = parent.depth + 1, +/// - 用户修订跳(父节点为 UserRevisionRequested):depth/round 都原样继承, +/// 因为该跳由用户显式触发,不属于 runaway-agent 质量返工。 +/// - 质量返工跳(父节点不处于上述两种状态):depth = parent.depth + 1, /// round 重置为 0,因为返工后策划节点重新开工,不能因为返工吃掉预设的澄清轮次预算。 /// /// 防环 / 防越界:反向重放阶段一旦遇到重复 id、缺失的上游节点,或跳数超出 @@ -1140,6 +1155,8 @@ pub(crate) fn static_delegate_lineage_counters( for parent in &chain[..chain.len().saturating_sub(1)] { if static_delegate_original_is_awaiting_clarification(*parent) { round += 1; + } else if static_delegate_original_is_user_revision_requested(*parent) { + // 用户明确触发的修订不是 runaway-agent 返工;保留两个运行时派生计数。 } else { depth += 1; round = 0; @@ -1215,6 +1232,12 @@ pub(crate) fn validate_static_delegate_repair_request_at( if original_clarification_round >= clarification_round_limit { return Err("静态委派澄清轮次已达上限".to_string()); } + } else if static_delegate_original_is_user_revision_requested(original) { + // 用户修订不消耗 repair_depth/clarification_round,但链上重放的 fail-closed + // 哨兵仍不能被绕过;否则损坏或成环的 durable lineage 可能被误放行。 + if original_repair_depth == u32::MAX || original_clarification_round == u32::MAX { + return Err("静态委派谱系计数无效,拒绝继续".to_string()); + } } else if original_repair_depth >= 1 { return Err("静态委派返工深度最多为 1".to_string()); } @@ -2575,4 +2598,288 @@ mod tests { fs::remove_dir_all(root).ok(); } + + fn claimed_static_delegate_for_lineage_test( + parent_run_id: &str, + delegation_id: &str, + repair_of_delegation_id: Option<&str>, + contract_status: StaticDelegateContractStatus, + ) -> StaticDelegateDeliveryRecord { + let mut delivery = new_static_delegate_delivery_with_contract( + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + "m1c0-lineage-parent-session", + parent_run_id, + &format!("{delegation_id}-action"), + delegation_id, + "design-director", + &format!("{delegation_id}-session"), + &format!("{delegation_id}-run"), + &[], + &[], + repair_of_delegation_id, + ); + let mut result = StaticDelegateStructuredResult::default(); + result.contract_status = contract_status; + delivery.status = StaticDelegateDeliveryStatus::ClaimedByParent; + delivery.terminal_status = Some("completed".to_string()); + delivery.result_summary = Some("M1C-0 lineage test".to_string()); + delivery.structured_result = Some(result); + delivery.claimed_by_action_id = Some(format!("{delegation_id}-claim-action")); + delivery + } + + #[test] + fn static_delegate_user_revision_preserves_counters_and_bypasses_depth_gate() { + let root = std::env::temp_dir().join(format!( + "genarrative-static-user-revision-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system time after unix epoch") + .as_nanos() + )); + init_local_game_project_at(&root, "m1c0-user-revision", "M1C-0 用户修订分类测试") + .expect("project init"); + let parent_run_id = "m1c0-user-revision-parent-run"; + let root_delivery = claimed_static_delegate_for_lineage_test( + parent_run_id, + "m1c0-user-revision-d0", + None, + StaticDelegateContractStatus::NeedsRepair, + ); + write_static_delegate_delivery_at(&root, &root_delivery).expect("write root delivery"); + + // 先证明做游戏链路的普通返工仍然受 depth=1 门限制。 + let mut first_revision = claimed_static_delegate_for_lineage_test( + parent_run_id, + "m1c0-user-revision-d1", + Some(&root_delivery.delegation_id), + StaticDelegateContractStatus::NeedsRepair, + ); + write_static_delegate_delivery_at(&root, &first_revision) + .expect("write first repair delivery"); + let ordinary_repair_error = validate_static_delegate_repair_request_at( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + parent_run_id, + "m1c0-user-revision-d2-ordinary", + "design-director", + &[], + &[], + Some(&first_revision.delegation_id), + ) + .expect_err("ordinary repair at depth=1 must remain blocked"); + assert!(ordinary_repair_error.contains("深度最多为 1")); + + // 同一节点被用户审批标记为修订后,第一次用户修订不应再被当作质量返工。 + first_revision + .structured_result + .as_mut() + .expect("first revision structured result") + .contract_status = StaticDelegateContractStatus::UserRevisionRequested; + write_static_delegate_delivery_at(&root, &first_revision) + .expect("rewrite first delivery as user revision"); + let deliveries = list_static_delegate_deliveries_at(&root).expect("list deliveries"); + assert_eq!( + static_delegate_lineage_counters(&deliveries, &first_revision.delegation_id), + (1, 0), + "用户修订跳的父节点已有一次普通返工,两个计数仍应保持 (1,0)" + ); + validate_static_delegate_repair_request_at( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + parent_run_id, + "m1c0-user-revision-d2", + "design-director", + &[], + &[], + Some(&first_revision.delegation_id), + ) + .expect("user revision must bypass the depth=1 quality-repair gate"); + + // 连续第二次用户修订仍挂在同一 lineage 上,不能把任一计数重新解释成返工。 + let second_revision = claimed_static_delegate_for_lineage_test( + parent_run_id, + "m1c0-user-revision-d2", + Some(&first_revision.delegation_id), + StaticDelegateContractStatus::UserRevisionRequested, + ); + write_static_delegate_delivery_at(&root, &second_revision) + .expect("write second user revision delivery"); + let deliveries = list_static_delegate_deliveries_at(&root).expect("list deliveries"); + assert_eq!( + static_delegate_lineage_counters(&deliveries, &second_revision.delegation_id), + (1, 0), + "连续用户修订不能增加 repair_depth,也不能重置 clarification_round" + ); + validate_static_delegate_repair_request_at( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + parent_run_id, + "m1c0-user-revision-d3", + "design-director", + &[], + &[], + Some(&second_revision.delegation_id), + ) + .expect("a second consecutive user revision must remain admissible"); + + fs::remove_dir_all(root).ok(); + } + + #[test] + fn static_delegate_user_revision_preserves_existing_clarification_round() { + let mut quality_root = new_static_delegate_delivery( + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + "m1c0-counter-session", + "m1c0-counter-run", + "m1c0-counter-d0-action", + "m1c0-counter-d0", + "design-director", + "m1c0-counter-d0-session", + "m1c0-counter-d0-run", + ); + let mut quality_result = StaticDelegateStructuredResult::default(); + quality_result.contract_status = StaticDelegateContractStatus::NeedsRepair; + quality_root.structured_result = Some(quality_result); + + let mut clarification = new_static_delegate_delivery_with_contract( + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + "m1c0-counter-session", + "m1c0-counter-run", + "m1c0-counter-d1-action", + "m1c0-counter-d1", + "design-director", + "m1c0-counter-d1-session", + "m1c0-counter-d1-run", + &[], + &[], + Some("m1c0-counter-d0"), + ); + let mut clarification_result = StaticDelegateStructuredResult::default(); + clarification_result.contract_status = StaticDelegateContractStatus::NeedsUserInput; + clarification.structured_result = Some(clarification_result); + + let mut user_revision = new_static_delegate_delivery_with_contract( + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + "m1c0-counter-session", + "m1c0-counter-run", + "m1c0-counter-d2-action", + "m1c0-counter-d2", + "design-director", + "m1c0-counter-d2-session", + "m1c0-counter-d2-run", + &[], + &[], + Some("m1c0-counter-d1"), + ); + let mut user_revision_result = StaticDelegateStructuredResult::default(); + user_revision_result.contract_status = StaticDelegateContractStatus::UserRevisionRequested; + user_revision.structured_result = Some(user_revision_result); + + let deliveries = vec![quality_root, clarification, user_revision]; + assert_eq!( + static_delegate_lineage_counters(&deliveries, "m1c0-counter-d2"), + (1, 1), + "用户修订必须继承此前的 depth=1 与 clarification_round=1" + ); + } + + #[test] + fn static_delegate_lineage_boundary_and_status_deserialization_fail_closed() { + let mut deliveries = Vec::new(); + let mut ids = Vec::new(); + for index in 0..33 { + ids.push(format!("m1c0-lineage-boundary-{index}")); + } + for index in 0..33 { + let parent = (index > 0).then(|| ids[index - 1].as_str()); + let mut delivery = new_static_delegate_delivery( + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + "m1c0-boundary-session", + "m1c0-boundary-run", + &format!("{}-action", ids[index]), + &ids[index], + "design-director", + &format!("{}-session", ids[index]), + &format!("{}-run", ids[index]), + ); + delivery.repair_of_delegation_id = parent.map(str::to_string); + let mut result = StaticDelegateStructuredResult::default(); + result.contract_status = StaticDelegateContractStatus::UserRevisionRequested; + delivery.structured_result = Some(result); + deliveries.push(delivery); + } + assert_eq!( + static_delegate_lineage_counters(&deliveries[..32], &ids[31]), + (0, 0), + "32 条 delivery(31 跳)仍应在安全阀内" + ); + assert_eq!( + static_delegate_lineage_counters(&deliveries, &ids[32]), + (u32::MAX, u32::MAX), + "超过 32-hop 安全阀必须 fail closed" + ); + + let encoded = serde_json::to_value(StaticDelegateContractStatus::UserRevisionRequested) + .expect("serialize user revision status"); + assert_eq!(encoded, serde_json::json!("user-revision-requested")); + assert_eq!( + serde_json::from_value::(encoded) + .expect("round-trip user revision status"), + StaticDelegateContractStatus::UserRevisionRequested + ); + let unknown = serde_json::from_value::(serde_json::json!( + "future-contract-status" + )) + .expect_err("unknown durable contract status must fail closed"); + assert!(unknown.to_string().contains("unknown variant")); + + // 额外钉住 durable sidecar 读取入口:未知状态不能在 JSON sidecar 层被吞掉或 + // 降级成 Default(NeedsRepair)。 + let root = std::env::temp_dir().join(format!( + "genarrative-static-unknown-status-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system time after unix epoch") + .as_nanos() + )); + init_local_game_project_at(&root, "m1c0-unknown-status", "未知静态委派状态解析测试") + .expect("project init"); + let mut record = new_static_delegate_delivery( + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + "m1c0-unknown-status-session", + "m1c0-unknown-status-run", + "m1c0-unknown-status-action", + "m1c0-unknown-status-delivery", + "design-director", + "m1c0-unknown-status-target-session", + "m1c0-unknown-status-target-run", + ); + record.status = StaticDelegateDeliveryStatus::Ready; + record.terminal_status = Some("completed".to_string()); + record.result_summary = Some("unknown status fixture".to_string()); + let mut result = StaticDelegateStructuredResult::default(); + result.contract_status = StaticDelegateContractStatus::UserRevisionRequested; + record.structured_result = Some(result); + write_static_delegate_delivery_at(&root, &record).expect("write known status fixture"); + let delivery_path = root + .join(STATIC_DELEGATE_DELIVERY_DIR) + .join(format!("{}.json", record.delegation_id)); + let mut raw = serde_json::to_value(&record).expect("serialize status fixture"); + raw["structuredResult"]["contractStatus"] = serde_json::json!("future-contract-status"); + fs::write( + &delivery_path, + serde_json::to_vec(&raw).expect("serialize unknown status fixture"), + ) + .expect("overwrite unknown status fixture"); + let sidecar_error = read_static_delegate_delivery_at(&root, &record.delegation_id) + .expect_err("unknown durable status must fail at sidecar read"); + assert!( + sidecar_error.contains("解析") && sidecar_error.contains("静态委派 delivery"), + "unexpected sidecar parse error: {sidecar_error}" + ); + fs::remove_dir_all(root).ok(); + } } 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 5c7b0cbe9..01eefedc1 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 @@ -3796,6 +3796,151 @@ fn alternating_repair_and_clarification_hop_blocks_second_repair_at_depth_two() fs::remove_dir_all(root).ok(); } +#[test] +fn static_delegate_user_revision_requested_continuation_passes_real_delegate_gate_after_depth_one() { + // 这里手工把 delivery 状态切到 UserRevisionRequested,是对 M1C-1 未来审批写入的 + // durable fixture;委派本身仍走真实 observe_agent_runtime_agent_delegate 生产路径。 + let root = unique_project_path(); + init_local_game_project_at( + &root, + "project-user-revision-real-gate", + "用户修订真实委派门测试", + ) + .expect("project init"); + let run_id = "project-supervisor-user-revision-real-gate-run"; + let state = start_game_creator_agent_runtime_task_at( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + "验收策划子 Agent 并处理用户修订", + run_id, + "agent-chat", + "等待策划回执", + vec!["完成用户修订后的方案".to_string()], + ) + .expect("start supervisor run"); + let acceptance_criteria = vec!["必须交付策划产物".to_string()]; + let expected_artifacts = vec!["game/user-revision-real-gate.md".to_string()]; + let target_agent_id = "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 = "user-revision-real-gate-d0"; + create_dispatched_static_delegate_delivery( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + &state.session_id, + run_id, + "user-revision-real-gate-d0-action", + d0_id, + target_agent_id, + "user-revision-real-gate-d0-session", + "user-revision-real-gate-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, + "user-revision-real-gate-d0-session", + "user-revision-real-gate-d0-run", + d0_id, + &expected_artifacts, + "user-revision-real-gate-d0-claim", + ); + + let d1_action = "user-revision-real-gate-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, + "user-revision-real-gate-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"); + + // D1 的链上 depth 已经是 1;只有 UserRevisionRequested 分支能让这次真实委派继续。 + let d2_action = "user-revision-real-gate-d2-revision-action"; + let d2_id = agent_runtime_delegation_id( + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + run_id, + target_agent_id, + d2_action, + ); + let d2_observation = dispatch_static_delegate_plain_repair( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + run_id, + target_agent_id, + "按用户审批修改方案", + &acceptance_criteria, + &expected_artifacts, + &d1_id, + d2_action, + ); + assert_eq!( + d2_observation.status, "ok", + "UserRevisionRequested 不能被 depth=1 质量返工门误拒:{d2_observation:?}" + ); + let d2_delivery = read_static_delegate_delivery_at(&root, &d2_id) + .expect("read D2") + .expect("D2 exists"); + let deliveries = list_static_delegate_deliveries_at(&root).expect("list deliveries"); + assert_eq!( + static_delegate_lineage_counters(&deliveries, &d2_id), + (1, 0), + "用户修订续跑应保留已有 depth=1 且不重置此前 round" + ); + assert_eq!( + d2_delivery.repair_of_delegation_id.as_deref(), + Some(d1_id.as_str()) + ); + + drop(target_lock); + fs::remove_dir_all(root).ok(); +} + #[test] fn quality_rework_resets_clarification_round_budget() { let root = unique_project_path(); diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 512bea8b9..15f7b9f57 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -1,5 +1,14 @@ # 决策记录 +## 2026-08-14 M1C-0:用户修订状态进入静态委派 lineage 分类 + +- 落地:`StaticDelegateContractStatus` 新增 `UserRevisionRequested`(serde durable 值为 `user-revision-requested`)。`static_delegate_lineage_counters` 现在按三类传播:`NeedsUserInput` 只增加 `clarification_round`;`UserRevisionRequested` 原样继承 `repair_depth` 与 `clarification_round`;其它状态继续按质量返工增加 `repair_depth` 并重置 `clarification_round`。 +- 门禁:父 delivery 为 `UserRevisionRequested` 时,后续同链续跑不再被 `repair_depth=1` 的质量返工门误拒,因此连续用户修订可以继续通过;链上 `(u32::MAX, u32::MAX)` fail-closed 哨兵仍拒绝,不得借用户修订分支绕过 32-hop、成环或缺失上游保护。普通做游戏链路没有该状态,`repair_depth≤1` 结论未被推翻。 +- 范围:本包没有任何审批状态写入方、`gdd-approval` pending、receipt 或前端状态;`UserRevisionRequested` 仍由后续 `M1C-1` 的审批命令与 receipt 同步写入。本包不 bump `STATIC_DELEGATE_DELIVERY_SCHEMA_VERSION`,未知 durable status 由 serde 直接报错,禁止静默降级为 `NeedsRepair`。 +- 回归:新增连续用户修订、保留已有澄清轮次、普通 depth=1 拒绝、32-hop fail-closed、`user-revision-requested` serde round-trip 与未知 variant fail-closed 覆盖;无新状态的历史记录继续按原质量返工分类。 +- 关联文档:`docs/technical/【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md` 第 23.7、23.8 节;后续审批写入依赖 `M1B-2`、`M1C-1`。 +- 合入说明:本包在隔离 worktree 上以 `09c7d7af8`(`M1A-2` 收口)为基线开发,未包含 `M1A-4`、M1A 残余收口、`M1A-2` 回归修复与 `M1B-1`。合回时代码零冲突(本包改的是顶层 `src-tauri/src/delegation.rs`,`M1A-4` 改的是 `src-tauri/src/agent/runtime_tools/delegation.rs`,同名不同文件),仅两份文档的状态句冲突:合并时以原分支为准保留 `M1A-4` / `M1B-1` 的已落地事实,删去本包基线上「`.agent/planning` 存储仍未实现」「`M1B-1` 及之后仍未开始」两句已被 `M1B-1` 推翻的表述。 + ## 2026-08-14 修复 M1A-2 引入的回归:未知工具名不是身份违规 - 症状:`background_agent_runtime_persists_receipts_for_rejected_actions` 在 feature 分支恒失败(3/3),`master` 通过。首个 tool-plan 请求能收到,动作被拒绝后**第二次 Provider follow-up 请求不再发出**,测试等待超时。**不是已知的 mock-LLM 本机 flake**,是确定性回归。 @@ -30,6 +39,7 @@ 2. **plan 之外的 source 多了一个失败面。** 两个 observe 函数新增的 task journal 读取,其 `Err` 分支位于弱判据之前,对 `project-supervisor-gui/cli/game-chat` 同样生效。正常状态不触发(仅 journal I/O 损坏等异常)。保留的依据是:读不到 task 就无法判断这条 run 是不是 plan 根,此时 fail closed 是正确取向;改成「读失败就放行」会引入一个新的 fail-open。**这是对第 19 节「做游戏链路逐字不变」的一处有意例外,仅限异常路径。** 3. **上下文层回归是弱断言。** `plan_root_supervisor_prompt_drops_intro_and_visual_contract_sections` 用「不含某几条独有短句」断言,不是对照组那种字节级 `assert_eq`。已实测非永真。已知漏报场景:若将来 `$visualContract` / `supervisorIntro` 被替换成措辞不同但仍暗示专业组扇出的新文本,这条不会报警。**执行层的 `A1`/`A2` 是该场景的唯一保障**——这也是本包把硬门排在裁段之前的原因。 - 关联文档:`docs/technical/【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md` 第 4.3、22、23.8、24 节。 + ## 2026-08-14 M1B-1:planning storage 基础与只挡写隔离完成(待合入) - 范围:在 `M1A-2` 的 planning 子 Agent 工具边界之上,先落地 Runtime-owned `.agent/planning/**` 的 typed storage 基础,不注册、不广告、不执行 `plan.submit_gdd`(该工具仍属于 `M1B-2`)。当前实现位于隔离 worktree 的 `apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_storage.rs`,由 `runtime_protocol.rs` 注册。 diff --git a/docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md b/docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md index 07a0e2467..5f787b869 100644 --- a/docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md +++ b/docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md @@ -1060,7 +1060,7 @@ static delivery 在 V1.16 的 parent/target Agent、Session、run、action、del ```text structuredResult = { - contractStatus: evidence-ready | needs-repair, + contractStatus: evidence-ready | needs-repair | needs-user-input | user-revision-requested, artifacts: [{ path, sha256 }], missingExpectedArtifacts: [path], verificationRequired: boolean, @@ -1070,6 +1070,11 @@ structuredResult = { } ``` +其中 `needs-user-input` 是静态委派澄清中转状态,`user-revision-requested` 是 Fast GDD +审批扩展预留的用户修订状态;后者不由通用 Runtime 自动生成,且不得被反序列化失败静默 +降级为 `needs-repair`。用户修订的 lineage 计数语义与 M1C-0/M1C-1 边界以 +`【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md` 第 23.7 节为准。 + `artifacts` 与 `missingExpectedArtifacts` 必须恰好分割全部 `expectedArtifacts`,路径保持合同中的精确相对路径;存在的普通文件在形成终态回执时重新计算 SHA-256。`evidence` 和 `error` 只允许有界、已清洗的安全摘要、项目内相对路径和哈希,不得保存凭据、绝对路径、私有 observation 正文或 Provider payload。旧空合同 delivery 可以继续没有 `structuredResult`,且不得为了升级格式改写原 sidecar;新带合同 delivery 进入 ready/claimed 时必须有完整 `structuredResult`。 ### 客观门禁与语义验收 diff --git a/docs/technical/【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md b/docs/technical/【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md index e1a9c3308..2f0fd5eb0 100644 --- a/docs/technical/【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md +++ b/docs/technical/【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md @@ -1,9 +1,9 @@ # 立项策划 Agent(Fast GDD)技术方案 - 日期:2026-08-10 -- 状态:2026-08-12 **M0 代码工作包全部完成**(`M0A-2`、`M0B-1`、`M0B-2` 已合入 M0 集成分支并通过各自门禁,见第 23.4 节);同日 **D6 作废、拓扑改变**,`M0A-1` 交付的文档基线随之失效,需以工作包 `M0A-3` 修订,**修订完成前 M0 不计完整完成**(见第 1.1 节)。2026-08-13 **D9 二次作废、D10 作废,由 D11 取代**:立项策划节点改为 Project Supervisor 通过 `agent.delegate` 发起的静态委派子 Agent,问询复用 PR #165 中转链路(见第 1.1 节「D11 新拓扑」);D11 依赖 WP1(静态委派澄清轮次与返工深度拆分)为强制前置,**该前置已于 2026-08-13 落地并合入**(`WP1` 生产代码 + `WP2` 回归,完成状态与门禁见第 23.5 节),澄清轮次上限现为 3(game-chat source 仍为 1)。随后 `M1A-1`、`M1A-2`、`M1A-3` 已分别落地:`M1A-2` 仅收口两层工具面、`project-planning` role brief 注入和 fail-closed 拒绝边界。**2026-08-14 M1B-1 已在隔离 worktree 完成并通过本包验收门禁,已提交于隔离分支 `f453c2ca2`,待合回原分支**:已落地 `.agent/planning` storage module、strict schema/typed 指纹/canonical parser、GDD 版本链、session 原子恢复、Runtime 写入身份及只挡写门禁;golden vector 与 11 个定向 storage 测试通过,writer/index/recovery 及全仓库门禁已完成。`plan.submit_gdd`、审批闭环和 UI 仍未实现,见第 23.6、23.8 节。后续执行计划见第 23.6 节。 +- 状态:2026-08-12 **M0 代码工作包全部完成**(`M0A-2`、`M0B-1`、`M0B-2` 已合入 M0 集成分支并通过各自门禁,见第 23.4 节);同日 **D6 作废、拓扑改变**,`M0A-1` 交付的文档基线随之失效,需以工作包 `M0A-3` 修订,**修订完成前 M0 不计完整完成**(见第 1.1 节)。2026-08-13 **D9 二次作废、D10 作废,由 D11 取代**:立项策划节点改为 Project Supervisor 通过 `agent.delegate` 发起的静态委派子 Agent,问询复用 PR #165 中转链路(见第 1.1 节「D11 新拓扑」);D11 依赖 WP1(静态委派澄清轮次与返工深度拆分)为强制前置,**该前置已于 2026-08-13 落地并合入**(`WP1` 生产代码 + `WP2` 回归,完成状态与门禁见第 23.5 节),澄清轮次上限现为 3(game-chat source 仍为 1)。随后 `M1A-1`、`M1A-2`、`M1A-3`、`M1A-4` 已分别落地:`M1A-2` 仅收口两层工具面、`project-planning` role brief 注入和 fail-closed 拒绝边界,`M1A-4` 收窄 plan 根 run 的子 Agent 创建面。**2026-08-14 M1B-1 已在隔离 worktree 完成并通过本包验收门禁,已提交于隔离分支 `f453c2ca2`,待合回原分支**:已落地 `.agent/planning` storage module、strict schema/typed 指纹/canonical parser、GDD 版本链、session 原子恢复、Runtime 写入身份及只挡写门禁;golden vector 与 11 个定向 storage 测试通过,writer/index/recovery 及全仓库门禁已完成。**2026-08-14 `M1C-0` 亦已在隔离 worktree 完成并合回本分支**:仅新增用户修订状态及 lineage 分类,不包含审批写入方。`plan.submit_gdd`、审批闭环和 UI 仍未实现,见第 23.6、23.8 节。后续执行计划见第 23.6 节。 - 适用范围:AI 游戏创作独立 App、Project Supervisor、Agent Runtime、本地项目策划 sidecar 与后续完整构建准入 -- 当前实现边界:本文件是后续详细设计与实现的仓库内阶段基线;M0 工作包冻结 Fast GDD 合同并修复现有 owner 验证、game-chat retry 与前端投影边界,`M1A-1`~`M1A-3` 已提供 plan source、两层工具面和角色 brief 的 Runtime 基础,`M1B-1` 当前 worktree 已提供 storage 基础与写入隔离,但尚未宣称最终合入完成;立项策划入口、`plan.submit_gdd` 提交、审批 UI 或构建绑定仍不可用 +- 当前实现边界:本文件是后续详细设计与实现的仓库内阶段基线;M0 工作包冻结 Fast GDD 合同并修复现有 owner 验证、game-chat retry 与前端投影边界,`M1A-1`~`M1A-4` 已提供 plan source、两层工具面、角色 brief 与子 Agent 创建面收窄的 Runtime 基础,`M1C-0` 已提供用户修订 lineage 分类,`M1B-1` 当前 worktree 已提供 storage 基础与写入隔离,但尚未宣称最终合入完成;立项策划入口、`plan.submit_gdd` 提交、审批 UI 或构建绑定仍不可用 ## 1. 背景与目标 @@ -1872,7 +1872,7 @@ M0 完成不表示完整策划闭环已经上线。`M1A-1`、`M1A-2`、`M1A-3` **完成状态**:上述门禁全部通过,`WP1` 生产代码与 `WP2` 回归已合入本分支。已知遗留:`project_supervisor_concurrent_repair_dispatch_creates_exactly_one_delivery` 是 Barrier 同步的并发用例,在本机 Windows 上间歇失败;经与改造前基线对照(同一用例各跑 6 次,改造前后同为 1 次失败)确认为既有抖动,不是本工作包引入,不作为回归处理。 -### 23.6 后续执行计划(2026-08-13 起) +### 23.6 后续执行计划(2026-08-13 起;2026-08-14 状态更新) 本节记录 D11 之后的执行顺序与各步状态,避免「设计结论持续更新、但做到哪了无人维护」。 @@ -1883,7 +1883,7 @@ M0 完成不表示完整策划闭环已经上线。`M1A-1`、`M1A-2`、`M1A-3` | 三 | `project-planning` 的 agentCatalog 登记 | **已完成**(机制冻结见第 3.1 节;**代码亦已落地**,2026-08-13:manifest、prompt bundle、runtime adapter、`prompt.rs` 角色合成分支及四处 needs_change 全部合入) | | 四 | `M0A-3` 批二:拓扑与工具面部分 | **已完成**(2026-08-13),拆解见下 | | 四之余 | schema 与 golden vector 收口 | **已完成**(2026-08-13),拆解见下 | -| 五 | M1 本体:策划闭环功能实现 | **`M1A-1`、`M1A-2`、`M1A-3` 已落地**;`M1B-1` 已于 2026-08-14 在隔离 worktree 完成并通过本包验收门禁,已提交于隔离分支 `f453c2ca2`,待合回原分支:storage 基础、strict schema、typed 指纹、版本链、session 原子恢复、只挡写门禁及 writer/index/recovery 验证均已完成,golden vector 与 11 个定向 storage 测试通过。`M1A-2` 只交付工具面、brief 注入和拒绝边界,不包含 GDD 提交/审批;`M1B-2` 及之后未开始。合入门见第 23.8 节 | +| 五 | M1 本体:策划闭环功能实现 | **`M1A-1`、`M1A-2`、`M1A-3`、`M1A-4`、`M1C-0` 已落地**;`M1B-1` 已于 2026-08-14 在隔离 worktree 完成并通过本包验收门禁,已提交于隔离分支 `f453c2ca2`,待合回原分支:storage 基础、strict schema、typed 指纹、版本链、session 原子恢复、只挡写门禁及 writer/index/recovery 验证均已完成,golden vector 与 11 个定向 storage 测试通过。`M1A-2` 只交付工具面、brief 注入和拒绝边界,`M1C-0` 只交付用户修订 lineage 分类,均不包含 GDD 提交/审批;`M1B-2` 及之后未开始。合入门见第 23.8 节 | 批二在 2026-08-13 拆成两半,因为其中一半在 M1 代码存在之前**做不完**: @@ -1965,7 +1965,7 @@ M0 完成不表示完整策划闭环已经上线。`M1A-1`、`M1A-2`、`M1A-3` | `M1A-4` | plan 根 run 的子 Agent 创建面收窄:`agent.delegate` 目标必须是 `project-planning`、`agent.spawn_isolated` 一律拒;plan source 下不拼 `supervisorIntro` 与 `$visualContract` | `M1A-2` | 执行层与上下文层**都要做且不可互相替代**(同第 19 节第 2 条纪律);`agent.spawn_isolated` 是第二条造子 Agent 的通道,只堵 `agent.delegate` 视为未完成;强判据 `Err` 必须落进拒绝分支而非当作「不是 plan 根」;`project-supervisor-gui/cli` 的委派与 spawn 行为正常路径不变。**必须早于 `M1D-2` 与 `M1E`** | | `M1B-1` | `.agent/planning/` 存储层、strict schema、typed 指纹、版本链;含 `.agent/planning/**` 只挡写判据 | `M1A-2` | **2026-08-14 已在隔离 worktree 完成并通过本包验收门禁,已提交于隔离分支 `f453c2ca2`,待合回原分支**: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 的预审批投影(最新版本 `ready_for_approval`、旧版本 `superseded`);M1C-1 接入 receipt 后必须重建 `approved/revise/reject/superseded` 状态。`plan.submit_gdd`、审批闭环仍留给 `M1B-2` 及后续包;create-only 与等前缀不可变 | | `M1B-2` | `plan.submit_gdd` 原生工具与提交点 | `M1B-1` | 全部拒绝分支;提交点前后强杀恢复;同 submissionId replay 不产生 vN+1 | -| `M1C-0` | 新增 `StaticDelegateContractStatus::UserRevisionRequested` 与分类分支 | `M1A-1` | **本 PR 无写入方、是惰性路径,行为零变化**;做游戏链路返工仍在 `depth=1` 被拒;无该变体的历史记录分类不变 | +| `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-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 重放幂等不增加轮次;答案绑定冲突被拒 |