修复游戏续跑与美术资产工作流

统一纯继续意图、原任务合同继承与同源边界
要求既有游戏由当前代码任务真实修改后再验证
为俄罗斯方块提供完整玩法与移动端触控 fallback
生成并原子持久化四类透明图集切片
强制游戏 Canvas 实际使用生成素材
同步 Runtime prompt、Skill、前端进度与项目文档
补齐续跑、素材门禁、fallback 与提交回滚回归测试
This commit is contained in:
2026-08-03 21:42:37 +08:00
parent e8aad0b739
commit ef128dd8da
24 changed files with 2258 additions and 210 deletions
@@ -80,6 +80,8 @@ Keep the existing autonomous-build task graph. Do not add a parallel task system
2. `design-foundation` generates `assets/ui-prototype.png` with `kind: "ui-design"`, using the registered art-spec resource ID in `referenceImageSrcs`.
3. `art-asset-plan` generates transparent `assets/art-spritesheet.png` through icon spritesheet generation, using the same registered art-spec resource ID as `referenceImageSrc` plus concrete `iconDescriptions`.
For a playable Canvas game, do not stop at generation. Make `code-prototype` depend on `art-asset-plan` and consume the persisted `iconImageSrcs` slices for core players, blocks or targets, scene obstacles, and feedback. For the four-category game-chat contract, require exactly four slices before registering the local runtime sheet; both fewer and extra components fail closed. Treat `art-spec.png` as reference-only. A full-sheet `<img>`, CSS background, path-only mention, guessed equal-grid crop, or code-drawn replacement for core entities is not runtime asset use. If slicing produces `sliceWarning`, keep the complete transparent sheet as a valid editor artifact, but fail the playable game asset gate until real slice files or verified atlas coordinates exist; never invent coordinates or replace the icon-spritesheet route with ordinary image generation.
Never use `assets/ui-prototype.png` as the spritesheet visual-spec reference. UI extraction is outside this canonical DAG.
## Scope Boundary
@@ -1,11 +1,14 @@
{
"id": "genarrative.agent-runtime",
"version": "2026-08-03.3",
"version": "2026-08-03.4",
"sections": {
"common": "common.md",
"isolatedAgentContract": "isolated-agent-contract.md",
"platformDefault": "platform/default.md",
"platformLinux": "platform/linux.md",
"roles": {
"codePrototypeGameChat": "roles/code-prototype-game-chat.md"
},
"supervisor": {
"intro": "supervisor/intro.md",
"visualContractWithoutEditor": "supervisor/visual-contract-without-editor.md",
@@ -0,0 +1,3 @@
game-chat 使用素材完整快车道。必须先保护既有游戏语义和已完成产物:如果 observation 尚未包含 game/index.html 的当前正文与摘要,本响应第一步必须调用 file.read(path=game/index.html),不得猜测入口仍是初始化占位。只有文件缺失或正文与初始化页面一致、且当前 run 尚未写入项目时,才允许用一次 file.write 生成完整、自包含、可运行的 game/index.html;检测到非占位入口、已有可玩实现、当前 revision 已验证,或当前 run 已发生修改时,禁止整文件 file.write,必须保留原玩法,优先执行静态检查/试玩,确需修复时只能依据已读取的精确原文做最小 file.patch。不得把“继续”、continue 或其它纯续跑词当作游戏主题;缺少可继承的具体目标时必须失败关闭,不得另造新玩法。
HTML 必须满足固定试玩合同,包含真实 Canvas 游戏循环、键盘与触控输入、开始、主要操作、重开、胜负状态和移动端布局。assets/art-spec.png 只能作为视觉规范与派生参考,不得在运行时加载、铺作背景或裁切实体。必须等待已登记且透明有效的 assets/art-spritesheet.png 与 assets/art-spritesheet-slices/manifest.json,从切片清单按 player、blocks-and-targets、obstacles-and-scene、feedback-effects 四种 usage 加载四个不同的独立透明素材,并在活动 Canvas 中通过 drawImage 绘制对应核心玩家、方块/目标、障碍/场景和反馈;不得猜测整张图集是等分网格,不得把整张图集作为 img、CSS background 或完整 drawImage 展示,也不得以纯代码几何替代核心实体。图集、四类切片或其可见使用任一缺失时不得交付。首次实现或最小修复后不要继续扩写功能;Runtime 会自动执行静态自检并在通过后立即试玩。
@@ -1,3 +1,3 @@
视觉产物始终按 owner 隔离:art-director 只声明 assets/art-spec.pngdesign-foundation 只声明 assets/ui-prototype.pngart-asset-plan 只声明 assets/manifest.art.json 与 assets/art-spritesheet.png;不得把 UI 与图集合并交给 art-director。旧派生图需要原位替换时,先在同一批次分别交给 design-foundation 与 art-asset-plan 建立精确原合同并取得 needs-repair,认领后再在同一批次分别发起各自唯一、完全继承原合同的 repair,两个 repair 共同构成一个显式视觉返工阶段。只有没有匹配专业角色、纯协调工作或一两步轻量读取时才由总控直接处理。
视觉产物始终按 owner 隔离:art-director 只声明 assets/art-spec.pngdesign-foundation 只声明 assets/ui-prototype.pngart-asset-plan 只声明 assets/manifest.art.json 与 assets/art-spritesheet.png;不得把 UI 与图集合并交给 art-director。art-spec 只作规范/referenceart-asset-plan 必须以它的稳定资源 ID 走 icon-spritesheet 生成透明图集并持久化服务端 iconImageSrcscode-prototype 必须等待切片清单并使用玩家、目标、场景和反馈四类独立素材,不能猜测图集网格或跳过为纯代码核心画面。旧派生图需要原位替换时,先在同一批次分别交给 design-foundation 与 art-asset-plan 建立精确原合同并取得 needs-repair,认领后再在同一批次分别发起各自唯一、完全继承原合同的 repair,两个 repair 共同构成一个显式视觉返工阶段。只有没有匹配专业角色、纯协调工作或一两步轻量读取时才由总控直接处理。
art-director 是规范图产物型任务,expectedArtifacts 必须包含 assets/art-spec.pngdesign-foundation 是图片产物型任务,expectedArtifacts 必须包含 assets/ui-prototype.pngart-asset-plan 也是图片产物型任务,负责透明图集,expectedArtifacts 必须同时包含 assets/manifest.art.json 与 assets/art-spritesheet.png。三者都不能用空 expectedArtifacts 或纯文本回执代替图片。
art-director 是规范图产物型任务,expectedArtifacts 必须包含 assets/art-spec.pngdesign-foundation 是图片产物型任务,expectedArtifacts 必须包含 assets/ui-prototype.pngart-asset-plan 也是图片产物型任务,负责透明图集,expectedArtifacts 必须同时包含 assets/manifest.art.json 与 assets/art-spritesheet.png。三者都不能用空 expectedArtifacts 或纯文本回执代替图片。code-prototype 必须依赖 art-asset-plan,并在活动 Canvas 中按 assets/art-spritesheet-slices/manifest.json 使用玩家、目标、场景和反馈四类独立切片;纯代码核心画面、猜测图集网格、整图展示和只引用路径均不满足交付。
@@ -14,7 +14,8 @@ mod tests;
mod trace;
pub(in crate::agent) use canvas_generation::{
commit_prepared_platform_art_asset_at, platform_art_generation_error_needs_reconciliation,
commit_prepared_platform_art_asset_at, commit_prepared_platform_art_asset_strict_slices_at,
platform_art_generation_error_needs_reconciliation,
request_platform_art_asset_with_runtime_options_at,
};
pub(in crate::agent) use draft_validation::validate_closed_game_script_blocks;
File diff suppressed because it is too large Load Diff
@@ -1,6 +1,6 @@
use super::*;
const RUNTIME_PROMPT_BUNDLE_VERSION: &str = "2026-08-03.3";
const RUNTIME_PROMPT_BUNDLE_VERSION: &str = "2026-08-03.4";
const RUNTIME_PROMPT_BUNDLE_MANIFEST: &str = include_str!("../../prompts/runtime/manifest.json");
const RUNTIME_PROMPT_COMMON: &str = include_str!("../../prompts/runtime/common.md");
const RUNTIME_PROMPT_ISOLATED_AGENT_CONTRACT: &str =
@@ -10,6 +10,9 @@ const RUNTIME_PROMPT_PLATFORM_DEFAULT: &str =
include_str!("../../prompts/runtime/platform/default.md");
#[cfg(any(test, target_os = "linux"))]
const RUNTIME_PROMPT_PLATFORM_LINUX: &str = include_str!("../../prompts/runtime/platform/linux.md");
#[cfg(test)]
const RUNTIME_PROMPT_CODE_PROTOTYPE_GAME_CHAT: &str =
include_str!("../../prompts/runtime/roles/code-prototype-game-chat.md");
#[cfg(not(target_os = "linux"))]
const RUNTIME_PROMPT_PLATFORM: &str = RUNTIME_PROMPT_PLATFORM_DEFAULT;
#[cfg(target_os = "linux")]
@@ -654,6 +657,9 @@ mod tests {
"isolatedAgentContract": "isolated-agent-contract.md",
"platformDefault": "platform/default.md",
"platformLinux": "platform/linux.md",
"roles": {
"codePrototypeGameChat": "roles/code-prototype-game-chat.md"
},
"supervisor": {
"intro": "supervisor/intro.md",
"visualContractWithoutEditor": "supervisor/visual-contract-without-editor.md",
@@ -669,6 +675,7 @@ mod tests {
RUNTIME_PROMPT_ISOLATED_AGENT_CONTRACT,
RUNTIME_PROMPT_PLATFORM_DEFAULT,
RUNTIME_PROMPT_PLATFORM_LINUX,
RUNTIME_PROMPT_CODE_PROTOTYPE_GAME_CHAT,
SUPERVISOR_INTRO,
SUPERVISOR_VISUAL_CONTRACT_WITHOUT_EDITOR,
SUPERVISOR_VISUAL_CONTRACT_WITH_EDITOR,
@@ -3,7 +3,8 @@ use crate::mcp::GAME_CREATOR_MCP_CALL_TOOL;
const AGENT_RUNTIME_COMPLETION_BLOCKER_TOOL_PLAN_PROTOCOL: &str = "通用完成阻断规则:如果最新 observation 的 tool 为 runtime.autonomous_completion 且 status 为 blocked,本轮禁止调用 respond_to_user;必须先读取该 observation.detail 的 nextRequiredAction,并据此调用合适的读取、修复和验证工具。只有完成要求的动作、取得后续可信 observation 且完成门禁不再阻断后,才能给最终回复;不得反复提交 final response,也不得按项目正文硬编码某一种 blocker 的处理方式。";
const GAME_CHAT_CODE_PROTOTYPE_FAST_PATH_PROMPT: &str = "game-chat 首版使用五分钟快车道。必须先保护既有游戏语义和已完成产物:如果已有 observation 尚未包含 game/index.html 的当前正文与摘要,本响应第一步必须调用 file.read(path=game/index.html),不得猜测入口仍是初始化占位。只有文件缺失或正文与初始化页面一致、且当前 run 尚未写入项目时,才允许用一次 file.write 生成完整、自包含、可运行的 game/index.html;检测到非占位入口、已有可玩实现、当前 revision 已验证,或当前 run 已发生修改时,禁止整文件 file.write,必须保留原玩法,优先执行静态检查/试玩,确需修复时只能依据已读取的精确原文做最小 file.patch。不得把“继续”、continue 或其它纯续跑词当作游戏主题;缺少可继承的具体目标时必须失败关闭,不得另造收集能量等新玩法。HTML 必须满足下方固定试玩合同,包含真实 Canvas 游戏循环、键盘与触控输入、开始、主要操作、重开、胜负状态和移动端布局。../assets/art-spec.png 是视觉规范图,只能用于理解风格、构图与色板,禁止把整张规范图铺成游戏背景,禁止从中裁切或绘制玩家、目标和场景实体,也不能把它当作运行时必需图片;只有存在已登记的独立派生 spritesheet 时才可加载该派生素材,否则使用与规范一致的自包含原创 Canvas 视觉,并保证规范图加载失败不影响游戏启动。首次实现或最小修复后不要继续扩写功能;Runtime 会自动执行静态自检并在通过后立即试玩。";
const GAME_CHAT_CODE_PROTOTYPE_FAST_PATH_PROMPT: &str =
include_str!("../../../prompts/runtime/roles/code-prototype-game-chat.md");
fn game_chat_fast_path_prompt_for_root_source(
agent_id: &str,
@@ -741,14 +742,14 @@ mod tests {
}
#[test]
fn game_chat_fast_path_prompt_protects_existing_game_and_treats_art_spec_as_reference() {
fn game_chat_fast_path_prompt_protects_existing_game_and_requires_cropped_spritesheet_use() {
let prompt = game_chat_fast_path_prompt_for_root_source(
"code-prototype",
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
)
.expect("game-chat code fast path prompt");
assert!(prompt.contains("五分钟快车道"));
assert!(prompt.contains("素材完整快车道"));
assert!(prompt.contains("第一步必须调用 file.read(path=game/index.html)"));
assert!(prompt.contains("正文与初始化页面一致"));
assert!(prompt.contains("当前 run 尚未写入项目"));
@@ -756,13 +757,14 @@ mod tests {
assert!(prompt.contains("禁止整文件 file.write"));
assert!(prompt.contains("最小 file.patch"));
assert!(prompt.contains("不得把“继续”"));
assert!(prompt.contains("../assets/art-spec.png"));
assert!(prompt.contains("只能用于理解风格、构图与色板"));
assert!(prompt.contains("禁止把整张规范图铺成游戏背景"));
assert!(prompt.contains("禁止从中裁切或绘制玩家、目标和场景实体"));
assert!(prompt.contains("自包含原创 Canvas 视觉"));
assert!(prompt.contains("规范图加载失败不影响游戏启动"));
assert!(prompt.contains("独立派生 spritesheet"));
assert!(prompt.contains("assets/art-spec.png"));
assert!(prompt.contains("不得在运行时加载"));
assert!(prompt.contains("assets/art-spritesheet.png"));
assert!(prompt.contains("assets/art-spritesheet-slices/manifest.json"));
assert!(prompt.contains("四个不同的独立透明素材"));
assert!(prompt.contains("不得猜测整张图集是等分网格"));
assert!(prompt.contains("不得以纯代码几何替代核心实体"));
assert!(prompt.contains("四类切片或其可见使用任一缺失时不得交付"));
assert!(game_chat_fast_path_prompt_for_root_source(
"quality-review",
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
File diff suppressed because it is too large Load Diff
@@ -1119,7 +1119,7 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
Some(&session_id),
LocalConversationMessage {
role: "assistant".to_string(),
content: "首版生成已达到五分钟硬上限,本轮已停止,不会继续在后台运行。"
content: "首版素材生成已达到七十五分钟硬上限,本轮已停止,不会继续在后台运行。"
.to_string(),
agent_id: None,
},
@@ -51,6 +51,70 @@ fn register_game_chat_art_spec_fixture(root: &Path) {
.expect("register game-chat art spec");
}
fn register_game_chat_art_spritesheet_fixture(root: &Path) {
image::RgbaImage::from_pixel(64, 64, image::Rgba([90, 140, 220, 0]))
.save(root.join("assets/art-spritesheet.png"))
.expect("write game-chat art spritesheet PNG");
register_local_asset_at(
root,
"assets/art-spritesheet.png",
"art-spritesheet",
"image/png",
"canvas",
GameCreationAppAssetSource {
kind: GameCreationAppAssetSourceKind::Canvas,
canvas_project_id: Some("game-chat-canvas".to_string()),
resource_id: Some("game-chat-art-spritesheet-resource".to_string()),
asset_object_id: Some("game-chat-art-spritesheet-object".to_string()),
task_id: Some("art-asset-plan".to_string()),
prompt: None,
model: None,
generation_route: Some(
"/api/external/v1/editor/icon-spritesheets/generations".to_string(),
),
generation_kind: Some("icon-spritesheet".to_string()),
reference_resource_ids: vec!["game-chat-art-spec-resource".to_string()],
},
)
.expect("register game-chat art spritesheet");
let directory = root.join("assets/art-spritesheet-slices");
fs::create_dir_all(&directory).expect("create game-chat slice fixture directory");
let usages = [
"player",
"blocks-and-targets",
"obstacles-and-scene",
"feedback-effects",
];
let slices = usages
.iter()
.enumerate()
.map(|(index, usage)| {
let path = format!("assets/art-spritesheet-slices/{usage}.png");
image::RgbaImage::from_pixel(32, 32, image::Rgba([90 + index as u8, 140, 220, 180]))
.save(root.join(&path))
.expect("write game-chat slice fixture");
serde_json::json!({
"name": format!("素材 {}", index + 1),
"path": path,
"width": 32,
"height": 32,
"usage": usage,
})
})
.collect::<Vec<_>>();
fs::write(
root.join("assets/art-spritesheet-slices/manifest.json"),
serde_json::to_vec_pretty(&serde_json::json!({
"schemaVersion": "game-art-slices.v1",
"source": "assets/art-spritesheet.png",
"sourceResourceId": "game-chat-art-spritesheet-resource",
"slices": slices,
}))
.expect("serialize game-chat slice fixture"),
)
.expect("write game-chat slice manifest");
}
fn queue_game_chat_fast_path_child(
root: &Path,
root_run_id: &str,
@@ -154,7 +218,7 @@ fn prepare_autonomous_completion_evidence(root: &Path, state: &AgentRuntimeState
("game/balance.json", r#"{"lives":3,"speed":1}"#),
(
"assets/manifest.art.json",
r#"{"assets":[],"status":"not-generated"}"#,
r#"{"assets":[{"path":"assets/art-spritesheet.png"}],"sliceManifest":"assets/art-spritesheet-slices/manifest.json","status":"generated"}"#,
),
("assets/manifest.audio.json", r#"{"bgm":[],"sfx":[]}"#),
("exports/README.md", "# 发布说明\n\n可试玩。\n"),
@@ -165,6 +229,7 @@ fn prepare_autonomous_completion_evidence(root: &Path, state: &AgentRuntimeState
revision
};
register_game_chat_art_spec_fixture(root);
register_game_chat_art_spritesheet_fixture(root);
for task in new_game_creation_app_seed_tasks() {
update_manifest_task_status_at(root, &task.id, GameCreationAppTaskStatus::Completed)
.expect("complete autonomous manifest task");
@@ -392,6 +457,66 @@ fn game_chat_art_director_uses_deterministic_canvas_plan_without_provider_planni
assert!(!root.join("assets/manifest.art.json").exists());
}
#[test]
fn game_chat_art_asset_plan_uses_deterministic_icon_spritesheet_generation() {
let _config_guard = crate::tests::write_test_local_config(
r#"{"editorApi":{"apiKey":"game-chat-fast-spritesheet-key"}}"#.to_string(),
);
let temporary = tempfile::tempdir().expect("create game-chat spritesheet root");
let root = temporary.path().join("project");
init_local_game_project_at(&root, "game-chat-art-assets", "水晶俄罗斯方块")
.expect("init game-chat spritesheet project");
register_game_chat_art_spec_fixture(&root);
let (root_state, child_state) = queue_game_chat_fast_path_child(
&root,
"game-chat-art-assets-root",
"制作水晶俄罗斯方块小游戏",
"art-asset-plan",
);
let binding = read_game_creator_agent_runtime_run_profile_binding(
&root,
&root_state.agent_id,
&root_state.run_id,
)
.expect("read game-chat root binding")
.expect("game-chat root binding exists");
let plan = game_chat_fast_path_plan_at(
&root,
&child_state,
&child_state.current_task,
binding.bound_at,
)
.expect("build deterministic art-asset-plan")
.expect("art-asset-plan must bypass Provider planning");
assert_eq!(plan.actions.len(), 1);
assert_eq!(plan.actions[0].tool, "canvas.asset_generate");
assert_eq!(
plan.actions[0].input["outputPath"],
"assets/art-spritesheet.png"
);
assert_eq!(plan.actions[0].input["assetKind"], "art-spritesheet");
assert_eq!(plan.actions[0].input["replaceExisting"], false);
assert!(!root.join("assets/manifest.art.json").exists());
fs::write(
root.join("assets/manifest.art.json"),
game_chat_fast_path_art_manifest_content(),
)
.expect("write generated art manifest fixture");
register_game_chat_art_spritesheet_fixture(&root);
let completion_plan = game_chat_fast_path_plan_at(
&root,
&child_state,
&child_state.current_task,
binding.bound_at,
)
.expect("build deterministic art completion plan")
.expect("art completion plan exists");
assert!(completion_plan.actions.is_empty());
assert!(completion_plan.response.contains("透明核心美术图集已生成"));
}
#[test]
fn game_chat_art_stage_fails_before_provider_when_editor_api_is_missing() {
let _config_guard = crate::tests::write_test_local_config("{}".to_string());
@@ -552,6 +677,103 @@ fn game_chat_code_prototype_ignores_art_director_global_revision_before_its_own_
);
}
#[test]
fn game_chat_existing_game_requires_code_mutation_before_smoke_and_then_converges() {
let temporary = tempfile::tempdir().expect("create existing game code root");
let root = temporary.path().join("project");
init_local_game_project_at(&root, "game-chat-existing-code", "水晶俄罗斯方块")
.expect("init existing game project");
let existing =
"<!doctype html><html><body><canvas></canvas><script>const game='俄罗斯方块';</script></body></html>";
fs::write(root.join(AGENT_RUNTIME_GAME_INDEX_PATH), existing)
.expect("write existing non-placeholder game");
let (root_state, mut code_state) = queue_game_chat_fast_path_child(
&root,
"game-chat-existing-code-root",
"继续完成水晶俄罗斯方块",
"code-prototype",
);
code_state.loop_iteration = 2;
let bound_at = read_game_creator_agent_runtime_run_profile_binding(
&root,
&root_state.agent_id,
&root_state.run_id,
)
.expect("read existing game root binding")
.expect("existing game root binding exists")
.bound_at;
let before_mutation = game_chat_fast_path_plan_at(
&root,
&code_state,
&code_state.current_task,
bound_at + GAME_CHAT_FIRST_PLAYABLE_SOFT_BUDGET_SECONDS,
)
.expect("evaluate existing game before code mutation");
assert!(
before_mutation.is_none(),
"existing game must return to the code Provider instead of repeating game.static_smoke"
);
{
let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
&root,
"test.game-chat.existing-code.mutate-and-verify",
)
.expect("acquire existing game mutation lock");
let revision = prepare_agent_runtime_project_mutation_locked(
&root,
&code_state.agent_id,
&code_state.run_id,
"file.patch",
)
.expect("advance existing game for code child");
assert_eq!(revision, 1);
write_local_project_file_at(
&root,
AGENT_RUNTIME_GAME_INDEX_PATH,
&format!("{existing}<!-- code-prototype continued -->"),
)
.expect("patch existing game");
let (revision, gate) = begin_agent_runtime_project_verification_locked(
&root,
&code_state.agent_id,
&code_state.run_id,
"game.static_smoke",
)
.expect("begin existing game smoke");
finish_agent_runtime_project_verification_locked(&root, &revision, gate, true)
.expect("finish existing game smoke");
}
let delivery = game_chat_fast_path_plan_at(
&root,
&code_state,
&code_state.current_task,
bound_at + GAME_CHAT_FIRST_PLAYABLE_SOFT_BUDGET_SECONDS,
)
.expect("evaluate existing game after code mutation")
.expect("verified code child uses deterministic delivery");
assert!(delivery.actions.is_empty());
assert!(delivery.response.contains("通过静态自检"));
let code_gate = read_game_creator_agent_runtime_verification_gate(
&root,
&code_state.agent_id,
&code_state.run_id,
)
.expect("read converged code gate");
assert_eq!(code_gate.mutation_revision, Some(1));
assert_eq!(code_gate.verified_revision, Some(1));
validate_agent_runtime_autonomous_specialist_response_delivery(
&code_state.agent_id,
&code_state.run_id,
false,
&code_gate,
&delivery,
)
.expect("specialist delivery accepts the code child's own verified mutation");
}
#[test]
fn autonomous_supervisor_empty_plan_uses_deterministic_final_reply_fallback() {
assert_eq!(
@@ -623,6 +845,7 @@ fn game_chat_single_round_converges_without_another_provider_plan_after_playtest
"design-director"
| "art-director"
| "code-director"
| "art-asset-plan"
| "code-prototype"
| "preview-readiness"
| "preview-playtest"
@@ -664,10 +664,15 @@ pub(in crate::agent) fn autonomous_manifest_seed_tasks_for_source(
"design-director" => Some(Vec::new()),
"art-director" => Some(Vec::new()),
"code-director" => Some(Vec::new()),
"art-asset-plan" => Some(vec![
"design-director".to_string(),
"art-director".to_string(),
]),
"code-prototype" => Some(vec![
"design-director".to_string(),
"art-director".to_string(),
"code-director".to_string(),
"art-asset-plan".to_string(),
]),
"preview-readiness" => Some(vec!["code-prototype".to_string()]),
"preview-playtest" => Some(vec!["preview-readiness".to_string()]),
@@ -1170,7 +1175,7 @@ pub(in crate::agent) fn project_autonomous_manifest_ready_task_terminal_at_locke
)?;
let game_chat_requires_visual_asset = root_parent_binding.source
== AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE
&& manifest_task.id == "art-director";
&& matches!(manifest_task.id.as_str(), "art-director" | "art-asset-plan");
if status == GameCreationAppTaskStatus::Completed
&& (autonomous_manifest_ready_task_requires_visual_asset(&manifest_task.id)
|| game_chat_requires_visual_asset)
@@ -1330,7 +1335,7 @@ pub(in crate::agent) fn render_autonomous_manifest_ready_task_background_prompt(
let code_visual_asset_requirement = if task.id == "code-prototype"
&& editor_api_key_is_configured()
{
" External Editor API 已配置时必须先调用 asset.list 核对 Canvas 登记与资源有效性GUI/CLI Run 使用独立派生的 assets/art-spritesheet.png,并在 game/index.html 中通过 HTML、CSS background 或 Canvas drawImage 真实可见使用;game-chat Run 的 assets/art-spec.png 只视觉规范参考,禁止把整张规范图铺成背景或从中裁切玩家、目标和场景实体,没有独立派生图集时应使用与规范一致的自包含原创 Canvas 视觉"
" External Editor API 已配置时必须先调用 asset.list 核对 Canvas 登记与资源有效性。所有 Run 都必须等待独立派生的 assets/art-spritesheet.pnggame-chat 还必须读取 assets/art-spritesheet-slices/manifest.json,并在活动 Canvas 中分别绘制玩家、方块/目标、障碍/场景和反馈四类独立透明切片。assets/art-spec.png 只视觉规范参考,禁止猜测图集等分坐标、把整张规范图或整张图集作为背景、直接图片或 CSS background 冒充运行时素材,也禁止以纯代码几何替代核心实体"
} else {
""
};
@@ -385,6 +385,7 @@ fn autonomous_code_prototype_art_asset_reference_gap_at(
root: &Path,
task_id: &str,
required_visual_task_id: Option<&str>,
require_core_slices: bool,
) -> Result<Option<String>, String> {
if task_id != "code-prototype" {
return Ok(None);
@@ -392,11 +393,6 @@ fn autonomous_code_prototype_art_asset_reference_gap_at(
let Some(required_visual_task_id) = required_visual_task_id else {
return Ok(None);
};
if required_visual_task_id == "art-director" {
// game-chat still requires art-director to deliver a valid visual specification,
// but the specification is a design reference rather than a runtime sprite/background.
return Ok(None);
}
let (asset_path, asset_label) = match required_visual_task_id {
"art-asset-plan" => ("assets/art-spritesheet.png", "art-spritesheet"),
_ => {
@@ -434,7 +430,20 @@ fn autonomous_code_prototype_art_asset_reference_gap_at(
"game/index.html(missing-visible-{asset_label}-use:{asset_path})"
)));
};
if !game_index_visibly_uses_visual_asset(&html, asset_path, asset_dimensions, false) {
if require_core_slices {
match game_index_missing_visible_art_slice(root, &html) {
Err(gap) => return Ok(Some(gap)),
Ok(Some(gap)) => return Ok(Some(gap)),
Ok(None) => {}
}
} else if !matches!(game_index_missing_visible_art_slice(root, &html), Ok(None))
&& !game_index_visibly_uses_visual_asset(
&html,
asset_path,
asset_dimensions,
VisualAssetUsageRequirement::AtlasCanvasCrop,
)
{
return Ok(Some(format!(
"game/index.html(missing-visible-{asset_label}-use:{asset_path})"
)));
@@ -442,6 +451,37 @@ fn autonomous_code_prototype_art_asset_reference_gap_at(
Ok(None)
}
fn game_index_missing_visible_art_slice(
root: &Path,
html: &[u8],
) -> Result<Option<String>, String> {
let slice_paths = game_chat_fast_path_art_slice_paths(root).map_err(|error| {
format!(
"assets/art-spritesheet-slices/manifest.json(invalid:{})",
sanitize_agent_runtime_text(&error, 240)
)
})?;
for slice_path in slice_paths {
let dimensions = resolve_local_project_path(root, &slice_path)
.ok()
.and_then(|path| fs::read(path).ok())
.and_then(|bytes| image::load_from_memory(&bytes).ok())
.map(|image| (image.width(), image.height()))
.ok_or_else(|| format!("无法读取已验证视觉切片尺寸:{slice_path}"))?;
if !game_index_visibly_uses_visual_asset(
html,
&slice_path,
dimensions,
VisualAssetUsageRequirement::CanvasDraw,
) {
return Ok(Some(format!(
"game/index.html(missing-visible-art-slice-use:{slice_path})"
)));
}
}
Ok(None)
}
fn strip_art_reference_comments(content: &str) -> String {
let bytes = content.as_bytes();
let mut output = Vec::with_capacity(bytes.len());
@@ -1099,11 +1139,18 @@ fn tag_visibly_uses_visual_asset(
.is_some_and(|style| css_contains_resolving_url(style, asset_path))
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum VisualAssetUsageRequirement {
AnyVisible,
CanvasDraw,
AtlasCanvasCrop,
}
fn game_index_visibly_uses_visual_asset(
html: &[u8],
asset_path: &str,
asset_dimensions: (u32, u32),
require_canvas_composition: bool,
requirement: VisualAssetUsageRequirement,
) -> bool {
let asset_path = asset_path.to_ascii_lowercase();
let Ok(html) = std::str::from_utf8(html) else {
@@ -1123,7 +1170,7 @@ fn game_index_visibly_uses_visual_asset(
};
let end = start + end_offset + 1;
let tag = &markup[start..end];
if !require_canvas_composition
if requirement == VisualAssetUsageRequirement::AnyVisible
&& tag_visibly_uses_visual_asset(tag, &asset_path, asset_dimensions)
&& !tag_is_hidden_by_stylesheet(tag, &markup, asset_dimensions)
{
@@ -1148,7 +1195,7 @@ fn game_index_visibly_uses_visual_asset(
let Some((selector, declarations)) = rule.rsplit_once('{') else {
continue;
};
if !require_canvas_composition
if requirement == VisualAssetUsageRequirement::AnyVisible
&& css_contains_resolving_url(declarations, &asset_path)
&& selector_is_bound_to_markup(selector, &markup)
&& !tag_is_obviously_hidden_or_tiny(
@@ -1176,8 +1223,6 @@ fn game_index_visibly_uses_visual_asset(
return false;
}
let mut significant_draws = 0usize;
let mut background_draws = 0usize;
let mut entity_draws = 0usize;
let function_ranges = named_javascript_function_ranges(&content);
let mut draw_cursor = 0;
while let Some(offset) = content[draw_cursor..].find("drawimage(") {
@@ -1197,21 +1242,44 @@ fn game_index_visibly_uses_visual_asset(
.first()
.is_some_and(|identifier| identifiers.contains(*identifier))
{
let (significant, background, entity) =
draw_image_metrics(&arguments, asset_dimensions);
let (significant, _, _) = draw_image_metrics(&arguments, asset_dimensions);
significant_draws += usize::from(significant);
background_draws += usize::from(background);
entity_draws += usize::from(entity);
if !require_canvas_composition && significant {
if requirement == VisualAssetUsageRequirement::AnyVisible && significant {
return true;
}
if requirement == VisualAssetUsageRequirement::CanvasDraw
&& matches!(arguments.len(), 5 | 9)
&& draw_image_has_visible_destination(&arguments)
{
return true;
}
if requirement == VisualAssetUsageRequirement::AtlasCanvasCrop
&& arguments.len() == 9
&& draw_image_has_visible_destination(&arguments)
{
return true;
}
}
draw_cursor = arguments_end + 1;
}
require_canvas_composition
&& significant_draws >= 3
&& background_draws >= 1
&& entity_draws >= 2
requirement == VisualAssetUsageRequirement::AnyVisible && significant_draws > 0
}
fn draw_image_has_visible_destination(arguments: &[&str]) -> bool {
let (width_index, height_index) = match arguments.len() {
5 => (3, 4),
9 => (7, 8),
_ => return false,
};
let parse = |value: &str| value.trim().parse::<f64>().ok();
match parse(arguments[width_index]).zip(parse(arguments[height_index])) {
Some((width, height)) => {
width.abs() >= 32.0 && height.abs() >= 32.0 && width.abs() * height.abs() >= 2048.0
}
None => {
!arguments[width_index].trim().is_empty() && !arguments[height_index].trim().is_empty()
}
}
}
pub(in crate::agent) fn game_creation_app_task_status_label(
@@ -1236,7 +1304,7 @@ fn autonomous_manifest_parent_completion_gaps_at(
.ok_or_else(|| "自主构建根 Supervisor Run 缺少 Run Profile 绑定".to_string())?;
let seed_tasks = crate::agent::autonomous_manifest_seed_tasks_for_source(&binding.source);
let required_visual_task_id = if binding.source == AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE {
Some("art-director")
Some("art-asset-plan")
} else if editor_api_key_is_configured() {
Some("art-asset-plan")
} else {
@@ -1260,7 +1328,13 @@ fn autonomous_manifest_parent_completion_gaps_at(
contract.baseline_index_sha256.as_deref(),
&contract.baseline_artifacts,
)?);
if required_visual_task_id == Some(seed_task.id.as_str()) {
let requires_visual_registration =
if binding.source == AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE {
matches!(seed_task.id.as_str(), "art-director" | "art-asset-plan")
} else {
required_visual_task_id == Some(seed_task.id.as_str())
};
if requires_visual_registration {
if let Err(error) =
validate_manifest_required_visual_asset(root, &manifest, &seed_task.id)
{
@@ -1276,6 +1350,7 @@ fn autonomous_manifest_parent_completion_gaps_at(
root,
&seed_task.id,
required_visual_task_id,
binding.source == AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
)? {
missing_paths.push(AutonomousManifestArtifactGap::new(gap));
}
@@ -1482,13 +1557,18 @@ fn autonomous_manifest_ready_task_completion_blocker_at_locked(
}
};
let required_visual_task_id = if root_source == AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE {
Some("art-director")
Some("art-asset-plan")
} else if editor_api_key_is_configured() {
Some("art-asset-plan")
} else {
None
};
if required_visual_task_id == Some(state.agent_id.as_str()) {
let requires_visual_registration = if root_source == AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE {
matches!(state.agent_id.as_str(), "art-director" | "art-asset-plan")
} else {
required_visual_task_id == Some(state.agent_id.as_str())
};
if requires_visual_registration {
if let Err(error) =
validate_manifest_required_visual_asset(root, &manifest, &state.agent_id)
{
@@ -1506,6 +1586,7 @@ fn autonomous_manifest_ready_task_completion_blocker_at_locked(
root,
&state.agent_id,
required_visual_task_id,
root_source == AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
) {
Ok(Some(gap)) => {
return Some(autonomous_completion_blocker(
@@ -1823,7 +1904,8 @@ pub(in crate::agent) fn is_pure_autonomous_continuation_intent(task: &str) -> bo
})
.chars()
.filter(|character| !character.is_whitespace())
.collect::<String>();
.collect::<String>()
.to_ascii_lowercase();
matches!(
normalized.as_str(),
"继续"
@@ -1848,6 +1930,9 @@ pub(in crate::agent) fn is_pure_autonomous_continuation_intent(task: &str) -> bo
| "继续之前的任务"
| "继续完成之前的任务"
| "接着完成之前的任务"
| "接着"
| "continue"
| "goon"
)
}
@@ -1899,6 +1984,9 @@ fn failed_terminal_autonomous_root_contract_before_task_at(
.into_iter()
.find(|record| record.run_id == *previous_run_id)
.ok_or_else(|| "自主构建续跑缺少上一根 Run 的最新 journal 投影".to_string())?;
if previous.source != task.source {
return Ok(None);
}
if !matches!(
game_creator_agent_runtime_terminal_status(&previous),
Some("failed" | "cancelled" | "budget-exhausted")
@@ -1913,13 +2001,19 @@ fn failed_terminal_autonomous_root_contract_before_task_at(
else {
return Ok(None);
};
let previous_task_sha256 = format!("{:x}", Sha256::digest(previous.task.as_bytes()));
let previous_task = autonomous_effective_root_task_at(
root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
&previous.run_id,
&previous.task,
)?;
let previous_task_sha256 = format!("{:x}", Sha256::digest(previous_task.as_bytes()));
if contract.task_sha256 != previous_task_sha256 {
return Err("自主构建续跑的上一根 Run 任务语义与完成合同不一致".to_string());
}
Ok(Some(AutonomousContinuationInheritance {
contract,
task: previous.task,
task: previous_task,
}))
}
@@ -2392,8 +2486,14 @@ pub(in crate::agent) fn autonomous_completion_contract_for_state_at(
}
let contract = read_autonomous_completion_contract(root, &state.agent_id, &state.run_id)?
.ok_or_else(|| "自主构建根 Supervisor Run 缺少完成合同".to_string())?;
let effective_task = autonomous_effective_root_task_at(
root,
&state.agent_id,
&state.run_id,
&state.current_task,
)?;
if contract.run_profile_binding_fingerprint != state.run_profile_binding_fingerprint
|| contract.task_sha256 != format!("{:x}", Sha256::digest(state.current_task.as_bytes()))
|| contract.task_sha256 != format!("{:x}", Sha256::digest(effective_task.as_bytes()))
{
return Err("自主构建完成合同与当前 Runtime 不匹配".to_string());
}
@@ -72,7 +72,7 @@ fn autonomous_supervisor_source_allowlist_includes_game_chat_only() {
}
#[test]
fn game_chat_manifest_seed_projection_starts_three_directors_then_runs_three_task_lane() {
fn game_chat_manifest_seed_projection_requires_art_assets_before_code() {
let game_chat_tasks =
autonomous_manifest_seed_tasks_for_source(AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE);
assert_eq!(
@@ -83,6 +83,7 @@ fn game_chat_manifest_seed_projection_starts_three_directors_then_runs_three_tas
[
"design-director",
"art-director",
"art-asset-plan",
"code-director",
"code-prototype",
"preview-readiness",
@@ -91,21 +92,26 @@ fn game_chat_manifest_seed_projection_starts_three_directors_then_runs_three_tas
);
assert_eq!(game_chat_tasks[0].dependencies, Vec::<String>::new());
assert_eq!(game_chat_tasks[1].dependencies, Vec::<String>::new());
assert_eq!(game_chat_tasks[2].dependencies, Vec::<String>::new());
assert_eq!(
game_chat_tasks[3].dependencies,
game_chat_tasks[2].dependencies,
vec!["design-director".to_string(), "art-director".to_string(),]
);
assert_eq!(game_chat_tasks[3].dependencies, Vec::<String>::new());
assert_eq!(
game_chat_tasks[4].dependencies,
vec![
"design-director".to_string(),
"art-director".to_string(),
"code-director".to_string(),
"art-asset-plan".to_string(),
]
);
assert_eq!(
game_chat_tasks[4].dependencies,
game_chat_tasks[5].dependencies,
vec!["code-prototype".to_string()]
);
assert_eq!(
game_chat_tasks[5].dependencies,
game_chat_tasks[6].dependencies,
vec!["preview-readiness".to_string()]
);
@@ -127,6 +133,7 @@ fn game_chat_manifest_seed_projection_starts_three_directors_then_runs_three_tas
"design-director"
| "art-director"
| "code-director"
| "art-asset-plan"
| "code-prototype"
| "preview-readiness"
| "preview-playtest"
@@ -153,6 +160,18 @@ fn game_chat_manifest_seed_projection_starts_three_directors_then_runs_three_tas
.unwrap_or_else(|| panic!("{task_id} task exists"))
.status = GameCreationAppTaskStatus::Completed;
}
assert_eq!(
autonomous_manifest_ready_task_ids(
&manifest_tasks,
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
),
vec!["art-asset-plan".to_string()]
);
manifest_tasks
.iter_mut()
.find(|task| task.id == "art-asset-plan")
.expect("art-asset-plan task exists")
.status = GameCreationAppTaskStatus::Completed;
assert_eq!(
autonomous_manifest_ready_task_ids(
&manifest_tasks,
@@ -162,6 +181,10 @@ fn game_chat_manifest_seed_projection_starts_three_directors_then_runs_three_tas
);
}
fn cropped_spritesheet_game_html() -> &'static str {
"<!doctype html><html><body><canvas width=320 height=180></canvas><script>const context=document.querySelector('canvas').getContext('2d');const playerArt=new Image();playerArt.src='../assets/art-spritesheet-slices/player.png';const targetArt=new Image();targetArt.src='../assets/art-spritesheet-slices/blocks-and-targets.png';const sceneArt=new Image();sceneArt.src='../assets/art-spritesheet-slices/obstacles-and-scene.png';const feedbackArt=new Image();feedbackArt.src='../assets/art-spritesheet-slices/feedback-effects.png';context.drawImage(playerArt,0,0,64,64);context.drawImage(targetArt,64,0,64,64);context.drawImage(sceneArt,128,0,64,64);context.drawImage(feedbackArt,192,0,64,64);</script></body></html>"
}
fn autonomous_fixture_with_setup(
task: &str,
run_id: &str,
@@ -280,6 +303,42 @@ fn prepare_completed_autonomous_manifest_fixture(root: &Path) {
register_autonomous_visual_fixture(root, "assets/art-spec.png", "icon-spec");
register_autonomous_visual_fixture(root, "assets/ui-prototype.png", "ui-prototype");
register_autonomous_visual_fixture(root, "assets/art-spritesheet.png", "art-spritesheet");
let directory = root.join("assets/art-spritesheet-slices");
fs::create_dir_all(&directory).expect("create autonomous slice fixture directory");
let usages = [
"player",
"blocks-and-targets",
"obstacles-and-scene",
"feedback-effects",
];
let slices = usages
.iter()
.enumerate()
.map(|(index, usage)| {
let path = format!("assets/art-spritesheet-slices/{usage}.png");
image::RgbaImage::from_pixel(32, 32, image::Rgba([90 + index as u8, 140, 220, 180]))
.save(root.join(&path))
.expect("write autonomous slice fixture");
serde_json::json!({
"name": format!("素材 {}", index + 1),
"path": path,
"width": 32,
"height": 32,
"usage": usage,
})
})
.collect::<Vec<_>>();
fs::write(
root.join("assets/art-spritesheet-slices/manifest.json"),
serde_json::to_vec_pretty(&serde_json::json!({
"schemaVersion": "game-art-slices.v1",
"source": "assets/art-spritesheet.png",
"sourceResourceId": "resource-art-spritesheet",
"slices": slices,
}))
.expect("serialize autonomous slice fixture"),
)
.expect("write autonomous slice manifest");
for task in new_game_creation_app_seed_tasks() {
update_manifest_task_status_at(root, &task.id, GameCreationAppTaskStatus::Completed)
.expect("complete autonomous seed task fixture");
@@ -536,7 +595,15 @@ fn append_failed_autonomous_root_projection(
#[test]
fn autonomous_continuation_intent_is_exact_and_does_not_swallow_new_requirements() {
for task in ["继续", " 继续完成。 ", "接着做", "请继续完成吧!"] {
for task in [
"继续",
" 继续完成。 ",
"接着",
"接着做",
"请继续完成吧!",
"continue",
"Go on!",
] {
assert!(is_pure_autonomous_continuation_intent(task), "task={task}");
}
for task in [
@@ -551,8 +618,11 @@ fn autonomous_continuation_intent_is_exact_and_does_not_swallow_new_requirements
#[test]
fn game_chat_pure_continue_inherits_failed_root_semantics_and_manifest_progress() {
let original_task = "做一个水晶主题的俄罗斯方块,完成移动、旋转、消行和重开";
let (_temporary, root, original_state, original_contract) =
autonomous_fixture(original_task, "crystal-tetris-original-run");
let (_temporary, root, original_state, original_contract) = autonomous_fixture_with_source(
original_task,
"crystal-tetris-original-run",
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
);
let original_record = read_latest_game_creator_agent_runtime_task_by_run_id(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
@@ -620,6 +690,30 @@ fn game_chat_pure_continue_inherits_failed_root_semantics_and_manifest_progress(
.expect("resolve continued root semantics"),
original_task
);
let continuation_state = agent_runtime_state_from_task_record(&continuation);
assert_eq!(
autonomous_completion_contract_for_state_at(&root, &continuation_state)
.expect("continued runtime must accept its inherited task semantics")
.expect("continued runtime keeps an autonomous completion contract"),
continuation_contract
);
update_manifest_task_status_at(&root, "design-director", GameCreationAppTaskStatus::Pending)
.expect("make one inherited task ready for scheduler validation");
let scheduled = schedule_autonomous_game_build_ready_tasks_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
&continuation.run_id,
3,
)
.expect("continued root must pass the scheduler contract gate");
assert_eq!(scheduled.len(), 1);
assert_eq!(scheduled[0].state.agent_id, "design-director");
update_manifest_task_status_at(
&root,
"design-director",
GameCreationAppTaskStatus::Completed,
)
.expect("restore inherited task completion after scheduler validation");
assert!(read_manifest_for_project(&root)
.expect("read continued tetris manifest")
.tasks
@@ -638,6 +732,52 @@ fn game_chat_pure_continue_inherits_failed_root_semantics_and_manifest_progress(
.expect("continued contract remains present"),
continuation_contract
);
append_failed_autonomous_root_projection(&root, &continuation, "budget-exhausted");
let second_continuation = append_unique_game_creator_agent_runtime_pending_task(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
&original_state.session_id,
"继续",
"crystal-tetris-second-continuation-run",
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD),
None,
)
.expect("queue a second continuation after the successor also fails");
let second_contract = read_autonomous_completion_contract(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
&second_continuation.run_id,
)
.expect("read second continuation contract")
.expect("second continuation contract exists");
assert_eq!(second_contract.task_sha256, original_contract.task_sha256);
assert_eq!(
autonomous_effective_root_task_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
&second_continuation.run_id,
&second_continuation.task,
)
.expect("resolve semantics through consecutive continuations"),
original_task
);
let second_state = agent_runtime_state_from_task_record(&second_continuation);
assert_eq!(
autonomous_completion_contract_for_state_at(&root, &second_state)
.expect("second continuation must pass runtime contract validation")
.expect("second continuation keeps the inherited contract"),
second_contract
);
assert!(schedule_autonomous_game_build_ready_tasks_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
&second_continuation.run_id,
3,
)
.expect("second continuation must pass the scheduler contract gate")
.is_empty());
}
#[test]
@@ -743,6 +883,56 @@ fn game_chat_pure_continue_does_not_inherit_across_sessions() {
.all(|task| task.status == GameCreationAppTaskStatus::Pending));
}
#[test]
fn game_chat_pure_continue_does_not_inherit_across_supervisor_sources() {
let (_temporary, root, original_state, original_contract) = autonomous_fixture_with_source(
"做一个水晶主题的俄罗斯方块",
"cross-source-original-run",
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE,
);
let original_record = read_latest_game_creator_agent_runtime_task_by_run_id(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
&original_state.run_id,
)
.expect("read cross-source original root")
.expect("cross-source original root exists");
append_failed_autonomous_root_projection(&root, &original_record, "failed");
let continuation = append_unique_game_creator_agent_runtime_pending_task(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
&original_state.session_id,
"继续",
"cross-source-continuation-run",
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD),
None,
)
.expect("queue cross-source continuation");
let continuation_contract = read_autonomous_completion_contract(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
&continuation.run_id,
)
.expect("read cross-source continuation contract")
.expect("cross-source continuation contract exists");
assert_eq!(
continuation_contract.task_sha256,
format!("{:x}", Sha256::digest("继续".as_bytes()))
);
assert_ne!(
continuation_contract.task_sha256,
original_contract.task_sha256
);
assert!(read_manifest_for_project(&root)
.expect("read cross-source reset manifest")
.tasks
.iter()
.all(|task| task.status == GameCreationAppTaskStatus::Pending));
}
#[test]
fn autonomous_completion_contract_recovery_rejects_v1_and_missing_baseline_artifacts() {
let (_temporary, root, state, contract) = autonomous_fixture(
@@ -1331,6 +1521,7 @@ fn game_chat_parent_completion_stops_after_preview_playtest_without_publish_task
"design-director"
| "art-director"
| "code-director"
| "art-asset-plan"
| "code-prototype"
| "preview-readiness"
| "preview-playtest"
@@ -1339,11 +1530,7 @@ fn game_chat_parent_completion_stops_after_preview_playtest_without_publish_task
.unwrap_or_else(|error| panic!("leave non-fast-path task pending: {error}"));
}
}
let revision = advance_game_index_revision(
&root,
&state,
"<!doctype html><html><body><canvas></canvas><script>const context=document.querySelector('canvas').getContext('2d');context.fillStyle='#42d9d0';context.fillRect(0,0,96,96);</script></body></html>",
);
let revision = advance_game_index_revision(&root, &state, cropped_spritesheet_game_html());
mark_verification_passed(&root, &state, "game.static_smoke");
let result = browser_result_fixture(&root, &state, revision, contract.playtest_scenario);
let action = AgentRuntimeToolAction {
@@ -1401,7 +1588,7 @@ fn game_chat_schedule_ready_tool_cannot_bypass_the_single_round_publish_boundary
}
#[test]
fn game_chat_code_prototype_treats_art_spec_as_reference_not_runtime_asset() {
fn game_chat_code_prototype_requires_cropped_spritesheet_use() {
let _config_guard = crate::tests::write_test_local_config(
r#"{"editorApi":{"apiKey":"game-chat-art-gate-key"}}"#.to_string(),
);
@@ -1420,10 +1607,45 @@ fn game_chat_code_prototype_treats_art_spec_as_reference_not_runtime_asset() {
&code_state,
"<!doctype html><html><body><canvas></canvas><script>const context=document.querySelector('canvas').getContext('2d');context.fillRect(0,0,96,96);</script></body></html>",
);
assert!(
autonomous_game_build_completion_blocker_at_locked(&root, &code_state).is_none(),
"game-chat code must not require the visual specification in the runtime DOM or Canvas"
let blocker = autonomous_game_build_completion_blocker_at_locked(&root, &code_state)
.expect("pure code core visuals must be rejected");
assert!(blocker
.detail
.as_deref()
.is_some_and(|detail| detail.contains("assets/art-spritesheet-slices/player.png")));
advance_game_index_revision(
&root,
&code_state,
"<!doctype html><html><body><img src='../assets/art-spritesheet-slices/player.png'><img src='../assets/art-spritesheet-slices/blocks-and-targets.png'><img src='../assets/art-spritesheet-slices/obstacles-and-scene.png'><img src='../assets/art-spritesheet-slices/feedback-effects.png'><canvas width=320 height=180></canvas></body></html>",
);
assert!(
autonomous_game_build_completion_blocker_at_locked(&root, &code_state).is_some(),
"showing the four slices outside the active game canvas must not satisfy runtime use"
);
advance_game_index_revision(
&root,
&code_state,
"<!doctype html><html><body><canvas width=320 height=180></canvas><script>const sheet=new Image();sheet.src='../assets/art-spritesheet.png';const context=document.querySelector('canvas').getContext('2d');context.drawImage(sheet,0,0);</script></body></html>",
);
assert!(
autonomous_game_build_completion_blocker_at_locked(&root, &code_state).is_some(),
"drawing the complete atlas must not count as cropped runtime use"
);
advance_game_index_revision(
&root,
&code_state,
"<!doctype html><html><body><canvas width=320 height=180></canvas><script>const sheet=new Image();sheet.src='../assets/art-spritesheet.png';const context=document.querySelector('canvas').getContext('2d');context.drawImage(sheet,0,0,32,32,0,0,64,64);</script></body></html>",
);
assert!(
autonomous_game_build_completion_blocker_at_locked(&root, &code_state).is_some(),
"guessing one atlas crop must not replace four persisted core slices"
);
advance_game_index_revision(&root, &code_state, cropped_spritesheet_game_html());
assert!(autonomous_game_build_completion_blocker_at_locked(&root, &code_state).is_none());
}
#[test]
@@ -1455,11 +1677,14 @@ fn cli_code_prototype_keeps_registered_canvas_spritesheet_gate_when_editor_is_co
&code_state,
"<!doctype html><html><body><img src=\"../assets/art-spritesheet.png\"><canvas></canvas></body></html>",
);
assert!(autonomous_game_build_completion_blocker_at_locked(&root, &code_state).is_some());
advance_game_index_revision(&root, &code_state, cropped_spritesheet_game_html());
assert!(autonomous_game_build_completion_blocker_at_locked(&root, &code_state).is_none());
}
#[test]
fn game_chat_code_prototype_does_not_require_art_spec_without_editor_configuration() {
fn game_chat_code_prototype_fails_closed_without_generated_spritesheet() {
let _config_guard = crate::tests::write_test_local_config("{}".to_string());
let (_temporary, root, parent_state, _contract) = autonomous_fixture_with_source(
"创建一轮程序化水晶小游戏",
@@ -1471,14 +1696,20 @@ fn game_chat_code_prototype_does_not_require_art_spec_without_editor_configurati
let code_record =
queue_autonomous_manifest_child_fixture(&root, &parent_state, "code-prototype");
let code_state = agent_runtime_state_from_task_record(&code_record);
fs::remove_file(root.join("assets/art-spec.png")).expect("remove optional art-spec file");
fs::remove_file(root.join("assets/art-spritesheet.png"))
.expect("remove required art-spritesheet file");
advance_game_index_revision(
&root,
&code_state,
"<!doctype html><html><body><canvas></canvas><script>const context=document.querySelector('canvas').getContext('2d');context.fillRect(0,0,96,96);</script></body></html>",
);
assert!(autonomous_game_build_completion_blocker_at_locked(&root, &code_state).is_none());
let blocker = autonomous_game_build_completion_blocker_at_locked(&root, &code_state)
.expect("game-chat must not fall back to programmatic core visuals");
assert!(blocker
.detail
.as_deref()
.is_some_and(|detail| detail.contains("assets/art-spritesheet.png")));
}
#[test]
@@ -1554,11 +1785,7 @@ fn game_chat_later_code_child_rejects_pending_then_projects_verified_completion(
let code_record =
queue_autonomous_manifest_child_fixture(&root, &parent_state, "code-prototype");
let mut code_state = agent_runtime_state_from_task_record(&code_record);
advance_game_index_revision(
&root,
&code_state,
"<!doctype html><html><body><canvas></canvas><script>const context=document.querySelector('canvas').getContext('2d');context.fillStyle='#42d9d0';context.fillRect(0,0,96,96);</script></body></html>",
);
advance_game_index_revision(&root, &code_state, cropped_spritesheet_game_html());
let blocker = autonomous_game_build_completion_blocker_at_locked(&root, &code_state)
.expect("later code child must keep the strict manifest status gate");
@@ -672,6 +672,12 @@ pub(in crate::agent) async fn observe_agent_runtime_platform_art_asset_generatio
detail: None,
};
}
let game_chat_requires_core_slices = if agent_id == "art-asset-plan" {
agent_runtime_root_source_at(root, agent_id, run_id)
.is_ok_and(|source| source.trim() == AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE)
} else {
false
};
let resumes_durable_generation = match pending_action {
Some(pending) => match platform_art_generation_runtime_recovery_at(root, pending) {
Ok(PlatformArtGenerationRuntimeRecovery::Missing) => false,
@@ -733,6 +739,17 @@ pub(in crate::agent) async fn observe_agent_runtime_platform_art_asset_generatio
};
}
};
if game_chat_requires_core_slices && prepared.slice_count() != 4 {
return AgentRuntimeToolObservation {
tool: "canvas.asset_generate".to_string(),
status: "failed".to_string(),
summary: format!(
"透明图集生成结果包含 {} 个独立切片;game-chat 必须恰好得到玩家、目标、场景和反馈四类真实素材,已在正式图集登记前失败关闭",
prepared.slice_count()
),
detail: None,
};
}
let _lock = match acquire_game_creator_agent_runtime_project_write_lock_with_wait(
root,
"canvas.asset_generate",
@@ -786,19 +803,49 @@ pub(in crate::agent) async fn observe_agent_runtime_platform_art_asset_generatio
);
}
};
match commit_prepared_platform_art_asset_at(root, prepared, &options, |_| {
let output_path = options
.output_path
.as_deref()
.expect("replaceExisting commit guard requires outputPath");
validate_agent_runtime_canvas_replacement_authorization_at(
root,
agent_id,
run_id,
output_path,
)
}) {
let committed = if game_chat_requires_core_slices {
commit_prepared_platform_art_asset_strict_slices_at(root, prepared, &options, |_| {
let output_path = options
.output_path
.as_deref()
.expect("replaceExisting commit guard requires outputPath");
validate_agent_runtime_canvas_replacement_authorization_at(
root,
agent_id,
run_id,
output_path,
)
})
} else {
commit_prepared_platform_art_asset_at(root, prepared, &options, |_| {
let output_path = options
.output_path
.as_deref()
.expect("replaceExisting commit guard requires outputPath");
validate_agent_runtime_canvas_replacement_authorization_at(
root,
agent_id,
run_id,
output_path,
)
})
};
match committed {
Ok(generated) => {
if game_chat_requires_core_slices {
if let Err(error) = write_local_project_file_at(
root,
"assets/manifest.art.json",
&game_chat_fast_path_art_manifest_content(),
) {
return AgentRuntimeToolObservation {
tool: "canvas.asset_generate".to_string(),
status: "failed".to_string(),
summary: "透明图集与四类切片已生成,但正式美术清单提交失败".to_string(),
detail: Some(redact_agent_runtime_project_paths(root, &error, 500)),
};
}
}
let verification = begin_agent_runtime_project_verification_locked(
root,
agent_id,
@@ -829,6 +876,7 @@ pub(in crate::agent) async fn observe_agent_runtime_platform_art_asset_generatio
"assetObjectId": generated.asset_object_id.clone(),
"taskId": generated.task_id.clone(),
"model": generated.model.clone(),
"sliceCount": generated.slices.len(),
}),
);
let slice_warning_summary = generated
@@ -849,7 +897,7 @@ pub(in crate::agent) async fn observe_agent_runtime_platform_art_asset_generatio
generated.asset.local_path
),
detail: Some(format!(
"assetId={}, localPath={}, resourceId={}, assetObjectId={}, taskId={}, model={}, warning={}, sliceWarning={}, verifiedRevision={mutation_revision}",
"assetId={}, localPath={}, resourceId={}, assetObjectId={}, taskId={}, model={}, warning={}, sliceWarning={}, sliceCount={}, verifiedRevision={mutation_revision}",
generated.asset.id,
generated.asset.local_path,
generated.resource_id.as_deref().unwrap_or(""),
@@ -857,7 +905,8 @@ pub(in crate::agent) async fn observe_agent_runtime_platform_art_asset_generatio
generated.task_id.as_deref().unwrap_or(""),
generated.model.as_deref().unwrap_or(""),
generated.warning.as_deref().unwrap_or(""),
generated.slice_warning.as_deref().unwrap_or("")
generated.slice_warning.as_deref().unwrap_or(""),
generated.slices.len()
)),
}
}
@@ -228,6 +228,7 @@ mod tests {
for task_id in [
"design-director",
"art-director",
"art-asset-plan",
"code-director",
"code-prototype",
"preview-readiness",
@@ -249,13 +250,13 @@ mod tests {
assert!(!detail.contains("publish-package"), "{detail}");
assert!(
detail.contains(
"seedTaskCounts: completed=6 running=0 pending=0 waiting=0 failed=0 total=6"
"seedTaskCounts: completed=7 running=0 pending=0 waiting=0 failed=0 total=7"
),
"{detail}"
);
assert!(
detail
.contains("taskCounts: completed=6 running=0 pending=0 waiting=0 failed=0 total=6"),
.contains("taskCounts: completed=7 running=0 pending=0 waiting=0 failed=0 total=7"),
"{detail}"
);
@@ -881,9 +881,20 @@ struct SyncCanvasProjectAssetsResult {
assets: Vec<UploadLocalAssetResult>,
}
#[derive(Debug, Eq, PartialEq)]
struct GeneratedPlatformArtAssetSlice {
name: String,
width: u32,
height: u32,
local_path: String,
resource_id: Option<String>,
asset_object_id: Option<String>,
}
#[derive(Debug, Eq, PartialEq)]
struct GeneratedPlatformArtAsset {
asset: UploadLocalAssetResult,
slices: Vec<GeneratedPlatformArtAssetSlice>,
resource_id: Option<String>,
asset_object_id: Option<String>,
task_id: Option<String>,
+1
View File
@@ -270,6 +270,7 @@ type GameChatPreviewValidationCandidate = GameChatPlayableRevision & {
const GAME_CHAT_STAGE_TASK_IDS = [
'design-director',
'art-director',
'art-asset-plan',
'code-director',
'code-prototype',
'preview-readiness',
@@ -60,6 +60,7 @@ const GAME_CHAT_FINAL_REPLY_MESSAGE_PREFIX = 'game-chat-final-reply:';
const GAME_CHAT_FINAL_REPLY_AGENT_IDS = new Set([
'design-director',
'art-director',
'art-asset-plan',
'code-director',
'code-prototype',
'preview-readiness',
@@ -328,6 +329,7 @@ export function buildGameChatProgressEvidence(
const fastPathTaskIds = new Set([
'design-director',
'art-director',
'art-asset-plan',
'code-director',
'code-prototype',
'preview-readiness',
@@ -135,6 +135,7 @@ function gameChatRuntimeState(
const GAME_CHAT_STAGE_TASK_IDS = [
'design-director',
'art-director',
'art-asset-plan',
'code-director',
'code-prototype',
'preview-readiness',
@@ -2859,10 +2860,11 @@ export function registerProjectSupervisorSurfaceTests() {
requestKind: 'tool-plan',
},
]);
expect(messages).toHaveLength(6);
expect(messages).toHaveLength(7);
expect(messages.map((message) => message.text)).toEqual([
expect.stringContaining('玩法方向已完成'),
expect.stringContaining('视觉方向已完成'),
expect.stringContaining('平台美术图集已生成并登记'),
expect.stringContaining('程序方案已完成'),
expect.stringContaining('代码原型已完成'),
expect.stringContaining('预览就绪检查已完成'),
@@ -2990,7 +2992,7 @@ export function registerProjectSupervisorSurfaceTests() {
let rendered = renderRelease();
await waitFor(() => {
expect(screen.getByText(/视觉方向已完成/)).not.toBeNull();
expect(screen.queryByText(/平台美术图集已生成并登记/)).toBeNull();
expect(screen.getByText(/平台美术图集已生成并登记/)).not.toBeNull();
expect(screen.getByText(/代码原型已完成/)).not.toBeNull();
expect(screen.getByText(/预览就绪已完成/)).not.toBeNull();
expect(screen.getByText(/试玩验证已完成/)).not.toBeNull();
@@ -3002,14 +3004,14 @@ export function registerProjectSupervisorSurfaceTests() {
String(args?.messageId ?? '').startsWith('game-chat-final-reply:'),
);
await waitFor(() => {
expect(finalReplyAppends()).toHaveLength(6);
expect(finalReplyAppends()).toHaveLength(7);
});
rendered.unmount();
rendered = renderRelease();
await waitFor(() => {
expect(screen.getByText(/代码原型已完成/)).not.toBeNull();
});
expect(finalReplyAppends()).toHaveLength(6);
expect(finalReplyAppends()).toHaveLength(7);
rendered.unmount();
});
@@ -3572,7 +3574,7 @@ export function registerProjectSupervisorSurfaceTests() {
expect(statusCard.textContent).toContain('生成 Agent 工具计划(本轮)');
});
it('counts only the six first-playable tasks in game-chat progress', () => {
it('counts only the seven first-playable tasks in game-chat progress', () => {
const manifest = createGameCreationAppManifest(
'game-chat-progress-total',
'game-chat-progress-total',
@@ -3592,7 +3594,7 @@ export function registerProjectSupervisorSurfaceTests() {
renderGameChatStatus({ runtime, manifest });
const progress = screen.getByLabelText('Supervisor 进度播报');
expect(progress.textContent).toContain('任务图 6/6');
expect(progress.textContent).toContain('任务图 7/7');
expect(progress.textContent).not.toContain('publish-strategy');
expect(progress.textContent).not.toContain('publish-package');
});
@@ -3772,7 +3774,7 @@ export function registerProjectSupervisorSurfaceTests() {
expect(within(progress).getByText('本轮生成进度')).not.toBeNull();
expect(within(progress).queryByText(/ 4 /u)).toBeNull();
expect(
within(progress).getByText('任务图 3/6 · 进行中 1 · 计划 1/3'),
within(progress).getByText('任务图 3/7 · 进行中 1 · 计划 1/3'),
).not.toBeNull();
expect(within(progress).getByText('核对首版试玩诊断')).not.toBeNull();
expect(within(progress).getByText('活跃专业 Agent')).not.toBeNull();
@@ -3841,7 +3843,7 @@ export function registerProjectSupervisorSurfaceTests() {
expect(within(progress).getByText('本轮生成进度')).not.toBeNull();
expect(within(progress).queryByText(/ 5 /u)).toBeNull();
expect(
within(progress).getByText('任务图 3/6 · 进行中 1 · 计划 2/3'),
within(progress).getByText('任务图 3/7 · 进行中 1 · 计划 2/3'),
).not.toBeNull();
expect(within(progress).getByText('安排程序 Agent 返工')).not.toBeNull();
expect(within(progress).getByText('返工决定')).not.toBeNull();
@@ -4307,6 +4309,7 @@ export function registerProjectSupervisorSurfaceTests() {
: [
'design-director',
'art-director',
'art-asset-plan',
'code-director',
'code-prototype',
'preview-readiness',
@@ -4448,7 +4451,7 @@ export function registerProjectSupervisorSurfaceTests() {
(stageRecordAppends()[0]?.[1] as { message?: { content?: string } })
?.message?.content ?? '',
);
expect(stageRecord).toContain('5/6');
expect(stageRecord).toContain('6/7');
});
it('archives a terminal game-chat run restored during initial hydration exactly once', async () => {
@@ -5914,17 +5914,17 @@
## 2026-08-03 game-chat 开发态前后端同源与快车道恢复
- 启动决策:`npm run agc``npm run agc:game-chat` 统一先经外层 Node 启动器预检 `3080`。在 marker 尚不能证明 worktree 归属时,任何已占用的 3080 都不得复用,并必须在原生窗口创建前失败关闭;Tauri CLI 退出后必须收束已启动的客户端进程树,不允许终端已退出但窗口与 Runner 仍假在线。
- 首波决策:普通 GUI / CLI 的正式 16 节点 DAG 与 game-chat 首版 lane 都只把 `design-director / art-director / code-director` 作为首波 ready Agent。正式 DAG 的 `design-foundation` 等待策划与美术 Director`code-prototype` 等待程序 Director 及数值、美术、音频三条底层产物链;game-chat 则在三个 Director 全部完成后启动 `code-prototype`,再串行执行静态检查和试玩。首波以外的非 repair 底层 Agent 只能由依赖就绪调度或上层明确返工合同按需激活。
- Runtime 决策:game-chat 任务 lane、平台美术、单轮收束和自动预览只由持久 `project-supervisor-game-chat` root source 启用。hydration 对 `Pending` 的容忍只适用于当前 source-aware lane 的三个零依赖首波任务,不再硬编码单个历史任务。页面进度、阶段记录和 final-reply 白名单统一使用项任务与 `x/6`
- 首波决策:普通 GUI / CLI 的正式 16 节点 DAG 与 game-chat 首版 lane 都只把 `design-director / art-director / code-director` 作为首波 ready Agent。正式 DAG 的 `design-foundation` 等待策划与美术 Director`code-prototype` 等待程序 Director 及数值、美术、音频三条底层产物链;game-chat 在策划与美术 Director 完成后启动 `art-asset-plan` 生成透明图集,`code-prototype` 必须同时等待三个 Director 与该图集任务,再串行执行静态检查和试玩。首波以外的非 repair 底层 Agent 只能由依赖就绪调度或上层明确返工合同按需激活。
- Runtime 决策:game-chat 任务 lane、平台美术、单轮收束和自动预览只由持久 `project-supervisor-game-chat` root source 启用。hydration 对 `Pending` 的容忍只适用于当前 source-aware lane 的三个零依赖首波任务。页面进度、阶段记录和 final-reply 白名单统一使用项任务与 `x/7`
- 显式协作合同:autonomous 的旧 `code-prototype + quality-review` 首批合同退出。显式 project collaboration policy 或持久 batch 恢复若进入首批 `agent.delegate` 路径,只允许且要求三个 Director 各一次;策划与程序 Director 是只读规划且 `expectedArtifacts=[]`,美术 Director 是非只读规范图任务且必须交付 `assets/art-spec.png`。任何非 repair 底层委派与 isolated child 都在首批失败关闭;默认 manifest DAG 仍是唯一自动首轮执行链,不额外复制三个 Director 委派。
- 输出决策:保留未提交 `streaming / ready` 的当前 revision 门;已提交的专业 Agent final reply 继续使用既有 durable response-stream 身份,后续项目 revision 变化不再隐藏早期阶段回复。
## 2026-08-03 game-chat 失败续跑与首版产物保护
- 事故事实:`game01` 的首个根 run 已交付并验证水晶下落方块原型,但父 run 在上下文压缩后因 token 上限失败;用户随后输入“继续”时,客户端创建了 task 仅为“继续”的新 game-chat root,完成合同重置 seed manifest,五分钟 fallback 再把该短语当主题整文件覆盖 `game/index.html`。这不是模型随机换题,而是 root 目标未继承、每新 run 无条件 reset 和 fallback 无条件 `file.write` 叠加形成的确定性缺陷。
- 续跑决策:同一 Supervisor Session 最近失败根 run 后严格继续短语创建 successor root,并继承前序原始任务、baseline revision / artifact 身份;真正新需求、跨 Session、前序正常完成或含具体新约束的输入继续创建独立新任务并重置本轮 manifest。successor 不复用旧网络请求、pending、action、Provider lifecycle 或 sidecar,只继承业务目标和已提交项目事实。
- 覆盖决策:game-chat fallback 只允许初始化缺失/占位入口的首次落盘。非占位 `game/index.html` 必须保留,先读取并局部修复或复用现有静态/试玩门;纯继续目标未恢复时失败关闭。完成门新增 baseline 玩法连续性和 action-driven state 检查,generic Canvas 非空、三个按钮存在或静态 smoke 通过都不能单独证明任务没有换题。
- 美术决策:`art-spec.png` 回归为规范图和下游派生参考,不能铺作完整场景,也不能裁剪成玩家/目标。首版允许依据规范使用程序化 Canvas 视觉;需要位图时使用独立透明素材。删除快车道中“必须把规范图作为主要背景/实体”的 prompt、fallback 与测试口径,并以“规范图不出现在运行画面、sequence 不随 RAF 空转”作为回归边界
- 续跑决策:同一 Supervisor Session、同一持久 source 的最近失败根 run 后严格继续短语创建 successor root,并继承前序原始任务、baseline revision / artifact 身份;纯继续识别统一由一个精确函数负责,覆盖“继续 / 接着 / continue / go on”等无新约束短语。真正新需求、跨 Session、跨 GUI / CLI / game-chat source、前序正常完成或含具体新约束的输入继续创建独立新任务并重置本轮 manifest。successor 不复用旧网络请求、pending、action、Provider lifecycle 或 sidecar,只继承业务目标和已提交项目事实。
- 覆盖决策:game-chat fallback 只允许初始化缺失/占位入口的首次落盘。非占位 `game/index.html` 必须保留,并由当前 `code-prototype` 先读取和实际 patch,取得本人 `mutationRevision` 后才能运行 `game.static_smoke` 与交付;只读 smoke 不得冒充续作。确定性 fallback 只允许已实现真实语义的显式玩法模板:俄罗斯方块模板必须具备 10×20 棋盘、下落、移动、旋转、锁定、消行和触顶失败,收集模板只用于明确收集类目标,未知玩法失败关闭。纯继续目标未恢复时同样失败关闭。完成门新增 baseline 玩法连续性和 action-driven state 检查,generic Canvas 非空、三个按钮存在或静态 smoke 通过都不能单独证明任务没有换题。
- 美术决策:`art-spec.png` 回归为规范图和下游派生 reference,不能铺作完整场景,也不能裁剪成玩家/目标。首版必须继续由 `art-asset-plan` 通过 icon-spritesheet 生成透明 `art-spritesheet.png`;桌面 Runtime 同时下载服务端 `iconImageSrcs`,按当前图集 resourceId 写入本地切片清单。game-chat 在任何本地落盘前要求稳定、非空的图集 resourceId,并在正式图集登记前要求切片严格等于四,全部切片累计下载最多 `32 MiB`;主图、四张 canonical 切片与切片清单作为一个提交合同,主图安装或资产登记失败时必须恢复整组旧合同。`code-prototype` 在活动 Canvas 中分别绘制玩家、方块/目标、障碍/场景和反馈四类不同切片。纯代码核心画面、猜测 atlas 等分坐标、单个裁切冒充全部类别、整图展示与路径诱饵失败关闭;编辑器仍允许仅有 `sliceWarning` 的完整透明图集完成,但 game-chat 必须等到真实切片可用
- 关联:`apps/ai-game-creator-shell/scripts/start-tauri-dev.mjs``start-dev-stack.mjs``src-tauri/src/agent/runtime_protocol/autonomous_completion.rs``response_stream.rs``docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`
## 2026-08-03 托管 MCP 未鉴权响应提供安全接入引导
@@ -81,9 +81,11 @@ cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml real_
修改 game-chat release flavor 后,至少执行壳配置门禁、AppSurface game-chat 定向测试、前端类型检查、AppData / 诊断日志 / release flavor 相关 Rust 定向测试、`npm run check:encoding``git diff --check`。打包 smoke 必须确认:安装信息和产物版本为 `0.1.1`;无参数启动直接进入且只能停留在 game-chat 页面;停止或断开 `api-server` 后本地工作台仍能打开;普通 dev / release 与 debug game-chat 仍走原认证入口;独立 AppData 生效。预览 smoke 应先让当前 run 成功验证 revision N,确认 Tauri registry 启动一个 server 且 iframe 自动出现;在 validate 后、start 取得锁前推进项目 revision,必须确认原子 `expectedRevision` 门禁拒绝启动且授权保留等待新证据;再验证 revision N+1,确认 server 进程和 loopback origin 不变、iframe 显示新版本且响应为 `no-store`。same-run steer 还要覆盖旧验证不消费新授权、旧异步 attempt 不清新 generationRunner registry 单独 running、失败 / 相同 / 更低 revision 均不能触发用户预览或重复刷新,停止后顶部显示“预览未启动”。独立包退出时必须通过 `runner.shutdown_for_client_exit` 先进入 draining 再结束本 boot,保留 durable sidecar 供下次 reconciliation,不把中断任务写成 completedWindows Runner 必须 `CREATE_SUSPENDED -> AssignProcessToJobObject -> ResumeThread`,分配或恢复失败时 kill + wait,客户端持有 kill-on-close Job 兜底,关闭主窗口后 Runner、MCP、command、ConPTY 及其后代都应消失。普通 dev / release 和 CLI 继续使用 `runner.shutdown_if_idle`
game-chat 迭代还必须确认以下行为:Supervisor ready 输出、`art-director / code-prototype / preview-readiness / preview-playtest` 个专业 Agent 的 durable `final-reply`,以及 Rust 明确生成 `eventId + publicText` 的每条公开 Runtime 输出都作为独立 assistant 消息逐条固化在项目聊天`art-asset-plan` 不进入进度、阶段记录或 final-reply 投影。消息通过顶层 `messageId` 幂等追加,事件 / 轮询 / hydration 重放不重复。前端禁止从原始 `summary / detail`、tool plan、Provider / Runner 元数据、命令输出或路径自行拼接持久消息;UI 把一个父 Run 统一显示为“本轮生成进度”,最新状态也不得暴露内部“第 N 轮”,完整 GUI / CLI 任务图可显示 `x/14`,首版快车道显示 `x/4`,终态不保留运行中进度卡;可信 `project-supervisor-game-chat` 一轮完成 `preview-playtest` 后直接收束,不请求下一次 Provider tool-plan;所有调度入口(包括 `agent.schedule_ready``task.list` 结果驱动的直接 `agent.delegate`均不得暴露或启动 `publish-strategy` / `publish-package`。game-chat 必须配置可用的 External Editor API `art-director` 通过一次平台 `images/generations` 生成并登记 `assets/art-spec.png``code-prototype` 必须把它显著用于用户可见的主要背景、玩家和目标;未配置 API 或缺少任一环节都必须失败关闭。普通 GUI / CLI autonomous 继续正式透明 `assets/art-spritesheet.png` 的完整 DAG。对应定向测试至少包括:
game-chat 迭代还必须确认以下行为:Supervisor ready 输出、`design-director / art-director / art-asset-plan / code-director / code-prototype / preview-readiness / preview-playtest` 个专业 Agent 的 durable `final-reply`,以及 Rust 明确生成 `eventId + publicText` 的每条公开 Runtime 输出都作为独立 assistant 消息逐条固化在项目聊天。消息通过顶层 `messageId` 幂等追加,事件 / 轮询 / hydration 重放不重复。前端禁止从原始 `summary / detail`、tool plan、Provider / Runner 元数据、命令输出或路径自行拼接持久消息;UI 把一个父 Run 统一显示为“本轮生成进度”,最新状态也不得暴露内部“第 N 轮”,完整 GUI / CLI 任务图可显示 `x/14`,首版快车道显示 `x/7`,终态不保留运行中进度卡;可信 `project-supervisor-game-chat` 一轮完成 `preview-playtest` 后直接收束,不请求下一次 Provider tool-plan;所有调度入口均不得暴露或启动 `publish-strategy` / `publish-package`。game-chat 必须配置可用的 External Editor API `art-spec.png -> icon-spritesheet -> art-spritesheet.png + iconImageSrcs 本地切片 -> code-prototype Canvas 使用四类切片` 推进;缺少任一环节都必须失败关闭。
game-chat 首版快车道采用独立四阶段口径:只显示 `art-director``code-prototype``preview-readiness``preview-playtest``x/4`,不把完整 DAG 或内部 loop 计入分母。父 Run 与全部 child Run 共用 240 秒软预算和 300 秒累计硬上限;首版最多一次 Provider 规划 / 写入请求,软预算后只能运行确定性本地 fallback、`game.static_smoke``preview.validate`,硬上限内未通过完成门必须失败,证据在上限后到齐也不得写 `single_round_converged`。live10 实测正式透明 `icon-spritesheet` 后处理超过 300 秒,因此 game-chat 改为一次平台 `images/generations` 生成并登记 `assets/art-spec.png`;fallback 只有该图片有效登记且真实存在时才允许生成,并必须把它显著绘制为主要背景、玩家和目标。未配置 External Editor API、图片生成失败、缺少 Canvas / manifest 登记、文件、HTML 引用或用户可见绘制时必须在 300 秒内失败关闭,绝不误报 completed。普通 GUI / CLI 继续正式透明 `assets/art-spritesheet.png` 的完整 DAG。对应定向测试至少包括:
game-chat 素材完整快车道采用七任务口径。父 Run 与全部 child Run 共用 4200 秒软预算和 4500 秒累计硬上限;先确定性生成并登记 `art-spec.png`,再由 `art-asset-plan` 通过专用 icon-spritesheet 路由生成透明 `art-spritesheet.png`,下载并持久化响应中的独立切片,最后才允许 code-prototype 写入或局部修复入口。软预算后只允许使用已登记图集、当前 resourceId 对应切片清单的确定性本地 fallback、`game.static_smoke``preview.validate`;不得退回普通生图、猜测 atlas 网格或纯代码核心画面。完成门要求活动 Canvas 分别绘制 player、blocks-and-targets、obstacles-and-scene、feedback-effects 四类不同切片;整图 `<img>`、CSS background、完整图集直绘、单个猜测裁切和路径诱饵均失败。对应定向测试至少包括:
失败续跑还必须覆盖同 Session 同 source 继承、跨 Session / 跨 source 不继承、首次与连续 successor 的 effective task / contract / scheduler 一致性,以及中英文纯继续短语使用同一识别函数。非占位入口的新 `code-prototype` 必须先产生本人 mutation 再 smoke;连续只读 smoke 不得收束。占位 fallback 只允许显式支持的真实玩法模板,俄罗斯方块必须验证棋盘、下落、旋转、锁定和消行语义,未知玩法必须失败关闭。
game-chat GUI 恢复还要覆盖两类竞态:root Runtime 先终态、manifest 四阶段后终态时,必须等到四任务最终状态后仅持久化一条 `【Supervisor 阶段记录】`;页面初始 hydration 直接读到真实终态时也要补写缺失记录,但不得把 `idle` 当作完成。同时,GUI 启动的 `agent.resume` 自动扫描必须先做只读恢复工作预检:新项目或无 task / retry / handoff / finalization / pending / reconciliation 工作的已终态项目不弹确认,存在任何 durable recovery artifact 则仍必须命中 `agent.resume` policy。
@@ -4027,8 +4027,8 @@
- 现象:原根 run 已经写出并验证目标玩法,但父 Runtime 因预算、上下文或 Provider 失败;用户在同一项目输入“继续”后,页面标题变成“继续”,玩法被默认收集/点击模板替换,美术规范总览图被直接铺进游戏画面。
- 原因:终态失败 run 不能 steer,提交层因此创建 task 只有继续短语的新 root;每个新 autonomous 根合同又无条件 reset seed manifestgame-chat fallback 从当前 root task 取主题并直接 `file.write game/index.html`。同时快车道把 `icon-spec` 误当运行素材,要求整图背景和象限裁剪实体。通用 smoke/generic playtest 只验证结构与最小交互,无法发现玩法目标已经漂移。
- 处理:严格继续意图必须在同一 Supervisor Session 内继承最近失败根 run 的原始目标和 baseline,但保持新的 run/Provider/sidecar 身份;真正新需求仍独立 reset。非占位入口禁止 fallback 整体覆盖,先保留并局部修复或验证`art-spec.png` 只作规范参考,运行时位图必须来自独立派生素材;程序化首版不应为了“可见使用平台图片”把规范板塞进画面
- 验证:覆盖失败根任务“水晶俄罗斯方块”后输入“继续”、跨 Session、正常完成后新输入、带具体新需求、既有非占位入口、初始化占位、纯继续目标缺失、规范图不在运行 DOM/Canvas、真实动作前后 `sequence` 与 RAF 空转。浏览器验收必须同时比较 baseline 玩法关键文本/控件/状态和当前 revision,不能只看 Canvas 非空与三个固定按钮。
- 处理:严格继续意图必须在同一 Supervisor Session、同一持久 source 内继承最近失败根 run 的原始目标和 baseline,但保持新的 run/Provider/sidecar 身份;纯继续词表只能有一个权威实现,中英文短语都走同一入口,真正新需求仍独立 reset。非占位入口禁止 fallback 整体覆盖,也不能反复运行只读 smoke;当前 `code-prototype` 必须先读取并实际 patch,取得本人 mutation 后才能验证和交付。占位 fallback 只支持具备真实语义的显式模板,俄罗斯方块必须实际实现棋盘、下落、旋转、锁定和消行,未知玩法失败关闭`art-spec.png` 只作规范参考,核心运行时位图必须来自独立派生的透明 `art-spritesheet.png` 及其 `iconImageSrcs` 本地切片;切片清单绑定当前图集 resourceId,Canvas 分别使用玩家、目标、场景和反馈四类素材。不得猜测图集是 2×2 等分、把规范板塞进画面或以纯代码核心实体绕过派生素材
- 验证:覆盖失败根任务“水晶俄罗斯方块”后输入“继续”、连续 successor、跨 Session、跨 source、正常完成后新输入、带具体新需求、既有非占位入口先 patch 后 smoke、初始化占位的俄罗斯方块真实语义、未知玩法失败关闭、纯继续目标缺失、规范图不在运行 DOM/Canvas、真实动作前后 `sequence` 与 RAF 空转。浏览器验收必须同时比较 baseline 玩法关键文本/控件/状态和当前 revision,不能只看 Canvas 非空与三个固定按钮。
- 关联:`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion.rs``apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/game_chat_fast_path.rs``apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_request_builders.rs`
## Tauri beforeDevCommand 失败不等于已启动客户端会自动退出(2026-08-03)
@@ -4051,7 +4051,7 @@
- 现象:生成提交发生客户端超时、连接中断或响应丢失后,调用方创建新的 `Idempotency-Key` 再提交一次;原任务其实已经入队,最终造成重复生成、重复扣费和重复画布 / 素材库写入。
- 原因:把“客户端没有收到结果”误判为“服务端没有受理”,又没有持久保留逻辑请求的幂等键和服务端返回的 `operationId`。托管 MCP 若绕过 External REST router 直接调用 worker 或 SpacetimeDB,也会形成第二套去重与状态语义。
- 处理:一次逻辑生成只分配一个稳定幂等键。桌面 Runtime 在 POST 前先把精确请求体、SHA-256 和幂等键原子写入私有生成账本并回读一致;收到 `202 + operationId` 后先把账本升级为 `accepted` 再轮询。重启时 `accepted` 只恢复 GET`prepared`、响应丢失、`202` 缺 operationId、轮询超时和状态损坏都进入 `needs-reconciliation`,绝不自动 POST。game-chat 五分钟硬截止可以结束本轮、关闭预览和客户端,但 executing 的 `canvas.asset_generate` 必须保留 pending action、provider batch 与生成账本;旧 `200` 图集的 `spritesheetResource` 允许为空,此时只在顶层 `spritesheetImageSrc` 是有效下载引用时优先使用,否则回退可用 `objectKey``postprocess-failed-source-preserved` 进入不可自动重生的对账边界;其它 non-blocking warning 继续消费成功结果并单独展示。旧 `200` 兼容不改变权威 External v1 的异步契约。MCP 生成工具必须把 `idempotencyKey` 映射到同一 REST header,并复用同一 External router、owner 和任务账本。
- 处理:一次逻辑生成只分配一个稳定幂等键。桌面 Runtime 在 POST 前先把精确请求体、SHA-256 和幂等键原子写入私有生成账本并回读一致;收到 `202 + operationId` 后先把账本升级为 `accepted` 再轮询。重启时 `accepted` 只恢复 GET`prepared`、响应丢失、`202` 缺 operationId、轮询超时和状态损坏都进入 `needs-reconciliation`,绝不自动 POST。game-chat 的 4500 秒硬截止可以结束本轮、关闭预览和客户端,但 executing 的 `canvas.asset_generate` 必须保留 pending action、provider batch 与生成账本;旧 `200` 图集的 `spritesheetResource` 允许为空,此时只在顶层 `spritesheetImageSrc` 是有效下载引用时优先使用,否则回退可用 `objectKey``postprocess-failed-source-preserved` 进入不可自动重生的对账边界;其它 non-blocking warning 继续消费成功结果并单独展示。旧 `200` 兼容不改变权威 External v1 的异步契约。MCP 生成工具必须把 `idempotencyKey` 映射到同一 REST header,并复用同一 External router、owner 和任务账本。
- 补充:不能把“accepted 分支里没有生成 POST”误当成 GET-only 恢复。若读取账本前仍重做项目/素材目录准备、输出路径预检或请求正文构造,恢复仍可能创建远端资源或在查询 operation 前失败。恢复必须直接使用 durable snapshot;清理必须最后删除 pending 身份锚点,活动 orphan 不得自动删除。完整恢复 future 还要在默认 Tokio worker 栈下验证,不能靠测试环境调大 `RUST_MIN_STACK` 掩盖栈溢出。
- 加固:durable snapshot 必须绑定不含明文凭据的 base URL/API Key 配置指纹,配置漂移时连 GET 也必须阻断。accepted operation 明确 failed 也不能在 observation 持久化前删账本。旧 `200` durable result 只保留允许字段与安全 objectKey/相对路径,签名 URL、query/fragment 和未知字段不落盘。提交只有契约明确的 `400 / 401 / 403` 可证明未入队并清理 prepared 账本;超时、冲突、限流、网关错误及其它意外状态均保留账本进入对账。账本根目录、扫描和删除必须通过受控路径解析逐级拒绝符号链接,不能让项目内链接把清理目标指向项目外。
- 验证:覆盖“服务端已入队但提交响应丢失”后原键重试仍返回同一 operation、换 owner 不可见、查询最终只出现一份 completed result 和一次计费 / 写回;MCP 与 REST 对同一 owner、同一请求和同一键必须命中同一 operation。
File diff suppressed because one or more lines are too long