M1C-1:落地 Fast GDD 审批闭环与完成门
新增 plan-gdd approval receipt、pending、审批命令及三动作幂等投影。 接入 receipt 恢复、generic submit 锚点精确消费与 terminal observation 完整性校验。 接入 exact plan-root completion blocker,并补充 pending、recovery、作用域和 identity 回归。 同步 Fast GDD 技术方案与项目决策记录。
This commit is contained in:
@@ -44,6 +44,7 @@ pub(in crate::agent) use run_status_observation::*;
|
||||
pub(in crate::agent) use structured_plan::*;
|
||||
pub(in crate::agent) use tool_plan_protocol::*;
|
||||
|
||||
pub(crate) use crate::agent::runtime_protocol::plan_gdd_completion_blocker_at_locked;
|
||||
#[cfg(test)]
|
||||
pub(crate) use action_audit::agent_runtime_action_receipt_public_safe_detail_for_test;
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -575,6 +575,7 @@ pub(in crate::agent) fn validate_agent_runtime_autonomous_plan_liveness_at(
|
||||
let barrier = static_delegate_completion_barrier_at(root, agent_id, run_id)?;
|
||||
if barrier.ready_unclaimed_count > 0
|
||||
|| barrier.unobserved_claim_count > 0
|
||||
|| barrier.user_revision_pending_count > 0
|
||||
|| barrier.unknown_contract_status_count > 0
|
||||
{
|
||||
let active_delegations =
|
||||
@@ -583,10 +584,11 @@ pub(in crate::agent) fn validate_agent_runtime_autonomous_plan_liveness_at(
|
||||
return Ok(());
|
||||
}
|
||||
return Err(format!(
|
||||
"{AGENT_RUNTIME_AUTONOMOUS_SUPERVISOR_DELIVERY_CONVERGENCE_LIVENESS_ERROR_PREFIX};当前父 run 在准备专业 repair 前仍有 activeDelegations={active_delegations}、readyUnclaimedReceipts={}、unobservedReceiptClaims={}、repairRequired={}、unknownContractStatus={}。必须先只调用 agent.run_status(agentId=null、scope=all、delegationId=null)原子认领并观察已有回执,再基于权威合同准备 repair",
|
||||
"{AGENT_RUNTIME_AUTONOMOUS_SUPERVISOR_DELIVERY_CONVERGENCE_LIVENESS_ERROR_PREFIX};当前父 run 在准备专业 repair 前仍有 activeDelegations={active_delegations}、readyUnclaimedReceipts={}、unobservedReceiptClaims={}、repairRequired={}、userRevisionPending={}、unknownContractStatus={}。必须先只调用 agent.run_status(agentId=null、scope=all、delegationId=null)原子认领并观察已有回执,再基于权威合同准备 repair",
|
||||
barrier.ready_unclaimed_count,
|
||||
barrier.unobserved_claim_count,
|
||||
barrier.repair_required_count,
|
||||
barrier.user_revision_pending_count,
|
||||
barrier.unknown_contract_status_count,
|
||||
));
|
||||
}
|
||||
@@ -601,6 +603,7 @@ 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.user_revision_pending_count > 0
|
||||
|| barrier.unknown_contract_status_count > 0
|
||||
|| active_delegations >= 3;
|
||||
if must_claim_or_wait {
|
||||
@@ -608,10 +611,11 @@ pub(in crate::agent) fn validate_agent_runtime_autonomous_plan_liveness_at(
|
||||
return Ok(());
|
||||
}
|
||||
return Err(format!(
|
||||
"{AGENT_RUNTIME_AUTONOMOUS_SUPERVISOR_DELIVERY_CONVERGENCE_LIVENESS_ERROR_PREFIX};当前父 run 的 activeDelegations={active_delegations}、waitingDelegations={}、readyUnclaimedReceipts={}、unobservedReceiptClaims={}、unknownContractStatus={}。必须先只调用 agent.run_status(agentId=null、scope=all、delegationId=null)认领并观察 ready delivery,或等待已有委派推进;不得创建第四次 agent.delegate。收束后若项目 revision 已推进,再验证当前 revision",
|
||||
"{AGENT_RUNTIME_AUTONOMOUS_SUPERVISOR_DELIVERY_CONVERGENCE_LIVENESS_ERROR_PREFIX};当前父 run 的 activeDelegations={active_delegations}、waitingDelegations={}、readyUnclaimedReceipts={}、unobservedReceiptClaims={}、userRevisionPending={}、unknownContractStatus={}。必须先只调用 agent.run_status(agentId=null、scope=all、delegationId=null)认领并观察 ready delivery,或等待已有委派推进;不得创建第四次 agent.delegate。收束后若项目 revision 已推进,再验证当前 revision",
|
||||
barrier.waiting_count,
|
||||
barrier.ready_unclaimed_count,
|
||||
barrier.unobserved_claim_count,
|
||||
barrier.user_revision_pending_count,
|
||||
barrier.unknown_contract_status_count,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -893,7 +893,12 @@ pub(in crate::agent) fn static_delegate_barrier_has_waiting_deliveries(detail: &
|
||||
.find_map(|part| part.strip_prefix("unknownContractStatus="))
|
||||
.and_then(|value| value.parse::<usize>().ok())
|
||||
.is_some_and(|count| count > 0);
|
||||
waiting || unknown_contract_status
|
||||
let user_revision_pending = detail
|
||||
.split_whitespace()
|
||||
.find_map(|part| part.strip_prefix("userRevisionPending="))
|
||||
.and_then(|value| value.parse::<usize>().ok())
|
||||
.is_some_and(|count| count > 0);
|
||||
waiting || user_revision_pending || unknown_contract_status
|
||||
}
|
||||
|
||||
pub(in crate::agent) fn static_delegate_barrier_requires_repair(detail: &str) -> bool {
|
||||
@@ -904,6 +909,14 @@ pub(in crate::agent) fn static_delegate_barrier_requires_repair(detail: &str) ->
|
||||
.is_some_and(|count| count > 0)
|
||||
}
|
||||
|
||||
pub(in crate::agent) fn static_delegate_barrier_requires_user_revision(detail: &str) -> bool {
|
||||
detail
|
||||
.split_whitespace()
|
||||
.find_map(|part| part.strip_prefix("userRevisionPending="))
|
||||
.and_then(|value| value.parse::<usize>().ok())
|
||||
.is_some_and(|count| count > 0)
|
||||
}
|
||||
|
||||
pub(in crate::agent) fn process_session_completion_blocker_at_locked(
|
||||
root: &Path,
|
||||
agent_id: &str,
|
||||
@@ -948,6 +961,7 @@ pub(in crate::agent) fn agent_runtime_non_verification_completion_blocker_at_loc
|
||||
run_id: &str,
|
||||
) -> Option<AgentRuntimeToolObservation> {
|
||||
provider_retry_completion_blocker_at_locked(root, agent_id, run_id)
|
||||
.or_else(|| plan_gdd_completion_blocker_at_locked(root, agent_id, run_id))
|
||||
.or_else(|| provider_action_batch_completion_blocker_at_locked(root, agent_id, run_id))
|
||||
.or_else(|| {
|
||||
supervisor_collaboration_policy_completion_blocker_at_locked(root, agent_id, run_id)
|
||||
|
||||
@@ -1897,6 +1897,9 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
|
||||
&runtime.run_id,
|
||||
)
|
||||
})
|
||||
.or_else(|| {
|
||||
plan_gdd_completion_blocker_at_locked(&root, &agent_id, &runtime.run_id)
|
||||
})
|
||||
.or_else(|| game_creator_agent_goal_completion_blocker_at_locked(&root, &runtime))
|
||||
.or_else(|| goal_contract_acceptance_completion_blocker_at_locked(&root, &runtime))
|
||||
.or_else(|| {
|
||||
@@ -1943,6 +1946,24 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
|
||||
runtime.current_action = "等待 Provider action 批次收束".to_string();
|
||||
runtime.waiting_on = "持久批次完成确认、执行、投影与 cursor 清理".to_string();
|
||||
runtime.next_step = "先恢复原批次,不能请求新计划或提交最终回复".to_string();
|
||||
} else if blocker.tool == "runtime.plan_gdd" {
|
||||
runtime.status = "running".to_string();
|
||||
if blocker.status == "blocked"
|
||||
&& blocker.detail.as_deref().is_some_and(|detail| {
|
||||
detail.contains("approvalPending=awaiting_decision")
|
||||
})
|
||||
{
|
||||
runtime.phase = "waiting-for-user-input".to_string();
|
||||
runtime.current_action = "等待 Fast GDD 审批决定".to_string();
|
||||
runtime.waiting_on = "用户在审批卡选择批准、修改或退回".to_string();
|
||||
runtime.next_step = "等待 decide_game_creator_plan_gdd;不得重新提交同一 GDD 或自行创建审批 pending".to_string();
|
||||
} else {
|
||||
runtime.phase = "needs-reconciliation".to_string();
|
||||
runtime.current_action = "Fast GDD 审批投影需要人工核对".to_string();
|
||||
runtime.waiting_on = "planning pending、receipt、原提交锚点、terminal observation、audit 与 session 的精确身份".to_string();
|
||||
runtime.next_step =
|
||||
"先恢复或核对现有 durable 事实,不能请求新 Provider 计划".to_string();
|
||||
}
|
||||
} else if blocker.tool == "runtime.collaboration_policy" {
|
||||
runtime.status = "running".to_string();
|
||||
runtime.phase = "planning".to_string();
|
||||
@@ -1977,6 +1998,10 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
|
||||
.detail
|
||||
.as_deref()
|
||||
.is_some_and(static_delegate_barrier_requires_repair);
|
||||
let user_revision_pending = blocker
|
||||
.detail
|
||||
.as_deref()
|
||||
.is_some_and(static_delegate_barrier_requires_user_revision);
|
||||
let user_input_required = blocker.detail.as_deref().is_some_and(|detail| {
|
||||
detail
|
||||
.split_whitespace()
|
||||
@@ -1985,7 +2010,15 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
|
||||
.is_some_and(|count| count > 0)
|
||||
});
|
||||
runtime.status = "running".to_string();
|
||||
if user_input_required {
|
||||
if user_revision_pending {
|
||||
runtime.phase = "planning".to_string();
|
||||
runtime.current_action =
|
||||
"等待 Project Supervisor 发起用户修订续跑".to_string();
|
||||
runtime.waiting_on =
|
||||
"审批卡修改/退回对应的 UserRevisionRequested delivery".to_string();
|
||||
runtime.next_step =
|
||||
"调用 agent.delegate,并把 repairOfDelegationId 指向原 delivery;不得把用户修订计入 repair_depth".to_string();
|
||||
} else if user_input_required {
|
||||
let deliveries = match claimed_static_delegate_deliveries_at(
|
||||
&root,
|
||||
&runtime.agent_id,
|
||||
@@ -2092,6 +2125,24 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
|
||||
"持久化专业 Agent 回执等待状态失败",
|
||||
AgentBackgroundTaskOutcome::WaitingForDelegateReceipts,
|
||||
))
|
||||
} else if observation.tool == "runtime.plan_gdd" {
|
||||
if observation.status == "blocked"
|
||||
&& detail.contains("approvalPending=awaiting_decision")
|
||||
{
|
||||
Some((
|
||||
"agent.runtime.plan.gdd.waiting",
|
||||
"waiting-for-user-input",
|
||||
"Fast GDD 审批等待状态持久化失败",
|
||||
AgentBackgroundTaskOutcome::WaitingForUserInput,
|
||||
))
|
||||
} else {
|
||||
Some((
|
||||
"agent.runtime.plan.gdd.reconciliation",
|
||||
"needs-reconciliation",
|
||||
"Fast GDD 审批投影需要人工核对",
|
||||
AgentBackgroundTaskOutcome::NeedsReconciliation,
|
||||
))
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
||||
@@ -823,6 +823,8 @@ pub(in crate::agent) fn resume_game_creator_agent_background_tasks_unredacted_at
|
||||
root: &Path,
|
||||
) -> Result<Vec<AgentRuntimeResult>, String> {
|
||||
validate_project_root(root)?;
|
||||
reconcile_plan_gdd_approval_projections_at(root)
|
||||
.map_err(|error| format!("恢复 GDD approval 投影失败:{error}"))?;
|
||||
if external_agent_runner_owns_background_execution() {
|
||||
resume_external_agent_runner(root)?;
|
||||
return read_game_creator_agent_runtimes_at(root);
|
||||
|
||||
@@ -8,6 +8,7 @@ mod context_window;
|
||||
mod finalization;
|
||||
mod json_sidecar;
|
||||
mod models;
|
||||
mod planning_approval;
|
||||
mod planning_storage;
|
||||
mod planning_submit;
|
||||
mod provider_control;
|
||||
@@ -23,6 +24,7 @@ pub(in crate::agent) use context_bundle::*;
|
||||
pub(in crate::agent) use finalization::*;
|
||||
pub(in crate::agent) use json_sidecar::*;
|
||||
pub(in crate::agent) use models::*;
|
||||
pub(crate) use planning_approval::*;
|
||||
pub(crate) use planning_storage::*;
|
||||
pub(crate) use planning_submit::*;
|
||||
pub(in crate::agent) use provider_control::*;
|
||||
|
||||
+1606
File diff suppressed because it is too large
Load Diff
+1098
-8
File diff suppressed because it is too large
Load Diff
+404
-14
@@ -1613,19 +1613,38 @@ fn project_submit_successors_locked(
|
||||
return true;
|
||||
}
|
||||
};
|
||||
if read_plan_gdd_index_with_recovery_locked(root, &context.created_at_utc).is_err() {
|
||||
recovery_pending = true;
|
||||
if let Ok(index) = build_plan_gdd_index(&chain, &context.created_at_utc) {
|
||||
// A submit replay may be repairing a projection written for the previous
|
||||
// lineage length. Rebuild from the complete current GDD/receipt facts on
|
||||
// every committed submit so a newly created vN can never leave a vN-1
|
||||
// index behind.
|
||||
let index = match build_plan_gdd_index_for_root_locked(root, &chain, &context.created_at_utc) {
|
||||
Ok(index) => {
|
||||
if write_plan_gdd_index_atomic_locked(root, &index).is_err() {
|
||||
recovery_pending = true;
|
||||
}
|
||||
Some(index)
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
recovery_pending = true;
|
||||
None
|
||||
}
|
||||
};
|
||||
// Markdown is a projection of the current latest authority, not of the
|
||||
// action being replayed. Replaying an older submission must never roll
|
||||
// `game/fast_gdd.md` back over a newer immutable version.
|
||||
let projection_gdd = chain.last().unwrap_or(gdd);
|
||||
if let Ok(markdown) = render_plan_fast_gdd_markdown(projection_gdd, "ready_for_approval") {
|
||||
let projection_status = index
|
||||
.as_ref()
|
||||
.and_then(|index| {
|
||||
index
|
||||
.status_cache
|
||||
.versions
|
||||
.iter()
|
||||
.find(|status| status.version == projection_gdd.version)
|
||||
})
|
||||
.map(|status| status.status.as_str())
|
||||
.unwrap_or("ready_for_approval");
|
||||
if let Ok(markdown) = render_plan_fast_gdd_markdown(projection_gdd, projection_status) {
|
||||
if write_plan_fast_gdd_markdown_atomic_locked(root, &markdown).is_err() {
|
||||
recovery_pending = true;
|
||||
}
|
||||
@@ -1719,6 +1738,7 @@ pub(crate) fn execute_plan_submit_gdd(
|
||||
validate_durable_child_binding(root, context)?;
|
||||
|
||||
let chain = read_plan_gdd_chain_locked(root)?;
|
||||
let approvals = read_plan_gdd_approvals_locked(root)?;
|
||||
// Keep a session read error until after durable action identity replay is
|
||||
// resolved. The GDD create is the commit point: if session projection was
|
||||
// lost/corrupted after that point, a retry must still return the committed
|
||||
@@ -1789,14 +1809,17 @@ pub(crate) fn execute_plan_submit_gdd(
|
||||
"actionFingerprint 已被其它 submissionId 使用",
|
||||
));
|
||||
}
|
||||
if !chain.is_empty() {
|
||||
// M1B-2 has no approval receipt writer yet. Consequently every
|
||||
// existing candidate is still pending; M1C-1 will refine this check
|
||||
// from receipt facts instead of weakening the submit boundary.
|
||||
return Err(submit_error(
|
||||
"PLAN_PENDING_GDD_EXISTS",
|
||||
"已有已提交 GDD;M1B-2 只允许同 submissionId 重放",
|
||||
));
|
||||
validate_plan_gdd_approvals_against_gdds(&chain, &approvals)?;
|
||||
if let Some(latest) = chain.last() {
|
||||
if !approvals
|
||||
.iter()
|
||||
.any(|receipt| receipt.version == latest.version)
|
||||
{
|
||||
return Err(submit_error(
|
||||
"PLAN_PENDING_GDD_EXISTS",
|
||||
"最新 GDD 尚未完成用户审批;只能重放原 submissionId",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let Some(current_session) = current_session else {
|
||||
@@ -1807,13 +1830,23 @@ pub(crate) fn execute_plan_submit_gdd(
|
||||
};
|
||||
validate_plan_session(current_session)?;
|
||||
validate_current_session_cas(current_session, context, input)?;
|
||||
let version = 1;
|
||||
let version = chain
|
||||
.last()
|
||||
.map(|latest| latest.version.saturating_add(1))
|
||||
.unwrap_or(1);
|
||||
if version > PLAN_MAX_VERSIONS {
|
||||
return Err(submit_error(
|
||||
"PLAN_VERSION_LIMIT_REACHED",
|
||||
"不能继续创建第 129 个 GDD 版本",
|
||||
));
|
||||
}
|
||||
let approval_request_id = context
|
||||
.approval_request_id
|
||||
.clone()
|
||||
.unwrap_or_else(generated_approval_request_id);
|
||||
let candidate =
|
||||
build_plan_gdd_from_submit_input(input, context, version, &approval_request_id)?;
|
||||
validate_next_plan_gdd_version(&chain, &candidate)?;
|
||||
let bytes = canonical_plan_gdd_bytes(&candidate)?;
|
||||
|
||||
// Durable create is the submit point. Everything below is best-effort
|
||||
@@ -2073,6 +2106,16 @@ mod tests {
|
||||
None,
|
||||
)
|
||||
.expect("bind plan root");
|
||||
start_game_creator_agent_runtime_task_at(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
"收敛 Fast GDD",
|
||||
"run-root-001",
|
||||
AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE,
|
||||
"等待 project-planning 提交 GDD",
|
||||
vec!["等待策划子 Run 提交 Fast GDD".to_string()],
|
||||
)
|
||||
.expect("start plan root task");
|
||||
let child_binding = bind_game_creator_agent_runtime_run_profile_at(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_PLANNING_AGENT_ID,
|
||||
@@ -2960,6 +3003,353 @@ mod tests {
|
||||
cleanup_fixture(root);
|
||||
}
|
||||
|
||||
fn approval_input(
|
||||
gdd: &PlanGddV1,
|
||||
action: &str,
|
||||
response_id: &str,
|
||||
comment: Option<String>,
|
||||
) -> DecidePlanGddInputV1 {
|
||||
DecidePlanGddInputV1 {
|
||||
gdd_id: gdd.gdd_id.clone(),
|
||||
version: gdd.version,
|
||||
fingerprint: gdd.fingerprint.clone(),
|
||||
pending_action_id: gdd.submission_id.clone(),
|
||||
approval_request_id: gdd.approval_request_id.clone(),
|
||||
response_id: response_id.to_string(),
|
||||
action: action.to_string(),
|
||||
comment,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn approval_pending_is_single_latest_unreceipted_projection() {
|
||||
let (root, context, input) = submit_fixture();
|
||||
execute_plan_submit_gdd(&root, &context, &input).expect("submit v1");
|
||||
let gdd = read_plan_gdd_chain(&root)
|
||||
.expect("read submitted GDD")
|
||||
.pop()
|
||||
.expect("GDD exists");
|
||||
|
||||
create_plan_gdd_approval_pending_at(&root, &gdd).expect("create approval pending");
|
||||
let pending = read_plan_gdd_approval_pending(&root)
|
||||
.expect("read approval pending")
|
||||
.expect("pending exists");
|
||||
assert_eq!(pending.status, "awaiting_decision");
|
||||
// Recreating the exact card is an idempotent replay.
|
||||
create_plan_gdd_approval_pending_at(&root, &gdd).expect("replay approval pending");
|
||||
|
||||
let mut forged_next = gdd.clone();
|
||||
forged_next.version = 2;
|
||||
forged_next.submission_id = "action-fedcba9876543210fedcba98".to_string();
|
||||
forged_next.approval_request_id =
|
||||
"gdd-approval-00000000-0000-4000-8000-000000000003".to_string();
|
||||
forged_next.action_fingerprint = "4".repeat(64);
|
||||
forged_next.fingerprint = plan_gdd_fingerprint(&forged_next).expect("next fingerprint");
|
||||
let stale = create_plan_gdd_approval_pending_at(&root, &forged_next)
|
||||
.expect_err("a non-latest GDD cannot receive an approval card");
|
||||
assert_eq!(stale.code(), "PLAN_STALE_APPROVAL");
|
||||
|
||||
let decision = decide_plan_gdd_at(
|
||||
&root,
|
||||
&approval_input(
|
||||
&gdd,
|
||||
"approve",
|
||||
"gdd-response-00000000-0000-4000-8000-000000000010",
|
||||
None,
|
||||
),
|
||||
)
|
||||
.expect("commit approval receipt");
|
||||
assert_eq!(decision.outcome, "committed");
|
||||
let after_receipt = create_plan_gdd_approval_pending_at(&root, &gdd)
|
||||
.expect_err("a receipt must close awaiting_decision recreation");
|
||||
assert_eq!(after_receipt.code(), "PLAN_STALE_APPROVAL");
|
||||
cleanup_fixture(root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plan_gdd_completion_blocker_requires_pending_and_terminal_observation() {
|
||||
let (root, context, input) = submit_fixture();
|
||||
execute_plan_submit_gdd(&root, &context, &input).expect("submit v1");
|
||||
let gdd = read_plan_gdd_chain(&root)
|
||||
.expect("read submitted GDD")
|
||||
.pop()
|
||||
.expect("GDD exists");
|
||||
|
||||
let blocker = plan_gdd_completion_blocker_at_locked(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
&context.root_run_id,
|
||||
)
|
||||
.expect("missing approval pending must block reconciliation");
|
||||
assert_eq!(blocker.status, "needs-reconciliation");
|
||||
assert!(blocker.summary.contains("审批 pending"));
|
||||
|
||||
create_plan_gdd_approval_pending_at(&root, &gdd).expect("create approval pending");
|
||||
let blocker = plan_gdd_completion_blocker_at_locked(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
&context.root_run_id,
|
||||
)
|
||||
.expect("awaiting approval must block completion");
|
||||
assert_eq!(blocker.status, "blocked");
|
||||
assert!(blocker.summary.contains("等待用户审批"));
|
||||
|
||||
let decision_input = approval_input(
|
||||
&gdd,
|
||||
"approve",
|
||||
"gdd-response-00000000-0000-4000-8000-000000000030",
|
||||
None,
|
||||
);
|
||||
let decision = decide_plan_gdd_at(&root, &decision_input).expect("commit receipt");
|
||||
assert_eq!(decision.outcome, "committed");
|
||||
assert!(decision.recovery_pending);
|
||||
let blocker = plan_gdd_completion_blocker_at_locked(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
&context.root_run_id,
|
||||
)
|
||||
.expect("receipt without terminal observation must reconcile");
|
||||
assert_eq!(blocker.status, "needs-reconciliation");
|
||||
assert!(blocker.summary.contains("terminal observation"));
|
||||
|
||||
append_agent_db_terminal_observation_if_missing_for_action(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_PLANNING_AGENT_ID,
|
||||
&gdd.created_by_run_id,
|
||||
&gdd.submission_id,
|
||||
serde_json::json!({
|
||||
"recordType": "agent.runtime.tool_observation",
|
||||
"agentId": GAME_CREATOR_PROJECT_PLANNING_AGENT_ID,
|
||||
"taskId": "task-plan-001",
|
||||
"runId": gdd.created_by_run_id,
|
||||
"actionId": gdd.submission_id,
|
||||
"actionFingerprint": gdd.action_fingerprint,
|
||||
"tool": PLAN_GDD_APPROVAL_TOOL,
|
||||
"status": "ok",
|
||||
"summary": "Fast GDD v1 已批准",
|
||||
"decision": "approval",
|
||||
}),
|
||||
)
|
||||
.expect("append exact terminal observation");
|
||||
assert!(!reconcile_plan_gdd_approval_projections_at(&root)
|
||||
.expect("reconcile receipt projections"));
|
||||
assert!(plan_gdd_completion_blocker_at_locked(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
&context.root_run_id,
|
||||
)
|
||||
.is_none());
|
||||
cleanup_fixture(root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plan_gdd_completion_blocker_is_scoped_to_exact_plan_root() {
|
||||
let (root, context, input) = submit_fixture();
|
||||
execute_plan_submit_gdd(&root, &context, &input).expect("submit v1");
|
||||
assert!(plan_gdd_completion_blocker_at_locked(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_PLANNING_AGENT_ID,
|
||||
"run-child-001",
|
||||
)
|
||||
.is_none());
|
||||
assert!(plan_gdd_completion_blocker_at_locked(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
"run-not-current",
|
||||
)
|
||||
.is_none());
|
||||
cleanup_fixture(root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plan_gdd_completion_blocker_rejects_mismatched_observed_pending() {
|
||||
let (root, context, input) = submit_fixture();
|
||||
execute_plan_submit_gdd(&root, &context, &input).expect("submit v1");
|
||||
let gdd = read_plan_gdd_chain(&root)
|
||||
.expect("read submitted GDD")
|
||||
.pop()
|
||||
.expect("GDD exists");
|
||||
create_plan_gdd_approval_pending_at(&root, &gdd).expect("create approval pending");
|
||||
decide_plan_gdd_at(
|
||||
&root,
|
||||
&approval_input(
|
||||
&gdd,
|
||||
"approve",
|
||||
"gdd-response-00000000-0000-4000-8000-000000000031",
|
||||
None,
|
||||
),
|
||||
)
|
||||
.expect("commit receipt");
|
||||
|
||||
let mut pending = read_plan_gdd_approval_pending(&root)
|
||||
.expect("read observed pending")
|
||||
.expect("observed pending remains during recovery");
|
||||
pending.run_identity.run_id = "run-other-001".to_string();
|
||||
pending.pending_fingerprint =
|
||||
plan_gdd_approval_pending_fingerprint(&pending).expect("recompute pending fingerprint");
|
||||
write_plan_gdd_approval_pending_atomic(&root, &pending)
|
||||
.expect("write mismatched projection");
|
||||
|
||||
let blocker = plan_gdd_completion_blocker_at_locked(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
&context.root_run_id,
|
||||
)
|
||||
.expect("mismatched pending must require reconciliation");
|
||||
assert_eq!(blocker.status, "needs-reconciliation");
|
||||
assert!(blocker.summary.contains("pending 尚未按 receipt 收口"));
|
||||
cleanup_fixture(root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn approval_comment_accepts_full_scalar_limit_and_observation_prefix() {
|
||||
let (root, context, input) = submit_fixture();
|
||||
execute_plan_submit_gdd(&root, &context, &input).expect("submit v1");
|
||||
let gdd = read_plan_gdd_chain(&root)
|
||||
.expect("read submitted GDD")
|
||||
.pop()
|
||||
.expect("GDD exists");
|
||||
let comment = "界".repeat(1_000);
|
||||
let normalized = normalize_plan_gdd_approval_comment("revise", Some(&comment))
|
||||
.expect("1,000 scalar comment is valid")
|
||||
.expect("comment remains present");
|
||||
assert_eq!(normalized.chars().count(), 1_000);
|
||||
let mut pending = PlanGddApprovalPendingV1 {
|
||||
schema_version: PLAN_GDD_APPROVAL_PENDING_SCHEMA_VERSION.to_string(),
|
||||
kind: PLAN_GDD_APPROVAL_PENDING_KIND.to_string(),
|
||||
project_id: gdd.project_id.clone(),
|
||||
agent_id: PLAN_GDD_APPROVAL_AGENT_ID.to_string(),
|
||||
gdd_ref: PlanGddRef {
|
||||
gdd_id: gdd.gdd_id.clone(),
|
||||
version: gdd.version,
|
||||
fingerprint: gdd.fingerprint.clone(),
|
||||
},
|
||||
submission: PlanGddApprovalPendingSubmission {
|
||||
tool: PLAN_GDD_APPROVAL_TOOL.to_string(),
|
||||
pending_action_id: gdd.submission_id.clone(),
|
||||
action_fingerprint: gdd.action_fingerprint.clone(),
|
||||
approval_request_id: gdd.approval_request_id.clone(),
|
||||
},
|
||||
run_identity: PlanGddApprovalPendingRunIdentity {
|
||||
source: PLAN_GDD_APPROVAL_SOURCE.to_string(),
|
||||
run_profile: gdd.run_profile.clone(),
|
||||
run_profile_binding_fingerprint: gdd.run_profile_binding_fingerprint.clone(),
|
||||
session_id: gdd.session_id.clone(),
|
||||
run_id: gdd.created_by_run_id.clone(),
|
||||
},
|
||||
status: "observed_revise".to_string(),
|
||||
observation: Some(PlanGddApprovalObservationV1 {
|
||||
tool: PLAN_GDD_APPROVAL_TOOL.to_string(),
|
||||
status: "ok".to_string(),
|
||||
summary: format!("Fast GDD v{} 需要修改", gdd.version),
|
||||
detail: Some(format!("用户修改意见:{normalized}")),
|
||||
}),
|
||||
pending_fingerprint: String::new(),
|
||||
};
|
||||
pending.pending_fingerprint =
|
||||
plan_gdd_approval_pending_fingerprint(&pending).expect("pending fingerprint");
|
||||
validate_plan_gdd_approval_pending(&pending)
|
||||
.expect("observation prefix must leave room for the full comment");
|
||||
let mut tampered = pending.clone();
|
||||
tampered
|
||||
.observation
|
||||
.as_mut()
|
||||
.expect("observation exists")
|
||||
.summary = "伪造的审批摘要".to_string();
|
||||
tampered.pending_fingerprint =
|
||||
plan_gdd_approval_pending_fingerprint(&tampered).expect("tampered fingerprint");
|
||||
assert!(validate_plan_gdd_approval_pending(&tampered).is_err());
|
||||
let too_long = format!("{comment}界");
|
||||
assert!(normalize_plan_gdd_approval_comment("revise", Some(&too_long)).is_err());
|
||||
let decision_error = decide_plan_gdd_at(
|
||||
&root,
|
||||
&approval_input(
|
||||
&gdd,
|
||||
"revise",
|
||||
"gdd-response-00000000-0000-4000-8000-000000000011",
|
||||
Some(too_long),
|
||||
),
|
||||
)
|
||||
.expect_err("invalid command comment must be rejected at transport boundary");
|
||||
assert_eq!(decision_error.code(), "PLAN_INVALID_REQUEST");
|
||||
assert_eq!(
|
||||
PLAN_GDD_APPROVAL_FINGERPRINT_DOMAIN,
|
||||
"genarrative.plan.gdd-approval-receipt.v1"
|
||||
);
|
||||
cleanup_fixture(root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn approval_actions_rebuild_receipt_aware_index_and_are_idempotent() {
|
||||
for (action, comment, expected_status) in [
|
||||
("approve", None, "approved"),
|
||||
("revise", Some("请收窄首局范围"), "revision_requested"),
|
||||
("reject", Some("当前方向需要重新梳理"), "rejected"),
|
||||
] {
|
||||
let (root, context, input) = submit_fixture();
|
||||
execute_plan_submit_gdd(&root, &context, &input).expect("submit v1");
|
||||
let gdd = read_plan_gdd_chain(&root)
|
||||
.expect("read submitted GDD")
|
||||
.pop()
|
||||
.expect("GDD exists");
|
||||
create_plan_gdd_approval_pending_at(&root, &gdd).expect("create approval pending");
|
||||
let first_input = approval_input(
|
||||
&gdd,
|
||||
action,
|
||||
"gdd-response-00000000-0000-4000-8000-000000000020",
|
||||
comment.map(str::to_string),
|
||||
);
|
||||
let first = decide_plan_gdd_at(&root, &first_input).expect("commit receipt");
|
||||
assert_eq!(first.outcome, "committed");
|
||||
assert_eq!(first.decision_ref.action, action);
|
||||
assert_eq!(first.approved_gdd_ref.is_some(), action == "approve");
|
||||
|
||||
let approvals = read_plan_gdd_approvals(&root).expect("read receipts");
|
||||
let index = build_plan_gdd_index_with_approvals(
|
||||
&read_plan_gdd_chain(&root).expect("read GDD chain"),
|
||||
&approvals,
|
||||
"2026-08-15T00:00:00.000Z",
|
||||
)
|
||||
.expect("receipt-aware index");
|
||||
assert_eq!(index.status_cache.versions[0].status, expected_status);
|
||||
assert_eq!(index.status_cache.pending_version, None);
|
||||
assert_eq!(
|
||||
index.status_cache.approved_version,
|
||||
(action == "approve").then_some(1)
|
||||
);
|
||||
|
||||
let replay = decide_plan_gdd_at(&root, &first_input).expect("replay same response");
|
||||
assert_eq!(replay.outcome, "replayed");
|
||||
let already_decided = decide_plan_gdd_at(
|
||||
&root,
|
||||
&approval_input(
|
||||
&gdd,
|
||||
action,
|
||||
"gdd-response-00000000-0000-4000-8000-000000000021",
|
||||
comment.map(str::to_string),
|
||||
),
|
||||
)
|
||||
.expect("different response returns existing decision");
|
||||
assert_eq!(already_decided.outcome, "already-decided");
|
||||
let conflicting = decide_plan_gdd_at(
|
||||
&root,
|
||||
&approval_input(
|
||||
&gdd,
|
||||
action,
|
||||
"gdd-response-00000000-0000-4000-8000-000000000020",
|
||||
if action == "approve" {
|
||||
Some("changed intent".to_string())
|
||||
} else {
|
||||
Some("changed intent".to_string())
|
||||
},
|
||||
),
|
||||
)
|
||||
.expect_err("same response with changed intent must conflict");
|
||||
assert_eq!(conflicting.code(), "PLAN_DECISION_IDENTITY_CONFLICT");
|
||||
cleanup_fixture(root);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn projection_failure_after_gdd_create_returns_recovery_pending_and_replays() {
|
||||
let (root, context, input) = submit_fixture();
|
||||
|
||||
@@ -337,6 +337,11 @@ pub(in crate::agent) fn wake_waiting_static_delegate_parent_run_at(
|
||||
ensure_static_delegate_user_input_wait_at(root, &mut state, &deliveries)?;
|
||||
return Ok(true);
|
||||
}
|
||||
if barrier.user_revision_pending_count > 0 {
|
||||
// A user revision is an explicit Supervisor decision boundary. Do
|
||||
// not auto-resume the parent before it dispatches the continuation.
|
||||
return Ok(false);
|
||||
}
|
||||
let state = advance_game_creator_agent_runtime_turn_at(
|
||||
root,
|
||||
state,
|
||||
|
||||
@@ -1206,6 +1206,48 @@ pub(crate) fn answer_game_creator_agent_runtime_user_input(
|
||||
)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) fn decide_game_creator_plan_gdd(
|
||||
project_path: String,
|
||||
gdd_id: String,
|
||||
version: u32,
|
||||
fingerprint: String,
|
||||
pending_action_id: String,
|
||||
approval_request_id: String,
|
||||
response_id: String,
|
||||
action: String,
|
||||
comment: Option<String>,
|
||||
) -> Result<PlanGddDecisionResultV1, String> {
|
||||
let root = validated_local_project_directory_path(project_path.trim())?;
|
||||
enforce_project_permission_policy(&root, "conversation.read")?;
|
||||
enforce_project_permission_policy(&root, "conversation.write")?;
|
||||
enforce_project_permission_policy(&root, "agent.run_status")?;
|
||||
enforce_project_permission_policy(&root, "agent.resume")?;
|
||||
let mut result = decide_plan_gdd_at(
|
||||
&root,
|
||||
&DecidePlanGddInputV1 {
|
||||
gdd_id,
|
||||
version,
|
||||
fingerprint,
|
||||
pending_action_id,
|
||||
approval_request_id,
|
||||
response_id,
|
||||
action,
|
||||
comment,
|
||||
},
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
if !result.recovery_pending {
|
||||
if wake_pending_game_creator_agent_background_tasks_at(&root).is_err() {
|
||||
// The receipt is already the user-decision linearization point;
|
||||
// surface a recoverable projection state instead of turning a
|
||||
// durable approval into a false command failure.
|
||||
result.recovery_pending = true;
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) fn read_game_creator_agent_runtime(
|
||||
project_path: String,
|
||||
|
||||
@@ -216,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) user_revision_pending_count: usize,
|
||||
pub(crate) unknown_contract_status_count: usize,
|
||||
}
|
||||
|
||||
@@ -226,21 +227,25 @@ impl StaticDelegateCompletionBarrier {
|
||||
&& self.unobserved_claim_count == 0
|
||||
&& self.repair_required_count == 0
|
||||
&& self.user_input_required_count == 0
|
||||
&& self.user_revision_pending_count == 0
|
||||
&& self.unknown_contract_status_count == 0
|
||||
}
|
||||
|
||||
pub(crate) fn has_waiting(self) -> bool {
|
||||
self.waiting_count > 0 || self.unknown_contract_status_count > 0
|
||||
self.waiting_count > 0
|
||||
|| self.user_revision_pending_count > 0
|
||||
|| self.unknown_contract_status_count > 0
|
||||
}
|
||||
|
||||
pub(crate) fn detail(self) -> String {
|
||||
format!(
|
||||
"waitingDelegations={} · readyUnclaimedReceipts={} · unobservedReceiptClaims={} · repairRequired={} · userInputRequired={} · unknownContractStatus={} · 必须认领专业 Agent 回执,处理 needs-user-input/needs-repair,或升级客户端后再继续",
|
||||
"waitingDelegations={} · readyUnclaimedReceipts={} · unobservedReceiptClaims={} · repairRequired={} · userInputRequired={} · userRevisionPending={} · unknownContractStatus={} · 必须认领专业 Agent 回执,处理 needs-user-input/needs-repair/user-revision-requested,或升级客户端后再继续",
|
||||
self.waiting_count,
|
||||
self.ready_unclaimed_count,
|
||||
self.unobserved_claim_count,
|
||||
self.repair_required_count,
|
||||
self.user_input_required_count,
|
||||
self.user_revision_pending_count,
|
||||
self.unknown_contract_status_count
|
||||
)
|
||||
}
|
||||
@@ -445,6 +450,42 @@ pub(crate) fn mark_static_delegate_delivery_ready_with_result_at(
|
||||
Ok(delivery)
|
||||
}
|
||||
|
||||
/// Mark an already claimed, evidence-ready planning delivery as waiting for a
|
||||
/// user-requested revision. Approval is the only producer of this durable
|
||||
/// status; keeping the transition here makes its evidence precondition and
|
||||
/// idempotency explicit instead of allowing a generic delivery writer to
|
||||
/// manufacture the state.
|
||||
pub(crate) fn mark_static_delegate_delivery_user_revision_requested_at(
|
||||
root: &Path,
|
||||
parent_agent_id: &str,
|
||||
parent_run_id: &str,
|
||||
delegation_id: &str,
|
||||
) -> Result<StaticDelegateDeliveryRecord, String> {
|
||||
validate_static_delegate_id(parent_agent_id, "parentAgentId", 96)?;
|
||||
validate_static_delegate_id(parent_run_id, "parentRunId", 160)?;
|
||||
validate_static_delegate_id(delegation_id, "delegationId", 160)?;
|
||||
let mut delivery = read_static_delegate_delivery_at(root, delegation_id)?
|
||||
.ok_or_else(|| format!("静态委派 delivery 不存在:{delegation_id}"))?;
|
||||
if delivery.parent_agent_id != parent_agent_id || delivery.parent_run_id != parent_run_id {
|
||||
return Err("用户修订只能改写同一 Supervisor 父 run 的 delivery".to_string());
|
||||
}
|
||||
if delivery.status != StaticDelegateDeliveryStatus::ClaimedByParent {
|
||||
return Err("用户修订只能改写已由 Supervisor 认领的 delivery".to_string());
|
||||
}
|
||||
let Some(result) = delivery.structured_result.as_mut() else {
|
||||
return Err("用户修订的原 delivery 缺少 structuredResult".to_string());
|
||||
};
|
||||
match result.contract_status {
|
||||
StaticDelegateContractStatus::UserRevisionRequested => return Ok(delivery),
|
||||
StaticDelegateContractStatus::EvidenceReady => {}
|
||||
_ => return Err("用户修订只能从 EvidenceReady delivery 派生".to_string()),
|
||||
}
|
||||
result.contract_status = StaticDelegateContractStatus::UserRevisionRequested;
|
||||
delivery.updated_at = unix_timestamp();
|
||||
write_static_delegate_delivery_at(root, &delivery)?;
|
||||
Ok(delivery)
|
||||
}
|
||||
|
||||
pub(crate) fn suppress_static_delegate_delivery_at(
|
||||
root: &Path,
|
||||
expected: &StaticDelegateDeliveryRecord,
|
||||
@@ -558,6 +599,25 @@ pub(crate) fn static_delegate_completion_barrier_at(
|
||||
})
|
||||
})
|
||||
.count();
|
||||
barrier.user_revision_pending_count = deliveries
|
||||
.iter()
|
||||
.filter(|delivery| {
|
||||
delivery.status == StaticDelegateDeliveryStatus::ClaimedByParent
|
||||
&& delivery.structured_result.as_ref().is_some_and(|result| {
|
||||
result.contract_status == StaticDelegateContractStatus::UserRevisionRequested
|
||||
})
|
||||
&& !deliveries.iter().any(|candidate| {
|
||||
candidate.repair_of_delegation_id.as_deref()
|
||||
== Some(delivery.delegation_id.as_str())
|
||||
&& matches!(
|
||||
candidate.status,
|
||||
StaticDelegateDeliveryStatus::Dispatched
|
||||
| StaticDelegateDeliveryStatus::Ready
|
||||
| StaticDelegateDeliveryStatus::ClaimedByParent
|
||||
)
|
||||
})
|
||||
})
|
||||
.count();
|
||||
barrier.unknown_contract_status_count = deliveries
|
||||
.iter()
|
||||
.filter(|delivery| {
|
||||
@@ -2281,28 +2341,39 @@ fn validate_static_delegate_structured_result(
|
||||
if result.verified_revision == Some(0) {
|
||||
return Err("静态委派 verifiedRevision 必须大于 0".to_string());
|
||||
}
|
||||
if result.contract_status == StaticDelegateContractStatus::EvidenceReady
|
||||
&& (terminal_status != "completed"
|
||||
|| !result.missing_expected_artifacts.is_empty()
|
||||
|| (result.verification_required && result.verified_revision.is_none()))
|
||||
{
|
||||
return Err("静态委派 evidence-ready 与客观证据冲突".to_string());
|
||||
match &result.contract_status {
|
||||
StaticDelegateContractStatus::EvidenceReady
|
||||
| StaticDelegateContractStatus::UserRevisionRequested => {
|
||||
if terminal_status != "completed"
|
||||
|| !result.missing_expected_artifacts.is_empty()
|
||||
|| (result.verification_required && result.verified_revision.is_none())
|
||||
{
|
||||
return Err(
|
||||
"静态委派 evidence-ready/user-revision-requested 与客观证据冲突".to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
StaticDelegateContractStatus::NeedsUserInput => {
|
||||
if terminal_status != "completed"
|
||||
|| result.user_input_questions.is_empty()
|
||||
|| result.user_input_questions.len() > 3
|
||||
{
|
||||
return Err("静态委派 needs-user-input 与终态或问题数量冲突".to_string());
|
||||
}
|
||||
let expected_sha = serde_json::to_vec(&result.user_input_questions)
|
||||
.map(|bytes| format!("{:x}", Sha256::digest(bytes)))
|
||||
.map_err(|error| format!("序列化静态委派用户问题失败:{error}"))?;
|
||||
if result.user_input_questions_sha256.as_deref() != Some(expected_sha.as_str()) {
|
||||
return Err("静态委派 needs-user-input 问题指纹无效".to_string());
|
||||
}
|
||||
}
|
||||
StaticDelegateContractStatus::NeedsRepair | StaticDelegateContractStatus::Unknown(_) => {}
|
||||
}
|
||||
if result.contract_status == StaticDelegateContractStatus::NeedsUserInput {
|
||||
if terminal_status != "completed"
|
||||
|| result.user_input_questions.is_empty()
|
||||
|| result.user_input_questions.len() > 3
|
||||
{
|
||||
return Err("静态委派 needs-user-input 与终态或问题数量冲突".to_string());
|
||||
}
|
||||
let expected_sha = serde_json::to_vec(&result.user_input_questions)
|
||||
.map(|bytes| format!("{:x}", Sha256::digest(bytes)))
|
||||
.map_err(|error| format!("序列化静态委派用户问题失败:{error}"))?;
|
||||
if result.user_input_questions_sha256.as_deref() != Some(expected_sha.as_str()) {
|
||||
return Err("静态委派 needs-user-input 问题指纹无效".to_string());
|
||||
}
|
||||
} else if !result.user_input_questions.is_empty()
|
||||
|| result.user_input_questions_sha256.is_some()
|
||||
if !matches!(
|
||||
result.contract_status,
|
||||
StaticDelegateContractStatus::NeedsUserInput
|
||||
) && (!result.user_input_questions.is_empty()
|
||||
|| result.user_input_questions_sha256.is_some())
|
||||
{
|
||||
return Err("非 needs-user-input 静态委派不能携带用户问题".to_string());
|
||||
}
|
||||
@@ -2932,6 +3003,145 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn static_delegate_user_revision_barrier_blocks_every_revision_until_continuation() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"genarrative-static-user-revision-barrier-{}-{}",
|
||||
std::process::id(),
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.expect("system time after unix epoch")
|
||||
.as_nanos()
|
||||
));
|
||||
init_local_game_project_at(
|
||||
&root,
|
||||
"m1c1-user-revision-barrier",
|
||||
"M1C-1 用户修订 barrier 测试",
|
||||
)
|
||||
.expect("project init");
|
||||
let parent_run_id = "m1c1-user-revision-barrier-parent-run";
|
||||
let first = claimed_static_delegate_for_lineage_test(
|
||||
parent_run_id,
|
||||
"m1c1-user-revision-barrier-first",
|
||||
None,
|
||||
StaticDelegateContractStatus::EvidenceReady,
|
||||
);
|
||||
write_static_delegate_delivery_at(&root, &first).expect("write first delivery");
|
||||
mark_static_delegate_delivery_user_revision_requested_at(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
parent_run_id,
|
||||
&first.delegation_id,
|
||||
)
|
||||
.expect("mark first delivery for user revision");
|
||||
|
||||
let first_barrier = static_delegate_completion_barrier_at(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
parent_run_id,
|
||||
)
|
||||
.expect("read first revision barrier");
|
||||
assert_eq!(first_barrier.user_revision_pending_count, 1);
|
||||
assert!(!first_barrier.is_clear());
|
||||
assert!(first_barrier.has_waiting());
|
||||
assert!(first_barrier.detail().contains("userRevisionPending=1"));
|
||||
|
||||
let mut continuation = claimed_static_delegate_for_lineage_test(
|
||||
parent_run_id,
|
||||
"m1c1-user-revision-barrier-continuation",
|
||||
Some(&first.delegation_id),
|
||||
StaticDelegateContractStatus::EvidenceReady,
|
||||
);
|
||||
continuation.status = StaticDelegateDeliveryStatus::Dispatched;
|
||||
continuation.terminal_status = None;
|
||||
continuation.result_summary = None;
|
||||
continuation.structured_result = None;
|
||||
continuation.claimed_by_action_id = None;
|
||||
write_static_delegate_delivery_at(&root, &continuation).expect("write continuation");
|
||||
let active_barrier = static_delegate_completion_barrier_at(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
parent_run_id,
|
||||
)
|
||||
.expect("read active continuation barrier");
|
||||
assert_eq!(active_barrier.user_revision_pending_count, 0);
|
||||
assert_eq!(active_barrier.waiting_count, 1);
|
||||
|
||||
continuation.status = StaticDelegateDeliveryStatus::ClaimedByParent;
|
||||
continuation.terminal_status = Some("completed".to_string());
|
||||
continuation.result_summary = Some("second revision candidate".to_string());
|
||||
continuation.structured_result = Some(StaticDelegateStructuredResult {
|
||||
contract_status: StaticDelegateContractStatus::EvidenceReady,
|
||||
..StaticDelegateStructuredResult::default()
|
||||
});
|
||||
continuation.claimed_by_action_id =
|
||||
Some("m1c1-user-revision-barrier-continuation-claim".to_string());
|
||||
write_static_delegate_delivery_at(&root, &continuation)
|
||||
.expect("complete continuation delivery");
|
||||
assert!(
|
||||
static_delegate_completion_barrier_at(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
parent_run_id,
|
||||
)
|
||||
.expect("read completed continuation barrier")
|
||||
.is_clear(),
|
||||
"the previous revision is satisfied once its continuation is claimed"
|
||||
);
|
||||
|
||||
mark_static_delegate_delivery_user_revision_requested_at(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
parent_run_id,
|
||||
&continuation.delegation_id,
|
||||
)
|
||||
.expect("mark a repair-node delivery for the second revision");
|
||||
let second_barrier = static_delegate_completion_barrier_at(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
parent_run_id,
|
||||
)
|
||||
.expect("read second revision barrier");
|
||||
assert_eq!(
|
||||
second_barrier.user_revision_pending_count, 1,
|
||||
"a revision whose parent delivery is itself a repair node must still block"
|
||||
);
|
||||
assert!(!second_barrier.is_clear());
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn static_delegate_user_revision_reuses_evidence_ready_objective_constraints() {
|
||||
let base = StaticDelegateStructuredResult {
|
||||
contract_status: StaticDelegateContractStatus::UserRevisionRequested,
|
||||
..StaticDelegateStructuredResult::default()
|
||||
};
|
||||
validate_static_delegate_structured_result(&base, "completed", &[])
|
||||
.expect("completed user revision with complete evidence is valid");
|
||||
|
||||
let failed = validate_static_delegate_structured_result(&base, "failed", &[])
|
||||
.expect_err("failed terminal status must reject a user revision result");
|
||||
assert!(failed.contains("客观证据冲突"));
|
||||
|
||||
let mut missing = base.clone();
|
||||
missing.missing_expected_artifacts = vec!["game/fast_gdd.md".to_string()];
|
||||
let missing_error = validate_static_delegate_structured_result(
|
||||
&missing,
|
||||
"completed",
|
||||
&["game/fast_gdd.md".to_string()],
|
||||
)
|
||||
.expect_err("missing expected artifact must reject a user revision result");
|
||||
assert!(missing_error.contains("客观证据冲突"));
|
||||
|
||||
let mut unverified = base;
|
||||
unverified.verification_required = true;
|
||||
let verification_error =
|
||||
validate_static_delegate_structured_result(&unverified, "completed", &[])
|
||||
.expect_err("required verification without a revision must be rejected");
|
||||
assert!(verification_error.contains("客观证据冲突"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn static_delegate_lineage_boundary_and_status_deserialization_fail_closed() {
|
||||
let mut deliveries = Vec::new();
|
||||
|
||||
@@ -2208,6 +2208,7 @@ fn main() {
|
||||
confirm_game_creator_agent_runtime_task,
|
||||
reject_game_creator_agent_runtime_task,
|
||||
answer_game_creator_agent_runtime_user_input,
|
||||
decide_game_creator_plan_gdd,
|
||||
read_game_creator_agent_runtime,
|
||||
read_game_creator_agent_runtimes,
|
||||
resume_game_creator_agent_runtime_tasks,
|
||||
|
||||
@@ -6,6 +6,8 @@ use super::filesystem::PROJECT_FILE_FLAG_OPEN_REPARSE_POINT;
|
||||
|
||||
const AGENT_DB_MAX_RECORD_BYTES: usize = 1024 * 1024;
|
||||
const AGENT_DB_ACTION_RECEIPT_RECORD_TYPE: &str = "agent.runtime.action_receipt";
|
||||
const AGENT_DB_PLAN_GDD_DECISION_RECORD_TYPE: &str = "agent.runtime.plan.gdd_decided";
|
||||
const AGENT_DB_PLAN_GDD_DECISION_AUDIT_SCHEMA_V1: &str = "agent-runtime-plan-gdd-decided.v1";
|
||||
const AGENT_DB_PROVIDER_REQUEST_LIFECYCLE_RECORD_TYPE: &str =
|
||||
"agent.runtime.provider_request.lifecycle";
|
||||
const AGENT_DB_FINALIZATION_LIFECYCLE_RECORD_TYPE: &str = "agent.runtime.finalization.lifecycle";
|
||||
@@ -32,12 +34,22 @@ const AGENT_DB_LIFECYCLE_TERMINAL_RESERVE_RECORDS: u64 = 128;
|
||||
const AGENT_DB_LIFECYCLE_TERMINAL_RESERVE_BYTES: u64 =
|
||||
(AGENT_DB_FINALIZATION_CRITICAL_MAX_RECORD_BYTES as u64 + 1)
|
||||
* AGENT_DB_LIFECYCLE_TERMINAL_RESERVE_RECORDS;
|
||||
// Planning decisions are a separate durability lane. They must retain a
|
||||
// complete 128-version lineage even when ordinary/lifecycle records consume
|
||||
// the rest of the Agent DB budget, so they cannot share either existing tail.
|
||||
const AGENT_DB_PLAN_GDD_DECISION_MAX_RECORD_BYTES: usize = 16 * 1024;
|
||||
const AGENT_DB_PLAN_GDD_DECISION_RESERVE_RECORDS: u64 = 128;
|
||||
const AGENT_DB_PLAN_GDD_DECISION_RESERVE_BYTES: u64 =
|
||||
(AGENT_DB_PLAN_GDD_DECISION_MAX_RECORD_BYTES as u64 + 1)
|
||||
* AGENT_DB_PLAN_GDD_DECISION_RESERVE_RECORDS;
|
||||
const AGENT_DB_MAX_ORDINARY_APPEND_BYTES: u64 = AGENT_DB_MAX_ACTION_RECEIPT_SCAN_BYTES
|
||||
- AGENT_DB_TERMINAL_RESERVE_BYTES
|
||||
- AGENT_DB_LIFECYCLE_TERMINAL_RESERVE_BYTES;
|
||||
- AGENT_DB_LIFECYCLE_TERMINAL_RESERVE_BYTES
|
||||
- AGENT_DB_PLAN_GDD_DECISION_RESERVE_BYTES;
|
||||
const AGENT_DB_MAX_ORDINARY_APPEND_RECORDS: usize = AGENT_DB_MAX_SCAN_RECORDS
|
||||
- AGENT_DB_TERMINAL_RESERVE_RECORDS as usize
|
||||
- AGENT_DB_LIFECYCLE_TERMINAL_RESERVE_RECORDS as usize;
|
||||
- AGENT_DB_LIFECYCLE_TERMINAL_RESERVE_RECORDS as usize
|
||||
- AGENT_DB_PLAN_GDD_DECISION_RESERVE_RECORDS as usize;
|
||||
const AGENT_DB_FINALIZATION_CRITICAL_RECORDS_PER_SEQUENCE: usize = 7;
|
||||
const AGENT_DB_MAX_BOUNDED_READ_BYTES: u64 = 32 * 1024 * 1024;
|
||||
const AGENT_DB_MAX_BOUNDED_RECORDS: usize = 16_384;
|
||||
@@ -48,10 +60,14 @@ pub(super) enum AgentDbRecordAppendClass {
|
||||
ActionTerminal,
|
||||
LifecycleTerminal,
|
||||
FinalizationCritical,
|
||||
PlanGddDecision,
|
||||
}
|
||||
|
||||
pub(super) fn agent_db_record_append_class(record: &serde_json::Value) -> AgentDbRecordAppendClass {
|
||||
let record_type = record.get("recordType").and_then(serde_json::Value::as_str);
|
||||
if record_type == Some(AGENT_DB_PLAN_GDD_DECISION_RECORD_TYPE) {
|
||||
return AgentDbRecordAppendClass::PlanGddDecision;
|
||||
}
|
||||
if record_type == Some(AGENT_DB_ACTION_RECEIPT_RECORD_TYPE)
|
||||
|| agent_db_record_uses_terminal_reserve(record)
|
||||
{
|
||||
@@ -956,6 +972,9 @@ pub(crate) fn append_agent_db_record(root: &Path, record: serde_json::Value) ->
|
||||
Some(AGENT_DB_ACTION_RECEIPT_RECORD_TYPE) => {
|
||||
return Err("Agent 持久动作回执必须使用幂等终态 receipt 追加入口".to_string())
|
||||
}
|
||||
Some(AGENT_DB_PLAN_GDD_DECISION_RECORD_TYPE) => {
|
||||
return Err("Agent DB planning decision 必须使用专用幂等追加入口".to_string())
|
||||
}
|
||||
Some(
|
||||
AGENT_DB_PROVIDER_REQUEST_LIFECYCLE_RECORD_TYPE
|
||||
| AGENT_DB_FINALIZATION_LIFECYCLE_RECORD_TYPE,
|
||||
@@ -1087,6 +1106,14 @@ fn validate_agent_db_append_class_record_size(
|
||||
AGENT_DB_LIFECYCLE_TERMINAL_MAX_RECORD_BYTES
|
||||
));
|
||||
}
|
||||
if record_type.as_deref() == Some(AGENT_DB_PLAN_GDD_DECISION_RECORD_TYPE)
|
||||
&& line.len() > AGENT_DB_PLAN_GDD_DECISION_MAX_RECORD_BYTES
|
||||
{
|
||||
return Err(format!(
|
||||
"Agent DB planning decision 单条记录超过 {} 字节上限",
|
||||
AGENT_DB_PLAN_GDD_DECISION_MAX_RECORD_BYTES
|
||||
));
|
||||
}
|
||||
if append_class == AgentDbRecordAppendClass::FinalizationCritical
|
||||
&& line.len() > AGENT_DB_FINALIZATION_CRITICAL_MAX_RECORD_BYTES
|
||||
{
|
||||
@@ -2264,6 +2291,233 @@ pub(crate) fn append_agent_db_plan_submit_gdd_committed_if_missing_for_action(
|
||||
)
|
||||
}
|
||||
|
||||
/// Append the dedicated planning-decision audit lane. This intentionally
|
||||
/// does not reuse action-id idempotency: a response id is scoped by
|
||||
/// `(gddId, version)` and may be reused on another version, while two windows
|
||||
/// deciding one version with different intent must fail closed.
|
||||
pub(crate) fn append_agent_db_plan_gdd_decision_if_missing(
|
||||
root: &Path,
|
||||
record: serde_json::Value,
|
||||
) -> Result<bool, String> {
|
||||
const RECORD_TYPE: &str = AGENT_DB_PLAN_GDD_DECISION_RECORD_TYPE;
|
||||
const FIELDS: &[&str] = &[
|
||||
"recordType",
|
||||
"auditSchemaVersion",
|
||||
"projectId",
|
||||
"agentId",
|
||||
"gddId",
|
||||
"version",
|
||||
"gddFingerprint",
|
||||
"pendingActionId",
|
||||
"actionFingerprint",
|
||||
"approvalRequestId",
|
||||
"responseId",
|
||||
"source",
|
||||
"runProfile",
|
||||
"runProfileBindingFingerprint",
|
||||
"sessionId",
|
||||
"runId",
|
||||
"action",
|
||||
"decisionFingerprint",
|
||||
"commentHash",
|
||||
"commentLength",
|
||||
"receiptFingerprint",
|
||||
"decidedAtUtc",
|
||||
];
|
||||
if !agent_db_record_has_exact_payload_fields(&record, FIELDS)
|
||||
|| record.get("recordType").and_then(serde_json::Value::as_str) != Some(RECORD_TYPE)
|
||||
|| record
|
||||
.get("auditSchemaVersion")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
!= Some(AGENT_DB_PLAN_GDD_DECISION_AUDIT_SCHEMA_V1)
|
||||
|| record.get("agentId").and_then(serde_json::Value::as_str) != Some("project-supervisor")
|
||||
|| record.get("source").and_then(serde_json::Value::as_str)
|
||||
!= Some("project-supervisor-plan")
|
||||
|| record.get("runProfile").and_then(serde_json::Value::as_str) != Some("standard")
|
||||
|| !matches!(
|
||||
record.get("action").and_then(serde_json::Value::as_str),
|
||||
Some("approve" | "revise" | "reject")
|
||||
)
|
||||
{
|
||||
return Err("Agent DB planning decision 字段集合或固定身份无效".to_string());
|
||||
}
|
||||
let project_id = record
|
||||
.get("projectId")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or_default();
|
||||
let gdd_id = record
|
||||
.get("gddId")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or_default();
|
||||
let version = record
|
||||
.get("version")
|
||||
.and_then(serde_json::Value::as_u64)
|
||||
.unwrap_or(0);
|
||||
let gdd_fingerprint = record
|
||||
.get("gddFingerprint")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or_default();
|
||||
let pending_action_id = record
|
||||
.get("pendingActionId")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or_default();
|
||||
let action_fingerprint = record
|
||||
.get("actionFingerprint")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or_default();
|
||||
let approval_request_id = record
|
||||
.get("approvalRequestId")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or_default();
|
||||
let response_id = record
|
||||
.get("responseId")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or_default();
|
||||
let binding_fingerprint = record
|
||||
.get("runProfileBindingFingerprint")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or_default();
|
||||
let session_id = record
|
||||
.get("sessionId")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or_default();
|
||||
let run_id = record
|
||||
.get("runId")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or_default();
|
||||
let decision_fingerprint = record
|
||||
.get("decisionFingerprint")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or_default();
|
||||
let comment_hash = record
|
||||
.get("commentHash")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or_default();
|
||||
let receipt_fingerprint = record
|
||||
.get("receiptFingerprint")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or_default();
|
||||
let comment_length = record
|
||||
.get("commentLength")
|
||||
.and_then(serde_json::Value::as_u64)
|
||||
.unwrap_or(u64::MAX);
|
||||
let valid_uuid_suffix = |value: &str, prefix: &str| {
|
||||
value.strip_prefix(prefix).is_some_and(|suffix| {
|
||||
uuid::Uuid::parse_str(suffix)
|
||||
.ok()
|
||||
.is_some_and(|parsed| parsed.hyphenated().to_string() == suffix)
|
||||
})
|
||||
};
|
||||
let valid_typed = |value: &str| {
|
||||
value
|
||||
.strip_prefix("sha256-serde-json-v2:")
|
||||
.is_some_and(|suffix| is_valid_agent_db_sha256(suffix))
|
||||
};
|
||||
if project_id.is_empty()
|
||||
|| !is_safe_agent_db_lifecycle_identity(project_id)
|
||||
|| !valid_uuid_suffix(gdd_id, "gdd-")
|
||||
|| !(1..=128).contains(&version)
|
||||
|| !valid_typed(gdd_fingerprint)
|
||||
|| !pending_action_id
|
||||
.strip_prefix("action-")
|
||||
.is_some_and(|suffix| {
|
||||
suffix.len() == 24
|
||||
&& suffix
|
||||
.bytes()
|
||||
.all(|byte| (b'a'..=b'f').contains(&byte) || byte.is_ascii_digit())
|
||||
})
|
||||
|| !is_valid_agent_db_sha256(action_fingerprint)
|
||||
|| !valid_uuid_suffix(approval_request_id, "gdd-approval-")
|
||||
|| !valid_uuid_suffix(response_id, "gdd-response-")
|
||||
|| !is_valid_agent_db_sha256(binding_fingerprint)
|
||||
|| !is_safe_agent_db_lifecycle_identity(session_id)
|
||||
|| !is_safe_agent_db_lifecycle_identity(run_id)
|
||||
|| !valid_typed(decision_fingerprint)
|
||||
|| !valid_typed(comment_hash)
|
||||
|| comment_length > 1_000
|
||||
|| !valid_typed(receipt_fingerprint)
|
||||
{
|
||||
return Err("Agent DB planning decision durable identity/fingerprint 无效".to_string());
|
||||
}
|
||||
let decided_at = record
|
||||
.get("decidedAtUtc")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or_default();
|
||||
crate::agent::validate_timestamp(decided_at, "agent.db.decidedAtUtc")
|
||||
.map_err(|error| error.to_string())?;
|
||||
|
||||
let path = root.join(".agent/agent.db");
|
||||
let directory = open_agent_db_directory(root, true)?
|
||||
.ok_or_else(|| "创建项目 .agent 目录失败".to_string())?;
|
||||
let append_lock = project_append_lock_for(&path)?;
|
||||
let _append_guard = append_lock.lock_process("Agent 本地索引")?;
|
||||
verify_agent_db_directory_current(&directory)?;
|
||||
let mut storage = open_agent_db_storage(directory, true, true)?
|
||||
.ok_or_else(|| "创建 Agent 本地索引失败".to_string())?;
|
||||
verify_agent_db_storage_current(&storage)?;
|
||||
repair_truncated_jsonl_tail_unlocked(&mut storage.file, &storage.path, "Agent 本地索引")?;
|
||||
verify_agent_db_storage_current(&storage)?;
|
||||
|
||||
let length = storage
|
||||
.file
|
||||
.metadata()
|
||||
.map_err(|error| format!("读取 Agent 本地索引元数据失败:{error}"))?
|
||||
.len();
|
||||
if length > AGENT_DB_MAX_ACTION_RECEIPT_SCAN_BYTES {
|
||||
return Err("Agent 本地索引超过扫描上限,无法追加 planning decision".to_string());
|
||||
}
|
||||
storage
|
||||
.file
|
||||
.seek(SeekFrom::Start(0))
|
||||
.map_err(|error| format!("定位 Agent 本地索引失败:{error}"))?;
|
||||
let mut reader = BufReader::new(&mut storage.file);
|
||||
let mut count = 0usize;
|
||||
while let Some(line) = read_agent_db_jsonl_line_bounded(&mut reader, &storage.path)? {
|
||||
if !line.complete {
|
||||
break;
|
||||
}
|
||||
if line.content.iter().all(|byte| byte.is_ascii_whitespace()) {
|
||||
continue;
|
||||
}
|
||||
count = count.saturating_add(1);
|
||||
if count > AGENT_DB_MAX_SCAN_RECORDS {
|
||||
return Err("Agent 本地索引超过记录扫描上限".to_string());
|
||||
}
|
||||
let stored = serde_json::from_slice::<serde_json::Value>(&line.content)
|
||||
.map_err(|error| format!("解析 Agent 本地索引失败:{error}"))?;
|
||||
if stored.get("recordType").and_then(serde_json::Value::as_str) == Some(RECORD_TYPE)
|
||||
&& stored.get("gddId").and_then(serde_json::Value::as_str) == Some(gdd_id)
|
||||
&& stored.get("version").and_then(serde_json::Value::as_u64) == Some(version)
|
||||
&& stored.get("responseId").and_then(serde_json::Value::as_str) == Some(response_id)
|
||||
{
|
||||
if !agent_db_record_has_exact_payload_fields(&stored, FIELDS) {
|
||||
return Err("Agent DB planning decision 已有记录字段集合损坏".to_string());
|
||||
}
|
||||
let mut comparable = stored.clone();
|
||||
if let Some(object) = comparable.as_object_mut() {
|
||||
object.remove("schemaVersion");
|
||||
object.remove("updatedAt");
|
||||
}
|
||||
if comparable == record {
|
||||
return Ok(false);
|
||||
}
|
||||
return Err(
|
||||
"PLAN_DECISION_IDENTITY_CONFLICT: planning decision 幂等键 payload 不一致"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
drop(reader);
|
||||
let line = serialize_agent_db_record(record)?;
|
||||
validate_agent_db_append_class_record_size(AgentDbRecordAppendClass::PlanGddDecision, &line)?;
|
||||
append_agent_db_classified_line_unlocked(
|
||||
&mut storage,
|
||||
&line,
|
||||
AgentDbRecordAppendClass::PlanGddDecision,
|
||||
)?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub(crate) fn append_agent_db_record_if_missing_for_action_with_before_lock<F>(
|
||||
root: &Path,
|
||||
record_type: &str,
|
||||
@@ -3937,6 +4191,8 @@ struct AgentDbReservedTailCapacity {
|
||||
action_tail_bytes: u64,
|
||||
lifecycle_unlinked_tail_records: usize,
|
||||
lifecycle_unlinked_tail_bytes: u64,
|
||||
plan_decision_tail_records: usize,
|
||||
plan_decision_tail_bytes: u64,
|
||||
finalizations: BTreeMap<String, AgentDbFinalizationCapacityReservation>,
|
||||
}
|
||||
|
||||
@@ -3959,6 +4215,13 @@ impl AgentDbReservedTailCapacity {
|
||||
AgentDbRecordAppendClass::LifecycleTerminal => {
|
||||
self.observe_unlinked_lifecycle(in_record_tail, tail_bytes);
|
||||
}
|
||||
AgentDbRecordAppendClass::PlanGddDecision => {
|
||||
self.plan_decision_tail_records = self
|
||||
.plan_decision_tail_records
|
||||
.saturating_add(usize::from(in_record_tail));
|
||||
self.plan_decision_tail_bytes =
|
||||
self.plan_decision_tail_bytes.saturating_add(tail_bytes);
|
||||
}
|
||||
AgentDbRecordAppendClass::FinalizationCritical => {
|
||||
if !self.observe_finalization_record(record, in_record_tail, tail_bytes) {
|
||||
self.observe_unlinked_lifecycle(in_record_tail, tail_bytes);
|
||||
@@ -4285,6 +4548,24 @@ fn ensure_agent_db_classified_capacity_unlocked(
|
||||
));
|
||||
}
|
||||
}
|
||||
AgentDbRecordAppendClass::PlanGddDecision => {
|
||||
if capacity.plan_decision_tail_records
|
||||
> AGENT_DB_PLAN_GDD_DECISION_RESERVE_RECORDS as usize
|
||||
{
|
||||
return Err(format!(
|
||||
"Agent 本地索引已达到 {} 条 planning decision 尾部配额,无法继续追加:{}",
|
||||
AGENT_DB_PLAN_GDD_DECISION_RESERVE_RECORDS,
|
||||
path.display()
|
||||
));
|
||||
}
|
||||
if capacity.plan_decision_tail_bytes > AGENT_DB_PLAN_GDD_DECISION_RESERVE_BYTES {
|
||||
return Err(format!(
|
||||
"Agent 本地索引将超过 {} 字节 planning decision 尾部配额:{}",
|
||||
AGENT_DB_PLAN_GDD_DECISION_RESERVE_BYTES,
|
||||
path.display()
|
||||
));
|
||||
}
|
||||
}
|
||||
AgentDbRecordAppendClass::Ordinary => unreachable!(),
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# 决策记录
|
||||
|
||||
## 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`。
|
||||
- **恢复边界**:generic `plan.submit_gdd` 的 v5 standalone pending 与 v4 batch 仍是独立恢复锚点。receipt 投影只在 exact planning-submit batch(v4、单 action、cursor 已到 1、completed、observation 与 receipt 逐字相等)时清理残留;pending 已缺失但 terminal observation 存在时仍校验/清理该 batch,形状不 exact 则保持 `recoveryPending`,不猜测删除。无 receipt 的 GDD 不由本包自行重建审批 pending。
|
||||
- **明确后置**:技术方案第 13.0 节要求的 acceptance-gate 取证成功后才创建生产 `gdd-approval` pending;当前创建 helper 仅由定向测试调用,真实 submit/recovery caller 留给 `M1C-2a` 的验收前置门接线。审批 UI、验收图接线和构建准入仍未完成,不能把当前隔离 WIP 宣称为完整产品交付。
|
||||
- **验证**:专用 target `target-m1c1-current` 下 `cargo test --all-targets planning_submit --no-fail-fast`(37 passed,含 completion blocker 正向/阻塞/作用域/identity 回归)、`cargo test --all-targets planning_storage --no-fail-fast`(11 passed)、`cargo check --all-targets` 通过;`npm run check:encoding`(7848 files)和 `git diff --check` 通过。编译仍有仓库既有 warnings,不作为本包缺陷。
|
||||
- **关联**:`docs/technical/【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md` 第 13、14、23.6、23.8 节;本条只记录隔离工作树状态,合回原分支前仍需按既定合并流程复核。
|
||||
|
||||
## 2026-08-15 CI 五条失败归并为三个根因:修位置,不修症状
|
||||
|
||||
栈溢出修复后的全量跑出 5 条失败(`2051 passed / 5 failed`,**零栈溢出**,栈修复站住)。逐条定位后归并为 3 个根因,**全部先于本轮工作**:与栈修复、`M1C-0b`、以及刚合入的 master 都无关,master(`9f5c84ee7`)自身全绿,`deae1e08c`(栈修复前、`M1C-0b` 前)已全挂。三者形状相同——**新增的检查被放到了链路更靠前的位置,改变的是作用域而非严格程度**,详见 pitfalls 同日条。
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# 立项策划 Agent(Fast GDD)技术方案
|
||||
|
||||
- 日期:2026-08-10
|
||||
- 状态:2026-08-12 **M0 代码工作包全部完成**(`M0A-2`、`M0B-1`、`M0B-2` 已合入 M0 集成分支并通过各自门禁,见第 23.4 节);同日 **D6 作废、拓扑改变**,`M0A-1` 交付的文档基线随之失效,需以工作包 `M0A-3` 修订,**修订完成前 M0 不计完整完成**(见第 1.1 节)。2026-08-13 **D9 二次作废、D10 作废,由 D11 取代**:立项策划节点改为 Project Supervisor 通过 `agent.delegate` 发起的静态委派子 Agent,问询复用 PR #165 中转链路(见第 1.1 节「D11 新拓扑」);D11 依赖 WP1(静态委派澄清轮次与返工深度拆分)为强制前置,**该前置已于 2026-08-13 落地并合入**(`WP1` 生产代码 + `WP2` 回归,完成状态与门禁见第 23.5 节),澄清轮次上限现为 3(game-chat source 仍为 1)。随后 `M1A-1`、`M1A-2`、`M1A-3`、`M1A-4` 已分别落地:`M1A-2` 仅收口两层工具面、`project-planning` role brief 注入和 fail-closed 拒绝边界,`M1A-4` 收窄 plan 根 run 的子 Agent 创建面。**2026-08-14 `M1B-1` 已通过门禁并合入本分支**:已落地 `.agent/planning` storage module、strict schema/typed 指纹/canonical parser、GDD 版本链、session 原子恢复、Runtime 写入身份及只挡写门禁;golden vector 与 11 个定向 storage 测试通过,writer/index/recovery 门禁已完成。**2026-08-15 `M1B-2` 工作包已通过本包门禁并以 `27c3eb847` 合入本分支**:已落地 `plan.submit_gdd`、exact planning Provider binding/structured injection、专用提交点与崩溃恢复;本包不包含 `gdd-approval` planning pending、审批等待、receipt、审批命令或 UI。**2026-08-14 `M1C-0` 已合回本分支**:仅新增用户修订状态及 lineage 分类,不包含审批写入方。**2026-08-15 `M1C-0b` 已通过定向门禁并完成**:只改静态委派 durable status 的前向兼容读路径,未知字符串显式保留为 `Unknown(raw)` 并最大化阻塞;不含审批写入方。当前仍未实现完整审批闭环、UI、构建准入,见第 23.6、23.8 节。后续执行计划见第 23.6 节。
|
||||
- 状态:2026-08-12 **M0 代码工作包全部完成**(`M0A-2`、`M0B-1`、`M0B-2` 已合入 M0 集成分支并通过各自门禁,见第 23.4 节);同日 **D6 作废、拓扑改变**,`M0A-1` 交付的文档基线随之失效,需以工作包 `M0A-3` 修订,**修订完成前 M0 不计完整完成**(见第 1.1 节)。2026-08-13 **D9 二次作废、D10 作废,由 D11 取代**:立项策划节点改为 Project Supervisor 通过 `agent.delegate` 发起的静态委派子 Agent,问询复用 PR #165 中转链路(见第 1.1 节「D11 新拓扑」);D11 依赖 WP1(静态委派澄清轮次与返工深度拆分)为强制前置,**该前置已于 2026-08-13 落地并合入**(`WP1` 生产代码 + `WP2` 回归,完成状态与门禁见第 23.5 节),澄清轮次上限现为 3(game-chat source 仍为 1)。随后 `M1A-1`、`M1A-2`、`M1A-3`、`M1A-4` 已分别落地:`M1A-2` 仅收口两层工具面、`project-planning` role brief 注入和 fail-closed 拒绝边界,`M1A-4` 收窄 plan 根 run 的子 Agent 创建面。**2026-08-14 `M1B-1` 已通过门禁并合入本分支**:已落地 `.agent/planning` storage module、strict schema/typed 指纹/canonical parser、GDD 版本链、session 原子恢复、Runtime 写入身份及只挡写门禁;golden vector 与 11 个定向 storage 测试通过,writer/index/recovery 门禁已完成。**2026-08-15 `M1B-2` 工作包已通过本包门禁并以 `27c3eb847` 合入本分支**:已落地 `plan.submit_gdd`、exact planning Provider binding/structured injection、专用提交点与崩溃恢复;本包不包含 `gdd-approval` planning pending、审批等待、receipt、审批命令或 UI。**2026-08-14 `M1C-0` 已合回本分支**:仅新增用户修订状态及 lineage 分类,不包含审批写入方。**2026-08-15 `M1C-0b` 已通过定向门禁并完成**:只改静态委派 durable status 的前向兼容读路径,未知字符串显式保留为 `Unknown(raw)` 并最大化阻塞;不含审批写入方。**当前隔离 worktree 已完成 M1C-1 的审批核心、receipt 投影/恢复和 plan 根专用 completion blocker;生产 acceptance-gate pending caller、验收图接线、审批 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` 工作包已提供提交点与恢复,`M1C-0b` 已补齐静态委派未知 durable status 的前向兼容读路径(包括既有 Provider/终态消费点的 fail-closed guard),但不构成完整可交付:审批 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 的前向兼容读路径,当前隔离 worktree 另已补齐 M1C-1 receipt/审批核心、投影恢复和 plan 根完成门;生产 acceptance-gate pending caller、验收图接线、审批 UI、构建绑定和正式入口仍不可用
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
@@ -1754,7 +1754,7 @@ M0 文档 PR 本身最低验证:Markdown 结构与三张 Mermaid 图可解析
|
||||
| submit 专用提交/恢复分支 | `apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/pending_recovery.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/recovery_scan.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_action_batch.rs` | **2026-08-14 按 M1B-2 收口**:在普通 dispatch 前处理 Runtime-owned submit;提交后终止策划子 run,并保留 generic v5 standalone pending + v4 batch anchors,不复用 `WaitingForUserInput`,不创建 planning pending。审批等待属于 M1C-1 |
|
||||
| JSON sidecar | `apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/json_sidecar.rs:44-104,122-250` | 现有 writer 可覆盖;不可变文件必须新增 no-replace helper |
|
||||
| 项目锁 | `apps/ai-game-creator-shell/src-tauri/src/project/filesystem.rs:85-138`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/project_gates.rs:1467-1505` | 所有 planning mutation 在同一项目锁内重读事实 |
|
||||
| completion blocker | `apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/project_gates.rs:808-860,934-948`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop.rs:1792-1832` | **2026-08-13 收窄**:现役 collaboration blocker 对**策划子 Agent**返回不适用;Supervisor 根 run 侧不再整体豁免(见第 4.3 节)。仍需新增 plan GDD 专用完成门,检查提问/审批等待、receipt、observation、session 与 recovery 状态 |
|
||||
| completion blocker | `apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_approval.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/project_gates.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop.rs` | **M1C-1 隔离 worktree 已补齐**:仅对 exact `project-supervisor-plan` standard 顶层根 Run 生效,读取 GDD lineage、approval pending/receipt、generic submit anchors、terminal observation、decision audit、planning session 与 recovery,且只读不创建 pending;现役 collaboration blocker 对策划子 Agent 仍不适用 |
|
||||
| plan 根 run 子 Agent 创建面 | `apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/delegation.rs`(`observe_agent_runtime_agent_delegate` / `observe_agent_runtime_agent_spawn_isolated`)、`agent/runtime_protocol/run_configuration.rs`(`validate_project_supervisor_plan_root_binding_at`)、`agent/prompt.rs`(`game_creator_project_supervisor_tool_plan_prompt`) | **2026-08-14 `M1A-4` 已落地**:plan 根 run 只能委派 `project-planning`,`agent.spawn_isolated` 一律拒,两条通道共用 typed `kind=plan-root-child-target-unsupported`;plan source 下不拼 `supervisorIntro` 与 `$visualContract`。**已知残留(有意保留,见 decision-log 2026-08-14 `M1A-4` 条)**:`$base` 的 `$isolatedAgentTemplates` 段仍会向 plan 根 run 列出全部专业角色名——那是 `agent.spawn_isolated` 的模板目录,因执行层硬拒而成为死文本;因此**不得**写「plan 根 run 上下文不出现其它 Agent 名」这类验收句 |
|
||||
| plan retry | `apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/lifecycle_control.rs`(`resolve_game_creator_agent_runtime_retry_configuration_at`)、`runtime_driver.rs`(`supervisor_plan_root_identity_holds_at`) | **2026-08-13 `M1A-3` 已落地 source 保源**:`task.source == project-supervisor-plan` 时先走强判据,通过则保留该 source,失败 `kind=plan-root-retry-identity-unsupported`、不降级。gui/cli 仍走 `agent-background-task`。plan-session revision / `gddId` / 按 `gdd-approval` kind 禁 retry 仍属后续包(现役已拒 `waiting-*`) |
|
||||
| planning hydrate command | `apps/ai-game-creator-shell/src-tauri/src/commands.rs:856-875`、`apps/ai-game-creator-shell/src-tauri/src/main.rs:2178-2180`、`apps/ai-game-creator-shell/src/App.tsx:1550,1650,2957,3364,3702` | 新增单一 hydrate command/注册与前端生命周期调用;不把 runtime polling 当 GDD authority |
|
||||
@@ -1921,7 +1921,7 @@ M0 完成不表示完整策划闭环已经上线。`M1A-1`~`M1A-4`、`M1B-1`
|
||||
| 三 | `project-planning` 的 agentCatalog 登记 | **已完成**(机制冻结见第 3.1 节;**代码亦已落地**,2026-08-13:manifest、prompt bundle、runtime adapter、`prompt.rs` 角色合成分支及四处 needs_change 全部合入) |
|
||||
| 四 | `M0A-3` 批二:拓扑与工具面部分 | **已完成**(2026-08-13),拆解见下 |
|
||||
| 四之余 | schema 与 golden vector 收口 | **已完成**(2026-08-13),拆解见下 |
|
||||
| 五 | M1 本体:策划闭环功能实现 | **`M1A-1`、`M1A-2`、`M1A-3`、`M1A-4`、`M1B-1`、`M1B-2`、`M1C-0`、`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 节 |
|
||||
| 五 | 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 节 |
|
||||
|
||||
批二在 2026-08-13 拆成两半,因为其中一半在 M1 代码存在之前**做不完**:
|
||||
|
||||
@@ -2009,7 +2009,7 @@ 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、审批命令、receipt;receipt 写入上述 status | `M1B-2`、`M1C-0`(前向兼容粒度另见 `M1C-0b`) | 三动作全通;版本+指纹竞态防护;两窗口并发;**连续多次修订均可通过且 `repair_depth` 不变**。**`M1C-0` 合入复核留下的三条已于 2026-08-15 裁决**(全文见第 23.7 节「落地约束」裁决一/二/三与 `decision-log.md` 同日条):① 新增独立 barrier 计数 `user_revision_pending_count`,不并进现有两个,且**不带** `repair_of.is_none()`——否则第 2 次及以后的修订不阻塞;另有四处逐字段读 barrier 的调用点需逐处裁决;② `validate_static_delegate_structured_result` 复用 `EvidenceReady` 的三条客观证据约束,并把该处 if/else-if 链改成穷尽 `match`,但**不得更严**——该函数每次读取都跑,过严会把写入方 bug 变成 delivery 永久读不出;③ 前向兼容粒度移交 `M1C-0b`,本包二选一:`M1C-0b` 先落,或直接上线并在 release note 明写「用过策划审批后回滚旧版本会让该项目静态委派面不可用」 |
|
||||
| `M1C-1` | `gdd-approval` pending、审批命令、receipt;receipt 写入上述 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-2b` | 澄清中转接线、轮次派生、预算注入 | `M1C-2a` | 3 轮上限;continuation 重放幂等不增加轮次;答案绑定冲突被拒 |
|
||||
| `M1D-1` | 前端 hydrate 与 GDD 审批卡 | `M1C-2b` | 前端只经 `hydrate_game_creator_plan_gdd_state` 读权威状态,不在页面侧合成批准事实 |
|
||||
|
||||
Reference in New Issue
Block a user