合并隔离分支的M1C-1与M1C-2a改动

This commit is contained in:
2026-08-17 03:10:18 +00:00
15 changed files with 2640 additions and 78 deletions
@@ -500,6 +500,28 @@ fn agent_runtime_action_receipt_safe_detail_with_owner(
}))
.ok();
}
if observation.tool == "file.read" {
let first_line = observation.detail.as_deref()?.lines().next()?.trim();
let mut fields = first_line.split('·').map(str::trim);
let path = normalize_relative_path(fields.next()?).ok()?;
let sha_field = fields.next()?;
let content_sha256 = sha_field.strip_prefix("sha256=")?;
if content_sha256.len() != 64
|| !content_sha256.bytes().all(|byte| byte.is_ascii_hexdigit())
{
return None;
}
let lines = fields.next()?.strip_prefix("lines ")?;
if lines.is_empty() || path.is_empty() {
return None;
}
return serde_json::to_string(&serde_json::json!({
"path": path,
"contentSha256": content_sha256,
"lines": lines,
}))
.ok();
}
if observation.tool == GAME_CREATOR_MCP_CALL_TOOL {
return game_creator_mcp_public_result_metadata(
observation.detail.as_deref().unwrap_or_default(),
@@ -128,10 +128,8 @@ pub(in crate::agent) fn build_game_creator_agent_background_tool_plan_request(
};
let tool_policy =
agent_runtime_tool_policy_snapshot_for_run_at(root, agent_id, run_id, None, None)?;
let root_control_authority = read_game_creator_agent_runtime_run_profile_binding(
root, agent_id, run_id,
)?
.is_some_and(|binding| {
let root_binding = read_game_creator_agent_runtime_run_profile_binding(root, agent_id, run_id)?;
let root_control_authority = root_binding.as_ref().is_some_and(|binding| {
binding.agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
&& binding.root_agent_id == binding.agent_id
&& binding.root_run_id == binding.run_id
@@ -139,6 +137,18 @@ pub(in crate::agent) fn build_game_creator_agent_background_tool_plan_request(
&& binding.parent_run_id.is_none()
&& agent_runtime_supervisor_source_is_trusted(&binding.source)
});
let plan_root_candidate = root_binding
.as_ref()
.is_some_and(|binding| binding.source == AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE);
let plan_root = if plan_root_candidate {
// Source is the weak discriminator. Once it says "plan", every
// parent/profile/root field must pass the shared strong predicate;
// drift must not silently downgrade to the dynamic Goal Contract.
validate_project_supervisor_plan_root_binding_at(root, agent_id, run_id)?;
true
} else {
false
};
let root_goal_contract_context =
render_game_creator_agent_runtime_goal_contract_for_prompt_at(root, agent_id, run_id)?
.unwrap_or_else(|| "null".to_string());
@@ -301,7 +311,18 @@ pub(in crate::agent) fn build_game_creator_agent_background_tool_plan_request(
"{prompt}\n\n补充协议:project.verify 的 script 除 check、typecheck、test、lint、build 外,还可使用 check:<name>、test:<name>(例如 test:unit)、lint:<name>、typecheck:<name>、build:<name>、verify:<name>、validate:<name> 形式的命名脚本;冒号后的每个非空段必须以字母或数字开头且只能包含字母、数字、连字符、下划线或点,并且 script 与 expectedCommand 都必须原样来自项目根 package.json。{command_exec_contract};args 中的项目路径必须相对 cwd,禁止绝对路径、file URI、路径加行号以及把绝对路径嵌入脚本或说明文字。该工具默认需要精确确认,适合运行定向测试、构建检查和只读诊断。durable command.exec observation 会直接返回可复用的 sourceActionId;短 observation 不足以定位失败时,使用 command.output_read {{\"actionId\":\"该 sourceActionId\",\"startLine\":1,\"maxLines\":160}} 分页读取同一 Agent 的已清洗命令输出,并按 nextLine 继续,不要先猜 actionId 或为取得它额外查询动作历史,也不得仅凭输出尾部猜测。只有 cargo check/test/clippy/fmt/build、npm test 或命名为 check/typecheck/test/lint/build/verify/validate 的验证脚本,以及精确 node --test 测试文件可签发验证凭证;git、rg、cargo metadata 和普通 npm run 只作为诊断结果。每次成功执行 file.write、file.patch、file.delete、project.patchset 或 project.restore,以及每次真正启动 command.exec 或 command.start,都会产生新的项目 revision;最后一次修改后必须使用当前角色可用的 project.verify、可验证 command.exec 或 game.static_smoke 完成验证,才能调用 respond_to_user。文件回读不能替代验证,验证后再次修改必须重新验证。每 {AGENT_RUNTIME_BACKGROUND_LOOP_LIMIT} 轮只是一次进度 checkpoint 与停滞检测,不是上下文压缩或 run 的终止上限;只要 observation 出现新的独立进展,就在同一 run 继续下一窗口,只有窗口没有新进展时才按停滞处理。真正的上下文压缩仅由 token 阈值或显式 compact 触发。"
)
};
let prompt = if root_control_authority {
let prompt = if plan_root && root_goal_contract_required {
format!(
"{prompt}\n\n立项策划 Goal Contract 首轮协议:本轮必须且只能调用一次 agent.goal_contract。outcome、nonNegotiables、forbiddenAssumptions、openQuestions 必须来自你对当前用户意图的理解,preferences 必须提交空数组;acceptanceNodes 不得自定义,必须精确提交单个固定节点 {{\"criterionId\":\"{PLAN_FAST_GDD_ACCEPTANCE_NODE_ID}\",\"criterion\":\"{PLAN_FAST_GDD_ACCEPTANCE_NODE_CRITERION}\",\"required\":true,\"requiredEvidence\":[\"{PLAN_FAST_GDD_ACCEPTANCE_NODE_EVIDENCE}\"],\"dependsOn\":[]}}",
PLAN_FAST_GDD_ACCEPTANCE_NODE_ID = PLAN_FAST_GDD_ACCEPTANCE_NODE_ID,
PLAN_FAST_GDD_ACCEPTANCE_NODE_CRITERION = PLAN_FAST_GDD_ACCEPTANCE_NODE_CRITERION,
PLAN_FAST_GDD_ACCEPTANCE_NODE_EVIDENCE = PLAN_FAST_GDD_ACCEPTANCE_NODE_EVIDENCE,
)
} else if plan_root {
format!(
"{prompt}\n\n立项策划 Goal Contract 已冻结且不可重写。先完成 project-planning 委派;收到其 Fast GDD 提交后,由当前 Supervisor 根 Run 从 startLine=1 开始分页、无缺口且无重叠地读取 game/fast_gdd.md 直到 EOF,各页必须来自同一内容 SHA-256,并在 agent.acceptance_update.evidence 中提交全部分页 file.read 的 actionId。取证未通过时不得展示或创建审批卡,只能针对原策划 delivery 返工。"
)
} else if root_control_authority {
format!(
"{prompt}\n\n动态目标协议:agent.goal_contract 只允许当前可信根 Project Supervisor 调用,input 使用 {{\"outcome\":\"最终交付\",\"nonNegotiables\":[],\"preferences\":[],\"forbiddenAssumptions\":[],\"openQuestions\":[],\"acceptanceNodes\":[{{\"criterionId\":\"稳定短 ID\",\"criterion\":\"可核对标准\",\"required\":true,\"requiredEvidence\":[\"tool:project.verify\"],\"dependsOn\":[]}}]}}。该合同必须来自你对当前用户意图的理解;固定规则、关键词、资产探测和专家建议只能作为上下文,不能替你决定目标、工作流或实现方案。agent.acceptance_update 也只允许同一根 Supervisor 调用,input 使用 {{\"contractFingerprint\":\"当前合同指纹\",\"evaluations\":[{{\"criterionId\":\"节点 ID\",\"status\":\"passed|failed|not-observed\",\"evidence\":[{{\"agentId\":\"证据生产者\",\"runId\":\"证据 run\",\"actionId\":\"持久成功动作\"}}],\"summary\":\"结论\"}}]}}。只提交本轮实际重新验收的节点;未提交的 passed 节点保持不变。"
)
@@ -450,6 +471,9 @@ pub(in crate::agent) fn build_game_creator_agent_background_tool_plan_request(
mcp_catalog,
)?)
.with_tool_choice(platform_llm::LlmToolChoice::Required);
if plan_root {
restrict_plan_root_goal_contract_schema(&mut request.function_tools)?;
}
if runtime_owner_artifact_validation_available {
remove_autonomous_owner_manual_verification_tools(&mut request.function_tools)?;
}
@@ -530,9 +554,16 @@ pub(in crate::agent) fn build_game_creator_agent_background_tool_plan_request(
}
if root_goal_contract_required {
restrict_agent_runtime_root_goal_contract_tools(&mut request)?;
request.messages.push(LlmMessage::user(
"当前根 Run 尚未冻结 Goal Contract。本轮唯一可用工具是 agent.goal_contract;必须且只能调用一次,用 outcome 具体概括当前用户最终意图,acceptanceNodes 至少提交一项可核对标准。每个 requiredEvidence 必须选择在该标准所有合法结果下都能成功产生回执的工具;环境探测可能以 rejected/failed 表示正常否定结果时,不得把该探测工具写成必需成功回执(例如非 Git 项目不得要求 git.inspect 成功,应使用 project.index 的成功回执证明 isRepository=false)。nonNegotiables、preferences、forbiddenAssumptions、openQuestions 没有内容时传空数组。不得调用 update_agent_plan、respond_to_user 或任何其他动作,不得输出普通文本。",
));
let goal_contract_instruction = if plan_root {
format!(
"当前 plan 根 Run 尚未冻结 Goal Contract。本轮唯一可用工具是 agent.goal_contract;必须且只能调用一次。outcome 具体概括当前用户最终意图,nonNegotiables、forbiddenAssumptions、openQuestions 没有内容时传空数组,preferences 必须始终传空数组;acceptanceNodes 必须精确提交固定单节点 {{\"criterionId\":\"{PLAN_FAST_GDD_ACCEPTANCE_NODE_ID}\",\"criterion\":\"{PLAN_FAST_GDD_ACCEPTANCE_NODE_CRITERION}\",\"required\":true,\"requiredEvidence\":[\"{PLAN_FAST_GDD_ACCEPTANCE_NODE_EVIDENCE}\"],\"dependsOn\":[]}}。不得调用 update_agent_plan、respond_to_user 或任何其他动作,不得输出普通文本。"
)
} else {
"当前根 Run 尚未冻结 Goal Contract。本轮唯一可用工具是 agent.goal_contract;必须且只能调用一次,用 outcome 具体概括当前用户最终意图,acceptanceNodes 至少提交一项可核对标准。每个 requiredEvidence 必须选择在该标准所有合法结果下都能成功产生回执的工具;环境探测可能以 rejected/failed 表示正常否定结果时,不得把该探测工具写成必需成功回执(例如非 Git 项目不得要求 git.inspect 成功,应使用 project.index 的成功回执证明 isRepository=false)。nonNegotiables、preferences、forbiddenAssumptions、openQuestions 没有内容时传空数组。不得调用 update_agent_plan、respond_to_user 或任何其他动作,不得输出普通文本。".to_string()
};
request
.messages
.push(LlmMessage::user(goal_contract_instruction));
}
let mut request = apply_game_creator_llm_reasoning_effort(request, &llm)?;
request = if agent_id == GAME_CREATOR_PROJECT_PLANNING_AGENT_ID {
@@ -762,7 +793,9 @@ mod tests {
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE, AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE,
AGENT_RUNTIME_UPDATE_PLAN_FUNCTION_NAME, GAME_CREATOR_PROJECT_PLANNING_AGENT_ID,
GAME_CREATOR_PROJECT_PLANNING_FINAL_REPLY_SYSTEM_PROMPT,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, RUNTIME_PROMPT_SUPERVISOR_CHAT_COMPOSITION,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, PLAN_FAST_GDD_ACCEPTANCE_NODE_CRITERION,
PLAN_FAST_GDD_ACCEPTANCE_NODE_EVIDENCE, PLAN_FAST_GDD_ACCEPTANCE_NODE_ID,
RUNTIME_PROMPT_SUPERVISOR_CHAT_COMPOSITION,
};
fn native_input_required_fields(
@@ -1298,6 +1331,173 @@ mod tests {
.contains("本轮唯一可用工具是 agent.goal_contract")));
}
#[test]
fn plan_root_goal_contract_prompt_and_schema_change_after_the_first_turn() {
let directory = crate::tests::canonical_test_tempdir("provider-plan-goal-control-");
let root = directory.path().join("project");
init_local_game_project_at(&root, "plan-goal-control-project", "形成 Fast GDD")
.expect("project init");
let binding = bind_game_creator_agent_runtime_run_profile_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
"provider-plan-goal-control-root",
AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE,
Some(AGENT_RUNTIME_RUN_PROFILE_STANDARD),
None,
)
.expect("bind plan root");
let state = start_game_creator_agent_runtime_task_at(
&root,
&binding.agent_id,
"形成 Fast GDD",
&binding.run_id,
&binding.source,
"冻结立项目标",
vec!["冻结 Goal Contract".to_string()],
)
.expect("start plan root");
let catalog = GameCreatorMcpCatalog {
fingerprint: String::new(),
servers: Vec::new(),
tools: Vec::new(),
};
let (_, _, first, _) = build_game_creator_agent_background_tool_plan_request(
&root,
&state.agent_id,
&state.session_id,
&state.run_id,
&state.current_task,
&[],
0,
&catalog,
)
.expect("build first plan root request");
let goal_function =
crate::agent_native_tools::native_runtime_function_name("agent.goal_contract")
.expect("goal function name");
let fixed_schema = first
.function_tools
.iter()
.find(|function| function.name == goal_function)
.expect("plan goal function")
.parameters
.pointer("/properties/input/properties/acceptanceNodes")
.expect("fixed acceptance schema");
let fixed_preferences = first
.function_tools
.iter()
.find(|function| function.name == goal_function)
.expect("plan goal function")
.parameters
.pointer("/properties/input/properties/preferences/maxItems")
.expect("fixed preferences schema");
assert_eq!(first.function_tools.len(), 1);
assert_eq!(fixed_schema["maxItems"], serde_json::json!(1));
assert_eq!(fixed_preferences, &serde_json::json!(0));
assert_eq!(
fixed_schema["items"]["properties"]["criterionId"]["enum"],
serde_json::json!([PLAN_FAST_GDD_ACCEPTANCE_NODE_ID])
);
let first_prompt = first
.messages
.iter()
.map(|message| message.content.as_str())
.collect::<Vec<_>>()
.join("\n");
assert!(first_prompt.contains("Goal Contract 首轮协议"));
assert!(!first_prompt.contains("acceptanceNodes 至少提交一项"));
assert!(!first_prompt.contains("project.index 的成功回执"));
crate::agent::create_game_creator_agent_runtime_goal_contract_at(
&root,
&state.agent_id,
&state.run_id,
&state.current_task,
&AgentRuntimeGoalContractDraft {
outcome: "形成服务用户意图的 Fast GDD".to_string(),
non_negotiables: Vec::new(),
preferences: Vec::new(),
forbidden_assumptions: Vec::new(),
open_questions: Vec::new(),
acceptance_nodes: vec![AgentRuntimeGoalContractAcceptanceNodeDraft {
criterion_id: PLAN_FAST_GDD_ACCEPTANCE_NODE_ID.to_string(),
criterion: PLAN_FAST_GDD_ACCEPTANCE_NODE_CRITERION.to_string(),
required: true,
required_evidence: vec![PLAN_FAST_GDD_ACCEPTANCE_NODE_EVIDENCE.to_string()],
dependencies: Vec::new(),
}],
},
)
.expect("freeze plan contract");
let (_, _, later, _) = build_game_creator_agent_background_tool_plan_request(
&root,
&state.agent_id,
&state.session_id,
&state.run_id,
&state.current_task,
&[],
1,
&catalog,
)
.expect("build later plan root request");
let later_prompt = later
.messages
.iter()
.map(|message| message.content.as_str())
.collect::<Vec<_>>()
.join("\n");
assert!(later.function_tools.len() > 1);
assert!(later_prompt.contains("Goal Contract 已冻结且不可重写"));
assert!(later_prompt.contains("从 startLine=1 开始分页"));
assert!(later_prompt.contains("直到 EOF"));
assert!(later_prompt.contains("全部分页 file.read 的 actionId"));
assert!(!later_prompt.contains("Goal Contract 首轮协议"));
}
#[test]
fn plan_source_profile_drift_cannot_fall_back_to_dynamic_goal_contract() {
let directory = crate::tests::canonical_test_tempdir("provider-plan-profile-drift-");
let root = directory.path().join("project");
init_local_game_project_at(&root, "plan-profile-drift", "形成 Fast GDD")
.expect("project init");
let binding = bind_game_creator_agent_runtime_run_profile_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
"provider-plan-profile-drift-root",
AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE,
Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD),
None,
)
.expect("bind deliberately drifted plan root");
let state = start_game_creator_agent_runtime_task_at(
&root,
&binding.agent_id,
"形成 Fast GDD",
&binding.run_id,
&binding.source,
"冻结立项目标",
Vec::new(),
)
.expect("start drifted plan root");
let error = build_game_creator_agent_background_tool_plan_request(
&root,
&state.agent_id,
&state.session_id,
&state.run_id,
&state.current_task,
&[],
0,
&GameCreatorMcpCatalog {
fingerprint: String::new(),
servers: Vec::new(),
tools: Vec::new(),
},
)
.expect_err("plan source must pass the shared strong predicate");
assert!(error.contains("project-supervisor-plan standard 顶层根 Run"));
}
#[test]
fn provider_request_source_does_not_patch_natural_language_with_replace_chains() {
let source = include_str!("provider_request_builders.rs");
@@ -118,6 +118,28 @@ fn provider_collaboration_repair_instruction(protocol_error: &str, section_id: &
)
}
fn restrict_root_goal_contract_repair_request(
request: &mut LlmRunRequest,
plan_root: bool,
) -> Result<(), String> {
if plan_root {
restrict_plan_root_goal_contract_schema(&mut request.function_tools)?;
}
restrict_agent_runtime_root_goal_contract_tools(request)
}
fn root_goal_contract_repair_instruction(protocol_error: &str, plan_root: bool) -> String {
if plan_root {
format!(
"上一条输出不符合工具计划协议:{protocol_error}\n当前 plan 根 Run 尚未冻结 Goal Contract。本次修复的原生工具目录只保留 agent.goal_contract;必须且只能调用一次。outcome 具体概括当前用户最终意图,nonNegotiables、forbiddenAssumptions、openQuestions 没有内容时传空数组,preferences 必须始终传空数组;acceptanceNodes 必须精确提交固定单节点 {{\"criterionId\":\"{PLAN_FAST_GDD_ACCEPTANCE_NODE_ID}\",\"criterion\":\"{PLAN_FAST_GDD_ACCEPTANCE_NODE_CRITERION}\",\"required\":true,\"requiredEvidence\":[\"{PLAN_FAST_GDD_ACCEPTANCE_NODE_EVIDENCE}\"],\"dependsOn\":[]}}。不得调用 update_agent_plan、respond_to_user 或任何其他动作,不得输出普通文本、解释、markdown 或代码围栏。"
)
} else {
format!(
"上一条输出不符合工具计划协议:{protocol_error}\n当前根 Run 尚未冻结 Goal Contract。本次修复的原生工具目录只保留 agent.goal_contract;必须且只能调用一次,用 outcome 具体概括当前用户最终意图,acceptanceNodes 至少提交一项可核对标准。每个 requiredEvidence 必须选择在该标准所有合法结果下都能成功产生回执的工具;环境探测可能以 rejected/failed 表示正常否定结果时,不得把该探测工具写成必需成功回执(例如非 Git 项目不得要求 git.inspect 成功,应使用 project.index 的成功回执证明 isRepository=false)。nonNegotiables、preferences、forbiddenAssumptions、openQuestions 没有内容时传空数组。不得调用 update_agent_plan、respond_to_user 或任何其他动作,不得输出普通文本、解释、markdown 或代码围栏。"
)
}
}
pub(in crate::agent) fn append_game_creator_agent_tool_plan_audit_idempotent(
root: &Path,
record: serde_json::Value,
@@ -173,6 +195,15 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
let initial_request_slot = format!("loop-{loop_index}-repair-0");
let (run_profile, _) =
agent_runtime_run_profile_identity_at(root, agent_id, run_id, None, None)?;
let plan_root_candidate =
read_game_creator_agent_runtime_run_profile_binding(root, agent_id, run_id)?
.is_some_and(|binding| binding.source == AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE);
let plan_root = if plan_root_candidate {
validate_project_supervisor_plan_root_binding_at(root, agent_id, run_id)?;
true
} else {
false
};
let mcp_catalog = if agent_id == GAME_CREATOR_PROJECT_PLANNING_AGENT_ID {
// Planning has a structurally empty MCP surface. Do not even resolve
// the project MCP catalog here: doing so can start/connect required
@@ -1033,10 +1064,13 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
}
}
if force_root_goal_contract {
restrict_agent_runtime_root_goal_contract_tools(&mut request)?;
request.messages.push(LlmMessage::user(format!(
"上一条输出不符合工具计划协议:{protocol_error}\n当前根 Run 尚未冻结 Goal Contract。本次修复的原生工具目录只保留 agent.goal_contract;必须且只能调用一次,用 outcome 具体概括当前用户最终意图,acceptanceNodes 至少提交一项可核对标准。每个 requiredEvidence 必须选择在该标准所有合法结果下都能成功产生回执的工具;环境探测可能以 rejected/failed 表示正常否定结果时,不得把该探测工具写成必需成功回执(例如非 Git 项目不得要求 git.inspect 成功,应使用 project.index 的成功回执证明 isRepository=false)。nonNegotiables、preferences、forbiddenAssumptions、openQuestions 没有内容时传空数组。不得调用 update_agent_plan、respond_to_user 或任何其他动作,不得输出普通文本、解释、markdown 或代码围栏。"
)));
restrict_root_goal_contract_repair_request(&mut request, plan_root)?;
request
.messages
.push(LlmMessage::user(root_goal_contract_repair_instruction(
&protocol_error,
plan_root,
)));
} else if force_supervisor_initial_collaboration {
supervisor_collaboration_repair_active = true;
if let Some(actions) = supervisor_collaboration_candidate_actions.take() {
@@ -1388,4 +1422,38 @@ mod supervisor_collaboration_repair_tests {
assert_eq!(merged, vec![replacement]);
}
#[test]
fn plan_root_goal_contract_repair_keeps_the_fixed_schema_and_instruction() {
let catalog = GameCreatorMcpCatalog {
fingerprint: String::new(),
servers: Vec::new(),
tools: Vec::new(),
};
let mut request = LlmRunRequest::new(Vec::new())
.with_function_tools(
build_agent_runtime_native_function_tools(&catalog)
.expect("build native function tools"),
)
.with_tool_choice(platform_llm::LlmToolChoice::Required);
restrict_root_goal_contract_repair_request(&mut request, true)
.expect("restrict plan goal repair");
assert_eq!(request.function_tools.len(), 1);
let fixed = request.function_tools[0]
.parameters
.pointer("/properties/input/properties/acceptanceNodes")
.expect("fixed acceptance nodes schema");
assert_eq!(fixed["maxItems"], serde_json::json!(1));
assert_eq!(
fixed["items"]["properties"]["criterionId"]["enum"],
serde_json::json!([PLAN_FAST_GDD_ACCEPTANCE_NODE_ID])
);
let instruction = root_goal_contract_repair_instruction("test-error", true);
assert!(instruction.contains("固定单节点"));
assert!(instruction.contains(PLAN_FAST_GDD_ACCEPTANCE_NODE_CRITERION));
assert!(!instruction.contains("至少提交一项"));
assert!(!instruction.contains("project.index 的成功回执"));
}
}
@@ -390,6 +390,10 @@ pub(in crate::agent) fn resume_game_creator_agent_finalization_at(
let current_revision = read_game_creator_agent_runtime_project_revision(root)?;
let blocker = if let Some(blocker) = structured_plan_completion_blocker(&state) {
Some(blocker)
} else if let Some(blocker) =
plan_gdd_completion_blocker_at_locked(root, &journal.agent_id, &journal.run_id)
{
Some(blocker)
} else if let Some(blocker) =
game_creator_agent_goal_completion_blocker_at_locked(root, &state)
{
@@ -1612,6 +1612,9 @@ mod orphaned_external_generation_recovery_tests {
.error
.as_deref()
.is_some_and(|error| error.contains("恢复锚点同时缺失")));
assert!(read_plan_gdd_approval_pending(root)
.expect("read planning approval pending")
.is_none());
let audit_count = read_agent_db_records_bounded(root, 1024 * 1024)
.expect("read anchor-missing audit")
.0
@@ -1628,6 +1631,9 @@ mod orphaned_external_generation_recovery_tests {
);
let _ = resume_game_creator_agent_background_tasks_at(root)
.expect("second recovery scan may surface the existing reconciliation state");
assert!(read_plan_gdd_approval_pending(root)
.expect("reread planning approval pending")
.is_none());
let audit_count_after_second_scan = read_agent_db_records_bounded(root, 1024 * 1024)
.expect("reread anchor-missing audit")
.0
@@ -224,25 +224,16 @@ fn plan_goal_contract_draft() -> AgentRuntimeGoalContractDraft {
AgentRuntimeGoalContractDraft {
outcome: "完成一份可审批的 Fast GDD".to_string(),
non_negotiables: vec!["保留用户明确要求".to_string()],
preferences: vec!["优先复用现有资源".to_string()],
preferences: Vec::new(),
forbidden_assumptions: vec!["不能把工具成功当作目标完成".to_string()],
open_questions: vec!["最终视觉效果仍需观察".to_string()],
acceptance_nodes: vec![
AgentRuntimeGoalContractAcceptanceNodeDraft {
criterion_id: "behavior".to_string(),
criterion: "核心交互可运行".to_string(),
required: true,
required_evidence: vec!["tool:project.verify".to_string()],
dependencies: Vec::new(),
},
AgentRuntimeGoalContractAcceptanceNodeDraft {
criterion_id: "final-observation".to_string(),
criterion: "最终结果符合用户目标".to_string(),
required: true,
required_evidence: vec!["tool:preview.validate".to_string()],
dependencies: vec!["behavior".to_string()],
},
],
acceptance_nodes: vec![AgentRuntimeGoalContractAcceptanceNodeDraft {
criterion_id: PLAN_FAST_GDD_ACCEPTANCE_NODE_ID.to_string(),
criterion: PLAN_FAST_GDD_ACCEPTANCE_NODE_CRITERION.to_string(),
required: true,
required_evidence: vec![PLAN_FAST_GDD_ACCEPTANCE_NODE_EVIDENCE.to_string()],
dependencies: Vec::new(),
}],
}
}
@@ -10,6 +10,14 @@ const AGENT_RUNTIME_GOAL_CONTRACT_NODE_ID_MAX_CHARS: usize = 160;
const AGENT_RUNTIME_GOAL_CONTRACT_NODE_EVIDENCE_LIMIT: usize = 32;
const AGENT_RUNTIME_GOAL_CONTRACT_NODE_DEPENDENCY_LIMIT: usize = 32;
/// Planning uses a fixed Fast GDD acceptance node. Only the intent-bearing
/// Goal Contract fields vary with the project; the quality gate itself is a
/// protocol invariant and is not Provider-authored.
pub(crate) const PLAN_FAST_GDD_ACCEPTANCE_NODE_ID: &str = "fast-gdd-serves-intent";
pub(crate) const PLAN_FAST_GDD_ACCEPTANCE_NODE_CRITERION: &str =
"当前 Fast GDD 服务已冻结的用户意图、不可协商约束与禁止假设";
pub(crate) const PLAN_FAST_GDD_ACCEPTANCE_NODE_EVIDENCE: &str = "tool:file.read";
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub(crate) struct AgentRuntimeGoalContractAcceptanceNode {
@@ -271,6 +279,30 @@ fn validate_goal_contract_acceptance_graph(
Ok(())
}
fn validate_plan_fast_gdd_acceptance_nodes(
nodes: &[AgentRuntimeGoalContractAcceptanceNode],
) -> Result<(), String> {
if nodes.len() != 1 {
return Err(
"project-supervisor-plan 的 acceptanceNodes 必须是固定的单个 Fast GDD 验收节点"
.to_string(),
);
}
let node = &nodes[0];
if node.criterion_id != PLAN_FAST_GDD_ACCEPTANCE_NODE_ID
|| node.criterion != PLAN_FAST_GDD_ACCEPTANCE_NODE_CRITERION
|| !node.required
|| node.required_evidence != [PLAN_FAST_GDD_ACCEPTANCE_NODE_EVIDENCE]
|| !node.dependencies.is_empty()
{
return Err(
"project-supervisor-plan 的 acceptanceNodes 必须精确使用 Fast GDD 固定验收节点"
.to_string(),
);
}
Ok(())
}
fn validate_goal_contract_record(
root: &Path,
contract: &AgentRuntimeGoalContract,
@@ -340,6 +372,15 @@ fn validate_goal_contract_record(
return Err("Goal Contract 语义字段未规范化".to_string());
}
validate_goal_contract_acceptance_graph(&contract.acceptance_nodes)?;
if binding.source == AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE {
if !contract.preferences.is_empty() {
return Err(
"project-supervisor-plan 的 preferences 必须为空;偏好不得进入固定 Fast GDD 验收合同"
.to_string(),
);
}
validate_plan_fast_gdd_acceptance_nodes(&contract.acceptance_nodes)?;
}
for node in &contract.acceptance_nodes {
if node.revision != AGENT_RUNTIME_GOAL_CONTRACT_REVISION
|| node.created_at != contract.created_at
@@ -406,6 +447,9 @@ fn build_goal_contract(
acceptance_nodes.push(node);
}
validate_goal_contract_acceptance_graph(&acceptance_nodes)?;
if binding.source == AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE {
validate_plan_fast_gdd_acceptance_nodes(&acceptance_nodes)?;
}
let mut contract = AgentRuntimeGoalContract {
schema_version: AGENT_RUNTIME_GOAL_CONTRACT_SCHEMA_VERSION.to_string(),
project_id: game_creator_agent_runtime_context_project_id(root)?,
@@ -610,6 +654,23 @@ mod tests {
}
}
fn plan_goal_contract_draft(outcome: &str) -> AgentRuntimeGoalContractDraft {
AgentRuntimeGoalContractDraft {
outcome: outcome.to_string(),
non_negotiables: vec!["保留用户明确要求".to_string()],
preferences: Vec::new(),
forbidden_assumptions: vec!["不能把提交当作审批".to_string()],
open_questions: Vec::new(),
acceptance_nodes: vec![AgentRuntimeGoalContractAcceptanceNodeDraft {
criterion_id: PLAN_FAST_GDD_ACCEPTANCE_NODE_ID.to_string(),
criterion: PLAN_FAST_GDD_ACCEPTANCE_NODE_CRITERION.to_string(),
required: true,
required_evidence: vec![PLAN_FAST_GDD_ACCEPTANCE_NODE_EVIDENCE.to_string()],
dependencies: Vec::new(),
}],
}
}
fn root_fixture(source: &str) -> (tempfile::TempDir, PathBuf, AgentRuntimeRunProfileBinding) {
let temporary = crate::tests::canonical_test_tempdir("goal-contract-fixture-");
let root = temporary.path().join("project");
@@ -727,6 +788,56 @@ mod tests {
assert!(error.contains("根 Project Supervisor"));
}
#[test]
fn plan_goal_contract_acceptance_nodes_are_fixed_but_other_sources_stay_dynamic() {
let (_temporary, root, binding) = root_fixture(AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE);
create_game_creator_agent_runtime_goal_contract_at(
&root,
&binding.agent_id,
&binding.run_id,
"做一份 Fast GDD",
&plan_goal_contract_draft("完成一份可审批的 Fast GDD"),
)
.expect("fixed plan contract");
let (_temporary, dynamic_root, dynamic_binding) =
root_fixture(AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE);
create_game_creator_agent_runtime_goal_contract_at(
&dynamic_root,
&dynamic_binding.agent_id,
&dynamic_binding.run_id,
"创建一个游戏",
&goal_contract_draft("动态图仍可用"),
)
.expect("dynamic contract");
let (_temporary, invalid_root, invalid_binding) =
root_fixture(AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE);
let error = create_game_creator_agent_runtime_goal_contract_at(
&invalid_root,
&invalid_binding.agent_id,
&invalid_binding.run_id,
"做一份 Fast GDD",
&goal_contract_draft("自定义节点应被拒绝"),
)
.expect_err("plan source must reject provider-authored nodes");
assert!(error.contains("固定") || error.contains("acceptanceNodes"));
let (_temporary, preference_root, preference_binding) =
root_fixture(AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE);
let mut preference_draft = plan_goal_contract_draft("偏好不能进入 plan 合同");
preference_draft.preferences = vec!["优先复用旧素材".to_string()];
let error = create_game_creator_agent_runtime_goal_contract_at(
&preference_root,
&preference_binding.agent_id,
&preference_binding.run_id,
"做一份 Fast GDD",
&preference_draft,
)
.expect_err("plan preferences must stay empty");
assert!(error.contains("preferences 必须为空"));
}
#[test]
fn goal_contract_read_fails_closed_after_content_or_binding_tampering() {
let (_temporary, root, binding) = root_fixture(AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE);
@@ -144,12 +144,33 @@ fn pending_identity_matches_gdd(pending: &PlanGddApprovalPendingV1, gdd: &PlanGd
&& pending.run_identity.run_id == gdd.created_by_run_id
}
fn pending_matches_gdd(pending: &PlanGddApprovalPendingV1, gdd: &PlanGddV1) -> bool {
pub(crate) fn pending_matches_gdd(pending: &PlanGddApprovalPendingV1, gdd: &PlanGddV1) -> bool {
pending_identity_matches_gdd(pending, gdd)
&& pending.status == "awaiting_decision"
&& pending.observation.is_none()
}
pub(crate) fn pending_matches_receipt(
pending: &PlanGddApprovalPendingV1,
gdd: &PlanGddV1,
receipt: &PlanGddApprovalV1,
) -> bool {
if !pending_identity_matches_gdd(pending, gdd) {
return false;
}
if pending_matches_gdd(pending, gdd) {
return true;
}
let expected = approval_observation(receipt);
pending.status == format!("observed_{}", receipt.action)
&& pending.observation.as_ref().is_some_and(|observation| {
observation.tool == expected.tool
&& observation.status == expected.status
&& observation.summary == expected.summary
&& observation.detail == expected.detail
})
}
/// Construct the independent planning pending projection after an external
/// acceptance gate has succeeded. M1C-1 does not decide whether the gate
/// passed; the caller must supply that fact and the exact GDD identity.
@@ -240,6 +261,249 @@ pub(crate) fn create_plan_gdd_approval_pending_locked(
write_plan_gdd_approval_pending_atomic_locked(root, &pending)
}
/// Result of the M1C-2a acceptance gate. The gate is intentionally separate
/// from `create_plan_gdd_approval_pending_locked`: a pending card is a
/// projection that may only be created after the fixed Fast GDD acceptance
/// graph has converged for the current Markdown revision.
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum PlanGddAcceptanceGateOutcome {
NotApplicable,
WaitingForDeliveryClaim {
delegation_id: String,
detail: String,
},
WaitingForEvidence {
detail: String,
},
RepairRequired {
repair_of_delegation_id: String,
detail: String,
},
PendingCreated,
PendingAlreadyPresent,
AlreadyDecided,
}
fn latest_plan_gdd_for_root<'a>(gdds: &'a [PlanGddV1], root_run_id: &str) -> Option<&'a PlanGddV1> {
gdds.iter().rev().find(|gdd| {
gdd.agent_id == GAME_CREATOR_PROJECT_PLANNING_AGENT_ID
&& gdd.source == "agent-delegate"
&& gdd.run_profile == AGENT_RUNTIME_RUN_PROFILE_STANDARD
&& gdd.root_agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
&& gdd.root_run_id == root_run_id
})
}
pub(crate) fn ensure_plan_gdd_approval_pending_after_acceptance_locked(
root: &Path,
agent_id: &str,
run_id: &str,
) -> Result<PlanGddAcceptanceGateOutcome, String> {
if agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID || run_id.trim().is_empty() {
return Ok(PlanGddAcceptanceGateOutcome::NotApplicable);
}
let Some(binding) =
read_game_creator_agent_runtime_run_profile_binding(root, agent_id, run_id)?
else {
return Ok(PlanGddAcceptanceGateOutcome::NotApplicable);
};
if binding.source != AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE {
return Ok(PlanGddAcceptanceGateOutcome::NotApplicable);
}
if binding.profile != AGENT_RUNTIME_RUN_PROFILE_STANDARD {
return Err("Fast GDD acceptance gate 的 plan 根 Run Profile 已漂移".to_string());
}
let gdds = read_plan_gdd_chain_locked(root).map_err(|error| error.to_string())?;
let Some(gdd) = latest_plan_gdd_for_root(&gdds, run_id) else {
// A contract may be updated before the planning child has submitted a
// GDD. That is a normal intermediate state; it must never allocate a
// user-facing approval card.
return Ok(PlanGddAcceptanceGateOutcome::NotApplicable);
};
// The planning storage is one immutable GDD lineage per project. A
// replacement root may still have an older GDD in that lineage while a
// newer root has already submitted a later version. Such an old root is
// no longer eligible to expose an approval card; importantly, recovery
// must skip it rather than turning the whole scan into reconciliation.
let Some(global_latest) = gdds.last() else {
return Ok(PlanGddAcceptanceGateOutcome::NotApplicable);
};
if global_latest.gdd_id != gdd.gdd_id
|| global_latest.version != gdd.version
|| global_latest.fingerprint != gdd.fingerprint
{
return Ok(PlanGddAcceptanceGateOutcome::NotApplicable);
}
validate_plan_gdd(gdd).map_err(|error| error.to_string())?;
if gdd.root_run_id != run_id || gdd.source != "agent-delegate" {
return Err(
"Fast GDD acceptance gate 的 GDD root/source/profile identity 不一致".to_string(),
);
}
let approvals = read_plan_gdd_approvals_locked(root).map_err(|error| error.to_string())?;
validate_plan_gdd_approvals_against_gdds(&gdds, &approvals)
.map_err(|error| error.to_string())?;
let receipt = read_plan_gdd_approval_for_version_locked(root, gdd.version)
.map_err(|error| error.to_string())?;
let pending = read_plan_gdd_approval_pending_locked(root).map_err(|error| error.to_string())?;
if let Some(receipt) = receipt.as_ref() {
if pending
.as_ref()
.is_some_and(|pending| !pending_matches_receipt(pending, gdd, receipt))
{
return Err(
"Fast GDD acceptance pending 与已提交 receipt 的 identity/observation 冲突"
.to_string(),
);
}
return Ok(PlanGddAcceptanceGateOutcome::AlreadyDecided);
}
// An exact awaiting pending is already the durable projection of a
// passed gate. Recovery must preserve it without re-reading the file or
// allocating a new approvalRequestId.
if let Some(existing) = pending {
if !pending_matches_gdd(&existing, gdd) {
return Err("Fast GDD acceptance pending identity 与当前 GDD 不一致".to_string());
}
return Ok(PlanGddAcceptanceGateOutcome::PendingAlreadyPresent);
}
let root_binding = validate_project_supervisor_plan_root_binding_at(root, agent_id, run_id)?;
if root_binding.project_id != gdd.project_id
|| root_binding.source != PLAN_GDD_APPROVAL_SOURCE
|| root_binding.profile != AGENT_RUNTIME_RUN_PROFILE_STANDARD
{
return Err("Fast GDD acceptance gate 的根 Run binding identity 不一致".to_string());
}
let child_binding = validate_project_planning_child_binding_at(
root,
GAME_CREATOR_PROJECT_PLANNING_AGENT_ID,
&gdd.created_by_run_id,
)?;
if child_binding.project_id != gdd.project_id
|| child_binding.root_agent_id != gdd.root_agent_id
|| child_binding.root_run_id != gdd.root_run_id
|| child_binding.source != gdd.source
|| child_binding.profile != gdd.run_profile
|| child_binding.binding_fingerprint != gdd.run_profile_binding_fingerprint
{
return Err("Fast GDD acceptance gate 的策划子 Run binding identity 不一致".to_string());
}
let child_task = read_latest_game_creator_agent_runtime_task_by_run_id(
root,
GAME_CREATOR_PROJECT_PLANNING_AGENT_ID,
&gdd.created_by_run_id,
)?;
let child_task = child_task
.ok_or_else(|| "Fast GDD acceptance gate 缺少策划子 Run task 记录".to_string())?;
// Submit-anchor and child-completion recovery own every pre-terminal
// projection. The approval gate must stay inert until that earlier
// workflow has produced a completed task record.
if game_creator_agent_runtime_terminal_status(&child_task).as_deref() != Some("completed") {
return Ok(PlanGddAcceptanceGateOutcome::NotApplicable);
}
if child_task.agent_id != GAME_CREATOR_PROJECT_PLANNING_AGENT_ID
|| child_task.run_id != gdd.created_by_run_id
|| child_task.session_id != gdd.session_id
|| child_task.parent_agent_id.as_deref() != Some(gdd.root_agent_id.as_str())
|| child_task.parent_run_id.as_deref() != Some(gdd.root_run_id.as_str())
|| child_task.source != gdd.source
|| child_task.run_profile != gdd.run_profile
|| child_task.run_profile_binding_fingerprint != gdd.run_profile_binding_fingerprint
|| child_task.delegation_id.as_deref() != Some(gdd.delegation_id.as_str())
{
return Err(
"Fast GDD acceptance gate 的策划子 Run task 与提交 delivery identity 不一致"
.to_string(),
);
}
let delivery = read_static_delegate_delivery_at(root, &gdd.delegation_id)?;
let delivery =
delivery.ok_or_else(|| "Fast GDD acceptance gate 缺少原 planning delivery".to_string())?;
if delivery.delegation_id != gdd.delegation_id
|| delivery.parent_agent_id != gdd.root_agent_id
|| delivery.parent_run_id != gdd.root_run_id
|| delivery.target_agent_id != GAME_CREATOR_PROJECT_PLANNING_AGENT_ID
|| delivery.target_session_id != gdd.session_id
|| delivery.target_run_id != gdd.created_by_run_id
{
return Err("Fast GDD acceptance gate 的 planning delivery identity 不一致".to_string());
}
if delivery.status == StaticDelegateDeliveryStatus::Dispatched {
// Delegate-receipt recovery owns the Dispatched -> Ready transition.
// The acceptance gate must remain inert until that durable handoff is
// complete instead of treating an ordinary crash window as corruption.
return Ok(PlanGddAcceptanceGateOutcome::NotApplicable);
}
if delivery.status == StaticDelegateDeliveryStatus::Suppressed {
return Err("Fast GDD acceptance gate 的 planning delivery 已被 suppressed".to_string());
}
if delivery.terminal_status.as_deref() != Some("completed") {
return Err("Fast GDD acceptance gate 的 planning delivery 终态不一致".to_string());
}
validate_active_plan_root_for_decision_locked(root, gdd).map_err(|error| error.to_string())?;
let session = read_plan_session_with_recovery_locked(root)
.map_err(|error| error.to_string())?
.ok_or_else(|| "Fast GDD acceptance gate 缺少 planning session".to_string())?;
if !plan_gdd_session_matches_submission(&session, gdd) {
return Err(
"Fast GDD acceptance gate 的 planning session 尚未精确指向当前提交".to_string(),
);
}
if delivery.status == StaticDelegateDeliveryStatus::Ready {
return Ok(PlanGddAcceptanceGateOutcome::WaitingForDeliveryClaim {
delegation_id: gdd.delegation_id.clone(),
detail: format!(
"planning delivery 尚未由当前 Supervisor 根 Run 认领;先通过 agent.run_status 认领 delegationId={},认领后才能按同一合同发起返工或创建审批 pending",
gdd.delegation_id
),
});
}
match plan_fast_gdd_acceptance_status_at_locked(root, gdd)? {
PlanFastGddAcceptanceStatus::NeedsEvidence => {
return Ok(PlanGddAcceptanceGateOutcome::WaitingForEvidence {
detail: "Acceptance Graph 尚无当前 game/fast_gdd.md 的完整根 Run 取证;先从 startLine=1 分页无缺口读取到 EOF,并用全部 file.read actionId 更新验收节点;不得创建审批卡或提前返工".to_string(),
});
}
PlanFastGddAcceptanceStatus::RepairRequired => {
return Ok(PlanGddAcceptanceGateOutcome::RepairRequired {
repair_of_delegation_id: gdd.delegation_id.clone(),
detail: format!(
"Acceptance Graph 已基于当前 game/fast_gdd.md 明确判定未通过;不得创建审批卡;repairOfDelegationId={}",
gdd.delegation_id
),
});
}
PlanFastGddAcceptanceStatus::Passed => {}
}
create_plan_gdd_approval_pending_locked(root, gdd).map_err(|error| error.to_string())?;
Ok(PlanGddAcceptanceGateOutcome::PendingCreated)
}
/// Run the production acceptance gate after a successful root
/// `agent.acceptance_update`. All identity and projection reads happen under
/// the project lock; callers can safely retry this function after a crash.
pub(crate) fn ensure_plan_gdd_approval_pending_after_acceptance_at(
root: &Path,
agent_id: &str,
run_id: &str,
) -> Result<PlanGddAcceptanceGateOutcome, String> {
if agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID || run_id.trim().is_empty() {
return Ok(PlanGddAcceptanceGateOutcome::NotApplicable);
}
let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
root,
"planning.acceptance-gate",
)
.map_err(|error| format!("取得 acceptance-gate 项目锁失败:{error}"))?;
ensure_plan_gdd_approval_pending_after_acceptance_locked(root, agent_id, run_id)
}
fn approval_terminal_observation_exists_locked(
root: &Path,
receipt: &PlanGddApprovalV1,
@@ -776,20 +1040,51 @@ fn project_receipt_locked(
}
/// Reconcile receipt-derived projections after a Runner restart. Receipts
/// are the authority; this routine only repairs projections and never creates
/// an approval pending without an explicit upstream acceptance-gate caller.
/// are the authority; this routine also repairs the explicitly allowed
/// acceptance-gate projection when the graph already passed. It never
/// re-executes Provider or `file.read` evidence actions; before an exact
/// pending/receipt exists it may re-read the current Markdown only to verify
/// the persisted receipt hash. Recovery never changes any GDD/approval
/// identity.
pub(crate) fn reconcile_plan_gdd_approval_projections_at(
root: &Path,
) -> Result<bool, PlanningStorageError> {
let _lock = acquire_project_write_lock(root, "planning.approval-recovery")
.map_err(|error| approval_error("PLAN_DURABILITY_FAILED", error))?;
let gdds = read_plan_gdd_chain_locked(root)?;
let plan_root_run_ids = gdds
.iter()
.filter(|gdd| {
gdd.agent_id == GAME_CREATOR_PROJECT_PLANNING_AGENT_ID
&& gdd.source == "agent-delegate"
&& gdd.run_profile == AGENT_RUNTIME_RUN_PROFILE_STANDARD
&& gdd.root_agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
})
.map(|gdd| gdd.root_run_id.as_str())
.collect::<BTreeSet<_>>();
let mut recovery_pending = false;
for root_run_id in plan_root_run_ids {
match ensure_plan_gdd_approval_pending_after_acceptance_locked(
root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
root_run_id,
)
.map_err(|error| approval_error("PLAN_NEEDS_RECONCILIATION", error))?
{
PlanGddAcceptanceGateOutcome::PendingCreated => recovery_pending = true,
PlanGddAcceptanceGateOutcome::NotApplicable
| PlanGddAcceptanceGateOutcome::WaitingForDeliveryClaim { .. }
| PlanGddAcceptanceGateOutcome::WaitingForEvidence { .. }
| PlanGddAcceptanceGateOutcome::RepairRequired { .. }
| PlanGddAcceptanceGateOutcome::PendingAlreadyPresent
| PlanGddAcceptanceGateOutcome::AlreadyDecided => {}
}
}
let approvals = read_plan_gdd_approvals_locked(root)?;
if approvals.is_empty() {
return Ok(false);
return Ok(recovery_pending);
}
validate_plan_gdd_approvals_against_gdds(&gdds, &approvals)?;
let mut recovery_pending = false;
for receipt in &approvals {
recovery_pending |= project_receipt_locked(root, &gdds, receipt)?;
}
@@ -1175,7 +1470,7 @@ fn plan_gdd_anchor_state_locked(
Ok((pending_state, batch_state))
}
fn plan_gdd_session_matches_submission(session: &PlanSessionV1, gdd: &PlanGddV1) -> bool {
fn plan_gdd_session_identity_matches_gdd(session: &PlanSessionV1, gdd: &PlanGddV1) -> bool {
session.project_id == gdd.project_id
&& session.gdd_id == gdd.gdd_id
&& session.agent_id == GAME_CREATOR_PROJECT_PLANNING_AGENT_ID
@@ -1188,6 +1483,15 @@ fn plan_gdd_session_matches_submission(session: &PlanSessionV1, gdd: &PlanGddV1)
&& session.session_id == gdd.session_id
&& session.latest_submitted_ref.as_ref() == Some(&receipt_plan_ref_from_gdd(gdd))
&& session.active_run_id.is_none()
&& session.last_run_id == gdd.created_by_run_id
}
fn plan_gdd_session_matches_submission(session: &PlanSessionV1, gdd: &PlanGddV1) -> bool {
plan_gdd_session_identity_matches_gdd(session, gdd)
&& session.phase == "awaiting_gdd_approval"
&& session.session_revision == gdd.source_session_revision.saturating_add(1)
&& session.previous_fingerprint.as_deref() == Some(gdd.source_session_fingerprint.as_str())
&& session.last_decision_ref.is_none()
}
fn receipt_plan_ref_from_gdd(gdd: &PlanGddV1) -> PlanGddRef {
@@ -1203,7 +1507,7 @@ fn plan_gdd_session_matches_receipt(
gdd: &PlanGddV1,
receipt: &PlanGddApprovalV1,
) -> bool {
if !plan_gdd_session_matches_submission(session, gdd) {
if !plan_gdd_session_identity_matches_gdd(session, gdd) {
return false;
}
let expected_phase = match receipt.action.as_str() {
@@ -1320,7 +1624,25 @@ pub(crate) fn plan_gdd_completion_blocker_at_locked(
})
.collect::<Vec<_>>();
if plan_gdds.is_empty() {
return None;
return match read_game_creator_agent_runtime_goal_contract_at(
root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
run_id,
) {
Ok(Some(_)) => Some(plan_gdd_completion_blocker(
"blocked",
"当前立项策划根 Run 尚未提交 Fast GDD,不能收束任务",
format!(
"rootRunId={run_id} · nextRequiredAction=agent.delegate;上一根 Run 遗留的 game/fast_gdd.md 或 Acceptance Graph 不能代替本根提交"
),
)),
Ok(None) => None,
Err(error) => Some(plan_gdd_completion_blocker(
"needs-reconciliation",
"Goal Contract 无法读取,不能确认当前立项策划根 Run 的 GDD 提交状态",
error,
)),
};
}
let latest = *plan_gdds.last().expect("non-empty plan GDD lineage");
@@ -1347,6 +1669,80 @@ pub(crate) fn plan_gdd_completion_blocker_at_locked(
&& receipt.fingerprint == latest.fingerprint
});
let pending = match read_plan_gdd_approval_pending_locked(root) {
Ok(pending) => pending,
Err(error) => {
return Some(plan_gdd_completion_blocker(
"needs-reconciliation",
"Fast GDD approval pending 无法读取,不能收束任务",
error.to_string(),
));
}
};
if receipt.is_none() {
if let Some(pending) = pending.as_ref() {
if !pending_matches_gdd(pending, latest) {
return Some(plan_gdd_completion_blocker(
"needs-reconciliation",
"Fast GDD approval pending identity 与当前 GDD 不一致",
format!("gddVersion={}", latest.version),
));
}
}
// An exact awaiting pending is proof that the acceptance gate already
// passed. Do not re-read the Markdown after recovery or projection
// repair; only the no-pending path needs the graph preflight.
let pending_is_exact_awaiting = pending
.as_ref()
.is_some_and(|pending| pending_matches_gdd(pending, latest));
if !pending_is_exact_awaiting {
let contract = match read_game_creator_agent_runtime_goal_contract_at(
root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
run_id,
) {
Ok(contract) => contract,
Err(error) => {
return Some(plan_gdd_completion_blocker(
"needs-reconciliation",
"Goal Contract 无法读取,不能确认 Fast GDD 验收前置门",
error,
));
}
};
if contract.is_some() {
let global_latest_matches = gdds.last().is_some_and(|global_latest| {
global_latest.gdd_id == latest.gdd_id
&& global_latest.version == latest.version
&& global_latest.fingerprint == latest.fingerprint
});
if !global_latest_matches {
return Some(plan_gdd_completion_blocker(
"needs-reconciliation",
"当前 Fast GDD 不是项目 lineage 最新版本,不能自动收束旧 plan 根",
format!(
"gddVersion={} · rootRunId={} · 需由当前 lineage 根 Run 继续处理",
latest.version, run_id
),
));
}
match plan_fast_gdd_acceptance_passed_at_locked(root, latest) {
Ok(true) => {}
// Leave the not-yet-passed phase to the Acceptance Graph
// blocker so the Supervisor can take file.read evidence.
Ok(false) => return None,
Err(error) => {
return Some(plan_gdd_completion_blocker(
"needs-reconciliation",
"Fast GDD 验收前置门 identity 无法核对",
error,
));
}
}
}
}
}
let session = match read_plan_session_with_recovery_locked(root) {
Ok(Some(session)) => session,
Ok(None) => {
@@ -1364,7 +1760,7 @@ pub(crate) fn plan_gdd_completion_blocker_at_locked(
));
}
};
if !plan_gdd_session_matches_submission(&session, latest) {
if receipt.is_none() && !plan_gdd_session_matches_submission(&session, latest) {
return Some(plan_gdd_completion_blocker(
"needs-reconciliation",
"planning session 尚未收口到当前 Fast GDD 提交",
@@ -1378,16 +1774,6 @@ pub(crate) fn plan_gdd_completion_blocker_at_locked(
));
}
let pending = match read_plan_gdd_approval_pending_locked(root) {
Ok(pending) => pending,
Err(error) => {
return Some(plan_gdd_completion_blocker(
"needs-reconciliation",
"Fast GDD approval pending 无法读取,不能收束任务",
error.to_string(),
));
}
};
let (pending_anchor, batch_anchor) = match plan_gdd_anchor_state_locked(root, latest) {
Ok(state) => state,
Err(error) => {
File diff suppressed because it is too large Load Diff
@@ -148,23 +148,90 @@ pub(crate) fn observe_agent_runtime_acceptance_update(
)
})();
match result {
Ok(state) => AgentRuntimeToolObservation {
tool: "agent.acceptance_update".to_string(),
status: "ok".to_string(),
summary: format!("Acceptance Graph 已更新到 revision {}", state.revision),
detail: Some(
serde_json::to_string(&serde_json::json!({
"contractFingerprint": state.contract_fingerprint,
"acceptanceRevision": state.revision,
"stateFingerprint": state.state_fingerprint,
"evaluations": state.evaluations.iter().map(|evaluation| serde_json::json!({
"criterionId": evaluation.criterion_id,
"status": evaluation.status,
})).collect::<Vec<_>>(),
}))
.unwrap_or_default(),
),
},
Ok(state) => {
let gate = ensure_plan_gdd_approval_pending_after_acceptance_at(root, agent_id, run_id);
match gate {
Err(error) => AgentRuntimeToolObservation {
tool: "agent.acceptance_update".to_string(),
status: "needs-reconciliation".to_string(),
summary: format!(
"Acceptance Graph 已更新到 revision {},但审批前置门需要人工核对",
state.revision
),
detail: Some(redact_agent_runtime_project_paths(
root,
&format!(
"Acceptance Graph 已落盘,但 acceptance-gate 未能安全收束:{error}"
),
800,
)),
},
Ok(gate) => {
let mut detail = serde_json::json!({
"contractFingerprint": state.contract_fingerprint,
"acceptanceRevision": state.revision,
"stateFingerprint": state.state_fingerprint,
"evaluations": state.evaluations.iter().map(|evaluation| serde_json::json!({
"criterionId": evaluation.criterion_id,
"status": evaluation.status,
})).collect::<Vec<_>>(),
});
match gate {
PlanGddAcceptanceGateOutcome::WaitingForDeliveryClaim {
delegation_id,
detail: gate_detail,
} => {
detail["approvalPending"] =
serde_json::Value::String("not-created".to_string());
detail["nextRequiredAction"] =
serde_json::Value::String("agent.run_status".to_string());
detail["delegationId"] = serde_json::Value::String(delegation_id);
detail["gateDetail"] = serde_json::Value::String(gate_detail);
}
PlanGddAcceptanceGateOutcome::WaitingForEvidence {
detail: gate_detail,
} => {
detail["approvalPending"] =
serde_json::Value::String("not-created".to_string());
detail["nextRequiredAction"] =
serde_json::Value::String("file.read".to_string());
detail["gateDetail"] = serde_json::Value::String(gate_detail);
}
PlanGddAcceptanceGateOutcome::RepairRequired {
repair_of_delegation_id,
detail: gate_detail,
} => {
detail["approvalPending"] =
serde_json::Value::String("not-created".to_string());
detail["nextRequiredAction"] =
serde_json::Value::String("agent.delegate".to_string());
detail["repairOfDelegationId"] =
serde_json::Value::String(repair_of_delegation_id);
detail["gateDetail"] = serde_json::Value::String(gate_detail);
}
PlanGddAcceptanceGateOutcome::PendingCreated => {
detail["approvalPending"] =
serde_json::Value::String("created".to_string());
}
PlanGddAcceptanceGateOutcome::PendingAlreadyPresent => {
detail["approvalPending"] =
serde_json::Value::String("already-present".to_string());
}
PlanGddAcceptanceGateOutcome::AlreadyDecided => {
detail["approvalPending"] =
serde_json::Value::String("already-decided".to_string());
}
PlanGddAcceptanceGateOutcome::NotApplicable => {}
}
AgentRuntimeToolObservation {
tool: "agent.acceptance_update".to_string(),
status: "ok".to_string(),
summary: format!("Acceptance Graph 已更新到 revision {}", state.revision),
detail: Some(serde_json::to_string(&detail).unwrap_or_default()),
}
}
}
}
Err(error) => AgentRuntimeToolObservation {
tool: "agent.acceptance_update".to_string(),
status: "rejected".to_string(),
@@ -219,6 +219,48 @@ pub(crate) fn observe_agent_runtime_run_status(
})();
}
// `agent.run_status` already executes under the project snapshot lock.
// Re-run the locked gate on every plan-root status observation, even
// when no Ready delivery remains: an earlier run_status action may
// have durably claimed this delivery before its gate could persist.
let plan_gdd_acceptance_gate =
match ensure_plan_gdd_approval_pending_after_acceptance_locked(
root, agent_id, run_id,
)? {
PlanGddAcceptanceGateOutcome::NotApplicable => None,
PlanGddAcceptanceGateOutcome::WaitingForDeliveryClaim { .. } => {
return Err(
"agent.run_status 已认领 planning delivery,但 acceptance gate 仍观察到 Ready 状态"
.to_string(),
);
}
PlanGddAcceptanceGateOutcome::WaitingForEvidence {
detail: gate_detail,
} => Some(serde_json::json!({
"approvalPending": "not-created",
"nextRequiredAction": "file.read",
"gateDetail": gate_detail,
})),
PlanGddAcceptanceGateOutcome::RepairRequired {
repair_of_delegation_id,
detail: gate_detail,
} => Some(serde_json::json!({
"approvalPending": "not-created",
"nextRequiredAction": "agent.delegate",
"repairOfDelegationId": repair_of_delegation_id,
"gateDetail": gate_detail,
})),
PlanGddAcceptanceGateOutcome::PendingCreated => Some(serde_json::json!({
"approvalPending": "created",
})),
PlanGddAcceptanceGateOutcome::PendingAlreadyPresent => Some(serde_json::json!({
"approvalPending": "already-present",
})),
PlanGddAcceptanceGateOutcome::AlreadyDecided => Some(serde_json::json!({
"approvalPending": "already-decided",
})),
};
let claimed_delegate_deliveries = if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID {
claimed_static_delegate_deliveries_at(root, agent_id, run_id)?
} else {
@@ -258,6 +300,11 @@ pub(crate) fn observe_agent_runtime_run_status(
.map_err(|error| format!("序列化专业 Agent claimed contracts 失败:{error}"))?;
detail = format!("claimedDelegateContracts: {payload}\n\n{detail}");
}
if let Some(gate) = plan_gdd_acceptance_gate {
let payload = serde_json::to_string(&gate)
.map_err(|error| format!("序列化 Fast GDD acceptance gate 结果失败:{error}"))?;
detail = format!("planGddAcceptanceGate: {payload}\n\n{detail}");
}
// Ready payloads are complete evidence. The ordinary status summary may be shortened,
// but evidence must fit its budget before the corresponding claim is committed.
@@ -13,7 +13,8 @@ use crate::agent::{
agent_runtime_native_executable_tools, AgentRuntimePlanUpdate, AgentRuntimeToolAction,
AgentRuntimeToolPlan, AGENT_RUNTIME_BACKGROUND_TOOL_ACTION_LIMIT,
AGENT_RUNTIME_CANVAS_ASSET_KINDS, AGENT_RUNTIME_PLAN_STEP_LIMIT,
PLAN_SUBMIT_GDD_INPUT_SCHEMA_VERSION, PLAN_SUBMIT_GDD_TOOL,
PLAN_FAST_GDD_ACCEPTANCE_NODE_CRITERION, PLAN_FAST_GDD_ACCEPTANCE_NODE_EVIDENCE,
PLAN_FAST_GDD_ACCEPTANCE_NODE_ID, PLAN_SUBMIT_GDD_INPUT_SCHEMA_VERSION, PLAN_SUBMIT_GDD_TOOL,
};
use crate::mcp::{
validate_game_creator_mcp_tool_arguments, GameCreatorMcpCatalog, GameCreatorMcpCatalogTool,
@@ -359,6 +360,53 @@ pub(crate) fn build_agent_runtime_native_function_tools_for_agent(
Ok(functions)
}
/// Narrow only the request-scoped Goal Contract schema used by the plan root.
/// The capability registry itself must remain dynamic: game-chat and ordinary
/// Supervisor runs still author their own acceptance graph.
pub(crate) fn restrict_plan_root_goal_contract_schema(
functions: &mut [LlmFunctionTool],
) -> Result<(), String> {
let goal_contract_function = native_runtime_function_name("agent.goal_contract")
.ok_or_else(|| "无法生成 Goal Contract 工具函数名".to_string())?;
let Some(function) = functions
.iter_mut()
.find(|function| function.name == goal_contract_function)
else {
return Err("根 plan 请求缺少 agent.goal_contract 工具".to_string());
};
function.parameters = action_function_parameters(json!({
"type": "object",
"required": ["outcome", "nonNegotiables", "preferences", "forbiddenAssumptions", "openQuestions", "acceptanceNodes"],
"additionalProperties": false,
"properties": {
"outcome": { "type": "string", "minLength": 1, "maxLength": 4000 },
"nonNegotiables": string_array_schema(16),
"preferences": { "type": "array", "maxItems": 0, "items": { "type": "string" } },
"forbiddenAssumptions": string_array_schema(16),
"openQuestions": string_array_schema(16),
"acceptanceNodes": {
"type": "array", "minItems": 1, "maxItems": 1,
"items": {
"type": "object",
"required": ["criterionId", "criterion", "required", "requiredEvidence", "dependsOn"],
"additionalProperties": false,
"properties": {
"criterionId": { "type": "string", "enum": [PLAN_FAST_GDD_ACCEPTANCE_NODE_ID] },
"criterion": { "type": "string", "enum": [PLAN_FAST_GDD_ACCEPTANCE_NODE_CRITERION] },
"required": { "type": "boolean", "enum": [true] },
"requiredEvidence": {
"type": "array", "minItems": 1, "maxItems": 1,
"items": { "type": "string", "enum": [PLAN_FAST_GDD_ACCEPTANCE_NODE_EVIDENCE] }
},
"dependsOn": { "type": "array", "maxItems": 0, "items": { "type": "string" } }
}
}
}
}
}));
Ok(())
}
pub(crate) fn agent_runtime_native_tool_allowed_for_agent(agent_id: &str, tool: &str) -> bool {
if agent_id.trim() == GAME_CREATOR_PROJECT_PLANNING_AGENT_ID {
// update_agent_plan/respond_to_user are protocol controls and are
@@ -2068,6 +2116,7 @@ mod tests {
])
);
assert_eq!(goal["properties"]["acceptanceNodes"]["minItems"], 1);
assert_eq!(goal["properties"]["acceptanceNodes"]["maxItems"], 32);
assert_eq!(
goal["properties"]["acceptanceNodes"]["items"]["properties"]["requiredEvidence"]
["items"]["pattern"],
@@ -2098,6 +2147,23 @@ mod tests {
["required"],
json!(["agentId", "runId", "actionId"])
);
let goal_function = native_runtime_function_name("agent.goal_contract")
.expect("goal contract function name");
let mut functions = vec![LlmFunctionTool::new(
goal_function.clone(),
"goal",
action_function_parameters(runtime_tool_input_schema("agent.goal_contract")),
)];
restrict_plan_root_goal_contract_schema(&mut functions).expect("restrict plan schema");
let fixed = &functions[0].parameters["properties"]["input"];
assert_eq!(fixed["properties"]["acceptanceNodes"]["maxItems"], 1);
assert_eq!(fixed["properties"]["preferences"]["maxItems"], 0);
assert_eq!(
fixed["properties"]["acceptanceNodes"]["items"]["properties"]["criterionId"]["enum"],
json!([PLAN_FAST_GDD_ACCEPTANCE_NODE_ID])
);
assert_eq!(functions[0].name, goal_function);
}
#[test]
@@ -1,5 +1,14 @@
# 决策记录
## 2026-08-15 M1C-2a 隔离工作树实现:固定 Goal Contract 与审批前置门
- **本轮范围**:只实现 Supervisor 根 run 的 Goal Contract / Acceptance Graph 与 Fast GDD 审批前置门;不接 `M1C-2b` 澄清中转、审批 UI、构建准入或下游完整构建。当前变更仍在隔离 worktree,尚未合回原分支。
- **固定合同**`project-supervisor-plan / standard` 的首轮与格式修复请求都必须且只能调用一次 `agent.goal_contract`。按项目变化的字段只有 `outcome / nonNegotiables / forbiddenAssumptions / openQuestions``preferences` 固定空数组,`acceptanceNodes` 固定为唯一 `fast-gdd-serves-intent` 节点,required evidence 固定 `tool:file.read`。其它 source 保持动态合同,即使使用同名 criterionId 也不套 Fast GDD 特例。
- **证据合同**Fast GDD evidence 只接受当前 Supervisor 根 run 自己读取 `game/fast_gdd.md` 的成功 action receipt。receipt 保存规范化路径、完整内容 SHA-256 与行覆盖摘要;多页必须从 `startLine=1` 无缺口、无重叠覆盖到 EOF,全部页同 hash/同总行数,`agent.acceptance_update.evidence` 必须列出所有分页 actionId。普通 Graph 读取只验 durable receipt 形状与完整覆盖;当前 Markdown hash 只在审批前且无 exact pending/receipt 时复核,审批后的状态投影不会让旧 Graph 损坏。
- **取证与返工三态**planning delivery 先由同一根 run 的 `agent.run_status` durable 认领。Graph 缺失、not-observed、project revision/hash 过期或证据不完整属于 `NeedsEvidence`,下一步是 `file.read`,不得提前返工;只有当前完整证据支撑的显式 failed 属 `RepairRequired`,才返回原 delivery 的 `repairOfDelegationId`passed 才幂等创建 `gdd-approval` pending。`run_status` 在持有项目锁时调用 locked gate,并在 Ready 数为零时仍重放,封住旧 action 已认领但 gate 尚未落盘的崩溃窗口;GDD create 到 child/delivery 完成前保持惰性,不抢断 M1B-2 恢复。
- **恢复与完成门**acceptance update、delivery claim、Runner recovery、completion 与 finalization 都消费同一 gate。恢复不重新执行 Provider 或 `file.read` 动作,只复核 durable Graph、动作回执与当前 Markdown hashmissing pending 按原 approvalRequestId/identity 补建,exact pending 与 receipt 优先于 Graph/hash 复核,冲突则失败关闭。当前根没有自己的 GDD 时,上一根遗留 Markdown/Graph 不能绕过完成门。
- **回归与门禁**:已新增真实 `file.read` receipt 的错路径、错误 actor、旧 hash、120/200 行部分覆盖、两页完整覆盖,claim 前后、零 Ready 重放、Dispatched 中间态、审批后 Markdown 改写、pending 二次 recovery 幂等、无本根 GDD、非 plan 同名 criterion 与 M1B-2 双锚缺失恢复顺序用例;终审另补 exact pending 优先于 stale Graph、exact receipt 优先于后来 failed Graph 及 pending/receipt 冲突仍失败关闭。`cargo fmt --check``cargo test goal_contract`14 passed)、`cargo test m1c2a`14 passed)、`cargo test acceptance_graph`13 passed)、`cargo test planning_submit`51 passed)、`cargo test planning_approval`1 passed)、`cargo test finalization`52 passed)及 M1B-2 双锚恢复回归(1 passed)均通过;`cargo check --all-targets``npm run check:encoding`5403 files)与 `git diff --check` 通过。扩展 `cargo test autonomous_completion_contract` 在终审修复前曾整组 122 passed;最终代码下两次整组均为 121 passed,并由同一条无 plan 路径交集的 `game_chat_pure_continue_inherits_failed_root_semantics_and_manifest_progress` 停在 `planning` 超时,精确单测随即 1 passed(9.49s)。判为并行负载时序抖动,修复边界不明确且超出 M1C-2a,按本轮缺陷策略保留、不改 game-chat。门禁期间另发现 receipt 已存在时 completion gate 仍错误要求提交前 session 状态,导致审批后无法继续核对 terminal observation;已收窄为仅无 receipt 时校验提交态,有 receipt 时由既有 receipt session 强判据收口,整组 `planning_submit` 复跑通过。
## 2026-08-15 M1C-1 隔离工作树收口:审批核心与专用完成门已落地,生产前置门保持后置
- **本轮落地**:在 `planning_storage.rs` 增加 `plan-gdd-approval.v1` receipt、`plan-gdd-approval-pending.v1` projection、comment/decision/receipt/pending 的 typed fingerprint 与 strict canonical 校验;审批 observation 固定校验 tool/status、版本摘要、detail 前缀和规范化 comment。`planning_approval.rs` 增加 receipt create-only、三动作幂等(`committed / replayed / already-decided`)、版本/指纹竞态防护、receipt 后 index/Markdown/audit/terminal observation/session 投影与恢复,以及只读的 plan 根专用 completion blocker`commands.rs` 暴露 `decide_game_creator_plan_gdd`
@@ -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 分类,不包含审批写入方。**2026-08-15 `M1C-0b` 已通过定向门禁并完成**:只改静态委派 durable status 的前向兼容读路径,未知字符串显式保留为 `Unknown(raw)` 并最大化阻塞;不含审批写入方。**当前隔离 worktree 已完成 M1C-1 的审批核心、receipt 投影/恢复和 plan 根专用 completion blocker;生产 acceptance-gate pending caller、验收图接线、审批 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)` 并最大化阻塞;不含审批写入方。**当前隔离 worktree 已完成并通过 `M1C-2a` 本包门禁**:已接通固定 Goal Contract、Supervisor 根 run 的完整分页 `file.read` evidence、claim 后三态 acceptance gate、审批 pending 恢复及 completion/finalization 门;改动尚未合回。**`M1C-2b`、审批 UI构建准入与下游完整构建仍后置,M1 整体不可交付**(见第 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` 工作包已提供提交点与恢复,`M1C-0b` 已补齐静态委派未知 durable status 的前向兼容读路径,当前隔离 worktree 另已补齐 M1C-1 receipt/审批核心、投影恢复和 plan 根完成门;生产 acceptance-gate pending caller、验收图接线、审批 UI、构建绑定和正式入口仍不可用
- 当前实现边界:本文件是后续详细设计与实现的仓库内阶段基线;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 的前向兼容读路径,`M1C-1` 已提供 receipt/审批核心、投影恢复和 plan 根完成门;当前隔离 worktree 的 `M1C-2a` 已补齐固定 Goal Contract、验收图证据与生产 acceptance gate`M1C-2b` 澄清中转、审批 UI、构建绑定和正式入口仍不可用
## 1. 背景与目标
@@ -1206,7 +1206,11 @@ type PlanSubmitGddResult = {
此时用户以为已经定稿、系统却卡住,且 receipt 不可回滚(要改只能提交并决定新版本),这个状态对用户不可解释。
**固定顺序**Supervisor 收到 `plan.submit_gdd``Submitted` 回执后,**必须先**以 `tool:file.read``game/fast_gdd.md` 取证并确认合同的全部 required 验收节点,**通过后才允许把 `gdd-approval` pending 暴露给用户**。取证未通过时**不创建审批卡**,改为发起返工委派(`repair_of` 指向原 delivery),由策划子 Agent 修订后重新提交。
**固定顺序**Supervisor 收到 `plan.submit_gdd``Submitted` 回执后,**必须先**以 `tool:file.read``game/fast_gdd.md` 取证并确认合同的全部 required 验收节点,**通过后才允许把 `gdd-approval` pending 暴露给用户**。尚未取证、分页未覆盖全文或回执 SHA-256 已不是当前 Markdown 时,下一步仍是由当前 Supervisor 根 run 重新读取并更新验收图,不能提前返工;只有当前根、当前 project revision、当前 Markdown 全文证据支撑的显式 `failed` 才表示“取证未通过”,此时**不创建审批卡**,改为发起返工委派(`repair_of` 指向原 delivery),由策划子 Agent 修订后重新提交。
M1C-2a 将该取证合同进一步冻结为:evidence 必须来自当前 `project-supervisor-plan / standard` 根 run 自己的 `file.read`,策划子 run 或同根其它 child 的回执不能替代 Supervisor 的语义判断;路径必须精确为 `game/fast_gdd.md`。读取从 `startLine=1` 开始,按工具上限分页,无缺口、无重叠地覆盖到 EOF;所有页必须报告同一完整内容 SHA-256 和总行数,`agent.acceptance_update.evidence` 必须列出全部分页 actionId。普通 Acceptance Graph durable 读取只复核回执形状、身份与完整覆盖,不把审批决定后 Markdown 状态投影造成的 hash 变化误判为 Graph 损坏;当前 Markdown hash 只在尚无 receipt/pending 的审批前置门复核。
原 planning delivery 必须先由同一 Supervisor 根 run 通过 `agent.run_status` durable 认领。`Ready` 只返回认领动作,不给 `repairOfDelegationId`、也不建审批卡;`ClaimedByParent` 后才根据上述三态产生“继续取证 / 指向原 delivery 的返工 / 建卡”。`plan.submit_gdd` 已 create 但 child 或 delivery 尚未完成的恢复窗口保持惰性,由 M1B-2 原恢复链先收口,不能被审批门抢先判错。
**为什么必须是这个方向**:两道检查的性质不同,混序会让它们互相抵消。
@@ -1218,7 +1222,7 @@ type PlanSubmitGddResult = {
机器判合格在前、人判满意在后,三道门各自的失败路径互不干扰;反序则会出现「人已经批了、机器才说不合格」这种没有出路的组合。schema 那道天然满足本约束——校验失败不产生版本,不产生版本就没有审批卡,结构上不可能反序。
**恢复语义**:取证已完成但 pending 尚未创建时,按既有 identity 幂等补建审批卡,不重新取证;pending 已创建则证明取证已通过,恢复路径不得再次要求取证。取证结果本身不落新的 durable 记录,它就是现役 Acceptance Graph 的 evaluation。
**恢复语义**:取证已完成但 pending 尚未创建时,按既有 identity 幂等补建审批卡,不重新执行 Provider 或 `file.read` 动作;恢复只复核 durable Acceptance Graph、动作回执与当前 Markdown hash。pending 已创建则证明取证已通过,恢复路径不得再次要求取证,也不得因审批后的 Markdown 状态改写否定旧 Graph。取证结果本身不落新的平行记录,它就是现役 Acceptance Graph 的 evaluation。
### 13.1 `gdd-approval` pending
@@ -1827,7 +1831,7 @@ D9 之后,root 是未变的 Project Supervisor,它在 `standard` 下天然
仍然成立、且与本项无关的是**通用 handoff 安全边界**:任何 Provider 成功响应都必须先过现役 storage 的大小、控制字符、敏感键、绝对路径、容量与 durable identity 门并落盘,才允许被消费;storage 门拒绝或 handoff 无法 durable 提交时,禁止保存不安全正文、补 lifecycle completed 或自动重发。这是现役机制,策划链路照用,不需要额外裁决。
- ~~**plan source 与 Goal Contract 协议的关系**~~——**2026-08-13 已裁决**`project-supervisor-plan` **进** `agent_runtime_supervisor_source_is_trusted`;做方案链路正常参与 Goal Contract 协议,不做豁免。原「裁决冻结前依赖 plan source 可信身份的 M1 代码不得合入」的硬门随本裁决解除。
*原阻塞理由已失效*2026-08-12 排除该方向的依据是「出口门在验收阶段照卡且更难救——合同强制至少一个带 `requiredEvidence` 的 required 节点,而**策划 Agent 按设计一项证据工具都不该有**,`.agent/planning/**` 由 Runtime 写、不产生 Agent 回执,必然留下永不 `passed` 的节点」。该推理写于 D6/D9 拓扑,当时「plan run」就是策划 Agent 本身。D11 拆成两层后两个前提都不成立:① 策划子 Agent 的 exact allowlist 恰好含 `file.read` / `file.list`,二者都在 `agent_runtime_acceptance_evidence_tools()` 白名单内;② 更根本的是证据**不必由它出**——`validate_acceptance_evidence_identity_at``apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/acceptance_graph.rs:152`)对证据来源的唯一要求是 `binding.root_agent_id/root_run_id` 等于合同的根,**不要求是根 Agent 自己的回执**;委派子 Agent 的 binding 根就是 Supervisor,因此两层的 `file.read` 回执都是合法验收证据
*原阻塞理由已失效*2026-08-12 排除该方向的依据是「出口门在验收阶段照卡且更难救——合同强制至少一个带 `requiredEvidence` 的 required 节点,而**策划 Agent 按设计一项证据工具都不该有**,`.agent/planning/**` 由 Runtime 写、不产生 Agent 回执,必然留下永不 `passed` 的节点」。该推理写于 D6/D9 拓扑,当时「plan run」就是策划 Agent 本身。D11 拆成两层后Goal Contract 与最终语义验收都属于 Supervisor 根 run;策划子 Agent 负责提交结构化 GDD,不负责替根判断“是否服务用户意图”。M1C-2a 因此把 Fast GDD 特例收紧为**只接受当前 Supervisor 根 run 自己的 `file.read` 回执**,不再沿用通用 Acceptance Graph 对同根 child evidence 的宽松能力。Supervisor 的 standard 工具面已有 `file.read` / `agent.acceptance_update`,固定节点可正常收敛,同时不扩大策划子 Agent 的工具面
*三道门逐一可满足*:入口门只规定时序——合同不存在时本轮必须且只能是一个 `agent.goal_contract`,因此做方案链路的 turn 1 冻结合同、turn 2 才发委派,多一次 Provider 调用而已,不是阻塞;内容门要求 ≥1 个 required 节点且 `requiredEvidence` 命中白名单,`tool:file.read` 满足;出口门要求合同已冻结且验收图收敛,两者都能达成。
@@ -1838,7 +1842,7 @@ D9 之后,root 是未变的 Project Supervisor,它在 `standard` 下天然
- **`plan.submit_gdd` 的 strict schema** 承担机器可判的字段存在性与字段约束(`oneLiner` 长度、`pillars` 条数、MVP 系统条数等,见第 8.2 / 8.3 节),这是硬校验;
- **Goal Contract 验收图**承担「产物确实服务了用户这次的意图」,由 Supervisor 以 `tool:file.read``game/fast_gdd.md` 取证后确认节点。
Goal Contract 中**按项目变化**的只有 `outcome` / `nonNegotiables` / `forbiddenAssumptions` / `openQuestions` 四项——它们承载入口门 Prompt 要求的「自行理解用户真正要做的事」;`acceptanceNodes` 近乎固定,不算「照抄固定信号代替理解」,因为「什么算一份合格 GDD」本就不是本轮需要理解的东西
Goal Contract 中**按项目变化**的只有 `outcome` / `nonNegotiables` / `forbiddenAssumptions` / `openQuestions` 四项——它们承载入口门 Prompt 要求的「自行理解用户真正要做的事」;`preferences` 在该 source 下固定为空数组,`acceptanceNodes` 固定为唯一 `fast-gdd-serves-intent` 节点,不算「照抄固定信号代替理解」,因为「什么算一份合格 GDD」本就不是本轮需要理解的东西。其它可信 source 继续使用现役动态 Goal Contract,不因 criterionId 恰好同名而套用 Fast GDD 的路径、actor 或分页特例
本条**不改变** D1(游戏支柱按项目生成 2~4 条,四个候选不固定套用)与 D8(GDD 不含引擎字段,平台事实由 Runtime 注入)已冻结的口径;验收图据以生成的合格标准以本文档第 5.3、8.2、8.3 节为准。
- ~~**策划节点 `agentId` / source 命名**~~——**2026-08-13 已裁决冻结**:策划子 Agent `agentId=project-planning`Supervisor 侧承载「做方案」入口的新可信 source 取全新常量名 `project-supervisor-plan`(不沿用历史 `project-supervisor-plan-chat` 字面,避免语义改变后接错旧代码路径)。第 1.1 节批二不再被本项阻塞,改由下方新增的 `build.rs` 一致性待裁决项阻塞。
@@ -1921,7 +1925,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``M1C-0b` 已落地并合入**;当前隔离 worktree 已实现 M1C-1 审批核心、receipt 投影/恢复、terminal observation 完整性校验和 plan 根专用 completion blocker。**生产 acceptance-gate pending caller、验收图接线、审批 UI、M1C-2b 澄清中转与构建准入仍未完成**,因此 M1C-1 尚未合回、M1 整体不可交付。合入门见第 23.8 节 |
| 五 | M1 本体:策划闭环功能实现 | **`M1A-1``M1A-2``M1A-3``M1A-4``M1B-1``M1B-2``M1C-0``M1C-0b``M1C-1` 已落地并合入**;当前隔离 worktree `M1C-2a` 已完成固定 Goal Contract、完整分页 `file.read` evidence、claim 后三态 acceptance gatepending exactly-once 恢复与 completion/finalization 接线,并通过本包定向门禁,尚未合回。**`M1C-2b` 澄清中转、审批 UI、构建准入与下游完整构建仍未完成**,因此 M1 整体不可交付。合入门见第 23.8 节 |
批二在 2026-08-13 拆成两半,因为其中一半在 M1 代码存在之前**做不完**:
@@ -2009,8 +2013,8 @@ M0 完成不表示完整策划闭环已经上线。`M1A-1``M1A-4`、`M1B-1`
| `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 门禁均已通过;完整审批链路与产品可交付仍留给后续工作包。**2026-08-15 订正:本包的定向门禁漏掉了两处跨包回归,全量 CI 才暴露**——① `validate_next_entry``same_tool_plan_repair_chain` 提到 `loop_iteration` 分支之外,使含本轮量(steer cursor / goal revision / planning binding)的判据作用于跨 loop 续跑,两条 `tests::goal` 与一条交接用例失败;② 新增的 `missing_plan_submit_anchor_candidate_at` 在 resume 最前面强读 runtime state,短路了下游对不可读 state 的 fail-closed 兜底。均已修,详见 `decision-log.md` 同日条裁决一/二。**教训已记入门禁**:新增或移动校验必须同时补一条「正向必须被接受」的回归,只钉拒绝挡不住作用域被放大 |
| `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 与 plan 根完成门 | `M1B-2``M1C-0`(前向兼容粒度另见 `M1C-0b` | **隔离 worktree 已实现核心**:三动作幂等、版本/指纹竞态防护、receipt 后 index/Markdown/audit/terminal observation/session 投影与恢复、generic v5/v4 anchor 精确消费、terminal summary 完整性校验,以及仅作用于 exact plan 根的只读 completion blocker已补无 pending、awaiting、receipt 后 observation 缺失、完整收口、非 plan root scope 与 pending identity 漂移回归。**生产 acceptance-gate pending caller 与验收前置取证门后置到 `M1C-2a`审批 UI / 澄清中转 / 构建准入仍未完成;本包尚未合回原分支**。连续修订 barrier 与 `UserRevisionRequested` 规则按第 23.7 节执行 |
| `M1C-2a` | Goal Contract 接线:turn 1 冻结、固定验收图、审批前置门取证 | `M1C-1``M1A-3` | turn 1 只能是一个 `agent.goal_contract``acceptanceNodes` 不接受自定义;**第 13.0 节审批前置门:取证未通过时不出现审批卡、而是产生返工委派**(取证顺序是协议时序问题,归本包而非 `M1C-1` |
| `M1C-1` | `gdd-approval` pending、审批命令、receiptreceipt 写入上述 status 与 plan 根完成门 | `M1B-2``M1C-0`(前向兼容粒度另见 `M1C-0b` | **已落地并合入**:三动作幂等、版本/指纹竞态防护、receipt 后 index/Markdown/audit/terminal observation/session 投影与恢复、generic v5/v4 anchor 精确消费、terminal summary 完整性校验,以及仅作用于 exact plan 根的只读 completion blocker;生产 acceptance-gate pending caller 与验收前置取证门按拆包纪律由 `M1C-2a` 承接。审批 UI / 澄清中转 / 构建准入仍未完成。连续修订 barrier 与 `UserRevisionRequested` 规则按第 23.7 节执行 |
| `M1C-2a` | Goal Contract 接线:turn 1 冻结、固定验收图、审批前置门取证 | `M1C-1``M1A-3` | **当前隔离 worktree 已完成并通过本包门禁,尚未合回**turn 1 的 request-scoped schema 与格式修复都只允许一个固定 `agent.goal_contract`;按项目变化的四项之外,`preferences=[]`、唯一验收节点及证据工具均冻结。Fast GDD evidence 只接受当前 Supervisor 根 run 对 `game/fast_gdd.md` 从第 1 行到 EOF 的同 hash 完整分页;无/旧证据先继续读取,显式 failed 才给原 delivery 的 `repairOfDelegationId`passed 且 delivery 已认领才建 pending。pending/recovery/completion/finalization 均按同 identity 幂等,审批后 Markdown 改写不损坏 Graph。格式、Provider 强判据、M1C-2a、Acceptance Graph、planning submit/approval、finalization、all-targets、编码与 diff 门禁均通过;扩展 autonomous completion 整组的无关 game-chat 并行超时及精确复跑结果见 decision-log 同日条,不改该路径。不包含 `M1C-2b`、UI 或构建准入 |
| `M1C-2b` | 澄清中转接线、轮次派生、预算注入 | `M1C-2a` | 3 轮上限;continuation 重放幂等不增加轮次;答案绑定冲突被拒 |
| `M1D-1` | 前端 hydrate 与 GDD 审批卡 | `M1C-2b` | 前端只经 `hydrate_game_creator_plan_gdd_state` 读权威状态,不在页面侧合成批准事实 |
| `M1D-2` | 入口分流与阶段进度 | `M1D-1` | 「直接开建」跳过路径与现状零差异 |