diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/autonomous_policy.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/autonomous_policy.rs index 227681696..da06bd6a8 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/autonomous_policy.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/autonomous_policy.rs @@ -573,17 +573,21 @@ pub(in crate::agent) fn validate_agent_runtime_autonomous_plan_liveness_at( && agent_runtime_autonomous_supervisor_plan_prepares_repair(plan) { let barrier = static_delegate_completion_barrier_at(root, agent_id, run_id)?; - if barrier.ready_unclaimed_count > 0 || barrier.unobserved_claim_count > 0 { + if barrier.ready_unclaimed_count > 0 + || barrier.unobserved_claim_count > 0 + || barrier.unknown_contract_status_count > 0 + { let active_delegations = active_static_delegate_delivery_count_at(root, agent_id, run_id)?; if is_agent_runtime_autonomous_supervisor_delivery_convergence_plan(plan) { return Ok(()); } return Err(format!( - "{AGENT_RUNTIME_AUTONOMOUS_SUPERVISOR_DELIVERY_CONVERGENCE_LIVENESS_ERROR_PREFIX};当前父 run 在准备专业 repair 前仍有 activeDelegations={active_delegations}、readyUnclaimedReceipts={}、unobservedReceiptClaims={}、repairRequired={}。必须先只调用 agent.run_status(agentId=null、scope=all、delegationId=null)原子认领并观察已有回执,再基于权威合同准备 repair", + "{AGENT_RUNTIME_AUTONOMOUS_SUPERVISOR_DELIVERY_CONVERGENCE_LIVENESS_ERROR_PREFIX};当前父 run 在准备专业 repair 前仍有 activeDelegations={active_delegations}、readyUnclaimedReceipts={}、unobservedReceiptClaims={}、repairRequired={}、unknownContractStatus={}。必须先只调用 agent.run_status(agentId=null、scope=all、delegationId=null)原子认领并观察已有回执,再基于权威合同准备 repair", barrier.ready_unclaimed_count, barrier.unobserved_claim_count, barrier.repair_required_count, + barrier.unknown_contract_status_count, )); } } @@ -597,16 +601,18 @@ pub(in crate::agent) fn validate_agent_runtime_autonomous_plan_liveness_at( let active_delegations = active_static_delegate_delivery_count_at(root, agent_id, run_id)?; let must_claim_or_wait = barrier.ready_unclaimed_count > 0 || barrier.unobserved_claim_count > 0 + || barrier.unknown_contract_status_count > 0 || active_delegations >= 3; if must_claim_or_wait { if is_agent_runtime_autonomous_supervisor_delivery_convergence_plan(plan) { return Ok(()); } return Err(format!( - "{AGENT_RUNTIME_AUTONOMOUS_SUPERVISOR_DELIVERY_CONVERGENCE_LIVENESS_ERROR_PREFIX};当前父 run 的 activeDelegations={active_delegations}、waitingDelegations={}、readyUnclaimedReceipts={}、unobservedReceiptClaims={}。必须先只调用 agent.run_status(agentId=null、scope=all、delegationId=null)认领并观察 ready delivery,或等待已有委派推进;不得创建第四次 agent.delegate。收束后若项目 revision 已推进,再验证当前 revision", + "{AGENT_RUNTIME_AUTONOMOUS_SUPERVISOR_DELIVERY_CONVERGENCE_LIVENESS_ERROR_PREFIX};当前父 run 的 activeDelegations={active_delegations}、waitingDelegations={}、readyUnclaimedReceipts={}、unobservedReceiptClaims={}、unknownContractStatus={}。必须先只调用 agent.run_status(agentId=null、scope=all、delegationId=null)认领并观察 ready delivery,或等待已有委派推进;不得创建第四次 agent.delegate。收束后若项目 revision 已推进,再验证当前 revision", barrier.waiting_count, barrier.ready_unclaimed_count, barrier.unobserved_claim_count, + barrier.unknown_contract_status_count, )); } } diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/project_gates.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/project_gates.rs index bdd97774c..6d6cdbc22 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/project_gates.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/project_gates.rs @@ -883,11 +883,17 @@ pub(in crate::agent) fn isolated_join_barrier_has_waiting_groups(detail: &str) - } pub(in crate::agent) fn static_delegate_barrier_has_waiting_deliveries(detail: &str) -> bool { - detail + let waiting = detail .split_whitespace() .find_map(|part| part.strip_prefix("waitingDelegations=")) .and_then(|value| value.parse::().ok()) - .is_some_and(|count| count > 0) + .is_some_and(|count| count > 0); + let unknown_contract_status = detail + .split_whitespace() + .find_map(|part| part.strip_prefix("unknownContractStatus=")) + .and_then(|value| value.parse::().ok()) + .is_some_and(|count| count > 0); + waiting || unknown_contract_status } pub(in crate::agent) fn static_delegate_barrier_requires_repair(detail: &str) -> bool { diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_submit.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_submit.rs index 6cfb34285..0e33f9fa8 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_submit.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_submit.rs @@ -86,6 +86,16 @@ pub(crate) fn capture_plan_provider_structured_injections_at( if clarification_round == u32::MAX { return Err("planning Provider 无法从委派链推导 clarificationRound".to_string()); } + if static_delegate_lineage_contains_unknown_contract_status( + &deliveries, + &session.latest_delegation_id, + ) + .map_err(|error| format!("planning Provider 无法确认委派 contractStatus:{error}"))? + { + return Err( + "planning Provider 委派谱系含更新版本 contractStatus,当前版本拒绝继续".to_string(), + ); + } validate_plan_session_for_clarification_round(&session, clarification_round) .map_err(|error| error.to_string())?; let approval_observation = observations @@ -2146,6 +2156,38 @@ mod tests { assert!(validate_plan_gdd(&gdd).is_ok()); } + #[test] + fn provider_structured_injections_reject_unknown_delegate_lineage() { + let (root, context, _) = submit_fixture(); + let mut delivery = new_static_delegate_delivery( + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + "m1c0b-provider-root-session", + &context.root_run_id, + "m1c0b-provider-parent-action", + &context.delegation_id, + GAME_CREATOR_PROJECT_PLANNING_AGENT_ID, + &context.session_id, + &context.created_by_run_id, + ); + delivery.status = StaticDelegateDeliveryStatus::Ready; + delivery.terminal_status = Some("completed".to_string()); + delivery.result_summary = Some("future status fixture".to_string()); + let mut structured_result = StaticDelegateStructuredResult::default(); + structured_result.contract_status = + StaticDelegateContractStatus::Unknown("future-contract-status".to_string()); + delivery.structured_result = Some(structured_result); + write_static_delegate_delivery_at(&root, &delivery) + .expect("write unknown planning delivery"); + + let error = capture_plan_provider_structured_injections_at(&root, &context.session_id, &[]) + .expect_err("unknown planning lineage must block Provider injection"); + assert!( + error.contains("更新版本 contractStatus"), + "unexpected error: {error}" + ); + cleanup_fixture(root); + } + #[test] fn provider_binding_is_strict_and_recomputes_canonical_request_identity() { let binding = provider_binding(); diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/delivery.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/delivery.rs index a4bbf91db..064a4d6fd 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/delivery.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/delivery.rs @@ -960,7 +960,7 @@ pub(crate) fn publish_game_creator_agent_delegate_result( "contractStatus": delivery .structured_result .as_ref() - .map(|result| result.contract_status), + .map(|result| result.contract_status.clone()), "artifactCount": delivery .structured_result .as_ref() diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/run_status.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/run_status.rs index b58d27a2b..f420a6218 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/run_status.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/run_status.rs @@ -30,7 +30,7 @@ pub(in crate::agent) fn observe_claimed_static_delegate_contract_at( "contractStatus": delivery .structured_result .as_ref() - .map(|result| result.contract_status), + .map(|result| result.contract_status.clone()), } })) .map_err(|error| format!("序列化已认领委派合同失败:{error}"))?; @@ -236,7 +236,7 @@ pub(crate) fn observe_agent_runtime_run_status( "contractStatus": delivery .structured_result .as_ref() - .map(|result| result.contract_status), + .map(|result| result.contract_status.clone()), "needsUserInput": delivery .structured_result .as_ref() 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 9fc62401a..48e8e60b8 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/delegation.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/delegation.rs @@ -31,13 +31,58 @@ pub(crate) enum StaticDelegateDeliveryStatus { Suppressed, } -#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] -#[serde(rename_all = "kebab-case")] +#[derive(Clone, Debug, Eq, PartialEq)] pub(crate) enum StaticDelegateContractStatus { EvidenceReady, NeedsRepair, NeedsUserInput, UserRevisionRequested, + /// A structurally valid durable status introduced by a newer client. + /// + /// The raw wire value is retained so an old client can safely read and + /// rewrite the record without silently changing the newer status. + Unknown(String), +} + +impl StaticDelegateContractStatus { + pub(crate) fn is_unknown(&self) -> bool { + matches!(self, Self::Unknown(_)) + } + + fn durable_value(&self) -> &str { + match self { + Self::EvidenceReady => "evidence-ready", + Self::NeedsRepair => "needs-repair", + Self::NeedsUserInput => "needs-user-input", + Self::UserRevisionRequested => "user-revision-requested", + Self::Unknown(value) => value, + } + } +} + +impl serde::Serialize for StaticDelegateContractStatus { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(self.durable_value()) + } +} + +impl<'de> serde::Deserialize<'de> for StaticDelegateContractStatus { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let value = ::deserialize(deserializer)?; + Ok(match value.as_str() { + "evidence-ready" => Self::EvidenceReady, + "needs-repair" => Self::NeedsRepair, + "needs-user-input" => Self::NeedsUserInput, + "user-revision-requested" => Self::UserRevisionRequested, + _ => Self::Unknown(value), + }) + } } #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] @@ -171,6 +216,7 @@ pub(crate) struct StaticDelegateCompletionBarrier { pub(crate) unobserved_claim_count: usize, pub(crate) repair_required_count: usize, pub(crate) user_input_required_count: usize, + pub(crate) unknown_contract_status_count: usize, } impl StaticDelegateCompletionBarrier { @@ -180,20 +226,22 @@ impl StaticDelegateCompletionBarrier { && self.unobserved_claim_count == 0 && self.repair_required_count == 0 && self.user_input_required_count == 0 + && self.unknown_contract_status_count == 0 } pub(crate) fn has_waiting(self) -> bool { - self.waiting_count > 0 + self.waiting_count > 0 || self.unknown_contract_status_count > 0 } pub(crate) fn detail(self) -> String { format!( - "waitingDelegations={} · readyUnclaimedReceipts={} · unobservedReceiptClaims={} · repairRequired={} · userInputRequired={} · 必须认领专业 Agent 回执,并处理 needs-user-input 或对 needs-repair 原委派发起唯一返工后再继续", + "waitingDelegations={} · readyUnclaimedReceipts={} · unobservedReceiptClaims={} · repairRequired={} · userInputRequired={} · unknownContractStatus={} · 必须认领专业 Agent 回执,处理 needs-user-input/needs-repair,或升级客户端后再继续", self.waiting_count, self.ready_unclaimed_count, self.unobserved_claim_count, self.repair_required_count, - self.user_input_required_count + self.user_input_required_count, + self.unknown_contract_status_count ) } } @@ -510,6 +558,16 @@ pub(crate) fn static_delegate_completion_barrier_at( }) }) .count(); + barrier.unknown_contract_status_count = deliveries + .iter() + .filter(|delivery| { + delivery.status == StaticDelegateDeliveryStatus::ClaimedByParent + && delivery + .structured_result + .as_ref() + .is_some_and(|result| result.contract_status.is_unknown()) + }) + .count(); Ok(barrier) } @@ -1112,6 +1170,18 @@ fn static_delegate_original_is_user_revision_requested( }) } +/// A newer durable contract status is intentionally not a repairable contract. +/// The old client may preserve and report it, but must not manufacture a +/// mutation under semantics it does not understand. +fn static_delegate_original_has_unknown_contract_status( + delivery: &StaticDelegateDeliveryRecord, +) -> bool { + delivery + .structured_result + .as_ref() + .is_some_and(|result| result.contract_status.is_unknown()) +} + /// 沿 repair_of_delegation_id 反向重放整条链,现算目标 delivery 的 /// (repair_depth, clarification_round)。两个维度都是运行时派生值,故意不落盘: /// - 链根(repair_of_delegation_id 为 None):depth = 0,round = 0。 @@ -1129,25 +1199,9 @@ pub(crate) fn static_delegate_lineage_counters( deliveries: &[StaticDelegateDeliveryRecord], delegation_id: &str, ) -> (u32, u32) { - let mut chain: Vec<&StaticDelegateDeliveryRecord> = Vec::new(); - let mut seen_ids: std::collections::BTreeSet<&str> = std::collections::BTreeSet::new(); - let mut current_id = delegation_id; - loop { - if !seen_ids.insert(current_id) || chain.len() >= STATIC_DELEGATE_LINEAGE_MAX_HOPS { - return (u32::MAX, u32::MAX); - } - let Some(node) = deliveries - .iter() - .find(|delivery| delivery.delegation_id == current_id) - else { - return (u32::MAX, u32::MAX); - }; - chain.push(node); - match node.repair_of_delegation_id.as_deref() { - Some(parent_id) => current_id = parent_id, - None => break, - } - } + let Some(mut chain) = static_delegate_lineage_nodes(deliveries, delegation_id) else { + return (u32::MAX, u32::MAX); + }; // chain 目前是 [目标 .. 根],反转成 [根 .. 目标] 便于按 R1/R2/R3 正向传播。 chain.reverse(); let mut depth = 0u32; @@ -1165,6 +1219,49 @@ pub(crate) fn static_delegate_lineage_counters( (depth, round) } +/// Return whether the target's durable lineage contains a status introduced by +/// a newer client. A malformed lineage is an error rather than a negative +/// answer: callers that use this as a mutation/Provider gate must fail closed. +pub(crate) fn static_delegate_lineage_contains_unknown_contract_status( + deliveries: &[StaticDelegateDeliveryRecord], + delegation_id: &str, +) -> Result { + let chain = static_delegate_lineage_nodes(deliveries, delegation_id) + .ok_or_else(|| "静态委派谱系无效,无法检查未知 contractStatus".to_string())?; + Ok(chain.iter().any(|delivery| { + delivery + .structured_result + .as_ref() + .is_some_and(|result| result.contract_status.is_unknown()) + })) +} + +fn static_delegate_lineage_nodes<'a>( + deliveries: &'a [StaticDelegateDeliveryRecord], + delegation_id: &str, +) -> Option> { + let mut chain: Vec<&StaticDelegateDeliveryRecord> = Vec::new(); + let mut seen_ids: std::collections::BTreeSet<&str> = std::collections::BTreeSet::new(); + let mut current_id = delegation_id; + loop { + if !seen_ids.insert(current_id) || chain.len() >= STATIC_DELEGATE_LINEAGE_MAX_HOPS { + return None; + } + let Some(node) = deliveries + .iter() + .find(|delivery| delivery.delegation_id == current_id) + else { + return None; + }; + chain.push(node); + match node.repair_of_delegation_id.as_deref() { + Some(parent_id) => current_id = parent_id, + None => break, + } + } + Some(chain) +} + /// 澄清轮次上限只按发起请求所在 Run 的 source 区分(详见常量注释), /// 与仓库已有的 game-chat 分支先例(见 agent/runtime_tools/delegation.rs 的 /// may_be_game_chat 判定)同源:source 缺失 binding 时按非 game-chat 处理。 @@ -1221,6 +1318,11 @@ pub(crate) fn validate_static_delegate_repair_request_at( if original.target_agent_id != target_agent_id { return Err("静态委派返工必须交回原专业 Agent".to_string()); } + if static_delegate_original_has_unknown_contract_status(original) { + return Err( + "静态委派原 delivery 的 contractStatus 由更新版本写入,当前版本拒绝返工".to_string(), + ); + } let (original_repair_depth, original_clarification_round) = static_delegate_lineage_counters(&deliveries, &original.delegation_id); if static_delegate_original_is_awaiting_clarification(original) { @@ -1238,6 +1340,16 @@ pub(crate) fn validate_static_delegate_repair_request_at( } else if original_repair_depth >= 1 { return Err("静态委派返工深度最多为 1".to_string()); } + if static_delegate_lineage_contains_unknown_contract_status( + &deliveries, + &original.delegation_id, + ) + .map_err(|_| "静态委派谱系计数无效,拒绝继续".to_string())? + { + return Err( + "静态委派原 delivery 所在谱系含更新版本 contractStatus,当前版本拒绝返工".to_string(), + ); + } if original.acceptance_criteria != acceptance_criteria || original.expected_artifacts != expected_artifacts { @@ -2856,22 +2968,48 @@ mod tests { "超过 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")); + for (status, wire) in [ + ( + StaticDelegateContractStatus::EvidenceReady, + "evidence-ready", + ), + (StaticDelegateContractStatus::NeedsRepair, "needs-repair"), + ( + StaticDelegateContractStatus::NeedsUserInput, + "needs-user-input", + ), + ( + StaticDelegateContractStatus::UserRevisionRequested, + "user-revision-requested", + ), + ] { + let encoded = serde_json::to_value(&status).expect("serialize known status"); + assert_eq!(encoded, serde_json::json!(wire)); + assert_eq!( + serde_json::from_value::(encoded) + .expect("round-trip known status"), + status + ); + } + let unknown_wire = "future-contract-status"; + let unknown = + serde_json::from_value::(serde_json::json!(unknown_wire)) + .expect("unknown durable contract status is preserved explicitly"); assert_eq!( - serde_json::from_value::(encoded) - .expect("round-trip user revision status"), - StaticDelegateContractStatus::UserRevisionRequested + unknown, + StaticDelegateContractStatus::Unknown(unknown_wire.to_string()) + ); + assert_eq!( + serde_json::to_value(&unknown).expect("serialize unknown status"), + serde_json::json!(unknown_wire) + ); + assert!( + serde_json::from_value::(serde_json::json!(42)).is_err(), + "non-string durable contract status must still fail closed" ); - 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)。 + // Durable sidecar 的未知变体可读,但必须进入 barrier、拒绝返工,并在读-改-写后 + // 保留原始 wire 字符串;不能吞掉或降级成 Default(NeedsRepair)。 let root = std::env::temp_dir().join(format!( "genarrative-static-unknown-status-{}-{}", std::process::id(), @@ -2882,7 +3020,8 @@ mod tests { )); init_local_game_project_at(&root, "m1c0-unknown-status", "未知静态委派状态解析测试") .expect("project init"); - let mut record = new_static_delegate_delivery( + let acceptance_criteria = vec!["保留未知合同状态".to_string()]; + let mut record = new_static_delegate_delivery_with_contract( GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, "m1c0-unknown-status-session", "m1c0-unknown-status-run", @@ -2891,30 +3030,113 @@ mod tests { "design-director", "m1c0-unknown-status-target-session", "m1c0-unknown-status-target-run", + &acceptance_criteria, + &[], + None, ); - record.status = StaticDelegateDeliveryStatus::Ready; + record.status = StaticDelegateDeliveryStatus::ClaimedByParent; record.terminal_status = Some("completed".to_string()); record.result_summary = Some("unknown status fixture".to_string()); + record.claimed_by_action_id = Some("m1c0-unknown-status-claim-action".to_string()); let mut result = StaticDelegateStructuredResult::default(); - result.contract_status = StaticDelegateContractStatus::UserRevisionRequested; + result.contract_status = StaticDelegateContractStatus::Unknown(unknown_wire.to_string()); record.structured_result = Some(result); - write_static_delegate_delivery_at(&root, &record).expect("write known status fixture"); + write_static_delegate_delivery_at(&root, &record).expect("write unknown status fixture"); + let loaded = read_static_delegate_delivery_at(&root, &record.delegation_id) + .expect("read unknown status fixture") + .expect("unknown status delivery exists"); + assert_eq!(loaded, record); + + let barrier = static_delegate_completion_barrier_at( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + "m1c0-unknown-status-run", + ) + .expect("read unknown status completion barrier"); + assert_eq!(barrier.unknown_contract_status_count, 1); + assert!(!barrier.is_clear()); + assert!(barrier.has_waiting()); + assert!(barrier.detail().contains("unknownContractStatus=1")); + + let repair_error = validate_static_delegate_repair_request_at( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + "m1c0-unknown-status-run", + "m1c0-unknown-status-repair-candidate", + "design-director", + &acceptance_criteria, + &[], + Some(&record.delegation_id), + ) + .expect_err("unknown root status must reject the first repair unconditionally"); + assert!(repair_error.contains("更新版本") && repair_error.contains("拒绝返工")); + + // 读-改-写只改变外层字段时,未知 status 的 raw wire 值必须原样保留。 + let mut rewritten = loaded; + rewritten.result_summary = Some("unknown status rewritten summary".to_string()); + rewritten.updated_at = unix_timestamp(); + write_static_delegate_delivery_at(&root, &rewritten).expect("rewrite unknown status"); 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"), + let rewritten_raw: serde_json::Value = serde_json::from_slice( + &fs::read(&delivery_path).expect("read rewritten unknown status sidecar"), ) - .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}" + .expect("parse rewritten unknown status sidecar"); + assert_eq!( + rewritten_raw["structuredResult"]["contractStatus"], + serde_json::json!(unknown_wire) ); + + // lineage 中的 Unknown 按“其它”分支计数,不能被误识别成用户修订或澄清。 + let mut unknown_lineage = deliveries[..2].to_vec(); + unknown_lineage[0] + .structured_result + .as_mut() + .expect("lineage root structured result") + .contract_status = StaticDelegateContractStatus::Unknown(unknown_wire.to_string()); + assert_eq!( + static_delegate_lineage_counters(&unknown_lineage, &ids[1]), + (1, 0) + ); + assert!(static_delegate_lineage_contains_unknown_contract_status( + &unknown_lineage, + &ids[1] + ) + .expect("inspect unknown lineage")); + + // 损坏仍按原有整体 fail-closed 语义处理;即使目录里另有合法 delivery,也不能 + // 跳过坏记录后继续计算 barrier/lineage。 + let unrelated = new_static_delegate_delivery( + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + "m1c0-unrelated-session", + "m1c0-unrelated-run", + "m1c0-unrelated-action", + "m1c0-unrelated-delivery", + "art-director", + "m1c0-unrelated-target-session", + "m1c0-unrelated-target-run", + ); + write_static_delegate_delivery_at(&root, &unrelated) + .expect("write unrelated valid delivery"); + for (label, bytes) in [ + ("截断 JSON", b"{not-json".to_vec()), + ("非 UTF-8", vec![b'{', 0xff, b'}']), + ( + "超限 sidecar", + vec![b' '; STATIC_DELEGATE_DELIVERY_MAX_BYTES + 1], + ), + ] { + fs::write(&delivery_path, &bytes).expect("write damaged delivery sidecar"); + let error = list_static_delegate_deliveries_at(&root) + .expect_err("damaged sidecar must lock the whole delivery directory"); + assert!( + error.contains("静态委派 delivery"), + "{label} returned an unrelated error: {error}" + ); + write_static_delegate_delivery_at(&root, &rewritten) + .expect("restore valid unknown status sidecar"); + } fs::remove_dir_all(root).ok(); } } diff --git a/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/terminal_classification.rs b/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/terminal_classification.rs index 155ac9ac3..4cefa163f 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/terminal_classification.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/terminal_classification.rs @@ -322,6 +322,10 @@ pub(super) fn original_delivery_has_successful_repair( claimed_deliveries: &[StaticDelegateDeliveryRecord], ) -> bool { original.repair_of_delegation_id.is_none() + && !original + .structured_result + .as_ref() + .is_some_and(|result| result.contract_status.is_unknown()) && claimed_deliveries.iter().any(|candidate| { candidate.repair_of_delegation_id.as_deref() == Some(original.delegation_id.as_str()) && candidate.terminal_status.as_deref() == Some("completed") diff --git a/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/tests.rs b/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/tests.rs index 05e6e3049..2d4fd8f55 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/tests.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/tests.rs @@ -1293,6 +1293,26 @@ fn original_specialist_failure_is_recoverable_but_repair_failure_closes() { classify_failed_specialist(&parent, &child, Some(&repair), false), SwarmSpecialistFailureDisposition::Failed ); + + let mut successful_repair = repair.clone(); + successful_repair.terminal_status = Some("completed".to_string()); + let mut evidence_ready = StaticDelegateStructuredResult::default(); + evidence_ready.contract_status = StaticDelegateContractStatus::EvidenceReady; + successful_repair.structured_result = Some(evidence_ready); + assert!(original_delivery_has_successful_repair( + &original, + &[successful_repair.clone()] + )); + + let mut unknown_original = original.clone(); + let mut unknown_result = StaticDelegateStructuredResult::default(); + unknown_result.contract_status = + StaticDelegateContractStatus::Unknown("future-contract-status".to_string()); + unknown_original.structured_result = Some(unknown_result); + assert!( + !original_delivery_has_successful_repair(&unknown_original, &[successful_repair]), + "a newer contract status must not be classified as already repaired" + ); } #[test] diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 8791b80a8..845f9949e 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -12,6 +12,15 @@ - **拆包与门禁。** ①② 进 `M1C-1`(它们约束的正是 `M1C-1` 新增的写入方);③ 拆 `M1C-0b`,与 `M1C-0` 同形状——无写入方、纯读路径、对既有记录零行为变化可证;塞进审批闭环的 diff 就是重犯第 23.8 节「拆包纪律」自己写的错。`M1C-1` 门禁因此为二选一:`M1C-0b` 先落,或直接上线并在 release note 明写「用过策划审批后回滚旧版本会让该项目静态委派面不可用」;**建议前者**,多机/版本不一致不需要用户主动回滚就会发生。`WP1` 的 depth≤1 结论仍未被推翻。 - 关联文档:`docs/technical/【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md` 第 23.7 节「落地约束」裁决一/二/三与「回归必须覆盖」、第 23.8 节 `M1C-0b` / `M1C-1` 行与「拆包纪律」。 +## 2026-08-15 M1C-0b:静态委派 durable status 前向兼容实现完成 + +- **落地**:`StaticDelegateContractStatus` 采用手写 serde。四个已知 durable 值保持原有字符串;结构良好的未知字符串解析为 `Unknown(raw)`,并在再次序列化及读-改-写时原样保留 raw;非字符串输入仍拒绝。该包只改读路径,不新增状态写入方。 +- **门禁**:`Unknown` 进入 completion barrier 的独立计数与 waiting blocker,不能让 Supervisor 在未知状态未处理时收束;返工入口遇到 `Unknown` 无条件拒绝,即使 lineage depth 为 0;lineage 将其按“其它”质量返工分支保守计数(`depth + 1`、`round = 0`),只会拒绝、不会放宽额度。 +- **损坏边界**:截断、非法 JSON、非 UTF-8 或超过 128 KiB 的 sidecar 仍维持整目录 fail closed,不改成单条跳过或告警,以免 barrier 少计数而错误放行。 +- **范围与依赖**:不包含 `M1C-1` 的审批写入、`gdd-approval` pending、receipt、UI 或构建准入;不 bump durable schema 版本。为覆盖所有既有读路径,补了 planning Provider、自治 liveness 与终态扫描的 fail-closed 消费门,但没有新增或改变 `M1B-*` 功能依赖;该包仍可在 `M1C-0` 基础上独立验证。既有三种状态及历史记录行为保持不变。 +- **结论**:`WP1` 的 `repair_depth≤1` 结论未被推翻;Unknown 只增加前向不兼容时的保守阻塞,不会放宽普通做游戏链路的返工深度门。 +- **验收门禁与关联**:定向回归须覆盖未知 raw round-trip、barrier/waiting、无条件返工拒绝、保守 lineage 分类,以及损坏 sidecar 整体锁死;详见技术方案第 23.7 节裁决三与第 23.8 节 `M1C-0b` 门禁。 + ## 2026-08-14 M1C-0 合入复核:三条 M1C-1 前置、一条文档订正、一条已排除假设 合入 `M1C-0`(见本文件同日条)后对该包做整组复核。**结论:包本身可合,「今天行为零变化」成立且可证**;但新变体在 `M1C-1` 写入之日会同时点亮三个默认值,而这三个默认值都不是裁决出来的,是「给用 `==` 比较(而非穷尽 `match`)的 durable enum 加变体,编译器不报,新变体静默落到作者没想过的那一侧」的结果——与本文件「修复 M1A-2 引入的回归」条同源,是同一失败模式的第二次出现。(复核完成后 `M1B-2` 已合入;其对 `delegation.rs` 的改动是 rustfmt 换行、无语义变化,本条结论不受影响。) @@ -19,7 +28,7 @@ - **不可达性已实证,不是靠注释。** `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` 行。 +- **前置三:前向兼容失败的粒度是整个子系统,不是单条记录。** `list_static_delegate_deliveries_at` 对每条 sidecar 用 `?` 上抛,一条解析失败即整个目录枚举失败,barrier、lineage、返工校验、run status 投影同时不可用。`M1C-0` 当时有意不 bump `STATIC_DELEGATE_DELIVERY_SCHEMA_VERSION`,并在自身范围内把「未知状态 serde 直接报错」记为安全属性;**该前向兼容语义现由 `M1C-0b` 改为显式 `Unknown`,损坏输入仍整体锁死**。注意 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 门拒,用例变红)。 @@ -40,8 +49,8 @@ - 落地:`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 覆盖;无新状态的历史记录继续按原质量返工分类。 +- 范围:本包没有任何审批状态写入方、`gdd-approval` pending、receipt 或前端状态;`UserRevisionRequested` 仍由后续 `M1C-1` 的审批命令与 receipt 同步写入。本包不 bump `STATIC_DELEGATE_DELIVERY_SCHEMA_VERSION`,也不处理未知 durable status;禁止将未知值静默降级为 `NeedsRepair`,其显式 `Unknown` 前向兼容由后续 `M1C-0b` 收口。 +- 回归:新增连续用户修订、保留已有澄清轮次、普通 depth=1 拒绝、32-hop fail-closed 与 `user-revision-requested` serde round-trip;未知 durable variant 的显式 `Unknown` 前向兼容与损坏输入锁死由 `M1C-0b` 单独覆盖,无新状态的历史记录继续按原质量返工分类。 - 关联文档:`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` 推翻的表述。 diff --git a/docs/technical/【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md b/docs/technical/【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md index dffe33e29..4e4b7b9b8 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-4` 已分别落地:`M1A-2` 仅收口两层工具面、`project-planning` role brief 注入和 fail-closed 拒绝边界,`M1A-4` 收窄 plan 根 run 的子 Agent 创建面。**2026-08-14 `M1B-1` 已通过门禁并合入本分支**:已落地 `.agent/planning` storage module、strict schema/typed 指纹/canonical parser、GDD 版本链、session 原子恢复、Runtime 写入身份及只挡写门禁;golden vector 与 11 个定向 storage 测试通过,writer/index/recovery 门禁已完成。**2026-08-15 `M1B-2` 工作包已通过本包门禁并以 `27c3eb847` 合入本分支**:已落地 `plan.submit_gdd`、exact planning Provider binding/structured injection、专用提交点与崩溃恢复;本包不包含 `gdd-approval` planning pending、审批等待、receipt、审批命令或 UI。**2026-08-14 `M1C-0` 已合回本分支**:仅新增用户修订状态及 lineage 分类,不包含审批写入方。当前仅表示 M1B-2 工作包完成,完整审批闭环、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` 已通过门禁并合入本分支**:已落地 `.agent/planning` storage module、strict schema/typed 指纹/canonical parser、GDD 版本链、session 原子恢复、Runtime 写入身份及只挡写门禁;golden vector 与 11 个定向 storage 测试通过,writer/index/recovery 门禁已完成。**2026-08-15 `M1B-2` 工作包已通过本包门禁并以 `27c3eb847` 合入本分支**:已落地 `plan.submit_gdd`、exact planning Provider binding/structured injection、专用提交点与崩溃恢复;本包不包含 `gdd-approval` planning pending、审批等待、receipt、审批命令或 UI。**2026-08-14 `M1C-0` 已合回本分支**:仅新增用户修订状态及 lineage 分类,不包含审批写入方。**2026-08-15 `M1C-0b` 已通过定向门禁并完成**:只改静态委派 durable status 的前向兼容读路径,未知字符串显式保留为 `Unknown(raw)` 并最大化阻塞;不含审批写入方。当前仍未实现完整审批闭环、UI、构建准入,见第 23.6、23.8 节。后续执行计划见第 23.6 节。 - 适用范围:AI 游戏创作独立 App、Project Supervisor、Agent Runtime、本地项目策划 sidecar 与后续完整构建准入 -- 当前实现边界:本文件是后续详细设计与实现的仓库内阶段基线;M0 工作包冻结 Fast GDD 合同并修复现有 owner 验证、game-chat retry 与前端投影边界,`M1A-1`~`M1A-4` 已提供 plan source、两层工具面、角色 brief 与子 Agent 创建面收窄的 Runtime 基础,`M1C-0` 已提供用户修订 lineage 分类,已合入的 `M1B-1` 提供 storage 基础与写入隔离;`M1B-2` 工作包已通过本包门禁并合入,但不构成完整可交付:审批 UI、receipt、审批等待、构建绑定和正式入口仍不可用 +- 当前实现边界:本文件是后续详细设计与实现的仓库内阶段基线;M0 工作包冻结 Fast GDD 合同并修复现有 owner 验证、game-chat retry 与前端投影边界,`M1A-1`~`M1A-4` 已提供 plan source、两层工具面、角色 brief 与子 Agent 创建面收窄的 Runtime 基础,`M1C-0` 已提供用户修订 lineage 分类,已合入的 `M1B-1` 提供 storage 基础与写入隔离;`M1B-2` 工作包已提供提交点与恢复,`M1C-0b` 已补齐静态委派未知 durable status 的前向兼容读路径(包括既有 Provider/终态消费点的 fail-closed guard),但不构成完整可交付:审批 UI、receipt、审批等待、构建绑定和正式入口仍不可用 ## 1. 背景与目标 @@ -1881,7 +1881,7 @@ M0 收口时的三项已知残留,均已定性且不阻塞后续阶段: - `set_task_status` 无秩序守卫的无条件覆盖,属 master 既有问题,另行提 issue,不在 M0 范围内修。 - 第 23.1 节的 M1 入口前置决策:**2026-08-13 起为空**。三项当日全部关闭——「plan source 与 Goal Contract 协议的关系」裁决为进可信 matcher;「plan run 是否允许 steer」裁决为不允许;「checkpoint handoff 私有持久化」随 D10 一并作废、无需裁决。M1 不再有未决的合入前置决策。 -M0 完成不表示完整策划闭环已经上线。`M1A-1`~`M1A-4`、`M1B-1`、`M1B-2` 与 `M1C-0` 已合入,其中 `.agent/planning` strict schema/版本链已由 M1B-1 提供;M1B-2 已提供 `plan.submit_gdd`、exact planning Provider binding/structured injection、专用提交点与崩溃恢复,审批 pending/receipt、前端入口和构建准入仍待后续 M1C~M1E 工作包。第 19 节第 2 条中关于工具面与执行拒绝的目标不变量已由 M1A-2/M1A-4 覆盖,其余 GDD 审批不变量仍按第 23.8 节推进。 +M0 完成不表示完整策划闭环已经上线。`M1A-1`~`M1A-4`、`M1B-1`、`M1B-2`、`M1C-0` 与 `M1C-0b` 已合入,其中 `.agent/planning` strict schema/版本链已由 M1B-1 提供;M1B-2 已提供 `plan.submit_gdd`、exact planning Provider binding/structured injection、专用提交点与崩溃恢复,M1C-0b 已为既有静态委派读路径补未知 durable status 的显式保留与保守阻塞,审批 pending/receipt、前端入口和构建准入仍待后续 M1C~M1E 工作包。第 19 节第 2 条中关于工具面与执行拒绝的目标不变量已由 M1A-2/M1A-4 覆盖,其余 GDD 审批不变量仍按第 23.8 节推进。 ### 23.5 `WP1` / `WP2`:静态委派澄清轮次与返工深度拆分(2026-08-13 完成) @@ -1910,7 +1910,7 @@ M0 完成不表示完整策划闭环已经上线。`M1A-1`~`M1A-4`、`M1B-1` **完成状态**:上述门禁全部通过,`WP1` 生产代码与 `WP2` 回归已合入本分支。已知遗留:`project_supervisor_concurrent_repair_dispatch_creates_exactly_one_delivery` 是 Barrier 同步的并发用例,在本机 Windows 上间歇失败;经与改造前基线对照(同一用例各跑 6 次,改造前后同为 1 次失败)确认为既有抖动,不是本工作包引入,不作为回归处理。 -### 23.6 后续执行计划(2026-08-13 起;2026-08-14 状态更新) +### 23.6 后续执行计划(2026-08-13 起;2026-08-15 状态更新) 本节记录 D11 之后的执行顺序与各步状态,避免「设计结论持续更新、但做到哪了无人维护」。 @@ -1921,7 +1921,7 @@ M0 完成不表示完整策划闭环已经上线。`M1A-1`~`M1A-4`、`M1B-1` | 三 | `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`、`M1A-4`、`M1B-1`、`M1B-2`、`M1C-0` 已落地并合入**;其中 `M1B-1` 的 storage 基础、strict schema、typed 指纹、版本链、session 原子恢复、只挡写门禁及 writer/index/recovery 验证均已完成,golden vector 与 11 个定向 storage 测试通过。**`M1B-2` 工作包已通过本包门禁并合入**:已接入 `plan.submit_gdd`、四类 exact planning v3 binding/structured injection、v4 sole-submit batch、create-only 提交点、index/Markdown/session successor、策划子 run 终止及 generic v5/v4 anchor 恢复;不创建 `gdd-approval` planning pending 或审批等待。`M1C-1` 及之后仍未实现。合入门见第 23.8 节 | +| 五 | M1 本体:策划闭环功能实现 | **`M1A-1`、`M1A-2`、`M1A-3`、`M1A-4`、`M1B-1`、`M1B-2`、`M1C-0`、`M1C-0b` 已落地并合入**;其中 `M1B-1` 的 storage 基础、strict schema、typed 指纹、版本链、session 原子恢复、只挡写门禁及 writer/index/recovery 验证均已完成,golden vector 与 11 个定向 storage 测试通过。**`M1B-2` 工作包已通过本包门禁并合入**:已接入 `plan.submit_gdd`、四类 exact planning v3 binding/structured injection、v4 sole-submit batch、create-only 提交点、index/Markdown/session successor、策划子 run 终止及 generic v5/v4 anchor 恢复;不创建 `gdd-approval` planning pending 或审批等待。**`M1C-0b` 工作包已通过定向门禁并合入**:未知 durable status 解析为 `Unknown(raw)`,进入 completion barrier/waiting blocker,返工/Provider/终态消费点 fail closed,读-改-写保留 raw,损坏 sidecar 仍整目录锁死。`M1C-1` 及之后仍未实现。合入门见第 23.8 节 | 批二在 2026-08-13 拆成两半,因为其中一半在 M1 代码存在之前**做不完**: @@ -1987,7 +1987,7 @@ M0 完成不表示完整策划闭环已经上线。`M1A-1`~`M1A-4`、`M1B-1` **落地约束**: - 拆包(2026-08-13 按第 23.8 节更正):**变体与分类分支在 `M1C-0`**——该 PR 无写入方、是惰性路径、行为零变化,便于把「做游戏链路返工额度未被误放宽」这条关键回归单独证明;**写入方与 receipt 在 `M1C-1` 同进同退**——只置 status 不写 receipt(或反之)会让链路计数与审批事实脱节。这样拆全程不产生半状态:`UserRevisionRequested` 直到 `M1C-1` 才可能被写出。 -- `StaticDelegateContractStatus` 是 durable schema 的一部分。新增变体属**前向兼容**问题(旧代码读不了新记录),非后向兼容问题:只有做方案链路会写它,做游戏链路既有记录不受影响。反序列化必须 fail closed,**不得静默降级为 `NeedsRepair`**——那会让用户修订被误计成返工,正是本裁决要消除的行为。 +- `StaticDelegateContractStatus` 是 durable schema 的一部分。新增 `UserRevisionRequested` 属**前向兼容**问题(旧代码读不了新记录),非后向兼容问题:只有做方案链路会写它,做游戏链路既有记录不受影响。`M1C-0` 本包不处理未知 durable status,但不得把未知值静默降级为 `NeedsRepair`;未知值的前向兼容由 `M1C-0b` 单独收口为显式 `Unknown`(并原样回写),损坏输入仍整体 fail closed。 - **(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 已先失败。**(2026-08-15 订正)此处原写「要处置只能改枚举粒度(单条损坏跳过并告警)」并要求 `M1C-1` 上线前二选一——两者都不成立:单条跳过对 delivery 不安全,裁决见下方「裁决三」。** - **(2026-08-15 裁决一,落在 `M1C-1`)新变体必须进 barrier,且必须是独立的第六个计数 `user_revision_pending_count`。** 不补的后果:`repair_required_count` 与 `user_input_required_count` 都用 `==` 比具体变体,`UserRevisionRequested` 两边都不落,`is_clear()` 因此可能为真——写入方一落地就意味着 **Supervisor 可以在用户修订尚未派出时收束用户任务**(收束门是 `project_gates.rs` 的 `static_delegate_completion_blocker_at_locked`)。不复用现有两个计数的理由是硬的:`repair_required_count` 带 `repair_of_delegation_id.is_none()`,只算原始委派——因 depth≤1,返工的返工本就不该阻塞;而**用户第 2 次修订的父节点自身就是 repair 节点**,并进去会让第 2 次及以后的修订全部不阻塞,恰好在最需要处失效。`user_input_required_count` 的语义是「等用户回答」,与「用户已发话、等 Supervisor 派发」不同,且 `detail()` 文案会说反。判据形状照 `user_input_required_count`——`ClaimedByParent` + 该 status + 无活跃 child(`repair_of` 指向自身且状态属 `Dispatched|Ready|ClaimedByParent`),**不带** `repair_of.is_none()`;该字段正是仓库里「轮次无上限」概念的既有先例。**连带必须逐处裁决**:除 `is_clear()` 与 `detail()` 外,另有四处调用点逐字段读而不走 `is_clear()`(`autonomous_policy.rs` 两处、`runtime_tools/delivery.rs` 两处),加字段不会自动传播过去。这是 `M1C-0` 那个失败模式的同构版本,载体从 enum 变体换成 struct 字段,编译器同样沉默。 - **(2026-08-15 裁决二,落在 `M1C-1`)正向一致性锁死在「只能从 `EvidenceReady` 改写而来」,不得更严。** `UserRevisionRequested` 复用 `EvidenceReady` 的三条客观证据约束:终态为 `completed`、`missing_expected_artifacts` 为空、`verification_required` 时 `verified_revision` 必须存在。依据是本节定的唯一转移——审批命令把一条既有 delivery 从 `EvidenceReady` 改写过来,且第 13.0 节审批前置门保证取证已过才会出现审批卡。产物覆盖校验本就对所有状态生效,「不得携带 `user_input_questions`」已被现有 else 分支挡住,均不需改。**实现形状是本条重点:把 `validate_static_delegate_structured_result` 里按 `contract_status` 分支的 if/else-if 链改成穷尽 `match`、不留 `_` 通配**——同一失败形状在本仓库已出现两次,靠纪律没拦住,穷尽 `match` 把它变成编译错误。**反向风险必须一并记住**:该函数不是入口过滤器,`read_static_delegate_delivery_at` → `validate_static_delegate_delivery_record` 让它**每次读取都跑**;约束定得比写入方实际产出更严,等于把写入方的一个 bug 变成「该 delivery 永久读不出来」,直接触发裁决三的锁死。故不得再加 `EvidenceReady` 自己都没有的条款(例如要求 `error` 为 `None`)。**另订正一处可能的误读**:本条不是既有漏洞——专业 Agent 自证「用户要求修订」今天不可达,唯一派生点 `build_static_delegate_structured_result_at` 只能产出三个旧变体,claim 回执的 `structuredResult` 从 delivery 拷贝,均为 Runtime 侧;本条约束的是 `M1C-1` 引入的**第二个写入方**。 @@ -2007,8 +2007,8 @@ M0 完成不表示完整策划闭环已经上线。`M1A-1`~`M1A-4`、`M1B-1` | `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 已通过门禁并合入本分支**: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-0b` | 静态委派 durable enum 的前向兼容粒度:未知 `contract_status` 解析为显式 `Unknown` 并最大化阻塞 | `M1C-0` | 与 `M1C-0` 同形状:无写入方、纯读路径、对既有记录行为零变化可证。`Unknown` 进 barrier、返工门无条件拒、谱系按最保守分类;损坏(截断/非 UTF-8/超限)仍整体锁死不变;`Serialize` 原样回写原始字符串。裁决与理由见第 23.7 节「落地约束」裁决三。**不依赖 `M1B-*`,可与之并行;若不落,`M1C-1` 须走 release note 兜底** | +| `M1C-0` | 新增 `StaticDelegateContractStatus::UserRevisionRequested` 与分类分支 | `M1A-1` | **已落地**:无审批写入方、是惰性路径;用户修订跳不增 `repair_depth` 也不重置 `clarification_round`,连续修订可通过;做游戏链路返工仍在 `depth=1` 被拒;原有三种状态与 `UserRevisionRequested` 的已知行为保持不变,未知 durable status 的前向兼容由已完成的 `M1C-0b` 显式承接,不在本包静默降级或改变 | +| `M1C-0b` | 静态委派 durable enum 的前向兼容粒度:未知 `contract_status` 解析为显式 `Unknown` 并最大化阻塞 | `M1C-0` | **已完成**:纯读路径、无写入方、审批状态、pending、receipt 或 UI(不含 `M1C-1`)。四种已知 durable 值保持原 serde;未知字符串解析为 `Unknown(raw)`,非字符串仍拒绝,`Serialize` 及读-改-写均原样保留 raw。`Unknown` 计入 completion barrier 与 waiting blocker,返工入口无条件拒绝(含 `depth=0`),lineage 按“其它”最保守分类(`depth + 1`、`round = 0`);planning Provider、自治 liveness、终态扫描等既有读路径同步 fail closed。截断、非法 JSON、非 UTF-8、超过 128 KiB 的 sidecar 仍按整目录 fail closed,不做单条跳过。**不新增或改变 `M1B-*` 功能依赖(仅复核其既有读路径);不包含 `M1C-1` 的审批写入、receipt、UI 或构建准入** | | `M1C-1` | `gdd-approval` pending、审批命令、receipt;receipt 写入上述 status | `M1B-2`、`M1C-0`(前向兼容粒度另见 `M1C-0b`) | 三动作全通;版本+指纹竞态防护;两窗口并发;**连续多次修订均可通过且 `repair_depth` 不变**。**`M1C-0` 合入复核留下的三条已于 2026-08-15 裁决**(全文见第 23.7 节「落地约束」裁决一/二/三与 `decision-log.md` 同日条):① 新增独立 barrier 计数 `user_revision_pending_count`,不并进现有两个,且**不带** `repair_of.is_none()`——否则第 2 次及以后的修订不阻塞;另有四处逐字段读 barrier 的调用点需逐处裁决;② `validate_static_delegate_structured_result` 复用 `EvidenceReady` 的三条客观证据约束,并把该处 if/else-if 链改成穷尽 `match`,但**不得更严**——该函数每次读取都跑,过严会把写入方 bug 变成 delivery 永久读不出;③ 前向兼容粒度移交 `M1C-0b`,本包二选一:`M1C-0b` 先落,或直接上线并在 release note 明写「用过策划审批后回滚旧版本会让该项目静态委派面不可用」 | | `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 重放幂等不增加轮次;答案绑定冲突被拒 |