立项策划:落地 M1B-1 planning storage 与写入隔离

- 新增 plan GDD、index、session 与提交输入的 strict schema、canonical JSON 和 typed 指纹。

- 落地 GDD 连续版本链、index 权威对账与锁内 recovery、session CAS/原子替换/受限恢复。

- 封锁 planning sidecar 与 fast_gdd 投影的通用写入、patch、删除和 checkpoint restore,并校验专用 writer 身份。

- 同步 Fast GDD 技术方案、决策记录与排障记忆。
This commit is contained in:
2026-08-14 06:47:14 +00:00
parent 323db581fa
commit f453c2ca27
9 changed files with 4203 additions and 6 deletions
@@ -8,6 +8,7 @@ mod context_window;
mod finalization;
mod json_sidecar;
mod models;
mod planning_storage;
mod provider_control;
mod provider_retry;
mod real_e2e_checkpoint;
@@ -21,6 +22,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(in crate::agent) use planning_storage::*;
pub(in crate::agent) use provider_control::*;
pub(in crate::agent) use provider_retry::*;
pub(in crate::agent) use real_e2e_checkpoint::*;
@@ -329,6 +329,18 @@ pub(in crate::agent) fn agent_role_project_path_mutation_block(
tool: &str,
path: &str,
) -> Option<AgentRuntimeToolObservation> {
if is_agent_planning_storage_path(path) || is_plan_fast_gdd_projection_path(path) {
return Some(AgentRuntimeToolObservation {
tool: tool.to_string(),
status: "blocked".to_string(),
summary: if is_agent_planning_storage_path(path) {
"`.agent/planning/**` 只能由立项策划 Runtime 专用存储层写入".to_string()
} else {
"`game/fast_gdd.md` 只能由立项策划 Runtime renderer 写入".to_string()
},
detail: Some(format!("agentId={agent_id} · runId={run_id} · path={path}")),
});
}
match autonomous_owner_artifact_validation_available_for_run_at(root, agent_id, run_id) {
Ok(true) => {
let allowed = autonomous_manifest_owner_artifact_paths(agent_id);
@@ -7,7 +7,9 @@ use std::io::{Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
use unicode_normalization::UnicodeNormalization;
use crate::project::{normalize_relative_path, validate_project_root};
use crate::project::{
is_plan_fast_gdd_projection_path, normalize_relative_path, validate_project_root,
};
const PROJECT_PATCHSET_MAX_CHANGES: usize = 12;
const PROJECT_PATCHSET_MAX_TOTAL_BODY_BYTES: usize = 256 * 1024;
@@ -464,6 +466,11 @@ fn normalize_and_validate_patchset_inputs(
let path = normalize_relative_path(change.path())?;
reject_sensitive_patchset_path(&path)?;
if is_plan_fast_gdd_projection_path(&path) {
return Err(
"project.patchset 不得直接修改 Runtime-owned game/fast_gdd.md 投影".to_string(),
);
}
let change = match change {
ParsedProjectPatchsetChange::Create { content, .. } => {
validate_input_text(&content, &path)?;
@@ -1365,6 +1372,11 @@ fn metadata_is_link_or_reparse(metadata: &Metadata) -> bool {
}
fn reject_sensitive_patchset_path(relative_path: &str) -> Result<(), String> {
if is_plan_fast_gdd_projection_path(relative_path) {
return Err(
"project.patchset 不得直接修改 Runtime-owned game/fast_gdd.md 投影".to_string(),
);
}
let components = relative_path
.split('/')
.map(str::to_ascii_lowercase)
@@ -1694,6 +1706,8 @@ mod tests {
for path in [
".agent/runtime/state.json",
".agent/planning/gdd.v1.json",
"game/fast_gdd.md",
".env.local",
"config/private.pem",
"data/runtime.sqlite",
@@ -566,6 +566,7 @@ pub(crate) fn restore_local_project_checkpoint_at(
pub(crate) fn should_skip_project_restore_path(relative_path: &str) -> bool {
should_skip_project_snapshot_path(relative_path)
|| is_plan_fast_gdd_projection_path(relative_path)
|| relative_path == ".agent/agent.db"
|| relative_path == PROJECT_PERMISSION_POLICY_PATH
|| relative_path == PROJECT_WRITE_LOCK_PATH
@@ -281,6 +281,54 @@ pub(crate) fn reject_agent_runtime_private_control_path(
Ok(())
}
/// `.agent/planning/**` is a Runtime-owned sidecar. It remains readable by
/// the narrow planning read tools, but generic project mutation helpers must
/// never be able to create, replace, patch, or delete it. Keeping this gate
/// separate from `reject_agent_runtime_private_control_path` is deliberate:
/// the planning Agent needs `file.read`/`file.list` observations while its
/// durable writer is still the only component allowed to mutate the sidecar.
pub(crate) fn is_agent_planning_storage_path(normalized_path: &str) -> bool {
normalized_path.eq_ignore_ascii_case(".agent/planning")
|| normalized_path
.to_ascii_lowercase()
.starts_with(".agent/planning/")
}
pub(crate) fn is_agent_planning_managed_write_path(normalized_path: &str) -> bool {
is_agent_planning_storage_path(normalized_path) || normalized_path == "game/fast_gdd.md"
}
pub(crate) fn reject_agent_planning_storage_write_path(
normalized_path: &str,
) -> Result<(), String> {
if is_agent_planning_managed_write_path(normalized_path) {
return Err(
"`.agent/planning/**` 与 `game/fast_gdd.md` 只能由立项策划 Runtime 专用存储层写入,通用文件写入被拒绝"
.to_string(),
);
}
Ok(())
}
/// `game/fast_gdd.md` is the human-readable planning projection. It lives
/// outside `.agent/planning`, but it is still Runtime-owned and must not be
/// mutated by generic file tools. Keep this predicate write-only so planning
/// observations can continue to read the projection.
pub(crate) fn is_plan_fast_gdd_projection_path(normalized_path: &str) -> bool {
normalized_path.eq_ignore_ascii_case("game/fast_gdd.md")
}
pub(crate) fn reject_plan_projection_write_path(normalized_path: &str) -> Result<(), String> {
reject_agent_planning_storage_write_path(normalized_path)?;
if is_plan_fast_gdd_projection_path(normalized_path) {
return Err(
"`game/fast_gdd.md` 只能由立项策划 Runtime renderer 写入,通用文件写入被拒绝"
.to_string(),
);
}
Ok(())
}
fn reject_agent_control_path_delete(normalized_path: &str) -> Result<(), String> {
if matches!(
normalized_path.split('/').next(),
@@ -317,6 +365,7 @@ pub(crate) fn write_local_project_file_at(
) -> Result<LocalProjectFileMutationResult, String> {
let normalized_path = normalize_relative_path(relative_path)?;
reject_agent_runtime_private_control_path(&normalized_path)?;
reject_plan_projection_write_path(&normalized_path)?;
let path = resolve_local_project_path(root, &normalized_path)?;
if path.exists() && !path.is_file() {
return Err("只能写入文件".to_string());
@@ -341,6 +390,7 @@ pub(crate) fn delete_local_project_file_at(
) -> Result<LocalProjectFileMutationResult, String> {
let normalized_path = normalize_relative_path(relative_path)?;
reject_agent_runtime_private_control_path(&normalized_path)?;
reject_plan_projection_write_path(&normalized_path)?;
reject_agent_control_path_delete(&normalized_path)?;
let path = resolve_local_project_path(root, &normalized_path)?;
if !path.exists() {
@@ -30,6 +30,16 @@
2. **plan 之外的 source 多了一个失败面。** 两个 observe 函数新增的 task journal 读取,其 `Err` 分支位于弱判据之前,对 `project-supervisor-gui/cli/game-chat` 同样生效。正常状态不触发(仅 journal I/O 损坏等异常)。保留的依据是:读不到 task 就无法判断这条 run 是不是 plan 根,此时 fail closed 是正确取向;改成「读失败就放行」会引入一个新的 fail-open。**这是对第 19 节「做游戏链路逐字不变」的一处有意例外,仅限异常路径。**
3. **上下文层回归是弱断言。** `plan_root_supervisor_prompt_drops_intro_and_visual_contract_sections` 用「不含某几条独有短句」断言,不是对照组那种字节级 `assert_eq`。已实测非永真。已知漏报场景:若将来 `$visualContract` / `supervisorIntro` 被替换成措辞不同但仍暗示专业组扇出的新文本,这条不会报警。**执行层的 `A1`/`A2` 是该场景的唯一保障**——这也是本包把硬门排在裁段之前的原因。
- 关联文档:`docs/technical/【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md` 第 4.3、22、23.8、24 节。
## 2026-08-14 M1B-1:planning storage 基础与只挡写隔离完成(待合入)
- 范围:在 `M1A-2` 的 planning 子 Agent 工具边界之上,先落地 Runtime-owned `.agent/planning/**` 的 typed storage 基础,不注册、不广告、不执行 `plan.submit_gdd`(该工具仍属于 `M1B-2`)。当前实现位于隔离 worktree 的 `apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_storage.rs`,由 `runtime_protocol.rs` 注册。
- strict 合同:实现 `plan-gdd.v1`、`plan-gdd-index.v1`、`plan-session.v1` 与 `plan.submit_gdd` input 的 `deny_unknown_fields` 结构校验,并复用统一的文本、ID、时间、枚举、数量/字节上限和 `basis=null` 约束。canonical storage bytes 固定为 Rust struct 声明顺序的 compact UTF-8 JSON;BOM、尾换行/空白、重复键、字段重排和语义等价但非 canonical 的 bytes 均拒绝。typed 指纹固定使用 `sha256-serde-json-v2:<64 位小写 hex>` 与 domain separation,GDD fingerprint 排除外层自身字段。
- durable 边界:GDD/index 等不可变事实使用项目锁 + 同目录临时文件 + `sync_all` + 回读 + OS no-replace 发布;相同 canonical bytes 只返回 replay,其它同路径内容返回 identity conflict。session 使用原子 replace 与单份 `.session.json.previous`,按 `revision + 1` / `previousFingerprint` 链校验;primary 损坏不得静默被 previous 覆盖,只有 primary 缺失且 previous 唯一有效时才允许持锁提升。
- 写入隔离:`.agent/planning/**` 保持 `file.read` / `file.list` 可读,但通用 `file.write`、`file.patch`、`file.delete`、`project.patchset` 和 checkpoint restore 只挡写;`game/fast_gdd.md` 作为 Runtime renderer 的人读投影同样禁止通用写入。专用 writer 另做 `project-planning + agent-delegate + standard + project-supervisor` identity 校验,失败关闭。
- 当前验证:第 9.1 节 golden vector 已逐字节复核(3857 bytes,指纹 `sha256-serde-json-v2:a59856de7ef134cf2f49c4dedd2ba10ae4ab2340a9634d402eb792b6ee5458f0`),planning storage 定向测试 11/11 通过,writer 已按目标 schema 重解析并核对文件名/版本,index 与权威 GDD 逐项对账,新增 index recovery API 在锁内从严格 GDD 链重建缺失、损坏或陈旧 index,GDD 文件枚举/连续链读取、session request/decision/phase 约束及 recovery 分叉矩阵均有回归覆盖;`cargo check --offline`、`npm run check:encoding` 与 `git diff --check` 均通过。当前实现仍在隔离 worktree,待提交并合回原分支。
- index 的 `statusCache` 在 M1B-1 仍是无 approval receipt 的预审批投影:多版本只把最新版本标为 `ready_for_approval`,旧版本标为 `superseded`。M1B-1 尚无 approval receipt schema;M1C-1 接入 receipt 后必须重建真实的 `approved` / `revise` / `reject` / `superseded` 状态。`clarification_round` 与完整 root/session identity 绑定留给后续 `M1B-2` / `M1C-2b` 接线。
- 边界:本条不代表 GDD 提交点、approval pending/receipt、审批 UI、`game/fast_gdd.md` renderer 或构建 `approvedGddRef` 已可用;这些仍按 `M1B-2` 及后续 `M1C~M1E` 交付。上述边界不影响 M1B-1 存储层本身已完成。
- 关联:`docs/technical/【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md` 第 8.3~10.2、23.6、23.8 节;`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_storage.rs`;`apps/ai-game-creator-shell/src-tauri/src/project/filesystem.rs`。
## 2026-08-13 M1A-2:planning 子 Agent 两层工具面与角色 brief 注入
@@ -37,7 +47,7 @@
- planning 子 Agent 的当前 native action exact allowlist 只有 `file.read`、`file.list`;`update_agent_plan` / `respond_to_user` 是协议控制函数,不计入 action capability。MCP catalog 强制为空,`webSearchEnabled=false`,`collaborationPolicy=null`。`plan.submit_gdd` 刻意未注册、未广告、未执行,留给后续 `M1B-2`,因此本条不代表 GDD 提交、版本存储或审批闭环已完成。
- Prompt:Prompt Bundle 新增并登记 `project-planning` role brief,standard planning child 的初始请求与 repair/rebuild 请求均注入同一 brief;Supervisor 和其它 Agent 不注入该 section。brief 只描述 Fast GDD 澄清、终态 `AGC_NEEDS_USER_INPUT_V1`、三轮边界、平台事实与低幻觉约束,不授予任何写入、命令、MCP、预览、生成或审批能力。
- 纵深拒绝:广告层不再向 planning child 暴露 `user.input_request`;Provider parser、action batch/pending、并行只读、执行层和状态恢复均按原始 tool identity 再校验。伪造写入/命令/MCP、`project.search` 等映射为 `file.read` 的 alias、`user.input_request` 都 fail-closed;恢复旧快照不得把 planning 工具面扩回全量目录。委派子 Agent 原有 `validate_user_input_action_owner` 执行层拒绝继续保留。
- 回归与边界:覆盖 planning 函数目录精确集合、brief 只注入 planning、MCP/web search/collaboration 收窄、原始工具身份拒绝及状态归一化不扩权;Supervisor 根 run 的 standard 工具面保持既有行为。M1A-3 的 source 保留、强判据与 retry 语义不改;`M1B-1`/`M1B-2` 的 planning 存储、strict schema、typed 指纹和 `plan.submit_gdd` 提交仍未实现。
- 回归与边界:覆盖 planning 函数目录精确集合、brief 只注入 planning、MCP/web search/collaboration 收窄、原始工具身份拒绝及状态归一化不扩权;Supervisor 根 run 的 standard 工具面保持既有行为。M1A-3 的 source 保留、强判据与 retry 语义不改;`M1B-1` 的 planning 存储、strict schema、typed 指纹和写入隔离已完成,`plan.submit_gdd` 提交仍留给 `M1B-2`。
- 关联文档:`docs/technical/【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md` 第 4.3、6、19.2、23.6、23.8 节。
## 2026-08-13 M1A-3:plan 根 run 强判据与 retry 保源
@@ -1,5 +1,13 @@
# 踩坑与排障记录
## 2026-08-14 planning sidecar 必须区分“可读”与“可写”,canonical bytes 也不等于 typed 指纹
- 现象:如果为了保护 Runtime-owned 事实,直接把 `.agent/planning/**` 加进现有 private-control **读**门,planning 子 Agent 的 `file.read` / `file.list` 会一起失败;反过来若只依赖工具面约束,通用 `file.write`、`file.patch`、`file.delete`、`project.patchset` 或 checkpoint restore 仍可能覆盖 GDD/session。另一个常见误判是把“能反序列化且语义相同”的 JSON 当成已提交文件,导致尾换行、字段重排或重复键绕过不可变事实的字节身份。
- 原因:`.agent/planning/**` 是 Runtime 专用 durable sidecar,但 planning Agent 需要只读观察;`game/fast_gdd.md` 又是 Runtime renderer 的人读投影,二者都不能复用“读写一体”的旧 private path 判据。存储 bytes 与 typed fingerprint 是两个门:前者约束磁盘 canonical serialization(Rust struct 字段顺序、compact UTF-8、无 BOM/尾空白、无重复键),后者约束 domain-separated 业务 payload 的完整性;只过其中一门都不能视为 authoritative。
- 处理:保持现有 private-control **读**门不含 planning,新增只挡写 predicate;所有通用 mutation 与 restore 路径在推进 revision 前先拒绝 `.agent/planning/**` / `game/fast_gdd.md`,专用 writer 再校验 `project-planning + agent-delegate + standard + project-supervisor` 身份。GDD/index 等不可变文件走项目锁、同目录临时文件、`sync_all`、回读与 no-replace 发布;相同 canonical bytes 才是 replay,任何其它内容都是 identity conflict。session 只保留一个 `.session.json.previous`,primary 损坏时 fail closed,不得拿 previous 猜测新旧。
- 验证:先用 planning Agent 的只读 action 验证 sidecar 可列出/读取,再逐项证明 `file.write`、`file.patch`、`file.delete`、`project.patchset` 与 checkpoint restore 均拒绝;storage 测试应覆盖 duplicate key、BOM/尾空白、字段顺序、symlink/目录/硬链接、create-only replay/conflict、session 缺 primary 提升、primary 损坏和合法 successor。第 9.1 节 golden vector 当前为 3857 bytes / `sha256-serde-json-v2:a59856de7ef134cf2f49c4dedd2ba10ae4ab2340a9634d402eb792b6ee5458f0`。
- 关联:`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_storage.rs`、`apps/ai-game-creator-shell/src-tauri/src/project/filesystem.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/file_ops.rs`、`apps/ai-game-creator-shell/src-tauri/src/patchset.rs`、`docs/technical/【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md` 第 8.3~10.2 节。
## 2026-08-12 在 autonomous-game-build 下试图向用户提问,会让整条工作流永久瘫痪
- 现象:给自主构建链路加「问用户一句」的需求时,最自然的两个想法——让 DAG 节点自己问、或让父 Supervisor 代问——**都不成立**,而且第二个的失败方式是灾难性的。
@@ -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-2` 仅收口两层工具面、`project-planning` role brief 注入和 fail-closed 拒绝边界;`plan.submit_gdd`、`.agent/planning` 存储、提交/审批闭环仍未实现,见第 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-2` 仅收口两层工具面、`project-planning` role brief 注入和 fail-closed 拒绝边界。**2026-08-14 M1B-1 已在隔离 worktree 完成并通过本包验收门禁,当前待提交、合回原分支**:已落地 `.agent/planning` storage module、strict schema/typed 指纹/canonical parser、GDD 版本链、session 原子恢复、Runtime 写入身份及只挡写门禁;golden vector 与 11 个定向 storage 测试通过,writer/index/recovery 及全仓库门禁已完成。`plan.submit_gdd`、审批闭环和 UI 仍未实现,见第 23.6、23.8 节。后续执行计划见第 23.6 节。
- 适用范围:AI 游戏创作独立 App、Project Supervisor、Agent Runtime、本地项目策划 sidecar 与后续完整构建准入
- 当前实现边界:本文件是后续详细设计与实现的仓库内阶段基线;M0 工作包冻结 Fast GDD 合同并修复现有 owner 验证、game-chat retry 与前端投影边界,`M1A-1`~`M1A-3` 已提供 plan source、两层工具面和角色 brief 的 Runtime 基础,但不代表立项策划入口、GDD/`.agent/planning` 持久化、`plan.submit_gdd` 提交、审批 UI 或构建绑定已经可用
- 当前实现边界:本文件是后续详细设计与实现的仓库内阶段基线;M0 工作包冻结 Fast GDD 合同并修复现有 owner 验证、game-chat retry 与前端投影边界,`M1A-1`~`M1A-3` 已提供 plan source、两层工具面和角色 brief 的 Runtime 基础,`M1B-1` 当前 worktree 已提供 storage 基础与写入隔离,但尚未宣称最终合入完成;立项策划入口、`plan.submit_gdd` 提交、审批 UI 或构建绑定仍不可用
## 1. 背景与目标
@@ -1883,7 +1883,7 @@ M0 完成不表示完整策划闭环已经上线。`M1A-1`、`M1A-2`、`M1A-3`
| 三 | `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` 已落地**;`M1B-1` 及之后仍未开始。M1A-2 只交付工具面、brief 注入和拒绝边界,不包含 GDD 提交/审批。合入门见第 23.8 节 |
| 五 | M1 本体:策划闭环功能实现 | **`M1A-1`、`M1A-2`、`M1A-3` 已落地**;`M1B-1` 已于 2026-08-14 在隔离 worktree 完成并通过本包验收门禁,当前待提交、合回原分支:storage 基础、strict schema、typed 指纹、版本链、session 原子恢复、只挡写门禁及 writer/index/recovery 验证均已完成,golden vector 与 11 个定向 storage 测试通过。`M1A-2` 只交付工具面、brief 注入和拒绝边界,不包含 GDD 提交/审批;`M1B-2` 及之后未开始。合入门见第 23.8 节 |
批二在 2026-08-13 拆成两半,因为其中一半在 M1 代码存在之前**做不完**:
@@ -1963,7 +1963,7 @@ M0 完成不表示完整策划闭环已经上线。`M1A-1`、`M1A-2`、`M1A-3`
| `M1A-2` | 两层工具面 + `project-planning` 角色 brief | `M1A-1` | **已落地**:Supervisor 根 run 保持 standard 工具面;planning 子 Agent 按 `agent-delegate`/`standard`/Supervisor parent 身份构建 exact native allowlist(`file.read`、`file.list` 与两个协议控制函数),MCP 为空、web search 关闭、collaboration policy 为 `null`;Prompt Bundle brief 只注入 `project-planning`,repair/rebuild 与初始请求一致;伪造写入、命令、MCP、`project.search` alias、`user.input_request` 等原始工具在广告/解析/执行/恢复路径均 fail-closed。**不包含 `plan.submit_gdd` 注册或 GDD 提交/审批。** |
| `M1A-3` | plan 根 run 强判据函数 + retry 保源(本节下方「`M1A-3` 的由来」,规格见第 4.1 节第 5、7 段) | `M1A-1` | **已落地**(source 保源与强判据)。`kind=plan-root-retry-identity-unsupported`;gui/cli 兜底不变。第 4.1 节第 7 段里依赖 plan-session / `gddId` / `gdd-approval` kind 的部分仍后置。必须早于 `M1C-2a` |
| `M1A-4` | plan 根 run 的子 Agent 创建面收窄:`agent.delegate` 目标必须是 `project-planning`、`agent.spawn_isolated` 一律拒;plan source 下不拼 `supervisorIntro` 与 `$visualContract` | `M1A-2` | 执行层与上下文层**都要做且不可互相替代**(同第 19 节第 2 条纪律);`agent.spawn_isolated` 是第二条造子 Agent 的通道,只堵 `agent.delegate` 视为未完成;强判据 `Err` 必须落进拒绝分支而非当作「不是 plan 根」;`project-supervisor-gui/cli` 的委派与 spawn 行为正常路径不变。**必须早于 `M1D-2` 与 `M1E`** |
| `M1B-1` | `.agent/planning/` 存储层、strict schema、typed 指纹、版本链;含 `.agent/planning/**` 只挡写判据 | `M1A-2` | **第 9.1 节 golden vector 逐字节相等且指纹相等**(先于其它测试);create-only 与等前缀不可变 |
| `M1B-1` | `.agent/planning/` 存储层、strict schema、typed 指纹、版本链;含 `.agent/planning/**` 只挡写判据 | `M1A-2` | **2026-08-14 已在隔离 worktree 完成并通过本包验收门禁,当前待提交、合回原分支**:GDD / index / session strict DTO、canonical bytes、duplicate-key 与后缀门、typed 指纹、连续版本链、session 原子写入/恢复、Runtime writer identity 及 `game/fast_gdd.md` / `.agent/planning/**` 通用写入拒绝;第 9.1 节 golden vector(3857 bytes)与 11 个定向 storage 测试通过,writer 目标 schema 重验、index 权威对账与锁内 index recovery(缺失/损坏/陈旧从严格 GDD 链重建)、恢复故障矩阵、`cargo check --offline`、`npm run check:encoding` 与 `git diff --check` 均通过。当前无 approval receipt schema,多版本 `statusCache` 只是无 receipt 的预审批投影(最新版本 `ready_for_approval`、旧版本 `superseded`);M1C-1 接入 receipt 后必须重建 `approved/revise/reject/superseded` 状态。`plan.submit_gdd`、审批闭环仍留给 `M1B-2` 及后续包;create-only 与等前缀不可变 |
| `M1B-2` | `plan.submit_gdd` 原生工具与提交点 | `M1B-1` | 全部拒绝分支;提交点前后强杀恢复;同 submissionId replay 不产生 vN+1 |
| `M1C-0` | 新增 `StaticDelegateContractStatus::UserRevisionRequested` 与分类分支 | `M1A-1` | **本 PR 无写入方、是惰性路径,行为零变化**;做游戏链路返工仍在 `depth=1` 被拒;无该变体的历史记录分类不变 |
| `M1C-1` | `gdd-approval` pending、审批命令、receipt;receipt 写入上述 status | `M1B-2`、`M1C-0` | 三动作全通;版本+指纹竞态防护;两窗口并发;**连续多次修订均可通过且 `repair_depth` 不变** |
@@ -1973,6 +1973,10 @@ M0 完成不表示完整策划闭环已经上线。`M1A-1`、`M1A-2`、`M1A-3`
| `M1D-2` | 入口分流与阶段进度 | `M1D-1` | 「直接开建」跳过路径与现状零差异 |
| `M1E` | 端到端与故障注入收口 | `M1D-2` | 第 21 节测试矩阵中跨层场景 |
**2026-08-14 `M1B-1` 实现验收快照(待提交、合回原分支)**:当前隔离 worktree 的实现集中在 `apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_storage.rs`,并由 `runtime_protocol.rs` 注册。已具备 `plan-gdd.v1`、`plan-gdd-index.v1`、`plan-session.v1` 及 `plan.submit_gdd` input 的 strict serde 形状校验、文本/ID/时间/枚举边界、typed serde fingerprint、canonical JSON(重复键、BOM、尾空白、字段顺序)解析、GDD 连续版本链、session `revision + 1` / `previousFingerprint` 链、create-only durable writer、session 原子替换与受限 recovery,以及 `project-planning / agent-delegate / standard / project-supervisor` writer identity。通用 `file.write`、`file.patch`、`file.delete`、`project.patchset` 与 checkpoint restore 对 `.agent/planning/**` 和 `game/fast_gdd.md` 只挡写,planning 的 `file.read` / `file.list` 仍可读;本包没有注册或执行 `plan.submit_gdd`,也没有实现 approval pending、receipt、UI 或构建准入。
当前已验证第 9.1 节 golden vector(canonical envelope 3857 bytes 与指纹 `sha256-serde-json-v2:a59856de7ef134cf2f49c4dedd2ba10ae4ab2340a9634d402eb792b6ee5458f0`)及 11 个定向 storage 测试;durable writer 的目标 schema 重解析与文件名/版本核对、index 与权威 GDD 对账、缺失/损坏/陈旧 index 的锁内链重建、`(requestId,responseId)` / decision/ref / active run/phase 等 session 约束、GDD 文件枚举/链读取及 primary 损坏/previous 提升/分叉 recovery 矩阵均已覆盖。M1B-1 尚无 approval receipt schema,因此多版本 `statusCache` 只表达无 receipt 的预审批投影;M1C-1 接入 receipt 后重建真实 approved/revise/reject/superseded 状态。`clarification_round` 与完整 root/session identity 绑定留给后续 `M1B-2` / `M1C-2b` 接线。`cargo check --offline`、`npm run check:encoding`、`git diff --check` 均通过;本包仅待提交并合回原分支,不把 `plan.submit_gdd`、审批闭环、UI 或 renderer 误报为已实现。
**拆包纪律**:
- `M1C-0` 与 `M1C-1` 拆开的理由是**风险类别不同**:前者改的是 master 已发布的静态委派机制,后者是 M1 新增功能。合并成一个 PR 会让「做游戏链路返工额度未被误放宽」这条最关键的回归淹没在审批闭环的 diff 里。拆开后 `M1C-0` 全程不产生半状态——它没有写入方,`UserRevisionRequested` 要到 `M1C-1` 才被写出。