M1C-0b:补齐静态委派状态前向兼容

- 未知 contractStatus 保留为 Unknown(raw) 并接入完成屏障、等待、返工与谱系门禁

- 补齐 planning Provider、自治 liveness 与终态扫描的 fail-closed 消费路径及回归

- 保持已知状态和损坏 sidecar 行为不变,更新技术方案与共享决策记录
This commit is contained in:
2026-08-15 07:04:40 +00:00
parent deae1e08ce
commit 89bbfd135a
10 changed files with 380 additions and 71 deletions
@@ -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_statusagentId=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_statusagentId=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_statusagentId=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_statusagentId=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 = 0round = 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/【技术方案】立项策划AgentFast 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 为 0lineage 将其按“其它”质量返工分支保守计数(`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/【技术方案】立项策划AgentFast 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 @@
# 立项策划 AgentFast 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 节),澄清轮次上限现为 3game-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 节),澄清轮次上限现为 3game-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-13manifest、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 vector3857 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、审批命令、receiptreceipt 写入上述 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 重放幂等不增加轮次;答案绑定冲突被拒 |