M1C-0b:补齐静态委派状态前向兼容
- 未知 contractStatus 保留为 Unknown(raw) 并接入完成屏障、等待、返工与谱系门禁 - 补齐 planning Provider、自治 liveness 与终态扫描的 fail-closed 消费路径及回归 - 保持已知状态和损坏 sidecar 行为不变,更新技术方案与共享决策记录
This commit is contained in:
@@ -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,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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::<usize>().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::<usize>().ok())
|
||||
.is_some_and(|count| count > 0);
|
||||
waiting || unknown_contract_status
|
||||
}
|
||||
|
||||
pub(in crate::agent) fn static_delegate_barrier_requires_repair(detail: &str) -> bool {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.durable_value())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for StaticDelegateContractStatus {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let value = <String as serde::Deserialize>::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<bool, String> {
|
||||
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<Vec<&'a StaticDelegateDeliveryRecord>> {
|
||||
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::<StaticDelegateContractStatus>(encoded)
|
||||
.expect("round-trip known status"),
|
||||
status
|
||||
);
|
||||
}
|
||||
let unknown_wire = "future-contract-status";
|
||||
let unknown =
|
||||
serde_json::from_value::<StaticDelegateContractStatus>(serde_json::json!(unknown_wire))
|
||||
.expect("unknown durable contract status is preserved explicitly");
|
||||
assert_eq!(
|
||||
serde_json::from_value::<StaticDelegateContractStatus>(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::<StaticDelegateContractStatus>(serde_json::json!(42)).is_err(),
|
||||
"non-string durable contract status must still fail closed"
|
||||
);
|
||||
let unknown = serde_json::from_value::<StaticDelegateContractStatus>(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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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` 推翻的表述。
|
||||
|
||||
|
||||
@@ -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 重放幂等不增加轮次;答案绑定冲突被拒 |
|
||||
|
||||
Reference in New Issue
Block a user