From 530538842e011973f8b9645676413cc994e67e53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Wed, 23 Sep 2026 11:10:55 +0800 Subject: [PATCH 01/20] =?UTF-8?q?=E6=96=87=E6=A1=A3=EF=BC=9A=E6=94=B6?= =?UTF-8?q?=E5=8F=A3=E9=80=80=E5=BD=B9AGC=E9=A1=B9=E7=9B=AE=E5=AF=B9?= =?UTF-8?q?=E8=AF=9D=E6=96=9C=E6=9D=A0=E5=91=BD=E4=BB=A4=E4=B8=8E=E7=BB=88?= =?UTF-8?q?=E7=AB=AFswarm=20chat=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 ADR:记录斜杠命令与终端 swarm chat 入口整体退役的决策、保留项与影响边界 - 新增里程碑计划:登记删除范围、检查点、验证命令与未做边界 - 更新 decision-log:新增 2026-09-22 决策条目,清理被本次退役取代的历史命令条目 - 更新 pitfalls:移除已退役命令入口的现役表述 - 更新 CONTEXT.md 术语:把「斜杠命令」标为已退役说法 - 更新 docs/README.md:登记新 ADR - 更新 AGC 主实施计划:删除全部「聊天输入 /」能力条目与终端聊天入口承诺 - 更新 Agent Runtime V1.1 文档:删除终端 swarm chat 入口整节,控制面统一记为 --agent-* CLI - 更新 DirectProject 里程碑计划:/history 入口表述改为显式重新加载对话 --- CONTEXT.md | 8 + docs/README.md | 1 + ...对话斜杠命令与终端swarm chat入口-2026-09-22.md | 33 +++ ...碑】DirectProject聊天真相源收敛-2026-09-16.md | 2 +- ...目对话斜杠命令与终端swarm chat入口-2026-09-22.md | 76 +++++ .../shared-memory/decision-log.md | 152 +++------- docs/project-memory/shared-memory/pitfalls.md | 20 +- ...案】AI游戏创作Agent Runtime V1.1-2026-07-12.md | 82 ++---- ...案】AI游戏创作智能体App实施计划-2026-06-24.md | 275 +++--------------- 9 files changed, 241 insertions(+), 408 deletions(-) create mode 100644 docs/adr/【ADR】退役AGC项目对话斜杠命令与终端swarm chat入口-2026-09-22.md create mode 100644 docs/project-memory/plans/【里程碑】退役AGC项目对话斜杠命令与终端swarm chat入口-2026-09-22.md diff --git a/CONTEXT.md b/CONTEXT.md index 724d95de3..2d9b54ed5 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -194,6 +194,14 @@ _Avoid_: 进度通知、快照轮询、第二套历史 把项目对话历史条目与运行态事件转换成消息气泡和工具卡片的读取期转换;不持久化,也不构成事实源。 _Avoid_: 投影缓存文件、已脱敏卡片库、第二套 reducer +**项目对话输入**: +AGC 项目对话的输入只有自然语言回合(含 `@` 素材引用与附件);需要动作时由 Runtime 工具与确认卡承接,不从输入文本解析控制词。 +_Avoid_: 斜杠命令、聊天命令草稿、命令发现列表 + +**项目命令 id**: +AGC 运行期工具与项目权限策略使用的稳定标识(`GAME_CREATION_APP_COMMANDS` 与 `GameCreationAppPermission`),由 Rust 策略校验与确认卡消费,不是用户输入语法。 +_Avoid_: 把命令 id 当作可输入的聊天命令、为权限位补聊天入口 + ## Relationships - 一个 **汪汪声浪大作战** 单局包含多个 **有效声浪触发**。 diff --git a/docs/README.md b/docs/README.md index d1af5b3c0..eb0e5fa3d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -40,6 +40,7 @@ - [DirectProject Codex 原始历史与异常恢复](<./technical/【技术方案】DirectProject Codex原始历史与异常恢复-2026-09-04.md>):原始 Responses item 持久化、线程注入与异常回合收尾。 - [DirectProject 对话历史单一事实源](./adr/【ADR】DirectProject对话历史单一事实源-2026-09-16.md):AGC 项目开发对话只以项目对话历史与运行态事件为真相源,聊天投影不落盘。 - [DirectProject 独立聊天容器与工作台钱包布局](./adr/【ADR】DirectProject独立聊天容器与工作台钱包布局-2026-09-18.md):DirectProject 与 Supervisor 等路径分容器,钱包入口由项目工作台布局独立承载。 +- [退役 AGC 项目对话斜杠命令](./adr/【ADR】退役AGC项目对话斜杠命令与终端swarm chat入口-2026-09-22.md):AGC 项目对话与终端 swarm chat 均不再解析斜杠命令,终端聊天入口一并退役;实现、测试、门禁与文档承诺全部删除,命令 id 与权限位作为项目策略词汇表保留。 - [GameAgent 对话工具调用卡片](./technical/【技术方案】GameAgent对话工具调用卡片-2026-09-14.md):把右侧对话里的执行命令 / 写文件投影成 Codex 风格可折叠卡片,含采集、独立历史文件、事件字段与回读契约。 - [DirectProject 客户端 Skill 与 MCP 扩展导入方案](./technical/【技术方案】DirectProject客户端Skill与MCP扩展导入方案-2026-08-31.md):客户端扩展导入、按独立 Skill/MCP 拆分、命名、启用和启动时注入边界。 - [AGC 通用插件宿主与编辑器适配](./technical/【技术方案】AGC通用插件宿主与编辑器适配-2026-09-09.md):通用插件宿主、SDK、权限审计、UI 挂载和 Cocos 编辑器适配边界。 diff --git a/docs/adr/【ADR】退役AGC项目对话斜杠命令与终端swarm chat入口-2026-09-22.md b/docs/adr/【ADR】退役AGC项目对话斜杠命令与终端swarm chat入口-2026-09-22.md new file mode 100644 index 000000000..c93799589 --- /dev/null +++ b/docs/adr/【ADR】退役AGC项目对话斜杠命令与终端swarm chat入口-2026-09-22.md @@ -0,0 +1,33 @@ +# 【ADR】退役AGC项目对话斜杠命令与终端swarm入口-2026-09-22 + +状态:已接受 + +## 背景 + +AGC(`apps/ai-game-creator-shell`)的项目对话曾把用户能力挂在「聊天输入 `/`」上:除真正带执行语义的 `/history` 外,还累积了几十个只生成后续草稿或只读摘要的命令(`/brief`、`/status`、`/read`、`/trace`、`/art`、`/export`、`/cover`、`/remember` 等)。无 GUI 的终端 swarm chat 入口 `--swarm-chat` 另有一套控制命令(`/help`、`/agents`、`/status`、`/history`、`/compact`、`/resume`、`/goal`、`/quit`)。 + +这些命令的实际状态是:正式对话面已经换成 DirectProject 单容器,斜杠命令列表不再有渲染入口,`/history` 之外没有任何现役调用方;但命令字面量仍分散在控制器分支、`chatPromptPolish` 的 `/` 前缀绕过、命令参数校验文案、`projectSummaryConstants` 的命令清单、只服务已退役摘要面板的 `project-summary/*Summaries.ts`、构建期门禁 `scripts/check-config.mjs`、无人调用的 Tauri 能力清单命令、`swarm_cli` 的终端输入解析与帮助输出,以及多份权威文档的承诺里。 + +保留它的代价持续存在:每次调整对话形态都要同步维护这套死词汇表和它的门禁,而且「命令」一词在 AGC 里同时指用户斜杠命令与项目权限命令 id(`GAME_CREATION_APP_COMMANDS`)两件事,术语歧义会直接误导后续改动。 + +## 决策 + +- 斜杠命令语义整体退役,按「从未存在」处理:不保留入口、不做兼容提示、不写 tombstone,实现、专属测试、构建期门禁条目与文档承诺一并删除,历史由 Git 保存。 +- 删除项:Direct 聊天的 `/history` 精确匹配分支与 `reloadHistory`;`chatPromptPolish` 的 `/` 前缀绕过;`chatCommandMetadata`、`projectSummaryConstants.chatCommandHelp`、`memoryCommands.parseRememberInput` 等命令清单与参数解析;只服务退役 Supervisor 摘要面板、零外部调用的 `project-summary/*Summaries.ts` 与 `agentTrace.ts`;草稿回填死链(前端 `agentPresentation.ts` 的草稿推导与 Rust `suggested_canvas_tool_call`);无人调用的 Tauri 命令 `get_game_creation_agent_capabilities` 与 `get_limited_local_commands`;钉住上述字符串的门禁条目与专属测试。 +- 终端 swarm chat 入口连带其命令层整体退役:`--swarm-chat`、`src-tauri/src/swarm_cli.rs` 与整个 `swarm_cli/` 目录(`/help`、`/agents`、`/status`、`/history`、`/compact`、`/resume`、`/goal`、`/quit` 的解析、帮助输出、turn 派发、观察器、报告与专属测试)一并删除;`SwarmChatFlow`、`SwarmTurnObservation`、`SwarmTurnOutcome::Quit`、`SwarmConfirmationResolution::Quit`、`SWARM_TURN_*_ERROR`、`agent.compact` / `agent.resume` / `agent.run_status` 权限门禁与 `print_runtime_response_stream_status` 也随之消失。只服务终端交互内核的 `agent/interaction.rs` 整层(`AgentInteractionAction`、tool registry、`game_creator_agent_uses_interaction_kernel`、`decide_game_creator_agent_interaction_turn_for_session_at`、`AgentInteractionProviderStreamSink`)同样删除,该文件只保留自然语言 steer 决策路径 `decide_game_creator_agent_runtime_steer_at`。 +- 保留项(它们不是斜杠命令):命令 id 注册表 `GAME_CREATION_APP_COMMANDS` 与 `GameCreationAppPermission`(Rust 运行期项目权限策略的词汇表,开发 Agent UI 仍在用)、`needsInitializedChatProject` 与项目权限策略链路、Rust 侧路径与路由的 `/` 前缀校验、`--agent-*` CLI 控制命令(Goal、steer、cancel、retry、context compact、resume、状态查询)与 Tauri IPC 注册。 +- 应用内项目对话的输入只剩自然语言回合(外加 `@` 素材引用与附件);需要动作时由 Runtime 工具、确认卡和既有 CLI 控制命令承接,不由聊天文本解析控制词。 +- 需要显式控制时改用现有 `--agent-*` CLI 命令:手动压缩是 `--agent-context-compact`,恢复扫描是 `--agent-resume`,Goal 生命周期是 `--agent-goal-*`。 + +## 备选方案与取舍 + +1. **只删正式用户窗口的入口,保留解析层**:看似省事,但命令字面量与分支继续存在,新对话形态仍要绕过它们,正是本次要消除的持续维护成本。 +2. **保留 `/history` 作为唯一命令**:它确实是唯一有执行语义的入口,但保留一个精确匹配的 `/` 语法就要求保留前缀绕过判断、命令被发现与文档承诺的整套口径;DirectProject 的历史重读改由重新进入对话/重新订阅自然完成,不需要用户输入控制词。 +3. **加兼容层(识别到已知命令时给提示或忽略)**:等于把死词汇表永久固化在解析层,与「按从未存在处理」相反,且会长期占据用户可见面。 + +## 影响 + +- 术语收敛:AGC 里的「命令」此后指项目内部命令 id 与权限位,「斜杠命令」作为已退役说法不再出现在权威文档与代码注释中。 +- 应用内项目对话的可见行为不变:正式对话面本来就不渲染命令列表,`/history` 之外没有可执行路径;删除后唯一的用户可见差异是输入以 `/` 开头时按普通文本处理。终端侧不再有 swarm chat 入口,也没有任何斜杠命令面。 +- 删除范围包含构建期门禁条目,因此不得为退役概念新增守卫测试或断言残留字符串的 check 条目;防止概念回归依靠架构边界(没有解析层可写)而不是字符串钉桩。 +- `.agent/logs/command.log`、项目权限确认卡与 `commandRuns` 属于项目命令审计,继续保留,不受本次退役影响。 diff --git a/docs/project-memory/plans/【里程碑】DirectProject聊天真相源收敛-2026-09-16.md b/docs/project-memory/plans/【里程碑】DirectProject聊天真相源收敛-2026-09-16.md index bb852bae7..0a1ebbe3d 100644 --- a/docs/project-memory/plans/【里程碑】DirectProject聊天真相源收敛-2026-09-16.md +++ b/docs/project-memory/plans/【里程碑】DirectProject聊天真相源收敛-2026-09-16.md @@ -42,7 +42,7 @@ AGC 项目开发对话的显示与恢复只依赖两项输入:**项目对话 - 未知 item 类型由 Rust 原样透传(只有类型与身份,Rust 侧留 TODO),当前由前端投影丢弃。 - 前端聊天卡片的工具形状是 `Omit`;`tool-calls.jsonl` 的持久化形状与 DirectRuntime 的写入保持不变。 - 「可显示」的判据取**前端回合反馈**:一次翻页操作连拉到「合并后聊天投影的回合数增加」为止。工具卡片与思考文本虽然能通过 `projectDirectThreadItem`,但可能整页落进已渲染回合的折叠「执行过程」,不构成用户可见反馈;口径只在 `directHistoryPaging.ts` 里实现一份,首屏与「显示更早」共用。 -- 首屏切片的**新端边界**只认 `subscribe` 回执里的 `lastCompletedItemId`(含该条):回执到达之前不读首屏,也不退化成「取文件尾」;锚点缺失(订阅不可用 / 失败 / 历史为空)时才按文件尾取尾屏,`/history` 手动重读保持按当前文件尾取尾屏的恢复语义。 +- 首屏切片的**新端边界**只认 `subscribe` 回执里的 `lastCompletedItemId`(含该条):回执到达之前不读首屏,也不退化成「取文件尾」;锚点缺失(订阅不可用 / 失败 / 历史为空)时才按文件尾取尾屏,手动重读保持按当前文件尾取尾屏的恢复语义。 ## 依赖与前置条件 diff --git a/docs/project-memory/plans/【里程碑】退役AGC项目对话斜杠命令与终端swarm chat入口-2026-09-22.md b/docs/project-memory/plans/【里程碑】退役AGC项目对话斜杠命令与终端swarm chat入口-2026-09-22.md new file mode 100644 index 000000000..76cad37f8 --- /dev/null +++ b/docs/project-memory/plans/【里程碑】退役AGC项目对话斜杠命令与终端swarm chat入口-2026-09-22.md @@ -0,0 +1,76 @@ +# 【里程碑】退役AGC项目对话斜杠命令与终端swarm chat入口-2026-09-22 + +状态:已完成 +父规范:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md` + +## 目标与判据 + +一句话目标:AGC 应用内不再存在任何斜杠命令语义,终端 swarm chat CLI 入口(`--swarm-chat`)连同其命令层整体退役,且不再残留只服务这两者的实现、测试、文案与构建期门禁;行为上等同于它们从未存在。 + +验收判据: +- AGC 应用内(`apps/ai-game-creator-shell/src/**`)除路径/路由字面量外,不存在形如 `'/xxx'` 的命令字面量,也不存在对 `/` 开头输入的命令分支。 +- 斜杠命令专属模块与其导出全部消失,`projectSummary` 桶文件只保留现役导出。 +- 权威文档不再承诺「聊天输入 `/`」能力,终端章节点也不再把控制命令列为入口。 +- 终端侧不再存在 swarm chat 入口与其命令层;`--agent-run`、`--agent-enqueue`、`--agent-steer`、`--agent-resume`、`--agent-context-compact`、`--preview-serve` 等运维类控制命令保持现役。 +- 依赖终端入口的真实 E2E 与脚本(`user-input`、`supervisor-autonomous-playable-lane-defense`、三个 supervisor-swarm 混合套件、`agc:test`、`agc:test:chat*`、`agc:chat`、`agc:swarm`)与交互式 CLI harness 管道一并消失。 +- 不新增任何守卫测试或 check 脚本条目。 + +## 范围 + +范围内(删除): +- `features/project-summary/projectSummaryConstants.ts` 的 `chatCommandHelp` 与全部命令清单常量。 +- `features/project-summary/chatCommandMetadata.ts`(斜杠命令参数校验文案)。 +- Direct 聊天的 `/history` 精确匹配分支与 `reloadHistory`。 +- `chatPromptPolish` 的 `/` 前缀绕过分支。 +- `memoryCommands.ts` 的 `parseRememberInput`(`/remember` 参数解析器)。 +- 只服务退役 Supervisor 摘要面板、零外部调用的 `project-summary/*Summaries.ts` 与 `agentTrace.ts`。 +- `/sync-canvas-project`、`/read `、`/trace` 的草稿回填死链(前端 `agentPresentation.ts` + Rust `suggested_canvas_tool_call`)。 +- 无人调用的 Tauri 命令 `get_game_creation_agent_capabilities`、`get_limited_local_commands`。 +- 钉住上述字符串的构建期门禁条目与专属测试。 +- 终端 swarm chat 入口与整个命令层:`cli.rs` 的 `SwarmChat` 变体、`--swarm-chat` 解析与派发、`src/swarm_cli.rs` 与 `src/swarm_cli/` 整个目录(输入解析与帮助输出、`/agents`、`/status` 打印器、`/goal` 引擎、`/compact`、`/resume`、`/quit` 退出分支、观察器、报告、turn 派发与等待)、`SwarmChatFlow`、`SwarmTurnObservation`、`SwarmTurnOutcome::Quit`、`SwarmConfirmationResolution::Quit`、`SWARM_TURN_*_ERROR`,以及 `agent.compact` / `agent.resume` / `agent.run_status` 三个只为终端命令存在的权限门禁。 +- 只服务终端交互内核的 `agent/interaction.rs` 整层:`AgentInteractionAction`、tool registry、`game_creator_agent_uses_interaction_kernel`、`decide_game_creator_agent_interaction_turn_for_session_at`、`AgentInteractionProviderStreamSink`;只保留自然语言 steer 决策 LLM 路径 `decide_game_creator_agent_runtime_steer_at`。 +- 只能由命令触达的打印器:`print_runtime_response_stream_status` 及其专属测试。 +- 依赖终端入口的脚本与套件:`scripts/agent-swarm-test-chat.mjs`、`scripts/agent-runtime-deterministic-playable-e2e.mjs`、`scripts/deterministic-lane-defense-provider.mjs`,套件 `user-input`、`supervisor-autonomous-playable-lane-defense`、`supervisor-swarm-autonomous-chat`、`supervisor-swarm-static-isolated-autonomous-chat`、`supervisor-swarm-collaboration-policy-mixed-recovery`,以及 harness 里的交互式 CLI 管道(`startInteractiveCli`、`writeInteractiveCliLine`、`waitForInteractiveCli*`、`closeInteractiveCli`、`activeInteractiveCliSessions`、`answerRemainingInteractiveQuestions` 等)与只服务这些套件的混合套件分支、状态字段、sentinel 与 npm 脚本。 同时清掉只被这些套件调用的 harness 残件:`collaboration-assertions.mjs` 的混合/静态隔离断言族(`supervisorSwarmMixed*`、`observeSupervisorSwarmStaticIsolatedProviderOverlap`)、`repair-recovery.mjs` 的静态隔离观测调用点、evidence 模板与校验里的 `mixed*` / `staticIsolated*` / `initialBatchRecovery*` 占位字段,以及 `self-test.mjs` 中对应的合成用例与汇总字段。 + +范围外(保留): +- 命令 id 注册表 `GAME_CREATION_APP_COMMANDS` 与 `GameCreationAppPermission`(项目权限策略词汇表,Rust 运行期仍在用)。 +- `needsInitializedChatProject` 与项目权限策略链路。 +- Rust 侧路径/路由的 `/` 前缀校验。 +- `--agent-run`、`--agent-enqueue`、`--agent-task`、`--agent-steer`、`--agent-retry`、`--agent-cancel`、`--agent-confirm`、`--agent-resume`、`--agent-runtime-status`、`--agent-goal-*`、`--agent-context-compact`、`--runner-status`、`--runner-shutdown-if-idle`、`--llm-status`、`--preview-serve`、`--environment-check`、`--direct-codex-chat` 等运维与开发 CLI 控制命令。 +- 保留的真实 E2E 套件:`supervisor-swarm`、`supervisor-swarm-transient-retry`、`supervisor-swarm-final-reply-transient-retry`、`supervisor-swarm-tool-plan-handoff-runner-kill`、`goal-runtime`、`response-stream`、`web-search`、`context-compaction`、`scoped-agents`、`project-skill`、`parallel-read`、`steer-runner-kill`、`process-session`。 + +## 检查点 + +1. 斜杠语义层:Direct `/history`、润色绕过、`chatCommandHelp`/`chatCommandMetadata`、`parseRememberInput`、桶文件与门禁、专属测试。 +2. 死链与死模块:草稿回填链(前端 + Rust + 断言)、零调用摘要模块、`agentTrace.ts`。 +3. 无人调用的能力清单 Tauri 命令。 +4. 文档收口:AGC 主专题命令承诺、ADR、`decision-log.md`、`CONTEXT.md`。 +5. 终端 swarm chat 命令层与只服务它的权限门禁、打印器、专属测试。 +6. 终端 swarm chat 入口本体、交互内核、`--swarm-chat` 派发与其专属 Rust 测试。 +7. 依赖终端入口的 e2e 套件、deterministic wrapper/provider、npm 脚本、构建期门禁条目、harness 交互式 CLI 管道、混合套件分支,以及随之失效的混合/静态隔离断言族与 evidence 占位字段。 +8. 文档收口:终端入口退役后主实施计划、Runtime 文档、ADR、decision-log、pitfalls、里程碑计划的最终口径。 + +## 验证 + +- `npx tsc -p apps/ai-game-creator-shell/tsconfig.json --noEmit`:通过;`--noUnusedLocals` 只报一条与本变更无关的既有 `ResolvedAssetImage.tsx` 未使用导入。 +- `npm run --workspace apps/ai-game-creator-shell typecheck`(含 `skill-pack:check` 与 `check-config.mjs`):通过。 +- 定向用例 `npx vitest run tests/appSurface.test.ts`:202 passed / 9 skipped;`chatPromptPolish`、`rememberCommand`、`ChatMarkdownMessage` 定向用例通过。 +- `cargo check --tests`:0 error;告警消息集合与仓库基线一致(A/B 对照,无新增告警消息)。 +- `npm run check:encoding`、`git diff --check`:通过。 +- 定向 `npx vitest run tests/appSurface.test.ts`:通过(`agentSwarmTestEntry.test.ts` 已随脚本删除)。 +- 真实 E2E harness 自检 `node scripts/agent-runtime-real-e2e.mjs --self-test`:通过。 + +## 文档收口 + +- `docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`:删除全部「聊天输入 `/`」能力条目,保留并改写其中的非命令事实;`/compact`、`/mcp`、`/goal`、`/resume` 的终端承诺改为对应 `--agent-*` 入口。 +- `docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md`:删除终端 swarm chat 入口整节,控制面统一记为 `--agent-*` CLI(`/goal` → `--agent-goal-*`、`/compact` → `--agent-context-compact`、`/mcp` → 开发配置面板与真实 E2E 核验);终端不再承担任何交互式聊天职责。 +- `docs/adr/【ADR】退役AGC项目对话斜杠命令与终端swarm chat入口-2026-09-22.md`:本次决策与影响边界。 +- `docs/project-memory/shared-memory/decision-log.md`:新增 2026-09-22 决策条目,并清理已被本次退役取代的历史命令条目。 +- `docs/project-memory/shared-memory/pitfalls.md`、`docs/project-memory/plans/【里程碑】DirectProject聊天真相源收敛-2026-09-16.md`:移除 `/history` 入口表述,改为「显式重新加载对话」。 +- `CONTEXT.md`:新增「项目对话输入」「项目命令 id」术语,把「斜杠命令」标为已退役说法;不再出现 `/history` 重读入口。 +- `docs/README.md`:登记新 ADR。 + +## 未做与边界 + +- 不为退役概念新增守卫测试或断言残留字符串的 check 条目。 +- 不为终端入口与已删套件补充替代实现、兼容别名或迁移提示。 diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 5913f4cfb..26edef05a 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -1,5 +1,14 @@ # 决策记录 +## 2026-09-22 退役 AGC 项目对话斜杠命令与终端 swarm chat 入口 + +- 背景:AGC 项目对话曾把大量能力挂在「聊天输入 `/`」上(`/history`、`/read`、`/help`、`/status`、`/trace`、`/export`、`/preview`、`/remember`、`/brief` 等),无 GUI 的终端 swarm chat 入口 `--swarm-chat` 又自带一套控制命令(`/help`、`/agents`、`/status`、`/history`、`/compact`、`/resume`、`/goal`、`/quit`)。两套入口都没有现役调用方,撤回成本却持续存在:命令字面量散落在前端命令分支、润色绕过、摘要模块、`swarm_cli` 终端输入解析、构建期门禁条目和文档承诺里,任何新对话形态都要额外维护这套死词汇表。 +- 决策:斜杠命令语义与终端 swarm chat 入口整体退役,按「从未存在」处理。应用侧删除 Direct 聊天的 `/history` 精确匹配分支与 `reloadHistory`、`chatPromptPolish` 的 `/` 前缀绕过、`chatCommandMetadata` / `chatCommandHelp` / `memoryCommands` 的命令清单与参数解析、只服务退役 Supervisor 摘要面板的 `project-summary/*Summaries.ts` 与 `agentTrace.ts`、草稿回填死链(前端 `agentPresentation.ts` + Rust `suggested_canvas_tool_call`)、无人调用的 Tauri 命令 `get_game_creation_agent_capabilities` / `get_limited_local_commands`,以及钉住这些字符串的构建期门禁条目与专属测试。终端侧连同入口一并删除:`--swarm-chat`、`src-tauri/src/swarm_cli.rs` 与整个 `swarm_cli/` 目录(命令解析与帮助输出、turn 派发、观察器、报告、专属测试)、`SwarmChatFlow`、`SwarmTurnObservation`、`SwarmTurnOutcome::Quit`、`SwarmConfirmationResolution::Quit`、`SWARM_TURN_*_ERROR`、只服务这些命令的 `agent.compact` / `agent.resume` / `agent.run_status` 权限门禁与 `print_runtime_response_stream_status` 打印器,以及只服务终端交互内核的 `agent/interaction.rs` 整层(`AgentInteractionAction`、tool registry、`game_creator_agent_uses_interaction_kernel`、`decide_game_creator_agent_interaction_turn_for_session_at`、`AgentInteractionProviderStreamSink`);该文件只保留自然语言 steer 决策路径 `decide_game_creator_agent_runtime_steer_at`。真实 E2E 的交互式 CLI 管道、`scripts/agent-swarm-test-chat.mjs`、`agentSwarmTestEntry.test.ts` 与 `agc:test:chat` / `agc:test:chat:manual` / `agc:chat` / `agc:swarm` 等 npm 脚本同步删除。 +- 保留项:命令 id 注册表 `GAME_CREATION_APP_COMMANDS` 与 `GameCreationAppPermission`(项目权限策略词汇表,开发 Agent UI 仍在用)、`needsInitializedChatProject`,以及 `--agent-run` / `--agent-enqueue` / `--agent-steer` / `--agent-resume` / `--agent-context-compact` 等非聊天 CLI 控制命令与 Tauri IPC 注册。 +- 影响范围:`apps/ai-game-creator-shell/src/**`(Direct 聊天控制器、润色、`project-summary`、`project-workspace`)、`src-tauri/src/**`(`cli.rs`、`main.rs`、`swarm_cli` 整目录删除、`agent/interaction.rs` 收敛、命令注册、canvas 生成、provider / project 测试)、`apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/**`、`scripts/check-config.mjs`、root 与 App 的 `package.json` 脚本、`tests/**`,以及 AGC 主实施计划文档、Runtime V1.1 文档与 `CONTEXT.md` 术语。 +- 验证方式:`npx tsc -p apps/ai-game-creator-shell/tsconfig.json --noEmit`、`npm run --workspace apps/ai-game-creator-shell typecheck`(含 `check-config.mjs` 的脚本与门禁一致性)、`npx vitest run apps/ai-game-creator-shell/tests/appSurface.test.ts`、`cargo check --tests`(告警消息集与基线一致)、`npm run check:encoding`、`git diff --check`;保留的 e2e 套件为 `supervisor-swarm`、`-transient-retry`、`-final-reply-transient-retry`、`-tool-plan-handoff-runner-kill`、`goal-runtime`、`response-stream`、`web-search`、`context-compaction`、`scoped-agents`、`project-skill`、`parallel-read`、`steer-runner-kill`、`process-session`。 +- 关联文档:[【ADR】退役AGC项目对话斜杠命令与终端swarm chat入口-2026-09-22](../../adr/【ADR】退役AGC项目对话斜杠命令与终端swarm chat入口-2026-09-22.md)、[【里程碑】退役AGC项目对话斜杠命令与终端swarm chat入口-2026-09-22](../plans/【里程碑】退役AGC项目对话斜杠命令与终端swarm chat入口-2026-09-22.md)、`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`。 + ## 2026-09-21 合并 origin/master:Supervisor 永久退役,策划 V1V2 退役落到当前两条产品路径 - 背景:`refactor/split-direct-project`(DirectProject 独立聊天容器)与 `origin/master`(#355 退役策划 Agent V1/V2)在 2026-09-18 之后各走一条线:本分支删掉 Supervisor 前端链路、把立项策划收敛到 `view/project-development/planning/`,master 删掉整套策划 V1/V2(前端会话 / 审批卡 / 适配器 / 类型与 Rust `planning_*_v2` 命令、`planning_gdd_model.rs`、`planning_policy_v2.rs`、`planning_session_v2.rs`)只保留 Design Agent。两边都在删 Supervisor,冲突集中在 `App.tsx`、聊天视图(`PlanningChatView`、`DirectProjectTurn`、`ToolCallGroup`)、Direct composer / 引用输入区、`styles.css`、Rust direct user item 与 appSurface 用例。 @@ -289,7 +298,7 @@ Godot 编辑器操控复用既有 AGC 插件宿主、EditorAdapter、Runner 和 - 背景:ADR「首屏历史由 `subscribe` 返回的 `lastCompletedItemId` 锚定,再取最近切片」只落了一半。`DirectThreadManager` 是搬运层,内存里没有「已完成条目」的锚点,`subscribe` 一律返回 `last_completed_item_id: None`;`commands.rs` 的 `subscribe_direct_project_thread` 在为空时用 `read_direct_project_last_item_id_at` 从磁盘回填,所以线上回执里的值是真的(订阅那一刻文件里最后一条可显示条目的原始 item id)。前端侧:首屏一直在 `loadProjectConversation` 里用 `beforeItemId: null` 直接取文件尾一屏,`lastCompletedItemId` 自 `1b40f030e` 起不再被任何代码读取。 - 决策(锚点语义):首屏切片的新端(较新一侧)边界就是这个锚点,**含锚点条目本身**;切片命令新增 `throughItemId` 参数表达「取到这条为止」。比锚点更新的条目只从运行态事件来,历史切片与实时流因此不重叠(原来的文件尾读取会把订阅回执之后才完成的条目也拉进历史,与运行态事件同 id 重叠,只靠前端合并兜住)。 -- 决策(读取时机):订阅回执到达之前不读首屏,也不退化成「取文件尾」;锚点缺失(订阅不可用 / 失败 / 历史为空)时才按文件尾取尾屏。`/history` 手动重读保持「按当前文件尾取尾屏」的恢复语义,不锚定。 +- 决策(读取时机):订阅回执到达之前不读首屏,也不退化成「取文件尾」;锚点缺失(订阅不可用 / 失败 / 历史为空)时才按文件尾取尾屏。手动重读保持「按当前文件尾取尾屏」的恢复语义,不锚定。 - 决策(翻页不变):向后翻页仍用切片返回的 `firstItemId` 作 `beforeItemId`(不含锚点),`hasMore` 与连拉口径不变。 - 影响范围:`agent/direct_project_history.rs`(切片锚点 + `through_item_id` 参数)、`commands.rs`(`read_direct_project_history_slice` 命令参数)、AGC 前端首屏读取接线与测试骨架。**未改**:DirectRuntime 的 `turn-stream.jsonl` / `tool-calls.jsonl` 写入与进度事件、`list_game_creator_direct_active_turns`、SpacetimeDB 与 HTTP 契约。 - 验证方式(已跑):Rust 侧 `cargo test agent::direct_project_history`(22 passed,含「窗口取到锚点那条、排除比锚点更新的条目、`beforeItemId` 与 `throughItemId` 互斥报错」三类用例);前端 `npx vitest run .../directHistoryAnchorGate.test.ts`(10 passed)与 appSurface 的 `anchors the first history page at the subscribe receipt instead of the file tail`(全量 475 tests / 457 passed / 17 skipped;唯一失败 `edits the published runtime config without leaking API keys into chat` 与本次改动无关,stash 掉本次前端改动后同样变红);`tsc` / ESLint / prettier / `check:encoding` / `check:doc-index` / `git diff --check` 全绿。变异验证:闸门忽略「已消费」、首屏不等闸门两处改动各自让对应用例变红。 @@ -2300,7 +2309,7 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在 - 2026-07-12 补充,2026-07-27 更正:OpenAI Chat / Responses 的后台 Agent 工具 planning 改用唯一 `submit_agent_tool_plan` 原生 function tool,字符串 `tool_choice=required` 和 strict schema;只接受恰好一次同名调用,arguments 继续经过本地计划 schema、工具白名单和权限策略校验,错误函数、多调用或非法 arguments 进入原有两次格式修复预算且不产生副作用。本条原写「Anthropic 保留文本 JSON 回退;planning 非流式」,已由 2026-07-27「Anthropic 与流式统一使用 Provider 原生工具」取代——Anthropic 同样发送原生工具目录,planning 不再因协议强制非流式。每轮成功协议写 `agent.runtime.tool_plan.protocol`,修复审计记录 protocol、callId 和 functionName。 - 2026-07-10 补充:后台 Agent Runtime 的白名单工具继续扩到 `preview.start`,让 Agent 在完成写盘或静态自检后能按策略自行启动当前项目的 `127.0.0.1` 本地 HTTP 预览。该工具复用 `preview.start` 权限策略、项目写锁、共享 `PreviewRegistry`、manifest 预览状态、`.agent/logs/preview.log` 和 run trace 追加逻辑;写入 `.agent/agent.db` 的审计类型为 `agent.runtime.preview.start`。发给 LLM 的 observation 只包含 localhost URL 和端口,不包含用户项目绝对路径。 - 2026-07-10 补充:后台 Agent Runtime 的白名单工具继续扩到 `canvas.asset_generate`,让美术类 Agent 可在 loop 中自行请求生成首版美术素材。该工具读取 AppData / Tauri 配置中的 `editorApi`,复用 `canvas.asset_generate` 权限策略、项目写锁、External Editor API 生成和下载链路、manifest 资产登记以及 `canvas.asset_generate` 本地索引记录;另写 `agent.runtime.canvas.asset_generate` 记录到 `.agent/agent.db`,标明触发的 agent 与本地素材路径。API Key 不进入 prompt observation、manifest、agent.db 或日志;策略要求确认或拒绝时不会调用外部 API。 -- 补充:规范 Agent ID 统一使用 manifest taskId,例如 `art-asset-plan` 和 `code-prototype`;历史前端曾使用的 `group-role` 别名只在 Tauri command 层兼容并映射到规范 taskId。主窗口 Agent 状态列表通过 `read_game_creator_agent_runtimes` 批量读取 `.agent/runtime/agents/.json` 和最近任务,把每个 Agent 的 Runtime 状态、当前动作和最近 task 直接显示在状态卡片和 `/agents` 汇总里。 +- 补充:规范 Agent ID 统一使用 manifest taskId,例如 `art-asset-plan` 和 `code-prototype`;历史前端曾使用的 `group-role` 别名只在 Tauri command 层兼容并映射到规范 taskId。主窗口 Agent 状态列表通过 `read_game_creator_agent_runtimes` 批量读取 `.agent/runtime/agents/.json` 和最近任务,把每个 Agent 的 Runtime 状态、当前动作和最近 task 直接显示在状态卡片和 Agent 汇总里。 - 2026-07-10 补充:单 Agent 聊天和后台 planning prompt 统一注入本 Agent 的 Runtime 连续上下文,包括最近状态、runId、当前任务、计划、观察、最近回复、最近工具动作、最近事件、最近任务和工具策略摘要;上下文只按规范 taskId 读取本 Agent runtime,进入 prompt 前过滤密钥和本机绝对路径。新后台 run 启动时继承同 Agent 上次 `recentToolCalls` 和 `lastResponse`,让下一轮任务能基于前一轮真实行动证据继续推理,同时不串入其他 Agent 的 runtime。 - 2026-07-10 补充:后台 Agent Runtime 的白名单工具继续扩到 `file.list`,让 Agent 可先列出项目文件摘要或某个相对目录下的条目,再决定是否读取具体文件或继续行动。该工具复用 `file.list` 项目权限策略,策略要求确认或拒绝时不会枚举项目文件;observation 只包含项目相对路径、类型和大小,不读取文件内容、不返回项目绝对路径。 - 2026-07-10 补充:后台 Agent Runtime 的白名单工具继续扩到 `project.diff`,让 Agent 可基于已存在 checkpoint 观察本地项目新增、修改和删除摘要。该工具复用 `project.diff` 项目权限策略,策略要求确认或拒绝时不会执行 diff;observation 只包含 checkpoint id、三类计数和项目相对路径,不返回本机绝对路径或文件正文。 @@ -2329,18 +2338,18 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在 ## 2026-07-03 AI 游戏创作 App 本地试玩包导出只打包运行白名单 - 背景:AI 游戏创作 App 需要给普通用户提供首版本地试玩包,但不能把项目记忆、trace、日志、运行时配置或密钥类文件混入可分发 ZIP。 -- 决策:v1 新增 `/export` 聊天入口和 `project.export_package` 确认命令。导出前重新校验 `game/index.html` 是可试玩自包含 HTML;ZIP 只包含 `game/**`、`assets/**` 和 `exports/README.md`,输出到 `exports/playtest-package-*.zip`;导出拒绝符号链接和不安全条目路径,并写入 manifest `commandRuns`、`.agent/logs/command.log` 和 `.agent/agent.db`。 -- 补充:新增 `/exports` 只读聊天入口和 `project.export_list` 自动命令,用于列出当前项目 `exports/playtest-package-*.zip` 历史试玩包;该入口只读、不删除旧包、不做系统分享,给用户继续 `/export` 或显示目录的草稿。 -- 影响范围:`apps/ai-game-creator-shell` 的聊天命令、Tauri 本地项目能力、共享命令契约和 AI 游戏创作 App 实施计划。 +- 决策:v1 新增 `project.export_package` 确认命令。导出前重新校验 `game/index.html` 是可试玩自包含 HTML;ZIP 只包含 `game/**`、`assets/**` 和 `exports/README.md`,输出到 `exports/playtest-package-*.zip`;导出拒绝符号链接和不安全条目路径,并写入 manifest `commandRuns`、`.agent/logs/command.log` 和 `.agent/agent.db`。 +- 补充:新增 `project.export_list` 只读自动命令,用于列出当前项目 `exports/playtest-package-*.zip` 历史试玩包;该入口只读、不删除旧包、不做系统分享。 +- 影响范围:`apps/ai-game-creator-shell` 的 Tauri 本地项目能力、共享命令契约和 AI 游戏创作 App 实施计划。 - 验证方式:运行 AI 游戏创作壳主窗口 smoke、Tauri `export` 定向测试、共享契约测试、类型检查、编码检查和 `git diff --check`。 - 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`。 ## 2026-07-01 AI 游戏创作 App v1 使用本地 JSONL 对话和派生 Agent 状态 - 背景:AI 游戏创作 App 已有 Godcoder 式本地工程护栏、项目黑板、角色私有记忆、manifest 和 run trace;新增结构化对话记录、agent 状态列表和单 agent 对话入口时,需要避免引入平行状态源或提前承诺后台 runner 能力。 -- 决策:v1 结构化对话记录统一使用本地 `.agent/conversations/` append-only JSONL。普通聊天写 `.agent/conversations/project.jsonl`;从 agent 状态列表进入单个 agent 后,用户消息、agent 回复、工具建议和错误只写对应 `.agent/conversations/agents/.jsonl`。Agent 状态列表从 `.agent/manifest.json` 的任务 / 角色清单和 `.agent/run.latest.json` / `.agent/runs/.json` 的 step、taskGraph、passPlans、lifecycleStatus 派生,并把 `taskGraph.tasks` 的任务状态与 active / carry-over / ready 编排标记显示在主窗口和单 agent 对话入口中;单 agent 最近证据里的安全相对输入 / 输出路径只填入 `/read ` 草稿,仍由用户发送并走既有 `file.read` / `agent.trace_read` 权限流。不新增独立状态数据库。项目黑板和角色私有记忆继续只保存稳定摘要,不承载原始对话流水。 +- 决策:v1 结构化对话记录统一使用本地 `.agent/conversations/` append-only JSONL。普通聊天写 `.agent/conversations/project.jsonl`;从 agent 状态列表进入单个 agent 后,用户消息、agent 回复、工具建议和错误只写对应 `.agent/conversations/agents/.jsonl`。Agent 状态列表从 `.agent/manifest.json` 的任务 / 角色清单和 `.agent/run.latest.json` / `.agent/runs/.json` 的 step、taskGraph、passPlans、lifecycleStatus 派生,并把 `taskGraph.tasks` 的任务状态与 active / carry-over / ready 编排标记显示在主窗口和单 agent 对话入口中;单 agent 最近证据里的安全相对输入 / 输出路径仍由既有 `file.read` / `agent.trace_read` 权限流处理。不新增独立状态数据库。项目黑板和角色私有记忆继续只保存稳定摘要,不承载原始对话流水。 - 补充:2026-07-08 起普通用户入口改为单窗口客户端首页;旧独立启动器 / 主窗口切换口径废止。首页发送需求或项目组新建项目时,先选择目录并在非空目录时二次确认,初始化成功后写最近项目并切到项目开发占位;取消或初始化失败则不切换视图、不写最近项目。最近工作区只保存在本机 WebView storage,可单项移除或清空,不进入项目文件或共享记忆;已初始化项目优先显示 manifest 项目名并保留路径副信息,`.agent/run.latest.json` 可读时显示最近 run 状态。最近项目路径缺失、不是目录、缺少可读 `.agent/manifest.json` 或检查失败时禁用打开,刷新只重新执行只读检查;“显示”只用系统文件管理器打开已确认存在的本地目录,未初始化但存在的目录也可显示,避免把历史路径误当新项目重建。 -- 补充:项目开发占位“显示目录”复用同一只读目录打开能力,只打开当前本地项目目录,不初始化项目、不写项目文件、不切换工作区;顶部只读显示 manifest 项目名、项目路径、最近 `.agent/run.latest.json` 的 run 状态摘要和当前预览状态,并通过“刷新状态”重新读取同一 trace,不新增状态数据库。最近项目资产入口只读展示 localPath、kind、mediaType 和 source.kind,点击仍走原 `file.read` 权限流;旁边的“读取命令”只填入 `/read ` 草稿,不直接读取文件或绕过权限。项目开发占位里的项目黑板和 Agent 状态快捷入口仍只填入聊天草稿,不直接读取 run 辅助文件、不写 `.agent/policy.json`、不调用 LLM。 +- 补充:项目开发占位“显示目录”复用同一只读目录打开能力,只打开当前本地项目目录,不初始化项目、不写项目文件、不切换工作区;顶部只读显示 manifest 项目名、项目路径、最近 `.agent/run.latest.json` 的 run 状态摘要和当前预览状态,并通过“刷新状态”重新读取同一 trace,不新增状态数据库。最近项目资产入口只读展示 localPath、kind、mediaType 和 source.kind,读取仍走原 `file.read` 权限流,不直接读取文件或绕过权限。 - 补充:首页、项目组和项目开发占位共用同一个运行时配置弹窗,配置只读写 Tauri 应用配置目录中的 `game-creator.config.json`,不写入项目文件或对话历史。 - 补充:项目组“打开”只进入已初始化且 `.agent/manifest.json` 可读的 AI 游戏项目;路径不存在、不是文件夹或只是普通文件夹时不切换到项目开发占位、不创建目录,用户需要创建或初始化时走“新建项目”。 - 影响范围:`apps/ai-game-creator-shell` 的主窗口 agent 状态列表、单 agent 对话入口、本地项目文件结构、共享契约和 AI 游戏创作 App 实施计划。 @@ -2350,7 +2359,7 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在 ## 2026-06-30 AI 游戏创作 App 使用客户端配置文件 - 背景:`apps/ai-game-creator-shell` 是客户端 App,不应通过 `.env` 或进程环境变量承载 LLM / 画板同步配置;旧口径会让本地 secrets、CLI wrapper 和桌面 App 启动逻辑混在一起。 -- 决策:仓库内 `apps/ai-game-creator-shell/game-creator.config.json` 只作为默认模板;发布 App 启动时在 Tauri 应用配置目录写入默认 `game-creator.config.json`,真实密钥和本机覆盖项都保存在该运行时配置文件中。主窗口提供“配置”面板读写该运行时 JSON;开发 CLI 无 AppHandle 时才回退读取仓库旁边的模板和 gitignored 本机覆盖文件。`llm.apiKey/baseUrl/model/apiKind/stream/requestTimeoutMs/maxRetries/retryBackoffMs` 驱动全局 LLM 路径,`agentLlm.` 可为 Planner、Generator 和角色 agent 单独覆盖 API Key、base URL、模型、API 类型和流式请求,空项继承全局配置;`editorApi.baseUrl/apiKey` 驱动画板项目同步;`/llm-status` 只展示全局和各 agent resolved 后的 baseUrl、model、apiKind、stream 和 API Key 是否存在,不显示密钥;`/llm-routes` 复用同一只读检查结果,按 agent 展示 resolved provider 路由、单独路由数量和缺口数量,不请求上游、不显示密钥、不写项目。生成游戏或平台美术遇到 LLM / editorApi 缺配置错误时,主窗口自动打开运行时配置弹窗,但错误消息仍只显示缺失项,不回显密钥值。 +- 决策:仓库内 `apps/ai-game-creator-shell/game-creator.config.json` 只作为默认模板;发布 App 启动时在 Tauri 应用配置目录写入默认 `game-creator.config.json`,真实密钥和本机覆盖项都保存在该运行时配置文件中。主窗口提供“配置”面板读写该运行时 JSON;开发 CLI 无 AppHandle 时才回退读取仓库旁边的模板和 gitignored 本机覆盖文件。`llm.apiKey/baseUrl/model/apiKind/stream/requestTimeoutMs/maxRetries/retryBackoffMs` 驱动全局 LLM 路径,`agentLlm.` 可为 Planner、Generator 和角色 agent 单独覆盖 API Key、base URL、模型、API 类型和流式请求,空项继承全局配置;`editorApi.baseUrl/apiKey` 驱动画板项目同步;主窗口运行时配置面板与生成入口只展示全局和各 agent resolved 后的 baseUrl、model、apiKind、stream 和 API Key 是否存在,不显示密钥、不请求上游、不写项目。生成游戏或平台美术遇到 LLM / editorApi 缺配置错误时,主窗口自动打开运行时配置弹窗,但错误消息仍只显示缺失项,不回显密钥值。 - 影响范围:AI 游戏创作 App 的 Tauri Rust 配置加载、主窗口配置面板、CLI wrapper、agent-run smoke、`check-config` 门禁、`.gitignore` 和实施计划文档。 - 验证方式:运行 `npm run ai-game-creator-shell:typecheck`、`cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml`、`npm run check:encoding` 和 `git diff --check`。 - 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`。 @@ -2462,16 +2471,16 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在 ## 2026-06-26 AI 游戏创作 App 生成过程必须在聊天可见 - 背景:普通用户窗口只保留聊天入口,但如果生成确认后只显示“已生成草案”和本地产物路径,真实 LLM / Agent loop 会被误解成固定模板落盘。 -- 决策:`game.generate_draft` 保持正式用户窗口不展示开发面板,但必须通过聊天实时显示 Planner LLM、Orchestrator、6 组角色 brief、Generator LLM、Evaluator、ArtifactWriter 和自检进度;生成完成后普通聊天消息直接展示 `.agent/run.latest.json` 的 Run、LLM 对话、loop 轮次、active / carry-over 任务、编排轮次、最近步骤、建议命令和本地产物快照;没有同步建议命令时,首个安全产物只提供 `/read` 草稿,`/trace` 继续读取同一份完整证据。 +- 决策:`game.generate_draft` 保持正式用户窗口不展示开发面板,但必须通过聊天实时显示 Planner LLM、Orchestrator、6 组角色 brief、Generator LLM、Evaluator、ArtifactWriter 和自检进度;生成完成后普通聊天消息直接展示 `.agent/run.latest.json` 的 Run、LLM 对话、loop 轮次、active / carry-over 任务、编排轮次、最近步骤、建议命令和本地产物快照;产物证据仍从同一份 run trace 读取。 - 影响范围:`apps/ai-game-creator-shell/src/App.tsx`、`apps/ai-game-creator-shell/src-tauri/src/main.rs`、AI 游戏创作 App 聊天体验和实施计划文档。 - 验证方式:运行 `npm run ai-game-creator-shell:check`、`npm run check:encoding` 和 `git diff --check`。 - 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`。 ## 2026-06-26 AI 游戏创作 App 增加显式质量评审 Gate -- 背景:AI 游戏创作 App 已有 Evaluator loop 和静态 smoke,但任务图、能力清单和 trace 中没有单独的质检 / 评审任务,用户无法从 `/tasks`、`/trace` 或 `/audit` 看出质量评审是明确环节。 +- 背景:AI 游戏创作 App 已有 Evaluator loop 和静态 smoke,但任务图、能力清单和 trace 中没有单独的质检 / 评审任务,用户无法从任务图、run trace 或审计摘要看出质量评审是明确环节。 - 决策:保持策划、美术、程序、数值、音乐、运营 6 个专业组不变,在程序组内新增 `quality-review` / `Review` 角色任务;Evaluator 的评审 step 绑定到该任务,依赖顺序为 `code-prototype -> quality-review -> preview-readiness -> preview-playtest -> publish-strategy -> publish-package`。`game.static_smoke` 只完成 `preview-readiness`,不代替质量评审。 -- 影响范围:AI 游戏创作 App 任务图、共享契约、Tauri trace / manifest 状态推导、聊天 `/capabilities` `/tasks` `/trace` `/audit` 摘要和实施计划文档。 +- 影响范围:AI 游戏创作 App 任务图、共享契约、Tauri trace / manifest 状态推导、任务图 / trace 摘要和实施计划文档。 - 验证方式:运行 `npm run ai-game-creator-shell:check`、`npm run check:encoding` 和 `git diff --check`。 - 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`。 @@ -2489,7 +2498,7 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在 - 背景:AI 游戏创作 App 的 `game.generate_draft` 已接入 LLM,但单次请求仍不能体现 Planner / Generator / Evaluator 的协作闭环,也无法把评估反馈作为下一轮生成输入。 - 决策:v1 使用最小文件驱动 loop,不引入 LangChain、AutoGen、Microsoft Agent Framework 或 OpenAI Agents SDK sidecar。Planner 写 `.agent/spec.md`;每轮先调用策划、数值、美术、音乐、程序、运营 6 组下的 15 个角色 agent,角色 brief 写到 `.agent/passes/pass-N/groups//*.md`,再由 `GroupCoordinator` 汇总到 `.agent/passes/pass-N/groups/*.md`;Generator 读取 spec、`.agent/findings.md` 和 6 组汇总 brief 生成结构化游戏草案。LLM JSON 必须带 `handoffs` 数组并覆盖 `design`、`balance`、`art`、`audio`、`code`、`publishing` 6 个专业组;每轮再把这些结构化交接快照写到 `.agent/passes/pass-N/`。Evaluator 做本地静态验收并写 `.agent/findings.md`,最多 3 轮;返工轮必须把 findings 转成结构化 `repairRoutes`,记录每条问题命中的 taskIds 和 reason,再据此选择 activeTaskIds。每次运行另写 `.agent/run.latest.json` 和 `.agent/runs/.json`,记录 step、角色级 `toolCalls`、组汇总、专业组交接、输入输出路径、artifact 字节数与 `fnv1a64:` checksum;每个 step 带 phase、taskId、group 和 role,trace 顶层 `taskGraph` 记录 goal、readyTaskIds、activeTaskIds、carriedTaskIds、repairFocus、repairRoutes 和当前任务状态,`passPlans` 逐轮记录 mode、summary、activeTaskIds、carriedTaskIds、dependencyWaves、repairFocus 和 repairRoutes;latest 是当前指针,runs 目录保留历史 trace,作为开发窗口和后续工具调用 trace 的事实源,schema 由共享 TS/Rust 契约 `game-creator-agent-run.v1` 固定。最终产物写盘时追加 `ArtifactWriter / file.write.local_artifacts` step,随后自动跑白名单 `game.static_smoke`,检查 `game/index.html` 具备 canvas、canvas 渲染上下文、绘制调用、主循环、输入监听、明确目标、失败或胜利状态和重开路径,且不使用远程资源、`eval`、`new Function`、`localStorage`、`fetch`、`WebSocket` 或 `ServiceWorker`,再把 Playtest 工具调用写回 trace;通过后把 runId、状态、轮次、下一步、active / carry-over 任务和最终本地产物摘要追加到 `memory/session.md` 与 `memory/project.md`,让下一次 Planner / 角色 agent / Generator 从记忆输入直接看到上一轮稳定原型;后续 `preview.start` 会在已有 trace 上追加 Preview 工具调用和本地预览 URL。 -- 决策补充:普通用户聊天 `/trace` 读取同一份 `.agent/run.latest.json`,但摘要必须把 activeTaskIds、carriedTaskIds、repairRoutes 和 dependencyWaves 从内部 taskId 映射成专业组 / 角色 / 任务名,确保不打开开发窗口也能看出 6 组 agent、组内角色、返工路线和 carry-over 真实发生。 +- 决策补充:run trace 摘要读取同一份 `.agent/run.latest.json`,但摘要必须把 activeTaskIds、carriedTaskIds、repairRoutes 和 dependencyWaves 从内部 taskId 映射成专业组 / 角色 / 任务名,确保不打开开发窗口也能看出 6 组 agent、组内角色、返工路线和 carry-over 真实发生。 - 影响范围:`apps/ai-game-creator-shell/src-tauri/src/main.rs`、`packages/shared/src/contracts/gameCreationApp.ts`、`server-rs/crates/shared-contracts/src/game_creation_app.rs` 和 AI 游戏创作智能体 App 实施计划。 - 验证方式:运行 AI 游戏创作壳 Rust 测试、共享契约 TS/Rust 测试、壳 typecheck、编码检查和 `git diff --check`。 - 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`。 @@ -2497,7 +2506,7 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在 ## 2026-06-24 AI 游戏创作 App 编排 v1 使用 ready-task 选择器 - 背景:AI 游戏创作 App 已有专业组任务拆分和依赖字段,但如果没有当前可执行任务选择器,“任务编排”只停留在静态清单,普通用户在聊天里也看不到下一步由哪组 agent 接手。 -- 决策:v1 编排先使用最小 ready-task 规则:只选择 `pending` 且所有依赖任务均为 `completed` 的任务;共享 TS/Rust 契约和 `platform-agent` 都提供同一语义的选择器,聊天 `/tasks` 只展示下一步可执行专业组,不新增独立编排面板或外部 agent 框架。 +- 决策:v1 编排先使用最小 ready-task 规则:只选择 `pending` 且所有依赖任务均为 `completed` 的任务;共享 TS/Rust 契约和 `platform-agent` 都提供同一语义的选择器,任务摘要只展示下一步可执行专业组,不新增独立编排面板或外部 agent 框架。 - 影响范围:`packages/shared/src/contracts/gameCreationApp.ts`、`server-rs/crates/shared-contracts/src/game_creation_app.rs`、`server-rs/crates/platform-agent/src/game_creation.rs`、`apps/ai-game-creator-shell/src/App.tsx` 和 AI 游戏创作智能体 App 实施计划。 - 验证方式:运行共享契约测试、`platform-agent` 与 `shared-contracts` 的 Rust 测试、AI 游戏创作壳 typecheck、`npm run check:encoding` 和 `git diff --check`。 - 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`。 @@ -6285,88 +6294,19 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在 - 2026-06-25 调整:新增 `npm run ai-game-creator-shell:agent-run:smoke` 作为无密钥开发验证入口。脚本在本机启动 OpenAI-compatible 测试 provider,预置一个本地上传图片和一个本地上传音频,并复用真实 `--agent-run`、本地落盘、`game.static_smoke` 和本地 HTTP 预览;脚本会断言 provider 请求体包含图片与音频资产上下文、生成 HTML 引用 `/assets/...`、预览服务能用 `GET` 读取这些资产、用 `HEAD` 返回真实资源长度和对应 MIME、headless Chrome 打开预览后至少执行一帧游戏 JS,且通过确定性亮色探针采样证明 canvas 不是空白画布、第二轮重跑 Evaluator 命中任务及其下游影响任务,未受影响组 carry-over,再自动给 CLI 发送回车停止预览。该脚本仅验证 runtime,不作为产品生成 fallback。 - 2026-06-25 调整:新增根级 `npm run ai-game-creator-shell:check` 作为 v1 开发验收入口,串起壳 typecheck、`platform-agent` 编排测试、`shared-contracts` 契约测试、Tauri Rust 测试和无密钥本地 provider 端到端 smoke,避免测试口径散落成多条手工命令。 - 2026-06-25 调整:`scripts/check-native-shells.mjs` 的 AI 游戏创作项从单独 typecheck 升级为 `npm run ai-game-creator-shell:check`,让原生壳总门禁覆盖 agent loop、本地落盘、静态自检和本地 HTTP 预览 smoke。 -- 2026-06-25 调整,2026-06-30 更新:普通用户通过聊天输入 `/llm-status` 触发只读 `llm.config_check`,用于检查 LLM base_url、model 和 API Key 是否已从客户端配置读取;状态消息不得显示或保存 API Key。终端可用 `npm run ai-game-creator-shell:llm-status` 做同类配置自检,缺配置时以非零状态退出。发布 App 的真实密钥只放 Tauri 应用配置目录中的 `game-creator.config.json`;主窗口“配置”面板可读写该文件,但 API Key 不写入聊天、本地项目、trace 或 manifest。 - 2026-06-25 调整:`npm run ai-game-creator-shell:dev` 固定加载 `http://127.0.0.1:3080/`,Vite 继续 `strictPort` 与 Tauri `devUrl` 对齐。`beforeDevCommand` 改为先复用已经跑在 3080 且页面标题为 `AI 游戏创作` 的本 app Vite server,避免上次 Tauri 退出后遗留的同 app Vite 进程导致二次启动失败;如果 3080 是其它服务,仍直接失败并要求释放端口,不做端口漂移。 - 2026-06-25 调整:`preview.start` / `preview.stop` 必须追加 `.agent/logs/preview.log`,并把该日志列入 Preview trace step 的输出路径和 artifact 清单;这样 `preview-playtest` 任务声明的日志产物与实际本地 HTTP 预览行为一致。 - 2026-06-25 调整:AI 游戏创作 App v1 仍只维护一个全局本地 HTTP 预览实例;启动新项目预览替换旧预览时,必须 best-effort 把旧项目的 manifest preview 状态、`.agent/logs/preview.log` 和 run trace 记录为 stopped,避免旧项目状态残留 `running`。旧项目目录已删除时不阻断新预览启动。 -- 2026-06-25 调整,2026-07-18 替代:正式用户 App 的项目运行工作台承载当前授权项目的本地游戏预览,release / dev CSP 都只允许 `frame-src http://127.0.0.1:*`;`/preview`、`/run` 和生成完成后的用户侧路径启动 `127.0.0.1` HTTP preview 后直接切换客户端运行视图,不再调用系统外部浏览器。 -- 2026-06-25 调整:`project.create` 成功后的 durable 权限证据必须在聊天 `/project` 和开发窗口初始化两条入口统一写入 `.agent/logs/command.log`,避免同一能力因为入口不同导致 `/audit` 或开发排障证据不一致。 +- 2026-06-25 调整,2026-07-18 替代:正式用户 App 的项目运行工作台承载当前授权项目的本地游戏预览,release / dev CSP 都只允许 `frame-src http://127.0.0.1:*`;生成完成后的用户侧路径启动 `127.0.0.1` HTTP preview 后直接切换客户端运行视图,不再调用系统外部浏览器。 +- 2026-06-25 调整:`project.create` 成功后的 durable 权限证据必须在开发窗口初始化入口写入 `.agent/logs/command.log`,避免同一能力因为入口不同导致审计或开发排障证据不一致。 - 2026-06-25 调整:`.agent/run.latest.json` 和 `.agent/runs/.json` 必须记录 loop 的 `maxPasses` 与 `stopReason`,开发窗口直接展示该状态,避免只从 summary 文案推断 loop 是否跑满、通过、返工、写入产物或进入预览。本地 HTTP 预览的 `/` 映射到 `game/index.html`,路径解析必须 canonicalize 项目根目录和目标文件,只允许访问项目内 `game/` 与 `assets/`,拒绝 `memory/`、`.agent/`、`exports/`、`..`、反斜杠和符号链接越界;常见图片、音频、视频和 Web 资源必须返回对应 MIME。这样上传和画板回流资产能被生成游戏引用,但记忆、trace 和导出包不会被预览服务暴露。 -- 2026-06-26 调整,2026-07-03 更新:AI 游戏创作 App 借鉴 Harbour 的控制平面思想,但不搬 Harbour 后台。最近 run 在 `.agent/run.latest.json` 增加可选 `lifecycleStatus`,并通过 `/agent-status`、`/agent-kill`、`/agent-retry`、`/agent-resume [说明]` 控制本地生命周期,写入 `.agent/activity.jsonl`、`.agent/output.jsonl` 和 `.agent/context.bundle.json`;聊天里的状态 / 控制结果可填入 `/read .agent/output.jsonl` 草稿继续查看 run 输出,但不直接读取文件或绕过 `file.read` 策略。v1 的 kill/retry/resume 只更新本地状态和上下文包,不伪装成能中断已发出的上游 LLM 请求;后续引入独立 runner 后再把 `pending` 接入 claim。 -- 2026-07-03 调整:主窗口 Agent 状态栏新增“继续说明”,只把 `/agent-resume ` 填入聊天输入框,让用户补充说明后再走原确认流;策略快捷入口新增 project.index、asset.register、memory.write、preview.open、preview.stop、conversation.read 和 conversation.write 确认草稿,同样只填输入框,不直接写 `.agent/policy.json`。 +- 2026-06-26 调整,2026-07-03 更新:AI 游戏创作 App 借鉴 Harbour 的控制平面思想,但不搬 Harbour 后台。最近 run 在 `.agent/run.latest.json` 增加可选 `lifecycleStatus`,并通过 Runtime 状态控制动作维护本地生命周期,写入 `.agent/activity.jsonl`、`.agent/output.jsonl` 和 `.agent/context.bundle.json`。v1 的 kill/retry/resume 只更新本地状态和上下文包,不伪装成能中断已发出的上游 LLM 请求;后续引入独立 runner 后再把 `pending` 接入 claim。 - 2026-07-03 调整:主窗口 header 常驻项目摘要只从当前已加载的 manifest / trace 派生任务完成数、ready 数、资产来源分布和最近命令结果;未选择工作区时不显示,不为了摘要额外触发 Tauri 读取或写入,也不把任务、文件、run history 或预览开发面板搬进普通用户窗口。 -- 2026-07-03 调整:普通用户通过聊天输入 `/brief` 触发项目简报入口,只基于主窗口当前已加载的 manifest、最近 run trace、预览状态、资产数量和最近命令生成聊天内简报,并提供 `/next` 作为后续草稿;该入口不得触发 Tauri 读写、不得读取文件、不得启动或打开预览,也不得新增普通用户面板。 -- 2026-07-03 调整:普通用户通过聊天输入 `/goal` 查看创作目标,只基于当前 manifest.goal、最近 run goal 和 taskGraph.goal 汇总项目目标来源,并提供 `/agent-resume 细化目标:` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得读取 spec、上下文或 trace 文件,也不得新增普通用户目标面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/guide` 查看操作导引,只基于当前 manifest、最近 run trace、preview 和已加载命令状态判断未开始、需修复、可预览、可导出或已导出阶段,给出最多 3 个推荐命令和首选草稿;该入口不得触发 Tauri 读写、不得读取文件、不得启动 run、不得启动预览、不得写项目,也不得新增普通用户导引面板。`/guide` 只回答“下一步怎么操作”,不承接 `/brief` 的项目快照、`/mvp` 的最小范围或 `/plan` 的分工计划。 -- 2026-07-04 调整:普通用户通过聊天输入 `/progress` 查看项目进度,只基于当前 manifest、最近 run trace、preview、任务、素材和已加载命令状态汇总项目阶段、任务完成度、最近 run、预览、素材和交付进度,并提供 `/run`、`/review`、`/share`、`/test-plan`、`/todo`、`/trace` 或 `/guide` 草稿;该入口不得触发 Tauri 读写、不得读取文件、不得启动 run、不得启动预览、不得导出试玩包、不得写项目,也不得新增普通用户进度面板。`/progress` 只回答“当前走到哪了”,不承接 `/status` 的项目状态详情、`/ready` 的试玩门槛判断、`/groups` 的逐组进度或 `/next` 的长命令目录。 -- 2026-07-04 调整:普通用户通过聊天输入 `/spec` 查看创作规格包,只基于当前 manifest、最近 run trace、任务声明产物和 trace 输入 / 输出路径汇总 Planner 规格、玩法设计、数值表、美术清单、音频清单和发布说明状态,并提供 `/read .agent/spec.md` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得读取规格文件、不得启动预览、不得写项目,也不得新增普通用户规格面板。 -- 2026-07-03 调整:普通用户通过聊天输入 `/mvp` 查看本轮最小可玩范围,只基于当前 manifest、最近 run trace、preview、任务、资产和最近命令汇总 MVP 内、当前状态、试玩包状态和暂不做事项,并提供 `/review`、`/criteria`、`/trace`、`/run`、`/export`、`/exports` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得读取文件、不得启动或打开预览、不得导出试玩包,也不得新增普通用户 MVP 面板。 -- 2026-07-03 调整:普通用户通过聊天输入 `/pitch` 查看试玩定位与卖点,只基于当前 manifest、最近 run trace 和 preview 状态汇总试玩定位、一句话、核心乐趣、当前可演示状态、测试者讲解口径和暂不承诺事项,并提供 `/mvp`、`/review`、`/trace`、`/open-preview` 或 `/run` 草稿;该入口不得触发 Tauri 读写、不得读取文件、不得启动或打开预览、不得直接继续 run,也不得新增普通用户定位面板。该入口服务试玩讲解,不承接 `/listing` 的作品页包装。 -- 2026-07-04 调整:普通用户通过聊天输入 `/demo` 准备 30 秒试玩讲解稿,只基于当前 manifest、最近 run trace 和 preview 状态汇总开场、讲解顺序、口播稿、演示状态、最近试玩证据和收反馈口径,并提供 `/run`、`/open-preview`、`/trace`、`/review` 或 `/test-plan` 草稿;该入口不得触发 Tauri 读写、不得读取文件、不得启动或打开预览、不得导出试玩包、不得发布作品,也不得新增普通用户讲解面板。 -- 2026-07-03 调整:普通用户通过聊天输入 `/rules` 查看玩法操作与规则,只基于当前 manifest、最近 run trace.taskGraph、trace artifacts 和 steps 汇总玩法目标、操作 / 胜负 / 重开口径、设计与入口产物状态、相关任务和最近程序 / 试玩步骤,并提供 `/read game/game_design.md`、`/agent-resume 操作说明:...` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得读取设计文件、不得启动预览或继续 run,也不得新增普通用户规则面板。 -- 2026-07-03 调整:普通用户通过聊天输入 `/tutorial` 查看新手引导检查,只基于当前 manifest、最近 run trace、preview 和任务状态汇总首屏目标、首局 30 秒引导、原型证据、试玩任务、最近引导证据和补齐项,并提供 `/rules`、`/review`、`/agent-resume 新手引导:...`、`/open-preview` 或 `/run` 草稿;该入口不得触发 Tauri 读写、不得读取设计文件、不得启动或打开预览、不得直接继续 run,也不得新增普通用户引导面板。 -- 2026-07-03 调整:普通用户通过聊天输入 `/mobile` 查看移动试玩检查,只基于当前 manifest、最近 run trace、preview 和任务状态汇总移动试玩目标、键盘 / 触屏输入口径、原型证据、移动检查项、关联任务和最近移动相关步骤,并提供 `/rules`、`/review`、`/agent-resume 移动试玩:...`、`/open-preview` 或 `/run` 草稿;该入口不得触发 Tauri 读写、不得读取代码文件、不得启动或打开预览、不得直接继续 run,也不得新增普通用户移动适配面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/compatibility` 准备兼容性说明,只基于当前 manifest、最近 run trace、preview 和静态自检状态汇总推荐环境、输入兼容、不承诺范围、反馈口径和参考命令,并提供 `/run`、`/mobile`、`/review`、`/trace` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得读取文件、不得启动或打开预览、不得导出试玩包、不得上传云端、不得发布作品、不得写项目,也不得新增普通用户兼容性面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/accessibility` 查看可读性与无障碍检查,只基于当前 manifest、最近 run trace、preview 和任务状态汇总文字可读、颜色对比、按钮 / 状态命名、键盘等价、可见焦点、非颜色唯一反馈和静音可玩检查,并提供 `/rules`、`/review`、`/agent-resume 可读性与无障碍:...`、`/open-preview` 或 `/run` 草稿;该入口不得触发 Tauri 读写、不得读取代码或 trace 文件、不得启动或打开预览、不得直接继续 run,也不得新增普通用户无障碍面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/localization` 查看本地化与文案检查,只基于当前 manifest、最近 run trace、preview 和发布说明产物状态汇总默认语言、文案范围、关联任务、检查口径、暂不做事项和参考命令,并提供 `/read exports/README.md`、`/agent-resume 本地化与文案:...`、`/review` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得读取文件、不得启动或打开预览、不得导出试玩包、不得上传云端、不得发布作品、不得写项目,也不得新增普通用户本地化面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/performance` 查看性能与加载检查,只基于当前 manifest、最近 run trace、preview、资产数量和 trace artifact 摘要汇总入口自包含、首屏不空白、素材体积、主循环稳定、无远程依赖和预览启动检查,并提供 `/run-artifacts`、`/review`、`/open-preview`、`/run` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得读取产物或日志文件、不得启动或打开预览、不得直接继续 run,也不得新增普通用户性能面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/polish` 查看试玩前打磨清单,只基于当前 manifest、最近 run trace、preview、最近自检和资产数量汇总试玩前打磨范围、推荐检查顺序、关联任务和最近打磨相关步骤,并提供 `/agent-resume 打磨:...`、`/review`、`/feedback` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得读取文件、不得启动预览、不得导出试玩包、不得写项目,也不得新增普通用户打磨面板。 -- 2026-07-03 调整:普通用户通过聊天输入 `/credits` 查看素材署名与来源,只基于当前 manifest.assets 汇总素材数量、上传 / 生成 / 画板来源分布、来源清单和交付前需要确认的授权 / 模型 / 画板资源口径,并提供 `/assets` 草稿;该入口不得触发 Tauri 读写、不得刷新资产、不得读取素材清单、不得导出试玩包,也不得新增普通用户署名面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/blockers` 查看当前阻塞项,只基于当前 manifest、最近 run trace、preview、最近命令、ready / failed 任务、导出记录和资产概况汇总当前阻塞项,并提供 `/run`、`/export`、`/todo`、`/review`、`/trace`、`/tasks`、`/logs`、`/art` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得读取文件、不得启动预览、不得导出试玩包、不得写项目,也不得新增普通用户阻塞面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/ready` 查看试玩就绪度,只基于当前 manifest、最近 run trace、preview、最近自检、导出记录、ready / failed 任务和资产概况汇总可交付判断,并提供 `/run`、`/export`、`/todo`、`/review`、`/trace`、`/tasks`、`/art`、`/share` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得读取文件、不得启动预览、不得导出试玩包、不得写项目,也不得新增普通用户就绪度面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/evidence` 查看当前验证证据台账,只基于当前 manifest、最近 run trace、preview、最近命令、静态自检、导出记录、素材和最近试玩步骤汇总已有验证证据与缺口,并提供 `/run`、`/export`、`/art`、`/logs`、`/review`、`/next` 或 `/ready` 草稿;该入口不得触发 Tauri 读写、不得读取文件、不得启动或打开预览、不得导出试玩包、不得写项目,也不得新增普通用户证据面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/deps` 查看任务依赖链,只基于当前 manifest.tasks 和最近 run trace.taskGraph 汇总 active / carry / ready / 等待依赖、可执行任务与等待依赖,并提供 `/criteria`、`/todo`、`/tasks` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得读取任务文件、不得启动 run、不得修改项目,也不得新增普通用户依赖面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/revise` 准备下一轮改版说明草稿,只基于当前 manifest 和最近 run trace 汇总返工焦点、失败 / active / carry / ready 任务、最近评审 / 试玩步骤、预览和导出缺口,并填入 `/agent-resume 改版说明:...` 草稿;该入口不得触发 Tauri 读写、不得读取文件、不得继续 run、不得启动预览、不得导出试玩包、不得写项目,也不得新增普通用户改版面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/privacy` 查看隐私与导出边界,只基于当前 manifest、授权项目路径、最近 run trace、preview、资产来源和导出记录汇总 API Key、预览、本地试玩包、内部文件、素材来源和 trace 的隐私 / 交付边界,并提供 `/credits`、`/exports` 或 `/config` 草稿;该入口不得触发 Tauri 读写、不得读取文件、不得导出试玩包、不得启动预览、不得写项目,也不得新增普通用户隐私面板。 -- 2026-07-03 调整:普通用户通过聊天输入 `/risks` 查看当前项目风险,只基于主窗口当前已加载的 manifest、最近 run trace、预览状态、任务状态、资产来源和最近命令派生风险摘要,并提供首个风险处理草稿;该入口不得触发 Tauri 读写、不得读取文件、不得启动或打开预览,也不得新增普通用户面板。 -- 2026-07-03 调整:普通用户通过聊天输入 `/criteria` 查看当前任务验收标准,只基于当前 manifest.tasks 和最近 run trace.taskGraph 汇总 active、carry、ready、失败或待处理任务的验收条件和产物,并提供 `/tasks` 草稿;该入口不得触发 Tauri 读写、不得读取任务文件或 trace 文件,也不得新增普通用户验收面板。 -- 2026-07-03 调整:普通用户通过聊天输入 `/groups` 查看专业组进度,只基于当前 manifest.tasks 和最近 run trace.taskGraph / passPlans 汇总六个专业组的完成、active、carry、ready、失败数量和下一步任务,并提供 `/tasks` 草稿;该入口不得触发 Tauri 读写、不得读取任务文件或 trace 文件,也不得新增普通用户专业组面板。 -- 2026-07-03 调整:普通用户通过聊天输入 `/budget` 查看最近 run 预算,只基于当前最近 run trace 汇总轮次、工具调用、stopReason 和下一步建议,并提供 `/review`、`/publish`、`/trace` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得读取 trace 文件,也不得新增普通用户预算面板。 -- 2026-07-03 调整:普通用户通过聊天输入 `/qa` 查看质量检查清单,只基于当前 manifest、最近 run trace、最近命令和 preview 状态汇总 Evaluator、任务、静态自检、试玩和产物状态,并提供 `/review`、`/tasks`、`/trace`、`/playtest`、`/publish` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得读取 trace 或日志文件、不得启动或打开预览,也不得新增普通用户 QA 面板。 -- 2026-07-03 调整:普通用户通过聊天输入 `/changes` 查看最近生成变更,只基于当前 manifest、最近 run trace 的 artifacts / steps 和最近命令汇总可验产物、最近输出、当前资产和真实差异查看方向,并提供 `/read <首个可验产物>` 或 `/run-artifacts` 草稿;该入口不得触发 Tauri 读写、不得读取产物或日志文件、不得执行 checkpoint diff,也不得新增普通用户变更面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/todo` 查看下一轮小步清单,只基于当前 manifest 和最近 run trace 汇总失败、active、carry、ready 或待处理任务,并提供 `/tasks`、`/review` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得读取任务文件、不得启动 run、不得修改项目,也不得新增普通用户小步面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/plan` 查看下一轮分工计划,只基于当前 manifest 和最近 run trace 汇总协作顺序、各专业组接手任务、空档组和首个继续执行草稿,并提供 `/agent-resume 下一轮计划:...`、`/review` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得读取任务文件、不得启动 run、不得修改项目,也不得新增普通用户计划面板。 -- 2026-07-03 调整:普通用户通过聊天输入 `/review` 查看 Evaluator 评审状态,只基于主窗口当前已加载的最近 run trace 派生通过 / 需返工状态、返工焦点、返工路线和最近评审步骤,并提供 `/read .agent/findings.md` 或 `/agent-resume ` 草稿;该入口不得直接读取评审文件、不得触发 Tauri 读写,也不得新增普通用户评审面板。 -- 2026-07-03 调整:普通用户通过聊天输入 `/context` 查看生成上下文来源,只基于当前 manifest 和最近 run trace 列出项目对话、短期记忆、长期记忆、项目黑板、Agent 对话、Agent 私有记忆、manifest、最近 trace 和最近 LLM 输入路径,并提供 `/read` 或 `/memory blackboard` 草稿;该入口不得触发 Tauri 读写、不得读取上下文文件,也不得新增普通用户上下文面板。 -- 2026-07-03 调整:普通用户通过聊天输入 `/timeline` 查看项目活动时间线,只基于当前 manifest.commandRuns 和最近 run trace 汇总最近命令、日志读取草稿和最近 Agent 步骤,并提供 `/read`、`/trace` 或 `/history` 草稿;该入口不得触发 Tauri 读写、不得读取日志或 trace 文件,也不得新增普通用户时间线面板。 -- 2026-07-03 调整:普通用户通过聊天输入 `/playtest` 查看试玩状态,只基于主窗口当前已加载的 manifest、最近 run trace 和 preview 状态派生原型是否通过、预览是否运行、Playtest 任务状态、最近试玩步骤和预览日志读取命令,并提供 `/run`、`/open-preview`、`/trace` 或 `/review` 草稿;该入口不得触发 Tauri 读写、不得启动或打开预览、不得读取日志,也不得新增普通用户试玩面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/test-plan` 准备手动测试计划,只基于主窗口当前已加载的 manifest、最近 run trace 和 preview 状态汇总手动用例、关联 Preview / Playtest 任务和最近试玩证据,并提供 `/run`、`/open-preview`、`/trace`、`/review` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得读取文件、不得启动或打开预览、不得直接继续 run,也不得新增普通用户测试面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/audience` 查看首批试玩对象,只基于主窗口当前已加载的 manifest、最近 run trace、preview 和试玩任务状态汇总首批试玩人群、测试者规模、观察重点和暂不面向场景,并提供 `/run`、`/feedback`、`/review`、`/trace` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得读取文件、不得启动或打开预览、不得导出试玩包、不得直接继续 run,也不得新增普通用户对象面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/invite` 准备试玩邀请文案,只基于主窗口当前已加载的 manifest、最近 run trace 和 preview 状态汇总邀请对象、短文案、发送前检查和收反馈口径,并提供 `/run`、`/feedback`、`/review`、`/trace` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得读取文件、不得启动或打开预览、不得导出试玩包、不得写项目,也不得新增普通用户邀请面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/bug-report` 准备缺陷复现记录,只基于主窗口当前已加载的 manifest、最近 run trace 和 preview 状态汇总复现入口、最近试玩证据、记录模板、严重度口径和修复草稿,并提供 `/run`、`/agent-resume 缺陷修复:`、`/review`、`/trace` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得读取文件、不得启动或打开预览、不得导出试玩包、不得写项目,也不得新增普通用户缺陷面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/survey` 准备试玩问卷问题,只基于主窗口当前已加载的 manifest、最近 run trace 和 preview 状态汇总问卷使用场景、五个核心问题、记录格式和追踪方式,并提供 `/run`、`/invite`、`/review`、`/trace` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得读取文件、不得启动或打开预览、不得导出试玩包、不得写项目,也不得新增普通用户问卷面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/cover` 准备封面与缩略图检查,只基于主窗口当前已加载的 manifest、最近 run trace、preview 和资产状态汇总封面候选、用途尺寸、选择口径和补齐路径,并提供 `/run`、`/art`、`/listing`、`/review`、`/trace` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得截屏、不得裁剪、不得读取文件、不得启动或打开预览、不得导出试玩包、不得上传云端、不得发布作品、不得写项目,也不得新增普通用户封面面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/screenshots` 准备宣传截图清单,只基于主窗口当前已加载的 manifest、最近 run trace、preview 和资产状态汇总截图目标、拍摄顺序、命名建议和作品页搭配,并提供 `/run`、`/listing`、`/review`、`/trace` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得截屏、不得读取文件、不得启动或打开预览、不得导出试玩包、不得写项目,也不得新增普通用户截图面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/trailer` 准备试玩短视频脚本,只基于主窗口当前已加载的 manifest、最近 run trace、preview 和资产状态汇总 15 秒结构、镜头清单、口播节奏和录制提示,并提供 `/run`、`/share`、`/review`、`/trace` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得录屏、不得读取文件、不得启动或打开预览、不得导出试玩包、不得上传云端、不得写项目,也不得新增普通用户录屏面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/faq` 准备试玩常见问答,只基于主窗口当前已加载的 manifest、最近 run trace 和 preview 状态汇总试玩问答、回答口径、测试者提醒和交付搭配,并提供 `/run`、`/share`、`/review`、`/trace` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得读取文件、不得启动或打开预览、不得导出试玩包、不得上传云端、不得写项目,也不得新增普通用户 FAQ 面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/post` 准备社区发布文案,只基于主窗口当前已加载的 manifest、最近 run trace、preview 和资产状态汇总短文案、长文案结构、标签建议和 CTA,并提供 `/run`、`/store`、`/review`、`/trace` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得上传云端、不得发布作品、不得读取文件、不得启动或打开预览、不得导出试玩包、不得写项目,也不得新增普通用户社区发布面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/store` 准备上架资料清单,只基于主窗口当前已加载的 manifest、最近 run trace、preview、资产和发布说明状态汇总必备资料、首发范围、上架前检查和参考命令,并提供 `/run`、`/listing`、`/review`、`/trace`、`/read exports/README.md` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得上传云端、不得发布作品、不得读取文件、不得启动或打开预览、不得导出试玩包、不得写项目,也不得新增普通用户上架面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/media-kit` 准备媒体资料包清单,只基于主窗口当前已加载的 manifest、最近 run trace、preview、资产和发布说明状态汇总对外资料、素材缺口、组装顺序和参考命令,并提供 `/run`、`/screenshots`、`/review`、`/trace`、`/read exports/README.md` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得截屏、不得录屏、不得读取文件、不得启动或打开预览、不得导出试玩包、不得上传云端、不得发布作品、不得写项目,也不得新增普通用户媒体包面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/release-notes` 准备试玩更新说明,只基于主窗口当前已加载的 manifest、最近 run trace、preview、资产和发布说明状态汇总本轮变化、主要产物、玩家可见说明和已知限制,并提供 `/run`、`/media-kit`、`/review`、`/trace`、`/read exports/README.md` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得读取文件、不得启动或打开预览、不得导出试玩包、不得上传云端、不得发布作品、不得写项目,也不得新增普通用户更新说明面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/known-issues` 准备已知问题清单,只基于主窗口当前已加载的 manifest、最近 run trace、preview 和任务状态汇总已知问题、试玩限制、反馈入口和发送前检查,并提供 `/run`、`/share`、`/review`、`/trace` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得读取文件、不得启动或打开预览、不得导出试玩包、不得上传云端、不得发布作品、不得写项目,也不得新增普通用户已知问题面板。 -- 2026-07-03 调整:普通用户通过聊天输入 `/feedback` 准备试玩反馈和修改说明,只基于当前 manifest、最近 run trace 和 preview 状态列出反馈方向、反馈模板和参考命令,并提供 `/run`、`/agent-resume 试玩反馈:`、`/review` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得读取文件、不得启动或打开预览、不得直接继续 run,也不得新增普通用户反馈面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/retention` 准备首轮复玩/留存观察清单,只基于当前 manifest、最近 run trace、preview、最近试玩证据、素材数量、发布说明和导出状态汇总测试者样本、复玩信号、记录模板和暂不做事项,并提供 `/run`、`/feedback`、`/review`、`/trace` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得读取文件、不得启动或打开预览、不得导出试玩包、不得上传云端、不得发布作品、不得写项目,也不得新增普通用户留存面板;首版不做真实埋点、留存报表、用户画像、A/B 实验、排行榜或账号留存。 -- 2026-07-03 调整:普通用户通过聊天输入 `/listing` 准备作品页文案清单,只基于当前 manifest、最近 run trace、发布组任务和资产清单汇总标题、一句话卖点、标签口径、封面素材、发布说明和最近运营步骤,并提供 `/read exports/README.md`、`/review`、`/art`、`/publish` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得读取发布说明、不得上传云端、不得发布作品,也不得新增普通用户作品页面板。 -- 2026-07-03 调整:普通用户通过聊天输入 `/handoff` 生成当前项目交接摘要,只基于主窗口当前已加载的 manifest、授权项目路径、最近 run trace、Agent 状态和已加载 run 历史生成交接信息,并提供 `/next` 后续草稿;该入口不得触发 Tauri 读写、不得读取文件、不得启动或打开预览,也不得新增普通用户面板。 -- 2026-07-03 调整:普通用户通过聊天输入 `/runs` 查看已加载 Run 历史读取命令,只基于主窗口当前已加载的 latest trace 和最多 100 个历史 run 中已经载入的批次生成 `/trace` 或 `/read .agent/runs/...` 草稿;该入口不得额外触发 Tauri 读取、不得滚动加载更多历史、不得启动或打开预览,也不得新增普通用户面板。 -- 2026-07-03 调整:普通用户通过聊天输入 `/run-files` 查看 Agent 运行辅助文件读取命令,只列出 `.agent/output.jsonl`、`.agent/activity.jsonl` 和 `.agent/context.bundle.json` 对应 `/read` 草稿并提供首个草稿;该入口不得直接读取辅助文件、不得触发 Tauri 读写,也不得新增普通用户面板。 -- 2026-07-03 调整:`/llm-status` 读取到的 agent 级 LLM 配置状态可回填到主窗口 Agent 状态列表、聊天侧 `/agents` 汇总和单 Agent 对话头部,显示 provider 类型、模型、流式开关和 API Key 是否已读取;密钥本体仍不能进入聊天、状态列表、manifest、trace 或本地项目文件。 -- 2026-07-03 调整:开发窗口日志面板提供 `.agent/logs/command.log`、`.agent/logs/preview.log` 和 `.agent/logs/agent.log` 的只读查看入口,复用 `file.read` 授权策略;普通用户聊天输入 `/logs` 只列出这三个日志文件对应的 `/read ...` 草稿 / 命令并提供首个草稿,不直接读取日志,不新增普通用户日志面板,实际读取仍走聊天侧 `file.read`。 -- 2026-07-03 调整:单 Agent 对话面板允许用户把当前输入手动追加到该 agent 的 `memory/agents//.md` 私有记忆;写入复用 `memory.write` 项目策略、项目锁和 Tauri 本地目录能力,不把普通对话流水自动混入私有记忆;聊天侧 `/agent-conversations` 和 `/agent-memories` 只列出同一批 Agent 对话与私有记忆读取命令并提供首个 `/read` 草稿,不直接读取文件。 -- 2026-07-03 调整:普通用户通过聊天输入 `/art` 查看美术素材,只基于当前 manifest 盘点图片、视频和序列帧素材的数量、来源、画板接入状态和路径,并提供 `/generate-art 首版核心美术素材` 或 `/read assets/manifest.art.json` 草稿;该入口不得触发 Tauri 读写、平台生成、画板同步或新增普通用户美术面板。 -- 2026-07-03 调整:主窗口新增音效登记和画板音频导入快捷入口,只填入 `/asset-register assets/audio/sfx.wav audio audio/wav` 或 `/import-canvas-asset assets/audio/sfx.wav ` 草稿;聊天输入 `/audio` 只基于当前 manifest 盘点音频素材、来源和路径,并给出登记音效或读取 `assets/manifest.audio.json` 的草稿。音乐组仍复用现有资产登记 / 画板回流链路,不新增独立音频生成系统。 -- 2026-07-03 调整:普通用户通过聊天输入 `/balance` 查看数值与难度口径,只基于当前 manifest.tasks、最近 run trace.taskGraph、trace artifacts 和 steps 汇总数值组任务、验收口径、`game/balance.json` 状态和最近数值步骤,并提供 `/read game/balance.json` 或 `/agent-resume 数值调整:...` 草稿;该入口不得触发 Tauri 读写、不得读取数值表、不得启动预览或继续 run,也不得新增普通用户数值面板。 -- 2026-07-03 调整:主窗口新增常用生成产物读取入口,只把入口 HTML、设计、数值、美术清单、音频清单和发布说明对应的 `/read` 草稿填入聊天输入框;聊天命令 `/artifacts` 只列出同一组固定读取命令并提供首个读取草稿,`/run-artifacts` 只列出最近 trace 里的产物读取命令并提供首个 `/read` 草稿,`/logs` 只列出固定日志读取命令;实际读取仍走聊天侧 `file.read` 权限流,不直接读本地文件。 -- 2026-07-03 调整:普通用户通过聊天输入 `/share` 准备试玩交付清单,只基于当前 manifest、授权项目路径、最近 run trace、preview 状态和 manifest.commandRuns 汇总原型通过状态、本地预览、本地试玩包、测试者说明和反馈收集方向,并提供 `/export`、`/exports`、`/trace`、`/review` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得导出试玩包、不得列出历史包、不得上传云端、不得生成公开分享链接,也不得新增普通用户分享面板。 -- 2026-07-03 调整,2026-07-04 更新:普通用户通过聊天输入 `/next` 触发下一步建议入口,只基于主窗口当前已加载的 manifest、最近 run trace 和最近命令摘要生成聊天建议,列出 `/goal`、`/guide`、`/progress`、`/spec`、`/mvp`、`/pitch`、`/demo`、`/rules`、`/tutorial`、`/mobile`、`/compatibility`、`/accessibility`、`/localization`、`/performance`、`/polish`、`/blockers`、`/ready`、`/evidence`、`/deps`、`/revise`、`/privacy`、`/audience`、`/invite`、`/bug-report`、`/survey`、`/cover`、`/screenshots`、`/trailer`、`/faq`、`/post`、`/store`、`/media-kit`、`/release-notes`、`/known-issues`、`/tasks`、`/criteria`、`/groups`、`/balance`、`/budget`、`/qa`、`/changes`、`/plan`、`/todo`、`/trace`、`/review`、`/context`、`/timeline`、`/playtest`、`/test-plan`、`/feedback`、`/retention`、`/share`、`/listing`、`/run`、`/open-preview`、`/assets`、`/credits`、`/art`、`/audio`、`/publish`、`/artifacts`、`/run-artifacts`、`/passes`、`/run-files`、`/internals`、`/logs`、`/agent-resume ` 等安全命令草稿方向,并提供一个首选草稿;该命令不得直接执行 Tauri 读写、启动或打开预览、读取本地文件,也不得绕过原有命令确认和 `file.read` 权限流。 -- 2026-07-03 调整:普通用户通过聊天输入 `/publish` 生成发布准备清单,只基于主窗口当前已加载的 manifest、最近 run trace、预览状态、资产来源和最近命令摘要列出原型通过、预览、任务、资产、音频、包装说明和试玩包状态,并提供 `/run`、`/trace`、`/agent-resume ` 或 `/export` 草稿;该入口不得触发 Tauri 读写、不得启动或打开预览、不得读取文件,也不得新增普通用户发布面板。 -- 2026-07-03 调整:普通用户通过聊天输入 `/internals` 只列出 `.agent/manifest.json`、`.agent/run.latest.json`、`.agent/spec.md`、`.agent/findings.md`、`.agent/policy.json`、`.agent/project.index.json`、`.agent/agent.db` 和 `.agent/conversations/project.jsonl` 的 `/read` 草稿,并提供首个读取草稿;该入口不得直接读取内部文件、不得触发 Tauri 读写,也不得新增普通用户内部文件面板。 -- 2026-07-03 调整:普通用户通过聊天输入 `/passes` 只从当前已加载的最近 run trace artifacts 中筛选 `.agent/passes/` 轮次产物,列出 `/read` 草稿并提供首个读取草稿;该入口不得直接读取轮次文件、不得触发 Tauri 读写,也不得新增普通用户轮次面板。 +- 2026-07-03 调整:agent 级 LLM 配置状态可回填到主窗口 Agent 状态列表和单 Agent 对话头部,显示 provider 类型、模型、流式开关和 API Key 是否已读取;密钥本体仍不能进入聊天、状态列表、manifest、trace 或本地项目文件。 +- 2026-07-03 调整:开发窗口日志面板提供 `.agent/logs/command.log`、`.agent/logs/preview.log` 和 `.agent/logs/agent.log` 的只读查看入口,复用 `file.read` 授权策略;普通用户窗口不提供这组日志入口。 +- 2026-07-03 调整:单 Agent 对话面板允许用户把当前输入手动追加到该 agent 的 `memory/agents//.md` 私有记忆;写入复用 `memory.write` 项目策略、项目锁和 Tauri 本地目录能力,不把普通对话流水自动混入私有记忆。 +- 2026-07-03 调整:音乐组复用现有资产登记 / 画板回流链路,不新增独立音频生成系统。 - 2026-06-25 调整:本地 HTTP 预览静态 `HEAD` 必须返回与 `GET` 相同的真实 `Content-Length`,但不返回 body;浏览器、图片、音频和视频探测不能拿到 `Content-Length: 0` 的假响应。 -- 2026-06-25 调整:普通用户通过聊天输入 `/run` 触发待确认 `game.run_local`,确认后只能复用白名单 `game.static_smoke` 自检当前 `game/index.html`,通过后启动 `127.0.0.1` 本地 HTTP 预览。独立执行 `game.static_smoke` 时如果已有 `.agent/run.latest.json`,必须追加 `Playtest / game.static_smoke` trace step,避免“运行了代码但编排 trace 不可见”。 -- 2026-06-25 调整:普通用户通过聊天输入 `/trace` 触发只读 `agent.trace_read`,读取 `.agent/run.latest.json` 并在聊天里摘要 loop 轮次、stopReason、nextStep、active / carry-over 任务、repairRoutes、agent 建议命令和最近 step。trace 面板仍只在开发窗口展示,普通用户窗口不新增面板。 -- 2026-06-25 调整:普通用户通过聊天输入 `/import-canvas-export /绝对/画板素材.zip 画板项目ID` 触发待确认 `canvas.export_import`,读取现有 `/editor/canvas` 素材导出 ZIP。导入命令只读取用户指定 ZIP,写入当前本地项目 `assets/canvas-imports/`,基础护栏限制路径逃逸、文件数量和解压体积;导出包没有真实 resourceId 时,用 `canvas-export:` 作为可追踪 assetObjectId,不伪造后端画板资源行。 -- 普通用户通过聊天输入 `/sync-canvas-project 画板项目ID` 触发待确认 `canvas.project_sync`;普通模式用当前陶泥儿登录态读取 `/api/editor/projects/{projectId}` 并通过 `/api/assets/read-url` 换签,高级模式使用对应 External v1 路由。固定官方 origin、owner 和凭据均不写入 manifest、Agent DB、trace 或日志。 - `game.generate_draft` 在当前模式具备画板服务授权且美术组缺少 `canvas` 来源图片资产时,复用同一平台生成链路生成首版美术素材并下载到本地;普通模式使用登录态内部路由,高级模式使用 External v1。 - 美术组 `Asset` 和音乐组 `SFX` 在缺少对应 `canvas` 来源资产时建议同步;普通模式未登录或高级模式 Developer Key 缺失时只给出准确的能力不可用说明,不伪造生成结果。 - 2026-06-24 调整:同一本地项目多次 `game.generate_draft` 必须追加 `memory/session.md` 与 `memory/project.md`,不得覆盖历史对话和创作目标记录。 @@ -6400,29 +6340,15 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在 - 2026-07-10 调整:Agent Runtime V1 新增 `resume_game_creator_agent_runtime_tasks` 恢复入口。客户端读取项目 Runtime 时对每个项目路径最多自动尝试一次恢复;恢复命令必须通过 `agent.resume` 自动权限,默认需要确认或被拒绝时不会静默启动。恢复扫描 `.agent/runtime/tasks/.jsonl` 里的上一进程遗留 `running` 或 `pending` 任务,同一 Agent 同时存在二者时先重接遗留 `running`,再由既有 drain 串行继续 `pending`,并写 `agent.runtime.background_task.recovered` 审计记录。该能力只恢复本地 JSONL 队列到当前 App 进程,不是跨重启常驻 worker,也不承诺恢复已发出的上游 LLM 请求。 - 2026-07-10 调整:每个 Agent 新增独立持久化 Session 管理。legacy `agent-session-` 继续读写 `.agent/conversations/agents/.jsonl`;新 Session 写 `.agent/conversations/agents//sessions/.jsonl`,`.agent/runtime/sessions/.json` 原子保存 Session catalog 和 active Session。开发单 Agent 聊天页支持列表、创建、切换、归档和归档历史只读查看;归档不删除消息,运行中、排队中、等待确认、取消中或 `needs-reconciliation` 的 Session 不允许改变 active/归档。聊天、流式回调、后台 run、任务历史、事件历史和 prompt 连续上下文按启动时 `sessionId` 归属并过滤,`conversation.read` 和 self `agent.run_status` 通过 runId 使用同一 Session;恢复或处理待确认动作前校验 task、runtime state 和 pending action 的 Session 一致性。Runtime 的 OS 锁、FIFO 队列和恢复屏障仍属于 Agent,同一 Agent 不因多个 Session 获得并行执行能力。 - 2026-07-01 调整:AI 游戏创作 App 借鉴 Godcoder 的本地工程护栏,但只收敛到五项本地机制:`ArtifactWriter` 写入前 checkpoint、写入后 diff、用户确认 restore;进入 LLM 前过滤密钥和本机配置痕迹;`.agent/agent.db` 继续作为轻量 JSONL 项目索引,`/index` 额外刷新 `.agent/project.index.json`;同一项目写入通过 `.agent/project.lock` 串行化;`.agent/policy.json` 记录项目级命令拒绝 / 确认策略。v1 不引入通用 IDE 插件、云工作区、SQLite 或任意 shell 代理。 -- 2026-07-03 调整:主窗口最近 checkpoint 列表必须直接展示 checkpoint id、文件数、大小和创建时间,并提供直接对比、填入 `/diff`、确认回滚和填入 `/restore` 的轻量操作;回滚仍走 `project.restore` 确认卡,不在列表按钮中直接写项目文件。 -- 2026-06-24 调整:普通用户通过聊天输入 `/help` 发现可用内置命令;命令发现必须留在聊天消息里,不得因此暴露开发面板。 +- 2026-07-03 调整:主窗口最近 checkpoint 列表必须直接展示 checkpoint id、文件数、大小和创建时间,并提供直接对比和确认回滚的轻量操作;回滚仍走 `project.restore` 确认卡,不在列表按钮中直接写项目文件。 - 2026-06-24 调整:聊天区待确认命令的日志语义必须区分 `permission.pending`、`permission.confirm` 和 `permission.cancel`;待确认卡片必须展示本地写入目标路径,避免用户在不知道落盘位置时确认。 -- 2026-06-24 调整:普通用户通过聊天输入 `/status` 读取 `.agent/manifest.json` 的项目状态摘要,只在聊天消息里展示项目目录、任务状态、资产数量、预览状态和最近命令;不得为了状态查看暴露任务、文件或日志面板。 -- 2026-06-24 调整:普通用户通过聊天输入 `/files` 触发只读 `file.list`,只在聊天消息里展示本地项目文件摘要;不得把文件读写面板暴露到普通用户窗口。 -- 2026-06-24 调整,2026-07-03 更新:普通用户通过聊天输入 `/assets` 触发只读 `asset.list`,只在聊天消息里展示本地项目资产路径、类型和来源;资产列表消息可以填入首个资产的 `/read` 草稿,方便从聊天继续查看资产文本元数据,但仍不直接读取文件或绕过聊天命令;不得把资产面板暴露到普通用户窗口。 -- 2026-06-24 调整:普通用户通过聊天输入 `/read 本地相对路径` 触发只读 `file.read`,只在聊天消息里展示项目内文本文件并截断长文本;不得开放聊天里的文件写入或删除能力。 -- 2026-06-24 调整:普通用户通过聊天输入 `/tasks` 触发只读 `task.list`,只在聊天消息里展示专业组、角色、任务状态和产物交接;不得把任务面板暴露到普通用户窗口。 -- 2026-06-24 调整:普通用户只能通过聊天触发内置命令;当前 `/smoke` 映射到白名单 `command.run_limited game.static_smoke` 并走待确认卡片,不允许扩展成任意 shell 或自由命令解析。 -- 2026-06-24 调整:普通用户通过聊天输入 `/project /绝对路径` 触发 `project.create` 待确认命令,用于授权并初始化本地项目目录;相对路径不会生成待确认命令;不要把开发窗口项目路径输入框暴露到正式用户界面。 -- 2026-06-25 调整:普通用户侧所有会写入、运行、查看 / 打开预览或导入本地产物的命令必须先完成 `/project` 初始化,包括 `game.generate_draft`、`asset.upload`、`game.run_local`、`command.run_limited`、`preview.start`、`preview.status`、`preview.open`、`preview.stop`、`memory.write`、`memory.delete`、`canvas.project_sync`、`canvas.asset_import` 和 `canvas.export_import`;没有已授权本地项目时只提示设置项目,不得落到默认 `/tmp` 草稿目录。 +- 2026-06-25 调整:普通用户侧所有会写入、运行、查看 / 打开预览或导入本地产物的命令必须先完成项目初始化,包括 `game.generate_draft`、`asset.upload`、`game.run_local`、`command.run_limited`、`preview.start`、`preview.status`、`preview.open`、`preview.stop`、`memory.write`、`memory.delete`、`canvas.project_sync`、`canvas.asset_import` 和 `canvas.export_import`;没有已授权本地项目时只提示设置项目,不得落到默认 `/tmp` 草稿目录。 - 2026-06-24 调整,2026-06-30 更新:终端测试入口使用同一个 Tauri Rust 二进制的 `--agent-run <本地项目绝对路径> <创作需求>`,只复用现有 `game.generate_draft`、`game.static_smoke` 和本地 HTTP 预览链路,不另建第二套 agent runtime;发布 App 的 LLM 配置从 Tauri 应用配置目录读取,不写入仓库默认配置或项目文件。需要自动验证时可追加 `--no-wait`,生成预览 trace 后立即停止本地预览,避免命令卡在回车等待。 - 2026-07-04 调整,2026-07-08 更新:`apps/ai-game-creator-shell/src-tauri/src/main.rs` 拆成薄入口,继续只保留共享类型 / 常量、模块声明、CLI preflight、`tauri::Builder`、运行时配置初始化和 `invoke_handler` 清单;CLI 参数解析与终端运行输出放入 `cli.rs`,Tauri command 包装放入 `commands.rs`,运行时配置 / LLM 配置检查放入 `config.rs`,Agent loop 与生成编排放入 `agent.rs`,上传 / 画板 / 平台美术生成接入放入 `assets.rs`,本地项目文件、记忆、对话、权限、checkpoint、manifest 和通用路径工具放入 `project.rs`,本地 HTTP 预览 server、preview registry 和 preview Tauri command 放入 `preview.rs`,旧窗口 URL 与兼容 command 放入 `windows.rs`,Rust 单测放入 `tests.rs`。拆分不得改变 Tauri command 名、JSON 字段、`.agent/*` 路径、项目权限策略或错误语义。 - 2026-06-24 调整,2026-07-08 更新:AI 游戏创作 App 的 release 配置只登记一个普通用户窗口,登录后在同一 WebView 中进入首页、项目组和项目开发占位;开发专用单 Agent 对话、任务、文件、记忆、预览、日志和能力面板只能通过 Vite dev 的 `?dev/#dev` 分支或 debug 构建自动打开的 `developer` 开发窗口查看,不进入普通用户窗口。旧工作区窗口切换 command 只保留兼容,用户主流程不得调用它。 - 2026-06-24 调整,2026-07-18 更新:`check:native-shells` 必须静态守住 AI 游戏创作 App 的用户 / 开发边界:release 只保留一个普通用户窗口,用户侧预览只在项目运行工作台嵌入当前 `127.0.0.1` 游戏,且 Tauri 激活命令不得调用 opener;开发面板只能在 `devMode` 分支或 debug-only `developer` 窗口渲染,`developer` 窗口当前使用 `index.html?agent-chat` 并复用 `.agent/conversations/agents/.jsonl` 持久化单 Agent 对话;发布入口和普通用户窗口不得暴露 `Agent 聊天` 导航,也不得调用旧工作区窗口切换 command。 - 2026-07-10 调整:AI 游戏创作 App 的 Runtime 实时状态依赖 Tauri event listen。`src-tauri/capabilities/events.json` 必须覆盖 `client`、`developer`、`main`、`launcher`,只授予 `core:event:allow-listen` 与 `core:event:allow-unlisten`,不得向前端授予 emit;`check-config.mjs` 静态守住窗口和权限边界。Vite 开发服务器必须把仓库根目录加入 `server.fs.allow`,因为 App 直接加载 `packages/shared/src`;否则真实 WebView 会因共享源码 403 白屏,即使 TypeScript 检查仍通过。 - 2026-06-25 调整:`check:native-shells` 在 `ai-game-creator-shell:check` 之后必须追加 `ai-game-creator-shell:build -- --no-bundle`,让原生壳总门禁同时证明 AI 游戏创作独立 Tauri 壳能完成 release 编译,而不是只证明前端 / Rust 逻辑测试通过。 -- 2026-06-24 调整,2026-07-18 更新:普通用户通过聊天输入 `/preview` 触发待确认 `preview.start`,完成 `/project` 初始化后可通过 `/open-preview` 触发待确认 `preview.open` 并只激活当前已授权项目对应的 `127.0.0.1` 客户端运行视图,通过 `/preview-status` 查询当前项目预览,通过 `/preview-stop` 停止当前项目预览;用户工作台仅嵌入当前项目的 loopback 游戏,开发预览状态面板仍只在开发窗口可见,不能把 `preview.open` 扩展成任意 URL 打开能力,也不能展示或停止其它本地项目遗留的全局预览。 -- 2026-06-25 调整:`/preview-status` 虽然是只读命令,也必须写入 `preview.status` 命令日志并向聊天返回错误,不得因查询失败产生未捕获异常或无审计记录。 -- 2026-06-24 调整:普通用户通过聊天输入 `/memory [short]` 读取长期或短期记忆,通过 `/remember 内容` 待确认追加长期记忆,通过 `/forget-memory [short]` 待确认删除记忆;不得为了记忆查看或编辑暴露独立用户面板。 -- 2026-06-25 调整:`/remember` 支持可选 scope:`/remember short 内容` 追加短期记忆,`/remember long 内容` 或未写 scope 时追加长期记忆;仍统一走待确认 `memory.write`,不暴露独立用户面板。 -- 2026-06-24 调整:普通用户通过聊天输入 `/canvas 画板项目ID` 触发待确认 `canvas.project_open`,只打开本机 Genarrative 编辑器 `/editor/canvas?projectid=...`;不得把它扩展成远程站点或任意 URL 打开能力。 -- 2026-06-24 调整:普通用户通过聊天输入 `/import-canvas-asset 本地路径 画板项目ID 资源ID|object:资产对象ID [kind] [mediaType]` 触发待确认 `canvas.asset_import`,只登记项目目录内已有文件为 `canvas` 来源资产;只有 `assetObjectId` 时使用 `object:` 前缀,不伪造 resourceId;画板导出包回流使用 `/import-canvas-export /绝对/画板素材.zip 画板项目ID`。 - 验证方式:`npm run ai-game-creator-shell:typecheck`、`cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml`、`npm run test -- packages/shared/src/contracts/gameCreationApp.test.ts`、`cargo test -p shared-contracts game_creation_app --manifest-path server-rs/Cargo.toml`、`cargo test -p platform-agent --manifest-path server-rs/Cargo.toml`、`npm run check:encoding`、`git diff --check`。 ## 2026-06-30 唯一码和私有码按用户限兑一次 @@ -6779,14 +6705,14 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在 - Finalization 生产闭集:四条 lifecycle 只允许固定 lifecycle 字段和统一 `schemaVersion / updatedAt` envelope,并绑定 `responseChars / conversationPath`;assistant 审计只允许 `recordType / agentId / sessionId / role / path / messageId / finalizationId`,两条 completed 审计只允许 `recordType / agentId / taskId / sessionId / runId / source / finalizationId / messageId / responseFingerprint / responseChars`,再加同一 envelope。匹配必须逐字核对 finalization/message、Agent/task/Session/run/source、Goal/plan 快照、response fingerprint/chars 和 conversation path,四阶段还必须核对 ordinal/previousStage 与 JSONL 物理顺序;任何额外生产字段都不能获得 finalization reservation。 - 公共投影边界:task、Goal/steer、委派任务、`project.verify` 命令和 Provider/Runtime error 正文只保留在对应私有执行事实中。event、Agent DB、receipt、activity、output 与报告统一只存身份、状态、SHA-256、字符/字节/条目计数和经 URL、项目根、其它绝对路径及凭据清洗的有界摘要;公共 task 固定不存正文,委派只存 `taskSha256 / taskChars`,verify 只存脚本安全标识、`expectedCommandSha256 / expectedCommandChars`、timeout 和结果计数,error 只存 kind/fingerprint/chars 或脱敏摘要。禁止保留 task/Goal/委派/命令/error 的正文、preview、head 或 tail;普通非 Goal 任务也不例外。 - 真实验收器:revision 2 marker/path/content 不再预埋首轮项目 fixture;两个 revision 都必须命中同一交付路径的真实 `file.write` 或 `project.patchset create` 待确认动作,edit 前最终 marker/文件必须不存在,revision 1 已完成步骤在 revision 2 和终态不可回退。Goal suite 使用带 sentinel 的专用 AppData,配置只以 hardlink 复用并在清理前核对 inode/hash;全部 CLI 固定指向专用 config dir,Runner 强杀绑定 endpoint、boot、实际二进制/argv 和 OS 启动指纹,endpoint 丢失只允许回收已认领的同指纹进程。CLI JSON 只接受精确 assigned 前缀,Goal completion evidence 按四项生产契约逐字核对,公共扫描同时包含完整正文和两个 marker,失败报告从现存 task/event/Agent DB/conversation 分面容错回收部分证据而不再全报 0。 -- 展示边界:开发 Agent UI 使用 `执行 / 聊天 / 目标` 三段模式,Goal 创建/编辑通过独立弹层完成,并展示状态、revision、完成标准和暂停/恢复/清理;纯聊天 CLI 提供对应 `/goal` 命令。正式用户 Project Supervisor 页面不暴露 Goal 管理控件。 +- 展示边界:开发 Agent UI 使用 `执行 / 聊天 / 目标` 三段模式,Goal 创建/编辑通过独立弹层完成,并展示状态、revision、完成标准和暂停/恢复/清理;纯聊天 CLI 通过 `--agent-goal-*` 入口管理同一 Goal。正式用户 Project Supervisor 页面不暴露 Goal 管理控件。 - 验收现状:确定性回归与 UI 覆盖不能替代真实 Provider 长链路。截至 2026-07-15 尚未记录 V1.18 真实 Provider PASS;最新现场仍在首轮 planning、零 plan/action 时由对端关闭长连接,Rust 25.2 秒短请求成功只能证明基础通道。恢复后必须用一次性项目完成 Goal edit、pause、Runner 强杀、重启保持 paused、显式同 run resume、唯一 assistant 和零旧动作重放的交叉取证。 ## 2026-07-15 Project Supervisor 纯聊天短入口 - 决策:无 GUI 开发聊天省略 `parentAgentId` 时固定进入 `project-supervisor`;新增 `npm run agc:chat -- --config-dir [--init] ` 作为总控入口。原 `agc:swarm` 和显式 `` 继续保留给专业父 Agent 调试,不改变既有调用兼容性。 - 边界:短入口只复用现有 Swarm CLI、External Runner、Supervisor active Session、conversation、黑板、记忆和 durable 委派协议,不新增 Agent、HTTP 服务、数据库或旁路 Provider 调用。 -- 验收:CLI 单测覆盖省略 ID 默认总控和显式 ID 兼容;真实入口 smoke 用一次性项目启动 `agc:chat`,终端显示 `project-supervisor`、创建空总控 Session,并在未发起 LLM 请求时通过 `/quit` 正常退出和清理。 +- 验收:CLI 单测覆盖省略 ID 默认总控和显式 ID 兼容;真实入口 smoke 用一次性项目启动 `agc:chat`,终端显示 `project-supervisor`、创建空总控 Session,并在未发起 LLM 请求时通过 EOF(Ctrl-D)正常退出和清理。 ## 2026-07-15 后台 Agent 最终回复使用真实增量流 @@ -6812,9 +6738,9 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在 - 边界:只压缩旧 Agent/legacy conversation 和当前 run 的旧 observation,保留最近精确 tail;Goal、任务、结构化计划、steer、pending action、project/repository revision、verification、process/join/delegate、receipt 和 finalization 身份保持规范事实,不进入摘要改写。 - 持久化:私有 `game-creator-runtime-context-compaction.v1` sidecar 绑定 Agent/Session、source prefix 指纹、可选 run、summary 指纹、预算与 usage;同源幂等,追加后 revision 单调,前缀漂移失败关闭。context bundle 只绑定压缩元数据,不复制 summary 正文。 - 请求安全:compaction 使用独立 Provider lifecycle、稳定 request slot、零工具和零 web search。未知 started 或 completed 后 sidecar 未提交均按 orphan barrier 进入 reconciliation,禁止自动重发;sidecar 已提交后恢复直接复用。 -- 入口:自动压缩只发生在 background planning 安全边界;开发 Agent UI 与 `agc:chat` / `agc:swarm` 提供 `/compact`,但 in-flight Provider、执行中工具、pending confirmation 或未收束 Runtime 时拒绝手动压缩。正式用户 Supervisor 页面不增加压缩控件。 +- 入口:自动压缩只发生在 background planning 安全边界;显式手动压缩入口为 `--agent-context-compact`,但 in-flight Provider、执行中工具、pending confirmation 或未收束 Runtime 时拒绝手动压缩。正式用户 Supervisor 页面不增加压缩控件。 - 验收:除配置、幂等、篡改、恢复和公共零正文回归外,真实套件必须完成至少 30 轮、两次压缩和一次 Runner 强杀,证明请求低于阈值、原身份不变、工具零重放、唯一 assistant 与早期约束可召回;此前不得宣称整体 PASS。 -- 语义修正:历史“每 6 轮形成上下文压缩窗口”的表述由本条取代;6 轮只形成进度 checkpoint 并执行停滞检测,不改写 observation。真正摘要只由 token 阈值或显式 `/compact` 触发。 +- 语义修正:历史“每 6 轮形成上下文压缩窗口”的表述由本条取代;6 轮只形成进度 checkpoint 并执行停滞检测,不改写 observation。真正摘要只由 token 阈值或显式手动压缩触发。 - 实现收口:显式用户约束由确定性保留层逐字钉住并继续做凭据/绝对路径脱敏;`runtime.compact` 单独使用 6 分钟 IPC 响应窗口,其他 Runner 方法仍为 10 秒;普通后台任务公共审计只保存 `taskChars + taskSha256`;终态旧 bundle 只有在完整身份、Goal、revision、verification、observation、sidecar、steer 校验通过后才可刷新 legacy plan 投影。 - 真实验收:2026-07-15 正式 `openai_chat / gpt-5.5` 路由的隔离 `context-compaction` suite PASS。30/30 轮、两次 compaction revision、一次 pidfd Runner 强杀恢复、早期约束召回和 29134/64000 最大估算输入均满足;30 个 tool-plan 与 2 个 compaction lifecycle 唯一闭合,fallback replay、重复 message/audit、工具重放和公共正文/summary/API Key/诱饵/项目路径/正式配置路径泄漏均为 0。首轮第 22 轮 Provider transport 终态按规则 FAIL 且零重放,新 disposable 项目完整重跑取得 PASS,全部一次性现场已按 sentinel 清理。 @@ -7209,7 +7135,7 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在 - 背景:`--swarm-chat` 曾在模型调用前用字符串包含判断选择 Chat / Execute / Resume,否定句、复合请求和未列入词表的工作请求都会误路由;busy Runtime 期间的裸聊天还可能绕过 Agent lane 并与原 run 交错写同一 Session。 - 决策:删除自然语言关键词分类和硬编码自然语言直答。Project Supervisor 与角色目录中 `role.id=director` 的六个部门负责人使用统一 interaction loop;自然语言回复与 `project_location / runtime_execute / runtime_resume` 都来自同一次 Provider turn 的直接文本或原生 function tool。叶子专业 Agent 保持合同执行者,不接入该外层决策能力。 - Canonical 输入:`runtime_execute` 不允许模型提交 task 参数,真正入队始终使用用户原始消息,避免模型改写时丢失否定、范围和验收条件。非原生 tool Provider 使用同构严格 JSON envelope 适配;模型只能提出 intention,不能选择 runId、越过权限或直接执行项目副作用。 -- 并发与 Runner:`SwarmChat` 恢复为 External Runner 写入口,启动前必须显式使用项目外 AppData。已有 active Goal 或 busy Runtime 时,新输入只进入同 run durable steer;空闲 direct reply 的 user / assistant 在 Agent Session lane 内成对落盘。`/resume` 是显式控制命令,不能再由“继续”等字符串特判。 +- 并发与 Runner:`SwarmChat` 恢复为 External Runner 写入口,启动前必须显式使用项目外 AppData。已有 active Goal 或 busy Runtime 时,新输入只进入同 run durable steer;空闲 direct reply 的 user / assistant 在 Agent Session lane 内成对落盘。恢复扫描只能由显式恢复入口(`--agent-resume`)触发,不能再由“继续”等字符串特判。 - 扩展边界:首版 interaction capability 由一个定义同时派生工具名、描述、schema 和 dispatch kind,作为后续统一 Tool Registry 的窄入口。现有 Runtime Store、Tool Host、Goal、delegation、sandbox、revision、verification、finalization 和 exactly-once 保持自研且不迁入 Prompt 或 Skill;本轮不引入 Pi Node sidecar,也不宣称已完成全量工具 registry、PromptSection 或 Cargo crate 拆分。 - Provider 兼容:真实 OpenAI-compatible smoke 发现部分网关会在纯文本回复中返回 `tool_calls: null`;`platform-llm` 将该字段按缺省空列表解析,并保留真实工具调用数组语义。 - 验证:interaction parser `7/7`、swarm CLI `39/39`、Runner/config 门禁回归和 `platform-llm` null-tool-calls 回归通过。隔离 AppData 的真实 Provider 连续验证了身份直接回复、否定执行的架构解释、模型选择 `project_location` 和模型选择 `runtime_execute`;执行轮产生 `[已投递]` 后以 `turn.report outcome=settled`、busy/pending/reconciliation 均为 `0` 收束。 @@ -8518,7 +8444,7 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在 - 权限边界:开放的是 `regenerate / registered resources / playtest` 等产品语义,不是原始最高权限。`regenerate` 只由当前请求最新一条原始 User 消息授权并绑定客户端稳定 `clientTurnId`;模型参数、MCP 自动批准和缺失 clientTurnId 都失败关闭。授权输入先对完整原文做 Unicode NFKC 与撇号规范化,随后整串必须完整匹配审核过的独立立即执行指令,只允许句号/感叹号收尾;不得剥离引号、方括号或代码片段,动作前后也不得携带 brief、条件、否定、选择、确认、费用、延迟或其它文本。复杂风格需求先单独描述,再由下一条独立确认消息授权,不能用开放式 deny 词表推断付费同意。同一进程重复水合相同 stable turn 时,“回合仍在运行”只作为非终态占用提示,不得以该 turn 的稳定 assistant messageId 持久化并覆盖原执行结果。DirectProject 的 cwd、sandbox writable root 与文件批准根只允许 canonical 且非 symlink/reparse point 的真实 `game/`,canonical 项目根的原生 OS 路径字节和权威 manifest `projectId` 经域标签及独立长度前缀编码后共同绑定连接池与 thread 身份;项目根、`assets/`、`.agent/` 不可写,网络关闭,命令、MCP 扩权和额外权限批准全部拒绝。受控 `agc_tools` 只在客户端内部从同一真实 `game/` cwd 反查已校验的 canonical 项目根,不把项目根加入 Codex writable roots。Codex 不获得任意 Tauri invoke、Token/Key/Cookie;`resources` 也只投影稳定身份与相对路径,不返回 prompt、provider route、URL 或绝对路径。 - Direct 恢复 claim:同一 App 实例重复水合相同 stable turn 并收到“仍在运行”时,必须释放该 `projectPath + clientTurnId` 的恢复 claim,且不得写稳定 assistant 终态。后续显式刷新对话可按原身份重新读取或续跑;不新增无界自动重试。 - 严格图集崩溃收口:workflow 在严格图集调用前先持久化 `strictSpritesheetPending` 并冻结底层严格事务覆盖的九项旧合同身份;旧路径可精确冻结为缺失。Provider 完成结果先绑定原 retained stage ledger。恢复在同一项目锁内对账严格事务;只有新九项合同、规范图/背景图替换锚点与 retained spritesheet result 三者一致才补写 `completed`,旧九项合同才允许补偿。旧合同判定、写 `compensating`、恢复两项素材与登记、回读和清锚点必须在同一项目锁内,重启已有 `compensating` 也重新判定;第三种混合、漂移或 foreign result 状态进入 reconciliation。不能在主图集与四切片已整体提交后仍按两文件 rollback 制造混合包;若中断前阶段告警尚未进入 durable completed result,恢复结果追加“原阶段告警无法完整重放”的明确 warning,不静默清空。 -- Direct 对话恢复从新到旧扫描全部合法 User 回合,遇到较新已回答回合继续向前,不得丢失更早未回答回合。成功返回时 Rust 已先持久化 assistant,前端冗余 append 失败也不得重跑 Provider;普通错误终态的显式 append 失败后,恢复 claim 必须保持到 React fallback writer 对同一稳定 assistant messageId 的写入明确成功或失败,不能在 writer 尚在途时按旧 `/history` 快照重跑。fallback 成功后释放 claim;fallback 失败时跳过该 writer 的无界迟到重试并释放 claim,后续显式 `/history` 才可复用原稳定 `clientTurnId`。终态收敛后删除 claim,避免长会话无界增长。 +- Direct 对话恢复从新到旧扫描全部合法 User 回合,遇到较新已回答回合继续向前,不得丢失更早未回答回合。成功返回时 Rust 已先持久化 assistant,前端冗余 append 失败也不得重跑 Provider;普通错误终态的显式 append 失败后,恢复 claim 必须保持到 React fallback writer 对同一稳定 assistant messageId 的写入明确成功或失败,不能在 writer 尚在途时按旧会话快照重跑。fallback 成功后释放 claim;fallback 失败时跳过该 writer 的无界迟到重试并释放 claim,后续显式重新加载对话才可复用原稳定 `clientTurnId`。终态收敛后删除 claim,避免长会话无界增长。 - 正式资源提交结算遵守同一顺序:阶段三 commit 成功后先持久化 `asset-commit-settlement-pending`,恢复器幂等补齐 `asset-durable-committed` 公开投影与 staging revision,再发布私有终态;公开投影已经存在时不得重复增加草稿 revision。恢复必须把私有回执与阶段三 commit ledger、transaction journal、manifest 资产和事件 payload 的完整身份绑定,任一错配都保留 pending 并失败关闭。回归同时覆盖三个 durable write cut,以及私有回执、commit ledger、journal 错配。 ## 2026-08-24 AGC Direct 抠图语义工具 @@ -8642,7 +8568,7 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在 - 应用级日志持续写入 Tauri AppData 并滚动;报告系统完全忽略项目 `.agent/logs`、源码、prompt、配置、项目产物和截图。用户只可补充文字描述。 - 用户点击独立“报告问题”面板并确认后,批量提交当前进程事件和可取消的脱敏应用日志;失败只允许当前进程手动再次提交。 - 上传接口为登录态 `/api/error-reports`,后台新增 error-reports Tab、专用文件化诊断包、状态与受控下载;管理员查看/下载进入审计链路。 -- `/bug-report` 仅作为打开该面板的快捷入口,追加简短提示,不再生成包含项目、run 或截图口径的缺陷模板。 +- “报告问题”入口只打开该面板并追加简短提示,不再生成包含项目、run 或截图口径的缺陷模板。 - 2026-08-31 追加:事件 DTO 精简为 `eventId/fingerprint/source/message/stack/occurredAt/count`,提交请求携带 `submissionId` 做幂等。归档固定为 `events.jsonl`,服务端使用 `agc/error-reports/v1/{batchId}.zip` 私有 OSS key;元数据只保留 batch、用户、状态、大小、SHA-256 和 OSS key,事件正文/说明/日志从归档读取。OSS 不可用或上传失败时不写数据库,客户端可重新提交。 - 2026-09-01 追加:`application.log` 不再写结构化错误事件;Rust `app_log!` 和 WebView console 都写入普通文本 raw log,结构化事件仅保留在当前进程内,提交时才生成 ZIP 内的 `events.jsonl`。 - 2026-09-01 review 收口:错误报告修复详情请求竞态、下载 anchor 生命周期、客户端采集脱敏/指纹降级与 4xx 噪声、用户级幂等隔离、`agc` 私有 OSS 前缀越权、日志读取链接检查、ZIP 同名日志和元数据/归档清理一致性;同步在 `review.txt` 标注仍需产品/运维决定的架构项。 @@ -8706,7 +8632,7 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在 - 每个 Genarrative 用户在认证成功后都必须幂等准备独立 Router 账号:api-server 使用管理员 Token 创建随机密码普通用户,查询用户 ID,设置用户 `group=taonier`,登录、创建或复用固定标识 `agc_auto_generate` 的无限额度 Token(Token/API Key 使用 `default` 分组;发现旧 Token 为其它分组时先更新为 `default`)并签发 API Key。Router 账号用户名、随机密码、access token(如需)和 API Key 作为一个服务端加密 bundle 保存到 `llm_router_account.credential_ciphertext`,脱敏账号信息和 API Key 核心字段保存到 `llm_router_account`;客户端和普通用户永远不可见 Router Key。管理员 Token 仅存在 api-server 私有配置,不写入数据库或日志;Router 凭据只来源于这条正式账号流程。 - 该账号 provisioning 使用持久 saga 状态:远端注册、登录、token 或 Key 签发结果不确定时进入 `unknown` / `reconciliation_required`,禁止重复注册;远端 Key 已确定签发但本地 `llm_router_account` 写入失败时保持 `key_issued`,后续使用确定 key id 重试落库。Router 确定返回 401/403 时撤销当前 Key 并把账号状态置为 `retryable`,复用已保存的账号密码重新签发替代 Key。 - AGC 调用固定为客户端 access token -> api-server -> Router。计费读取账号 `used_quota`,每 50000 quota 扣 1 泥点,美元数值乘 10、不乘汇率。首次模型调用前以当前累计额度完整建立免追扣基线,之后调用前后同步;扣钱包、写 `llm_router_consume` 流水与推进已结算额度同事务完成。小数和余额不足未支付部分继续累计,失败或重复同步不推进已结算额度,不使用本地 WAL 或余数队列。完整合同见 `docs/technical/【技术方案】LLM累计额度结算-2026-09-05.md`。 -- AGC 状态面收口:Tauri `check_game_creator_llm_config`、`/llm-status` 与 `/llm-routes` 只返回账号凭据状态、官方路由锁定状态和运行参数;不序列化 Router 地址、模型、协议名或任何密钥/凭据字段,内部固定路由仅留在运行时配置与服务端代理中。 +- AGC 状态面收口:Tauri `check_game_creator_llm_config` 只返回账号凭据状态、官方路由锁定状态和运行参数;不序列化 Router 地址、模型、协议名或任何密钥/凭据字段,内部固定路由仅留在运行时配置与服务端代理中。 ## 2026-09-01 LLM Router provisioning 环境隔离与测试门禁 diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 9de45201a..58ef5983a 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -4570,9 +4570,9 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/` - 现象:`npm run agc:test:chat` 在进入聊天前报“Agent Runner 版本与当前客户端不一致,但旧 Runner 仍有任务,暂不能重启”;正式客户端仍能看到自己的待确认或委派任务,重复执行测试也持续失败。 - 原因:Runner 复用身份同时绑定协议版本和当前可执行文件 SHA-256。`cargo run` 重新编译后的 debug 二进制与正在运行的 release Runner 指纹不同,而旧入口只隔离测试项目、仍把正式 AppData 直接传给 CLI,于是测试会向正式 endpoint 发升级探测。正式 Runner 有 pending action、Provider sidecar、进程会话或非终态队列时拒绝退出是正确的安全门禁,不能通过强退或放宽 idle 判定让测试通过。 -- 处理:正式 AppData 只作只读配置来源。每次人工测试在系统临时根创建 `0700` sentinel 隔离目录,只把主配置和可选 local overlay 私有复制为 `0600` 普通文件;不得复制 endpoint、lock、`.previous` 或其它状态。LLM 检查与 Swarm CLI 全部使用隔离目录。退出时通过内部 CLI 请求 `runner.shutdown_if_idle`,确认隔离 endpoint 消失后才删除配置;仍有任务或无法确认退出时同时保留测试项目和隔离配置并报告路径。正式 Runner 的 PID、bootId、端口和 executable fingerprint 必须保持不变。 +- 处理:正式 AppData 只作只读配置来源。每次人工测试在系统临时根创建 `0700` sentinel 隔离目录,只把主配置和可选 local overlay 私有复制为 `0600` 普通文件;不得复制 endpoint、lock、`.previous` 或其它状态。LLM 检查与端到端测试入口全部使用隔离目录。退出时通过内部 CLI 请求 `runner.shutdown_if_idle`,确认隔离 endpoint 消失后才删除配置;仍有任务或无法确认退出时同时保留测试项目和隔离配置并报告路径。正式 Runner 的 PID、bootId、端口和 executable fingerprint 必须保持不变。 - 验证:单元测试覆盖私有 inode、权限、local overlay、禁止复制 endpoint/lock/备份、符号链接拒绝、sentinel 清理和 endpoint 存在时拒绝删除;真实 smoke 使用隔离 AppData 启动并收束空闲 Runner,前后比较正式 endpoint 身份且确认正式 PID 存活,再检查本轮 `/tmp` 项目和隔离配置均已清理。 -- 关联:`apps/ai-game-creator-shell/scripts/agent-swarm-test-chat.mjs`、`apps/ai-game-creator-shell/tests/agentSwarmTestEntry.test.ts`、`apps/ai-game-creator-shell/src-tauri/src/runner/client.rs`、`apps/ai-game-creator-shell/src-tauri/src/cli.rs`。 +- 关联:`apps/ai-game-creator-shell/src-tauri/src/runner/client.rs`、`apps/ai-game-creator-shell/src-tauri/src/cli.rs`。 ## Swarm 队列 busy 不能直接当成 canonical run 可 steer @@ -4580,7 +4580,7 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/` - 原因:旧 `runtime_is_busy` 同时包含当前 state 和队列汇总,调用方看到 `task_queue.pending > 0` 后仍从 canonical state 反推 steer、失败扫描和 turn report 的 runId;取消 tombstone 还会让恢复扫描在处理 A 后无条件跳过 B。底层拒绝 terminal steer 和保留 A 的真实失败历史都是正确行为,不能通过放宽门禁或删除历史记录修复。 - 处理:保留 queue busy 用于 Runner 存活判断,另由 Runtime 协议层提供唯一 steerable 判定。start mutation 返回实际 `acceptedRunId`,CLI 以它建立不可变 turn baseline;失败、reconciliation、用户交互、收束和报告只观察该 run。canonical 已推进到后续 run 时从 task journal 读取目标 run 的最终记录。旧 cancelled canonical 若仍有 pending 且无 running,恢复扫描跳过旧 run 的 pending action 恢复,直接启动队首 pending。若输入与已落盘 pending task 及最后一条 user 消息相同,则只观察原 run。Goal 路径也必须核对同一 Agent、Session、runId、Run Profile 和 steerable 状态。连续 run 的回复必须按确定性 finalization message ID 过滤;历史 specialist 失败必须以 `(agentId, runId)` 为键读取完整 journal,不能让滞后的非失败 state 删除 journal 已记录的失败;报告计数也不能退回 `recent_tasks` 的 12 条窗口。 - 验证:构造 cancelled run A、保留 A cancel tombstone、pending run B 和单份已落盘用户消息,证明恢复后 B 进入 running 并完成且 conversation 不重复。另覆盖观察 B 时忽略 A 及 A 子任务失败、观察 A 时仍正常失败、B 完成后 canonical 已推进到 C 仍可从 journal 收束 B、`turn.report.parentRunId` 始终为 baseline,以及 expected Goal runId 不一致时不选中目标。 -- 关联:`apps/ai-game-creator-shell/src-tauri/src/swarm_cli/turn_dispatch.rs`、`apps/ai-game-creator-shell/src-tauri/src/swarm_cli/terminal_classification.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/steering.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_protocol/steering.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/recovery_scan.rs`。 ## 2026-07-25 autonomous-game-build 不能只检查 game/index.html 就宣称正式项目完成 @@ -4655,7 +4655,7 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/` ## tool-plan handoff 不能把计划叙述和源码字段当成配置载荷扫描 -- 现象:Provider 已返回 HTTP 200 并计费,tool-plan lifecycle 却只有 `started`,handoff 账本停在上一 loop,Runtime 进入 `needs-reconciliation`;重启 Runner 或 `/resume` 后仍原样被屏障阻断。 +- 现象:Provider 已返回 HTTP 200 并计费,tool-plan lifecycle 却只有 `started`,handoff 账本停在上一 loop,Runtime 进入 `needs-reconciliation`;重启 Runner 或运行显式恢复(`--agent-resume`)后仍原样被屏障阻断。 - 原因:在解析 function arguments 之前,对整段 `response.text` 和序列化 arguments 统一执行 `.env`、`game-creator.config` 等字面标记扫描。安全叙述如“无需读取 `.env`”,或 `oldText / newText / content / patch` 中的普通源码字面量,会在真实路径和内容字段尚未区分时被误判。原始响应未成功交接时不会留下正文,因此现场只能结合 loop 边界和最小复现定位,不能把高概率分支冒充已恢复的原响应证据。 - 处理:计划叙述与规范源码内容字段只检查真实密钥 token 形状、凭据头标记和不安全控制字符;结构化敏感 JSON key、非内容字段的配置痕迹和绝对路径、真实 token、容量、thinking、身份、顺序及账本完整性继续失败关闭。成功 handoff 失败时只在 Runtime event/state 和 Agent DB 保存受控 `failureKind`、脱敏错误 SHA-256、字符数与 requestId,禁止保存正文、arguments、密钥和绝对路径。 - 验证:必须同时覆盖 narrative 和 `oldText / newText / content / html / patch` 提及 `.env` / `game-creator.config` 可 round-trip,`path=.env.local` 与 `sk-...` 真实 token 仍拒绝,全部 handoff 回归通过;诊断审计必须断言不存在 `error / response / arguments` 原文。修复后的外部 Provider 重试仍需新起独立轮次,不能与故障轮或确定性回归拼接为 PASS。 @@ -4663,10 +4663,10 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/` ## 自然语言“继续修复”不能在无活动 Runtime 时落入空 resume -- 现象:旧 run 已取消且 `/resume` 明确报告无可恢复任务,用户随后输入“继续”“继续之前干的事情”或“那就继续修复”,interaction 仍返回 `resume`,宿主反复扫描后不新建任务。 -- 原因:interaction 模型能看到会话历史,却不知道宿主已经排除了 active、pending 和可 steer Runtime;宿主又把自然语言 `resume` 与显式 `/resume` 当成相同控制动作机械执行。 -- 处理:活动 Runtime、排队任务和 Goal 仍在 interaction 前优先 steer/恢复;只有这些门禁全部为空后,自然语言 interaction 返回的 `resume` 才规范化为 `execute` 并创建新 run。显式 `/resume` 继续保持纯恢复控制,不因无任务而隐式执行。 -- 验证:保留自然语言必须进入统一 interaction loop 与显式 `/resume` 命令测试,并新增无活动 Runtime 时 `Resume -> Execute`、普通 reply 不变的回归。 +- 现象:旧 run 已取消且显式恢复入口明确报告无可恢复任务,用户随后输入“继续”“继续之前干的事情”或“那就继续修复”,interaction 仍返回 `resume`,宿主反复扫描后不新建任务。 +- 原因:interaction 模型能看到会话历史,却不知道宿主已经排除了 active、pending 和可 steer Runtime;宿主又把自然语言 `resume` 与显式恢复入口当成相同控制动作机械执行。 +- 处理:活动 Runtime、排队任务和 Goal 仍在 interaction 前优先 steer/恢复;只有这些门禁全部为空后,自然语言 interaction 返回的 `resume` 才规范化为 `execute` 并创建新 run。显式恢复入口(`--agent-resume`)继续保持纯恢复控制,不因无任务而隐式执行。 +- 验证:保留自然语言必须进入统一 interaction loop 与显式恢复入口测试,并新增无活动 Runtime 时 `Resume -> Execute`、普通 reply 不变的回归。 ## tool-plan 中项目内绝对路径应在成功交接时规范化 @@ -5427,7 +5427,7 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/` - 同进程恢复补充:命中“同一 stable turn 仍在运行”后除禁止写 assistant 终态外,还必须删除当前 App 实例的恢复 claim。这样原调用随后成功时显式刷新能读取其终态,随后失败时也能按相同 `clientTurnId` 再次续跑;不要靠重载 WebView 清理进程内 claim,也不要用无界定时轮询制造并发调用。 - 严格图集崩溃补充:规范图和背景图的两文件 rollback 不覆盖严格图集事务已经整体修改的 `.agent/manifest.json`、私有回执、公开清单、主图集、四切片和切片清单。必须在严格调用前持久化 pending 及九项旧合同身份;重启恢复先对账底层严格事务,完整新合同直接收口完成,完整旧合同才补偿前两阶段,混合或漂移状态失败关闭。不要在严格提交成功后局部恢复前两张图。 - 部分旧包补充:rollback 的规范图/背景图必须保存旧字节与旧 manifest entry,不能把这两项缺失隐式当成空内容;显式 `regenerate` 因此只在这两项可信可回滚时开放。历史主图集、私有回执、公开清单或 canonical 切片可以缺失,但八个严格路径与受管顶层 asset identity 必须逐项冻结其真实 `Present/Some` 或 `Missing/None` 状态,补偿也必须恢复相同存在性。不要因为旧美术包缺切片而阻断重生成,也不要把本轮新建的严格文件误记成旧文件。 -- 对话扫描与 claim 补充:历史中出现 `User A / User B / Assistant B` 时,B 已回答不代表 A 已回答,扫描必须继续寻找 A。成功 Direct 回复在 Rust 返回前已经落盘,前端冗余 append 失败不能据此重跑;普通错误回复的显式落盘失败时,恢复 claim 要保持到 React fallback writer 的同一 messageId append 明确收敛。writer 成功或明确失败后才释放;失败路径要停止该消息的自动迟到重试,再由显式 `/history` 复用原 stable turn。终态后及时删除 claim,避免 Set 无界增长。 +- 对话扫描与 claim 补充:历史中出现 `User A / User B / Assistant B` 时,B 已回答不代表 A 已回答,扫描必须继续寻找 A。成功 Direct 回复在 Rust 返回前已经落盘,前端冗余 append 失败不能据此重跑;普通错误回复的显式落盘失败时,恢复 claim 要保持到 React fallback writer 的同一 messageId append 明确收敛。writer 成功或明确失败后才释放;失败路径要停止该消息的自动迟到重试,再由显式重新加载对话复用原 stable turn。终态后及时删除 claim,避免 Set 无界增长。 ## GDD 历史审批回执误触发当前恢复提示(2026-08-27) @@ -5929,7 +5929,7 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/` - **现象**:`chat_with_game_creator_direct_codex` 的 Tauri 入参曾并列带一个 `prompt: String`(前端把 canonical content 渲染成 `@显示名` 文本),Rust 侧从不读取——`cargo check` 直接报 `src/agent/direct_runtime/user_input.rs` 的 `unused variable: prompt`;前端每次提交仍要算一遍,界面测试也钉着这份投影文本。 - **成因**:DirectProject 回合真正的输入只来自 canonical `userItem`。`direct_codex_user_item_to_codex_turn_input`(`agent/direct_codex_user_item/wire.rs`)在 DirectProject 分支重建 `turn/start.input`,`LlmRunRequest` 里那份 `user_prompt` 会被覆盖;客户端投影走的是另一套口径(素材被删或改名时退化成裸 resourceId),一旦有人把它接回 Codex,就把 `@显示名` 标签污染了模型输入。 -- **现行口径**:IPC 只传 `projectPath`、`clientTurnId`、`userItem` 与可选 `creationType`;Rust 只从 `userItem` 派生回合输入(空判定与三维契约探测用的派生 prompt 仍在 Rust 内部生成)。前端那份 `@显示名` 投影只服务本地的 `/history` 识别与队列 chip 文案,不出 IPC;界面断言只能读 `userItem.content`。 +- **现行口径**:IPC 只传 `projectPath`、`clientTurnId`、`userItem` 与可选 `creationType`;Rust 只从 `userItem` 派生回合输入(空判定与三维契约探测用的派生 prompt 仍在 Rust 内部生成)。前端那份 `@显示名` 投影只服务本地的历史条目识别与队列 chip 文案,不出 IPC;界面断言只能读 `userItem.content`。 - **关联**:`apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/user_input.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_user_item/wire.rs`、`apps/ai-game-creator-shell/src/view/project-development/chat/controller/useDirectProjectChatController.ts`、`apps/ai-game-creator-shell/tests/appSurface/chat-composer.suite.ts`。 ## 2026-09-21 DirectProject 结构化消息不能逐个拒绝空白文本片段 diff --git a/docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md b/docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md index b07266394..e0d311d4f 100644 --- a/docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md +++ b/docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md @@ -361,7 +361,7 @@ npm run ai-game-creator-shell:agent-runtime:real-e2e -- --config-dir - - 历史账本曾在下一固定窗口合并旧账本和新里程碑,并用 `runtime.context` / `runtime.milestones` 代替部分普通 observation。 - checkpoint 内容 diff 与 Git 工作树 diff 使用两个独立保护槽。后续 `git.inspect` 不得再挤掉已完成 `project.diff` 的 checkpointId/hunk,反之亦然;两类大 detail 仍共同受 128 KiB context bundle 总上限约束。 -当时修复后的真实 `gpt-5.5` 回归在 10 轮内完成并收束,唯一 spawn / patchset / join 均保持 1 次,两次 Git 审阅和两类 diff 同时留在最终 context bundle,重复副作用为 0。当前 Runtime 每 6 轮只做进度 checkpoint 与停滞检测,完整 observation 继续保留在私有 bundle;真正摘要只由 V1.21 token 阈值或显式 `/compact` 触发,副作用去重继续以规范 action receipt 与各类 durable barrier 为准。 +当时修复后的真实 `gpt-5.5` 回归在 10 轮内完成并收束,唯一 spawn / patchset / join 均保持 1 次,两次 Git 审阅和两类 diff 同时留在最终 context bundle,重复副作用为 0。当前 Runtime 每 6 轮只做进度 checkpoint 与停滞检测,完整 observation 继续保留在私有 bundle;真正摘要只由 V1.21 token 阈值或显式手动压缩触发,副作用去重继续以规范 action receipt 与各类 durable barrier 为准。 ## V1.6 持久动作回执与模型回查 @@ -661,21 +661,6 @@ V1.14 对标 `codex fork`,允许开发者从任意已有静态 Agent 会话创 确定性验收必须覆盖 active / archived / legacy 源、空会话、消息与 messageId 精确复制、源与分叉后续隔离、provenance 持久化、运行中父任务和委派 child 阻断、非 active 源任务阻断、损坏 task journal 失败关闭、默认 Session Runtime 入队与分叉线性化、未提交分叉文件不可见、非法源 ID、catalog 写入失败清理以及重复点击创建不同 Session。前端测试必须证明按钮调用精确源 Session、成功后加载复制历史并切换 active、后续消息写入新 Session 且源会话不变、归档源可分叉、Runtime 忙时按钮禁用。 -## V1.15 Agent Swarm 纯聊天验证入口 - -V1.15 新增不依赖 Tauri WebView 或正常客户端 GUI 的终端聊天入口,用于开发阶段直接验证多 Agent 协作。入口固定为 `--swarm-chat [--init] <本地项目绝对路径> [parentAgentId]`;省略 `parentAgentId` 时固定使用 `project-supervisor`,推荐通过 `npm run agc:chat -- --config-dir <项目外 AppData 绝对路径> [--init] ` 启动总控聊天。需要直接调试其他父 Agent 时仍可使用 `npm run agc:swarm -- --config-dir <项目外 AppData 绝对路径> [--init] `。它不是新的 Agent 实现:每条普通输入都投递给现有父 Agent background Runtime,继续由同一发布二进制的 External Runner 执行;不得退化到一次性 `--agent-chat`,也不得新建本地 HTTP 服务、旁路 Provider 客户端或第二套持久化。 - -- 首版复用父 Agent 当前 active Session;Runtime 继续把 user / assistant 写入 `.agent/conversations/agents//sessions/.jsonl`,静态委派、动态 `child-*`、私有记忆、项目黑板、durable action、verification gate 和 all-join 均沿用现有身份与恢复语义。`--init` 只复用现有项目初始化函数;Runtime 写命令仍强制显式传项目外 `--config-dir`。入口启动和一轮准备收束前都执行现有 resume / reconciliation 扫描,不能只凭 idle 快照跳过尚未发布的 receipt 或 join 修复。 -- 终端只提供聊天所需的轻量控制命令:`/help`、`/agents`、`/status`、`/history`、`/quit`。空闲时普通文本创建父 Agent 新 run;父 Agent run 仍处于 pending / running 时普通文本追加为同一 run steer,只有 child 忙而父 Agent 已终态时拒绝吞掉输入并要求稍后重发。确认动作在终端显示 Agent、run、action、tool 和安全摘要,并接受 `approve / reject`,分别调用现有 confirm / reject Runtime 路径,不能要求回到开发窗口。stdin 由独立读取线程投递,因此活跃 run 中 `/quit`、EOF、状态命令和 steer 仍可响应;退出只结束观察客户端。 -- 每轮轮询 `read_game_creator_agent_runtimes_at`,按 Agent / run / event 去重输出状态、phase、委派来源、父 Agent、delegationId、动态 child 和 join / receipt 事件。Provider token delta 当前没有经过 Runner RPC 暴露,首版只承诺 Runtime 状态与事件的持续输出以及持久化后的最终父 Agent 回复,禁止用拆字或延时打印伪装 token streaming。 -- 一轮只有在所有已发现 Runtime 都不处于 `pending / running / waiting-for-confirmation / cancelling / needs-reconciliation`,全部任务队列为空,并持续经过稳定观察窗口后才能收束。父 run 暂时 idle 但 delegated child 尚未终态、receipt 尚未入队或 all-join 尚未认领时不得提前返回。失败、取消和 reconciliation 要明确显示并保留项目现场,不自动重试副作用。 -- 每轮进入 `settled` 或 `needs-reconciliation` 终态后,终端必须额外输出且只输出一条 `[turn.report] <单行 JSON>`,schema 固定为 `game-creator-swarm-turn-report.v1`。报告只从本轮 authoritative conversation 与 Runtime snapshot 计算,白名单字段至少包含 outcome、父 Agent/Session/run 身份、Runtime 忙闲数量、四类任务队列计数、新增 assistant 数量、最终回复字符数和 reconciliation Agent 数量;不得包含项目路径、对话/任务/回复正文、observation、prompt、event detail、隐藏 thinking、Provider payload、凭据或本地存储路径。该行用于开发终端和真实 E2E 定位一轮边界,不替代 task/event/delivery/claim/receipt/conversation 等持久事实;`/quit` 不伪造 turn report。 -- 终端退出只结束观察客户端,不终止 External Runner、已投递 run 或 Runner-owned process session;下次启动先调用现有 resume,再从 conversation 与 Runtime journal 恢复。首版只允许一个前台输入流,不承诺多个终端并发编辑同一 active Session。 - -确定性验收必须覆盖 CLI parse、项目绝对路径与 `--config-dir` 门禁、`--init`、空输入和 EOF、命令分流、历史恢复、连续两轮写入同一 Session、状态与事件去重、两个静态 Agent 并行委派、多个隔离 child 并行与唯一 all-join、confirm / reject、父 Agent receipt 汇总、稳定窗口不早退、Runner / 终端重启恢复以及失败与 reconciliation 显示。真实 Provider 验收必须保存一份脱敏 transcript,并以 task / event / Agent DB / receipt / conversation 的结构化事实证明并行、最终父回复唯一、副作用无重放和密钥零泄漏;未实际运行时只能标记未验收,不能凭确定性测试宣称 swarm 可用。 - -2026-07-14 首轮真实 `gpt-5.5` 验证已证明终端入口能够启动真实 External Runner、持久化父 Session、实时展示状态 / event / parent / delegation、并行运行 `design-foundation` 与 `balance-seed`,并在终端完成两次 `agent.delegate` approve、一次重复委派 reject、重启恢复、receipt 续跑、`file.write` reject 和活跃 run 中 `/quit`。独立收束复验在同一 `code-prototype` Session 连续完成 4 轮固定回复,重启后 `/history` 读取 8 条 user / assistant 消息,最后一轮按 `idle -> 安静窗口 -> receipt/join 恢复扫描 -> 安静窗口 -> 最终回复` 返回 `FOURTH_OK`。但端到端 swarm 汇总未通过:第一轮父 Agent 反复调用全量 `agent.run_status`,因输出截断无法看到目标 Agent,18 轮后 `budget-exhausted` 并压掉两条排队 receipt;第二轮按 receipt 模式续跑时,父 Agent 没有恢复原始“只读汇总”目标,转而读取项目并请求写 `game/balance.json`,已由终端拒绝。当前结论只能是“V1.15 验证入口可用、现有静态委派的父回执汇总策略未验收”,不得标记完整 Agent Swarm 通过;后续需修复定向状态查询或 receipt continuation 原目标恢复后再跑唯一最终父回复验收。动态 isolated child / all-join 也仍待通过该入口真实复验。 - ## V1.16 Project Supervisor 总控 Agent V1.16 把正式用户主聊天从一次性自然语言问答升级为现有 External Runner 中的根协调 Agent。规范 Runtime ID 固定为 `project-supervisor`,显示名为“项目总控 Agent”;它不是 manifest 任务、专业组角色或 isolated spawn 模板,不加入 `GAME_CREATOR_AGENT_GROUP_DEFINITIONS`。恢复扫描必须固定包含该 ID。LLM 首选 `agentLlm.project-supervisor`,发布 AppData 仍只有旧 `agentLlm.chat` 时把它作为兼容回退并继续继承全局配置,不复制或暴露 API Key。 @@ -767,7 +752,7 @@ OpenAI Chat / Responses 的 strict function schema 顶层固定为 `thinkingSumm ## V1.18 单 Agent 持久 Goal mode -V1.18 对标 Codex CLI `/goal` 的长任务语义:目标文本既是首轮任务,也是后续完成判断的上层标准。Goal 不是 `currentGoal` 的展示别名,也不建立第二套 Runner;它绑定一个 Agent active Session 和同一 run,复用现有持久计划、steer、工具策略、确认、verification gate、context bundle、finalization 与 External Runner。 +V1.18 的持久 Goal 长任务语义:目标文本既是首轮任务,也是后续完成判断的上层标准。Goal 不是 `currentGoal` 的展示别名,也不建立第二套 Runner;它绑定一个 Agent active Session 和同一 run,复用现有持久计划、steer、工具策略、确认、verification gate、context bundle、finalization 与 External Runner。 ### Goal 身份与持久化 @@ -777,7 +762,7 @@ V1.18 对标 Codex CLI `/goal` 的长任务语义:目标文本既是首轮任 ### 启动、编辑与运行隔离 -- `/goal <文本>`、Tauri start command 或 `--agent-goal-start [--init] --stdin` 创建 Goal 并用同一 outcome 启动首个 background run;CLI 的 `--init` 只在 manifest 缺失时初始化一次性/新项目,不借道其它 Agent task。未显式提供 verification 时,outcome 自身作为完成标准。Goal 活跃或暂停期间,当前 Session 禁止另起不相关 run;后续普通输入默认继续走同 run steer,独立任务应使用另一 Session。 +- Tauri start command 或 `--agent-goal-start [--init] --stdin` 创建 Goal 并用同一 outcome 启动首个 background run;CLI 的 `--init` 只在 manifest 缺失时初始化一次性/新项目,不借道其它 Agent task。未显式提供 verification 时,outcome 自身作为完成标准。Goal 活跃或暂停期间,当前 Session 禁止另起不相关 run;后续普通输入默认继续走同 run steer,独立任务应使用另一 Session。 - 编辑 Goal 先在项目写锁内提交 revision,再把规范化的新目标作为同 run steer 持久化。Provider 正在 planning 时允许中断;确认中或工具执行中只排队,旧动作在下一安全边界前必须校验 Goal revision,不能在目标已变更后继续执行。旧自动动作或待确认动作若绑定旧 Goal 快照,统一转成 `blocked` observation 并在同一 run 重规划,不执行旧副作用,也不创建 retry run。编辑失败不得回退已提交 revision,Runtime 会以 sidecar 为事实源重规划并拒绝旧 finalization。 - Goal 内容进入每轮 planning/final reply 的显式“持久目标”上下文。模型仍通过 V1.17 `planUpdate` 维护可观察步骤;Goal revision 不推进 project revision、不改变权限或 verification gate,也不能放宽 sandbox/approval。 @@ -798,7 +783,7 @@ V1.18 对标 Codex CLI `/goal` 的长任务语义:目标文本既是首轮任 ### 控制面与验收 -- 纯聊天入口支持 `/goal <文本>`、`/goal status`、`/goal pause`、`/goal resume`、`/goal edit <文本>`、`/goal clear`;开发 Agent UI 使用 `执行 / 聊天 / 目标` 三段模式,Goal 创建/编辑通过独立弹层提交,并在状态行显示 outcome、revision、状态与完成标准,提供暂停/恢复/清理。普通用户 Supervisor 首页不暴露开发 Goal 管理控件。 +- 开发 Agent UI 使用 `执行 / 聊天 / 目标` 三段模式,Goal 创建/编辑通过独立弹层提交,并在状态行显示 outcome、revision、状态与完成标准,提供暂停/恢复/清理;CLI 通过 `--agent-goal-status`、`--agent-goal-start`、`--agent-goal-edit`、`--agent-goal-pause`、`--agent-goal-resume`、`--agent-goal-clear` 管理同一 Goal。普通用户 Supervisor 首页不暴露开发 Goal 管理控件。 - background planning / final reply 的专用 Provider 客户端强制 `max_retries=0`;每个 `agent.runtime.provider_request.lifecycle` 从 `started` 到唯一 `completed / failed / interrupted` 最多对应一次物理请求。`Timeout / Connectivity / Transport / EmptyResponse / 408 / 429 / 5xx` 以及无法证明请求未被上游接收的其它错误,不得在同一 lifecycle 内自动原样重放;只记录 error kind、SHA-256、字符数或脱敏摘要。显式 steer、Goal resume 或人工 reconciliation 决定再次调用时,必须使用新的 request slot/lifecycle;格式修复同样使用 `loop--repair-` 新 slot,不能伪装成底层 retry。 - 每次 request snapshot 固定绑定 `projectId / agentId / taskId / sessionId / runId / source / goalId / goalRevision / goalSnapshotFingerprint / appliedSteerCursor / requestKind / requestSlot`,requestId 从该闭集稳定派生。真正进入 Provider future 前,Runtime 在同一项目写锁内重读 task/Runtime 身份、queued steer、cancel tombstone、规范 Goal 状态与快照;已生效控制只返回未启动,不得写伪 `started`。Provider lifecycle 的生产字段闭集只允许 `recordType / auditSchemaVersion / agentId / taskId / sessionId / runId / source / requestId / requestKind / requestSlot / status`,持久层只可再添加统一 `schemaVersion / updatedAt` envelope;不包含 prompt、工具输入、URL、模型、回复或错误正文。 - 启动新请求前必须在 Agent DB 锁内全量扫描同 Agent/run 的 Provider lifecycle,不依赖 recent tail。只要发现 `started` 后没有可信唯一终态,就把原 run/task/state 收束到 `needs-reconciliation` orphan barrier,阻断后续 Provider、工具和 finalization;同 request 多终态、缺 started、物理顺序倒置、重复阶段、身份/字段冲突或额外生产字段同样失败关闭,禁止自动补发。paused Runner 重启窗口必须以 started 数量零增长证明没有暗中请求,不能只看 plan/action 是否落盘。 @@ -819,7 +804,7 @@ V1.19 对标 Codex 富客户端的增量 turn 事件:工具开始、完成和 ### 流身份与私有快照 - 新增 `game-creator-runtime-response-stream.v1` 私有快照,路径固定为 `.agent/runtime/response-streams//.json`,两个 hash 都取稳定身份 SHA-256 十六进制前 32 位。记录绑定 `agentId / taskId / sessionId / runId / requestKind=final-reply / requestSlot / appliedSteerCursor / responseRevision`,并保存单调 `sequence`、`status=streaming|ready|committed|discarded|failed`、`accumulatedText`、可选 `finishReason` 和时间。正文最多 32000 字符;路径、标识、schema、状态、sequence 和正文限制任一不合法时只关闭该展示流,不能把不可信内容显示给用户或据此恢复 Runtime。 -- 流快照是可丢失的本地展示缓存,不是 assistant、Provider lifecycle、任务完成或 finalization 的事实源。写入采用同路径原子替换并允许节流;Tauri 关闭、CLI 断线或单次快照写失败不能让已经可靠完成的 Provider 请求失败。`AgentRuntimeResult.responseStream` 只在快照与当前 Runtime 的 Agent/Session/run、`phase=response|finalizing|completed`、steer cursor 和请求身份一致时返回。 +- 流快照是可丢失的本地展示缓存,不是 assistant、Provider lifecycle、任务完成或 finalization 的事实源。写入采用同路径原子替换并允许节流;Tauri 关闭、客户端断线或单次快照写失败不能让已经可靠完成的 Provider 请求失败。`AgentRuntimeResult.responseStream` 只在快照与当前 Runtime 的 Agent/Session/run、`phase=response|finalizing|completed`、steer cursor 和请求身份一致时返回。 - 开始新的 final-reply request slot 时先写空 `streaming` 快照。SSE delta 只在经过增量 `...` 过滤后追加;标记可跨 chunk,未闭合 thinking 永不外显。sequence 只随公开 accumulated text 或 finish reason 的真实变化增加。Provider 完整返回后用最终 `strip_llm_thinking_blocks` 结果校准为 `ready`,确保草稿与最终候选一致。 ### Provider、控制与 finalization 边界 @@ -829,14 +814,14 @@ V1.19 对标 Codex 富客户端的增量 turn 事件:工具开始、完成和 - 完整候选仍必须通过 verification、plan、Goal、process/join/delegate 和项目 revision 门禁。`finish_game_creator_agent_background_runtime_turn_at` 仍是唯一 finalization 入口;只有 assistant 已按稳定 messageId 恰好一次写入并完成 Runtime 投影后,快照才可标记 `committed`。失败消息和 `plan.response` fallback 必须覆盖为其实际候选,不能保留不同 Provider 草稿。 - 公共 event、Agent DB、receipt、activity/output 和报告不得复制 delta 或 accumulated text,只记录流身份、状态、sequence、字符数和 SHA-256。conversation、finalization、Runtime task/state 和 response-stream 都是本地私有事实面,可保存 canonical 最终正文;其中 response-stream 只是可丢失候选缓存,且不得保存 API Key、请求头、URL、模型 thinking、工具计划或原始 Provider error。 -### 客户端与 CLI +### 客户端 - 普通 Project Supervisor 聊天把匹配的 `streaming|ready` 快照渲染成一条 `runtimeOwned` 临时 assistant 消息;刷新、窗口重开和 Tauri event 丢失时由现有 750ms Runtime 轮询恢复。`committed` 后以 conversation 中的规范 assistant 替换草稿,不把临时消息写回 legacy project conversation 或 Agent Session。 -- `agc:chat` / `agc:swarm` 按 accumulated text 前缀增量打印 UTF-8 suffix;新 request slot、非前缀校准或 reconnect 要明确重置。已经完整流出的父回复在 settle 时只补完成换行/状态,不再整段重复打印。`/status` 只显示流状态、sequence 和字符数,不显示隐藏 planning 或 thinking。 +- 开发窗口按 accumulated text 前缀增量打印 UTF-8 suffix;新 request slot、非前缀校准或 reconnect 要明确重置。已经完整流出的父回复在 settle 时只补完成换行/状态,不再整段重复打印。流式状态行只显示流状态、sequence 和字符数,不显示隐藏 planning 或 thinking。 ### 验收口径 -- 确定性测试覆盖 Chat / Responses SSE 至少两个真实 delta、chunk 边界 thinking 过滤、sequence 单调、32K 上限、损坏/错身份快照不显示、Tauri 轮询恢复、CLI suffix/reconnect/非前缀重置、steer/取消/失败旧流失效、非流配置单次 ready、finalization 后唯一 assistant 与 committed 精确一致,以及公共持久面零正文。 +- 确定性测试覆盖 Chat / Responses SSE 至少两个真实 delta、chunk 边界 thinking 过滤、sequence 单调、32K 上限、损坏/错身份快照不显示、Tauri 轮询恢复、suffix/reconnect/非前缀重置、steer/取消/失败旧流失效、非流配置单次 ready、finalization 后唯一 assistant 与 committed 精确一致,以及公共持久面零正文。 - 真实 Provider 使用一次性项目和独立 AppData,把目标 Agent 的 `stream=true`,证明首次公开 delta 发生在 Provider/finalization 终态之前、至少两个非空增量可观察、最终 conversation assistant 与 ready/committed 全文一致、同一 lifecycle 不发生应用层重试,并扫描密钥、thinking canary、项目绝对路径和 delta 正文在 event、Agent DB、receipt、activity/output 与报告等公共面泄漏为 0。上游物理请求数无法直接观测时,必须明确记录证明模式,不能把 lifecycle 计数冒充网络请求计数。 - 2026-07-15 真实 `gpt-5.5` `response-stream` suite 已 PASS:隔离 AppData 只以 hardlink 读取正式配置并使用无密钥 `stream=true` overlay,正式配置 CLI 调用为 0、源 Runner endpoint 未变化。39 个不同非空 streaming 快照先于终态,sequence 从 1 单调推进到 418,最终以 425 committed;canonical 正文 883 字,conversation 恰好 1 条 user 和 1 条 assistant,final-reply lifecycle 恰好 1 组 `started -> completed`,fallback replay、重复 message/receipt 均为 0。该次上游物理请求计数未直接观测,证明模式为 lifecycle slot 与 canonical response identity 交叉核对。公共正文、API Key、thinking、诱饵、项目绝对路径及 transcript/report 路径泄漏均为 0;隔离 Runner 由 Linux pidfd 精确停止,AppData 和一次性项目按 sentinel 清理。 @@ -874,7 +859,7 @@ V1.20 对标 Codex CLI 的可选 Web Search,但只声明当前 `platform-llm` ## V1.21 单 Agent token-aware 持久上下文压缩 -V1.21 对标 Codex CLI 的 `model_context_window`、`model_auto_compact_token_limit`、`tool_output_token_limit` 和 `/compact`。它替换“固定保留最近 12 条就算压缩”的能力口径,但不删除原始 conversation、task、event 或工具事实,也不把模型摘要提升为 Goal、计划、权限、验证或副作用事实源。 +V1.21 对标 Codex CLI 的 `model_context_window`、`model_auto_compact_token_limit`、`tool_output_token_limit` 和显式手动压缩。它替换“固定保留最近 12 条就算压缩”的能力口径,但不删除原始 conversation、task、event 或工具事实,也不把模型摘要提升为 Goal、计划、权限、验证或副作用事实源。 ### 配置与预算 @@ -899,18 +884,18 @@ V1.21 对标 Codex CLI 的 `model_context_window`、`model_auto_compact_token_li ### 自动与手动入口 - 每次 background tool-plan 构建后先计算输入估算。超过解析后的 `autoCompactTokenLimit` 时,在同 Agent/Session/run 的安全 planning 边界压缩可压缩 prefix,重建请求并再次估算;重建后仍超阈值或没有新的可压缩 prefix 时失败关闭并给出配置/新 Session 建议,不能继续发送已知超限请求。 -- `agc:chat` / `agc:swarm` 新增 `/compact`,开发 Agent 窗口提供同一动作和状态。手动压缩只允许当前 Agent active Session 没有 in-flight Provider、执行中工具、待确认动作或未收束 Runtime 时进行;有活动 run 时由自动安全边界处理,不能从 UI 直接打断副作用。正式用户 Project Supervisor 页面不增加压缩按钮。 -- `/status` 和开发面板展示 `estimatedInputTokens / autoCompactTokenLimit / lastPromptTokens / lastCompletionTokens / compactionRevision / lastCompactedAt`。手动和自动都写相同的哈希/计数审计,公共 event、Agent DB、receipt、activity/output 和报告不得出现 summary、原 conversation/observation、任务、路径或凭据正文。 +- 显式手动压缩入口为 `--agent-context-compact`,开发 Agent 面板展示压缩状态。手动压缩只允许当前 Agent active Session 没有 in-flight Provider、执行中工具、待确认动作或未收束 Runtime 时进行;有活动 run 时由自动安全边界处理,不能从 UI 直接打断副作用。正式用户 Project Supervisor 页面不增加压缩按钮。 +- 终端流式状态行和开发面板展示 `estimatedInputTokens / autoCompactTokenLimit / lastPromptTokens / lastCompletionTokens / compactionRevision / lastCompactedAt`。手动和自动都写相同的哈希/计数审计,公共 event、Agent DB、receipt、activity/output 和报告不得出现 summary、原 conversation/observation、任务、路径或凭据正文。 ### 验收口径 -- 确定性测试覆盖配置默认值与 per-Agent 继承、预算非法组合、token 估算包含 function schema、工具输出限额、自动阈值、手动 `/compact`、最近 tail 保留、同源幂等、追加后 revision 单调、conversation/observation 前缀篡改失败关闭、sidecar/bundle 身份冲突、summary 上限和公共审计零正文。 +- 确定性测试覆盖配置默认值与 per-Agent 继承、预算非法组合、token 估算包含 function schema、工具输出限额、自动阈值、手动压缩、最近 tail 保留、同源幂等、追加后 revision 单调、conversation/observation 前缀篡改失败关闭、sidecar/bundle 身份冲突、summary 上限和公共审计零正文。 - Provider 生命周期测试覆盖 started 前退出可安全重试、started 未终态进入 reconciliation、completed 后 sidecar 缺失零重放、sidecar 已提交后恢复复用,以及 Goal、计划、steer、pending、verification 和副作用身份压缩前后逐字段相同。 - 真实 Provider 使用隔离 AppData 和 disposable 项目完成至少 30 轮多轮任务,跨越至少两次压缩和一次 Runner 强杀;证明 planning 始终低于阈值、原 Agent/Session/run 身份稳定、已完成工具零重放、最终 assistant 唯一、summary 能引用早期用户约束,且全部公共持久面 API Key、原始对话/observation、项目绝对路径和 summary 正文泄漏为 0。未完成该长链路前只能记录确定性通过,不能宣称 V1.21 整体 PASS。 2026-07-15 使用正式 AppData 的 `openai_chat / gpt-5.5` 路由和隔离 Runner 执行 `context-compaction` suite,V1.21 整体 **PASS**。同一 Agent active Session 完成 30/30 轮、60 条 conversation message 和 30 个唯一 assistant audit;两次真实 Provider compaction 形成 revision 1/2,30 个 tool-plan 与 2 个 compaction request 共 32 组 lifecycle,全部唯一 `started -> completed`,fallback replay 为 0。Runner 通过 Linux pidfd `SIGKILL` 后 boot 变化、Session 身份保持稳定,早期用户显式约束可从最终回复召回;最大估算输入 29134,低于 64000 自动阈值。公共 task/event/Agent DB/conversation/report 中原始正文、summary、API Key、诱饵、项目绝对路径和正式配置路径泄漏均为 0,重复 message/audit、工具执行与 finalization journal 均为 0;隔离 AppData 和 disposable 项目按 sentinel 清理。首轮复验在第 22 轮收到 Provider `transport` 终态并按规则 FAIL,未自动重放;新 disposable 项目完整重跑后取得上述 PASS。 -真实长链收口时同步修正四项实现边界:显式用户约束由确定性保留层逐字钉住并继续做密钥/绝对路径脱敏;`runtime.compact` 单独使用 6 分钟 IPC 响应窗口,其他 Runner 方法仍保持 10 秒;普通后台任务公共审计只保存 `taskChars + taskSha256`,任务正文仅留在私有 task ledger/conversation;每 6 轮只做进度 checkpoint 与停滞检测,真正摘要只由 token 阈值或显式 `/compact` 触发。终态旧 context bundle 仅允许在完整 schema、身份、Goal、revision、verification、observation、sidecar 和 steer 校验通过后刷新 legacy plan 投影差异。 +真实长链收口时同步修正四项实现边界:显式用户约束由确定性保留层逐字钉住并继续做密钥/绝对路径脱敏;`runtime.compact` 单独使用 6 分钟 IPC 响应窗口,其他 Runner 方法仍保持 10 秒;普通后台任务公共审计只保存 `taskChars + taskSha256`,任务正文仅留在私有 task ledger/conversation;每 6 轮只做进度 checkpoint 与停滞检测,真正摘要只由 token 阈值或显式手动压缩触发。终态旧 context bundle 仅允许在完整 schema、身份、Goal、revision、verification、observation、sidecar 和 steer 校验通过后刷新 legacy plan 投影差异。 ## V1.22 Runner-owned MCP 动态工具 @@ -939,7 +924,7 @@ V1.22 对标 Codex CLI 的 MCP tool 能力,在现有单 Agent Runtime 内增 ### 开发入口与验收 -- 开发配置面板管理 MCP server,敏感字段沿用密码输入;保存前完成本地结构校验,连接测试走 Runner,不由 WebView 直接联网或启动进程。`agc:chat` / `agc:swarm` 提供 `/mcp` 查看 server 状态和有界工具目录;正式用户 Supervisor 首页不展示 MCP 配置或调试正文,但 Runtime 可以按已配置策略使用工具。 +- 开发配置面板管理 MCP server,敏感字段沿用密码输入;保存前完成本地结构校验,连接测试走 Runner,不由 WebView 直接联网或启动进程。MCP 状态与有界工具目录只通过开发配置面板和真实 E2E 门禁核验;正式用户 Supervisor 首页不展示 MCP 配置或调试正文,但 Runtime 可以按已配置策略使用工具。 - 确定性测试覆盖两种 transport、initialize/instructions/tools list、allow/deny、审批映射、动态 schema/token 预算、配置/catalog 漂移、required/optional 失败、超时、Runner 强杀、pending/reconciliation、结果 sidecar、二进制降级和全部公共零正文。 - 真实本地 E2E 使用一次性 STDIO fixture 与 Streamable HTTP fixture,各自让真实 Provider 发现并调用至少一个只读工具;再让一个有副作用 fixture 停在确认、批准后只执行一次,并在调用窗口强杀 Runner 证明零重放。报告必须证明 tool schema 来自 MCP、server instructions 被标为不可信、同一 Agent/Session/run 身份稳定、结果可回灌、重复调用/assistant 为 0,且凭据、arguments、结果正文、项目/配置绝对路径公共泄漏为 0。 @@ -968,10 +953,10 @@ V1.23 对齐 Codex Plan/Goal 在任务未完成时主动澄清并进入 `Needs i - durable pending action 增加 `waiting-for-user-input`。Runtime state/phase 同名,保持原 Agent/task/Session/run/Goal、结构化计划和 steer cursor;不完成 active plan step、不生成 finalization、不消费下一任务。普通 steer 在该状态被拒绝,回答只能走精确 request API。 - Runner 重启时:无 sidecar可安全补建;`pending`/`answer-prepared` 修复缺失的幂等 conversation 并继续等待;`answered` 从 sidecar 全量重算 observation 后继续原 run;任一身份、问题、答案或会话消息冲突进入 `needs-reconciliation`。取消将未回答请求标记 cancelled;Goal pause 保留请求,恢复后仍回到 Needs input。 -- `AgentRuntimeResult` 只在 owning Session 当前 run 暴露一个 pending request。Project Supervisor 主聊天、开发 Agent 窗口和 `agc:chat` 展示同一结构化问题;桌面端按题提供 2-3 个选项和自由输入,全部必答后才能提交。提交中禁用重复操作,刷新/切 Agent/重启后从 sidecar 恢复。 +- `AgentRuntimeResult` 只在 owning Session 当前 run 暴露一个 pending request。Project Supervisor 主聊天与开发 Agent 窗口展示同一结构化问题;桌面端按题提供 2-3 个选项和自由输入,全部必答后才能提交。提交中禁用重复操作,刷新/切 Agent/重启后从 sidecar 恢复。 - 确定性验收覆盖 schema/预算、sole-action、child deny、幂等 create/answer、responseId 冲突、会话中间失败、Runner 强杀三窗口、Goal pause/resume、cancel、普通 steer 拒绝、跨 Agent/Session/run/request 回答拒绝和公共零正文。真实 Provider 必须在复杂任务中自主提问,用户回答后同 run 完成唯一最终回复,并验证问题/答案各一条、Provider 未在等待期调用、Runner 强杀后零重复。 -2026-07-16 使用正式 AppData 的 `openai_chat / gpt-5.5` 路由执行隔离 `user-input-runtime` suite,V1.23 真实验收 **PASS**。Project Supervisor 自主发起 1 个含 2 个选项的结构化问题,等待期使用 Linux pidfd 强杀 Runner 并换 boot 恢复;Provider started 记录在重启前后保持 `1 -> 1`,未暗中请求。回答后保持同一 Agent/Session/run,会话恰好为 1 条初始任务、1 条 assistant 问题、1 条 user 回答和 1 条最终 assistant;全程 2 个 Provider request identity 均唯一闭合,重复 message、遗留 finalization、公共问题/答案正文、API Key、项目/配置路径和报告泄漏均为 0,隔离 Runner、AppData 和一次性项目已清理。 +2026-07-16 使用正式 AppData 的 `openai_chat / gpt-5.5` 路由在隔离 AppData 与一次性项目上完成 V1.23 真实验收 **PASS**。Project Supervisor 自主发起 1 个含 2 个选项的结构化问题,等待期使用 Linux pidfd 强杀 Runner 并换 boot 恢复;Provider started 记录在重启前后保持 `1 -> 1`,未暗中请求。回答后保持同一 Agent/Session/run,会话恰好为 1 条初始任务、1 条 assistant 问题、1 条 user 回答和 1 条最终 assistant;全程 2 个 Provider request identity 均唯一闭合,重复 message、遗留 finalization、公共问题/答案正文、API Key、项目/配置路径和报告泄漏均为 0,隔离 Runner、AppData 和一次性项目已清理。 ## V1.24 Codex 式 scoped `AGENTS.md` 仓库指令 @@ -1028,7 +1013,7 @@ V1.27 在 V1.26 一次最多三个原生 action 的基础上,让同一 Agent ## V1.28 Project Supervisor 合同委派与单回复收束 -V1.28 收紧 V1.16 的静态专业 Agent 协作协议:`project-supervisor` 是正式用户唯一默认对话 Agent,也是唯一可以向正式用户提交最终回复的 Agent;静态专业 Agent 与 isolated child 只向父 run 交付内部回执、摘要和证据。开发窗口仍可直调单个专业 Agent,`agc:swarm` 仍可显式指定其它父 Agent 做调试,但这些入口不构成正式用户对话或第二条用户回复。若本节与 V1.16 或实施计划中的旧表述冲突,以本节为准。 +V1.28 收紧 V1.16 的静态专业 Agent 协作协议:`project-supervisor` 是正式用户唯一默认对话 Agent,也是唯一可以向正式用户提交最终回复的 Agent;静态专业 Agent 与 isolated child 只向父 run 交付内部回执、摘要和证据。开发窗口仍可直调单个专业 Agent,但这些入口不构成正式用户对话或第二条用户回复。若本节与 V1.16 或实施计划中的旧表述冲突,以本节为准。 **状态:PASS。合同委派、结构化回执、单层 repair、Supervisor finalization、正式用户 GUI 接入和 Runtime 显式瞬时重试均已落地;2026-07-17 正式 `openai_chat / gpt-5.5` `supervisor-swarm` 已完成双专业 Agent 真并行、唯一 repair、pidfd Runner 强杀恢复、唯一 Supervisor assistant、零重复与零泄漏的完整验收。** @@ -1137,41 +1122,41 @@ repair 深度固定为 `1`;同一原 delivery 同时最多存在一个非 `sup 最终加强版正式 `openai_chat / gpt-5.5` 复验 PASS:46 个 Provider request identity 全部形成唯一终态,`46 started / 46 terminal / 45 completed / 1 failed`,恰好 1 条 retry audit;失败 attempt 与后继 `-transient-1` 使用不同 request identity,Agent/task/Session/run/source/request kind 保持一致。forwarding gate 放行前 action、receipt、专业子委派、claim、assistant、pending、project revision 和 upstream forwarding 均为 `0`。代理观察到的 10 个目标 Agent 请求与该 Agent lifecycle 数量一致,其中 1 个注入失败、1 个暂停、9 个转发。放行后仍完成 2 个初始专业 Agent 真重叠、2+1 delivery、2 个 Observed claim、1 次 targeted contract read、唯一 repair、pidfd Runner 强杀/boot 恢复、5 步父计划、唯一 Supervisor assistant 与 3 条内部专业 assistant;27/27 成功计划和 14/14 repair 均为 `native_runtime_tools`。重复、残留 sidecar、Provider payload、私有正文、API Key、项目/正式配置路径、报告、secret 与 lure 泄漏均为 `0`;source-dir suite-prefix guard 与 `sourceAppDataDirectoryUntouched` 证明正式 AppData 未被写入,物理请求/lifecycle 一一对应和失败 partial checkpoint 门禁均通过,代理、隔离 Runner/AppData/项目全部清理。 -该受控 suite 是 V1.28 协议与恢复的故障注入门禁,不替代后续自主 Swarm 验收。现有 fixture 明确给出两个专业方向、同轮要求和一次 repair 上限;“Supervisor 在不提供 Agent ID、并行配方或 repair 次数时自主选择编排”仍需独立 `supervisor-swarm-autonomous` 真实 suite 证明。真实 `--swarm-chat`、同一 run 的 static delivery + isolated all-join 组合以及 Tauri/WebView 宿主级 Supervisor GUI 也仍是单独完成项。 +该受控 suite 是 V1.28 协议与恢复的故障注入门禁,不替代后续自主 Swarm 验收。现有 fixture 明确给出两个专业方向、同轮要求和一次 repair 上限;“Supervisor 在不提供 Agent ID、并行配方或 repair 次数时自主选择编排”仍需独立自主编排真实 Provider 验收证明。同一 run 的 static delivery + isolated all-join 组合以及 Tauri/WebView 宿主级 Supervisor GUI 仍是单独完成项。 -## V1.30 Project Supervisor 自主终端协作验收 +## V1.30 Project Supervisor 自主协作验收 -V1.30 新增独立 `supervisor-swarm-autonomous-chat` 真实 Provider suite,同时证明 Project Supervisor 的自主专业编排和正式 `agc:chat / --swarm-chat` 入口。它复用 V1.28 的 static delivery/claim/repair、External Runner、隔离 AppData、确认、恢复、finalization 和唯一回复事实源,不新增 Agent、调度器、Provider 客户端或第二套对话持久化。现有 `supervisor-swarm` 与 `supervisor-swarm-transient-retry` 继续分别承担固定协议链和受控瞬态故障门禁,不能被本 suite 替代。 +V1.30 通过真实 Provider 验收证明 Project Supervisor 的自主专业编排。该验收复用 V1.28 的 static delivery/claim/repair、External Runner、隔离 AppData、确认、恢复、finalization 和唯一回复事实源,不新增 Agent、调度器、Provider 客户端或第二套对话持久化。现有 `supervisor-swarm` 与 `supervisor-swarm-transient-retry` 继续分别承担固定协议链和受控瞬态故障门禁,不能被该验收替代。 - 唯一用户任务只能表达业务结果,例如把试玩项目推进到可交给首批玩家体验并汇报交付、验证和风险;任务不得出现静态 Agent ID、Agent 数量、同轮/并行要求、planning 轮次、返工/repair 次数、原生工具名、run/action/delegation 身份或 Runner 操作。一次性仓库规则只描述玩家体验规格、发布质量记录、语义验收、修改后验证和安全边界;不得指定由哪个 Agent 承担、必须同批委派、必须返工几次或调用什么工具。 - fixture 提供一项缺失的体验规格和一项“客观文件/验证存在但语义仍不满足”的质量记录。质量记录在初始阶段必须先独立审阅再允许修改;harness 只通过项目 policy 暂时拒绝质量角色写入,弱回执被父 run 认领后解除 policy,不发送 steer、不改业务文件、不补充新任务。初始弱回执必须是 `completed + evidence-ready` 且无缺失产物,确保后续 repair 来自 Supervisor 对 acceptance criteria 的语义判断,而不是 Runtime 自动把客观失败标成 `needs-repair`。 -- `--swarm-chat --init ` 必须由真实发布二进制启动,省略 parentAgentId 后进入 `project-supervisor`;用户任务通过 stdin 发送,所有确认也经同一终端 `approve` 入口完成。允许 Supervisor 先做必要读取,但首个包含专业委派的 native Provider 批次必须自主选择至少两个不同规范专业 Agent,并在同批形成两个初始合同;两个 child 的真实 Provider lifecycle 必须重叠,不能用同一 Agent 的 retry/format repair 或仅凭 delegate action 时间冒充并行。 +- 真实 Provider 验收必须由真实发布二进制启动 `project-supervisor`,用户任务只表达业务结果。允许 Supervisor 先做必要读取,但首个包含专业委派的 native Provider 批次必须自主选择至少两个不同规范专业 Agent,并在同批形成两个初始合同;两个 child 的真实 Provider lifecycle 必须重叠,不能用同一 Agent 的 retry/format repair 或仅凭 delegate action 时间冒充并行。 - 弱质量 claim 进入 `Observed` 后,Supervisor 必须在同一父 Session/run 自主创建引用原 delivery 的唯一 repair;目标 Agent、acceptanceCriteria 和 expectedArtifacts 必须完整继承,repair action 必须晚于弱 claim、早于唯一最终回复。repair 待确认边界继续执行 pidfd Runner 强杀与 boot 恢复,任务、delivery、claim、pending action 和 Provider started 身份不得漂移或重放。 -- 终局必须同时满足:严格 host oracle 判定两项产物语义正确,最后修改后的验证凭证有效;`[turn.report]` 为 v1/settled、父 Agent/Session/run 与 journal 一致、新增 assistant 恰好 1、队列和 reconciliation 计数为 0;正式用户会话只有 1 条 user 和 1 条 Supervisor assistant,专业 assistant 仅留在内部 Session;无 steer、重复 delivery/action/message/receipt/Provider lifecycle、残留 sidecar、Provider payload、私有正文、API Key、诱饵、项目/正式配置路径或报告泄漏,隔离 Runner/AppData/项目全部清理。任何一次带更明确提示的重跑都只能算新的失败后尝试,不能与原 run 拼接成 PASS。 +- 终局必须同时满足:严格 host oracle 判定两项产物语义正确,最后修改后的验证凭证有效;父 Agent/Session/run 与 journal 一致、新增 assistant 恰好 1、队列和 reconciliation 计数为 0;正式用户会话只有 1 条 user 和 1 条 Supervisor assistant,专业 assistant 仅留在内部 Session;无 steer、重复 delivery/action/message/receipt/Provider lifecycle、残留 sidecar、Provider payload、私有正文、API Key、诱饵、项目/正式配置路径或报告泄漏,隔离 Runner/AppData/项目全部清理。任何一次带更明确提示的重跑都只能算新的失败后尝试,不能与原 run 拼接成 PASS。 - `agent.message` 的语义身份固定绑定来源 Agent/run、目标 Agent/已解析 Session 和清洗截断后正文 SHA-256。同一语义消息重放只能复用唯一 conversation message 与 `agent.runtime.agent.message` 审计,并以 `messageAppended=false` 返回 durable no-op;不同正文、目标、Session、来源 Agent 或来源 run 仍是新消息。该 no-op 不得计入上下文窗口的新进展,也不能替代专业 Agent 自身最终回执。若模型持续重复同一消息,Runtime 最迟在当前完整 6 轮停滞窗口结束时写 `failed / budget-exhausted / loop-budget-exhausted`,保留原 `in_progress` 计划,不写 completed 或伪造成功回复;每个尝试的 action/observation/receipt 仍须完整落账且公共 receipt 不保存消息正文。 -2026-07-17 最终正式 `openai_chat / gpt-5.5` 诊断轮 **PASS**。唯一业务任务未提供 Agent ID、Agent 数量、并行、工具、repair 或 Runner 配方;Supervisor 在 1 个 native planning 批次自主选择 2 个不同专业 Agent,真实 Provider 区间重叠,并在同一父 Session/run 完成 `2` 份初始 delivery、`1` 次语义 repair、`2` 个 Observed claim、严格宿主验证、pidfd Runner 强杀、boot 切换和身份稳定恢复。最终 `[turn.report]` 为 `settled`,正式会话新增 Supervisor assistant 恰好 `1`,内部专业 assistant 为 `3`;父计划 `4/4` completed,pending/running/confirmation/user-input/reconciliation 均为 `0`。 +2026-07-17 最终正式 `openai_chat / gpt-5.5` 诊断轮 **PASS**。唯一业务任务未提供 Agent ID、Agent 数量、并行、工具、repair 或 Runner 配方;Supervisor 在 1 个 native planning 批次自主选择 2 个不同专业 Agent,真实 Provider 区间重叠,并在同一父 Session/run 完成 `2` 份初始 delivery、`1` 次语义 repair、`2` 个 Observed claim、严格宿主验证、pidfd Runner 强杀、boot 切换和身份稳定恢复。正式会话新增 Supervisor assistant 恰好 `1`,内部专业 assistant 为 `3`;父计划 `4/4` completed,pending/running/confirmation/user-input/reconciliation 均为 `0`。 该轮共形成 110 条 task、197 条 event、330 条 Agent DB 和 9 条会话消息;51 个 Provider request identity 全部唯一闭合为 `51 started / 51 terminal / 51 completed / 0 failed`,28/28 个成功工具计划和 19/19 个格式修复均为 `native_runtime_tools`,wrapper/text fallback 为 `0`。delivery、message、action lifecycle、executing action、receipt、Provider lifecycle 的重复计数均为 `0`,所有 batch/finalization/confirmation/user-input sidecar 为 `0`,Provider payload、私有正文、API Key、诱饵、项目/正式配置绝对路径和报告泄漏均为 `0`。隔离 AppData 自动清理;保留的 disposable 项目经 sentinel/进程核对后手动删除。此前两次独立尝试在 `maxRetries=0` 下各遇到 1 次外部 Provider 终态失败并在恢复边界前停止,均只作失败证据,未与本轮拼接。 确定性 `background_agent_runtime_bounds_duplicate_agent_message_livelock` 同时证明:6 次同指纹 Runtime action/observation/receipt 全部实际落账,目标 conversation、`conversation.message` 和 `agent.runtime.agent.message` 各仅 1 条,后 5 次为 durable no-op,第 6 轮保留 `in_progress` 计划并进入 `budget-exhausted`,不存在第 7 次 Provider 请求、context compaction 或 completed 投影。 -V1.30 至此只证明自主 static 专业编排与真实终端聊天可组合;同一父 run 的 static delivery + isolated all-join 真实组合恢复,以及 Tauri/WebView 宿主级 Supervisor E2E 仍需各自独立门禁。 +V1.30 至此只证明自主 static 专业编排;同一父 run 的 static delivery + isolated all-join 真实组合恢复,以及 Tauri/WebView 宿主级 Supervisor E2E 仍需各自独立门禁。 ## V1.31 Project Supervisor 静态与隔离子 Agent 混合协作门禁 -V1.31 新增独立 `supervisor-swarm-static-isolated-autonomous-chat` 真实 Provider suite,用于验证同一个 `project-supervisor` Session/run 可以自主同时使用 static `agent.delegate` 与 dynamic `agent.spawn_isolated(joinMode=all)`,并由同一个完成屏障、恢复链和 finalization journal 唯一收束。该 suite 复用 V1.28-V1.30 的 static delivery/claim/repair、isolated group/result/join delivery、External Runner、`--swarm-chat`、隔离 AppData 和零泄漏事实源,不新增调度器、对话入口、结果 sidecar 或第二种用户回复。 +V1.31 通过真实 Provider 验收证明同一个 `project-supervisor` Session/run 可以自主同时使用 static `agent.delegate` 与 dynamic `agent.spawn_isolated(joinMode=all)`,并由同一个完成屏障、恢复链和 finalization journal 唯一收束。该验收复用 V1.28-V1.30 的 static delivery/claim/repair、isolated group/result/join delivery、External Runner、隔离 AppData 和零泄漏事实源,不新增调度器、结果 sidecar 或第二种用户回复。 - 用户任务只明确业务范围包括仓库既有的正式交付、临时检查和实际验证,不得出现 Agent ID、数量、并行、static/isolated、delegate/spawn/join、repair 次数、run/action 身份或 Runner 操作。一次性仓库规则可声明既有验证要求和安全禁用边界,但不得指定 Agent 编排工具、调用顺序或 Runner 配方。Supervisor 系统策略要求提交首个协作批次前分别枚举长期专业交付与临时隔离检查;两类都非空时不得遗漏任一类。 - 首个形成协作的 native Provider 批次必须包含两个不同 static `agent.delegate` 与一个 `agent.spawn_isolated`;spawn 请求固定 `joinMode=all`,三个 child 的 expectedArtifacts 指向三个既有证据文件,writeScopes 互不重叠。批次必须先停在 `waiting-confirmation / nextActionIndex=0`,唯一 `provider_action_batch.confirmation_required` 与唯一 approval 都绑定 spawn 和原批次,approval 必须早于三个 action 的任何真实副作用;随后每个 action 的 side effect、observed、receipt 和终态 observation 严格按 `actionIndex` 推进。两个 static child 的 Provider 区间必须真实重叠,且至少一个 static child 与一个 isolated child 的 Provider 区间也必须真实重叠,不能用 action 时间、同 Agent retry 或格式修复冒充并行。 - static 与 isolated 继续使用各自 durable 事实源。static 的 2 份初始 delivery 与 1 份 repair 分别由两个 Observed claim 认领 2/1 份 receipt;isolated 形成 1 个 group、3 个唯一 instance/result、1 个 all-join delivery,并由同一父 run 的一个 `agent.run_status` action 认领。两类记录的 parent Agent/Session/run 必须一致;该 suite 要求 isolated child 的项目 mutation action 和实际文件修改均为 0,但这不是把生产 isolated 权限模型改成只读沙箱。 - completion blocker 在项目锁内先检查 plan/Goal,再按 `provider-action-batch -> process -> isolated join -> static receipts` fail closed,随后检查 response revision 和 verification。单一 waiting phase 只是当前首个 blocker 的 UI 投影,不是事实源;Runner 恢复和每次 finalization 都必须重新枚举两类 barrier。`project_supervisor_mixed_waiting_recovery_does_not_plan_until_all_join_ready`、`project_supervisor_mixed_waiting_recovery_does_not_plan_until_static_delivery_ready` 与 `project_supervisor_mixed_run_status_recovery_reuses_partial_isolated_claim_after_revision_drift` 三条确定性回归分别覆盖双向等待切换和同 action 部分认领恢复,不能代替真实 Provider suite。 -- repair 待确认动作持久化后执行 pidfd Runner 强杀。强杀前后必须逐项比较 static delivery/claim、isolated group/instance/result/join delivery、父 task/context、pending action 和完整 Provider started identity set;boot 必须变化,任何 child/action/receipt/join 不得重放。static claim 与 isolated join claim 的 observation 都必须早于父 finalization prepared,父计划、验证、确认、用户输入、process、两类 barrier 全部清零后,原 Supervisor run 才能写唯一 assistant 和 `turn.report=settled`。 +- repair 待确认动作持久化后执行 pidfd Runner 强杀。强杀前后必须逐项比较 static delivery/claim、isolated group/instance/result/join delivery、父 task/context、pending action 和完整 Provider started identity set;boot 必须变化,任何 child/action/receipt/join 不得重放。static claim 与 isolated join claim 的 observation 都必须早于父 finalization prepared,父计划、验证、确认、用户输入、process、两类 barrier 全部清零后,原 Supervisor run 才能写唯一 assistant。 - 最终报告必须单独给出两类 Provider 重叠、group/instance/result/join/claim、两类 parent identity、跨恢复身份稳定、isolated 项目修改、重复 continuation/group/join/action/receipt、残留 sidecar 和公共泄漏计数。任何一项缺失、从不同尝试拼接、用户/仓库规则含编排配方、isolated 修改项目、未认领即 final 或额外用户回复都必须 FAIL;确定性回归或 V1.30 static PASS 不能替代该门禁。 2026-07-17 最终正式 `openai_chat / gpt-5.5` 独立轮 **PASS**。该轮形成 149 条 task、261 条 event、451 条 Agent DB 和 14 条会话消息;67 个 Provider request identity 全部唯一闭合为 `67 started / 67 terminal / 67 completed / 0 failed`,37/37 个成功工具计划与 24/24 个格式修复均为 `native_runtime_tools`,wrapper/text fallback 为 0。首批 3 个 action 的 confirmation-required/approval 各 1 且时序有效,static-static 与 static-isolated Provider 区间均真实重叠;static 形成 2 份初始 delivery、1 份 repair 和 2 个 Observed claim,isolated 形成 1 个 group、3 个 completed result、1 个 parent-wake claimed join,全部绑定同一父 Session/run。 -Runner pidfd 强杀后的 boot、父 context、pending action、两类 durable identity 和完整 Provider identity set 均稳定恢复;isolated mutation action、isolated 文件修改、continuation、重复 delivery/group/instance/result/join/claim/message/action/receipt/Provider lifecycle、残留 sidecar和公共正文/凭据/绝对路径/报告泄漏均为 0。`turn.report=settled`,父计划 4/4 completed,正式 Supervisor assistant 恰好 1,内部专业 assistant 3、isolated assistant 3,最终 disposable 项目与隔离 AppData 均自动清理。此前不完整编排、child 合同不满足、外部 Provider 终态失败和调试验收器误判均各自作为独立失败轮停止,未与本轮 PASS 拼接。 +Runner pidfd 强杀后的 boot、父 context、pending action、两类 durable identity 和完整 Provider identity set 均稳定恢复;isolated mutation action、isolated 文件修改、continuation、重复 delivery/group/instance/result/join/claim/message/action/receipt/Provider lifecycle、残留 sidecar和公共正文/凭据/绝对路径/报告泄漏均为 0。父计划 4/4 completed,正式 Supervisor assistant 恰好 1,内部专业 assistant 3、isolated assistant 3,最终 disposable 项目与隔离 AppData 均自动清理。此前不完整编排、child 合同不满足、外部 Provider 终态失败和调试验收器误判均各自作为独立失败轮停止,未与本轮 PASS 拼接。 ## V1.32 Runtime 强制 Supervisor 协作合同 @@ -1189,7 +1174,7 @@ V1.31 证明真实 Provider 可以自主形成 static + isolated 混合协作, 2026-07-17 在最终代码 diff 上使用 `gpt-5.5 / openai_chat / high` 完成 V1.32 独立真实 PASS。`supervisor-swarm-collaboration-policy-mixed-recovery` 只在隔离 AppData 配置副本中把瞬态重试设为 `maxRetries=2 / retryBackoffMs=500`,正式 AppData、源配置和 Runner endpoint 均未修改;86 个 Provider lifecycle 全部完成,本轮未触发重试。首批 v2 batch 固化 3 个 action,包含 2 个指定 static delegate 与 1 个三 child isolated spawn;waiting-confirmation 零副作用边界、pidfd 强杀恢复、batch/contract/action identity、两类 Provider 重叠、1 次 repair、3 个 delivery、1 个 isolated group/3 个 instance/3 个 result/1 个 claimed join、宿主验证和唯一 Supervisor assistant 全部通过。 -成功报告共记录 178 个 task snapshot、326 个 event、556 个 Agent DB record、30 个 action execution 和 38 个 receipt;重复 delivery/group/instance/result/join/claim/message/action/receipt/Provider lifecycle 与 pending/batch/finalization/confirmation sidecar 均为 0,私密正文、Provider payload、API Key、项目路径、正式配置路径和最终报告泄漏均为 0。`turn.report=settled` 且 reconciliation Agent 为 0。49 次 native tool plan 中发生 30 次格式修复,未破坏动作幂等与最终结果,但说明真实链路仍有明显延迟和 Provider 调用成本,后续应单独收敛工具合同表达和 repair 频率。 +成功报告共记录 178 个 task snapshot、326 个 event、556 个 Agent DB record、30 个 action execution 和 38 个 receipt;重复 delivery/group/instance/result/join/claim/message/action/receipt/Provider lifecycle 与 pending/batch/finalization/confirmation sidecar 均为 0,私密正文、Provider payload、API Key、项目路径、正式配置路径和最终报告泄漏均为 0。reconciliation Agent 为 0。49 次 native tool plan 中发生 30 次格式修复,未破坏动作幂等与最终结果,但说明真实链路仍有明显延迟和 Provider 调用成本,后续应单独收敛工具合同表达和 repair 频率。 ## V1.33 原生工具计划 repair 收敛与分类 @@ -1353,7 +1338,7 @@ V1.38 把 collaboration policy 的执行语义从“每次动作或恢复都读 - 2026-07-19 确定性门禁已完成:E2E self-test **PASS**,同时覆盖 modern `provider_action_batch.confirmation_required` 与 legacy `tool_confirmation_required`、requirement/approval/receipt 唯一性、`confirmation` execution mode、目标 Session/run 和严格持久化顺序;snapshot/binding 的终态预期改为由已验真的非 `aborted` v2 collaboration batch 决定,不再用 mixed suite 拓扑代替 durable 事实。`supervisor_collaboration_` 52/52、`provider_action_batch_` 12/12、`project_supervisor_mixed_` 5/5 通过;Tauri/Rust 全量为 949 passed、4 个环境依赖用例按设计 ignored,`check:rustfmt` 通过。 - 确定性覆盖包含 snapshot 的 9 个完整字段、首次 `aborted` 零绑定与 matching binding 的 `aborted` v2 恢复、batch -> snapshot -> binding 双故障窗口、CAS 冲突、篡改 contract、binding/snapshot 丢失组合、contractless/v1 协作批次失败关闭与非协作批次兼容、legacy 非终态迁移与终态拒绝、危险 Agent/run ID 路径及锁隔离、global policy 漂移、已有 claim 恢复与新 claim 失败关闭。新增 `supervisor_collaboration_policy_snapshot_survives_terminal_runtime_cleanup` 证明终态只删除 pending/provider batch/confirmation 等临时 sidecar,snapshot/binding 字节保持不变且 resolver 继续返回 `run-snapshot`。 -- 真实 `supervisor-swarm-static-isolated-autonomous-chat` 曾在单次独立运行中完整形成 2 个 isolated group / 3 个 child、1 个 observed join claim 覆盖两组、唯一 repair、宿主验证、Runner pidfd 强杀恢复和唯一 Supervisor assistant;snapshot/binding 均为唯一、字节及字段稳定,global policy drift 被观察,重复、临时 sidecar、正文、API Key、项目路径和配置路径泄漏均为 0。但该轮运行期间正式客户端在测试外部重启了正式 Runner,source endpoint 所有权门禁按设计失败,因此该功能样本不能记为 PASS。 +- 一次真实混合协作运行曾在单次独立运行中完整形成 2 个 isolated group / 3 个 child、1 个 observed join claim 覆盖两组、唯一 repair、宿主验证、Runner pidfd 强杀恢复和唯一 Supervisor assistant;snapshot/binding 均为唯一、字节及字段稳定,global policy drift 被观察,重复、临时 sidecar、正文、API Key、项目路径和配置路径泄漏均为 0。但该轮运行期间正式客户端在测试外部重启了正式 Runner,source endpoint 所有权门禁按设计失败,因此该功能样本不能记为 PASS。 - 随后使用权限为 `0700/0600`、不含 endpoint/锁/会话的私有配置源副本隔离正式客户端干扰,source endpoint、源目录和清理门禁均稳定;五次最小 OpenAI-chat 探针全部 HTTP 200。然而多次独立完整运行仍在长链路耗尽 transient Provider retry。最后一轮隔离 overlay 已提高到 `requestTimeoutMs=300000 / maxRetries=3 / retryBackoffMs=500`,仍在首个业务批次前形成 4 个 failed lifecycle / 3 个 retry 后终止,child、delivery、claim 和项目 mutation 均为 0,现场清理与泄漏门禁通过。失败轮不得与前述功能完整轮拼接;截至当前,**V1.38 独立真实 Provider E2E 仍未 PASS**,需在外部 Provider 稳定后以最终代码重新独立运行。 ## V1.39 首次规划 Provider 瞬态重试持久等待态 @@ -1447,7 +1432,7 @@ V1.41 为 V1.40 明确留下的成功响应交接窗口增加 `.agent/runtime/pr 2026-07-20 当前最终实现的最新验证证据为:`provider_retry_` 21/21、`response_stream_` 23/23、`finalization_resume_` 12/12;Tauri/Rust 串行全量共 989 tests,`985 passed / 4 ignored / 0 failed`。这些数字替代 V1.40 较早快照,后续当前结果统一使用本行口径。 -上表是 V1.41 的确定性门禁,不代表真实外部 Provider E2E 结论。除表内窗口外,还必须继续扫描 task/event/Agent DB/CLI/report,确认 Provider 响应、compaction summary、API Key、Provider URL 和项目/配置绝对路径公共泄漏均为 0。 +上表是 V1.41 的确定性门禁,不代表真实外部 Provider E2E 结论。除表内窗口外,还必须继续扫描 task/event/Agent DB/report,确认 Provider 响应、compaction summary、API Key、Provider URL 和项目/配置绝对路径公共泄漏均为 0。 ## V1.42 Project Supervisor final-reply 瞬时重试 Runner 强杀真实门禁 @@ -1474,7 +1459,7 @@ npm run ai-game-creator-shell:agent-runtime:supervisor-swarm-final-reply-transie ### 终局门禁与六轮证据 - 恢复前后的父 tool-plan `started` 数必须完全相等,不得为收尾新增 tool-plan。终局只允许唯一成功的 parent final-reply、唯一 Project Supervisor assistant 和唯一 `committed` response stream,stream 身份绑定原 base final-reply slot 且正文与 assistant 完全一致。 -- retry、handoff、finalization artifacts 必须全部为 `0`;重复 delivery/claim/receipt/action/message/lifecycle 必须为 `0`。公共 task/event/Agent DB/CLI/report 中的 Provider/assistant 正文、API Key 和项目/发布配置绝对路径命中必须为 `0`。 +- retry、handoff、finalization artifacts 必须全部为 `0`;重复 delivery/claim/receipt/action/message/lifecycle 必须为 `0`。公共 task/event/Agent DB/report 中的 Provider/assistant 正文、API Key 和项目/发布配置绝对路径命中必须为 `0`。 - 旧 `supervisor-swarm-transient-retry` 继续只证明 Project Supervisor 首次 tool-plan 的持久退避与 Runner 强杀,不能替代本 suite,也不能把它的历史 PASS 外推为 V1.42 final-reply PASS。 - 2026-07-20 确定性与静态门禁已完成:fault proxy `14/14`、E2E self-test **PASS**、前端 `308/308`,以及 shell typecheck、`platform-llm 41/41`、`platform-agent 17/17`、`shared-contracts 7/7` 均通过。 - 真实外部 Provider suite 总计执行六轮,逐轮独立裁决且严禁拼接:第一、二轮沿用既有失败记录,均为 **FAIL**;第三轮已走通故障、持久重试和唯一回复,但验收过早观察到 `1` 个 finalization journal,仍为 **FAIL**,随后改为终态后显式等待 sidecar 全部清零并设置 `10s` 硬超时;第四轮在 quality-review 的普通 tool-plan 连续发生 transport/connectivity 失败并耗尽重试,未进入目标 final-reply 故障,仍为 **FAIL**;第五轮暴露并修复并行 Agent 的 `file.write` 与项目写锁竞争,失败 observation 携带绝对锁路径,继而触发 pending 持久化拒绝并进入 `needs-reconciliation`,仍为 **FAIL**。修复后 `file.write / file.patch / file.delete` 统一使用 Runtime 短等待项目写锁,`file.write` 错误在持久化前脱敏,并新增 `2` 条 Rust 回归测试。 @@ -1729,9 +1714,6 @@ V1.54 的公共编排层可以在运行前构造动态 DAG,但 LLM 在执行 - `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml isolated -- --nocapture --test-threads=1` - `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml project_supervisor_mixed_ -- --nocapture --test-threads=1` - `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml supervisor_collaboration_ -- --nocapture --test-threads=1` -- `npm run agc:collaboration-policy-e2e -- --config-dir ` -- `npm run agc:mixed-swarm-e2e -- --config-dir ` -- `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml swarm_cli::tests -- --nocapture` - `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml typed_goal_pause_and_cancel_require_durable_intent_and_keep_exact_run -- --nocapture` - `npm run ai-game-creator-shell:typecheck` - `npm run test -- apps/ai-game-creator-shell/tests` @@ -1746,14 +1728,12 @@ V1.54 的公共编排层可以在运行前构造动态 DAG,但 LLM 在执行 - `npm run ai-game-creator-shell:agent-runtime:real-e2e -- --config-dir --suite web-search` - `npm run ai-game-creator-shell:agent-runtime:real-e2e -- --config-dir --suite context-compaction` - `npm run ai-game-creator-shell:agent-runtime:real-e2e -- --config-dir --suite mcp-runtime` -- `npm run ai-game-creator-shell:agent-runtime:real-e2e -- --config-dir --suite user-input-runtime` - `npm run ai-game-creator-shell:agent-runtime:real-e2e -- --config-dir --suite scoped-agents` - `npm run ai-game-creator-shell:agent-runtime:real-e2e -- --config-dir --suite project-skill` - `npm run ai-game-creator-shell:agent-runtime:real-e2e -- --config-dir --suite parallel-read` - `npm run ai-game-creator-shell:agent-runtime:real-e2e -- --config-dir --suite supervisor-swarm` - `npm run ai-game-creator-shell:agent-runtime:supervisor-swarm-transient-retry-real-e2e -- --config-dir ` - `npm run ai-game-creator-shell:agent-runtime:supervisor-swarm-final-reply-transient-retry-real-e2e -- --config-dir <发布AppData绝对路径>` -- `npm run ai-game-creator-shell:agent-runtime:supervisor-swarm-autonomous-chat-real-e2e -- --config-dir ` - `npm run ai-game-creator-shell:agent-runtime:real-e2e -- --config-dir --suite full` - `npm run check:encoding` - `git diff --check` diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index 8613a787b..ba820afa9 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -525,10 +525,10 @@ Supervisor 认领该回执后,由父 run 自己为每个原 delivery 逐一创 1. `planRevision=0` 表示尚未建立结构化计划。第一次有效 `planUpdate` 必须完整替换 legacy `plan / planSteps / activePlanStepIndex`,不得保留 legacy 的 `completed / failed` 脚手架步骤。 2. 只有函数入口处已经存在结构化计划时,后续更新才保留 `completed / failed` 终态。结构化 `failed` 仍不可被 Provider 改写,普通工具失败和完成门失败继续 fail-closed;本修复不新增 receipt 特判恢复通道,也不放宽 static smoke、revision、desktop/mobile 试玩或最终交付门。 3. 修复序列回归必须覆盖 `revision 2 smoke failed -> 首份结构化修复计划 -> 同 Run file.patch 到 revision 3 -> 自动重跑当前 revision smoke -> desktop/mobile preview`;同时单独证明已建立结构化计划后的 failed 步骤仍不可变。 -4. Windows real-E2E 把直属 CLI 终态与 stdio 完整关闭拆成两阶段:完整换行的 turn report 是输出边界;直属 CLI `exit/error` 后不再向其 PID 发信号;finally 先按 sentinel、endpoint、boot、进程指纹和稳定 Windows process HANDLE 停止 suite 自有 Runner,再有界等待继承管道关闭。临时 AppData 和 sentinel 必须归当前用户私有 ACL,稳定 HANDLE 自测必须同时证明 close 不杀目标、signal 终止原目标。 +4. Windows real-E2E 把直属子进程终态与 stdio 完整关闭拆成两阶段:完整换行的输出是边界;子进程 `exit/error` 后不再向其 PID 发信号;finally 先按 sentinel、endpoint、boot、进程指纹和稳定 Windows process HANDLE 停止 suite 自有 Runner,再有界等待继承管道关闭。临时 AppData 和 sentinel 必须归当前用户私有 ACL,稳定 HANDLE 自测必须同时证明 close 不杀目标、signal 终止原目标。 5. 验收按“Rust 定向与分组回归 -> real-E2E self-test -> 串行 Rust 全量 -> 前端类型检查 -> 编码和差异门禁 -> 新的独立 `gpt-5.6-sol` / `max` 真实 Provider 空项目轮次”执行。真实轮次必须在同一父/子 Run 中取得当前 revision 的 static smoke、desktop/mobile 试玩和唯一 completed 终态;失败轮不得与后续轮拼接。 6. professional DAG 的图片产物任务必须保持任务文案与计划门禁一致:External Editor 已配置时,`art-director` 是只允许 `canvas.asset_generate` 受控素材事务的非只读视觉任务,不得被通用“只读协调”文案拒绝;`assets/art-spec.png` 的固定 `icon-spec / 1:1` 合同、其它文件写入禁令、mutation revision 和验证凭证均不放宽。 -7. 真实 E2E 报告必须绑定 effective model、API kind 和 endpoint 指纹,并递归核验唯一 Supervisor 根链、全部后代终态、最终入口 SHA-256 与 static smoke 凭证、交互 CLI 与隔离 AppData 的路径泄漏,以及清理后本轮 Runner/helper/Node/browser/command 后代残留为零;任一项无法证明即 FAIL。 +7. 真实 E2E 报告必须绑定 effective model、API kind 和 endpoint 指纹,并递归核验唯一 Supervisor 根链、全部后代终态、最终入口 SHA-256 与 static smoke 凭证、隔离 AppData 的路径泄漏,以及清理后本轮 Runner/helper/Node/browser/command 后代残留为零;任一项无法证明即 FAIL。 8. `reasoningEffort=max` 必须作为独立强度贯通配置校验、`platform-llm` Responses 请求、Provider 适配、Codex 映射、请求指纹和前端配置类型,不得静默降级为 `high` 或 `x-high`。本轮真实验收固定使用 `agentMode=provider`;由于 canonical Agent 会先应用角色默认推理强度,隔离配置必须为 `project-supervisor`、`code-prototype`、`art-director`、`art-asset-plan` 分别显式覆盖 `max`,并由报告中的 effective binding 证明四者一致。 ## 目标 @@ -660,15 +660,13 @@ Agent 可见的系统指令、工具与参数说明、恢复指引和上下文 2026-07-14 起,同一文档的“V1.14 Agent 会话分叉”补齐 `codex fork` 风格的开发会话分支。开发 Agent 窗口可从 active、archived 或 legacy Session 复制截至当前的持久 conversation,创建带来源记录的新 active Session;源会话不变,后续消息与 run 按新 Session 隔离。分叉不复制 Runtime / pending action / process session,不推进项目 revision,并在当前 Agent 或委派 child 未终态时拒绝执行;Session 变更与 Runtime 入队共用 per-Agent lane gate,损坏任务日志失败关闭,catalog 提交前的分叉文件不会被列表暴露。 -2026-07-14 起,同一文档的“V1.15 Agent Swarm 纯聊天验证入口”补充无 GUI 开发验收面。`npm run agc:chat -- --config-dir <项目外AppData> [--init] ` 默认把每轮输入投递给真实 `project-supervisor` background Runtime;`agc:swarm` 仍可显式追加 `` 调试其他父 Agent。两者都复用 External Runner、active Session、conversation、静态委派、动态隔离 child、私有记忆、项目黑板、确认策略和 all-join,不调用一次性 `--agent-chat`,不新建本地 HTTP 服务或平行数据库。终端持续显示全 Agent 状态、事件和父子 / 委派关系,提供 `/agents`、`/status`、`/history`、`/help`、`/quit` 以及 approve / reject;父 run 活跃时普通输入走 same-run steer,stdin channel 保证运行中仍可退出。入口启动和收束前执行恢复扫描,只有全 Runtime 非活跃、队列为空、恢复扫描无新增工作并通过稳定观察窗口后才输出绑定父 Session 的最后回复。首版没有 Runner Provider token delta,只承诺状态 / 事件实时输出和最终回复。V1.15 的历史父 Agent 汇总失败结论保留;V1.16 已通过 `project-supervisor` 双专业 Agent 并行、唯一最终回复和同 Session 第二轮真实 Provider 验收。 - 2026-07-14 起,Runtime V1.1 文档的“V1.16 Project Supervisor 总控 Agent”作为正式用户主聊天的新事实源。规范 ID 为 `project-supervisor`,使用独立 active Agent Session 和现有 External Runner;空闲输入创建新 run,匹配 Session 的活跃输入继续走 same-run steer。它可以澄清、使用白名单工具、维护黑板、在同一父 run 内最多并行等待 3 个静态专业 Agent 并汇总结果。旧 `agentLlm.chat` 只作 `agentLlm.project-supervisor` 的配置兼容回退。静态 `agent.delegate` 必须通过 durable delivery 和同一父 run 的等待 / 唤醒 / 认领屏障收束,不能再靠新 `delegate-receipt-*` run 产生第二次用户回复;普通用户面只展示紧凑状态、安全确认和唯一最终回复。 -2026-07-14 修复后,V1.16 的纯聊天真实 Provider 验收已通过:两个专业 Agent 同秒进入 running 并真实重叠 20 秒;父 run 在下一次 planning 前 durable 挂起,只写 1 条等待记录;两份回执 ready 后自动唤醒同一 run,以一个 `Observed` claim 原子认领并只写 1 条用户可见 assistant。随后同一 Supervisor Session 的第二轮请求直接引用上轮结论完成回复且没有新增委派。正式实现因此不再沿用 V1.15 的“模型反复全量 `agent.run_status`”等待策略:`agent.run_status` 对专业 Agent 的项目 revision 漂移保持中立,Session 入队释放 lane 后才通知 Runner,定向 wake 只有在精确目标已推进或无需推进时才缓存成功。 +2026-07-14 修复后,V1.16 的纯聊天真实 Provider 验收已通过:两个专业 Agent 同秒进入 running 并真实重叠 20 秒;父 run 在下一次 planning 前 durable 挂起,只写 1 条等待记录;两份回执 ready 后自动唤醒同一 run,以一个 `Observed` claim 原子认领并只写 1 条用户可见 assistant。随后同一 Supervisor Session 的第二轮请求直接引用上轮结论完成回复且没有新增委派。正式实现因此不再沿用早期“模型反复全量 `agent.run_status`”等待策略:`agent.run_status` 对专业 Agent 的项目 revision 漂移保持中立,Session 入队释放 lane 后才通知 Runner,定向 wake 只有在精确目标已推进或无需推进时才缓存成功。 当前委派协议已把 delivery 与 `Prepared -> Committed -> Observed` claim journal 作为事实源,认领前按 delegationId 排序并取得全部 delivery 锁,`.agent/agent.db` 只作 best-effort 诊断投影。恢复中的 executing 动作只允许 Supervisor `agent.delegate / agent.run_status` 经项目锁、pending 全身份和 policy 重验后补交;parent-wake 使用 singleflight、有界错误分类和稳定 Runner requestId。子终态只有在 parent/child/delivery 完整身份一致后才能 ready 或 suppression;错配不得改写 delivery。最终回复继续由原父 run 的 finalization journal 幂等写入。 -2026-07-17 起,Runtime V1.30 已把真实 `agc:chat / --swarm-chat` 与自主专业编排纳入同一门禁。用户只描述业务交付,不提供 Agent ID、数量、并行、工具、repair 或 Runner 配方;`project-supervisor` 必须在单个 native planning 批次自主选择至少两个不同专业 Agent 并形成真实 Provider 重叠,语义判定不满足时在同一父 Session/run 发起唯一 repair,最终以单条 `[turn.report] game-creator-swarm-turn-report.v1` 和唯一 Supervisor assistant 收束。正式 `openai_chat / gpt-5.5` 最终诊断轮已完成 `51/51` Provider lifecycle、双专业并行、2+1 delivery、2 个 Observed claim、pidfd Runner 强杀/boot 恢复、唯一正式回复和零重复/残留/泄漏,当前门禁状态为 PASS;static delivery + isolated all-join 的真实组合和 Tauri/WebView 宿主级 E2E 仍是独立后续项。 +2026-07-17 起,Runtime V1.30 已把自主专业编排纳入真实 Provider 门禁。用户只描述业务交付,不提供 Agent ID、数量、并行、工具、repair 或 Runner 配方;`project-supervisor` 必须在单个 native planning 批次自主选择至少两个不同专业 Agent 并形成真实 Provider 重叠,语义判定不满足时在同一父 Session/run 发起唯一 repair,最终以唯一 Supervisor assistant 收束。正式 `openai_chat / gpt-5.5` 最终诊断轮已完成 `51/51` Provider lifecycle、双专业并行、2+1 delivery、2 个 Observed claim、pidfd Runner 强杀/boot 恢复、唯一正式回复和零重复/残留/泄漏,当前门禁状态为 PASS;static delivery + isolated all-join 的真实组合和 Tauri/WebView 宿主级 E2E 仍是独立后续项。 2026-07-22 补充:自主构建的最终 `preview.validate` 在当前 revision 失败后,修复责任必须继续服从 Project Supervisor 的只编排边界。父 run 尚无协作事实时可沿用总控直接修复兼容路径;一旦 durable 协作事实已建立且 `orchestratorOnlyAfterDelegation=true`,活性门不得再强迫 Supervisor 调用 `file.write / file.patch / project.patchset`。没有 ready 回执且委派容量未满时,可向 `code-prototype` 创建 `repairOfDelegationId=null / runId=null` 的新后续修复任务,并把最新浏览器诊断和 `game/index.html` 验收产物写入合同;已有 ready 未认领回执或 active delivery 已达 3 个时,原生工具目录必须只保留 `agent.run_status`,先原子认领既有交付,不得创建第四次委派。专业 Agent 推进到更高 revision 后,固定顺序为“认领 ready delivery -> 取得当前 revision 的静态通过凭证 -> 父 Supervisor 重跑固定试玩”。每个固定 `data-playtest-id` 在对应动作发生时必须恰好匹配一个可见、启用且真实可点击的 HTMLElement;缺失、重复、隐藏或 disabled 都失败关闭。失败 revision、专业修改、总控复验与最终试玩之间不得用伪造 mutation 衔接。 @@ -676,7 +674,7 @@ Agent 可见的系统指令、工具与参数说明、恢复指引和上下文 该问题的收束修复严格限制为 `autonomous-game-build + project-supervisor + completion gates 已通过` 后的 `final-reply`。优先使用非空 `plan.response`;为空时才生成确定性回复,明确当前 revision 已完成生成并通过静态、桌面和移动试玩。普通 Agent、尚未收敛、任一完成门禁未通过或存在 reconciliation 时继续失败关闭。Provider lifecycle 中真实的 final-reply 失败证据必须保留,兜底只保证已完成项目能向用户收束,不把失败请求改写为 completed。修复后必须重新运行一轮独立真实外部 E2E;该轮完成前不能宣称外部 Provider 全链路 PASS。 -2026-07-22 修复后的最新独立真实外部轮次确认新 fallback 已命中:父 Supervisor 终局为 `idle / completed`,`turn.report` 为 `settled`,只产生 `1` 条 `44` 字符的 Supervisor assistant;pending、retry、handoff、finalization、reconciliation、重复、API Key 和路径泄漏均为 `0`。因此“完成后不回复”已在该轮解决。 +2026-07-22 修复后的最新独立真实外部轮次确认新 fallback 已命中:父 Supervisor 终局为 `idle / completed`,只产生 `1` 条 `44` 字符的 Supervisor assistant;pending、retry、handoff、finalization、reconciliation、重复、API Key 和路径泄漏均为 `0`。因此“完成后不回复”已在该轮解决。 该轮整体仍为 **FAIL**,不能称为 PASS:`105` 个 Provider identity 中 `103 completed / 2 failed`;两个原始专业 Agent 失败均由 repair 恢复,最终验收却命中 `supervisor-swarm-private-body-public-event-leak`。脱敏定位共 `6` 处:两个专业 Agent 的失败正文分别为 `149 / 123` 字符、对应 SHA-256 前缀 `494ce8 / 3089ad`,进入 `4` 条 `event.detail` 和 `2` 条 `agent.runtime.background_task.failed.error`;这些内容均属于 delivery result,不是 userTask、委派任务或对话正文,与 final-reply fallback 无直接关系。 @@ -684,7 +682,7 @@ Agent 可见的系统指令、工具与参数说明、恢复指引和上下文 2026-07-22 最终独立真实外部轮次已完整 **PASS**:`status=PASS`、`evidence=complete`、`privacy scan=complete`。本轮共有 `84` 个 Provider identity,`started / terminal / completed` 均为 `84`,`failed / retry / open / duplicate` 均为 `0`;`1` 个原专业任务以 `budget-exhausted` 终止,唯一 repair 已 `completed` 并 `recovered`,最终 child 为 `2 completed + 1 historical failed`,所有任务均处于终态。 -父 Supervisor 最终为 `idle / completed`,`turn.report` 为 `settled`,只产生 `1` 条 `297` 字符的 Supervisor assistant,`completed audit=1`,finalization 完成 `4` 个 stages。项目 revision 从 `0 -> 4`,`game/index.html` 为 `7639` bytes 且内容已变化,`game.static_smoke` passed;`lane-defense-v1` 的 desktop / mobile 浏览器验证均通过,固定试玩为 `37/37`。 +父 Supervisor 最终为 `idle / completed`,只产生 `1` 条 `297` 字符的 Supervisor assistant,`completed audit=1`,finalization 完成 `4` 个 stages。项目 revision 从 `0 -> 4`,`game/index.html` 为 `7639` bytes 且内容已变化,`game.static_smoke` passed;`lane-defense-v1` 的 desktop / mobile 浏览器验证均通过,固定试玩为 `37/37`。 终局 pending / confirmation / user-input / provider batch / retry / handoff / tool-plan handoff / finalization 残留 / reconciliation / duplicate 全为 `0`;Provider payload / private body / API Key / project path / config path / log / browser report leak 全为 `0`;人工 approve / answer / steer 全为 `0`。Runner 与 AppData 已清理,项目因 `--keep-project` 暂留后由主线程清理。此前 `114` identity 与 `105` identity 两个 **FAIL** 继续保留为独立历史失败,证据未与本轮拼接;最终 PASS 是这个单个新轮次的完整证据,当前外部 Provider 全链路状态现已 **PASS**。 @@ -692,17 +690,11 @@ Agent 可见的系统指令、工具与参数说明、恢复指引和上下文 2026-08-03 覆盖说明:上段 `code-prototype + quality-review` 首批身份已退出当前合同,现行首批为 `design-director + art-director + code-director`;策划与程序 Director 只读且 `expectedArtifacts=[]`,美术 Director 非只读并要求 `assets/art-spec.png`。V1.47 的 batch v3 恢复、只读工具限制、mutation / verification 与 claim 收束边界继续保留。 -V1.47 在只读工具边界和 batch v3/v2/v1 恢复终审修复后的最新独立真实外部轮次已完整 **PASS**:用户只输入一次任务后 stdin 立即 EOF,人工 approve / answer / steer 均为 `0`;一个原始专业任务失败后由唯一 repair 自行恢复,父 Supervisor 为 `idle / completed`,`turn.report=settled` 且只有 `1` 条 `44` 字符 assistant。项目 revision `0 -> 6`,`game/index.html` 为 `8080` bytes 且已变化,两次静态检查通过,desktop / mobile 的 `lane-defense-v1` 真实 Chrome 试玩为 `37/37`。`88` 个 Provider identity 全部 terminal,其中 `75 completed / 13 failed`,`12` 条 durable retry audit 与专业 repair 均自行恢复;open lifecycle、pending、confirmation、user-input、provider batch/retry/handoff/tool-plan handoff、finalization、reconciliation、duplicate 与各类泄漏终局均为 `0`,Runner、disposable 项目和隔离 AppData 已自动清理。该轮证明失败 attempt 可保留真实证据而循环仍能零人工干预收束,不能把它改写成 Provider 零失败。 - -2026-07-23 起,开发验收提供两个根级短入口。`npm run agc:test` 直接委托现有确定性可玩塔防 E2E,不复制 Runtime harness;`npm run agc:test:chat` 自动发现 Tauri identifier `world.genarrative.ai-game-creator` 对应 AppData,把 `game-creator.config.json` 和存在时的 `game-creator.config.local.json` 私有复制到单次 sentinel 隔离 AppData,绝不复制正式 Runner endpoint、lock、备份或其它文件,再创建带私有 sentinel 的一次性项目。LLM 状态检查和 `--swarm-chat --init --autonomous-game-build` 都只使用隔离 AppData,因此当前 debug 二进制指纹变化不会探测、退役或阻塞正在工作的正式客户端 Runner。用户只输入需求并发送 EOF;正常收束且存在 `game/index.html` 后,通过仅开发 CLI `--preview-serve` 复用正式 localhost preview server,自动打开固定形态的 loopback 试玩地址。预览按 `Ctrl+C` 结束后,脚本通过内部 `--runner-shutdown-if-idle` 只关闭已空闲的隔离 Runner,确认 endpoint 消失后再验证 sentinel 并清理隔离 AppData 和项目;隔离 Runner 仍有任务、退出失败、Swarm 未收束或预览启动失败时保留对应现场,不能强杀或误删。`--keep-project` 可主动保留项目但不额外保留已空闲的隔离配置;显式 `--project-dir` 永不删除,非空未初始化目录拒绝,`--config-dir` 只表示绝对配置来源目录,`--project-dir` 也只接受绝对路径。该人工入口用于快速体验,不能替代真实外部 Provider E2E 的完整生命周期、隐私和残留门禁。 - -2026-07-25 补充并取代上一段的统一持续预览口径:`npm run agc:test:chat` 默认投递固定植物塔防需求,作为单轮非交互真实测试,不依赖 stdin,也不把 EOF 当作“没有任务”。父 run 收束后必须校验本轮必需 seed task、当前配置对应的正式产物、最新 revision 静态检查和 Runtime 浏览器验收;全部通过后立即关闭空闲隔离 Runner,并按 sentinel 与保留参数清理测试 AppData / 项目,不再进入等待 `Ctrl+C` 的长期 preview。`npm run agc:test:chat:manual` 才是不带 `--task` 的手工聊天入口,继续读取多轮 stdin,并在成功收束后保留持续预览供人工试玩,直到用户显式退出。自动化、CI 或无 TTY 调用必须使用默认自动入口,不能依赖交互提示或持续预览生命周期。 - -2026-07-24 起,`--swarm-chat` 的普通自然语言不再由 CLI 关键词表预分类为 Chat / Execute / Resume。Project Supervisor 与六个部门 Director 进入同一个轻量 interaction loop:模型可以直接回复,或从统一 capability registry 中选择 `project_location / runtime_execute / runtime_resume`;`runtime_execute` 不接收模型改写后的任务正文,宿主始终把原始用户消息提交给持久 Runtime。显式 `/status /goal /resume` 继续属于确定性控制面。匹配 Session 已有 busy Runtime 或 active Goal 时,新消息不启动旁路 Provider,也不直接改写同一 canonical Session,而是进入原 run 的 durable steer;所有 Runtime mutation 继续要求项目外 `--config-dir`、External Runner、Agent task lock、Session lane、权限、确认、revision、verification 和 finalization 门禁。该 interaction registry 是 Pi 风格小内核与可组合工具边界的第一步,不代表现有 Runtime 工具已经完成单一 registry 迁移,也不允许把安全不变量下放到 Prompt、Skill 或模型判断。 +V1.47 在只读工具边界和 batch v3/v2/v1 恢复终审修复后的最新独立真实外部轮次已完整 **PASS**:用户只输入一次任务后立即结束输入,人工 approve / answer / steer 均为 `0`;一个原始专业任务失败后由唯一 repair 自行恢复,父 Supervisor 为 `idle / completed`,只有 `1` 条 `44` 字符 assistant。项目 revision `0 -> 6`,`game/index.html` 为 `8080` bytes 且已变化,两次静态检查通过,desktop / mobile 的 `lane-defense-v1` 真实 Chrome 试玩为 `37/37`。`88` 个 Provider identity 全部 terminal,其中 `75 completed / 13 failed`,`12` 条 durable retry audit 与专业 repair 均自行恢复;open lifecycle、pending、confirmation、user-input、provider batch/retry/handoff/tool-plan handoff、finalization、reconciliation、duplicate 与各类泄漏终局均为 `0`,Runner、disposable 项目和隔离 AppData 已自动清理。该轮证明失败 attempt 可保留真实证据而循环仍能零人工干预收束,不能把它改写成 Provider 零失败。 2026-07-15 起,Runtime V1.1 文档的“V1.17 单 Agent 持久计划”作为后台工具规划进度的新事实源。`submit_agent_tool_plan` 新增 nullable `planUpdate={explanation,steps[{step,status}]}`;步骤只接受 `pending / in_progress / completed`,最多 8 步且至多一个 `in_progress`。结构化计划一旦建立,legacy `plan` 只作旧协议 fallback;终态步骤必须保留,`planRevision` 只在真实变化时单调递增,工具 action 下标不得自动完成结构化步骤,存在未完成步骤时不得写最终回复或 completed。 -V1.17 计划快照随 `game-creator-runtime-context-bundle.v3` 持久化,v2 在通过原身份、revision 和 verification gate 校验后从当前 Runtime state 补齐计划字段继续恢复;计划元数据本身不推进项目 revision、不改变 verification gate,也不触发项目权限确认。开发 UI 和 CLI 有界展示 revision、说明与完整 8 步;正式用户的 Supervisor 只展示完成数、当前步骤、等待对象、下一步和协作数量的紧凑摘要。恢复、same-run steer 和真实 Provider 的完整验收矩阵以 Runtime V1.17 章节为准;2026-07-16 已在当前 v5 context 上完成正式 `openai_chat / gpt-5.5` 的同 run steer + Runner 强杀恢复专项,门禁状态为 PASS。 +V1.17 计划快照随 `game-creator-runtime-context-bundle.v3` 持久化,v2 在通过原身份、revision 和 verification gate 校验后从当前 Runtime state 补齐计划字段继续恢复;计划元数据本身不推进项目 revision、不改变 verification gate,也不触发项目权限确认。开发 UI 有界展示 revision、说明与完整 8 步;正式用户的 Supervisor 只展示完成数、当前步骤、等待对象、下一步和协作数量的紧凑摘要。恢复、same-run steer 和真实 Provider 的完整验收矩阵以 Runtime V1.17 章节为准;2026-07-16 已在当前 v5 context 上完成正式 `openai_chat / gpt-5.5` 的同 run steer + Runner 强杀恢复专项,门禁状态为 PASS。 2026-07-18 起,正式项目工作台的总控与策划 / 美术 / 程序 Agent 状态统一投影当前 Supervisor 父 run 的真实 Runtime;专业 Agent 只有在 `parentRunId` 精确匹配该父 run 时才可进入当前项目状态列表。普通项目页在 Tauri event 之外必须保留只读轮询,兜底独立 Runner 无法可靠投递 App event 的情况;短暂读取失败时保留最后一份可信快照,不得清空或倒退界面状态。正式面只展示真实运行阶段、计划完成数 / 总数、最近更新时间、失败、待确认与待回答等紧凑状态;专业 Agent 的确认或拒绝必须同时绑定真实 `agentId + runId + actionId`。`manifest.tasks` 只能在没有匹配 Runtime 时作为回退,不得覆盖真实 Runtime;正式面不展示内部 `currentAction`、`observation`、工具计划正文、Provider 错误原文、fingerprint 或字符计数。transport / timeout / 鉴权 / 限流、Codex 稳定错误分类,以及验证、预期产物、权限策略、预算、恢复对账和持久化等常见 Runtime 失败必须映射为可理解、可行动的安全文案;底部子 Agent 状态卡在失败时直接展示同一安全摘要,不能只写“失败”或“子 Agent 任务失败”。不得根据 manifest 或动画伪造生产中、进度百分比或完成状态。当前父 run 或专业状态集合变化时 Runtime 状态区回到顶部,总控摘要在内部滚动期间保持可见。 @@ -756,7 +748,7 @@ Agent Runtime 负责: ## Agent 能力清单 - 用户能力:项目开发工作台、陶泥儿聊天、上传文件、manifest 资源视图、客户端内运行表现层和专业 Agent 紧凑状态;正式用户窗口仍不展示原始任务 / 文件 / 日志、开发预览调试面板、能力清单或开发专用单 Agent 聊天入口。 -- 开发调试能力:无 GUI 调试使用 `npm run agc:chat` / `npm run agc:swarm` 终端入口,复用 External Runner、active Session、conversation 与各 Agent 私有记忆;`` 以 manifest taskId 为规范值,旧 `group-role` 别名只作为兼容输入映射到 taskId。 +- 开发调试能力:无 GUI 调试使用 `--agent-*` CLI 控制命令,复用 External Runner、active Session、conversation 与各 Agent 私有记忆;`` 以 manifest taskId 为规范值,旧 `group-role` 别名只作为兼容输入映射到 taskId。 - 命令能力:内置命令调用、权限 gate、执行日志;v1 只允许白名单受限命令,不执行任意 shell。 - 编排能力:任务拆分、任务图依赖、专业组调度、多智能体协作;Runtime V1 会为单 Agent 对话和生成 loop 中的角色 brief 写入独立 runtime state / event,先解决“每个 Agent 正在做什么、跑到哪一步、最近一次 task/run 是什么”的可观测性。 - 历史边界说明:下一条“后台任务能力”保留 V1.1 前的进程内演进记录,其中 App 内 tokio task、进程内 drain、旧工具箱和“不是独立 OS 进程”的描述均已失效;当前执行边界以上文独立 Runner 说明为准。 @@ -795,7 +787,7 @@ Agent Runtime 负责: - 2026-07-11 补充,2026-07-12 更新:代码修改后的真实验证由开发专用 `project.verify` 承接。输入固定为 `script / expectedCommand / timeoutSeconds`;`script` 允许项目根 `package.json` 中的固定脚本 `check / typecheck / test / lint / build`,以及以 `check: / test: / lint: / typecheck: / build: / verify: / validate:` 开头、后缀由安全非空段组成的命名脚本。脚本必须真实存在于项目根普通文件 `package.json` 的 `scripts` 中,`expectedCommand` 必须与执行时重新读取的脚本正文完全一致,`timeoutSeconds` 为 1-300;当前执行器只支持 npm,非 npm `packageManager` 或 pnpm / yarn / bun 锁文件明确失败,不接受自由命令、参数或工作目录。`pre* / post*` 生命周期脚本名不在允许范围,执行器再通过 npm `--ignore-scripts` 禁止所选脚本关联的 pre/post lifecycle。工具映射到独立且默认需确认的 `project.verify` 权限,确认动作指纹绑定完整输入,不再因为放行验证而同时放行 `command.run_limited` 静态 smoke。执行器由 npm 运行已确认脚本,使用空 stdin、隔离 HOME/TMP/cache、清理后的环境、独立进程组和有界脱敏输出;Unix 下无论根进程正常结束还是超时都会清理同组残留后代。项目写锁记录 PID 和唯一 nonce,活进程继续持锁,Unix 死进程锁或跨平台超过安全时限的无效锁可回收,且控制路径拒绝符号链接。进入进程执行后的终态写命令日志和 manifest command run,Agent 触发时另写 `agent.runtime.project.verify`;输入预检拒绝只写 Runtime observation / error 事件。失败输出作为 observation 回到下一轮 planning。项目级 revision 独立持久化到 `.agent/runtime/project-revision.json`;每个 run 的 gate 与验证结果持久化到 `.agent/runtime/verification//.json`。`file.write / file.patch / file.delete / project.restore` 在项目写锁内、实际修改前先保守推进 revision,并把 `requiresVerification` 单向置为 `true`,操作失败或崩溃也不回退;成功的 `project.verify` 或 `command.run_limited / game.static_smoke` 只为执行时的当前 revision 写入凭证。空 actions 前如果门禁仍要求验证、验证失败或凭证 revision 已过期,Runtime 注入 `runtime.verification: blocked` 并继续 replan;多窗口重复无进展而以 `loop-budget-exhausted` 终止时,仍未形成当前 revision 的通过结果则保持失败。该能力会执行用户项目脚本,环境隔离不是 OS 沙箱;普通用户 `/smoke` 与 `game.static_smoke` 保持原边界,不暴露该开发工具。 - 2026-07-11 补充:开发验证可用 `npm run ai-game-creator-shell:agent-task -- [--init] ` 无 UI 启动单 Agent 后台任务。CLI 只负责可选初始化、调用现有 Runtime、按 runId 轮询终态并打印 `status / phase / replyText / pendingActionId`,不复制 planning 或工具执行逻辑;默认 10 分钟轮询上限。`waiting-for-confirmation` 会以非零状态退出并要求转到开发窗口确认,CLI 不提供跳过项目权限的自动确认参数。该入口用于真实 provider 的可重复端到端验收,不进入普通用户界面。 - 2026-07-11 补充,2026-07-13 调整:后台工具规划与最终回复的 LLM 请求新增可恢复错误重试:`LlmError::EmptyResponse` 原样自动重试最多 3 次;`Timeout / Connectivity / Transport` 及上游 `408 / 429 / 5xx` 最多额外重试 5 次并按 `500ms / 1000ms / 1500ms / 2000ms / 2500ms` 退避。配置、请求、流能力、反序列化错误及其他 `4xx` 不重试。重试发生在工具计划被解析和执行前,或最终回复尚未落盘时,因此不会重复执行已经落盘的工具副作用;重试耗尽后仍写入原有 `error / turn.failed` 事件并把失败消息追加到当前 Agent 会话。 -- 2026-07-11 调整,2026-07-12 更新,2026-07-15 由 V1.21 澄清:后台单 Agent planning loop 每 6 轮形成一个进度 checkpoint,每轮最多 3 个工具动作;6 轮是停滞检测窗口,不是单个 run 的固定上限,也不是上下文压缩触发器。`loopIteration` 在同一 run 内连续递增,`maxLoopIterations` 指向当前 checkpoint 的结束轮次;待确认或重启恢复后按 context bundle 的 `nextLoopIndex` 在同一 run 继续。checkpoint 产生新的独立观察时继续下一窗口,最近 6 轮没有独立进展或相邻窗口指纹重复时才写入 `failed / budget-exhausted` 和 `loop-budget-exhausted`,不生成总结伪装完成;真正的旧历史摘要只由 V1.21 token 阈值或显式 `/compact` 触发。这只调整后台单 Agent Runtime;游戏草案 Generator/Evaluator 仍保持独立的 3 轮修复预算。旧摘要中“后台最多 3 轮”“整个 run 最多 6 轮”或“每 6 轮自动压缩”的描述不再有效。 +- 2026-07-11 调整,2026-07-12 更新,2026-07-15 由 V1.21 澄清:后台单 Agent planning loop 每 6 轮形成一个进度 checkpoint,每轮最多 3 个工具动作;6 轮是停滞检测窗口,不是单个 run 的固定上限,也不是上下文压缩触发器。`loopIteration` 在同一 run 内连续递增,`maxLoopIterations` 指向当前 checkpoint 的结束轮次;待确认或重启恢复后按 context bundle 的 `nextLoopIndex` 在同一 run 继续。checkpoint 产生新的独立观察时继续下一窗口,最近 6 轮没有独立进展或相邻窗口指纹重复时才写入 `failed / budget-exhausted` 和 `loop-budget-exhausted`,不生成总结伪装完成;真正的旧历史摘要只由 V1.21 token 阈值或显式手动压缩触发。这只调整后台单 Agent Runtime;游戏草案 Generator/Evaluator 仍保持独立的 3 轮修复预算。旧摘要中“后台最多 3 轮”“整个 run 最多 6 轮”或“每 6 轮自动压缩”的描述不再有效。 - 2026-07-12 补充并冻结,2026-07-13 调整容量,2026-07-15 由 V1.18 增加 Goal 快照,随后由 V1.21 升级压缩状态:后台 Agent 每个 run 的可恢复 planning 上下文通过临时文件替换原子写入 `.agent/runtime/context-bundles//.json`;当前 schema 为 `game-creator-runtime-context-bundle.v5`。除 Agent/Task/Session/Run、任务正文、project revision、verification gate、窗口、fallback response、压缩 observation、`contextStalled` 与完整结构化计划快照外,v4 新增 `goalId / goalRevision / goalStatus / goalSnapshotFingerprint`,v5 再绑定 compaction revision、source/summary fingerprint 和已压缩消息/observation 计数;任一当前 Goal 身份、状态、revision、指纹或压缩快照不一致都失败关闭。 - v4 在压缩状态校验通过后迁移为当前格式;v3 在原身份、任务、project revision、verification gate 和结构化计划校验通过后,从当前 Runtime/Goal sidecar 补齐 Goal 快照并继续;v2 先按 V1.17 规则补齐结构化计划,再补 Goal;后续 checkpoint 统一写 v5。v1、缺失既有 gate 关联或无法证明 Goal 快照的记录不自动迁移。Provider pause 中断/返回边界先持久化 continuation;该恢复快照把 `goalStatus` 设为恢复后的 `active`,避免 resume 后用 paused 上下文自相矛盾。 - stale continuation 必须清空旧 actions 与 fallback response,保留 blocker、loop 位置、窗口进度和结构化计划;`contextStalled` 一旦成立,同 run 重规划和重启不得清除。动态 revision 数字、时间戳与验证命令输出不构成独立进展;重复 stale 最迟在相邻窗口指纹重复时以 `loop-budget-exhausted` 终止。单文件最多 128 KiB、最多 12 条 observation;写入前统一限长并过滤敏感内容和项目绝对路径。revision 与验证资格仍以锁内独立文件为准,bundle 只是 Runtime 私有恢复上下文,不等同于根级 `.agent/context.bundle.json`,不得由通用文件工具暴露。 @@ -840,11 +832,11 @@ Agent Runtime 负责: - 2026-07-10 补充,2026-07-16 由 V1.28 澄清:后台 planning 与预算内 final reply 使用专用最小上下文,只预置 Agent 身份、sessionId、runId、执行模式和工具策略;Agent 私有记忆、项目记忆、黑板、对话、资产、项目索引与文件正文只能经对应工具通过权限 gate 后作为 observation 进入下一轮。只有开发窗口的专业 Agent 前台直调可使用对应角色上下文;正式用户前台现已统一进入 `project-supervisor`。长黑板、记忆和对话按尾部截断,确保最新结论与最新定向消息优先保留。 - 2026-07-10 补充,2026-07-16 由 V1.28 澄清:同一 Agent 的开发前台直调、流式调试和后台任务统一使用 `.agent/runtime/locks/.lock` OS 文件锁。开发前台不再在整个 LLM 请求期间占用项目级写锁;同 Agent 后台任务在开发前台运行时只入队,前台成功或失败后把当前 Agent 锁直接移交给 drain,不重新抢锁,也不允许 drain 启动异常把已经完成的调试结果改判为失败。正式用户 GUI 不通过该入口直聊专业 Agent;不同 Agent 继续并行,真实项目写工具只在副作用执行期间短暂申请项目写锁。 - 2026-07-10 补充,2026-08-01 更新:默认 `agent.resume=confirm` 时,客户端自动恢复命令先做只读 recovery preflight。全新项目和已完全终态且没有 task / retry / handoff / finalization / pending action / reconciliation 等 durable recovery work 的项目直接返回空结果,不显示虚假的 `agent.resume` 确认条。确实存在可恢复工作时,自动命令只做 auto gate 并返回待确认错误;主工作区和独立开发 Agent 聊天窗口显示 `agent.resume` 确认条,确认对象绑定发起时的项目路径,切换项目会取消旧确认,异步返回后也不得把旧项目 Runtime 合并到新项目 UI。开发者确认后调用独立 `confirm_resume_game_creator_agent_runtime_tasks`,该命令仍执行 deny-only 权限检查后才接回 durable queue。临时调用失败不锁死项目路径,允许后续刷新重试;明确 deny 或取消都不恢复任务。 -- 2026-07-10 补充,2026-07-12 更新,2026-07-15 增加 V1.17 完成门禁并由 V1.21 澄清:后台 Agent 返回空 `actions` 后,只有不存在 `project.verify` 等既有 blocker,且当前结构化计划的全部必要步骤均为 `completed`,才视为 loop 已收束。工具 action 序号和成功 observation 不会自动推进结构化计划;未完成时 Runtime 返回 `runtime.plan_update` blocker,在同一 run 要求 Agent 按真实进度更新。每 6 轮只做进度 checkpoint 与停滞检测;有新的独立 observation 时继续同一 run,最近 6 轮没有独立进展或相邻 checkpoint 重复时终态才写为 `status=failed / phase=budget-exhausted`,error 使用 `loop-budget-exhausted` 机器可读前缀,不再调用 final reply 后写 completed 审计。上下文摘要只由 token 阈值或显式 `/compact` 触发。解析阶段保留过滤后的 action 总数,每轮超过 3 个 action 时写入 `runtime.tool_budget` observation 并只执行前三个,要求下一轮重新排序。Runtime 默认 `allowedTools` 直接由实际可执行工具白名单派生,避免 UI 观测与执行边界漂移。 +- 2026-07-10 补充,2026-07-12 更新,2026-07-15 增加 V1.17 完成门禁并由 V1.21 澄清:后台 Agent 返回空 `actions` 后,只有不存在 `project.verify` 等既有 blocker,且当前结构化计划的全部必要步骤均为 `completed`,才视为 loop 已收束。工具 action 序号和成功 observation 不会自动推进结构化计划;未完成时 Runtime 返回 `runtime.plan_update` blocker,在同一 run 要求 Agent 按真实进度更新。每 6 轮只做进度 checkpoint 与停滞检测;有新的独立 observation 时继续同一 run,最近 6 轮没有独立进展或相邻 checkpoint 重复时终态才写为 `status=failed / phase=budget-exhausted`,error 使用 `loop-budget-exhausted` 机器可读前缀,不再调用 final reply 后写 completed 审计。上下文摘要只由 token 阈值或显式手动压缩触发。解析阶段保留过滤后的 action 总数,每轮超过 3 个 action 时写入 `runtime.tool_budget` observation 并只执行前三个,要求下一轮重新排序。Runtime 默认 `allowedTools` 直接由实际可执行工具白名单派生,避免 UI 观测与执行边界漂移。 - 2026-07-15 V1.18 补充:开发单 Agent 对话框使用 `执行 / 聊天 / 目标` 三段模式,Goal 创建/编辑在独立弹层完成,并可查看状态、revision、完成标准以及暂停/恢复/清理;正式用户窗口不展示 Goal 管理控件。Provider 中断边界先持久化可恢复的当前 v5 context;Runner 重启先收束 Goal control,`paused` 在 finalization/pending action 前直接保持休眠。resume 只从 `paused` 续接,先删除同一 run 旧 cancel tombstone;finalization v3 在 assistant 后先投影 Runtime completed,再写 Goal completed 并补 Goal 终态投影。 - 任务图能力:每轮 Orchestrator agenda、ready / active task 选择、Evaluator 结构化返工路由、返工轮 carry-over。 - 记忆能力:短期记忆 `memory/session.md`、长期记忆 `memory/project.md`、项目级黑板 `memory/blackboard.md` 和角色私有记忆 `memory/agents//.md`;黑板用于共享重要跨 agent 记忆,角色私有记忆只给对应角色 brief 读取和追加。最近 project / agent conversation 会作为短期 prompt 上下文读取,不替代正式 memory 文件。 -- 对话能力:结构化对话记录统一落在 `.agent/conversations/` 的 append-only JSONL;正式用户 GUI 只写 `project-supervisor` 的 active Agent Session,`.agent/conversations/project.jsonl` 仅保留为 legacy 只读背景。`agc:chat` 与正式项目开发页均已按该路由落盘;开发窗口直调单个专业 Agent 时只写对应 `.agent/conversations/agents//` Session;专业 Agent 与 child 的终态只形成内部回执/证据,不写正式用户 assistant,也不把原始对话混进项目黑板或角色私有记忆。 +- 对话能力:结构化对话记录统一落在 `.agent/conversations/` 的 append-only JSONL;正式用户 GUI 只写 `project-supervisor` 的 active Agent Session,`.agent/conversations/project.jsonl` 仅保留为 legacy 只读背景。正式项目开发页已按该路由落盘;开发窗口直调单个专业 Agent 时只写对应 `.agent/conversations/agents//` Session;专业 Agent 与 child 的终态只形成内部回执/证据,不写正式用户 assistant,也不把原始对话混进项目黑板或角色私有记忆。 - 本地能力:生成代码和资产落盘、本地 manifest、受限运行命令、本地 HTTP 预览。 - 文件能力:`file.list/read/write/delete` 只允许访问项目目录内的相对路径,拒绝绝对路径、`..`、反斜杠和符号链接。 - 产物治理能力:写入前生成本地 checkpoint,写入后记录相对路径 diff,用户确认后可 restore 到最近 checkpoint;restore 会回滚到 checkpoint 状态,包括删除 checkpoint 后新增的受跟踪文件。 @@ -1032,62 +1024,11 @@ game-project/ - 资源管理可在按依赖 / 按类型之间切换、搜索资源并点击打开当前资源详情;dependency 模式展示可验证的资源引用和聚合任务流,搜索过滤端点、选择高亮直接上下游。资源卡不可拖动,Pointer Move 不更新坐标、线段或手动布局;所有展示数据来自当前 manifest、当前资源投影或当前项目导入附件。 - 首个 `code-prototype` 任务未完成时运行入口不可进入并给出可感知提示;完成后可进入运行表现层,真实预览直接加载到客户端内受限运行容器。 - 审批档位通过独立弹出面板切换,默认严格审批;界面选择不得绕过 Runtime 现有确认门禁。 -- 聊天输入 `/plan` 可在普通聊天消息里查看下一轮分工计划,不读取任务文件、不启动 run、不修改项目,也不新增普通用户计划面板。 -- 聊天输入 `/guide` 可在普通聊天消息里查看普通用户操作导引,不读取文件、不启动 run、不启动预览、不写项目,也不新增普通用户导引面板。 -- 聊天输入 `/progress` 可在普通聊天消息里查看项目进度,不读取文件、不启动 run、不启动预览、不导出试玩包、不写项目,也不新增普通用户进度面板。 -- 用户侧目标态看到当前工作区项目名 / 路径、最近 run、预览状态、任务完成数 / ready 数、资产数量 / 来源分布、最近命令摘要、`project-supervisor` 聊天框、上传入口和专业 Agent 协作状态列表;聊天输入 `/brief` 可在普通聊天消息里生成当前项目简报并提供 `/next` 草稿,不新增普通用户面板;聊天输入 `/risks` 可在普通聊天消息里查看当前项目风险并提供首个风险处理草稿,不新增普通用户面板;聊天输入 `/review` 可在普通聊天消息里查看 Evaluator 评审、返工焦点和返工路线并提供读取评审记录或继续修复草稿,不新增普通用户面板;聊天输入 `/context` 可在普通聊天消息里查看生成上下文来源并提供读取首个上下文或查看黑板草稿,不新增上下文面板;聊天输入 `/timeline` 可在普通聊天消息里查看项目活动时间线并提供最近日志、trace 或历史草稿,不新增时间线面板;聊天输入 `/playtest` 可在普通聊天消息里查看最近 run 是否可试玩、当前预览状态和 Playtest 任务状态并提供启动或打开预览草稿,不新增普通用户试玩面板;聊天输入 `/handoff` 可在普通聊天消息里生成当前项目交接摘要并提供 `/next` 草稿,不新增普通用户面板;聊天输入 `/runs` 可在普通聊天消息里列出已加载 Run 历史读取命令,不新增普通用户面板;聊天输入 `/run-files` 可在普通聊天消息里列出 Agent 运行辅助文件读取命令,不新增普通用户面板;聊天输入 `/internals` 可在普通聊天消息里列出项目内部真相源读取命令,不新增内部文件面板;聊天输入 `/art` 可基于当前 manifest 盘点美术素材并提供生成或读取草稿,不新增美术面板;聊天输入 `/audio` 可基于当前 manifest 盘点音频素材并提供登记或读取草稿,不新增音频生成系统;聊天输入 `/publish` 可基于当前 manifest 和最近 run 生成发布准备清单并提供运行、trace、继续或导出草稿,不新增发布面板;聊天输入 `/listing` 可基于当前 manifest 和最近 run 汇总作品页标题、卖点、标签口径、封面素材和发布说明状态并提供读取发布说明或发布准备草稿,不新增作品页面板;聊天输入 `/cover` 可基于当前 manifest 和最近 run 准备封面与缩略图检查并提供运行、美术或作品页草稿,不新增封面面板;聊天输入 `/screenshots` 可基于当前 manifest 和最近 run 准备宣传截图清单并提供运行或作品页草稿,不新增截图面板;聊天输入 `/trailer` 可基于当前 manifest 和最近 run 准备试玩短视频脚本并提供运行或交付草稿,不新增录屏面板;聊天输入 `/faq` 可基于当前 manifest 和最近 run 准备试玩常见问答并提供运行或交付草稿,不新增 FAQ 面板;聊天输入 `/post` 可基于当前 manifest 和最近 run 准备社区发布文案并提供运行或上架草稿,不新增社区发布面板;聊天输入 `/store` 可基于当前 manifest 和最近 run 准备上架资料清单并提供运行或作品页草稿,不新增上架面板;聊天输入 `/media-kit` 可基于当前 manifest 和最近 run 准备媒体资料包清单并提供运行、截图或发布说明草稿,不新增媒体包面板;聊天输入 `/release-notes` 可基于当前 manifest 和最近 run 准备试玩更新说明并提供运行、资料包或发布说明草稿,不新增更新说明面板;聊天输入 `/known-issues` 可基于当前 manifest 和最近 run 准备已知问题清单并提供运行、评审或交付草稿,不新增已知问题面板;聊天输入 `/export` 确认后把当前可试玩原型导出为本地试玩 ZIP,聊天输入 `/exports` 可只读列出已导出的本地试玩包;`project-supervisor` 主聊天与专业 Agent 协作状态列表在 `/llm-status` 后只显示各 Agent 当前 LLM provider / 模型 / 流式 / API Key 读取状态,但不显示密钥本体;最近项目资产入口显示本地路径、kind、mediaType 和来源类型,并可一键填入 `/read` 草稿,开发环境通过独立窗口查看任务拆分、专业组细节、产物、文件面板、嵌入预览以及 `.agent/logs/command.log` / `preview.log` / `agent.log`。 -- 聊天输入 `/llm-routes` 会触发同一只读 LLM 配置检查,在普通聊天消息里列出全局 LLM 和每个 agent resolved 后的 provider 路由、模型、API 类型、流式开关、API Key 是否已读取、单独路由数量和缺口数量;该命令不请求上游、不显示 API Key 本体、不写本地项目、不新增普通用户配置面板,缺口处理只提供 `/config` 草稿。 -- 聊天输入 `/goal` 可在普通聊天消息里查看创作目标来源,不读取 spec、上下文或 trace 文件,也不新增普通用户目标面板。 -- 聊天输入 `/progress` 可在普通聊天消息里查看项目阶段、任务完成度、最近 run、预览、素材和交付进度,不读取文件、不启动 run、不启动预览、不导出试玩包、不写项目,也不新增普通用户进度面板。 -- 聊天输入 `/spec` 可在普通聊天消息里查看创作规格包,不读取规格文件、不启动预览、不写项目,也不新增普通用户规格面板。 -- 聊天输入 `/mvp` 可在普通聊天消息里查看本轮最小可玩范围、当前状态、试玩包状态和暂不做事项,不读取文件、不启动预览、不导出试玩包,也不新增普通用户 MVP 面板。 -- 聊天输入 `/pitch` 可在普通聊天消息里查看试玩定位与卖点,不读取文件、不启动或打开预览、不直接继续 run,也不新增普通用户定位面板。 -- 聊天输入 `/demo` 可在普通聊天消息里准备 30 秒试玩讲解稿,不读取文件、不启动或打开预览、不导出试玩包、不发布作品,也不新增普通用户讲解面板。 -- 聊天输入 `/rules` 可在普通聊天消息里查看玩法操作与规则口径,不读取设计文件、不启动预览、不直接继续 run,也不新增普通用户规则面板。 -- 聊天输入 `/tutorial` 可在普通聊天消息里查看新手引导检查,不读取设计文件、不启动或打开预览、不直接继续 run,也不新增普通用户引导面板。 -- 聊天输入 `/mobile` 可在普通聊天消息里查看移动试玩检查,不读取代码文件、不启动或打开预览、不直接继续 run,也不新增普通用户移动适配面板。 -- 聊天输入 `/compatibility` 可在普通聊天消息里准备兼容性说明,不读取文件、不启动或打开预览、不导出试玩包、不上传云端、不发布作品、不写项目,也不新增普通用户兼容性面板。 -- 聊天输入 `/accessibility` 可在普通聊天消息里查看可读性与无障碍检查,不读取代码或 trace 文件、不启动或打开预览、不直接继续 run,也不新增普通用户无障碍面板。 -- 聊天输入 `/localization` 可在普通聊天消息里查看本地化与文案检查,不读取文件、不启动或打开预览、不导出试玩包、不上传云端、不发布作品、不写项目,也不新增普通用户本地化面板。 -- 聊天输入 `/performance` 可在普通聊天消息里查看性能与加载检查,不读取产物或日志文件、不启动或打开预览、不直接继续 run,也不新增普通用户性能面板。 -- 聊天输入 `/polish` 可在普通聊天消息里查看试玩前打磨清单,不读取文件、不启动预览、不导出试玩包、不写项目,也不新增普通用户打磨面板。 -- 聊天输入 `/credits` 可在普通聊天消息里查看素材署名与来源口径,不刷新资产、不读取素材清单、不导出试玩包,也不新增普通用户署名面板。 -- 聊天输入 `/blockers` 可在普通聊天消息里查看当前阻塞项,不读取文件、不启动预览、不导出试玩包、不写项目,也不新增普通用户阻塞面板。 -- 聊天输入 `/ready` 可在普通聊天消息里查看试玩就绪度,不读取文件、不启动预览、不导出试玩包、不写项目,也不新增普通用户就绪度面板。 -- 聊天输入 `/evidence` 可在普通聊天消息里查看当前验证证据台账,不读取文件、不启动或打开预览、不导出试玩包、不写项目,也不新增普通用户证据面板。 -- 聊天输入 `/deps` 可在普通聊天消息里查看任务依赖链,不读取任务文件、不启动 run、不修改项目,也不新增普通用户依赖面板。 -- 聊天输入 `/revise` 可在普通聊天消息里准备下一轮改版说明草稿,不读取文件、不继续 run、不启动预览、不导出试玩包、不写项目,也不新增普通用户改版面板。 -- 聊天输入 `/privacy` 可在普通聊天消息里查看隐私与导出边界,不读取文件、不导出试玩包、不启动预览、不写项目,也不新增普通用户隐私面板。 -- 聊天输入 `/criteria` 可在普通聊天消息里查看当前任务验收标准和预期产物,不读取任务文件或 trace 文件,也不新增普通用户验收面板。 -- 聊天输入 `/groups` 可在普通聊天消息里查看六个专业组进度和下一步任务,不读取任务文件或 trace 文件,也不新增普通用户专业组面板。 -- 聊天输入 `/balance` 可在普通聊天消息里查看数值与难度口径,不读取数值表、不启动预览、不直接继续 run,也不新增普通用户数值面板。 -- 聊天输入 `/budget` 可在普通聊天消息里查看最近 run 轮次和工具调用预算,不读取 trace 文件,也不新增普通用户预算面板。 -- 聊天输入 `/qa` 可在普通聊天消息里查看质量检查清单,不读取 trace 或日志文件、不启动或打开预览,也不新增普通用户 QA 面板。 -- 聊天输入 `/changes` 可在普通聊天消息里查看最近生成变更,不读取产物或日志文件、不执行 checkpoint diff,也不新增普通用户变更面板。 -- 聊天输入 `/todo` 可在普通聊天消息里查看下一轮小步清单,不读取任务文件、不启动 run、不修改项目,也不新增普通用户小步面板。 -- 聊天输入 `/test-plan` 可在普通聊天消息里准备手动测试计划,不读取文件、不启动或打开预览、不直接继续 run,也不新增普通用户测试面板。 -- 聊天输入 `/audience` 可在普通聊天消息里查看首批试玩对象,不读取文件、不启动或打开预览、不导出试玩包、不写项目,也不新增普通用户对象面板。 -- 聊天输入 `/invite` 可在普通聊天消息里准备试玩邀请文案,不读取文件、不启动或打开预览、不导出试玩包、不写项目,也不新增普通用户邀请面板。 -- 聊天输入 `/bug-report` 只打开独立“报告问题”面板并允许填写用户说明;不读取项目文件、不启动或打开预览、不导出试玩包、不上传截图或任意文件、不写项目,也不依赖 run / run_id。 -- 聊天输入 `/survey` 可在普通聊天消息里准备试玩问卷问题,不读取文件、不启动或打开预览、不导出试玩包、不写项目,也不新增普通用户问卷面板。 -- 聊天输入 `/cover` 可在普通聊天消息里准备封面与缩略图检查,不截屏、不裁剪、不读取文件、不启动或打开预览、不导出试玩包、不上传云端、不发布作品、不写项目,也不新增普通用户封面面板。 -- 聊天输入 `/screenshots` 可在普通聊天消息里准备宣传截图清单,不截屏、不读取文件、不启动或打开预览、不导出试玩包、不写项目,也不新增普通用户截图面板。 -- 聊天输入 `/trailer` 可在普通聊天消息里准备试玩短视频脚本,不录屏、不读取文件、不启动或打开预览、不导出试玩包、不上传云端、不写项目,也不新增普通用户录屏面板。 -- 聊天输入 `/faq` 可在普通聊天消息里准备试玩常见问答,不读取文件、不启动或打开预览、不导出试玩包、不上传云端、不写项目,也不新增普通用户 FAQ 面板。 -- 聊天输入 `/post` 可在普通聊天消息里准备社区发布文案,不上传云端、不发布作品、不读取文件、不启动或打开预览、不导出试玩包、不写项目,也不新增普通用户社区发布面板。 -- 聊天输入 `/store` 可在普通聊天消息里准备上架资料清单,不上传云端、不发布作品、不读取文件、不启动或打开预览、不导出试玩包、不写项目,也不新增普通用户上架面板。 -- 聊天输入 `/media-kit` 可在普通聊天消息里准备媒体资料包清单,不截屏、不录屏、不读取文件、不启动或打开预览、不导出试玩包、不上传云端、不发布作品、不写项目,也不新增普通用户媒体包面板。 -- 聊天输入 `/release-notes` 可在普通聊天消息里准备试玩更新说明,不读取文件、不启动或打开预览、不导出试玩包、不上传云端、不发布作品、不写项目,也不新增普通用户更新说明面板。 -- 聊天输入 `/known-issues` 可在普通聊天消息里准备已知问题清单,不读取文件、不启动或打开预览、不导出试玩包、不上传云端、不发布作品、不写项目,也不新增普通用户已知问题面板。 -- 聊天输入 `/feedback` 可在普通聊天消息里准备试玩反馈和修改说明,不读取文件、不启动预览、不直接继续 run,也不新增普通用户反馈面板。 -- 聊天输入 `/retention` 可在普通聊天消息里准备首轮复玩/留存观察清单,不读取文件、不启动或打开预览、不导出试玩包、不上传云端、不发布作品、不写项目,也不新增普通用户留存面板;首版不做真实埋点、留存报表、用户画像、A/B 实验、排行榜或账号留存。 -- 聊天输入 `/share` 可在普通聊天消息里准备试玩交付清单,不导出试玩包、不列出历史包、不上传云端,也不新增普通用户分享面板。 -- 聊天输入 `/listing` 可在普通聊天消息里准备作品页文案清单,不读取发布说明、不上传云端、不发布作品,也不新增普通用户作品页面板。 -- 聊天输入 `/passes` 可在普通聊天消息里列出最近 run trace 中 `.agent/passes/` 轮次产物读取命令,不直接读取轮次文件,也不新增普通用户轮次面板。 +- 用户侧目标态看到当前工作区项目名 / 路径、最近 run、预览状态、任务完成数 / ready 数、资产数量 / 来源分布、`project-supervisor` 聊天框、上传入口和专业 Agent 协作状态列表;`project-supervisor` 主聊天与专业 Agent 协作状态列表只显示各 Agent 当前 LLM provider / 模型 / 流式 / API Key 读取状态,但不显示密钥本体;最近项目资产入口显示本地路径、kind、mediaType 和来源类型;开发环境通过独立窗口查看任务拆分、专业组细节、产物、文件面板、嵌入预览以及 `.agent/logs/command.log` / `preview.log` / `agent.log`。 - 生成代码和资产进入用户本地项目目录。 - 本地 HTTP 预览能启动,并在客户端项目运行工作台内展示可玩原型。 - 美术/音乐资产能从画板链路回流到本地项目。 -- 短期记忆、长期记忆、项目黑板和角色私有记忆按授权本地项目路径读写;普通用户仍只通过聊天命令访问短期 / 长期 / 黑板记忆,角色私有记忆只在单 agent 对话和生成 loop 中按目标 agent 读取。 +- 短期记忆、长期记忆、项目黑板和角色私有记忆按授权本地项目路径读写;短期 / 长期 / 黑板记忆只由 `project-supervisor` 运行期的记忆工具在授权项目内读写,角色私有记忆只在单 agent 对话和生成 loop 中按目标 agent 读取。 - 结构化对话记录按授权本地项目路径追加 JSONL;正式聊天读取 Supervisor active Session 与只读 legacy 项目历史,开发单 Agent 对话读取对应 Agent Session。开发入口已支持本地 Session 新建、切换、归档和分叉,但不提供云端同步。 - Agent 状态列表从 `.agent/manifest.json` 的任务 / 角色清单、`.agent/run.latest.json` / `.agent/runs/.json` 的 step、taskGraph、passPlans、lifecycleStatus,以及 `read_game_creator_agent_runtimes` 批量读取的 `.agent/runtime/agents/.json` 和最近任务派生;v1 不新增独立状态数据库,也不承诺完整后台 runner。 - 首页发送、项目组目录选择和本地文件选择必须使用 Tauri 非阻塞原生 picker,并把选择器绑定到当前 `client` 窗口;禁止在同步 command 中调用 `blocking_pick_folder` / `blocking_pick_file` 阻塞 WebView 事件循环。选择器打开期间保留首页草稿可编辑,取消后恢复“开启创作”按钮并回显“已取消”。 @@ -1101,75 +1042,11 @@ game-project/ ## v1 验收证据矩阵 - 2026-08-03 资源管理阶段七收口:阶段零至阶段六已逐项对照飞书需求、当前 PRD、实现、测试与提交证据。AppSurface 已覆盖原生视频 `controls + preload="metadata"` 与资源读取策略失败安全空态,并确认失败时右侧 Project Supervisor 对话和底部 Agent Dock 不被中央主视窗替换;相关前端、Rust、原生壳、内容 / 编码和生产运维检查均已完成。本地 `.env*` 不进入提交。 -- `npm run ai-game-creator-shell:check`:覆盖壳 typecheck、聊天命令单测、用户 / 开发窗口 UI 边界 smoke、主窗口命令按钮复用 `/help` 命令列表、主窗口项目摘要从 manifest / trace 派生任务完成数、ready 数、资产来源分布和最近命令且未选项目时不显示、灵感草稿只填充输入框不提交、能力按钮复用 `/capabilities`、LLM状态按钮复用 `/llm-status` 且结果回填 Agent 状态列表、聊天侧 `/agents` 汇总和单 Agent 对话的 provider / 模型 / 流式 / API Key 读取状态、开发日志面板只读读取 `.agent/logs/command.log` / `preview.log` / `agent.log`、项目状态按钮复用 `/status`、权限按钮复用 `/policy` 且策略草稿按钮只填入 `/policy-confirm project.index` / `/policy-confirm asset.register` / `/policy-confirm memory.write` / `/policy-confirm preview.start` / `/policy-confirm preview.open` / `/policy-confirm preview.stop` / `/policy-confirm agent.run_status` / `/policy-confirm conversation.read` / `/policy-confirm conversation.write`、审计按钮复用 `/audit`、资产按钮复用 `/assets` 且资产结果可一键复用 `/read`、任务按钮复用 `/tasks`、聊天侧 `/agents` 汇总每个 Agent 的当前状态、聊天侧 `/agent-conversations` 列出 Agent 对话读取命令、聊天侧 `/agent-memories` 列出 Agent 私有记忆读取命令、Trace 按钮复用 `/trace`、文件按钮复用 `/files` 且文件结果可一键复用 `/read`、索引按钮复用 `/index`、记忆 / 短期记忆 / 黑板按钮复用 `/memory long|short|blackboard`、快照按钮复用 `/checkpoint`、快照列表按钮复用 `/checkpoints` 且 checkpoint 结果可一键复用 `/diff` / `/restore`、历史按钮复用 `/history`、受限命令白名单按钮复用 `/commands` 且无需项目初始化、静态自检快捷按钮复用 `/smoke`、预览状态快捷按钮复用 `/preview-status`、主窗口运行时配置面板读写 Tauri 配置目录中的 `game-creator.config.json`、支持全局与每个 agent 单独选择 LLM Provider 且不把 API Key 写入聊天、单 Agent 对话面板可手动追加私有记忆且走 `memory.write` 策略、主窗口提供音效登记、画板音频导入和常用生成产物读取草稿入口,聊天侧 `/art` 可盘点美术素材且不直接触发平台生成或画板同步,聊天侧 `/context` 可盘点生成上下文来源且不直接读取上下文文件,聊天侧 `/timeline` 可汇总项目活动时间线且不直接读取日志或 trace 文件,聊天侧 `/artifacts` 可列出常用生成产物读取命令,聊天侧 `/run-artifacts` 可列出最近 run 产物读取命令,聊天侧 `/run-files` 可列出 Agent 运行辅助文件读取命令,聊天侧 `/logs` 可列出固定日志读取命令且不直接读取日志,聊天侧 `/brief` 只基于当前已加载的 manifest / 最近 run trace / 预览状态 / 资产数量 / 最近命令生成项目简报,聊天侧 `/goal` 只基于当前 manifest.goal / 最近 run goal / taskGraph.goal 汇总创作目标来源,提供 `/next` 或 `/agent-resume 细化目标:` 后续草稿且不直接触发 Tauri 读写、文件读取、预览启动或新增面板,聊天侧 `/mvp` 只基于当前 manifest / 最近 run trace / preview / 任务 / 资产 / 最近命令汇总本轮最小可玩范围,提供 `/run` 等后续草稿且不直接触发 Tauri 读写、文件读取、预览启动、导出或新增面板,聊天侧 `/audience` 只基于当前 manifest / 最近 run trace / preview 准备首批试玩对象和观察重点且不直接触发 Tauri 读写、预览启动、导出或继续 run,聊天侧 `/feedback` 只基于当前 manifest / 最近 run trace / preview 准备试玩反馈模板和修改说明草稿且不直接触发 Tauri 读写、预览启动或继续 run,聊天侧 `/next` 基于当前已加载的 manifest / 最近 run trace 输出下一步建议和 `/goal` / `/mvp` / `/accessibility` / `/performance` / `/tasks` / `/criteria` / `/groups` / `/budget` / `/qa` / `/changes` / `/trace` / `/run` / `/open-preview` / `/test-plan` / `/audience` / `/feedback` / `/assets` / `/art` / `/context` / `/timeline` / `/artifacts` / `/run-artifacts` / `/run-files` / `/logs` / `/agent-resume ` 等安全命令草稿方向,提供一个首选草稿且不直接触发 Tauri 读写、预览启动或文件读取,聊天侧 `/capabilities` 展示标准 Agent 能力清单且不打开开发面板、聊天侧 `/audit` 从 manifest / 本地文件 / `.agent/run.latest.json` 分别汇总用户面、6 组任务配置、6 组协作证据、任务编排、loop、记忆、本地产物、HTTP 预览、画板回流和权限日志证据且不打开开发面板;未生成 `.agent/run.latest.json` 前,`/audit` 只能标记任务配置通过,不能把 6 组协作证据误判为通过;trace 已存在但状态为 `failed`、`needs-revision`、`running`、`max-passes-exhausted` 或缺少 `Evaluator passed` 步骤时,`/audit` 不能把 loop 误判为通过。聊天侧 `/llm-status` 只显示 base_url / model / API Key 已读取状态且不泄露密钥本体、聊天侧长期记忆查看 / 追加 / 覆盖 / 删除的授权本地项目路径、上传资产写入后的 manifest 刷新和 `/assets` 聊天可见性、`/smoke` 聊天侧确认后只通过授权本地项目路径执行白名单 `game.static_smoke`、`/run` 聊天侧确认后通过授权本地项目路径执行 `game.static_smoke`、启动 `127.0.0.1` 本地预览并切换客户端运行视图、`/preview` 聊天侧确认后通过授权本地项目路径启动 `127.0.0.1` 本地预览并切换客户端运行视图、`/status` 聊天侧项目 / 任务 / 资产 / 预览 / 最近命令汇总、`/files` 聊天侧本地项目文件列表、`/read` 聊天侧文件读取的授权本地项目路径、`/tasks` 聊天侧任务拆分与下一步专业组展示的授权本地项目路径、聊天确认生成后实时展示 Planner / Orchestrator / 角色 brief / Generator / Evaluator / 写盘 / 自检进度,并自动读取 `.agent/run.latest.json` 在普通聊天消息里展示 Run、LLM 对话、loop 轮次、工具调用、active / carry-over 任务、返工焦点、编排轮次、最近步骤、画板同步建议命令和本地产物快照、`/trace` 聊天侧读取 `.agent/run.latest.json` 并展示 loop 轮次 / active 任务 / 返工路线 / dependency waves 的授权本地项目路径、`platform-agent` 编排测试、共享契约测试、Tauri 本地能力测试和无密钥本地 provider 端到端 smoke;用于证明独立 App、真实 LLM-compatible loop、本地落盘、自检和 HTTP 预览闭环,并覆盖 loop 跑满 3 轮失败时不会写入最终游戏产物。 +- `npm run ai-game-creator-shell:check`:串联壳 typecheck 与 `check-config.mjs` 构建期配置门禁、`apps/ai-game-creator-shell/tests` 前端用例、Rust shell 分片用例、`platform-llm` / `shared-contracts` 用例和 `agent-run:smoke` 无密钥本地 provider 端到端 smoke;用于证明独立 App、真实 LLM-compatible loop、本地落盘、自检和 HTTP 预览闭环。 - V1.17 单 Agent 持久计划验收:Rust 定向用例覆盖 native function 显式 `planUpdate`、文本 JSON omission 兼容、输入上限、单调 revision、外层 failed / budget-exhausted 保留最后可信进度、终态保留、工具 action 下标零推进、未完成步骤阻止 final、损坏状态失败关闭、context bundle v3/v2 恢复、finalization v2 计划快照与 assistant 已落盘后的 state 丢失恢复,以及 thinking / legacy plan / repair 公共审计零正文;`appSurface.test.ts` 覆盖开发 UI 刷新后完整 8 步仍在,以及普通用户 Supervisor 只显示完成数、当前步骤、等待、下一步和协作数量。2026-07-16 正式 `openai_chat / gpt-5.5` 的 `steer-runner-kill` suite 已证明 Runner boot 切换和 same-run steer 后 Agent/Session/run 不变、终态步骤不丢、revision 不回退、旧动作零执行、副作用零重放、唯一 assistant/completed 与零正文/密钥/路径公共泄漏,当前门禁状态为 PASS。 - V1.18 单 Agent 持久 Goal mode 验收:Rust/Runner 定向用例覆盖 Goal CAS 生命周期、当前 Session/run 隔离、Provider 中断安全边界、paused 重启不自启、同 run resume、旧 cancel tombstone 清理、当前 v5 context、v5 pending action 的 Goal 快照门禁、旧 schema 失败关闭、Goal edit 后自动/确认动作转 `blocked` 并重规划、finalization v3 以及 assistant 后 Runtime/Goal completed 顺序;`appSurface.test.ts` 覆盖 `执行 / 聊天 / 目标`、独立 Goal 弹层、完整控制动作和正式用户界面隔离。2026-07-16 真实 `openai_chat / gpt-5.5` 已完成 revision 1 -> 2、旧动作 blocked 且零执行、真实失败后 patchset 修复、pause、Runner pidfd 强杀换 boot、重启零推进、显式同 run resume;最终代码快照复跑有 11 组 Provider lifecycle 闭合、计划 revision 11 八步完成、唯一 assistant、零副作用重放和零 Goal/密钥/路径公共泄漏,门禁状态为 PASS。 - `file.delete` 的 Runtime 验收必须覆盖:删除普通文件与缺失文件的幂等结果、缺少路径、目录、绝对路径、父目录、反斜杠、有效与悬空符号链接和整个 `.agent/**` 控制面拒绝、独立 `confirm / deny` 策略、确认前无副作用、确认期间全局 revision 漂移失败关闭、durable action ledger 的 approved / executing / observed 恢复边界、`agent.runtime.file.delete` 审计,以及删除前 revision 推进、删除后必须通过当前 revision 的 `project.verify` 或 `game.static_smoke` 才能收束。另用完整后台 loop 和开发 CLI 真实任务证明 Agent 能自主选择删除并完成验证;普通用户窗口继续没有文件写入或删除入口。 -- `/risks` 聊天入口由 `appSurface.test.ts` 的主窗口 smoke 覆盖:只基于当前已加载的 manifest / trace / 预览 / 任务 / 资产 / 最近命令生成风险摘要,提供首个风险处理草稿,不触发 Tauri 读写、文件读取、预览启动或新增普通用户面板。 -- `/goal` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载 manifest.goal、最近 run goal 和 taskGraph.goal 汇总创作目标来源,提供 `/agent-resume 细化目标:` 或 `/next` 草稿,不触发 Tauri 读写、不读取 spec / 上下文 / trace 文件、不新增普通用户目标面板。 -- `/guide` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载 manifest、最近 run trace、preview 和已加载命令状态判断普通用户当前阶段,给出最多 3 个推荐命令和首选草稿,不触发 Tauri 读写、不读取文件、不启动 run、不启动预览、不写项目、不新增普通用户导引面板。 -- `/progress` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载 manifest、最近 run trace、preview、任务、素材和已加载命令状态汇总项目阶段、任务完成度、最近 run、预览、素材和交付进度,提供 `/run`、`/review`、`/share`、`/test-plan`、`/todo`、`/trace` 或 `/guide` 草稿,不触发 Tauri 读写、不读取文件、不启动 run、不启动预览、不导出试玩包、不写项目、不新增普通用户进度面板。 -- `/spec` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载 manifest、最近 run trace、任务声明产物和 trace 输入 / 输出路径汇总 Planner 规格、玩法设计、数值表、美术清单、音频清单和发布说明状态,提供 `/read .agent/spec.md` 或 `/next` 草稿,不触发 Tauri 读写、不读取规格文件、不启动预览、不写项目、不新增普通用户规格面板。 -- `/mvp` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载 manifest、最近 run trace、preview、任务、资产和最近命令汇总 MVP 内、当前状态、试玩包状态和暂不做事项,提供 `/review`、`/criteria`、`/trace`、`/run`、`/export`、`/exports` 或 `/next` 草稿,不触发 Tauri 读写、不读取文件、不启动或打开预览、不导出试玩包、不新增普通用户 MVP 面板。 -- `/pitch` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载 manifest、最近 run trace 和 preview 状态汇总试玩定位、一句话、核心乐趣、可演示状态、测试者讲解口径和暂不承诺事项,提供 `/mvp`、`/review`、`/trace`、`/open-preview` 或 `/run` 草稿,不触发 Tauri 读写、不读取文件、不启动或打开预览、不直接继续 run、不新增普通用户定位面板。 -- `/demo` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载 manifest、最近 run trace 和 preview 状态准备 30 秒试玩讲解稿、讲解顺序、口播稿、演示状态和收反馈口径,提供 `/run`、`/open-preview`、`/trace`、`/review` 或 `/test-plan` 草稿,不触发 Tauri 读写、不读取文件、不启动或打开预览、不导出试玩包、不发布作品、不新增普通用户讲解面板。 -- `/rules` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载 manifest、最近 run trace.taskGraph、trace artifacts 和 steps 汇总玩法目标、操作 / 胜负 / 重开口径、设计与入口产物状态、相关任务和最近程序 / 试玩步骤,提供 `/read game/game_design.md`、`/agent-resume 操作说明:...` 或 `/next` 草稿,不触发 Tauri 读写、不读取设计文件、不启动预览或继续 run、不新增普通用户规则面板。 -- `/tutorial` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载 manifest、最近 run trace、preview 和任务状态汇总首屏目标、首局 30 秒引导、原型证据、试玩任务、最近引导证据和补齐项,提供 `/rules`、`/review`、`/agent-resume 新手引导:...`、`/open-preview` 或 `/run` 草稿,不触发 Tauri 读写、不读取设计文件、不启动或打开预览、不直接继续 run、不新增普通用户引导面板。 -- `/mobile` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载 manifest、最近 run trace、preview 和任务状态汇总移动试玩目标、键盘 / 触屏输入口径、原型证据、移动检查项、关联任务和最近移动相关步骤,提供 `/rules`、`/review`、`/agent-resume 移动试玩:...`、`/open-preview` 或 `/run` 草稿,不触发 Tauri 读写、不读取代码文件、不启动或打开预览、不直接继续 run、不新增普通用户移动适配面板。 -- `/compatibility` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载 manifest、最近 run trace、preview 和静态自检状态准备推荐环境、输入兼容、不承诺范围、反馈口径和参考命令,提供 `/run`、`/mobile`、`/review`、`/trace` 或 `/next` 草稿,不触发 Tauri 读写、不读取文件、不启动或打开预览、不导出试玩包、不上传云端、不发布作品、不写项目、不新增普通用户兼容性面板。 -- `/accessibility` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载 manifest、最近 run trace、preview 和任务状态汇总文字可读、颜色对比、按钮 / 状态命名、键盘等价、可见焦点、非颜色唯一反馈和静音可玩检查项,提供 `/rules`、`/review`、`/agent-resume 可读性与无障碍:...`、`/open-preview` 或 `/run` 草稿,不触发 Tauri 读写、不读取代码或 trace 文件、不启动或打开预览、不直接继续 run、不新增普通用户无障碍面板。 -- `/localization` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载 manifest、最近 run trace、preview 和发布说明产物状态汇总默认语言、文案范围、关联任务、检查口径、暂不做事项和参考命令,提供 `/read exports/README.md`、`/agent-resume 本地化与文案:...`、`/review` 或 `/next` 草稿,不触发 Tauri 读写、不读取文件、不启动或打开预览、不导出试玩包、不上传云端、不发布作品、不写项目、不新增普通用户本地化面板。 -- `/performance` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载 manifest、最近 run trace、preview、资产数量和 trace artifact 摘要汇总入口自包含、首屏不空白、素材体积、主循环稳定、无远程依赖和预览启动检查,提供 `/run-artifacts`、`/review`、`/open-preview`、`/run` 或 `/next` 草稿,不触发 Tauri 读写、不读取产物或日志文件、不启动或打开预览、不直接继续 run、不新增普通用户性能面板。 -- `/polish` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载 manifest、最近 run trace、preview、最近自检和资产数量汇总试玩前打磨范围、推荐检查顺序、关联任务和最近打磨相关步骤,提供 `/agent-resume 打磨:...`、`/review`、`/feedback` 或 `/next` 草稿,不触发 Tauri 读写、不读取文件、不启动预览、不导出试玩包、不写项目、不新增普通用户打磨面板。 -- `/credits` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载 manifest.assets 汇总素材数量、上传 / 生成 / 画板来源分布、来源清单和交付前需要确认的授权 / 模型 / 画板资源口径,提供 `/assets` 草稿,不触发 Tauri 读写、不刷新资产、不读取素材清单、不导出试玩包、不新增普通用户署名面板。 -- `/blockers` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载 manifest、最近 run trace、preview、最近命令、ready / failed 任务、导出记录和资产概况汇总当前阻塞项,提供 `/run`、`/export`、`/todo`、`/review`、`/trace`、`/tasks`、`/logs`、`/art` 或 `/next` 草稿,不触发 Tauri 读写、不读取文件、不启动预览、不导出试玩包、不写项目、不新增普通用户阻塞面板。 -- `/ready` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载 manifest、最近 run trace、preview、最近自检、导出记录、ready / failed 任务和资产概况汇总试玩就绪度,提供 `/run`、`/export`、`/todo`、`/review`、`/trace`、`/tasks`、`/art`、`/share` 或 `/next` 草稿,不触发 Tauri 读写、不读取文件、不启动预览、不导出试玩包、不写项目、不新增普通用户就绪度面板。 -- `/evidence` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载 manifest、最近 run trace、preview、最近命令、静态自检、导出记录、素材和最近试玩步骤汇总验证证据台账与缺口,提供 `/run`、`/export`、`/art`、`/logs`、`/review`、`/next` 或 `/ready` 草稿,不触发 Tauri 读写、不读取文件、不启动或打开预览、不导出试玩包、不写项目、不新增普通用户证据面板。 -- `/deps` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载 manifest.tasks 和最近 run trace.taskGraph 汇总 active / carry / ready / 等待依赖、可执行任务与依赖等待,提供 `/criteria`、`/todo`、`/tasks` 或 `/next` 草稿,不触发 Tauri 读写、不读取任务文件、不启动 run、不修改项目、不新增普通用户依赖面板。 -- `/revise` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载 manifest 和最近 run trace 汇总返工焦点、失败 / active / carry / ready 任务、最近评审 / 试玩步骤、预览和导出缺口,并填入 `/agent-resume 改版说明:...` 草稿,不触发 Tauri 读写、不读取文件、不继续 run、不启动预览、不导出试玩包、不写项目、不新增普通用户改版面板。 -- `/privacy` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载 manifest、授权项目路径、最近 run trace、preview、资产来源和导出记录汇总 API Key、预览、本地试玩包、内部文件、素材来源和 trace 的隐私 / 交付边界,提供 `/credits`、`/exports` 或 `/config` 草稿,不触发 Tauri 读写、不读取文件、不导出试玩包、不启动预览、不写项目、不新增普通用户隐私面板。 -- `/criteria` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载 manifest.tasks 和最近 run trace.taskGraph 汇总 active、carry、ready、失败或待处理任务的验收条件和产物,提供 `/tasks` 草稿,不触发 Tauri 读写、不读取任务文件或 trace 文件、不新增普通用户验收面板。 -- `/groups` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载 manifest.tasks 和最近 run trace.taskGraph / passPlans 汇总六个专业组的完成、active、carry、ready、失败数量和下一步任务,提供 `/tasks` 草稿,不触发 Tauri 读写、不读取任务文件或 trace 文件、不新增普通用户专业组面板。 -- `/balance` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载 manifest.tasks、最近 run trace.taskGraph、trace artifacts 和 steps 汇总数值组任务、验收口径、`game/balance.json` 状态和最近数值步骤,提供 `/read game/balance.json` 或 `/agent-resume 数值调整:...` 草稿,不触发 Tauri 读写、不读取数值表、不启动预览或继续 run、不新增普通用户数值面板。 -- `/budget` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载最近 run trace 汇总轮次、工具调用、stopReason 和下一步建议,提供 `/review`、`/publish`、`/trace` 或 `/next` 草稿,不触发 Tauri 读写、不读取 trace 文件或新增普通用户预算面板。 -- `/qa` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载 manifest、最近 run trace、最近命令和 preview 状态汇总 Evaluator、任务、静态自检、试玩和产物状态,提供 `/review`、`/tasks`、`/trace`、`/playtest`、`/publish` 或 `/next` 草稿,不触发 Tauri 读写、不读取 trace 或日志文件、不启动或打开预览、不新增普通用户 QA 面板。 -- `/changes` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载 manifest、最近 run trace 的 artifacts / steps 和最近命令汇总可验产物、最近输出、当前资产和真实差异查看方向,提供 `/read <首个可验产物>` 或 `/run-artifacts` 草稿,不触发 Tauri 读写、不读取产物或日志文件、不执行 checkpoint diff、不新增普通用户变更面板。 -- `/todo` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载 manifest 和最近 run trace 汇总失败、active、carry、ready 或待处理任务的下一轮小步,提供 `/tasks`、`/review` 或 `/next` 草稿,不触发 Tauri 读写、不读取任务文件、不启动 run、不修改项目、不新增普通用户小步面板。 -- `/plan` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载 manifest 和最近 run trace 汇总下一轮协作顺序、各专业组接手任务、空档组和首个继续执行草稿,提供 `/agent-resume 下一轮计划:...`、`/review` 或 `/next` 草稿,不触发 Tauri 读写、不读取任务文件、不启动 run、不修改项目、不新增普通用户计划面板。 -- `/review` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载 run trace 汇总 Evaluator 状态、返工焦点、返工路线和最近评审步骤,提供 `/read .agent/findings.md` 或 `/agent-resume ` 草稿,不直接读取评审文件、不触发 Tauri 读写或新增普通用户面板。 -- `/context` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载 manifest 和最近 run trace 列出项目对话、短期 / 长期记忆、黑板、Agent 对话、Agent 私有记忆、manifest、最近 trace 和最近 LLM 输入路径,提供 `/read` 或 `/memory blackboard` 草稿,不触发 Tauri 读写、不读取上下文文件或新增普通用户面板。 -- `/timeline` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载 manifest.commandRuns 和最近 run trace 汇总最近命令、日志读取草稿和最近 Agent 步骤,提供 `/read`、`/trace` 或 `/history` 草稿,不触发 Tauri 读写、不读取日志或 trace 文件、不新增普通用户时间线面板。 -- `/playtest` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载 manifest、最近 run trace 和 preview 状态汇总是否可试玩、Playtest 任务状态和预览日志读取命令,提供 `/run`、`/open-preview`、`/trace` 或 `/review` 草稿,不触发 Tauri 读写、预览启动 / 打开或新增普通用户试玩面板。 -- `/test-plan` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载 manifest、最近 run trace 和 preview 状态准备手动测试用例、关联任务和试玩证据,提供 `/run`、`/open-preview`、`/trace`、`/review` 或 `/next` 草稿,不触发 Tauri 读写、不读取文件、不启动或打开预览、不直接继续 run、不新增普通用户测试面板。 -- `/audience` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载 manifest、最近 run trace、preview 和试玩任务状态汇总首批试玩对象、邀请顺序和观察重点,提供 `/run`、`/feedback`、`/review`、`/trace` 或 `/next` 草稿,不触发 Tauri 读写、不读取文件、不启动或打开预览、不导出试玩包、不写项目、不新增普通用户对象面板。 -- `/invite` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载 manifest、最近 run trace 和 preview 状态准备测试者邀请对象、邀请文案、发送前检查和收反馈口径,提供 `/run`、`/feedback`、`/review`、`/trace` 或 `/next` 草稿,并在 `/next` 与 `/help` 暴露入口;不触发 Tauri 读写、不读取文件、不启动或打开预览、不导出试玩包、不写项目、不新增普通用户邀请面板。 -- `/bug-report` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只追加“已打开报告问题面板”提示并触发独立面板;面板提交前读取应用级诊断日志(不读取 `.agent/logs/*`),只上传当前进程错误事件、脱敏日志和用户文字说明,不上传截图或任意文件,不触发项目读写、预览、导出或 run / run_id。 -- `/survey` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载 manifest、最近 run trace 和 preview 状态准备首批测试者问卷、五个核心问题、记录格式和追踪方式,提供 `/run`、`/invite`、`/review`、`/trace` 或 `/next` 草稿,并在 `/next` 与 `/help` 暴露入口;不触发 Tauri 读写、不读取文件、不启动或打开预览、不导出试玩包、不写项目、不新增普通用户问卷面板。 -- `/retention` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载 manifest、最近 run trace、preview、最近试玩证据、素材数量、发布说明和导出状态准备首轮复玩/留存观察清单,提供 `/run`、`/feedback`、`/review`、`/trace` 或 `/next` 草稿,并在 `/next` 与 `/help` 暴露入口;不触发 Tauri 读写、不读取文件、不启动或打开预览、不导出试玩包、不上传云端、不发布作品、不写项目、不新增普通用户留存面板,也不做真实埋点、留存报表、用户画像、A/B 实验、排行榜或账号留存。 -- `/cover` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载 manifest、最近 run trace、preview 和资产状态准备封面与缩略图候选、用途尺寸、选择口径和补齐路径,提供 `/run`、`/art`、`/listing`、`/review`、`/trace` 或 `/next` 草稿,并在 `/next` 与 `/help` 暴露入口;不触发 Tauri 读写、不截屏、不裁剪、不读取文件、不启动或打开预览、不导出试玩包、不上传云端、不发布作品、不写项目、不新增普通用户封面面板。 -- `/screenshots` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载 manifest、最近 run trace、preview 和资产状态准备宣传截图目标、拍摄顺序、命名建议和作品页搭配,提供 `/run`、`/listing`、`/review`、`/trace` 或 `/next` 草稿,并在 `/next` 与 `/help` 暴露入口;不触发 Tauri 读写、不截屏、不读取文件、不启动或打开预览、不导出试玩包、不写项目、不新增普通用户截图面板。 -- `/trailer` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载 manifest、最近 run trace、preview 和资产状态准备 15 秒试玩短视频结构、镜头清单、口播节奏和录制提示,提供 `/run`、`/share`、`/review`、`/trace` 或 `/next` 草稿,并在 `/next` 与 `/help` 暴露入口;不触发 Tauri 读写、不录屏、不读取文件、不启动或打开预览、不导出试玩包、不上传云端、不写项目、不新增普通用户录屏面板。 -- `/faq` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载 manifest、最近 run trace 和 preview 状态准备试玩常见问答、回答口径、测试者提醒和交付搭配,提供 `/run`、`/share`、`/review`、`/trace` 或 `/next` 草稿,并在 `/next` 与 `/help` 暴露入口;不触发 Tauri 读写、不读取文件、不启动或打开预览、不导出试玩包、不上传云端、不写项目、不新增普通用户 FAQ 面板。 -- `/post` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载 manifest、最近 run trace、preview 和素材状态准备社区发布短文案、长文案结构、标签和 CTA,提供 `/run`、`/store`、`/review`、`/trace` 或 `/next` 草稿,并在 `/next` 与 `/help` 暴露入口;不触发 Tauri 读写、不上传云端、不发布作品、不读取文件、不启动或打开预览、不导出试玩包、不写项目、不新增普通用户社区发布面板。 -- `/store` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载 manifest、最近 run trace、preview、资产和发布说明状态准备上架资料、首发范围、上架前检查和参考命令,提供 `/run`、`/listing`、`/review`、`/trace`、`/read exports/README.md` 或 `/next` 草稿,并在 `/next` 与 `/help` 暴露入口;不触发 Tauri 读写、不上传云端、不发布作品、不读取文件、不启动或打开预览、不导出试玩包、不写项目、不新增普通用户上架面板。 -- `/media-kit` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载 manifest、最近 run trace、preview、资产和发布说明状态准备媒体资料包清单、素材缺口、组装顺序和参考命令,提供 `/run`、`/screenshots`、`/review`、`/trace`、`/read exports/README.md` 或 `/next` 草稿,并在 `/next` 与 `/help` 暴露入口;不触发 Tauri 读写、不截屏、不录屏、不读取文件、不启动或打开预览、不导出试玩包、不上传云端、不发布作品、不写项目、不新增普通用户媒体包面板。 -- `/release-notes` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载 manifest、最近 run trace、preview、资产和发布说明状态准备试玩更新说明、本轮变化、主要产物、玩家可见说明和已知限制,提供 `/run`、`/media-kit`、`/review`、`/trace`、`/read exports/README.md` 或 `/next` 草稿,并在 `/next` 与 `/help` 暴露入口;不触发 Tauri 读写、不读取文件、不启动或打开预览、不导出试玩包、不上传云端、不发布作品、不写项目、不新增普通用户更新说明面板。 -- `/known-issues` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载 manifest、最近 run trace、preview 和任务状态准备已知问题、试玩限制、反馈入口和发送前检查,提供 `/run`、`/share`、`/review`、`/trace` 或 `/next` 草稿,并在 `/next` 与 `/help` 暴露入口;不触发 Tauri 读写、不读取文件、不启动或打开预览、不导出试玩包、不上传云端、不发布作品、不写项目、不新增普通用户已知问题面板。 -- `/feedback` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载 manifest、最近 run trace 和 preview 状态列出反馈方向、反馈模板和参考命令,提供 `/run`、`/agent-resume 试玩反馈:`、`/review` 或 `/next` 草稿,不触发 Tauri 读写、不读取文件、不启动或打开预览、不直接继续 run、不新增普通用户反馈面板。 -- `/share` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载 manifest、授权项目路径、最近 run trace、preview 状态和 manifest.commandRuns 汇总原型通过状态、本地预览、本地试玩包、测试者说明和反馈收集方向,提供 `/export`、`/exports`、`/trace`、`/review` 或 `/next` 草稿,不触发 Tauri 读写、不导出试玩包、不列出历史包、不上传云端、不生成公开分享链接、不新增普通用户分享面板。 -- `/listing` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载 manifest、最近 run trace、发布组任务和资产清单汇总作品页标题、卖点、标签口径、封面素材、发布说明和最近运营步骤,提供 `/read exports/README.md`、`/review`、`/art`、`/publish` 或 `/next` 草稿,不触发 Tauri 读写、不读取发布说明、不上传云端、不发布作品、不新增普通用户作品页面板。 -- `/handoff` 聊天入口由 `appSurface.test.ts` 的主窗口 smoke 覆盖:只基于当前已加载的 manifest / trace / agent 状态 / run history 生成交接摘要,提供 `/next` 草稿,不触发 Tauri 读写、文件读取、预览启动或新增普通用户面板。 -- `/runs` 聊天入口由 `appSurface.test.ts` 的主窗口 smoke 覆盖:只基于当前已加载的 latest trace 和已载入历史 run 批次生成 `/trace` 或 `/read .agent/runs/...` 草稿,不额外触发 Tauri 读取、不滚动加载更多历史、不新增普通用户面板。 -- `/run-files` 聊天入口由 `appSurface.test.ts` 的主窗口 smoke 覆盖:只列出 `.agent/output.jsonl`、`.agent/activity.jsonl` 和 `.agent/context.bundle.json` 的 `/read` 草稿,不直接读取辅助文件、不触发 Tauri 读写或新增普通用户面板。 -- `/passes` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只从当前已加载 run trace 的 artifacts 中筛选 `.agent/passes/` 相对路径,列出 `/read` 草稿并提供首个草稿,不直接读取轮次文件、不触发 Tauri 读写或新增普通用户面板。 -- `/internals` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只列出 `.agent/manifest.json`、`.agent/run.latest.json`、`.agent/spec.md`、`.agent/findings.md`、`.agent/policy.json`、`.agent/project.index.json`、`.agent/agent.db` 和 `.agent/conversations/project.jsonl` 的 `/read` 草稿,不直接读取内部文件、不触发 Tauri 读写或新增普通用户面板。 -- `/export` 聊天入口由 `appSurface.test.ts` 的主窗口 smoke 和 Tauri Rust 测试覆盖:确认后执行 `project.export_package`,导出 `exports/playtest-package-*.zip`,只打包 `game/**`、`assets/**` 和 `exports/README.md`,不包含 `.agent/`、`memory/`、运行时配置、日志、trace 或密钥文件;导出前必须通过 `game/index.html` 可试玩静态验收,并写入 manifest `commandRuns`、`.agent/logs/command.log` 和 `.agent/agent.db`。 -- `/exports` 聊天入口由 `appSurface.test.ts` 主窗口 smoke、项目权限确认测试和 Tauri Rust 测试覆盖:只读执行 `project.export_list`,列出 `exports/playtest-package-*.zip`,跳过符号链接和非试玩包文件,不删除旧包、不做系统分享、不新增普通用户面板。 -- `/art` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 和美术资产定向测试覆盖:只基于当前已加载 manifest 盘点图片、视频和序列帧美术素材数量、来源、画板接入状态和路径,提供 `/generate-art 首版核心美术素材` 或 `/read assets/manifest.art.json` 草稿,不触发 Tauri 文件读取、平台生成、画板同步、资产写入或新增普通用户面板。 -- `/audio` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 和音频资产定向测试覆盖:只基于当前已加载 manifest 盘点音频素材数量、来源和路径,提供 `/asset-register assets/audio/sfx.wav audio audio/wav` 或 `/read assets/manifest.audio.json` 草稿,不触发 Tauri 文件读取、资产写入、画板导入或新增普通用户面板。 -- `/publish` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载 manifest、最近 run trace、预览状态、资产来源和最近命令生成发布准备清单,提供 `/run`、`/trace`、`/agent-resume ` 或 `/export` 草稿,不触发 Tauri 读写、预览启动、文件读取或新增普通用户面板。 -- 主窗口策略快捷入口只填入 `/policy-confirm project.index`、`/policy-confirm asset.register`、`/policy-confirm memory.write`、`/policy-confirm preview.start`、`/policy-confirm preview.open`、`/policy-confirm preview.stop`、`/policy-confirm agent.run_status`、`/policy-confirm conversation.read` 或 `/policy-confirm conversation.write` 草稿;Agent 状态栏的“继续说明”只填入 `/agent-resume ` 草稿,不直接触发 run 生命周期写入。 -- 主窗口最近 checkpoint 列表展示 checkpoint id、文件数、大小和创建时间,同时提供直接对比、填入 `/diff`、确认回滚和填入 `/restore`;回滚继续走 `project.restore` 确认卡,不直接写项目文件。 +- 主窗口最近 checkpoint 列表展示 checkpoint id、文件数、大小和创建时间,同时提供直接对比和确认回滚;回滚继续走 `project.restore` 确认卡,不直接写项目文件。 - `npm run check:native-shells`:覆盖 AI 游戏创作壳的 release/dev 边界、正式用户 App 只嵌入当前 loopback 游戏预览、用户侧预览命令不调用系统浏览器和 Tauri release `--no-bundle` 构建 smoke;该 smoke 必须实际经过最新 `frontendDist` 门禁。门禁 helper 的定向测试同时覆盖干净嵌套产物、嵌套 JS 命中旧 `/tmp` 默认值、产物外测试 fixture 不受影响和链接绕过失败关闭。上述证据用于证明正式发布只登记一个用户窗口,登录后同一 WebView 渲染首页、项目组和项目开发页,Tauri 不自动额外打开 `developer` 窗口,开发面板只在 Vite dev 的 `?dev` / `#dev` 分支渲染,旧窗口切换 command 仅保留兼容、不进入用户主流程。 - `npm run check:encoding` 与 `git diff --check`:覆盖中文文档、中文命令文案和补丁空白;用于避免乱码、尾随空白和无关格式漂移。 - `npm run ai-game-creator-shell:llm-status`:只检查 LLM 客户端配置是否就绪,不请求上游、不显示 API Key;用于本机联调前确认配置。发布版启动时会在 Tauri 应用配置目录生成默认 `game-creator.config.json`,仓库内 `apps/ai-game-creator-shell/game-creator.config.json` 只作为默认模板。 @@ -1186,7 +1063,7 @@ game-project/ - “打开项目”自动识别 Godot:先检查用户所选工作区根的普通文件 `project.godot`;根未命中时只检查一层直接子目录。根命中优先,子目录只有唯一命中才通过;多个子工程时必须在任何 `.agent` 写入前明确报错,要求用户改选具体工程,不能猜测。候选目录与 `project.godot` 都拒绝符号链接和 Windows reparse point,二层及更深目录不递归。首次导入只在用户所选工作区根初始化 `.agent/manifest.json`、`.agent/agent.db`、`.agent/logs/` 与 `.agent/runtime/`,manifest 以 `.` 或单段直接子目录名记录可移植的 `godotProjectRoot`;Agent DB 的 `project.import` 同步记录该相对根。后续项目上下文、文件工具、Session、Runner 和 Agent Runtime 始终使用工作区根;只有未来 Godot 专属启动、构建、测试或导出动作才以经过校验的 Godot 根作为相对 cwd。已有 `.agent/manifest.json` 缺少或记录错误的 `godotProjectRoot` 时,仅在 Godot 打开边界按唯一文件布局校准;歧义时不改写 manifest。 - v1 conversation 记录使用 append-only JSONL,每行带 `schemaVersion`、`role`、`content`、`agentId` 和 `updatedAt`,作为聊天历史和单 agent 对话历史的事实源;目录在首次写入时创建。 - 开发窗口和项目内 Agent 对话弹窗的“后台运行”只启动或排队单 Agent 后台任务,不阻塞等待回复;用户可刷新同一 Agent 对话或 runtime 状态查看进度和结果,也可对当前 run 执行取消 / 重试,待确认 run 还可执行“确认继续”或“拒绝并继续”。后台任务会向 `.agent/runtime/tasks/.jsonl` 追加任务视角记录,任务状态使用 `pending / running / waiting-for-confirmation / cancelled / completed / failed`,读取时按 `runId` 去重返回最近任务;`runId` 在同一 Agent 内是单个 run 的身份,后台入队会自动把重复 runId 改写为唯一实际 runId,防止不同任务互相覆盖;runtime state 自身仍可在完成后显示 `idle / completed`,二者语义分开。同一 Agent 的 pending 任务由持有 `.agent/runtime/locks/.lock` 的后台 drain 串行执行,避免同一 Agent 并发抢上下文;不同 Agent 仍可并行;若某个工具动作命中确认策略,该 Agent 会停在 `waiting-for-confirmation` 并暂停继续消费队列,等待后续确认或策略调整;Runtime 会把完整 `AgentRuntimePendingToolAction` 经过敏感内容和项目绝对路径校验后原子写入 `.agent/runtime/pending-actions//.json`,公共 `pendingToolAction` 只公开安全摘要;确认或拒绝必须匹配 `runId + actionId` 并通过工具名与完整输入 JSON 的 SHA-256 校验。确认在同一 run 直接执行原 action 并把 observation 接回后续 loop;拒绝不执行工具,而是写入 `blocked` observation 后在同一 run 继续规划。待确认状态可跨 App 重启读取并回收上一进程锁;等待期间同 Agent 新任务保持 pending,确认/拒绝续跑结束后由同一 drain 串行排空;若用户取消 pending 任务,drain 不再消费该 run,若取消 running 任务,则在当前 LLM 或工具调用返回后的检查点停止,不继续执行工具或保存最终 assistant 回复。客户端重开项目时会对当前项目路径自动尝试一次 Runtime 恢复;恢复命令必须通过 `agent.resume` 自动权限,默认确认策略下不会静默启动;同一 Agent 同时存在上一进程遗留 `running` 和 `pending` 时,先重接 `running`,再由 drain 继续 `pending`。开发构建和后台 Agent 工具箱都可通过 `agent.schedule_ready` 权限确认入口把 manifest ready task 投递进同一后台队列,命令会先把 ready task 标成 `running`,再用 taskId 作为 Agent id 入队,source 为 `agent-ready-task-scheduler`;该入口不新增独立 worker。后台任务的核心 loop 每 6 轮形成一个上下文压缩窗口:每轮把已有 observation 带回 LLM 让 Agent 重新规划;只有合法工具计划返回空 actions,且不存在未通过或项目修改后未重跑的 `project.verify` 时才收束,response 为空时进入独立最终回复生成。窗口边界会压缩 observation;有新的独立观察时在同一 run 继续下一窗口,最近窗口重复无进展时才以 `budget-exhausted / loop-budget-exhausted` 失败。后台任务完成后会把 assistant 回复追加到对应 `.agent/conversations/agents/.jsonl`,并向 `.agent/agent.db` 写入 `agent.runtime.background_task.queued` / `agent.runtime.background_task` / `agent.runtime.background_task.recovered` / `agent.runtime.ready_task.scheduled` / `agent.runtime.tool_observation` / `agent.runtime.tool_confirmation_required` / `agent.runtime.tool_confirmation.approved` / `agent.runtime.tool_confirmation.rejected` / `agent.runtime.memory.write` / `agent.runtime.project.verify` / `agent.runtime.file.write` / `agent.runtime.file.patch` / `agent.runtime.file.delete` / `agent.runtime.tool_plan.repair` / `agent.runtime.task.create` / `agent.runtime.task.update` / `agent.runtime.command.run_limited` / `agent.runtime.blackboard.write` / `agent.runtime.agent.message` / `agent.runtime.agent.delegate` / `agent.runtime.background_task.cancelled` / `agent.runtime.background_task.retry` / `agent.runtime.background_task.completed` / `agent.runtime.background_task.failed` 审计记录。当前工具箱开放只读工具 `memory.read`、`conversation.read`、`asset.list`、`project.index`、`project.search`、`project.diff`、`file.list`、`file.read`、`task.list`、`agent.run_status`,以及受策略保护的写/运行工具 `memory.write`、`project.checkpoint`、`project.restore`、`project.verify`、`file.write`、`file.patch`、`file.delete`、`task.create`、`task.update`、`command.run_limited`、`preview.start`、`canvas.asset_generate`、`blackboard.write`、`agent.message`、`agent.delegate` 和 `agent.schedule_ready`;`memory.write scope=agent` 只允许写当前 Agent 自己的私有记忆,跨 Agent 共享必须改用 `blackboard.write` 或 `agent.message`;`project.checkpoint` 只创建本地 checkpoint,不返回本机绝对路径;`project.restore` 只按 checkpoint id 恢复当前项目,不返回本机绝对路径,默认确认策略下不会静默回滚;`file.delete` 只删除项目内普通文件,默认确认且不能访问 `.agent/**`;`task.create` 只追加新 manifest 任务,`task.update` 只更新已有任务状态;`agent.schedule_ready` 只调度 manifest ready task,不创建平行 runtime;若项目策略拒绝,对应工具不会执行,Runtime 会把策略结果作为 observation 回给 Agent 修正计划;若项目策略要求确认,Runtime 会持久化精确待确认动作并保留 waiting 状态,不执行该工具;只有确认入口通过 `runId + actionId + SHA-256` 校验后才直接执行原 action,拒绝入口则生成 `blocked` observation。`toolPolicy` 保存当前工具级权限快照,供 planning prompt 和状态面板展示;`recentToolCalls` 保存最近 20 条结构化工具动作及安全 `inputSummary`,供状态面板展示最近动作和确认目标;append-only JSONL 写入按目标文件路径在当前进程内串行追加完整行,覆盖 `.agent/agent.db`、对话、Runtime events/tasks、activity 和 output,减少多个 Agent 同时完成时的行交错风险。 -- 2026-07-15 V1.21 修正:上一条中“每 6 轮形成上下文压缩窗口并压缩 observation”的描述已经失效;6 轮边界现在只形成进度 checkpoint 并执行停滞检测,真正的持久摘要只在 token 估算超过自动阈值或开发者显式执行 `/compact` 时发生。 +- 2026-07-15 V1.21 修正:上一条中“每 6 轮形成上下文压缩窗口并压缩 observation”的描述已经失效;6 轮边界现在只形成进度 checkpoint 并执行停滞检测,真正的持久摘要只在 token 估算超过自动阈值或开发者显式执行手动压缩时发生。 - 2026-07-15 V1.17 收束补充:上一条“空 actions 且 verification 通过即可收束”只适用于没有结构化计划的 legacy run。`planRevision > 0` 后,Runtime 还必须确认全部计划步骤均为 `completed`;未完成时在同一 run 返回 `runtime.plan_update` blocker,不写 assistant 或 completed。计划更新只写 Runtime 控制面,不经过工具权限策略,也不推进 project revision 或 verification gate。 - 2026-07-10 补充:当前工具箱还开放 `preview.start`,审计记录类型为 `agent.runtime.preview.start`;该工具不会打开任意 URL,只启动当前授权项目的 `127.0.0.1` 本地预览,并和 Tauri 用户命令共用同一个 `PreviewRegistry`。 - 2026-07-10 补充:当前工具箱还开放 `canvas.asset_generate`,审计记录类型为 `agent.runtime.canvas.asset_generate`;该工具只通过配置好的 External Editor API 生成并回流素材,不暴露任意上传 / 任意网络请求能力。 @@ -1199,10 +1076,10 @@ game-project/ - 2026-07-10 补充:当前工具箱还开放 `agent.run_status`;该工具只读返回自己、目标 Agent 或一组 Agent 的 Runtime 状态摘要,并受 `agent.run_status` 项目权限策略保护。 - 2026-07-10 补充:当前工具箱还开放 `agent.delegate`,审计记录类型为 `agent.runtime.agent.delegate`;该工具只把任务投递到目标 Agent 既有后台队列,不创建平行 runtime,不绕过目标 Agent 的锁和队列,也受 `agent.delegate` 项目权限策略保护。 - 2026-07-10 补充:当前 Runtime 命令和后台工具箱都开放 `agent.schedule_ready`,审计记录类型为 `agent.runtime.ready_task.scheduled`;该命令把 manifest ready task 批量投递到对应 Agent 的既有后台队列,默认需要确认,确认前不会启动下游 Agent。 -- 普通用户可在聊天框输入 `/project /绝对路径` 生成待确认的 `project.create` 命令,用于授权并初始化本地项目目录;相对路径不会生成待确认命令;开发窗口仍可直接编辑项目路径。 +- 普通用户通过首页 / 项目组页的原生目录选择器授权并初始化本地项目目录;开发窗口仍可直接编辑项目路径。 - 单窗口首页和项目组页不保留常驻路径输入;项目组只展示最近项目只读检查后的工作区绝对路径。打开 / 新建使用原生目录选择器,行尾菜单可显示已确认存在的项目目录,非法或相对路径不会调用系统文件管理器。 -- 普通用户侧的生成、上传、运行、自检、预览状态 / 启动 / 打开 / 停止、记忆写入和画板资产导入都必须先完成 `/project` 初始化;未初始化时只提示设置本地项目,不落到默认临时目录。 -- 终端可用 `npm run ai-game-creator-shell:llm-status` 检查 LLM 客户端配置是否就绪;桌面 App 主窗口“配置”面板可读写 Tauri 应用配置目录中的 `game-creator.config.json`,`/llm-status` / 生成入口读取同一份配置,CLI 开发入口无 AppHandle 时才回退读取仓库旁边的配置模板和 gitignored 本机覆盖文件;不请求上游、不显示 API Key,缺配置时以非零状态退出或在聊天里提示未就绪。 +- 普通用户侧的生成、运行、预览状态 / 启动 / 打开 / 停止、记忆写入和画板资产导入都必须先完成项目初始化;未初始化时只提示设置本地项目,不落到默认临时目录。 +- 终端可用 `npm run ai-game-creator-shell:llm-status` 检查 LLM 客户端配置是否就绪;桌面 App 主窗口“配置”面板可读写 Tauri 应用配置目录中的 `game-creator.config.json`,生成入口读取同一份配置,CLI 开发入口无 AppHandle 时才回退读取仓库旁边的配置模板和 gitignored 本机覆盖文件;不请求上游、不显示 API Key,缺配置时以非零状态退出。 - 终端可用 `npm run ai-game-creator-shell:check` 跑 v1 开发验收:壳 typecheck、`platform-llm` 网关测试、共享契约测试、Tauri Rust 测试和无密钥本地 provider 端到端 smoke;已退役的 `platform-agent` 不再进入 workspace 或该门禁。 - 终端可用 `npm run ai-game-creator-shell:agent-run -- /绝对项目路径 "游戏创作需求"` 跑一次真实 LLM 生成、落盘、`game.static_smoke` 和本地 HTTP 预览;发布 App 读取 Tauri 应用配置目录中的 `game-creator.config.json`,开发 CLI 无 AppHandle 时才读取仓库旁边的配置模板和 gitignored 本机覆盖文件,不把 API Key 写入仓库或项目文件。自动验证可加 `--no-wait`,例如 `npm run ai-game-creator-shell:agent-run -- --no-wait /tmp/genarrative-ai-game-test "像素风反弹弹幕厨房"`,生成预览 trace 后立即停止本地预览,避免终端卡在回车等待。默认 API kind 为 `openai_responses`,且 `llm.stream` 默认开启;旧 Chat Completions 兼容网关设置 `llm.apiKind` 为 `openai_chat`,Anthropic Messages 网关设置 `llm.apiKind` 为 `anthropic`。不支持流式响应的兼容网关可显式设为 `false`。 - 终端可用 `npm run ai-game-creator-shell:agent-run:smoke` 跑一次无密钥本地端到端 smoke:脚本启动本机 OpenAI-compatible SSE 流式测试 provider,预置一个本地上传图片和一个本地上传音频,复用真实 `--agent-run`、Planner / Orchestrator / 角色 agent / Generator / Evaluator loop、本地落盘、`game.static_smoke` 和本地 HTTP 预览,并断言每次 provider 请求都使用 `stream: true`、Planner 与 Generator 分别命中自己的 `agentLlm` provider 配置、provider prompt 收到图片与音频资产上下文以及最近对话上下文、生成 HTML 引用这些资产、预览服务能用 `GET` 读取 `/assets/...`、用 `HEAD` 返回真实资源长度和对应 MIME、headless Chrome 打开预览后至少执行一帧游戏 JS,且通过确定性亮色探针采样证明 canvas 不是空白画布、`.agent/run.latest.json` 的 step group 覆盖 design / balance / art / audio / code / publishing 六组、第二轮会重跑 Evaluator 命中任务及其下游影响任务,未受影响角色 carry-over;随后脚本自动给 CLI 发送回车停止预览。该脚本只用于开发验证,不进入产品生成路径。 @@ -1215,11 +1092,10 @@ game-project/ - 主窗口的 agent 状态列表以 manifest 角色任务为底表,再合并最近 run trace 中 `taskGraph.tasks` 的任务状态、同 taskId / group / role 的最新 step 状态、输入输出路径、错误摘要、lifecycleStatus 和 `activeTaskIds` / `carriedTaskIds` / `readyTaskIds` 编排标记;如果 trace 缺失或过期,只展示 manifest 的静态任务状态和“暂无最近运行证据”。 - 共享契约和 `platform-agent` 会按任务依赖与 `completed` 状态计算当前可执行任务,作为 v1 的最小编排选择器;每轮 `Orchestrator` 的 activeTaskIds、carriedTaskIds、repairRoutes 和 dependencyWaves 由 `platform-agent` 纯编排内核产出,`apps/ai-game-creator-shell` 只负责写入 `.agent/passes/pass-N/` 和执行本地工具;`Evaluator` 会在 `.agent/findings.md` 写出 `## Repair Routes` JSON,下一轮编排优先采用该结构化 taskIds,解析不到时才退回关键词路由;返工路由会按任务图自动扩展下游影响任务,例如美术资产变化会继续触发程序预览和运营包装重算。 - `game.generate_draft` 使用 OpenAI-compatible LLM 配置生成结构化 JSON 草案,发布 App 的配置项来自 Tauri 应用配置目录中的 `game-creator.config.json`:`llm.apiKey`、`llm.baseUrl`、`llm.model`、`llm.apiKind`、`llm.stream`、`llm.requestTimeoutMs`、`llm.maxRetries`、`llm.retryBackoffMs`;默认 API kind 为 `openai_responses`,`llm.stream` 默认开启,可设 `llm.apiKind=openai_chat` 切回旧 Chat Completions 兼容网关,或 `llm.apiKind=anthropic` 走 Anthropic Messages;显式 `llm.stream=false` 时 Planner 和 Generator 使用非流式请求;缺少配置或模型返回非法 JSON 时直接失败,不静默回退固定模板。 -- 聊天输入 `/llm-status` 会触发只读 `llm.config_check`,确认全局 LLM 以及各 agent resolved 后的 base_url、model、API 类型和 API Key 是否已从客户端配置读取;状态消息不会显示或保存 API Key;当前生成链路只要求 Planner / Generator 就绪。`/llm-routes` 复用同一检查结果,但输出按 agent 展开的路由清单和缺口摘要,用于确认哪些 agent 解析后走全局路由、哪些 agent 走单独 provider。 - `game.generate_draft` 的 LLM JSON 必须包含 `handoffs` 数组,覆盖 `design`、`balance`、`art`、`audio`、`code`、`publishing` 6 个专业组;每组必须给出 role、summary、outputs 和 next,缺组或交接内容不完整会判定为模型输出无效并进入返工。 - `game.generate_draft` 的真实生成路径使用最小 Planner / Orchestrator / 组内角色 agent / Generator / Evaluator loop:Planner 写 `.agent/spec.md`;每轮 Orchestrator 先写 `.agent/passes/pass-N/agenda.md` 和 `.agent/passes/pass-N/task-graph.json`,首轮全量调度 16 个角色任务,返工轮按 `.agent/findings.md` 生成结构化 `repairRoutes`,重跑命中问题的角色任务及其下游依赖任务,其余角色 brief 从上一轮 carry-over;`task-graph.json` 记录 activeTaskIds、carriedTaskIds、repairFocus、repairRoutes 和按依赖排序的 dependencyWaves;角色 brief 执行层按 dependencyWaves 调度,wave 之间串行、同一 wave 内多个角色 Agent 以 async task 并行运行;每个角色 brief 必须读取自己的私有记忆 `memory/agents//.md`、本 Agent 对话和项目黑板 `memory/blackboard.md`,写入 `.agent/passes/pass-N/groups//*.md`,再按原组顺序汇总为 `.agent/passes/pass-N/groups/*.md`;Generator 必须读取用户需求、记忆、`.agent/spec.md`、本轮 `agenda.md`、`task-graph.json`、`.agent/findings.md` 和 6 组汇总 brief 后返回结构化 JSON;每轮会把 Generator 草案拆成 6 组交接快照,写入 `.agent/passes/pass-N/`;Evaluator 做质量评审并写 `.agent/findings.md`,通过后才进入 `game.static_smoke` 静态自检和预览试玩。 - loop 最多执行 3 轮;Evaluator 发现 HTML 非自包含、缺少 `canvas`、缺少 `requestAnimationFrame`、缺少输入监听或用户输入未转义时,把问题写入 `.agent/findings.md` 并让下一轮 Generator 修复。3 轮仍失败则 `game.generate_draft` 失败,不写最终游戏产物。 -- loop 每次运行会写 `.agent/run.latest.json` 和 `.agent/runs/.json`,记录 `Planner` / `Orchestrator` agenda / 16 个组内角色 brief 或 carry-over / 6 个 `GroupCoordinator` 汇总 / `Generator` / 6 个专业组交接 / `Evaluator` 质量评审 / `ArtifactWriter` / `Playtest` step、每步 `toolCalls`、输入文件、输出文件、状态、轮次、maxPasses、toolCallCount、maxToolCalls、stopReason、nextStep 和错误摘要;Planner、角色 agent 和 Generator 的 `inputPaths` 必须包含对应记忆文件、`.agent/conversations/project.jsonl`、`.agent/conversations/agents/`、`.agent/manifest.json` 和 agenda 等上下文来源,其中角色 agent 必须包含自己的 `memory/agents//.md` 和 `memory/blackboard.md`;conversation 输入只取最近少量 project / agent 对话摘要,不读取全量历史;每个 step 必须带 phase、taskId、group 和 role,`.agent/run.latest.json.taskGraph` 必须记录 goal、readyTaskIds、activeTaskIds、carriedTaskIds、repairFocus、repairRoutes 和当前任务状态;`.agent/run.latest.json.passPlans` 必须按轮记录 mode、summary、activeTaskIds、carriedTaskIds、dependencyWaves、repairFocus 和 repairRoutes,作为 `/trace` 与开发窗口判断编排 loop 是否真实发生的直接证据;`run.latest.json` 是当前指针,`.agent/runs/` 保留历史 run trace;开发窗口读取 latest 展示编排过程,并复用 `file.list/read` 按文件修改时间先载入最近 20 个历史 run,滚动时再按批次读取剩余历史,普通用户窗口不展示。 +- loop 每次运行会写 `.agent/run.latest.json` 和 `.agent/runs/.json`,记录 `Planner` / `Orchestrator` agenda / 16 个组内角色 brief 或 carry-over / 6 个 `GroupCoordinator` 汇总 / `Generator` / 6 个专业组交接 / `Evaluator` 质量评审 / `ArtifactWriter` / `Playtest` step、每步 `toolCalls`、输入文件、输出文件、状态、轮次、maxPasses、toolCallCount、maxToolCalls、stopReason、nextStep 和错误摘要;Planner、角色 agent 和 Generator 的 `inputPaths` 必须包含对应记忆文件、`.agent/conversations/project.jsonl`、`.agent/conversations/agents/`、`.agent/manifest.json` 和 agenda 等上下文来源,其中角色 agent 必须包含自己的 `memory/agents//.md` 和 `memory/blackboard.md`;conversation 输入只取最近少量 project / agent 对话摘要,不读取全量历史;每个 step 必须带 phase、taskId、group 和 role,`.agent/run.latest.json.taskGraph` 必须记录 goal、readyTaskIds、activeTaskIds、carriedTaskIds、repairFocus、repairRoutes 和当前任务状态;`.agent/run.latest.json.passPlans` 必须按轮记录 mode、summary、activeTaskIds、carriedTaskIds、dependencyWaves、repairFocus 和 repairRoutes,作为开发窗口判断编排 loop 是否真实发生的直接证据;`run.latest.json` 是当前指针,`.agent/runs/` 保留历史 run trace;开发窗口读取 latest 展示编排过程,并复用 `file.list/read` 按文件修改时间先载入最近 20 个历史 run,滚动时再按批次读取剩余历史,普通用户窗口不展示。 - `.agent/run.latest.json` 的 `artifacts` 使用结构化记录,包含相对路径、字节数和 `fnv1a64:` checksum;除最终本地产物外,也会收集 `.agent/passes/pass-N/` 快照,便于确认返工前后的产物差异。 - 通过 Evaluator 和 `game.static_smoke` 后,Agent loop 会把本次 runId、状态、轮次、下一步、active / carry-over 任务和最终本地产物摘要追加到 `memory/session.md` 与 `memory/project.md`,把重要跨 agent 决策 / 依赖 / 风险摘要追加到 `memory/blackboard.md`,并把各角色本轮成功产出的角色摘要追加到 `memory/agents//.md`;下一次 Planner、组内角色和 Generator 会通过记忆输入自然读取上一轮稳定原型状态,而不只依赖开发窗口 trace。 - 单 agent 对话入口读取对应 agent conversation;用户提交后先追加用户消息,再调用 `chat_with_game_creator_role_agent` / `chat_with_game_creator_role_agent_stream` 让对应 `agentLlm.` 结合项目上下文、Agent 私有记忆和本 Agent 历史对话生成回复,随后把回复写入对应 `.agent/conversations/agents/.jsonl`。这里的 `` 以任务 `taskId` 为规范值,Tauri 只兼容旧 `group-role` 别名并映射到 taskId。每轮对话会同步写 `.agent/runtime/agents/.json` 和 `.agent/runtime/events/.jsonl`,字段包含 `agentId`、`taskId`、`sessionId`、`runId`、`source`、`status`、`phase`、`currentTask`、`currentGoal`、`currentAction`、`waitingOn`、`nextStep`、`loopIteration`、`maxLoopIterations`、`toolActionBudget`、`plan`、`planSteps`、`activePlanStepIndex`、`observations`、`recentToolCalls`、`toolPolicy`、`allowedTools`、`lastResponse` 和 `error`;流式事件会把最新 `runtimeState` 回传给界面。Runtime state 写入使用临时文件替换,event JSONL 读取会跳过坏行;`currentTask`、`currentGoal`、event detail、`lastResponse` 和 `agent.db` 摘要复用敏感上下文过滤,不保存明显 API Key / Bearer / Cookie 片段。单 agent 面板可把当前输入手动追加到对应 `memory/agents//.md`,写入前复用 `memory.write` 项目策略和本地项目锁;最近对话可作为本次生成 prompt 上下文读取,但只有经过显式总结、用户显式手动沉淀或生成 loop 成功沉淀的稳定结论,才追加到 `memory/blackboard.md` 或 `memory/agents//.md`。 @@ -1233,11 +1109,10 @@ game-project/ - 2026-07-13 最终提交门禁:最终回复取得项目锁后、创建 finalization journal 前再次读取持久 all-join 交付并确认当前父 run 已认领;未认领按 `Stale` 回到同 run planning,不写 journal、assistant 或 completed。 - 2026-07-13 action 投影恢复:task / event 使用 `runId + actionId + phase` 区分等待确认与终态 observation;Agent DB 终态 observation 幂等扫描忽略同 action 的非终态前置记录,只对终态做全字段冲突检查;`recentToolCalls` 对同 action 原位更新,确保界面不会长期停留在 waiting 状态。 - `game.generate_draft`、资产登记 / 导入、记忆写入、预览状态写入、checkpoint / restore、agent 生命周期控制、画板资源回流 / 生成和 policy 写入会先按 `.agent/policy.json` 判断本次命令是否被项目策略拒绝,再拿项目级 `.agent/project.lock` 串行化;锁只保护同一本地项目,v1 不做后台锁管理。`confirmCommands` 可把索引、状态读取、资产登记、checkpoint、预览、agent 生命周期、画板资源回流 / 生成、memory 读写删除和 conversation 读写等命令转成项目策略确认,命中时用户确认后才执行;用户可用 `/policy-confirm 命令` 加入确认列表,用 `/policy-auto 命令` 移除确认项。 -- `ArtifactWriter` 写入最终产物前把当前项目文件保存到 `.agent/checkpoints//`,写入后把新增、修改、删除计数记录到 `.agent/agent.db`;聊天命令 `/checkpoint`、`/checkpoints`、`/diff checkpoint-id` 和 `/restore checkpoint-id` 允许用户手动保存、列出最近 checkpoint、对比和确认回滚到 checkpoint,回滚时会删除 checkpoint 后新增的受跟踪项目文件。`.agent/runtime/` 属于运行观测状态,不进入项目索引、checkpoint diff 或 restore 删除范围。 +- `ArtifactWriter` 写入最终产物前把当前项目文件保存到 `.agent/checkpoints//`,写入后把新增、修改、删除计数记录到 `.agent/agent.db`;确认回滚走 `project.restore` 确认卡,回滚时会删除 checkpoint 后新增的受跟踪项目文件。`.agent/runtime/` 属于运行观测状态,不进入项目索引、checkpoint diff 或 restore 删除范围。 - Planner、组内角色和 Generator 读取上下文前会先做安全过滤:拒绝 `.env*`、`game-creator.config*`、Authorization / Cookie / API Key / Token / Bearer 等密钥样式内容,并清理 `sk-*` / `tnr_sk_*` token;memory、资产摘要和 conversation JSONL 中被过滤的内容不进入 LLM prompt。 - `.agent/run.latest.json` 的 schema 固定为共享契约 `GAME_CREATION_AGENT_RUN_SCHEMA_VERSION = game-creator-agent-run.v1`;TS 与 Rust 都从共享契约读取 run trace 类型,避免开发窗口和 Tauri 写入结构漂移。 -- `.agent/run.latest.json` 增加可选 `lifecycleStatus`,把一次生成 run 映射到本地最小生命周期:`scheduled / running / waiting / pending / done / failed / killed`。聊天命令 `/agent-status` 读取最近 run,`/agent-kill` 标记为 `killed`,`/agent-retry` 与 `/agent-resume [说明]` 标记为 `pending`,并写入 `.agent/activity.jsonl`、`.agent/output.jsonl` 和 `.agent/context.bundle.json`;状态 / 控制结果消息可一键填入 `/read .agent/output.jsonl` 草稿继续查看 run 输出,Agent 状态栏也可填入 output / activity / context bundle 的 `/read` 草稿。v1 只做本地状态控制,不承诺真正中断已在上游执行中的 LLM 请求;后续引入独立 runner 后再把 `pending` 接入 claim。 -- 主窗口 Agent 状态栏的“继续”保留确认卡,“继续说明”只把 `/agent-resume ` 放入聊天输入框,方便用户补充说明后再走同一确认流。 +- `.agent/run.latest.json` 增加可选 `lifecycleStatus`,把一次生成 run 映射到本地最小生命周期:`scheduled / running / waiting / pending / done / failed / killed`;该字段与 run 的 `.agent/activity.jsonl` / `.agent/output.jsonl` / `.agent/context.bundle.json` 写入由 Runtime 维护。v1 只做本地状态控制,不承诺真正中断已在上游执行中的 LLM 请求;后续引入独立 runner 后再把 `pending` 接入 claim。 - v1 的 agent 状态列表和单 agent 对话都复用上述本地文件事实源:状态从 manifest / run trace 派生,单 agent 消息写对应 Session conversation JSONL;Session 支持创建、切换和只改元数据的归档,不引入对话 fork,也不把 `pending` 包装成已经具备后台 claim / resume runner。 - `game.generate_draft` 写入最终产物后会复用白名单受限命令 `game.static_smoke` 做一次生成后自检,至少检查 `game/index.html` 包含 canvas、canvas 渲染上下文、绘制调用、主循环、非空输入监听、明确目标、失败或胜利状态和重开路径,且不使用远程资源、`eval`、`new Function`、`localStorage`、`fetch`、`WebSocket` 或 `ServiceWorker`,也不得包含固定星核传送门模板词、纯按钮计分模板或 `TODO` / `待实现` / `这里省略` 等未完成实现;画板资源占位引用允许出现在 asset id 或说明中,并把该工具调用写入 `.agent/run.latest.json` 与 `.agent/logs/command.log`;自检失败则本次命令失败,不继续启动预览。 - `ArtifactWriter` step 使用 `file.write.local_artifacts` 工具调用记录最终写入的 `memory/`、`memory/agents/`、`game/`、`assets/`、`exports/` 和 `.agent/manifest.json` 路径;写入完成后 `nextStep` 指向 `game.static_smoke`。 @@ -1250,81 +1125,16 @@ game-project/ - `game.generate_draft` 会把组内角色 brief、专业组汇总 brief、专业组 / 角色 / 产物交接摘要写入 `.agent/passes/pass-N/groups/`、`game/game_design.md`、`exports/README.md` 和 `.agent/logs/agent.log`,用于追踪 6 组 agent 的最小协作结果。 - 文件选择会生成待确认的 `asset.upload` 内置命令;用户确认后才把文件写入 `assets/uploads/` 并登记到 manifest。 - `game.generate_draft` 会从 `.agent/manifest.json` 派生本地资产摘要,把已上传、已登记和画板回流资产的 id、类型、媒体类型、本地路径、来源和画板追踪 ID 注入 Planner / 组内角色 / Generator 共用的上下文;资产摘要排在长期记忆前,避免长期记忆过长时被 prompt 截断;不新增平行资产记忆文件。 -- 聊天输入 `/help` 会在聊天里列出当前可用内置命令,普通用户不需要打开任何开发面板来发现命令;`/capabilities` 会直接列出聊天 / 上传 / 内置命令、任务拆分、编排、Planner / Generator / Evaluator loop、工具预算、多智能体协作、组内角色协作、短期 / 长期记忆、本地产物、本地 HTTP 预览、画板同步、权限 gate 和 trace 日志等标准 Agent 能力;`/audit` 会只读聚合 manifest、文件列表和最近 run trace,逐项显示当前本地项目是否已经具备这些能力证据,并区分“6 组任务已配置”“最近 run trace 已实际覆盖 6 组协作”和“最近 loop 是否真正通过 Evaluator”。 - 主窗口“灵感草稿”只把一条可编辑的小游戏需求写入聊天输入框,不提交、不创建项目、不调用 LLM;用户仍需自行发送并确认生成命令。 - 聊天区待确认命令必须先记录 `permission.pending`,用户点击后再记录 `permission.confirm` 或 `permission.cancel`;已授权本地项目内的权限事件同步追加到 `.agent/logs/command.log`,待确认卡片显示本地写入目标路径。 -- 聊天输入 `/status` 会读取 `.agent/manifest.json` 并在聊天里汇总项目目录、任务状态、资产数量、预览状态和最近命令,不向普通用户暴露任务或文件面板。 -- 聊天输入 `/files` 会通过 `file.list` 只读列出本地项目内的文件摘要;主窗口最近项目文件可一键读取,也可一键填入 `/read` 或 `/asset-register` 草稿,但资产登记仍必须走聊天确认;`/checkpoints` 复用 `file.list` / `file.read` 只读列出最近 checkpoint id、文件数、大小和可复制的 `/diff` / `/restore` 命令,主窗口最近 checkpoint 列表也可填入对应草稿;普通用户仍不暴露文件读写面板。 -- 聊天输入 `/assets` 会读取 `.agent/manifest.json` 并在聊天里列出本地项目资产路径、类型和来源,资产列表消息和主窗口最近项目资产入口都可一键填入对应资产的 `/read` 草稿;聊天输入 `/art` 只使用当前已加载 manifest 盘点美术素材,并提供首版美术生成或读取美术清单草稿,不直接读取文件、不触发平台生成或画板同步;聊天输入 `/audio` 只使用当前已加载 manifest 盘点音频素材,并提供登记音效或读取音频清单草稿,不直接读取文件、不触发资产写入;`/asset-register 路径 [kind] [mediaType]` 可确认后登记项目内已有资产;主窗口音效快捷入口只填入 `/asset-register assets/audio/sfx.wav audio audio/wav` 草稿,不直接写 manifest;普通用户仍不暴露资产面板。 -- 聊天输入 `/read 本地相对路径` 会通过 `file.read` 只读返回项目内文本文件内容并在聊天中截断长文本;聊天回执中的正文必须包装为安全的 `
` 代码块,保留源码字面量但不得执行 HTML;普通用户仍不暴露文件写入或删除能力。
-- 主窗口常用生成产物入口只把 `game/index.html`、`game/game_design.md`、`game/balance.json`、`assets/manifest.art.json`、`assets/manifest.audio.json` 和 `exports/README.md` 的 `/read` 草稿填入聊天输入框;聊天输入 `/artifacts` 只列出这组固定读取命令并提供首个 `/read` 草稿,聊天输入 `/run-artifacts` 只列出最近 run trace 里的产物读取命令并提供首个 `/read` 草稿,聊天输入 `/run-files` 只列出 `.agent/output.jsonl`、`.agent/activity.jsonl` 和 `.agent/context.bundle.json` 的读取命令并提供首个 `/read` 草稿;读取仍由聊天侧 `file.read` 权限流执行。
-- 聊天输入 `/logs` 只列出 `.agent/logs/command.log`、`.agent/logs/preview.log` 和 `.agent/logs/agent.log` 对应的 `/read ...` 草稿 / 命令,并提供首个 `/read` 草稿;该命令不直接读取日志,不新增普通用户日志面板,实际读取仍由聊天侧 `file.read` 权限流执行。
-- 聊天输入 `/tasks` 会读取 `.agent/manifest.json` 并在聊天里列出专业组、角色、任务状态、产物交接和下一步可执行任务;普通用户仍不暴露任务面板。
-- 聊天输入 `/trace` 会通过 `agent.trace_read` 只读读取 `.agent/run.latest.json`,在聊天里汇总最近一次 loop 的轮次、stopReason、nextStep、active/carry-over 任务、repairRoutes、agent 建议命令、产物快照和最近 step;没有待同步建议命令时,首个安全产物可一键填入 `/read ` 草稿;active、carry-over、repairRoutes 和 dependencyWaves 必须把 taskId 映射成专业组 / 角色 / 任务名,避免普通用户只能看到内部 id;普通用户仍不暴露开发 trace 面板。
-- 聊天输入 `/brief` 只使用主窗口当前已加载的 manifest、最近 run trace、预览状态、资产数量和最近命令摘要,在聊天里生成项目简报,并提供 `/next` 作为后续草稿;该命令不调用 Tauri 读写、不读取文件、不启动或打开预览、不新增普通用户面板,用户可再发送 `/next` 草稿继续。
-- 聊天输入 `/goal` 只使用主窗口当前已加载的 manifest.goal、最近 run goal 和 taskGraph.goal,在聊天里列出项目目标来源、上下文查看方向和后续目标细化草稿;该命令不调用 Tauri 读写、不读取 spec、上下文或 trace 文件、不新增普通用户目标面板,真正补充目标仍由用户发送 `/agent-resume 细化目标:` 并走原确认流。
-- 聊天输入 `/spec` 只使用主窗口当前已加载的 manifest、最近 run trace、任务声明产物和 trace 输入 / 输出路径,在聊天里列出 Planner 规格、玩法设计、数值表、美术清单、音频清单和发布说明的状态,并提供 `/read .agent/spec.md` 或 `/next` 草稿;该命令不调用 Tauri 读写、不读取规格文件、不启动预览、不写项目、不新增普通用户规格面板,真正读取规格仍由用户发送 `/read` 并走原权限流。
-- 聊天输入 `/mvp` 只使用主窗口当前已加载的 manifest、最近 run trace、preview、任务、资产和最近命令摘要,在聊天里列出本轮 MVP 内、当前状态、任务、预览、资产、试玩包状态和暂不做事项;该命令不调用 Tauri 读写、不读取文件、不启动或打开预览、不导出试玩包、不新增普通用户 MVP 面板,真正运行、导出或查看包仍由用户发送对应草稿并走原权限流。
-- 聊天输入 `/pitch` 只使用主窗口当前已加载的 manifest、最近 run trace 和 preview 状态,在聊天里列出试玩定位、一句话、核心乐趣、当前可演示状态、测试者讲解口径和暂不承诺事项;该命令不调用 Tauri 读写、不读取文件、不启动或打开预览、不直接继续 run、不新增普通用户定位面板,真正运行、打开预览或查看 trace 仍由用户发送对应草稿并走原权限流。
-- 聊天输入 `/demo` 只使用主窗口当前已加载的 manifest、最近 run trace 和 preview 状态,在聊天里列出 30 秒开场、讲解顺序、口播稿、演示状态、最近试玩证据和收反馈口径;该命令不调用 Tauri 读写、不读取文件、不启动或打开预览、不导出试玩包、不发布作品、不新增普通用户讲解面板,真正运行、打开预览或记录反馈仍由用户发送对应草稿并走原权限流。
-- 聊天输入 `/rules` 只使用主窗口当前已加载的 manifest、最近 run trace.taskGraph、trace artifacts 和 steps,在聊天里列出玩法目标、操作 / 胜负 / 重开口径、设计与入口产物状态、相关任务和最近程序 / 试玩步骤;该命令不调用 Tauri 读写、不读取设计文件、不启动预览、不直接继续 run、不新增普通用户规则面板,真正读取设计或补充操作说明仍由用户发送对应草稿并走原权限流。
-- 聊天输入 `/tutorial` 只使用主窗口当前已加载的 manifest、最近 run trace、preview 和任务状态,在聊天里列出首屏目标、首局 30 秒引导、原型证据、试玩任务、最近引导证据和补齐项;该命令不调用 Tauri 读写、不读取设计文件、不启动或打开预览、不直接继续 run、不新增普通用户引导面板,真正补齐引导、运行或打开预览仍由用户发送对应草稿并走原权限流。
-- 聊天输入 `/mobile` 只使用主窗口当前已加载的 manifest、最近 run trace、preview 和任务状态,在聊天里列出移动试玩目标、键盘 / 触屏输入口径、原型证据、移动检查项、关联任务和最近移动相关步骤;该命令不调用 Tauri 读写、不读取代码文件、不启动或打开预览、不直接继续 run、不新增普通用户移动适配面板,真正补齐移动试玩、运行或打开预览仍由用户发送对应草稿并走原权限流。
-- 聊天输入 `/accessibility` 只使用主窗口当前已加载的 manifest、最近 run trace、preview 和任务状态,在聊天里列出可读性与无障碍检查范围、原型证据、关联任务和最近无障碍相关步骤;该命令不调用 Tauri 读写、不读取代码或 trace 文件、不启动或打开预览、不直接继续 run、不新增普通用户无障碍面板,真正补齐可读性与无障碍事项仍由用户发送对应草稿并走原权限流。
-- 聊天输入 `/performance` 只使用主窗口当前已加载的 manifest、最近 run trace、preview、资产数量和 trace artifact 摘要,在聊天里列出性能与加载检查范围、关键产物、关联任务和最近性能相关步骤;该命令不调用 Tauri 读写、不读取产物或日志文件、不启动或打开预览、不直接继续 run、不新增普通用户性能面板,真正查看产物或启动试玩仍由用户发送对应草稿并走原权限流。
-- 聊天输入 `/polish` 只使用主窗口当前已加载的 manifest、最近 run trace、preview、最近自检和资产数量,在聊天里列出试玩前打磨范围、推荐顺序、关联任务和最近打磨相关步骤;该命令不调用 Tauri 读写、不读取文件、不启动预览、不导出试玩包、不写项目、不新增普通用户打磨面板,真正继续打磨、查看评审或准备反馈仍由用户发送对应草稿并走原权限流。
-- 聊天输入 `/credits` 只使用主窗口当前已加载的 manifest.assets,在聊天里列出素材数量、来源分布、来源清单和交付前需要确认的授权 / 模型 / 画板资源口径;该命令不调用 Tauri 读写、不刷新资产、不读取素材清单、不导出试玩包、不新增普通用户署名面板,真正查看完整资产仍由用户发送 `/assets` 并走原资产读取流。
-- 聊天输入 `/blockers` 只使用主窗口当前已加载的 manifest、最近 run trace、preview、最近命令、ready / failed 任务、导出记录和资产概况,在聊天里列出当前阻塞项和首个处理草稿;该命令不调用 Tauri 读写、不读取文件、不启动预览、不导出试玩包、不写项目、不新增普通用户阻塞面板,真正处理阻塞仍由用户发送对应草稿并走原权限流。
-- 聊天输入 `/ready` 只使用主窗口当前已加载的 manifest、最近 run trace、preview、最近自检、导出记录、ready / failed 任务和资产概况,在聊天里列出试玩就绪度和首个处理草稿;该命令不调用 Tauri 读写、不读取文件、不启动预览、不导出试玩包、不写项目、不新增普通用户就绪度面板,真正启动预览、导出试玩包或交付仍由用户发送对应草稿并走原权限流。
-- 聊天输入 `/deps` 只使用主窗口当前已加载的 manifest.tasks 和最近 run trace.taskGraph,在聊天里列出任务依赖链、可执行任务和等待依赖;该命令不调用 Tauri 读写、不读取任务文件、不启动 run、不修改项目、不新增普通用户依赖面板,真正查看完整任务或验收标准仍由用户发送对应草稿并走原权限流。
-- 聊天输入 `/revise` 只使用主窗口当前已加载的 manifest 和最近 run trace,在聊天里准备下一轮改版说明草稿;该命令不调用 Tauri 读写、不读取文件、不继续 run、不启动预览、不导出试玩包、不写项目、不新增普通用户改版面板,真正继续改版仍由用户发送 `/agent-resume 改版说明:...` 草稿并走原确认流。
-- 聊天输入 `/privacy` 只使用主窗口当前已加载的 manifest、授权项目路径、最近 run trace、preview、资产来源和导出记录,在聊天里列出 API Key、预览、本地试玩包、内部文件、素材来源和 trace 的隐私 / 交付边界;该命令不调用 Tauri 读写、不读取文件、不导出试玩包、不启动预览、不写项目、不新增普通用户隐私面板,真正查看素材来源、导出或配置仍由用户发送对应草稿并走原权限流。
-- 聊天输入 `/risks` 只使用主窗口当前已加载的 manifest、最近 run trace、预览状态、任务状态、资产来源和最近命令摘要,在聊天里列出当前项目风险,并提供首个风险处理草稿;该命令不调用 Tauri 读写、不读取文件、不启动或打开预览、不新增普通用户面板,用户必须再发送草稿并按原命令权限流继续。
-- 聊天输入 `/criteria` 只使用主窗口当前已加载的 manifest.tasks 和最近 run trace.taskGraph,在聊天里列出 active、carry、ready、失败或待处理任务的验收条件和预期产物,并提供 `/tasks` 草稿;该命令不调用 Tauri 读写、不读取任务文件或 trace 文件、不新增普通用户验收面板,真正查看完整任务仍由用户发送 `/tasks` 并走原读取流。
-- 聊天输入 `/groups` 只使用主窗口当前已加载的 manifest.tasks 和最近 run trace.taskGraph / passPlans,在聊天里列出六个专业组的完成、active、carry、ready、失败数量和下一步任务,并提供 `/tasks` 草稿;该命令不调用 Tauri 读写、不读取任务文件或 trace 文件、不新增普通用户专业组面板,真正查看完整任务仍由用户发送 `/tasks` 并走原读取流。
-- 聊天输入 `/balance` 只使用主窗口当前已加载的 manifest.tasks、最近 run trace.taskGraph、trace artifacts 和 steps,在聊天里列出数值组任务、验收口径、`game/balance.json` 状态、最近数值步骤和试玩关联;该命令不调用 Tauri 读写、不读取数值表、不启动预览、不直接继续 run、不新增普通用户数值面板,真正读取或提交数值反馈仍由用户发送对应草稿并走原权限流。
-- 聊天输入 `/budget` 只使用主窗口当前已加载的最近 run trace,在聊天里列出轮次、工具调用、stopReason 和下一步建议,并提供 `/review`、`/publish`、`/trace` 或 `/next` 草稿;该命令不调用 Tauri 读写、不读取 trace 文件、不新增普通用户预算面板,真正查看完整 trace 仍由用户发送 `/trace` 并走原读取流。
-- 聊天输入 `/qa` 只使用主窗口当前已加载的 manifest、最近 run trace、最近命令和 preview 状态,在聊天里列出 Evaluator、任务、静态自检、试玩、最近试玩步骤和产物数量,并提供 `/review`、`/tasks`、`/trace`、`/playtest`、`/publish` 或 `/next` 草稿;该命令不调用 Tauri 读写、不读取 trace 或日志文件、不启动或打开预览、不新增普通用户 QA 面板,真正运行自检或打开预览仍由用户发送对应草稿并走原确认流。
-- 聊天输入 `/changes` 只使用主窗口当前已加载的 manifest、最近 run trace 的 artifacts / steps 和最近命令,在聊天里列出最近 run、可验产物数量、关键产物、最近输出、当前资产、全部产物读取入口、trace 入口和真实 diff 查看方向,并提供 `/read <首个可验产物>` 或 `/run-artifacts` 草稿;该命令不调用 Tauri 读写、不读取产物或日志文件、不执行 checkpoint diff、不新增普通用户变更面板,真正读取产物或对比 checkpoint 仍由用户发送对应草稿并走原权限流。
-- 聊天输入 `/todo` 只使用主窗口当前已加载的 manifest 和最近 run trace,在聊天里列出下一轮优先小步,优先失败、active、carry 和 ready 任务,没有 trace 时回退当前 ready 任务,并提供 `/tasks`、`/review` 或 `/next` 草稿;该命令不调用 Tauri 读写、不读取任务文件、不启动 run、不修改项目、不新增普通用户小步面板,真正查看任务或继续修复仍由用户发送对应草稿并走原权限流。
-- 聊天输入 `/plan` 只使用主窗口当前已加载的 manifest 和最近 run trace,在聊天里列出下一轮协作顺序、各专业组接手任务、空档组和继续执行草稿;该命令不调用 Tauri 读写、不读取任务文件、不启动 run、不修改项目、不新增普通用户计划面板,真正继续执行仍由用户发送 `/agent-resume 下一轮计划:...` 并走原确认流。
-- 聊天输入 `/handoff` 只使用主窗口当前已加载的 manifest、最近 run trace、Agent 状态和已载入历史 run 批次,在聊天里生成项目交接摘要,并提供 `/next` 作为后续草稿;该命令不调用 Tauri 读写、不读取文件、不启动或打开预览、不新增普通用户面板,用户必须再发送草稿并按原命令权限流继续。
-- 聊天输入 `/runs` 只使用主窗口当前已加载的 latest trace 和已载入历史 run 批次,在聊天里列出 `/trace` 和 `/read .agent/runs/...` 读取草稿;该命令不调用 Tauri 读写、不滚动加载更多历史、不启动或打开预览、不新增普通用户面板,用户必须再发送草稿并按原命令权限流继续。
-- 聊天输入 `/next` 只使用主窗口当前已加载的 manifest、最近 run trace 和最近命令摘要,在聊天里给出下一步建议,列出 `/goal`、`/guide`、`/progress`、`/spec`、`/mvp`、`/pitch`、`/demo`、`/rules`、`/tutorial`、`/mobile`、`/compatibility`、`/accessibility`、`/localization`、`/performance`、`/polish`、`/blockers`、`/ready`、`/evidence`、`/llm-routes`、`/deps`、`/revise`、`/privacy`、`/audience`、`/invite`、`/bug-report`、`/survey`、`/cover`、`/screenshots`、`/trailer`、`/faq`、`/post`、`/store`、`/media-kit`、`/release-notes`、`/known-issues`、`/tasks`、`/criteria`、`/groups`、`/balance`、`/budget`、`/qa`、`/changes`、`/todo`、`/trace`、`/review`、`/context`、`/timeline`、`/playtest`、`/test-plan`、`/feedback`、`/retention`、`/share`、`/listing`、`/run`、`/export`、`/exports`、`/open-preview`、`/assets`、`/credits`、`/art`、`/audio`、`/publish`、`/artifacts`、`/run-artifacts`、`/passes`、`/run-files`、`/internals`、`/logs`、`/agent-resume ` 等安全命令草稿方向,并提供一个首选草稿;该命令不调用 Tauri 读写、不启动或打开预览、不读取文件,用户必须再发送草稿并按原命令权限流继续。
-- `/next` 的安全命令草稿方向同步包含 `/plan`,用于从下一步建议进入下一轮分工摘要;该补充不改变 `/next` 的只读边界。
-- 聊天输入 `/review` 只使用主窗口当前已加载的最近 run trace,在聊天里列出 Evaluator 通过 / 需返工状态、返工焦点、返工路线、下一步和最近评审步骤;该命令不调用 Tauri 读写、不读取 `.agent/findings.md`、不新增普通用户评审面板,真正读取评审记录仍由用户发送 `/read .agent/findings.md` 并走 `file.read` 权限流,需要返工时只填入 `/agent-resume ` 草稿。
-- 聊天输入 `/context` 只使用主窗口当前已加载的 manifest 和最近 run trace,在聊天里列出项目对话、短期记忆、长期记忆、项目黑板、Agent 对话、Agent 私有记忆、manifest、最近 trace 和最近 LLM 输入路径;该命令不调用 Tauri 读写、不读取上下文文件、不新增普通用户上下文面板,真正查看上下文仍由用户发送 `/read` 或 `/memory blackboard` 并走原权限流。
-- 聊天输入 `/timeline` 只使用主窗口当前已加载的 manifest.commandRuns 和最近 run trace,在聊天里列出最近命令、日志读取草稿、最近 Run 状态和最近 Agent 步骤;该命令不调用 Tauri 读写、不读取日志或 trace 文件、不新增普通用户时间线面板,真正查看日志、trace 或历史仍由用户发送对应草稿并走原权限流。
-- 聊天输入 `/playtest` 只使用主窗口当前已加载的 manifest、最近 run trace 和 preview 状态,在聊天里列出原型是否已通过、预览是否运行、Playtest 任务状态、最近试玩步骤和 `.agent/logs/preview.log` 读取命令;该命令不调用 Tauri 读写、不启动或打开预览、不读取日志、不新增普通用户试玩面板,真正运行或打开仍由用户发送 `/run` 或 `/open-preview` 并走原确认流。
-- 聊天输入 `/test-plan` 只使用主窗口当前已加载的 manifest、最近 run trace 和 preview 状态,在聊天里列出手动测试用例、Preview / Playtest 任务和最近试玩证据;该命令不调用 Tauri 读写、不读取文件、不启动或打开预览、不直接继续 run、不新增普通用户测试面板,真正运行、打开预览或记录反馈仍由用户发送对应草稿并走原权限流。
-- 聊天输入 `/audience` 只使用主窗口当前已加载的 manifest、最近 run trace、preview 和试玩任务状态,在聊天里列出首批试玩对象、邀请顺序、观察重点和暂不面向的人群;该命令不调用 Tauri 读写、不读取文件、不启动或打开预览、不导出试玩包、不直接继续 run、不新增普通用户对象面板,真正运行、反馈或交付仍由用户发送对应草稿并走原权限流。
-- 聊天输入 `/feedback` 只使用主窗口当前已加载的 manifest、最近 run trace 和 preview 状态,在聊天里列出反馈方向、反馈模板和参考命令;该命令不调用 Tauri 读写、不读取文件、不启动或打开预览、不直接继续 run、不新增普通用户反馈面板,真正提交反馈仍由用户发送 `/agent-resume 试玩反馈:` 并走原确认流。
-- 聊天输入 `/share` 只使用主窗口当前已加载的 manifest、授权项目路径、最近 run trace、preview 状态和 manifest.commandRuns,在聊天里列出原型通过状态、本地预览、本地试玩包、给测试者的简短说明和反馈收集方向;该命令不调用 Tauri 读写、不导出试玩包、不列出历史包、不上传云端、不生成公开分享链接、不新增普通用户分享面板,真正导出或查看试玩包仍由用户发送 `/export` 或 `/exports` 并走原权限流。
-- 聊天输入 `/listing` 只使用主窗口当前已加载的 manifest、最近 run trace、发布组任务和资产清单,在聊天里列出作品页标题、一句话卖点、发布任务、封面素材、说明文案、标签口径和最近运营步骤;该命令不调用 Tauri 读写、不读取发布说明、不上传云端、不发布作品、不新增普通用户作品页面板,真正读取发布说明或继续发布准备仍由用户发送 `/read exports/README.md` 或 `/publish` 并走原权限流。
-- 聊天输入 `/passes` 只使用主窗口当前已加载的最近 run trace,从 `artifacts` 中筛选 `.agent/passes/` 相对路径,在聊天里列出轮次产物读取命令并提供首个 `/read` 草稿;该命令不调用 Tauri 读写、不读取文件、不新增普通用户轮次面板,真正读取仍由用户发送 `/read` 并走 `file.read` 权限流。
-- 聊天输入 `/internals` 只列出项目内部真相源读取命令,覆盖 `.agent/manifest.json`、`.agent/run.latest.json`、`.agent/spec.md`、`.agent/findings.md`、`.agent/policy.json`、`.agent/project.index.json`、`.agent/agent.db` 和 `.agent/conversations/project.jsonl`;该命令不调用 Tauri 读写、不读取文件、不新增普通用户内部文件面板,真正读取仍由用户发送 `/read` 并走 `file.read` 权限流。
-- 聊天输入 `/publish` 只使用主窗口当前已加载的 manifest、最近 run trace、预览状态、资产来源和最近命令摘要,在聊天里生成发布准备清单,列出原型通过状态、预览、任务、资产、音频、包装说明和试玩包导出状态,并提供 `/run`、`/trace`、`/agent-resume ` 或 `/export` 草稿;该命令不调用 Tauri 读写、不启动或打开预览、不读取文件、不新增普通用户面板,真正导出仍由用户发送 `/export` 并走确认流。
 - 开发窗口可从 Agent 状态列表进入单个专业 Agent 对话并管理其 Session;正式用户项目开发页只读展示底部专业 Agent 状态和悬浮详情,不提供单 Agent 对话入口、Session 控件或调度工具台。
 - v1 普通用户登录后直接进入单窗口客户端首页;同一窗口中切换首页、项目组、指南 / 反馈和项目开发页。项目组页以紧凑表格管理最近项目,顶部提供搜索、打开项目、新建项目,显示目录和移除最近记录进入行尾菜单;打开项目只切换到项目开发页,不调用 `open_game_creator_workspace_window` 打开第二窗口。旧 Tauri 窗口 command 只保留兼容,不进入用户主流程。
 - 2026-07-16 补充,2026-07-17 扩展:普通用户窗口顶部账户资产统一复用 `PlatformMudPointWalletEntry`;首屏余额来自 `/api/profile/dashboard`,展开时按需读取 `/api/profile/recharge-center` 中的泥点拆分。顶部资产“充值”与侧栏账户菜单“充值泥点”共用 `PlatformProfileRechargeModal`,桌面壳固定使用 `wechat_native` 下单并在弹窗内完成扫码与到账确认,成功后同步刷新顶部余额和泥点拆分。“使用详情”统一打开 `packages/shared` 中的 `PlatformProfileWalletLedgerModal`,由各宿主分别维护打开、加载、失败重试状态并读取 `/api/profile/wallet-ledger`,共享组件只承接账单来源文案、金额与日期展示及 loading / empty / error 视图,不发请求、不持有账户事实。AI 游戏创作壳的 Tailwind 入口必须显式扫描 `packages/shared/src/components`,避免共享组件的 utility 样式在构建时被遗漏。
-- 主窗口可通过系统文件管理器显示当前项目目录,也可在聊天输入 `/open-project` 走同一只读打开动作;该操作只打开本地目录,不初始化项目、不写项目文件、不切换工作区。主窗口头部显示最近 `.agent/run.latest.json` 的 run 状态摘要和当前项目预览状态,并通过“刷新状态”重新读取同一 trace,不新增状态数据库。
+- 主窗口可通过系统文件管理器显示当前项目目录;该操作只打开本地目录,不初始化项目、不写项目文件、不切换工作区。主窗口头部显示最近 `.agent/run.latest.json` 的 run 状态摘要和当前项目预览状态,不新增状态数据库。
 - 首页、项目组页和项目开发页共用单窗口壳的全局运行时配置弹窗,读写 Tauri 应用配置目录中的 `game-creator.config.json`;正式 Supervisor 项目页缺配置时只显示错误,不自动打开该弹窗。API Key 仍不进入本地项目、trace、manifest 或聊天记录。
 - 首页发送以及项目组打开 / 新建都通过绑定当前 `client` 的 Tauri 非阻塞原生目录选择器选择项目路径;用户取消目录选择时不覆盖已有输入或草稿。
 - 最近项目列表只来自本机 WebView storage,可逐行移除;该操作不触碰项目目录,也不触发窗口切换。项目组页自动只读检查最近项目路径,已初始化项目优先显示 `.agent/manifest.json` 里的项目名并保留路径副信息;如果 `.agent/run.latest.json` 可读,则把 status / stopReason 转成面向用户的状态。缺失、非目录、manifest 损坏或检查失败的条目禁用打开;目录仍存在时可从行尾菜单显示,检查中临时禁用打开 / 显示。页面不恢复清空或手动刷新按钮,本地搜索也不改项目列表。
 - 项目组页“打开”直接进入已初始化且 `.agent/manifest.json` 可读的 AI 游戏项目;发现根目录或一层子目录中的唯一 Godot 工程时允许导入并进入项目。路径不存在、不是文件夹、普通未初始化目录或 Godot 歧义时留在当前页提示,且不写 `.agent`。“新建项目”和聊天 `project.create` 新建入口会先检查目标目录,目标目录已经存在且非空时必须弹出确认提醒;用户继续后才调用 `init_local_game_project`,默认项目名取目标文件夹名,再写入最近项目并切到项目开发页;用户取消或初始化失败时不切换视图、也不写入最近项目。
-- 聊天输入 `/commands` 会只读列出 Tauri runtime 暴露的受限命令白名单,读取失败或非 Tauri 环境下回退到共享契约默认列表;该命令不执行白名单命令,也不要求先初始化项目。
-- 聊天输入 `/smoke` 会生成待确认的 `command.run_limited` 内置命令,当前只映射到白名单 `game.static_smoke`,不开放任意命令解析。
-- 聊天输入 `/run` 会生成待确认的 `game.run_local` 内置命令,确认后复用白名单 `game.static_smoke` 运行当前 `game/index.html`,通过后启动只读本地 HTTP 预览并切换到客户端内运行视图;该命令不开放任意 shell。
-- 聊天输入 `/export` 会生成待确认的 `project.export_package` 内置命令,确认后只把 `game/**`、`assets/**` 和 `exports/README.md` 打包到 `exports/playtest-package-*.zip`;导出前重新校验 `game/index.html` 是可试玩自包含 HTML,拒绝符号链接和越界路径,不把 `.agent/`、`memory/`、日志、trace、运行时配置或密钥文件写入 ZIP。
-- 聊天输入 `/exports` 会只读执行 `project.export_list`,列出当前项目 `exports/playtest-package-*.zip` 历史试玩包,并提供显示目录或继续 `/export` 的草稿;该命令不删除文件、不分享文件、不新增面板。
-- 聊天输入 `/preview` 会生成待确认的 `preview.start` 内置命令,确认后启动只读本地 HTTP 预览并切换到客户端内运行视图;`/open-preview` 在本地项目已初始化后生成待确认的 `preview.open`,只激活当前授权项目对应的 `127.0.0.1` 运行容器;`/preview-status` 只查询当前授权项目的本地 HTTP 预览并写入 `preview.status` 命令日志;`/preview-stop` 只停止当前项目预览,不展示或停止其它项目遗留的全局预览。
-- 聊天输入 `/memory [short|long|blackboard]` 读取短期、长期或黑板记忆;`/remember [short|long|blackboard] 内容` 生成待确认的 `memory.write` 并追加短期、长期或黑板记忆,未写 scope 时默认追加长期记忆;主窗口“记到黑板”“覆盖黑板”“清空黑板”只填入 `/remember blackboard `、`/memory-set blackboard ` 或 `/forget-memory blackboard` 草稿,仍由用户补内容并走聊天确认;`/memory-set [short|long|blackboard] 内容` 生成待确认的 `memory.write` 并覆盖保存对应记忆;`/forget-memory [short|long|blackboard]` 生成待确认的 `memory.delete`。
-- 聊天输入 `/canvas 画板项目ID` 会生成待确认的 `canvas.project_open`,只打开本机 Genarrative 编辑器里的指定画板项目,不开放任意 URL;确认后聊天先反馈正在打开,再回写真实打开 URL。画板项目 ID 为空或包含控制字符时在聊天侧直接拒绝。
-- 聊天输入 `/sync-canvas-project 画板项目ID` 会生成待确认的 `canvas.project_sync`,通过 External Editor API 把该画板项目资源下载到 `assets/canvas-sync/` 并登记为画板来源资产;画板项目 ID 为空或包含控制字符时在聊天侧直接拒绝。
-- 聊天输入 `/generate-art 提示词` 会生成待确认的 `canvas.asset_generate`,通过 External Editor API 生成首版美术素材并写入 `assets/canvas-generated/`;提示词为空时在聊天侧直接拒绝。
-- 主窗口“打开画板”“同步画板”“生成美术”和“导入画板包”按钮只填入对应聊天命令草稿并聚焦输入框,不绕过聊天侧确认流;其中“打开画板”只生成 `/canvas ` 草稿,不要求本地项目已初始化;“导入画板包”通过 Tauri 原生文件选择器选择本地 ZIP 后填入 `/import-canvas-export /绝对路径 `,仍需用户补画板项目 ID 并确认后才导入。
-- 聊天输入 `/import-canvas-asset 本地路径 画板项目ID 资源ID|object:资产对象ID [kind] [mediaType]` 会生成待确认的 `canvas.asset_import`,只把项目目录内已有文件登记为画板来源资产;主窗口画板音频快捷入口只预填 `assets/audio/sfx.wav`,由用户补齐画板项目和资源 ID 后再走同一确认流。画板只有 `assetObjectId` 时使用 `object:` 前缀,不伪造 resourceId。画板项目 ID 为空或包含控制字符时在聊天侧直接拒绝。
-- 聊天输入 `/import-canvas-export /绝对/画板素材.zip 画板项目ID` 会生成待确认的 `canvas.export_import`,把画板导出的素材 ZIP 解包到 `assets/canvas-imports/` 并登记为画板来源资产。画板项目 ID 为空或包含控制字符时在聊天侧直接拒绝。
 - 开发窗口里的 `confirm` 权限命令使用原生确认门,取消时只写命令日志,不执行本地写入、删除、预览或画板打开操作。
 - 原生文件上传会写入 `assets/uploads/`,并追加登记到 `.agent/manifest.json`。
 - 开发模式可把项目内已有文件登记为资产;`asset.register` 支持 `uploaded`、`generated`、`canvas` 三种来源,并可记录画板 project/resource/object 元数据。
@@ -1337,7 +1147,7 @@ game-project/
 - `check:native-shells` 会运行 `ai-game-creator-shell:check` 和 `ai-game-creator-shell:build -- --no-bundle`,并静态检查 release 与 debug 启动都只登记 `client / index.html` 这一个默认窗口、禁止 Tauri setup 自动打开 developer 窗口、开发面板必须挂在 `devMode` 分支内,正式用户 App 的运行容器只接受 `http://127.0.0.1:*`,release / dev CSP 都只为该 loopback origin 开放 `frame-src`,Tauri 预览激活命令不得调用 opener,用户主流程不得调用旧工作区窗口切换 command。
 - 共享契约提供 `GAME_CREATION_AGENT_CAPABILITIES` 和内置命令权限枚举;开发模式会展示能力列表。
 - 共享契约提供 manifest task schema 和 ready-task 选择器,用于记录任务拆分、专业组、角色模板、依赖、产物、验收条件和当前可执行任务。
-- 开发模式可读取、保存、删除短期记忆和长期记忆文件;普通用户通过聊天命令完成同类能力。
+- 开发模式可读取、保存、删除短期记忆和长期记忆文件;正式用户界面不提供记忆管理入口,短期 / 长期 / 黑板记忆只由 `project-supervisor` 运行期的记忆工具在授权项目内读写。
 - 共享契约提供 `GAME_CREATION_APP_LIMITED_RUN_COMMANDS`;当前真实命令为 `game.static_smoke`,用于检查 `game/index.html` 的可玩原型门槛并写入 `.agent/logs/command.log`。
 - 后台 Agent 的项目 revision 以 `.agent/runtime/project-revision.json` 为唯一事实源,per-run 验证门禁以 `.agent/runtime/verification//.json` 为事实源。每次 `file.write`、`file.patch`、`file.delete` 或 `project.restore` 都必须在实际修改前保守推进 revision,并永久记住当前 run 的 `requiresVerification=true`;失败或崩溃不回退。只有成功且绑定当前 revision 的 `project.verify` 或 `command.run_limited / game.static_smoke` 才能放行空 actions;未修改项目的只读任务不强制验证,但最终回复仍必须绑定请求开始时的 `responseRevision`。per-run context bundle 使用 v2,pending action 使用 v3 并绑定创建时的全局 revision;旧版恢复失败关闭。最终 assistant 和 completed 必须在项目写锁内重读 revision / gate 后依次落盘,文件回读、observation 或锁外旧快照都不能替代验证凭证。验收必须分别模拟待执行动作、修改 run 与只读 run 的跨 Agent revision 漂移,证明旧动作不执行、旧回复不落盘、不产生 completed 或 failed、per-Agent 锁不提前释放、原 run/session 在收到 blocker 后保持可恢复;stale continuation 经重启仍从原 `nextLoopIndex` 续跑,revision 数值或成功验证输出中的动态时间戳不能绕过 context stall。
 - `.agent/manifest.json` 会记录当前 `preview` 状态和 `commandRuns` 受限命令运行结果,作为本地产物索引的最小真相源。
@@ -1345,11 +1155,11 @@ game-project/
 - 2026-07-15 真实 `gpt-5.5` `response-stream` 专项已 PASS:39 个不同非空快照在终态前可见,sequence 为 `1 -> 418 -> 425 committed`,最终 883 字与唯一 conversation assistant 精确一致;final-reply lifecycle 唯一、fallback replay 和重复消息/回执为 0。公共正文、API Key、thinking、诱饵和项目绝对路径泄漏均为 0;`project.verify` Agent DB 审计固定保存 `.agent/logs/command.log` 相对路径,并在写入前脱敏 expectedCommand/output 中的项目根路径。
 - 2026-07-15 起,同一 Runtime 文档的“V1.20 单 Agent Provider 原生联网检索”作为联网能力事实源。配置新增默认关闭的 `llm.webSearchEnabled` 与可继承的 `agentLlm..webSearchEnabled`;只允许普通/角色直聊和后台首个 tool planning 开启,格式 repair 与 final reply 固定关闭。Anthropic 组合在保存和状态检查阶段失败;网页内容按不可信输入处理,不能改变 Goal、权限、确认、沙箱或工具协议,也不得把密钥、源码、路径、私有对话、记忆或黑板作为搜索词。后台 Provider lifecycle v2 只审计实际布尔值,不保存 query、网页 URL、结果正文或网页指令;当前布尔契约不宣称支持 Codex 的 indexed/live 模式。
 - 2026-07-15 当前正式 `openai_chat / gpt-5.5` 路由的三轮真实联网专项均 FAIL:上游接受搜索开启请求并完成 lifecycle,但模型没有获得原生搜索能力,无法命中动态 GitHub release baseline。客户端能力已落地但该路由不可启用;最终复验使用正式 AppData 同级的 `0600` 私有配置副本,源配置 inode/nlink/timestamps/hash 前后完全一致,隔离 Runner/AppData/项目和全部泄漏门禁均安全收束。
-- 2026-07-15 起,同一 Runtime 文档的“V1.21 单 Agent token-aware 持久上下文压缩”作为长会话预算事实源。全局/per-Agent LLM 配置提供 context window、自动压缩阈值和工具输出 token 限额;后台 planning 超阈值时只压缩旧 conversation/observation prefix,Goal、任务、计划、steer、pending、verification 与副作用身份逐字段保留。开发 Agent 窗口与 `agc:chat` 提供同一安全 `/compact`,正式用户首页不新增控制项。私有 sidecar、context bundle 绑定、Provider orphan barrier、公共零正文和 30 轮真实长链路按 Runtime V1.1 的 V1.21 章节验收。
+- 2026-07-15 起,同一 Runtime 文档的“V1.21 单 Agent token-aware 持久上下文压缩”作为长会话预算事实源。全局/per-Agent LLM 配置提供 context window、自动压缩阈值和工具输出 token 限额;后台 planning 超阈值时只压缩旧 conversation/observation prefix,Goal、任务、计划、steer、pending、verification 与副作用身份逐字段保留。开发 Agent 面板展示压缩状态,显式手动压缩入口为 `--agent-context-compact`,正式用户首页不新增控制项。私有 sidecar、context bundle 绑定、Provider orphan barrier、公共零正文和 30 轮真实长链路按 Runtime V1.1 的 V1.21 章节验收。
 - 2026-07-15 V1.21 已落地并完成真实验收:`context-compaction` suite 在正式 `openai_chat / gpt-5.5` 路由上完成 30/30 轮、两次压缩 revision、一次 Runner pidfd 强杀恢复和早期显式约束召回;最大估算输入 29134/64000,32 组 Provider lifecycle 唯一闭合,重复 assistant/audit、工具重放以及公共正文、summary、API Key、诱饵、项目路径和正式配置路径泄漏均为 0。首轮第 22 轮 Provider transport 失败按单次请求终态停止且零重放,新 disposable 项目完整重跑后 PASS。
 - 2026-07-15 起,同一 Runtime 文档的“V1.22 Runner-owned MCP 动态工具”作为外部工具扩展事实源。AppData 配置管理 STDIO / Streamable HTTP server、Bearer/static header、工具 allow/deny 与 `auto / confirm / writes / deny` 审批;独立 Runner 持有连接并把过滤后的真实 tool schema 和 server instructions 送入 planning。模型通过现有 `submit_agent_tool_plan` 请求 `mcp.call`,调用继续复用 durable pending action、确认、steer、Goal、reconciliation 和 V1.21 token 预算;完整结果只落私有 sidecar,正式用户首页不新增 MCP 调试配置。可选 server 的 tools/list、schema 归一化、重复 tool identity、单 server 或聚合目录容量、未支持 task-mode 错误只隔离该 server,目录状态记录 `connected=false + error` 且不暴露其工具;required server 对相同错误继续失败关闭。MCP input schema 包入原生 action 的 `input` 属性时,只把当前 schema document 根的 `#` 与 `#/...` JSON Pointer 重定位到 `#/properties/input...`;命名 anchor、外部引用和带 `$id` 的独立 schema resource 内 fragment 保持不变。本切片不宣称 OAuth、resources/prompts、sampling、elicitation 或 MCP task-mode 已实现。
-- 2026-07-15 V1.22 已落地并完成真实验收:开发配置窗可管理 server、敏感凭据、工具过滤和审批并通过 Runner 查看有界目录,`agc:chat` / `agc:swarm` 可用 `/mcp` 查询状态。正式 `openai_chat / gpt-5.5` 路由真实调用 STDIO/Streamable HTTP lookup 和确认后的 mutate,正常 run 的 action/sidecar/receipt 各 3 且最终 assistant 唯一;第二 run 在 HTTP mutate 副作用后强杀 Runner,只进入 1 次 reconciliation,调用、sidecar、receipt 和 assistant 均未重放。公共 arguments、结果正文、instructions、凭据和项目/配置路径泄漏为 0,一次性现场已清理。
-- 2026-07-16 起,同一 Runtime 文档的“V1.23 单 Agent 持久用户输入请求”作为 Needs input 事实源。Agent 可在计划未完成时通过 `user.input_request` 提出 1-3 个结构化问题,Runtime 保持同一 run 并暂停;Project Supervisor、开发 Agent 窗口和 `agc:chat` 从私有 sidecar 展示并提交答案。普通 steer、工具确认和最终回复不再承担问题回答语义,问题/答案正文不进入公共审计。
+- 2026-07-15 V1.22 已落地并完成真实验收:开发配置窗可管理 server、敏感凭据、工具过滤和审批并通过 Runner 查看有界目录,MCP 状态与有界工具目录由开发配置面板和真实 E2E 门禁核验。正式 `openai_chat / gpt-5.5` 路由真实调用 STDIO/Streamable HTTP lookup 和确认后的 mutate,正常 run 的 action/sidecar/receipt 各 3 且最终 assistant 唯一;第二 run 在 HTTP mutate 副作用后强杀 Runner,只进入 1 次 reconciliation,调用、sidecar、receipt 和 assistant 均未重放。公共 arguments、结果正文、instructions、凭据和项目/配置路径泄漏为 0,一次性现场已清理。
+- 2026-07-16 起,同一 Runtime 文档的“V1.23 单 Agent 持久用户输入请求”作为 Needs input 事实源。Agent 可在计划未完成时通过 `user.input_request` 提出 1-3 个结构化问题,Runtime 保持同一 run 并暂停;Project Supervisor 与开发 Agent 窗口从私有 sidecar 展示并提交答案。普通 steer、工具确认和最终回复不再承担问题回答语义,问题/答案正文不进入公共审计。
 - 2026-07-16 V1.23 已完成真实验收:正式 `openai_chat / gpt-5.5` 路由在 Project Supervisor 上产生 1 个含 2 选项的 Needs input,等待期 Runner pidfd 强杀恢复未增加 Provider 请求,回答后同 Session/run 完成唯一最终回复。问题/回答各一条,重复消息、公共正文、密钥、路径和报告泄漏均为 0,隔离现场已清理。
 - 2026-07-16 起,同一 Runtime 文档的“V1.24 Codex 式 scoped `AGENTS.md` 仓库指令”作为项目规范加载事实源。仓库启动上下文升级为 v2,根与嵌套 `AGENTS.md` 携带规范 scope 并按根到叶适用,更深规则只覆盖自身目录树,兄弟 scope 不串用;README/CONTEXT 明确保持不可信参考数据。项目指令不能扩大工具、确认、沙箱、隐私或完成门禁,旧 v1 pending fingerprint 必须先形成 repository drift blocker 再重规划。
 - 2026-07-16 V1.24 已完成真实验收:正式 `openai_chat / gpt-5.5` 的 `scoped-agents` suite 在无规则正文、期望内容和工具配方的任务下,让同一 Agent 只修改 `alpha / beta` 两个兄弟目录交付文件;根、父、各自叶规则全部精确命中且兄弟串用为 0,Agent `project.verify` 与宿主复验均通过。最终脚本复跑的 8 组 Provider lifecycle 唯一闭合,最终 assistant/completed 各 1,重复持久化,以及最终回复/公共审计/报告中的规则正文、API Key、诱饵、项目/配置路径泄漏均为 0,隔离现场完整清理;不再把 prompt 可见性代替模型遵循证据。
@@ -1367,10 +1177,10 @@ game-project/
 - Supervisor 认领原 delivery 后可为 needs-repair 或语义未通过创建一个 `repairOfDelegationId=<原 delegationId>` 的新委派。repair 必须完整继承原合同并交回原专业 Agent,只能留在同一父 run、深度为 1、同一原 delivery 同时最多一个非 suppressed 投递;相同重放幂等复用,不同重复/并发请求拒绝。`suppressed` repair 继续阻断,同一 action 可原地恢复;该 action 已持久失败时,新 action 只可在全部既有 repair 均 suppressed 时重做基础设施投递。所有必要 delivery/claim/repair、结构化计划、verification、确认、用户输入和其它既有 blocker 清零后,原 Supervisor run 才能写唯一用户回复。
 - V1.28 对 Provider 瞬态失败采用 Runtime 显式重试:`agentLlm..maxRetries / retryBackoffMs` 表示独立物理尝试及其有界指数退避,不得恢复为 `LlmClient` 在单 lifecycle 内隐式重放。每次尝试都重建禁用自动重试的 client,并写独立单次 lifecycle;首次 request slot 不变,第 `N` 次重试稳定使用 `-transient-N` 后缀。只有 `timeout / connectivity / transport` 可重试;工具协议无效仍进入独立 `repair-N` 格式修复,其他错误与重试耗尽按原失败路径收束。重试前必须重新检查 Goal、steer、cancel、task/run 与 orphan 门禁;控制请求可阻止下一次尝试,Runner 强杀后无可信终态的 `started` 仍进入 reconciliation,不能自动补发。重试发生在解析和副作用之前,不创建 action、pending、receipt、delivery、assistant 或 revision;既有 Runner、orphan、finalization 和隐私边界均不放宽,公共审计不得保存请求/响应正文、arguments、凭据或绝对路径。
 - V1.28 已于 2026-07-17 完成正式 `openai_chat / gpt-5.5` `supervisor-swarm` PASS:同一 native 批次双专业委派、真实 Provider 重叠、2 份初始 delivery、1 次 targeted contract read、1 份唯一 repair、pidfd Runner 强杀/boot 恢复、同一父 Session/run、唯一 Supervisor assistant 和 3 条内部专业 assistant 全部成立。报告包含 46/46 闭合且 completed 的 Provider lifecycle,成功计划 24/24、格式修复 20/20 全为原生工具协议;重复、残留 sidecar、Provider payload、私有正文、API Key、项目/正式配置绝对路径、报告、secret 与 lure 泄漏均为 0。正式 AppData 零 CLI 调用且源 Runner endpoint 未变化;规范复验命令为 `npm run ai-game-creator-shell:agent-runtime:real-e2e -- --config-dir  --suite supervisor-swarm`。
-- 2026-07-17 追加 `supervisor-swarm-transient-retry` 受控故障门禁:一次性本地回环代理只让 `design-director` 首个请求在正文转发前断线,并暂停后继请求,直到验收器确认唯一 failed lifecycle、唯一 retry audit、新 `-transient-1` identity,以及 action/receipt/子委派/claim/assistant/pending/revision/upstream forwarding 全为 0。E2E 启动 CLI/Runner 时会把 loopback 合并进 `NO_PROXY / no_proxy`,避免继承的系统 HTTP 代理接触故障门禁请求中的凭据和正文。最终加强版正式 `gpt-5.5` 报告为 46/46 lifecycle 闭合、45 completed/1 failed/1 retry;代理观察到的 10 个目标 Agent 请求与该 Agent lifecycle 数量一致,放行后完整双 Agent、唯一 repair、Runner 强杀恢复、唯一 Supervisor assistant、零重复/残留/泄漏继续 PASS。隔离 AppData 创建在正式目录同级,source-dir guard 与 `sourceAppDataDirectoryUntouched` 证明正式 AppData 未被写入,源配置与 endpoint 身份保持只读,失败 partial report 保留已取得的 retry checkpoint,代理与隔离现场全部清理。该 suite 只证明显式重试和既有协作链可组合,不把预置双 Agent fixture 扩大解释为自主编排;无 Agent ID/并行/repair 配方的自主 suite、真实 `agc:chat`、static+isolated 组合和 Tauri 宿主 E2E 仍待单独验收。
+- 2026-07-17 追加 `supervisor-swarm-transient-retry` 受控故障门禁:一次性本地回环代理只让 `design-director` 首个请求在正文转发前断线,并暂停后继请求,直到验收器确认唯一 failed lifecycle、唯一 retry audit、新 `-transient-1` identity,以及 action/receipt/子委派/claim/assistant/pending/revision/upstream forwarding 全为 0。E2E 启动 Runner 时会把 loopback 合并进 `NO_PROXY / no_proxy`,避免继承的系统 HTTP 代理接触故障门禁请求中的凭据和正文。最终加强版正式 `gpt-5.5` 报告为 46/46 lifecycle 闭合、45 completed/1 failed/1 retry;代理观察到的 10 个目标 Agent 请求与该 Agent lifecycle 数量一致,放行后完整双 Agent、唯一 repair、Runner 强杀恢复、唯一 Supervisor assistant、零重复/残留/泄漏继续 PASS。隔离 AppData 创建在正式目录同级,source-dir guard 与 `sourceAppDataDirectoryUntouched` 证明正式 AppData 未被写入,源配置与 endpoint 身份保持只读,失败 partial report 保留已取得的 retry checkpoint,代理与隔离现场全部清理。该 suite 只证明显式重试和既有协作链可组合,不把预置双 Agent fixture 扩大解释为自主编排;无 Agent ID/并行/repair 配方的自主 suite、static+isolated 组合和 Tauri 宿主 E2E 仍待单独验收。
 - 2026-07-19 正式客户端 `pvz` 现场出现专业 Agent 间歇性 `transport` 失败。早期私有 raw log 证明 native-tls/OpenSSL 路径会命中 TLS record bad-MAC,不是鉴权、模型不存在或 HTTP/JSON 错误;同配置的 Provider 模型查询与短/长认证请求均成功。现场同时发现 release GUI 仍复用前一晚的 debug Runner,原因是旧 endpoint 只校验协议和 ping。修复后 endpoint 增加当前 executable 内容 SHA-256,协议与构建身份都一致才可复用;身份缺失/不一致只允许 idle 安全退役,busy 不强杀。新配置默认 `maxRetries=2 / retryBackoffMs=500`,正式现场同步改为该值,使 `timeout / connectivity / transport` 继续按 V1.28 独立 lifecycle 有界重试;显式保存为 `0` 仍表示禁用重试。native-tls 即使执行两次退避重试仍可连续三次命中 bad-MAC,因此 LLM 专用 `reqwest` 固定为 Rustls,MCP 等其他 HTTP 客户端不变。该切换消除了已观测的旧 OpenSSL bad-MAC 路径,但后续 Rustls 现场 raw log 仍出现 `connection error: cannot decrypt peer's message`,程序 Agent 在三次尝试后失败;因此 TLS/transport 根因尚未彻底关闭。旧 run 因在途 Provider 请求安全进入 `needs-reconciliation` 后被显式取消,并以新 runId 在同源 Rustls Runner 重试;工作台的同项目重试只是用户恢复入口,不是 TLS 根因修复。
-- 2026-07-17 V1.30 `supervisor-swarm-autonomous-chat` 最终真实 PASS:唯一业务任务和仓库规则均不包含编排配方;Supervisor 在 1 个 native 批次自主选择两个不同专业 Agent,真实 Provider 重叠后形成 2 个初始 delivery,并基于 acceptance criteria 自主创建 1 个继承原合同的 repair。最终报告包含 110 条 task、197 条 event、330 条 Agent DB、9 条会话消息和 `51 started / 51 terminal / 51 completed / 0 failed` Provider lifecycle;28/28 成功计划与 19/19 格式修复均为原生工具协议,父计划 4/4 completed,Runner pidfd 强杀恢复后身份稳定,`turn.report` settled、正式 assistant 1、内部专业 assistant 3,重复、sidecar、正文、密钥、诱饵、项目/配置路径和报告泄漏均为 0。两次较早的独立尝试在 `maxRetries=0` 下各遇到 1 次外部 Provider 终态失败并中止,未与最终 PASS 拼接。
-- 2026-07-17 Runtime V1.31 的同父 run 混合协作门禁已完成独立真实 PASS;详细业务任务边界、首批 confirmation gate、static/isolated durable 合同、三条确定性回归、真实报告数字和失败轮隔离记录统一以同一 Runtime 文档的“V1.31 Project Supervisor 静态与隔离子 Agent 混合协作门禁”为事实源。App 侧复验入口为 `npm run agc:mixed-swarm-e2e -- --config-dir `,不在实施计划重复维护一次性拓扑和计数。
+- 2026-07-17 V1.30 自主专业编排最终真实 PASS:唯一业务任务和仓库规则均不包含编排配方;Supervisor 在 1 个 native 批次自主选择两个不同专业 Agent,真实 Provider 重叠后形成 2 个初始 delivery,并基于 acceptance criteria 自主创建 1 个继承原合同的 repair。最终报告包含 110 条 task、197 条 event、330 条 Agent DB、9 条会话消息和 `51 started / 51 terminal / 51 completed / 0 failed` Provider lifecycle;28/28 成功计划与 19/19 格式修复均为原生工具协议,父计划 4/4 completed,Runner pidfd 强杀恢复后身份稳定,正式 assistant 1、内部专业 assistant 3,重复、sidecar、正文、密钥、诱饵、项目/配置路径和报告泄漏均为 0。两次较早的独立尝试在 `maxRetries=0` 下各遇到 1 次外部 Provider 终态失败并中止,未与最终 PASS 拼接。
+- 2026-07-17 Runtime V1.31 的同父 run 混合协作门禁已完成独立真实 PASS;详细业务任务边界、首批 confirmation gate、static/isolated durable 合同、三条确定性回归、真实报告数字和失败轮隔离记录统一以同一 Runtime 文档的“V1.31 Project Supervisor 静态与隔离子 Agent 混合协作门禁”为事实源,不在实施计划重复维护一次性拓扑和计数。
 - `agent.message` 使用来源 Agent/run、目标 Agent/Session 和清洗后正文 SHA-256 形成稳定语义身份。同一语义消息只允许写 1 条目标 tool conversation、1 条 `conversation.message` 和 1 条 `agent.runtime.agent.message`;后续 Runtime action 仍完整落账,但返回 `messageAppended=false` 且不算新的 loop 进展。专业 Agent 不得用重复消息替代最终回执;持续重复时最多经过当前 6 轮停滞窗口即以 `loop-budget-exhausted` 失败,保留 `in_progress` 计划且不写 completed。完整后台回归同时断言 6 个 actionId、同一 action fingerprint、6 组 action/observation/receipt、消息持久化唯一、receipt 零正文、第 7 次 Provider 请求为 0。
 - 开发模式可通过本地项目文件面板执行 `file.list/read/write/delete`,普通用户界面不暴露文件面板。
 - 2026-07-17 起,同一 Runtime 文档的“V1.32 Runtime 强制 Supervisor 协作合同”作为 mixed swarm 可靠性事实源。项目可用 `.agent/collaboration-policy.json` 约束首波 static/isolated 模式、数量和 required static Agent;Runtime 在任何 child 副作用前整批校验并把合同指纹固化进 Provider batch v2。当前父 run 一旦形成 delivery/group,正式 `project-supervisor` 默认只负责编排、状态认领和验证,不再直接执行项目 mutation;专业 Agent/isolated child 权限与唯一 Supervisor 最终回复边界保持不变。
@@ -1401,7 +1211,7 @@ game-project/
 - 第六轮在同一轮内完整 **PASS**:`gpt-5.5 / openai_chat` 路由形成 `2` 条初始加 `1` 条 repair delivery、`3` 条专业 Agent assistant;故障精确命中 Project Supervisor base final-reply,可信宿主 verify marker 门禁通过。受控 Provider `failed=1 / retry=1`、incidental `failure=0 / retry=0`,`30s` backoff,pidfd `claim=2 / signal=2`,Runner `resumed=true / identityStable=true`;父 tool-plan 在故障前后均为 `13`,parent final-reply 与最终 assistant 唯一,response stream `sequence=2 / committed`。pending、retry、handoff、finalization、confirmation sidecar、全部重复计数,以及 API Key、私有正文、项目路径、正式配置路径和公共报告泄漏扫描命中均为 `0`。
 - 第六轮 PASS 不改变 V1.41 handoff 原子落盘并回读前的 unknown-result 边界,tool-plan 成功响应/function arguments 的 durable handoff 仍未覆盖。
 - 2026-07-20 起,同一 Runtime 文档的“V1.43 tool-plan 成功响应持久交接与 repair 链恢复”作为规划成功响应的现行恢复契约。V1.41 文本 handoff 保持不变;新增独立 `game-creator-tool-plan-handoff.v1` 私有账本,按同一 Agent/run 的 loop/repair 顺序保存实际 Provider requestId、retry identity、去 thinking 的响应、完整 function call envelope/arguments、usage 与响应指纹。`repair-0` 和全部 `repair-N` 统一进入持久 retry/handoff-first 路径,Runner 可从 base 开始零网络重放既有 repair 链。
-- tool-plan arguments 只允许出现在 `0600` 原子 sidecar 及后续 pending/action batch,不得进入 task/event/Agent DB/CLI/report。公共 protocol/repair 审计共同保存 Agent/task/Session/run/source、loop/repair/slot、响应指纹、Provider request ID SHA-256 和 protocol;protocol 只保存 function call 数量、call ID SHA-256 数组、catalog-bound function names、response ID SHA-256/字符数及 normalization 元数据,repair 只保存 attempt/maxAttempts、协议错误/preview 哈希和 call ID/function name SHA-256,不保存原始 callId/callIds/responseId/providerRequestId,并在 Agent DB append 锁内按完整身份全历史幂等追加。为了保持执行语义,参数禁止静默脱敏;命中密钥、配置痕迹、敏感 JSON key、Provider ID 中的秘密/绝对路径、结构化可执行路径中的项目或其它绝对路径、大小/顺序/身份冲突时直接 reconciliation。源码正文和计划叙述只做密钥检查,不能把 HTML 闭合标签当绝对路径;未闭合 thinking 只留无正文无效元数据并继续 repair。账本保留到 run 终态或明确作废;steer/cancel/漂移/终态清理前先闭合整本账本的实际 requestId,Runner 恢复严格扫描 hash/primary/`.previous`/安全临时文件并回收合法终态残留,确保单动作、多动作、confirmation、协作 batch 与直接回复在下一 durable owner 建立前都有恢复来源;未知、冲突、primary、`.previous` 或损坏账本都阻止 Runner idle shutdown。
+- tool-plan arguments 只允许出现在 `0600` 原子 sidecar 及后续 pending/action batch,不得进入 task/event/Agent DB/report。公共 protocol/repair 审计共同保存 Agent/task/Session/run/source、loop/repair/slot、响应指纹、Provider request ID SHA-256 和 protocol;protocol 只保存 function call 数量、call ID SHA-256 数组、catalog-bound function names、response ID SHA-256/字符数及 normalization 元数据,repair 只保存 attempt/maxAttempts、协议错误/preview 哈希和 call ID/function name SHA-256,不保存原始 callId/callIds/responseId/providerRequestId,并在 Agent DB append 锁内按完整身份全历史幂等追加。为了保持执行语义,参数禁止静默脱敏;命中密钥、配置痕迹、敏感 JSON key、Provider ID 中的秘密/绝对路径、结构化可执行路径中的项目或其它绝对路径、大小/顺序/身份冲突时直接 reconciliation。源码正文和计划叙述只做密钥检查,不能把 HTML 闭合标签当绝对路径;未闭合 thinking 只留无正文无效元数据并继续 repair。账本保留到 run 终态或明确作废;steer/cancel/漂移/终态清理前先闭合整本账本的实际 requestId,Runner 恢复严格扫描 hash/primary/`.previous`/安全临时文件并回收合法终态残留,确保单动作、多动作、confirmation、协作 batch 与直接回复在下一 durable owner 建立前都有恢复来源;未知、冲突、primary、`.previous` 或损坏账本都阻止 Runner idle shutdown。
 - V1.43 的确定性门禁必须覆盖 base handoff 与 repair handoff 两个 lifecycle-completed 前断点,关闭 mock Provider 后恢复零网络、原 requestId 唯一闭合、repair/protocol audit 幂等、唯一 assistant/completed/committed stream及终局零 sidecar。独立非默认真实门禁 `supervisor-swarm-tool-plan-handoff-runner-kill` 已实现并完成 Shell/Root 两级注册:它使用 sentinel-owned sibling AppData 与 metadata-only zero-fault proxy,以每轮随机 capability 严格绑定 project/Agent/run/实际 request slot;只有 tool-plan handoff 原子落盘并回读一致、同一实际 requestId lifecycle 尚未 `completed` 时才 ACK,随后通过 pidfd `SIGKILL` 强杀 suite 自有 Runner。恢复必须证明同一 requestId 唯一闭合且 `networkReplayCount=0`、protocol/repair audit 幂等、handoff 与 durable batch plan fingerprint 对应、恢复消费前 action/pending/delivery/claim 等副作用为 `0`,并在终局把 sidecar、重复记录、临时 capability/Runner/AppData 资源及公共正文、凭据、URL、项目/正式配置路径泄漏全部清零。2026-07-20 的真实外部 Provider 单轮已到达并通过 checkpoint,但随后专业 Agent 连续连接失败使整轮 FAIL;另一独立轮首批工具数不满足 fixture,也未通过。两轮不得拼接,当前仍无该 suite 的完整外部 PASS。Provider 成功到 handoff 原子落盘回读前的 unknown-result 和手动 context-compaction 仍不在本切片承诺内。
 - V1.43 当前确定性实现已通过本轮 `tool_plan_handoff_ 44/44`、Supervisor collaboration 相关过滤 `55/55`、权威返工合同 `1/1`,以及 Tauri/Rust 串行全量 `1054 passed / 4 ignored / 0 failed`;Linux `cargo check --tests` 与 `x86_64-pc-windows-gnu cargo check --tests` 均通过。E2E self-test、typecheck、变更脚本 ESLint、encoding 与 `git diff --check` 通过;默认并发全量只作竞态诊断,不替代串行门禁。Supervisor 真实 E2E 报告已把 `toolPlanHandoffSidecarCount` 纳入终局残留。handoff 跨平台存储使用 Unix 固定目录句柄、目录 `flock`、exchange/quarantine 与 Windows 相对父句柄、句柄枚举、独占 temp,不再根据 PID 推断写入方是否存活;主动忽略锁的同 UID 进程仍属于宿主 OS 信任边界。
 - 2026-07-27 文档更正:上文 V1.42 的 `platform-llm 41/41` 保留为 2026-07-20 历史门禁计数;当前验收命令为 `cargo test --manifest-path server-rs/Cargo.toml -p platform-llm`。platform-llm 的验收证据分为 checked-in SSE fixture parser 覆盖、本地解析单元测试与默认 `#[ignore]` 的真实端点归一工具调用 smoke;后者只校验最终工具名、id、完整参数 JSON 和文本字符数,未录制或逐事件比较原始 SSE,二者都不能证明转录无偏差。新增普通测试不需要更新固定数量,只有验收命令或测试类别边界变化时才需要更新本段。
@@ -1415,21 +1225,21 @@ game-project/
 - 第二轮拆分继续保留稳定 facade。仅供同一父模块下兄弟子模块调用的 helper 使用 `pub(super)`;访问被 `runtime_protocol::provider_retry` 遮蔽的 crate 根同名模块时显式使用 `crate::provider_retry`。原有兼容重导出不能按 `unused_imports` 告警机械删除,确认是旧调用面后只添加局部 `#[allow(unused_imports)]`。
 - 结构拆分的稳定树门禁为 Rust 串行全量 `1139 passed / 5 ignored / 0 failed`、客户端前端 `329/329`、typecheck、客户端 crate fmt、Prettier、ESLint、encoding,以及确定性 E2E self-test 和完整 E2E 均 PASS。完整 E2E 的项目 revision 为 `0 -> 2`,真实浏览器固定试玩 `37/37`,重复、残留和泄漏均为 `0`。并行写入期间发生在 CLI 构建阶段、尚未创建 run 的 exit `101` 不计作 Runtime E2E 结果。
 - 2026-07-22 的第三轮由四个 Agent 继续按稳定 facade 并行拆分:`tool_plan_handoff.rs` 从 `5614` 行降到 `24` 行并拆为 `10` 个子模块,最大生产模块为 Unix `1219` 行、Windows `1040` 行,测试模块为 `1697` 行;平台存储模块各自保留完整原子提交链,不再按行数机械切分。`agent/generation.rs` 从 `4566` 行降到 `92` 行并拆为 `10` 个子模块,最大生产模块 `trace.rs` 为 `834` 行,原 `123` 个 `pub(crate)` API 由 facade 显式重导出,仅 `4` 个跨 `crate::agent` helper 最小化为 `pub(in crate::agent)`。
-- 同轮 `swarm_cli.rs` 从 `4420` 行降到 `68` 行并拆为 `9` 个子模块,最大生产模块 `observer.rs` 为 `843` 行、测试模块为 `1529` 行;原 `35` 个测试名与 `turn.report` 字段/顺序不变。`browser.rs` 从 `4036` 行降到 `24` 行并拆为 `11` 个子模块,最大生产模块 `capture.rs` 为 `733` 行、`playtest/mod.rs` 为 `612` 行,测试模块为 `1169` 行;内嵌 raw JavaScript 的搬迁前后哈希一致。
+- 同轮 `browser.rs` 从 `4036` 行降到 `24` 行并拆为 `11` 个子模块,最大生产模块 `capture.rs` 为 `733` 行、`playtest/mod.rs` 为 `612` 行,测试模块为 `1169` 行;内嵌 raw JavaScript 的搬迁前后哈希一致。
 - 第三轮集成修复仅补 `tool_plan_handoff` 下沉测试缺失的 `response_fingerprint / validate_ledger / AsRawFd` import,并为兼容重导出添加局部 `#[allow(unused_imports)]`;兼容出口未删除,警告总数仍为 `18`。测试子模块不继承父模块 `use`,后续拆分必须显式补 import;并行写入期禁止全 crate 编译,统一门禁只能在四个 Agent 全部完成后的稳定共享树运行。
-- 第三轮稳定树已通过 `cargo fmt --check`、`cargo check`、`cargo check --tests`,以及 `tool_plan_handoff 44/44`、`swarm_cli 35/35`、`browser 21 passed / 3 real Chrome ignored`;Linux 串行全量为 `1146 passed / 5 ignored / 0 failed`。确定性真实 Runner + Chrome E2E 为 **PASS**,Provider lifecycle `17/17`、revision `0 -> 2`、固定试玩 `37/37`,残留与泄漏均为 `0`。Windows cross check 因宿主缺少 `x86_64-w64-mingw32-gcc`,在进入项目代码前停止,仍是明确的残余验证缺口。
+- 第三轮稳定树已通过 `cargo fmt --check`、`cargo check`、`cargo check --tests`,以及 `tool_plan_handoff 44/44`、`browser 21 passed / 3 real Chrome ignored`;Linux 串行全量为 `1146 passed / 5 ignored / 0 failed`。确定性真实 Runner + Chrome E2E 为 **PASS**,Provider lifecycle `17/17`、revision `0 -> 2`、固定试玩 `37/37`,残留与泄漏均为 `0`。Windows cross check 因宿主缺少 `x86_64-w64-mingw32-gcc`,在进入项目代码前停止,仍是明确的残余验证缺口。
 - 2026-07-22 的 V1.45 修复专业 Agent 并行修改导致的 stale pending 中断。执行前发现 project revision 漂移不再进入 `needs-reconciliation`,而是以 `blocked + projectRevisionDrift + replanRequired` observation 回到同一 run 继续 planning;`file.write / file.patch` 还会在项目写锁内复核 pending 身份、仓库上下文、revision 与 verification gate。该语义不放宽并发写安全:旧动作始终不执行,副作用未知、账本冲突或持久化损坏仍失败关闭。
-- V1.45 的独立真实 external-provider 验收只向 `--swarm-chat --init --autonomous-game-build` 写入一次“植物大战僵尸式塔防”需求后立即 EOF。最终 `status=PASS / turn.report=settled`,approve / answer / steer 为 `0`;两条初始 delivery 与一条 repair delivery 均被父 run 认领,项目 revision `0 -> 8`,`game/index.html` 为 `7814` bytes,static smoke、desktop / mobile 浏览器和 `lane-defense-v1 37/37` 全通过。唯一 Supervisor assistant 已提交,pending、confirmation、user-input、provider retry/handoff、tool-plan handoff、finalization、reconciliation、重复和全部隐私泄漏计数均为 `0`。
+- V1.45 的独立真实 external-provider 验收只写入一次“植物大战僵尸式塔防”需求后立即结束输入。最终 `status=PASS`,approve / answer / steer 为 `0`;两条初始 delivery 与一条 repair delivery 均被父 run 认领,项目 revision `0 -> 8`,`game/index.html` 为 `7814` bytes,static smoke、desktop / mobile 浏览器和 `lane-defense-v1 37/37` 全通过。唯一 Supervisor assistant 已提交,pending、confirmation、user-input、provider retry/handoff、tool-plan handoff、finalization、reconciliation、重复和全部隐私泄漏计数均为 `0`。
 - V1.45 稳定树同时通过三个新增/更新定向回归、`revision 32/32`、`cargo check --tests`、Linux 串行全量 `1147 passed / 5 ignored / 0 failed`、`cargo fmt --check`、encoding 与 `git diff --check`。确定性正式 E2E 继续为 **PASS**:Provider lifecycle `17/17`、revision `0 -> 2`、Chrome `37/37`,终局残留与泄漏均为 `0`。
 - 2026-07-22 的 V1.46 消除自主构建的人工确认等待。`autonomous-game-build` 仅把固定 auto-safe 白名单提升为自动执行;项目级、Agent 级和 MCP catalog 动态策略产生的其余 `RequiresConfirmation` 一律转为 `Denied`,向同一 run 返回“改用 auto-safe 工具或省略动作”的 observation。显式 deny 继续优先,标准 profile 的确认语义不变;包含拒绝成员的 Provider action 批次在任何工具执行前整体 `aborted`,不能执行 auto 前缀。
 - V1.46 的 Runner 恢复会把旧版本已持久化的自主 `pending-confirmation / waiting-confirmation` 迁移为 `observed-rejected / aborted`。Provider batch 是原子提交点,pending 只是可重建镜像;批次先落盘、pending 后落盘之间强杀时,下次恢复从 aborted batch 补齐拒绝账本并继续原 Session/run。公共状态和审计使用 `runtime-policy-rejected`,不得写成“已执行自动工具”或“开发者拒绝”。
 - V1.46 稳定树通过自主构建过滤 `20/20`、确认相关 `18/18`、旧等待态完整恢复、既有批次零副作用、`cargo check --tests`、Rust 串行全量 `1149 passed / 5 ignored / 0 failed`、fmt 与 diff 检查。确定性正式 E2E 为 **PASS**:Provider lifecycle `17/17`、revision `0 -> 2`、Chrome `37/37`、人工输入与残留均为 `0`。新的独立外部 Provider 同轮 E2E 也为 **PASS**:单条任务后 EOF,approve / answer / steer 为 `0`,Provider lifecycle `62/62`,revision `0 -> 5`,`game/index.html` 为 `7816` bytes,static smoke、desktop / mobile 与 `lane-defense-v1 37/37` 全通过,唯一 Supervisor assistant,全部 sidecar、reconciliation、重复和泄漏计数均为 `0`。
-- 2026-07-25 补充:Swarm CLI 将“Agent 执行通道仍忙”和“canonical run 可接受 steer”拆为两个判定。`task_queue.pending > 0` 继续用于 Runner 存活观察,但只有底层 steer 门禁认可的 `running / waiting-for-confirmation` Runtime 才能接收新指令;`completed/cancelled run A + pending run B` 必须先通过 Runner 恢复 B,禁止把消息追加到 A。start mutation 必须返回实际 `acceptedRunId`,CLI 以它建立 turn baseline,失败扫描、交互、收束和 `turn.report` 都只认 baseline run;若 Runner 已连续推进到 C,则从 task journal 读取 B 的终态。旧 A 的 cancel tombstone 不得阻断队首 B 恢复。若用户重复输入的内容正是已经落盘的 pending 任务且它仍是对话最后一条 user 消息,CLI 只观察原 run,不再追加第二份对话或创建新任务;active Goal 同样必须同时匹配 Agent、Session、runId、Run Profile 和可 steer 状态。连续 B/C 的 assistant 回复按 finalization message ID 归属,observer 不输出非目标 run 的状态或流;历史失败和报告计数都读取完整 task journal,失败聚合使用 `(agentId, runId)`,journal 失败终态不能被滞后的 state 投影覆盖。
+- 2026-07-25 补充:Agent 执行通道忙与 canonical run 可接受 steer 是两个判定。`task_queue.pending > 0` 继续用于 Runner 存活观察,但只有底层 steer 门禁认可的 `running / waiting-for-confirmation` Runtime 才能接收新指令;`completed/cancelled run A + pending run B` 必须先通过 Runner 恢复 B,禁止把消息追加到 A。start mutation 必须返回实际 `acceptedRunId`,失败扫描、交互与收束都只认该 run;若 Runner 已连续推进到 C,则从 task journal 读取 B 的终态。旧 A 的 cancel tombstone 不得阻断队首 B 恢复。若用户重复输入的内容正是已经落盘的 pending 任务且它仍是对话最后一条 user 消息,只观察原 run,不再追加第二份对话或创建新任务;active Goal 同样必须同时匹配 Agent、Session、runId、Run Profile 和可 steer 状态。连续 B/C 的 assistant 回复按 finalization message ID 归属,不输出非目标 run 的状态或流;历史失败和报告计数都读取完整 task journal,失败聚合使用 `(agentId, runId)`,journal 失败终态不能被滞后的 state 投影覆盖。
 - 2026-07-25 已落地:`autonomous-game-build` 的完成合同从“交付可玩原型”升级为“交付正式项目产物”。Runtime 继续按 seed manifest 的固定 16 个 task 分波推进;配置画布 Key 时,视觉链先由现有 `art-director` 生成正式规范图,再让 `design-foundation / art-asset-plan` 分别引用该规范图生成 UI 原型与透明图集,数值和音频需求仍按依赖并行,随后由程序整合、质量审查、当前 revision 静态检查、真实试玩和发布包装收束。每个新根 run 先重置本轮 seed task;普通 preview / smoke bookkeeping 不得代替自主 task 的真实执行和终态投影。
 - DAG 只能在 Project Supervisor 的 `agent.run_status` 回执认领已可靠观察、静态委派屏障清空后启动;正常执行、pending 续跑和重启恢复复用同一入口。调度在项目锁内预占 ready child 的 Agent Runtime lane 并落 durable journal,释放项目锁后才启动 drain;Runtime 的项目写锁统一提供约 10 秒有界等待,覆盖 Provider 首轮 tool-plan、并行只读结果投影和其它同 run 控制面写入,不能因 manifest 波次的短暂锁竞争把专业 child 投影为 failed。专业 child 终态只按父 run 身份、活跃合同和自身完成门禁投影,不等待独立静态委派屏障;该屏障仍约束下一波调度与父 run 收束。专业子 run 的验证允许 `verifiedRevision >= mutationRevision`,但 Supervisor 的最终静态和试玩证据仍必须精确覆盖最新全局 revision。
 - 画布能力最终一致性:凭据不可用时,`design-foundation / art-asset-plan` 的委派 `expectedArtifacts` 与对应 manifest task 终态投影必须同步采用文本产物降级;凭据可用时三张固定图片仍为严格完成条件。委派合同、完成合同与 manifest 投影必须从同一“普通平台会话或高级 External 凭据”事实派生。
 - Project Supervisor 只有在本轮必需 manifest tasks 全部 `completed`、当前配置对应的正式路径齐全且通过类型 / 可解析性检查、最新 project revision 的 `game.static_smoke` 与 `preview.validate` 都通过后,才能写入唯一最终回复。delivery 的 `completed / evidence-ready`、历史 revision 成功或单个文件存在都不能替代最终集成验收。已 `ready / claimed-by-parent` 的相同终态 delivery 在恢复扫描中按幂等重放,保留首次冻结结果,不再制造重复 `agent.delegate.result_failed`;真实终态冲突仍失败关闭。
-- 验证:`npm run agc:test` 已通过确定性 loopback Provider、真实 Runtime、项目写入和浏览器链路验收:同一父 Run 下 16 个 manifest task 均只有一个 logical run、一次 start、一次 completed 和一次 manifest projection,且无 failed / cancelled;父 run 与全部子 run 完成,最终 revision 为 `11`,基础正式产物、静态 smoke、桌面 / 移动 `37/37` 试玩通过,pending、reconciliation、Provider 失败、重复和泄漏计数均为 `0`。该结果不替代独立外部 Provider 验收。
+- 验证:确定性 loopback Provider、真实 Runtime、项目写入和浏览器链路验收已通过:同一父 Run 下 16 个 manifest task 均只有一个 logical run、一次 start、一次 completed 和一次 manifest projection,且无 failed / cancelled;父 run 与全部子 run 完成,最终 revision 为 `11`,基础正式产物、静态 smoke、桌面 / 移动 `37/37` 试玩通过,pending、reconciliation、Provider 失败、重复和泄漏计数均为 `0`。该结果不替代独立外部 Provider 验收。
 - 2026-07-26 本轮已验证 `npm run agc:config` 的终端 LLM 配置链路。向导与 GUI 使用同一 Tauri identifier 对应的系统 AppData 和同名 `game-creator.config.json` / 可选 local overlay;读取已有配置时只更新有效 LLM 层,保留其它现役配置,不提供普通画板凭据输入。LLM API Key 只从隐藏输入读取,拒绝 `--api-key`、仓库内目录、Git 已跟踪配置、符号链接,以及不是以 `world.genarrative.ai-game-creator` 为独立叶目录的 `--config-dir`。保存使用同目录 `0600` 临时文件原子替换,POSIX AppData 目录保持 `0700`,Windows 使用当前用户独占 DACL,写后复用真实 `--llm-status` 检查。
 - 2026-07-27 Windows DACL 启动回归修正,2026-08-03 补充 pwsh 模块隔离:`powershell.exe -Command` 后追加的位置参数会被 PowerShell 5.1 拼接进命令文本,不能用 `$args` 安全接收包含空格的 AppData / 临时目录。DACL 脚本改为从仅传给该子进程的环境变量读取目标绝对路径和目录标记,并复用 `Get-Item` 返回的 `FileSystemInfo.GetAccessControl()` 读取 ACL,不调用会因父 PowerShell 7 `PSModulePath` 污染而自动加载不兼容模块的 `Get-Acl`;`npm run agc:typecheck` 必须在真实 Windows 上执行配置回归,保证 `npm run agc` 的 `beforeDevCommand` 不因路径解析或模块加载失败退出。
 - 2026-07-31 macOS 临时路径回归修正:配置目的地安全检查返回解析过现存父目录的真实路径,回归 fixture 的期望值也必须先使用平台原生 `realpath` 规范化临时根目录。macOS 下 `/var/folders/...` 与 `/private/var/folders/...` 是同一目录身份,不得用未规范化字符串阻断 `agc:typecheck`。CI 还必须使用“真实目录 + 符号链接父目录 + 不存在叶目录”确定性复现该语义;Windows 使用 junction 覆盖驱动器号、大小写与链接路径差异。fixture 的规范化与断言必须位于同一 `try/finally` 清理边界内。
@@ -1440,7 +1250,6 @@ game-project/
 - 2026-07-28 pending / provider action 安全持久化约定:自然语言任务中的裸短语 `api key` 不是泄密证据,不能据此拒绝 action;否则 `agent.delegate` 的“不要暴露 External Editor API Key”等安全指令会被误判。API Key 赋值只允许完整受控状态或固定无密钥降级说明,禁止用安全状态前缀放行后续任意内容;`none-but-secret`、`not configured; actual value ...` 等必须失败关闭。Markdown 装饰、反引号或环境限定标签不能改变赋值语义,`**API Key**:`、`` `API Key`: ``、`API Key(生产):` 仍必须进入同一检测。持久化前继续检测结构化 `apiKey / api_key`、`Authorization / Cookie`、`token / Bearer` 标记和已知 secret token 形状,命中真实凭据时仍失败关闭。
 - 2026-07-28 Windows Provider retry 恢复修正:`provider_retry::list_at` 从绝对路径剥离项目 root 后,按路径组件重组成 `/` 分隔的 portable UTF-8 相对路径,再交给 Runtime JSON sidecar 读取器。不能直接使用 Windows `Path::to_str()` 的反斜杠文本,否则应用重启、Runner recovery scan 和正式 `--agent-resume` 都无法推进已到期的 `waiting-for-provider-retry` run。全部 provider retry 列举、previous 恢复、去重和路径冲突回归必须在真实 Windows 通过。
 - 2026-08-12 Windows Codex 启动链修正:AGC 不再直接依赖可能命中 WindowsApps shim 的 `codex` 命令,而是逐个执行 `--version` 验证候选,优先发现 npm 安装中的原生 `codex.exe`,并回退到 Codex Desktop 的原生 CLI;app-server 启动参数只关闭当前 CLI 仍支持的 feature flag。可信根 Project Supervisor 尚未冻结 Goal Contract 时,首轮和协议修复轮都只广告 `agent.goal_contract`,禁止计划更新、回复或其它动作抢跑。GUI 显式传入的 `--config-dir` 必须贯穿 Tauri 与 Cargo 的参数分隔并作为应用参数保留,setup 优先复用该目录,避免开发版或发布版误占默认 AppData 的 GUI owner lock。发布验收必须使用 release/安装目录 EXE 启动真实 Runner,并核对 CLI 版本、app-server 生命周期、Goal Contract 持久化、后续项目观察以及最终 completed/idle 状态,不能只以构建成功或 mock 测试代替。
-- `npm run agc:test:chat` 未显式指定配置且找不到 AppData 配置时,只在 stdin / stdout 都是 TTY 时询问并启动同一 `agc:config --configure-only` 向导,非 TTY 或显式无效 `--config-dir` 直接失败。测试环境只把主配置和存在时的 local overlay 复制到带随机 sentinel 的单次隔离 AppData;副本必须是独立的无符号链接普通文件,POSIX 权限为目录 `0700` / 文件 `0600`,不复制正式 Runner endpoint、lock 或其它 AppData。自动任务默认 50 分钟且可用 `--timeout-minutes` 显式设置;超时或信号会终止独立子进程树,POSIX 先向进程组发送 `SIGTERM`、等待 10 秒后发送 `SIGKILL` 并再等待 5 秒,Windows 使用 `taskkill /T` 并在强制阶段追加 `/F`。超时和信号分别以 `124 / 130 / 143` 失败退出,隔离 Runner 收束另有 20 秒上限;Runner 未空闲或收束失败时保留隔离配置和项目,验收未完成但 Runner 已安全退出时只保留一次性项目证据,不把中断报告为成功,也不误删正式 AppData。
 - 自动验收现在严格要求 manifest 恰好包含固定 16 个不重复 task ID 且全部为 `completed`,并逐任务核对当前父 Run 下唯一 logical run、一次 started、一次 completed、零 failed / cancelled 和一次 manifest projection;七份基础正式产物存在并满足文件 / JSON / 非占位入口检查,当前模式具备画板服务授权时再增加 `art-spec / ui-prototype / art-spritesheet` 三张图片。PNG 验收不止检查 magic / IHDR / 比例,还会校验 chunk CRC、zlib 解压、scanline 长度、索引色 PLTE 和未知 critical chunk。Runtime 根 Supervisor 的完成合同已升级为 `game-creator-autonomous-completion-contract.v2`,`baselineArtifacts` 必填并纳入指纹。
 - `design-foundation` 已增加专属职责边界:项目文件只允许写 `memory/project.md` 与 `game/game_design.md`;当前模式具备画板服务授权且合同要求界面原型时,只额外允许固定 `assets/ui-prototype.png`。它不得创建、修改、删除或补丁 `game/index.html`,不得改动其它程序实现、发布、音频或美术素材,也不得调用预览或试玩工具。
 - 自动验收现在严格要求 manifest 恰好包含固定 16 个不重复 task ID 且全部为 `completed`,并逐任务核对当前父 Run 下唯一 logical run、一次 started、一次 completed、零 failed / cancelled 和一次 manifest projection;七份基础正式产物存在并满足文件 / JSON / 非占位入口检查,配置画布 API Key 时再增加 `art-spec / ui-prototype / art-spritesheet` 三张图片。PNG 验收不止检查 magic / IHDR / 比例,还会校验 chunk CRC、zlib 解压、scanline 长度、索引色 PLTE 和未知 critical chunk。Runtime 根 Supervisor 的完成合同已升级为 `game-creator-autonomous-completion-contract.v2`,`baselineArtifacts` 必填并纳入指纹,旧 v1 或缺基线合同失败关闭;最终门禁要求最后一次验证工具是 `game.static_smoke`、状态通过且 `verifiedRevision == currentRevision`。`preview.validate` 回执必须绑定同一 Agent、run、current revision、当前 `game/index.html` 摘要、固定试玩场景、持久浏览器报告以及 desktop / mobile 两张截图的路径、摘要和 PNG 身份,任一证据缺失、变化、过期或来自其它 run / revision 都阻止最终回复。旧两图合同的确定性证据不替代新三图 DAG 验收;新合同实现后必须新起独立单轮。
@@ -1451,9 +1260,9 @@ game-project/
 - `canvas.asset_generate.replaceExisting` 默认并必须保持 `false`;只有静态专业 Agent 的 `delegated-*` 唯一 repair run 才能申请 `true`。Runtime 要求当前 delivery 带 `repairOfDelegationId`,原 delivery 已被同一父 Agent / 父 run 认领,原始与返工合同的目标 Agent 和精确 `expectedArtifacts` 路径一致;普通 run、未声明路径、错误 Agent、未认领原交付或缺失原图都失败关闭。图片生成仍服从 `art-director` / `design-foundation` / `art-asset-plan` 的固定输出路径、比例、尺寸、kind 和 label,禁止先删除正式图片;请求前记录旧文件 SHA-256,外部生成返回后在项目写锁内复核,旧图在网络请求期间变化即拒绝覆盖。授权替换先写私有临时文件,再以备份 / rename 切换;落盘或 manifest 登记失败时恢复旧图,不把新旧文件并存状态当作成功。
 - 视觉 Agent 只负责指导 Codex 选择合适的图片/编辑/图集工具并提供项目上下文,不再固定图片数量、文件槽位、素材类别或 spritesheet 布局;请求可按玩法需要生成单图、多图或任意切片布局。普通模式使用内部 `/api/editor/*`,standalone/高级模式使用对应 `/api/external/v1/*`;权限、计费、幂等、资源登记和安全校验保持不变。
 - 旧项目已有同路径派生图但缺少上述 provenance 时,一律标记为 legacy,不得只因文件、kind 或通用视觉检查存在就完成。原位替换仍走显式 repair:`design-foundation` 与 `art-asset-plan` 先在同一 Supervisor 批次分别建立 owner 精确原合同并交付 `needs-repair`,父 run 认领后再在同一批次分别发起各自唯一 repair;两个 repair 合称一个显式视觉返工阶段。`art-director` 不得跨 owner 声明或替换 UI / spritesheet,Runtime 在委派落盘前就拒绝这类合同,不再等到生图阶段才失败。
-- 2026-07-27 新起的“16 任务正式产物 + 两张真实画布图片 + current revision 静态 / 双视口浏览器 / PNG 证据 + 受限 repair 替换”独立外部 Provider 验收,使用 `npm run agc:test:chat -- --timeout-minutes 75`,约 `59m50s` 后以退出码 `0` 完整 **PASS**。同一轮真实生成并登记 `assets/ui-prototype.png`(`2829418` bytes)与 `assets/art-spritesheet.png`(`1361906` bytes),固定 `16` 个 manifest task 均为当前父 Run 下唯一 logical run、一次 started、一次 completed、零 failed / cancelled 和一次 manifest projection;七份基础正式产物、两张 PNG、当前 revision 的 `game.static_smoke`、desktop / mobile `lane-defense-v1` playtest、浏览器报告与截图全部通过。`turn.report=settled` 且唯一 assistant,busy / pending / running / confirmation / user-input / reconciliation 均为 `0`;隔离 Runner、一次性项目和隔离 AppData 已自动清理。此前失败轮继续独立保留,不与本轮拼接;未来合同变化仍须新起完整轮次复验。
+- 2026-07-27 新起的“16 任务正式产物 + 两张真实画布图片 + current revision 静态 / 双视口浏览器 / PNG 证据 + 受限 repair 替换”独立外部 Provider 验收约 `59m50s` 后以退出码 `0` 完整 **PASS**。同一轮真实生成并登记 `assets/ui-prototype.png`(`2829418` bytes)与 `assets/art-spritesheet.png`(`1361906` bytes),固定 `16` 个 manifest task 均为当前父 Run 下唯一 logical run、一次 started、一次 completed、零 failed / cancelled 和一次 manifest projection;七份基础正式产物、两张 PNG、当前 revision 的 `game.static_smoke`、desktop / mobile `lane-defense-v1` playtest、浏览器报告与截图全部通过。唯一 assistant,busy / pending / running / confirmation / user-input / reconciliation 均为 `0`;隔离 Runner、一次性项目和隔离 AppData 已自动清理。此前失败轮继续独立保留,不与本轮拼接;未来合同变化仍须新起完整轮次复验。
 - 2026-07-27 补充 tool-plan 成功响应交接的内容边界:Provider 的自然语言计划叙述,以及结构化 arguments 中 `body / code / content / css / html / newText / oldText / patch / script / text` 等源码内容字段,只检查真实密钥 token 形状、凭据头标记和不安全控制字符;仅仅提及 `.env` 或 `game-creator.config` 不能阻断已经计费的安全响应。结构化输入中的敏感 JSON key、非内容字段中的配置痕迹或绝对路径、真实 token、容量、thinking、身份、顺序和账本完整性门禁仍失败关闭。成功 handoff 失败进入 reconciliation 时,Runtime 额外只持久化受控 `failureKind`、脱敏错误 SHA-256 和字符数,不保存 Provider 正文、function arguments、密钥或绝对路径。定向回归覆盖叙述/源码字段放行、`.env.local` 路径和真实 token 拒绝、全部 tool-plan handoff 回归及诊断零正文。
-- 自然语言 interaction 的 `resume` 只代表“继续当前未完成 Runtime”。宿主在进入 interaction 前已确认当前 Session 没有可 steer、pending 或 running 的 Runtime 时,模型返回的自然语言 `resume` 必须规范化为 `execute`,基于会话历史新建 run;显式 `/resume` 仍只执行恢复扫描且无任务时不新建,避免“那就继续修复”被反复吞成空恢复。
+- 自然语言 interaction 的 `resume` 只代表“继续当前未完成 Runtime”。宿主在进入 interaction 前已确认当前 Session 没有可 steer、pending 或 running 的 Runtime 时,模型返回的自然语言 `resume` 必须规范化为 `execute`,基于会话历史新建 run;显式恢复入口(`--agent-resume`)仍只执行恢复扫描且无任务时不新建,避免“那就继续修复”被反复吞成空恢复。
 - `prepared` 发布后,创建阶段锚定的事务目录句柄和 identity 必须由 live rollback 对象一直持有到 `committed` 清理或 rollback 结束,提交和回滚不得按 `PathBuf` 重新接受替换目录;live commit 在发布 `committed` 前后都必须验证 retained handle 仍对应权威 pathname,身份漂移不得降级为提交成功 warning。Unix 清理先把权威叶子通过 no-replace rename 原子隔离为固定 retired 目录,复核 retained inode 后再清空和删除,删除前还需再次复核;进程若在隔离后退出,下次持项目写锁恢复先幂等清理 retired 目录。恢复在首个 canonical 写入前一次性冻结九路径全部前态,晚序普通文件内容变化也必须触发 CAS 冲突并逆序回滚早序安装。历史 `.previous / .replacement` 若没有 durable journal,只允许通过锚定父目录识别后进入 reconciliation;不得凭 pathname 自动 hard-link、move、恢复 canonical 或删除残留。
 - 图集本地提交以主图 staging 为线性化前置:任何新主图先写随机私有 staging 文件,替换时保留 previous,canonical 主图完整安装后才写四切片、公开清单、私有回执和项目资产登记。进程若在 backup/install 窗口退出,同一 accepted External generation 恢复先识别唯一同 suffix 的 previous/replacement 对并恢复旧主图,再按远端结果完成替换;若 canonical 已等于远端摘要,则不再要求替换授权,直接补齐其余合同。成功后清理主图、四切片、公开清单、私有回执和项目 manifest 的全部遗留 staging/backup。首次生成也禁止直接流式写 canonical 路径,避免部分 PNG 被误认为已安装结果。
 - ESM 投影中的顶层 function/class/variable 声明必须直接使用 Oxc statement span 提取,不能用首个分号或换行截断箭头函数、多行 initializer 或多 declarator;对象、数组、默认值与 rest 解构声明必须递归收集全部 binding,并保证同一声明只投影一次。import symbol 与 importer 自有 root binding 分开保存,即使名称仅大小写不同也不得在组合前折叠;投影根分配 canonical 名时必须避让 importer 与 origin 的全部非 import semantic binding,不能被嵌套局部捕获。模块组合必须按依赖深度迭代到稳定闭包,把被导出函数继续依赖的 imported origin 带入最终 consumer 单元;循环 ESM 以原始声明 identity 去重回流,不能不断生成重命名副本,并以最终 span replacement 后的单 unit `2 MiB`、累计投影处理 `32 MiB` 为失败关闭上限。固定字符串 dynamic import 同样由下游实际使用的 export 反向驱动加载;未使用 export、未调用嵌套函数和恒假分支中的 dynamic source 不得进入模块单元,同名 dynamic export 不能反向选择未引用的本地声明。只有被选声明中的 `await import` 解构、namespace member 或 `.then(...)` 静态 binding 才能进入组合投影;callback 参数、解构 alias 和 namespace member 必须按 semantic symbol span 改接到投影根,dynamic object shorthand 改名时还必须显式保留原属性键,不能靠追加同名文本跨过局部遮蔽。span replacement 完成后还要对完整组合 unit 重跑 parser 与 semantic,启发式扫描只能在原始源码完成 AST 解析和掩码后再统一小写。匿名 default function / arrow 必须在原始 AST 中以 collision-safe synthetic binding 注册可外调 root span,使 wrapper 内 imported member 的传递依赖继续传播;synthetic binding 必须避开用户真实根名,改名只能更新 default target,不能污染同名命名导出。namespace 经过 renamed re-export 时,同时保留 importer 使用的 member 名和最终 origin export 名:前者定位 importer member span,后者选择 origin declaration,不能混用。named import、namespace import 和 namespace 解构 alias 的成员调用必须保留完整静态成员路径,并按调用 span 排除恒假分支后再把 demand 传播到上游;对象 / class 直接成员、对象解构 alias、实例 alias 与下游 wrapper 都使用同一条可达性链。对象 method shorthand、函数表达式值、箭头函数值以及 class function-valued field 必须按精确函数 span 注册成员根,只加载被实际调用成员中的 dynamic dependency,不能因声明写法遗漏,也不能把属性内未调用的嵌套函数升级为根。
@@ -1595,7 +1404,7 @@ game-project/
 - `regenerate` 授权只取当前请求中最新一条原始 `role=User` 消息,并绑定外层稳定 `clientTurnId`;引号或代码中的按钮文案/示例、历史消息、模型自行填写的 `mode`、MCP 自动批准和缺失 clientTurnId 均不能形成付费替换授权。授权判定先对完整原文做 Unicode NFKC 与常见撇号规范化,随后整串必须完整匹配审核过的独立立即执行指令,只允许句号/感叹号收尾;不得剥离引号、方括号或代码片段,动作前后也不得携带 brief、条件、否定、选择、确认、费用、延迟或任意其它文本。复杂风格需求必须先在非付费消息中描述,再由下一条独立“请重新生成美术”确认消息签发授权;不能靠开放式 deny 词表猜测当前付费同意。工具桥只保留授权判定和摘要,不保存或回传用户原文。同一进程重复水合相同 `clientTurnId` 时,“回合仍在运行”只属于瞬时占用状态,前端不得以稳定 assistant messageId 将其写成终态;原执行的成功回复仍由 Tauri 在返回前持久化。DirectProject app-server 的 cwd、sandbox writable root 和文件变更批准根统一为用户选择的整个项目根;canonical 项目根的原生 OS 路径字节与权威 manifest `projectId` 通过域标签和各自长度前缀编码后共同进入 Direct 连接池和 thread 身份,稳定符号链接改指其它项目、同路径重建项目、不同非 UTF-8 路径或内嵌 NUL 的项目 ID 都不能复用旧连接。`assets/`、`game/` 与其它项目文件可写,`.agent/`、`.git/`、密钥文件和 Runtime 控制面由项目文件层拒绝,网络关闭,命令执行、MCP 扩权和额外权限申请一律拒绝。受控 `agc_tools` 子进程从同一项目根 cwd 经相同权限校验反查 canonical 项目根供客户端内部桥使用,不能把该根加入其它 Codex writable roots。`resources` 只返回本地 asset/path/kind/media type、Canvas project/resource/asset/task ID 与 reference resource IDs,不返回 prompt、model、provider route、绝对路径、URL、Token、Cookie 或 API Key。客户端付费资源生成(图片、视频、角色动画、音效、背景音乐)统一调用站内 `/api/editor/...` 路由并复用平台登录态,不走 External v1;External v1 只保留给外部开发者模式和历史账本重放兼容。
 - 成功响应中的 `warnings / sliceWarnings` 与错误响应采用同一脱敏边界:逐条移除宿主绝对路径、凭据与 URL,并设置固定长度上限;非阻断告警不成为绕开错误分支隐私保护的旁路。
 - Direct 同进程重复水合若收到“同一 stable turn 仍在运行”,必须释放当前 App 实例的恢复 claim;该结果不落 assistant 终态,后续显式刷新对话可按原 `clientTurnId` 再次读取已落盘回复或续跑,不要求重载整个 WebView,也不启动无界自动轮询。
-- 对话恢复从新到旧扫描全部合法 Direct User 回合;较新的 User 已有稳定 assistant 时必须继续寻找更早未回答回合,不能提前结束扫描。普通成功回复或普通错误回复若终态 assistant 持久化失败,同样必须释放当前 App 实例的恢复 claim,使后续显式 `/history` 能以原稳定 `clientTurnId` 重试;claim 只表示当前实例内正在恢复,不能成为磁盘终态的替代品。
+- 对话恢复从新到旧扫描全部合法 Direct User 回合;较新的 User 已有稳定 assistant 时必须继续寻找更早未回答回合,不能提前结束扫描。普通成功回复或普通错误回复若终态 assistant 持久化失败,同样必须释放当前 App 实例的恢复 claim,使后续显式重新加载对话时能以原稳定 `clientTurnId` 重试;claim 只表示当前实例内正在恢复,不能成为磁盘终态的替代品。
 - Direct 的运行态素材验收不再把 `assets/art-spec.png` 当作背景、角色、道具或反馈;规范图只作为派生 reference。标准核心图集无论首次创建还是显式重生成,都必须原子取得恰好四张 canonical 独立切片后才算本次生成成功;每张切片必须有真实 alpha、可见像素、唯一规范像素内容及唯一 Canvas `resourceId / assetObjectId`。旧项目只在私有回执与公开清单、当前源图和顶层登记完全一致时投影四条 `slicePaths`;部分、opaque、重复或缺回执状态只返回 warning,不得猜测或伪造衍生素材。
 - 机器门只证明 PNG、真实 alpha、非空可见像素、切片像素唯一、稳定平台身份、顶层登记及源码/双视口实际渲染。背景是否混入实体、地面是否无缝、素材语义是否匹配、最终绘制尺寸是否满足玩法仍由 Codex 检查工具图片和 desktop/mobile 试玩截图;prompt 约束本身不算通过证据。
 
@@ -1623,7 +1432,7 @@ DirectProject 使用 `approvalPolicy=never`,避免每次原生调用再经过
 ## 2026-09-04 AGC 项目聊天 Markdown 与流式回复渲染
 
 - 项目开发工作台的 `ProjectWorkspaceChatPane`、`ProjectSupervisorView` 与 `SupervisorChatOnlyView` 继续共享 `ChatMessage` / `visibleMessages` 数据结构;聊天 Markdown 只作为前端表现层能力,不新增消息字段、持久化格式、后端 DTO 或事件协议。
-- 三个视图统一复用 `apps/ai-game-creator-shell/src/components/ChatMarkdownMessage`。assistant 历史消息和流式临时回复使用 `react-markdown + remark-gfm` 渲染;用户消息与命令草稿保持纯文本。调用方仍先执行现有的 `projectSupervisorVisibleConversationText` 安全文案归一化,再交给展示组件。
+- 三个视图统一复用 `apps/ai-game-creator-shell/src/components/ChatMarkdownMessage`。assistant 历史消息和流式临时回复使用 `react-markdown + remark-gfm` 渲染;用户消息保持纯文本。调用方仍先执行现有的 `projectSupervisorVisibleConversationText` 安全文案归一化,再交给展示组件。
 - 流式回复以事件中的 `accumulatedText` 作为当前完整草稿:每次更新替换上一版临时正文,不在渲染层自行拼接 `deltaText`。既有 `runId` / sequence 去重、最终 assistant 持久化和历史回放语义保持不变。
 - Markdown 禁止原始 HTML;链接和图片只显示普通文本,不产生可点击或可加载的外部资源。后续若开放安全外链,必须另行评估协议白名单、窗口策略和审计边界,并保留实现 TODO。
 - Markdown 元素样式使用 Tailwind 内联 class,限定在聊天消息组件内部,不改全局 `.message`、资源文档预览或启动器全局 Agent 聊天。组件异常按单条消息回退纯文本,不能使整个聊天面板崩溃。
@@ -1634,7 +1443,7 @@ DirectProject 使用 `approvalPolicy=never`,避免每次原生调用再经过
 - 本次正式产品范围只包含 `DirectProject` 单 Codex Agent;`Provider`、`ToolHost`、`DirectHome` 不新增联网工具桥,也不纳入本次联网路由覆盖。受控联网唯一实现为 `agc_tools.agc_web_search`:Codex app-server 通过审核的 STDIO MCP 工具目录发起调用,客户端 loopback 工具桥执行固定 Bing RSS HTTPS 请求,过滤非 HTTPS、凭据 URL、回环 / 私网 / 本地域名,返回有界标题、摘要和结果链接,并以“不可信网页内容”标签回传。
 - Codex 原生 `web_search` 在 app-server 启动参数中始终保持 `web_search=\"disabled\"`。`llm.webSearchEnabled` 在 `DirectProject` 仅表示受控工具开关,不得被状态文案解释为 Codex 原生联网;工具桥关闭时即使收到旧调用也失败关闭。原生命令网络、任意外部 MCP、工具参数透传、网页内容改写系统规则 / 身份 / 权限 / 协议均继续禁止。
 - 配置文件新增 `schemaVersion: \"game-creator-config.v2\"`。新默认配置开启 `stream` 与受控联网;无版本旧配置在启动时补写 v2,旧 `codex_app_server` 路由仅在省略 `webSearchEnabled` 时按历史默认补为开启,显式 `false` 保留;Provider / Anthropic 路由未提供搜索覆盖时保持关闭,避免继承 DirectProject 默认。主配置按完整配置迁移;本地覆盖只补 schema 版本,不凭不完整 overlay 推断或写入 `agentMode` / 搜索布尔值。
-- `/llm-status`、开发单 Agent 状态和项目 Agent 状态卡对 Codex 模式显示“流式开启 / 关闭”“受控联网开启 / 关闭”“Codex 原生 web_search 关闭”,不显示 API Key、URL、请求头、绝对路径或 Provider 原始错误正文。
+- 开发单 Agent 状态和项目 Agent 状态卡对 Codex 模式显示“流式开启 / 关闭”“受控联网开启 / 关闭”“Codex 原生 web_search 关闭”,不显示 API Key、URL、请求头、绝对路径或 Provider 原始错误正文。
 - BDD 验收场景与测试映射:DirectProject 默认工具目录包含 `agc_web_search` 且原生搜索仍 disabled;未审核字段、越界数量和非公开 URL 在桥接端失败关闭;旧无版本配置迁移为 v2 且按路由得到正确默认;状态卡显示三态安全摘要。对应 Rust `configuration`、`direct_tools_mcp`、`direct_tool_bridge`、`codex_app_server` 定向测试及前端状态格式化 / AppSurface 测试。
 
 ## 2026-09-06 AGC LLM 代理请求体合同

From 6106b4e67caa6d55feaac374a86aa32ece2b5e64 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= 
Date: Wed, 23 Sep 2026 11:12:39 +0800
Subject: [PATCH 02/20] =?UTF-8?q?=E9=80=80=E5=BD=B9AGC=E9=A1=B9=E7=9B=AE?=
 =?UTF-8?q?=E5=AF=B9=E8=AF=9D=E6=96=9C=E6=9D=A0=E5=91=BD=E4=BB=A4=E4=B8=8E?=
 =?UTF-8?q?=E7=BB=88=E7=AB=AFswarm=20chat=E5=85=A5=E5=8F=A3=EF=BC=9A?=
 =?UTF-8?q?=E5=BA=94=E7=94=A8=E4=B8=8ERust=E5=AE=9E=E7=8E=B0=E5=88=A0?=
 =?UTF-8?q?=E9=99=A4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- 删除应用侧 /history 精确匹配分支与 reloadHistory、chatPromptPolish 的 / 前缀绕过、chatCommandMetadata、memoryCommands、projectSummaryConstants 命令清单
- 删除只服务退役摘要面板的 project-summary/*Summaries.ts 与 agentTrace.ts 及对应测试
- 删除 /sync-canvas-project、/read、/trace 草稿回填死链(agentPresentation.ts 与 Rust suggested_canvas_tool_call)
- 删除无人调用的 Tauri 命令 get_game_creation_agent_capabilities 与 get_limited_local_commands
- 删除 --swarm-chat 入口、SwarmChat 变体、src/swarm_cli.rs 与整个 swarm_cli/ 目录
- 收敛 agent/interaction.rs 至自然语言 steer 决策路径,删除交互内核整层
- 删除 SWARM_TURN_*_ERROR、print_runtime_response_stream_status 及其专属测试
- 更新 ChatMarkdownMessage、chatPromptPolish、rememberCommand 与 appSurface 用例,移除斜杠命令断言
---
 .../src-tauri/src/agent/generation.rs         |    5 +-
 .../src/agent/generation/canvas_generation.rs |   27 -
 .../agent/generation/loop_orchestration.rs    |    2 +-
 .../src/agent/generation/pass_artifacts.rs    |    9 +-
 .../src-tauri/src/agent/interaction.rs        |  494 +---
 .../src-tauri/src/agent/runtime_protocol.rs   |    2 +-
 .../src-tauri/src/cli.rs                      |  167 +-
 .../src-tauri/src/commands.rs                 |   16 +-
 .../src-tauri/src/main.rs                     |   22 +-
 .../src-tauri/src/swarm_cli.rs                |   72 -
 .../src-tauri/src/swarm_cli/commands.rs       |   79 -
 .../src-tauri/src/swarm_cli/conversation.rs   |  337 ---
 .../src-tauri/src/swarm_cli/goal_commands.rs  |  189 --
 .../src-tauri/src/swarm_cli/input.rs          |  390 ---
 .../src-tauri/src/swarm_cli/observer.rs       |  843 ------
 .../src-tauri/src/swarm_cli/report.rs         |  285 --
 .../src/swarm_cli/terminal_classification.rs  |  803 ------
 .../src-tauri/src/swarm_cli/tests.rs          | 2417 -----------------
 .../src-tauri/src/swarm_cli/turn_dispatch.rs  |  590 ----
 .../src-tauri/src/swarm_cli/turn_wait.rs      |  427 ---
 .../src-tauri/src/tests/project.rs            |   52 -
 .../src-tauri/src/tests/provider.rs           |   24 -
 apps/ai-game-creator-shell/src/App.tsx        |   77 +-
 .../project-summary/agentPresentation.ts      |  653 -----
 .../project-summary/agentRunSummaries.ts      |  311 ---
 .../features/project-summary/agentTrace.ts    |   96 -
 .../project-summary/chatCommandMetadata.ts    |   48 -
 .../projectArtifactSummaries.ts               |  368 ---
 .../project-summary/projectAssetSummaries.ts  |  282 --
 .../projectDeliverySummaries.ts               |  661 -----
 .../projectGuidanceSummaries.ts               |  271 --
 .../projectOverviewSummaries.ts               |  562 ----
 .../features/project-summary/projectPath.ts   |   12 -
 .../projectPlanningSummaries.ts               |  463 ----
 .../projectPlaytestSummaries.ts               |  786 ------
 .../projectQualitySummaries.ts                | 1078 --------
 .../projectReadinessSummaries.ts              |  811 ------
 .../project-summary/projectSummary.ts         |  134 -
 .../projectSummaryConstants.ts                |  215 --
 .../project-workspace/agentRunTrace.ts        |  104 -
 .../project-workspace/chatPromptPolish.ts     |    6 +-
 .../project-workspace/memoryCommands.ts       |   53 -
 .../project-workspace/projectCommandPolicy.ts |    8 -
 .../useDirectProjectChatController.ts         |   50 -
 .../tests/ChatMarkdownMessage.test.tsx        |    7 +-
 .../tests/agentRunTrace.test.ts               |  155 --
 .../tests/agentSwarmTestEntry.test.ts         | 1915 -------------
 .../tests/agentTraceSummary.test.ts           |  284 --
 .../tests/appSurface/harness.ts               |   16 -
 .../appSurface/project-conversation.suite.ts  |  158 --
 .../appSurface/project-development.suite.ts   |  166 --
 .../tests/chatPromptPolish.test.tsx           |   21 +-
 .../tests/rememberCommand.test.ts             |   42 +-
 53 files changed, 40 insertions(+), 17025 deletions(-)
 delete mode 100644 apps/ai-game-creator-shell/src-tauri/src/swarm_cli.rs
 delete mode 100644 apps/ai-game-creator-shell/src-tauri/src/swarm_cli/commands.rs
 delete mode 100644 apps/ai-game-creator-shell/src-tauri/src/swarm_cli/conversation.rs
 delete mode 100644 apps/ai-game-creator-shell/src-tauri/src/swarm_cli/goal_commands.rs
 delete mode 100644 apps/ai-game-creator-shell/src-tauri/src/swarm_cli/input.rs
 delete mode 100644 apps/ai-game-creator-shell/src-tauri/src/swarm_cli/observer.rs
 delete mode 100644 apps/ai-game-creator-shell/src-tauri/src/swarm_cli/report.rs
 delete mode 100644 apps/ai-game-creator-shell/src-tauri/src/swarm_cli/terminal_classification.rs
 delete mode 100644 apps/ai-game-creator-shell/src-tauri/src/swarm_cli/tests.rs
 delete mode 100644 apps/ai-game-creator-shell/src-tauri/src/swarm_cli/turn_dispatch.rs
 delete mode 100644 apps/ai-game-creator-shell/src-tauri/src/swarm_cli/turn_wait.rs
 delete mode 100644 apps/ai-game-creator-shell/src/features/project-summary/agentRunSummaries.ts
 delete mode 100644 apps/ai-game-creator-shell/src/features/project-summary/agentTrace.ts
 delete mode 100644 apps/ai-game-creator-shell/src/features/project-summary/chatCommandMetadata.ts
 delete mode 100644 apps/ai-game-creator-shell/src/features/project-summary/projectArtifactSummaries.ts
 delete mode 100644 apps/ai-game-creator-shell/src/features/project-summary/projectAssetSummaries.ts
 delete mode 100644 apps/ai-game-creator-shell/src/features/project-summary/projectDeliverySummaries.ts
 delete mode 100644 apps/ai-game-creator-shell/src/features/project-summary/projectGuidanceSummaries.ts
 delete mode 100644 apps/ai-game-creator-shell/src/features/project-summary/projectOverviewSummaries.ts
 delete mode 100644 apps/ai-game-creator-shell/src/features/project-summary/projectPlanningSummaries.ts
 delete mode 100644 apps/ai-game-creator-shell/src/features/project-summary/projectPlaytestSummaries.ts
 delete mode 100644 apps/ai-game-creator-shell/src/features/project-summary/projectQualitySummaries.ts
 delete mode 100644 apps/ai-game-creator-shell/src/features/project-summary/projectReadinessSummaries.ts
 delete mode 100644 apps/ai-game-creator-shell/src/features/project-summary/projectSummaryConstants.ts
 delete mode 100644 apps/ai-game-creator-shell/src/features/project-workspace/agentRunTrace.ts
 delete mode 100644 apps/ai-game-creator-shell/src/features/project-workspace/memoryCommands.ts
 delete mode 100644 apps/ai-game-creator-shell/tests/agentRunTrace.test.ts
 delete mode 100644 apps/ai-game-creator-shell/tests/agentSwarmTestEntry.test.ts
 delete mode 100644 apps/ai-game-creator-shell/tests/agentTraceSummary.test.ts

diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/generation.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/generation.rs
index 5b09bc5d6..f4be82c32 100644
--- a/apps/ai-game-creator-shell/src-tauri/src/agent/generation.rs
+++ b/apps/ai-game-creator-shell/src-tauri/src/agent/generation.rs
@@ -71,9 +71,8 @@ pub(crate) use canvas_generation::{
     normalize_platform_art_reference_asset_ids, normalize_platform_art_target_category,
     platform_art_asset_art_spec, platform_art_asset_output_extension_matches,
     platform_art_runtime_references_match_request_contract, prepare_platform_art_asset_output_path,
-    project_canvas_asset_media_types, role_has_canvas_assets, suggested_canvas_tool_call,
-    validate_platform_art_icon_prompt, PlatformArtAssetGenerationOptions,
-    PLATFORM_ART_ASSET_GENERATION_KINDS,
+    project_canvas_asset_media_types, role_has_canvas_assets, validate_platform_art_icon_prompt,
+    PlatformArtAssetGenerationOptions, PLATFORM_ART_ASSET_GENERATION_KINDS,
 };
 #[allow(unused_imports)]
 pub(crate) use draft_validation::{
diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/canvas_generation.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/canvas_generation.rs
index e79f40ac9..e016b9101 100644
--- a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/canvas_generation.rs
+++ b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/canvas_generation.rs
@@ -315,33 +315,6 @@ pub(crate) fn project_canvas_asset_media_types(root: &Path) -> Vec {
         .unwrap_or_default()
 }
 
-pub(crate) fn suggested_canvas_tool_call(
-    role_brief: &AgentRoleBrief,
-    input_paths: &[String],
-    canvas_asset_media_types: &[String],
-) -> Option {
-    if role_brief.status != "completed"
-        || role_has_canvas_assets(role_brief, canvas_asset_media_types)
-    {
-        return None;
-    }
-    let tool_id = match (
-        role_brief.group_definition.id,
-        role_brief.role_definition.id,
-    ) {
-        ("art", "asset") | ("audio", "sfx") => "agent.tool.suggest.canvas.project_sync",
-        _ => return None,
-    };
-    Some(GameCreationAgentToolCallTrace {
-        tool_id: tool_id.to_string(),
-        status: "suggested".to_string(),
-        input_paths: input_paths.to_vec(),
-        output_paths: Vec::new(),
-        summary: "项目还没有对应类型的画板回流素材;建议用户确认 /sync-canvas-project <画板项目ID> 后同步画板资源到本地 assets/。"
-            .to_string(),
-    })
-}
-
 pub(crate) async fn maybe_generate_platform_art_asset_step(
     root: &Path,
     prompt: &str,
diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/loop_orchestration.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/loop_orchestration.rs
index a79d03fbd..72fa03629 100644
--- a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/loop_orchestration.rs
+++ b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/loop_orchestration.rs
@@ -204,7 +204,7 @@ pub(crate) async fn run_game_creator_agent_loop_at(
         let platform_art_step =
             maybe_generate_platform_art_asset_step(root, prompt, &group_briefs, pass, progress)
                 .await;
-        append_group_brief_steps(root, pass, &agenda.relative_path, &group_briefs, &mut steps);
+        append_group_brief_steps(pass, &agenda.relative_path, &group_briefs, &mut steps);
         if let Some(step) = platform_art_step {
             steps.push(step);
         }
diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/pass_artifacts.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/pass_artifacts.rs
index 44a966e00..60e097107 100644
--- a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/pass_artifacts.rs
+++ b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/pass_artifacts.rs
@@ -395,13 +395,11 @@ pub(crate) fn append_agent_success_memories(
 }
 
 pub(crate) fn append_group_brief_steps(
-    root: &Path,
     pass: u8,
     agenda_relative_path: &str,
     briefs: &[AgentGroupBrief],
     steps: &mut Vec,
 ) {
-    let canvas_asset_media_types = project_canvas_asset_media_types(root);
     for brief in briefs {
         for role_brief in &brief.role_briefs {
             let input_paths = vec![
@@ -421,7 +419,7 @@ pub(crate) fn append_group_brief_steps(
                 output_paths.push(role_brief.memory_relative_path.clone());
                 output_paths.push(PROJECT_BLACKBOARD_MEMORY_PATH.to_string());
             }
-            let mut step = with_task_context(
+            let step = with_task_context(
                 agent_trace_step_owned(
                     pass,
                     &format!(
@@ -439,11 +437,6 @@ pub(crate) fn append_group_brief_steps(
                 Some(role_brief.role_definition.task_id),
                 "role-brief",
             );
-            if let Some(tool_call) =
-                suggested_canvas_tool_call(role_brief, &input_paths, &canvas_asset_media_types)
-            {
-                step.tool_calls.push(tool_call);
-            }
             steps.push(step);
         }
         let role_paths = brief
diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/interaction.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/interaction.rs
index 8e7390696..397c754f1 100644
--- a/apps/ai-game-creator-shell/src-tauri/src/agent/interaction.rs
+++ b/apps/ai-game-creator-shell/src-tauri/src/agent/interaction.rs
@@ -1,86 +1,8 @@
 use super::*;
-use agent_runtime_core::{CapabilityDefinition, CapabilityRegistry};
 
-const AGENT_INTERACTION_EXECUTE_TOOL: &str = "runtime_execute";
-const AGENT_INTERACTION_RESUME_TOOL: &str = "runtime_resume";
-const AGENT_INTERACTION_PROJECT_LOCATION_TOOL: &str = "project_location";
-const AGENT_INTERACTION_MAX_OUTPUT_TOKENS: u32 = 1_200;
-const AGENT_INTERACTION_PROVIDER_INSTANCE_ID: &str = "agc-interaction";
+const AGENT_RUNTIME_STEER_DECISION_MAX_OUTPUT_TOKENS: u32 = 1_200;
 pub(crate) const AGENT_RUNTIME_STEER_DECISION_TOOL: &str = "runtime_steer_decision";
 
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-enum AgentInteractionToolKind {
-    Execute,
-    Resume,
-    ProjectLocation,
-}
-
-fn agent_interaction_tool_registry() -> Result, String>
-{
-    let empty_input_schema = || {
-        serde_json::json!({
-            "type": "object",
-            "properties": {},
-            "additionalProperties": false
-        })
-    };
-    CapabilityRegistry::try_new([
-        CapabilityDefinition::try_new(
-            AGENT_INTERACTION_EXECUTE_TOOL,
-            AGENT_INTERACTION_EXECUTE_TOOL,
-            prompt_text!("interaction.execute_description"),
-            empty_input_schema(),
-            AgentInteractionToolKind::Execute,
-        )
-        .map_err(|error| format!("Agent interaction capability 无效:{error}"))?,
-        CapabilityDefinition::try_new(
-            AGENT_INTERACTION_RESUME_TOOL,
-            AGENT_INTERACTION_RESUME_TOOL,
-            prompt_text!("interaction.resume_description"),
-            empty_input_schema(),
-            AgentInteractionToolKind::Resume,
-        )
-        .map_err(|error| format!("Agent interaction capability 无效:{error}"))?,
-        CapabilityDefinition::try_new(
-            AGENT_INTERACTION_PROJECT_LOCATION_TOOL,
-            AGENT_INTERACTION_PROJECT_LOCATION_TOOL,
-            prompt_text!("interaction.project_location_description"),
-            empty_input_schema(),
-            AgentInteractionToolKind::ProjectLocation,
-        )
-        .map_err(|error| format!("Agent interaction capability 无效:{error}"))?,
-    ])
-    .map_err(|error| format!("Agent interaction registry 无效:{error}"))
-}
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub(crate) enum AgentInteractionAction {
-    Reply(String),
-    Execute,
-    Resume,
-    ProjectLocation,
-}
-
-impl AgentInteractionAction {
-    pub(crate) fn label(&self) -> &'static str {
-        match self {
-            Self::Reply(_) => "reply",
-            Self::Execute => "execute",
-            Self::Resume => "resume",
-            Self::ProjectLocation => "project_location",
-        }
-    }
-}
-
-#[derive(Debug, Deserialize)]
-#[serde(deny_unknown_fields, tag = "action", rename_all = "snake_case")]
-enum AgentInteractionTextEnvelope {
-    Reply { reply: String },
-    Execute,
-    Resume,
-    ProjectLocation,
-}
-
 #[derive(Debug, Deserialize)]
 #[serde(deny_unknown_fields, rename_all = "camelCase")]
 struct AgentRuntimeSteerDecisionArguments {
@@ -148,7 +70,7 @@ fn build_agent_runtime_steer_decision_request(
     );
     let request = LlmRunRequest::single_turn(system, user)
         .with_api_kind(api_kind)
-        .with_max_output_tokens(AGENT_INTERACTION_MAX_OUTPUT_TOKENS)
+        .with_max_output_tokens(AGENT_RUNTIME_STEER_DECISION_MAX_OUTPUT_TOKENS)
         .with_function_tools(vec![agent_runtime_steer_decision_function_tool()])
         .with_tool_choice(platform_llm::LlmToolChoice::Required);
     let request = apply_game_creator_llm_reasoning_effort(request, &llm)?;
@@ -247,253 +169,6 @@ pub(crate) async fn decide_game_creator_agent_runtime_steer_at(
     )
 }
 
-pub(crate) fn game_creator_agent_uses_interaction_kernel(agent_id: &str) -> bool {
-    if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID {
-        return true;
-    }
-    game_creator_agent_role_definition(agent_id).is_some_and(|(_group, role)| role.id == "director")
-}
-
-fn agent_interaction_function_tools() -> Result, String> {
-    Ok(agent_interaction_tool_registry()?
-        .iter()
-        .map(|definition| {
-            platform_llm::LlmFunctionTool::new(
-                definition.function_name(),
-                definition.description(),
-                definition.input_schema().clone(),
-            )
-            .with_strict(true)
-        })
-        .collect())
-}
-
-fn agent_interaction_system_prompt(agent_id: &str) -> String {
-    let role_prompt = if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID {
-        game_creator_project_supervisor_chat_system_prompt()
-    } else {
-        game_creator_role_agent_chat_system_prompt()
-    };
-    let protocol = prompt_text!("interaction.protocol");
-    format!(
-        prompt_text!("interaction.system"),
-        protocol = protocol,
-        role_prompt = role_prompt,
-    )
-}
-
-fn build_agent_interaction_llm_request(
-    agent_id: &str,
-    prompt: &str,
-    context: &str,
-    llm: &GameCreatorLlmConfig,
-) -> Result {
-    let api_kind = parse_game_creator_llm_api_kind(&llm.api_kind)?;
-    let user_prompt = if context.trim().is_empty() {
-        format!(prompt_text!("interaction.user"), prompt = prompt,)
-    } else {
-        format!(
-            prompt_text!("interaction.user_with_context"),
-            context = context,
-            prompt = prompt,
-        )
-    };
-    let function_tools = agent_interaction_function_tools()?;
-    let request = LlmRunRequest::new(vec![
-        LlmMessage::system(agent_interaction_system_prompt(agent_id)),
-        LlmMessage::user(user_prompt),
-    ])
-    .with_api_kind(api_kind)
-    .with_max_output_tokens(AGENT_INTERACTION_MAX_OUTPUT_TOKENS)
-    .with_function_tools(function_tools)
-    .with_tool_choice(platform_llm::LlmToolChoice::Auto);
-    apply_game_creator_llm_reasoning_effort(request, llm)
-}
-
-fn build_agent_interaction_request_for_session(
-    root: &Path,
-    agent_id: &str,
-    session_id: &str,
-    prompt: &str,
-) -> Result<(GameCreatorLlmConfig, String, LlmRunRequest), String> {
-    let prompt = prompt.trim();
-    if prompt.is_empty() {
-        return Err("交互内容不能为空".to_string());
-    }
-    if !game_creator_agent_uses_interaction_kernel(agent_id) {
-        return Err(format!("Agent 不启用交互决策层:{agent_id}"));
-    }
-    let (llm, config_path, context) =
-        build_game_creator_role_agent_context_for_session(root, agent_id, Some(session_id))?;
-    let request = build_agent_interaction_llm_request(agent_id, prompt, &context, &llm)?;
-    Ok((llm, config_path, request))
-}
-
-pub(crate) async fn decide_game_creator_agent_interaction_turn_for_session_at(
-    root: &Path,
-    agent_id: &str,
-    session_id: &str,
-    prompt: &str,
-    mut on_delta: F,
-) -> Result
-where
-    F: FnMut(&platform_llm::LlmStreamDelta),
-{
-    let (llm, config_path, request) =
-        build_agent_interaction_request_for_session(root, agent_id, session_id, prompt)?;
-    let api_kind = request.api_kind;
-    let client = build_game_creator_agent_runtime_llm_client(&llm, &config_path)?;
-    let llm_provider = client.config().provider();
-    let request = platform_llm::provider_request_from_llm_request("agent-interaction", request)
-        .map_err(|error| format!("{config_path} Agent interaction Provider 请求无效:{error}"))?;
-    let (registry, target) = platform_llm::build_platform_llm_provider_registry_for_api_kind(
-        AGENT_INTERACTION_PROVIDER_INSTANCE_ID,
-        client,
-        api_kind,
-    )
-    .map_err(|error| format!("{config_path} Agent interaction Provider 注册失败:{error}"))?;
-    let response = if llm.stream {
-        let fallback_request = request.clone();
-        let sink = AgentInteractionProviderStreamSink {
-            on_delta: &mut on_delta,
-        };
-        match registry.stream(&target, request, Box::new(sink)).await {
-            Ok(response) => response,
-            Err(error)
-                if matches!(
-                    error.kind(),
-                    agent_runtime_core::ProviderErrorKind::StreamUnavailable
-                        | agent_runtime_core::ProviderErrorKind::EmptyResponse
-                        | agent_runtime_core::ProviderErrorKind::Deserialize
-                ) =>
-            {
-                registry
-                    .invoke(&target, fallback_request)
-                    .await
-                    .map_err(|fallback_error| {
-                        format!(
-                        "{config_path} Agent interaction 流式协议不可用且普通请求回退失败:流式错误:{error};普通请求错误:{fallback_error}"
-                    )
-                    })?
-            }
-            Err(error) => {
-                return Err(format!(
-                    "{config_path} Agent interaction 调用 LLM 失败:{error}"
-                ));
-            }
-        }
-    } else {
-        registry
-            .invoke(&target, request)
-            .await
-            .map_err(|error| format!("{config_path} Agent interaction 调用 LLM 失败:{error}"))?
-    };
-    let response = platform_llm::llm_response_from_provider_response(llm_provider, response)
-        .map_err(|error| format!("{config_path} Agent interaction Provider 响应无效:{error}"))?;
-    parse_agent_interaction_response(&response)
-}
-
-struct AgentInteractionProviderStreamSink<'a, F> {
-    on_delta: &'a mut F,
-}
-
-impl agent_runtime_core::ProviderStreamSink for AgentInteractionProviderStreamSink<'_, F>
-where
-    F: FnMut(&platform_llm::LlmStreamDelta),
-{
-    fn emit(
-        &mut self,
-        event: agent_runtime_core::ProviderStreamEvent,
-    ) -> Result<(), agent_runtime_core::ProviderError> {
-        if let agent_runtime_core::ProviderStreamEvent::TextDelta {
-            accumulated_text,
-            delta_text,
-            finish_reason,
-        } = event
-        {
-            (self.on_delta)(&platform_llm::LlmStreamDelta {
-                accumulated_text,
-                delta_text,
-                accumulated_reasoning: String::new(),
-                reasoning_delta: String::new(),
-                finish_reason,
-            });
-        }
-        Ok(())
-    }
-}
-
-fn parse_agent_interaction_response(
-    response: &platform_llm::LlmRunResponse,
-) -> Result {
-    if response.tool_calls.len() > 1 {
-        return Err("Agent interaction 一轮最多只能选择一个宿主工具".to_string());
-    }
-    if let Some(call) = response.tool_calls.first() {
-        let registry = agent_interaction_tool_registry()?;
-        let definition = registry
-            .get_by_function_name(&call.name)
-            .ok_or_else(|| format!("Agent interaction 返回未知工具:{}", call.name))?;
-        return match definition.dispatch() {
-            AgentInteractionToolKind::Execute => {
-                let arguments = serde_json::from_str::>(
-                    call.arguments.as_str(),
-                )
-                .map_err(|error| format!("runtime_execute 参数无效:{error}"))?;
-                if !arguments.is_empty() {
-                    return Err("runtime_execute 不接受参数".to_string());
-                }
-                Ok(AgentInteractionAction::Execute)
-            }
-            AgentInteractionToolKind::Resume => {
-                validate_empty_interaction_tool_arguments(call)?;
-                Ok(AgentInteractionAction::Resume)
-            }
-            AgentInteractionToolKind::ProjectLocation => {
-                validate_empty_interaction_tool_arguments(call)?;
-                Ok(AgentInteractionAction::ProjectLocation)
-            }
-        };
-    }
-
-    let text = strip_llm_thinking_blocks(response.text.as_str());
-    if let Some(payload) = extract_json_payload(text.as_str()) {
-        if let Ok(envelope) = serde_json::from_str::(payload) {
-            return match envelope {
-                AgentInteractionTextEnvelope::Reply { reply } => {
-                    let reply = reply.trim();
-                    if reply.is_empty() {
-                        Err("Agent interaction.reply 不能为空".to_string())
-                    } else {
-                        Ok(AgentInteractionAction::Reply(reply.to_string()))
-                    }
-                }
-                AgentInteractionTextEnvelope::Execute => Ok(AgentInteractionAction::Execute),
-                AgentInteractionTextEnvelope::Resume => Ok(AgentInteractionAction::Resume),
-                AgentInteractionTextEnvelope::ProjectLocation => {
-                    Ok(AgentInteractionAction::ProjectLocation)
-                }
-            };
-        }
-    }
-    if text.is_empty() {
-        return Err("Agent interaction 未返回回复或工具调用".to_string());
-    }
-    Ok(AgentInteractionAction::Reply(text))
-}
-
-fn validate_empty_interaction_tool_arguments(
-    call: &platform_llm::LlmToolCall,
-) -> Result<(), String> {
-    let arguments =
-        serde_json::from_str::>(&call.arguments)
-            .map_err(|error| format!("{} 参数无效:{error}", call.name))?;
-    if !arguments.is_empty() {
-        return Err(format!("{} 不接受参数", call.name));
-    }
-    Ok(())
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -523,75 +198,6 @@ mod tests {
         }
     }
 
-    #[test]
-    fn interaction_kernel_is_limited_to_supervisor_and_department_directors() {
-        for agent_id in [
-            GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
-            "design-director",
-            "balance-director",
-            "art-director",
-            "audio-director",
-            "code-director",
-            "publish-strategy",
-        ] {
-            assert!(
-                game_creator_agent_uses_interaction_kernel(agent_id),
-                "{agent_id}"
-            );
-        }
-        for agent_id in ["design-foundation", "art-asset-plan", "code-prototype"] {
-            assert!(
-                !game_creator_agent_uses_interaction_kernel(agent_id),
-                "{agent_id}"
-            );
-        }
-    }
-
-    #[test]
-    fn interaction_response_uses_natural_text_as_direct_reply() {
-        assert_eq!(
-            parse_agent_interaction_response(&response("我是项目总控。", Vec::new())).unwrap(),
-            AgentInteractionAction::Reply("我是项目总控。".to_string())
-        );
-    }
-
-    #[test]
-    fn interaction_response_dispatches_registered_execute_tool() {
-        assert_eq!(
-            parse_agent_interaction_response(&response(
-                "",
-                vec![tool_call(AGENT_INTERACTION_EXECUTE_TOOL, "{}",)],
-            ))
-            .unwrap(),
-            AgentInteractionAction::Execute
-        );
-    }
-
-    #[test]
-    fn interaction_response_supports_text_protocol_for_non_native_provider() {
-        assert_eq!(
-            parse_agent_interaction_response(&response(
-                r#"{"action":"project_location"}"#,
-                Vec::new(),
-            ))
-            .unwrap(),
-            AgentInteractionAction::ProjectLocation
-        );
-    }
-
-    #[test]
-    fn interaction_response_rejects_multiple_host_actions() {
-        let error = parse_agent_interaction_response(&response(
-            "",
-            vec![
-                tool_call(AGENT_INTERACTION_RESUME_TOOL, "{}"),
-                tool_call(AGENT_INTERACTION_PROJECT_LOCATION_TOOL, "{}"),
-            ],
-        ))
-        .expect_err("multiple actions must fail closed");
-        assert!(error.contains("最多只能选择一个"));
-    }
-
     #[test]
     fn steer_decision_replies_without_interrupting_status_questions() {
         let decision = parse_agent_runtime_steer_decision_response(&response(
@@ -625,100 +231,4 @@ mod tests {
         assert!(decision.interrupt_current_provider);
         assert!(decision.reply.contains("改成回合制"));
     }
-
-    #[test]
-    fn interaction_registry_derives_unique_strict_function_tools() {
-        let registry = agent_interaction_tool_registry().expect("interaction registry");
-        let tools = agent_interaction_function_tools().expect("interaction tools");
-        assert_eq!(tools.len(), registry.len());
-        let names = tools
-            .iter()
-            .map(|tool| tool.name.as_str())
-            .collect::>();
-        assert_eq!(names.len(), tools.len());
-        assert!(tools.iter().all(|tool| tool.strict));
-        assert!(tools.iter().all(|tool| {
-            tool.parameters["additionalProperties"] == serde_json::json!(false)
-                && tool.parameters["properties"] == serde_json::json!({})
-        }));
-    }
-
-    #[test]
-    fn interaction_request_crosses_neutral_provider_contract_without_shape_drift() {
-        let mut llm = GameCreatorLlmConfig::default();
-        llm.api_kind = "openai_responses".to_string();
-        llm.reasoning_effort = "max".to_string();
-        let request = build_agent_interaction_llm_request(
-            GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
-            "制作一个三消经营游戏",
-            "",
-            &llm,
-        )
-        .expect("build interaction request");
-        let request = platform_llm::provider_request_from_llm_request("agent-interaction", request)
-            .expect("neutral request");
-        assert_eq!(
-            request.max_output_tokens(),
-            Some(AGENT_INTERACTION_MAX_OUTPUT_TOKENS)
-        );
-        assert_eq!(request.tools().len(), 3);
-        assert_eq!(
-            request.reasoning_effort(),
-            Some(agent_runtime_core::ProviderReasoningEffort::Max)
-        );
-        assert!(request.tools().iter().all(|tool| tool.strict()));
-        assert_eq!(
-            request.tool_choice(),
-            &agent_runtime_core::ProviderToolChoice::Auto
-        );
-    }
-
-    #[test]
-    fn interaction_request_propagates_invalid_reasoning_effort() {
-        let mut llm = GameCreatorLlmConfig::default();
-        llm.reasoning_effort = "maximum".to_string();
-        let error = build_agent_interaction_llm_request(
-            GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
-            "制作一个三消经营游戏",
-            "",
-            &llm,
-        )
-        .expect_err("invalid reasoning effort must fail before Provider request");
-        assert!(error.contains("reasoning_effort"));
-    }
-
-    #[test]
-    fn interaction_stream_sink_preserves_accumulation_delta_and_finish_reason() {
-        let mut observed = Vec::new();
-        let mut callback = |delta: &platform_llm::LlmStreamDelta| observed.push(delta.clone());
-        let mut sink = AgentInteractionProviderStreamSink {
-            on_delta: &mut callback,
-        };
-        agent_runtime_core::ProviderStreamSink::emit(
-            &mut sink,
-            agent_runtime_core::ProviderStreamEvent::TextDelta {
-                accumulated_text: "完成".to_string(),
-                delta_text: "成".to_string(),
-                finish_reason: Some("stop".to_string()),
-            },
-        )
-        .expect("emit");
-        assert_eq!(observed.len(), 1);
-        assert_eq!(observed[0].accumulated_text, "完成");
-        assert_eq!(observed[0].delta_text, "成");
-        assert_eq!(observed[0].finish_reason.as_deref(), Some("stop"));
-    }
-
-    #[test]
-    fn interaction_response_rejects_arguments_for_host_selected_action() {
-        let error = parse_agent_interaction_response(&response(
-            "",
-            vec![tool_call(
-                AGENT_INTERACTION_PROJECT_LOCATION_TOOL,
-                r#"{"path":"/tmp"}"#,
-            )],
-        ))
-        .expect_err("host facts must not accept model-selected arguments");
-        assert!(error.contains("不接受参数"));
-    }
 }
diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol.rs
index f35612757..e73fbaada 100644
--- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol.rs
+++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol.rs
@@ -85,7 +85,7 @@ pub(crate) use run_configuration::{
 pub(crate) use steering::{
     acquire_game_creator_agent_runtime_steer_project_write_lock_with_wait,
     append_game_creator_agent_runtime_steer_decision_failure_reply_at,
-    consume_game_creator_agent_runtime_steers, game_creator_agent_runtime_accepts_steer,
+    consume_game_creator_agent_runtime_steers,
     game_creator_agent_runtime_provider_request_count_for_roots,
     game_creator_agent_runtime_steer_ledger_path,
     interrupt_game_creator_agent_runtime_provider_for_decided_steer_at,
diff --git a/apps/ai-game-creator-shell/src-tauri/src/cli.rs b/apps/ai-game-creator-shell/src-tauri/src/cli.rs
index a38a27d76..75a11a58c 100644
--- a/apps/ai-game-creator-shell/src-tauri/src/cli.rs
+++ b/apps/ai-game-creator-shell/src-tauri/src/cli.rs
@@ -24,13 +24,6 @@ pub(crate) enum CliCommand {
         task: String,
         initialize: bool,
     },
-    SwarmChat {
-        project_path: PathBuf,
-        parent_agent_id: String,
-        initialize: bool,
-        run_profile: String,
-        supervisor_source: &'static str,
-    },
     AgentEnqueue {
         project_path: PathBuf,
         agent_id: String,
@@ -136,7 +129,6 @@ impl CliCommand {
         matches!(
             self,
             Self::AgentTask { .. }
-                | Self::SwarmChat { .. }
                 | Self::AgentEnqueue { .. }
                 | Self::AgentContextCompact { .. }
                 | Self::AgentConfirm { .. }
@@ -175,11 +167,6 @@ impl CliCommand {
                 initialize,
                 ..
             }
-            | Self::SwarmChat {
-                project_path,
-                initialize,
-                ..
-            }
             | Self::AgentEnqueue {
                 project_path,
                 initialize,
@@ -725,49 +712,6 @@ pub(crate) fn parse_cli_command(args: &[String]) -> Result, S
             prompt: prompt.trim().to_string(),
         }));
     }
-    if args.first().map(String::as_str) == Some("--swarm-chat") {
-        const USAGE: &str = "用法:--swarm-chat [--init] [--autonomous-game-build] <本地项目绝对路径> [parentAgentId]";
-        let mut rest = args[1..].to_vec();
-        let initialize = if let Some(index) = rest.iter().position(|arg| arg == "--init") {
-            rest.remove(index);
-            true
-        } else {
-            false
-        };
-        let autonomous_game_build = match rest
-            .iter()
-            .filter(|arg| arg.as_str() == "--autonomous-game-build")
-            .count()
-        {
-            0 => false,
-            1 => {
-                let index = rest
-                    .iter()
-                    .position(|arg| arg == "--autonomous-game-build")
-                    .expect("counted autonomous game build flag");
-                rest.remove(index);
-                true
-            }
-            _ => return Err(USAGE.to_string()),
-        };
-        if !(1..=2).contains(&rest.len()) || rest.iter().any(|value| value.trim().is_empty()) {
-            return Err(USAGE.to_string());
-        }
-        return Ok(Some(CliCommand::SwarmChat {
-            project_path: PathBuf::from(&rest[0]),
-            parent_agent_id: rest
-                .get(1)
-                .map(|value| value.trim().to_string())
-                .unwrap_or_else(|| GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string()),
-            initialize,
-            run_profile: if autonomous_game_build {
-                AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD.to_string()
-            } else {
-                AGENT_RUNTIME_RUN_PROFILE_STANDARD.to_string()
-            },
-            supervisor_source: AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE,
-        }));
-    }
     if args.first().map(String::as_str) == Some("--agent-task") {
         let mut rest = args[1..].to_vec();
         let initialize = if let Some(index) = rest.iter().position(|arg| arg == "--init") {
@@ -1057,7 +1001,7 @@ pub(crate) fn run_cli_command(command: CliCommand) -> Result<(), String> {
             } else if terminal.status == "waiting-for-confirmation" {
                 Err("单 Agent 任务正在等待开发者确认,请在开发窗口继续".to_string())
             } else if terminal.status == "waiting-for-user-input" {
-                Err("单 Agent 任务正在等待用户回答,请使用 agc:chat 继续".to_string())
+                Err("单 Agent 任务正在等待用户回答,请在开发窗口继续".to_string())
             } else {
                 Err(format!(
                     "单 Agent 任务未完成:{} / {}",
@@ -1065,23 +1009,6 @@ pub(crate) fn run_cli_command(command: CliCommand) -> Result<(), String> {
                 ))
             }
         }
-        CliCommand::SwarmChat {
-            project_path,
-            parent_agent_id,
-            initialize,
-            run_profile,
-            supervisor_source,
-        } => {
-            let project_path = canonicalize_cli_path(&project_path, "本地项目路径", initialize)?;
-            require_external_agent_runner_for_cli_runtime_write(&project_path)?;
-            initialize_cli_agent_project(&project_path, initialize)?;
-            run_game_creator_swarm_chat_at(
-                &project_path,
-                &parent_agent_id,
-                &run_profile,
-                supervisor_source,
-            )
-        }
         CliCommand::AgentEnqueue {
             project_path,
             agent_id,
@@ -1954,34 +1881,6 @@ mod tests {
         assert!(read_cli_agent_goal_payload(&mut oversized).is_err());
     }
 
-    #[test]
-    fn parses_swarm_chat_and_requires_external_config_dir() {
-        let project_path = std::env::current_dir().expect("current directory");
-        let mut command = parse_cli_command(&[
-            "--swarm-chat".to_string(),
-            "--init".to_string(),
-            project_path.display().to_string(),
-            "code-prototype".to_string(),
-        ])
-        .expect("parse swarm chat")
-        .expect("swarm chat command");
-
-        assert_eq!(
-            command,
-            CliCommand::SwarmChat {
-                project_path,
-                parent_agent_id: "code-prototype".to_string(),
-                initialize: true,
-                run_profile: AGENT_RUNTIME_RUN_PROFILE_STANDARD.to_string(),
-                supervisor_source: AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE,
-            }
-        );
-        assert!(command.requires_external_agent_runner());
-        let error = prepare_cli_command_paths(&mut command, None)
-            .expect_err("swarm chat must require config dir");
-        assert!(error.contains("--config-dir"));
-    }
-
     #[test]
     fn parses_idle_runner_shutdown_without_starting_a_new_runner() {
         let mut command = parse_cli_command(&["--runner-shutdown-if-idle".to_string()])
@@ -2035,68 +1934,4 @@ mod tests {
         ])
         .is_err());
     }
-
-    #[test]
-    fn swarm_chat_defaults_to_project_supervisor() {
-        let project_path = std::env::current_dir().expect("current directory");
-        let command = parse_cli_command(&[
-            "--swarm-chat".to_string(),
-            project_path.display().to_string(),
-        ])
-        .expect("parse supervisor chat")
-        .expect("supervisor chat command");
-
-        assert_eq!(
-            command,
-            CliCommand::SwarmChat {
-                project_path,
-                parent_agent_id: GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string(),
-                initialize: false,
-                run_profile: AGENT_RUNTIME_RUN_PROFILE_STANDARD.to_string(),
-                supervisor_source: AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE,
-            }
-        );
-    }
-
-    #[test]
-    fn swarm_chat_autonomous_game_build_flag_selects_autonomous_profile() {
-        let project_path = std::env::current_dir().expect("current directory");
-        let command = parse_cli_command(&[
-            "--swarm-chat".to_string(),
-            project_path.display().to_string(),
-            "--autonomous-game-build".to_string(),
-        ])
-        .expect("parse autonomous supervisor chat")
-        .expect("autonomous supervisor chat command");
-
-        assert_eq!(
-            command,
-            CliCommand::SwarmChat {
-                project_path,
-                parent_agent_id: GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string(),
-                initialize: false,
-                run_profile: AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD.to_string(),
-                supervisor_source: AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE,
-            }
-        );
-    }
-
-    #[test]
-    fn swarm_chat_rejects_missing_or_extra_arguments() {
-        assert!(parse_cli_command(&["--swarm-chat".to_string()]).is_err());
-        assert!(parse_cli_command(&[
-            "--swarm-chat".to_string(),
-            "/tmp/game-project".to_string(),
-            "code-prototype".to_string(),
-            "extra".to_string(),
-        ])
-        .is_err());
-        assert!(parse_cli_command(&[
-            "--swarm-chat".to_string(),
-            "--autonomous-game-build".to_string(),
-            "--autonomous-game-build".to_string(),
-            "/tmp/game-project".to_string(),
-        ])
-        .is_err());
-    }
 }
diff --git a/apps/ai-game-creator-shell/src-tauri/src/commands.rs b/apps/ai-game-creator-shell/src-tauri/src/commands.rs
index fa4163a4d..4c60b2b7c 100644
--- a/apps/ai-game-creator-shell/src-tauri/src/commands.rs
+++ b/apps/ai-game-creator-shell/src-tauri/src/commands.rs
@@ -1345,6 +1345,9 @@ pub(crate) async fn chat_with_game_creator_role_agent_stream(
 }
 
 #[tauri::command]
+#[allow(dead_code)]
+// Tauri IPC 入口:前端暂无调用方,内部实现 `*_at` 仍被 `--agent-steer`、goal 与测试使用。
+// 保留注册以维持既有 App IPC 表面;重接前端入口或删除属于单独的 native 能力取舍。
 pub(crate) fn start_game_creator_agent_runtime_task(
     project_path: String,
     agent_id: String,
@@ -1547,6 +1550,9 @@ pub(crate) fn clear_game_creator_agent_goal(
 }
 
 #[tauri::command]
+#[allow(dead_code)]
+// Tauri IPC 入口:前端暂无调用方,内部实现 `*_at` 仍被 `--agent-steer`、goal 与测试使用。
+// 保留注册以维持既有 App IPC 表面;重接前端入口或删除属于单独的 native 能力取舍。
 pub(crate) async fn steer_game_creator_agent_runtime_task(
     project_path: String,
     agent_id: String,
@@ -5301,16 +5307,6 @@ pub(crate) fn open_canvas_project(
     Ok(OpenCanvasProjectResult { url })
 }
 
-#[tauri::command]
-pub(crate) fn get_game_creation_agent_capabilities() -> Vec {
-    GAME_CREATION_AGENT_CAPABILITIES.to_vec()
-}
-
-#[tauri::command]
-pub(crate) fn get_limited_local_commands() -> Vec {
-    GAME_CREATION_APP_LIMITED_RUN_COMMANDS.to_vec()
-}
-
 #[tauri::command]
 pub(crate) fn run_limited_local_command(
     project_path: String,
diff --git a/apps/ai-game-creator-shell/src-tauri/src/main.rs b/apps/ai-game-creator-shell/src-tauri/src/main.rs
index 151a18cfe..f151f543d 100644
--- a/apps/ai-game-creator-shell/src-tauri/src/main.rs
+++ b/apps/ai-game-creator-shell/src-tauri/src/main.rs
@@ -36,20 +36,18 @@ use shared_contracts::game_creation_app::{
     game_creation_app_asset_effective_category, new_game_creation_app_manifest,
     new_game_creation_app_seed_tasks, normalize_game_creation_app_asset_tags,
     validate_game_iteration_versions, GameCreationAgentArtifactTrace,
-    GameCreationAgentCapabilityDescriptor, GameCreationAgentPassPlanTrace,
-    GameCreationAgentRepairRouteTrace, GameCreationAgentRunStep,
+    GameCreationAgentPassPlanTrace, GameCreationAgentRepairRouteTrace, GameCreationAgentRunStep,
     GameCreationAgentRunTaskGraphTrace, GameCreationAgentRunTrace, GameCreationAgentToolCallTrace,
     GameCreationAppAgentGroup, GameCreationAppAssetKind, GameCreationAppAssetManifestEntry,
     GameCreationAppAssetSource, GameCreationAppAssetSourceKind, GameCreationAppCommandRunState,
-    GameCreationAppCommandRunStatus, GameCreationAppLimitedRunCommandDescriptor,
-    GameCreationAppManifest, GameCreationAppPermission, GameCreationAppPreviewState,
-    GameCreationAppPreviewStatus, GameCreationAppTaskState, GameCreationAppTaskStatus,
-    GameIterationVersion, GameIterationVersionCreatedReason, GameIterationVersionResourceBinding,
-    ProjectResourceCanvasLayout, ProjectResourceCanvasLayoutMode, ProjectResourceCanvasPosition,
+    GameCreationAppCommandRunStatus, GameCreationAppManifest, GameCreationAppPermission,
+    GameCreationAppPreviewState, GameCreationAppPreviewStatus, GameCreationAppTaskState,
+    GameCreationAppTaskStatus, GameIterationVersion, GameIterationVersionCreatedReason,
+    GameIterationVersionResourceBinding, ProjectResourceCanvasLayout,
+    ProjectResourceCanvasLayoutMode, ProjectResourceCanvasPosition,
     UpdateProjectResourceCanvasLayoutResult, UpdateProjectResourceCanvasLayoutStatus,
-    GAME_CREATION_AGENT_CAPABILITIES, GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
-    GAME_CREATION_AGENT_TOOL_CALL_MAX, GAME_CREATION_APP_COMMANDS,
-    GAME_CREATION_APP_LIMITED_RUN_COMMANDS, GAME_CREATION_RESOURCE_LAYOUT_SCHEMA_VERSION,
+    GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, GAME_CREATION_AGENT_TOOL_CALL_MAX,
+    GAME_CREATION_APP_COMMANDS, GAME_CREATION_RESOURCE_LAYOUT_SCHEMA_VERSION,
 };
 // `Emitter` 同时被 `use super::*` 的子模块依赖(通知、Agent 事件等都从 crate 根取该 trait),
 // 不要因为根模块自身不再直接 `.emit(..)` 就删掉它。
@@ -151,7 +149,6 @@ mod repository_context;
 mod resource_inspect;
 mod resource_preview_scheduler;
 mod runner;
-mod swarm_cli;
 mod template_library;
 mod tool_plan_handoff;
 mod user_input;
@@ -193,7 +190,6 @@ use repository_context::*;
 use resource_inspect::*;
 use resource_preview_scheduler::*;
 use runner::*;
-use swarm_cli::*;
 use template_library::*;
 use user_input::*;
 use windows::*;
@@ -2682,8 +2678,6 @@ fn main() {
             start_local_project_asset_generation,
             list_local_project_asset_generations,
             open_canvas_project,
-            get_game_creation_agent_capabilities,
-            get_limited_local_commands,
             run_limited_local_command,
             append_local_permission_log,
             list_local_project_files,
diff --git a/apps/ai-game-creator-shell/src-tauri/src/swarm_cli.rs b/apps/ai-game-creator-shell/src-tauri/src/swarm_cli.rs
deleted file mode 100644
index 9bbdae1bb..000000000
--- a/apps/ai-game-creator-shell/src-tauri/src/swarm_cli.rs
+++ /dev/null
@@ -1,72 +0,0 @@
-use super::*;
-use std::collections::{BTreeMap, BTreeSet};
-use std::io::{BufRead, Write};
-use std::sync::mpsc::{self, Receiver, RecvTimeoutError};
-use std::time::{Duration, Instant};
-
-mod commands;
-mod conversation;
-mod goal_commands;
-mod input;
-mod observer;
-mod report;
-mod terminal_classification;
-mod turn_dispatch;
-mod turn_wait;
-
-use commands::*;
-use conversation::*;
-use goal_commands::*;
-use input::*;
-use observer::*;
-use report::*;
-use terminal_classification::*;
-use turn_dispatch::*;
-use turn_wait::*;
-
-#[cfg(test)]
-mod tests;
-
-pub(crate) fn run_game_creator_swarm_chat_at(
-    root: &Path,
-    parent_agent_id: &str,
-    run_profile: &str,
-    supervisor_source: &'static str,
-) -> Result<(), String> {
-    let (input_tx, input_rx) = mpsc::channel();
-    std::thread::spawn(move || {
-        let stdin = std::io::stdin();
-        let mut input = stdin.lock();
-        loop {
-            let mut line = String::new();
-            match input.read_line(&mut line) {
-                Ok(0) => {
-                    let _ = input_tx.send(SwarmInputEvent::Eof);
-                    break;
-                }
-                Ok(_) => {
-                    if input_tx
-                        .send(SwarmInputEvent::Line(line.trim().to_string()))
-                        .is_err()
-                    {
-                        break;
-                    }
-                }
-                Err(error) => {
-                    let _ = input_tx.send(SwarmInputEvent::Error(error.to_string()));
-                    break;
-                }
-            }
-        }
-    });
-    let stdout = std::io::stdout();
-    let mut output = stdout.lock();
-    run_game_creator_swarm_chat_with_input(
-        root,
-        parent_agent_id,
-        run_profile,
-        supervisor_source,
-        &input_rx,
-        &mut output,
-    )
-}
diff --git a/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/commands.rs b/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/commands.rs
deleted file mode 100644
index 4593c8f11..000000000
--- a/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/commands.rs
+++ /dev/null
@@ -1,79 +0,0 @@
-use super::*;
-
-pub(super) fn print_swarm_agents(root: &Path, output: &mut W) -> Result<(), String> {
-    writeln!(output, "静态 Agent:").map_err(|error| format!("写入终端失败:{error}"))?;
-    for group in GAME_CREATOR_AGENT_GROUP_DEFINITIONS {
-        for role in group.roles {
-            writeln!(
-                output,
-                "- {} / {}: {} ({})",
-                group.label, role.role, role.task_id, role.id
-            )
-            .map_err(|error| format!("写入终端失败:{error}"))?;
-        }
-    }
-    let dynamic = read_game_creator_agent_runtimes_at(root)?
-        .into_iter()
-        .filter(|runtime| runtime.state.agent_id.starts_with("child-"))
-        .collect::>();
-    if !dynamic.is_empty() {
-        writeln!(output, "动态隔离 Agent:").map_err(|error| format!("写入终端失败:{error}"))?;
-        for runtime in dynamic {
-            writeln!(
-                output,
-                "- {} <- {} run={} delegation={} status={}/{}",
-                runtime.state.agent_id,
-                runtime
-                    .state
-                    .parent_agent_id
-                    .as_deref()
-                    .unwrap_or("unknown"),
-                runtime.state.run_id,
-                runtime.state.delegation_id.as_deref().unwrap_or("unknown"),
-                runtime.state.status,
-                runtime.state.phase
-            )
-            .map_err(|error| format!("写入终端失败:{error}"))?;
-        }
-    }
-    Ok(())
-}
-
-pub(super) fn print_swarm_status(root: &Path, output: &mut W) -> Result<(), String> {
-    let runtimes = read_game_creator_agent_runtimes_at(root)?;
-    for runtime in runtimes.iter().filter(|runtime| {
-        !runtime.state.run_id.is_empty()
-            || runtime.task_queue.pending > 0
-            || runtime.task_queue.running > 0
-            || runtime.task_queue.waiting_for_confirmation > 0
-            || runtime.task_queue.waiting_for_user_input > 0
-    }) {
-        print_runtime_state(&runtime.state, &runtime.task_queue, output)?;
-        print_runtime_response_stream_status(runtime.response_stream.as_ref(), output)?;
-    }
-    if runtimes
-        .iter()
-        .all(|runtime| runtime.state.run_id.is_empty())
-    {
-        writeln!(output, "当前没有 Agent Runtime 记录。")
-            .map_err(|error| format!("写入终端失败:{error}"))?;
-    }
-    Ok(())
-}
-
-pub(super) fn print_runtime_response_stream_status(
-    stream: Option<&AgentRuntimeResponseStream>,
-    output: &mut W,
-) -> Result<(), String> {
-    let Some(stream) = stream else {
-        return Ok(());
-    };
-    writeln!(
-        output,
-        "[回复流] status={} sequence={} chars={}",
-        stream.status,
-        stream.sequence,
-        stream.accumulated_text.chars().count()
-    )
-    .map_err(|error| format!("写入终端失败:{error}"))
-}
diff --git a/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/conversation.rs b/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/conversation.rs
deleted file mode 100644
index a2afff4c8..000000000
--- a/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/conversation.rs
+++ /dev/null
@@ -1,337 +0,0 @@
-use super::*;
-
-pub(super) const SWARM_CHAT_HISTORY_LIMIT: usize = 50;
-
-#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
-pub(super) struct SwarmTurnConversationMetrics {
-    pub(super) new_assistant_message_count: usize,
-    pub(super) final_reply_chars: usize,
-}
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub(super) struct SwarmRecoveredAssistant {
-    pub(super) run_id: String,
-    pub(super) finalization_id: String,
-    pub(super) message_id: String,
-    pub(super) content: String,
-}
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub(super) struct SwarmTurnConversationBaseline {
-    pub(super) previous_message_count: usize,
-    pub(super) parent_run_id: String,
-    pub(super) recovered_assistant: Option,
-}
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub(super) struct SwarmTurnConversationSnapshot {
-    pub(super) metrics: SwarmTurnConversationMetrics,
-    pub(super) final_reply: Option,
-    pub(super) recovered_before_observation: bool,
-}
-
-pub(super) fn handle_swarm_context_compaction(
-    root: &Path,
-    parent_agent_id: &str,
-    output: &mut W,
-) -> Result<(), String> {
-    let conversation = read_local_conversation_for_session_at(root, Some(parent_agent_id), None)?;
-    match compact_external_agent_runner_context(
-        root,
-        parent_agent_id,
-        conversation.session_id.as_deref(),
-    ) {
-        Ok(result) => writeln!(
-            output,
-            "[上下文压缩] revision={} reused={} estimated={}->{} covered={}/{}/{}",
-            result.revision,
-            result.reused,
-            result.estimated_tokens_before,
-            result.estimated_tokens_after,
-            result.covered_agent_messages,
-            result.covered_project_messages,
-            result.covered_observations,
-        ),
-        Err(error) => writeln!(output, "[上下文压缩失败] {error}"),
-    }
-    .map_err(|error| format!("写入终端失败:{error}"))
-}
-
-pub(super) fn print_conversation_history(
-    root: &Path,
-    parent_agent_id: &str,
-    output: &mut W,
-) -> Result<(), String> {
-    let conversation = read_local_conversation_for_session_at(root, Some(parent_agent_id), None)?;
-    writeln!(
-        output,
-        "[历史] session={} messages={}",
-        conversation.session_id.as_deref().unwrap_or("unknown"),
-        conversation.messages.len()
-    )
-    .map_err(|error| format!("写入终端失败:{error}"))?;
-    let start = conversation
-        .messages
-        .len()
-        .saturating_sub(SWARM_CHAT_HISTORY_LIMIT);
-    for message in &conversation.messages[start..] {
-        let label = if message.role == "assistant" {
-            "Agent"
-        } else if message.role == "user" {
-            "你"
-        } else {
-            message.role.as_str()
-        };
-        writeln!(output, "{label}> {}", message.content)
-            .map_err(|error| format!("写入终端失败:{error}"))?;
-    }
-    Ok(())
-}
-
-pub(super) fn new_swarm_turn_conversation_baseline(
-    previous_message_count: usize,
-    parent_run_id: impl Into,
-) -> SwarmTurnConversationBaseline {
-    SwarmTurnConversationBaseline {
-        previous_message_count,
-        parent_run_id: parent_run_id.into(),
-        recovered_assistant: None,
-    }
-}
-
-pub(super) fn capture_recovered_swarm_assistant_at(
-    root: &Path,
-    parent_agent_id: &str,
-    session_id: &str,
-    baseline: &mut SwarmTurnConversationBaseline,
-) -> Result<(), String> {
-    if baseline.parent_run_id.trim().is_empty() || baseline.recovered_assistant.is_some() {
-        return Ok(());
-    }
-    let conversation =
-        read_local_conversation_for_session_at(root, Some(parent_agent_id), Some(session_id))?;
-    if baseline.previous_message_count > conversation.messages.len() {
-        return Err("Swarm turn 对话 baseline 超出当前 Session 消息数".to_string());
-    }
-    if baseline.previous_message_count < conversation.messages.len() {
-        return Ok(());
-    }
-    let Some(journal) = read_game_creator_agent_runtime_finalization_journal(
-        root,
-        parent_agent_id,
-        &baseline.parent_run_id,
-    )?
-    else {
-        return Ok(());
-    };
-    if journal.agent_id != parent_agent_id
-        || journal.session_id != session_id
-        || journal.run_id != baseline.parent_run_id
-    {
-        return Err("Swarm 恢复 finalization 与目标 parent Session/run 不匹配".to_string());
-    }
-    if !game_creator_agent_runtime_finalization_assistant_exists(root, &journal)? {
-        return Ok(());
-    }
-    baseline.recovered_assistant = Some(SwarmRecoveredAssistant {
-        run_id: journal.run_id,
-        finalization_id: journal.finalization_id,
-        message_id: journal.message_id,
-        content: journal.response,
-    });
-    Ok(())
-}
-
-pub(super) fn read_turn_conversation_snapshot(
-    root: &Path,
-    parent_agent_id: &str,
-    session_id: &str,
-    baseline: &SwarmTurnConversationBaseline,
-) -> Result {
-    let conversation =
-        read_local_conversation_for_session_at(root, Some(parent_agent_id), Some(session_id))?;
-    if baseline.previous_message_count > conversation.messages.len() {
-        return Err("Swarm turn 对话 baseline 超出当前 Session 消息数".to_string());
-    }
-    let target_message_id = game_creator_agent_runtime_finalization_message_id(
-        parent_agent_id,
-        session_id,
-        &baseline.parent_run_id,
-    );
-    if let Some(journal) = read_game_creator_agent_runtime_finalization_journal(
-        root,
-        parent_agent_id,
-        &baseline.parent_run_id,
-    )? {
-        if journal.agent_id != parent_agent_id
-            || journal.session_id != session_id
-            || journal.run_id != baseline.parent_run_id
-            || journal.message_id != target_message_id
-        {
-            return Err("Swarm finalization 与目标 parent Session/run 不匹配".to_string());
-        }
-    }
-    let (mut metrics, final_reply) = summarize_scoped_new_assistant_messages(
-        conversation
-            .messages
-            .iter()
-            .skip(baseline.previous_message_count)
-            .map(|message| {
-                (
-                    message.role.as_str(),
-                    message.content.as_str(),
-                    message.message_id.as_deref(),
-                )
-            }),
-        Some(&target_message_id),
-    );
-    let mut final_reply = final_reply.map(str::to_string);
-    let recovered_before_observation = baseline.recovered_assistant.is_some();
-    if let Some(recovered) = baseline.recovered_assistant.as_ref() {
-        if recovered.run_id != baseline.parent_run_id
-            || recovered.finalization_id.trim().is_empty()
-            || recovered.message_id.trim().is_empty()
-        {
-            return Err("Swarm 恢复 assistant 身份不完整".to_string());
-        }
-        if metrics.new_assistant_message_count != 0 {
-            return Err("Swarm 恢复 assistant 与 baseline 后的新回复重叠".to_string());
-        }
-        metrics.new_assistant_message_count = metrics.new_assistant_message_count.saturating_add(1);
-        if final_reply.is_none() {
-            metrics.final_reply_chars = recovered.content.chars().count();
-            final_reply = Some(recovered.content.clone());
-        }
-    }
-    Ok(SwarmTurnConversationSnapshot {
-        metrics,
-        final_reply,
-        recovered_before_observation,
-    })
-}
-
-pub(super) fn read_turn_conversation_metrics(
-    root: &Path,
-    parent_agent_id: &str,
-    session_id: &str,
-    baseline: &SwarmTurnConversationBaseline,
-) -> Result {
-    read_turn_conversation_snapshot(root, parent_agent_id, session_id, baseline)
-        .map(|snapshot| snapshot.metrics)
-}
-
-pub(super) fn summarize_new_assistant_messages<'a>(
-    messages: impl IntoIterator,
-) -> (SwarmTurnConversationMetrics, Option<&'a str>) {
-    summarize_scoped_new_assistant_messages(
-        messages
-            .into_iter()
-            .map(|(role, content)| (role, content, None)),
-        None,
-    )
-}
-
-pub(super) fn summarize_scoped_new_assistant_messages<'a>(
-    messages: impl IntoIterator)>,
-    target_message_id: Option<&str>,
-) -> (SwarmTurnConversationMetrics, Option<&'a str>) {
-    let mut scoped_count = 0;
-    let mut scoped_reply = None;
-    let mut legacy_count = 0;
-    let mut legacy_reply = None;
-    let mut identified_assistant_exists = false;
-    for (role, content, message_id) in messages {
-        if role != "assistant" {
-            continue;
-        }
-        match message_id {
-            Some(message_id) if target_message_id == Some(message_id) => {
-                identified_assistant_exists = true;
-                scoped_count += 1;
-                scoped_reply = Some(content);
-            }
-            None => {
-                legacy_count += 1;
-                legacy_reply = Some(content);
-            }
-            Some(_) => identified_assistant_exists = true,
-        }
-    }
-    let (new_assistant_message_count, final_reply) =
-        if target_message_id.is_some() && (scoped_count > 0 || identified_assistant_exists) {
-            (scoped_count, scoped_reply)
-        } else {
-            (legacy_count, legacy_reply)
-        };
-    (
-        SwarmTurnConversationMetrics {
-            new_assistant_message_count,
-            final_reply_chars: final_reply.map_or(0, |reply| reply.chars().count()),
-        },
-        final_reply,
-    )
-}
-
-pub(super) fn print_new_parent_reply(
-    root: &Path,
-    parent_agent_id: &str,
-    session_id: &str,
-    baseline: &SwarmTurnConversationBaseline,
-    output: &mut W,
-    observer: &mut SwarmRuntimeObserver,
-) -> Result {
-    let snapshot = read_turn_conversation_snapshot(root, parent_agent_id, session_id, baseline)?;
-    observer.close_response_line(output)?;
-    if snapshot.recovered_before_observation {
-        writeln!(output, "[本轮结束] 父 Agent 回复已在恢复前持久化。")
-            .map_err(|error| format!("写入终端失败:{error}"))?;
-    } else {
-        print_settled_parent_reply_for_run(
-            parent_agent_id,
-            session_id,
-            Some(&baseline.parent_run_id),
-            snapshot.final_reply.as_deref(),
-            observer,
-            output,
-        )?;
-    }
-    Ok(snapshot.metrics)
-}
-
-pub(super) fn print_settled_parent_reply(
-    parent_agent_id: &str,
-    session_id: &str,
-    reply: Option<&str>,
-    observer: &SwarmRuntimeObserver,
-    output: &mut W,
-) -> Result<(), String> {
-    print_settled_parent_reply_for_run(parent_agent_id, session_id, None, reply, observer, output)
-}
-
-pub(super) fn print_settled_parent_reply_for_run(
-    parent_agent_id: &str,
-    session_id: &str,
-    target_run_id: Option<&str>,
-    reply: Option<&str>,
-    observer: &SwarmRuntimeObserver,
-    output: &mut W,
-) -> Result<(), String> {
-    let Some(reply) = reply else {
-        return writeln!(output, "[本轮结束] 父 Agent 未产生新的最终回复。")
-            .map_err(|error| format!("写入终端失败:{error}"));
-    };
-    let stream_belongs_to_target = target_run_id.is_none_or(|target_run_id| {
-        observer
-            .response_streams
-            .get(parent_agent_id)
-            .is_some_and(|cursor| cursor.identity.run_id == target_run_id)
-    });
-    if stream_belongs_to_target
-        && observer.parent_reply_was_fully_streamed(parent_agent_id, session_id, reply)
-    {
-        writeln!(output, "[本轮结束] 父 Agent 回复已完整流式输出。")
-            .map_err(|error| format!("写入终端失败:{error}"))
-    } else {
-        writeln!(output, "\nAgent> {reply}").map_err(|error| format!("写入终端失败:{error}"))
-    }
-}
diff --git a/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/goal_commands.rs b/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/goal_commands.rs
deleted file mode 100644
index 4f3e6c5ce..000000000
--- a/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/goal_commands.rs
+++ /dev/null
@@ -1,189 +0,0 @@
-use super::*;
-
-#[derive(Debug, Eq, PartialEq)]
-pub(super) enum SwarmGoalCommand {
-    Status,
-    Start(String),
-    Edit(String),
-    Pause,
-    Resume,
-    Clear,
-}
-
-#[derive(Debug, Eq, PartialEq)]
-pub(super) struct SwarmGoalObservation {
-    pub(super) session_id: String,
-    pub(super) run_id: String,
-    pub(super) previous_message_count: usize,
-}
-
-pub(super) fn handle_swarm_goal_command(
-    root: &Path,
-    parent_agent_id: &str,
-    command: SwarmGoalCommand,
-    output: &mut W,
-) -> Result, String> {
-    match execute_swarm_goal_command(root, parent_agent_id, command, output) {
-        Ok(observation) => Ok(observation),
-        Err(error) => {
-            print_swarm_goal_error(output, &error)?;
-            Ok(None)
-        }
-    }
-}
-
-pub(super) fn execute_swarm_goal_command(
-    root: &Path,
-    parent_agent_id: &str,
-    command: SwarmGoalCommand,
-    output: &mut W,
-) -> Result, String> {
-    let conversation = read_local_conversation_for_session_at(root, Some(parent_agent_id), None)?;
-    let session_id = conversation
-        .session_id
-        .clone()
-        .ok_or_else(|| "父 Agent 当前 Session 缺失".to_string())?;
-    let previous_message_count = conversation.messages.len();
-    let current_goal = read_game_creator_agent_goal_at(root, parent_agent_id, &session_id)?;
-    let project_path = root.display().to_string();
-
-    match command {
-        SwarmGoalCommand::Status => {
-            print_swarm_goal_status(&session_id, current_goal.as_ref(), output)?;
-            Ok(None)
-        }
-        SwarmGoalCommand::Start(outcome) => {
-            let requested_run_id = format!("swarm-goal-{parent_agent_id}-{}", unix_millis());
-            let result = start_game_creator_agent_goal(
-                project_path,
-                parent_agent_id.to_string(),
-                Some(session_id.clone()),
-                outcome.clone(),
-                Vec::new(),
-                vec![outcome],
-                requested_run_id,
-            )?;
-            print_swarm_goal_mutation("已启动", &result, output)?;
-            Ok(Some(SwarmGoalObservation {
-                session_id,
-                run_id: result.goal.run_id.clone(),
-                previous_message_count,
-            }))
-        }
-        SwarmGoalCommand::Edit(outcome) => {
-            let goal = current_goal.ok_or_else(|| "当前 Session 没有 Agent Goal".to_string())?;
-            let result = edit_game_creator_agent_goal(
-                project_path,
-                parent_agent_id.to_string(),
-                session_id.clone(),
-                goal.goal_id,
-                goal.revision,
-                outcome.clone(),
-                Vec::new(),
-                vec![outcome],
-            )?;
-            print_swarm_goal_mutation("已编辑", &result, output)?;
-            Ok(
-                (result.goal.status == AGENT_GOAL_STATUS_ACTIVE).then_some(SwarmGoalObservation {
-                    session_id,
-                    run_id: result.goal.run_id.clone(),
-                    previous_message_count,
-                }),
-            )
-        }
-        SwarmGoalCommand::Pause => {
-            let goal = current_goal.ok_or_else(|| "当前 Session 没有 Agent Goal".to_string())?;
-            let result = pause_game_creator_agent_goal(
-                project_path,
-                parent_agent_id.to_string(),
-                session_id,
-                goal.goal_id,
-                goal.revision,
-            )?;
-            print_swarm_goal_mutation("已暂停", &result, output)?;
-            Ok(None)
-        }
-        SwarmGoalCommand::Resume => {
-            let goal = current_goal.ok_or_else(|| "当前 Session 没有 Agent Goal".to_string())?;
-            let result = resume_game_creator_agent_goal(
-                project_path,
-                parent_agent_id.to_string(),
-                session_id.clone(),
-                goal.goal_id,
-                goal.revision,
-            )?;
-            print_swarm_goal_mutation("已恢复", &result, output)?;
-            Ok(Some(SwarmGoalObservation {
-                session_id,
-                run_id: result.goal.run_id.clone(),
-                previous_message_count,
-            }))
-        }
-        SwarmGoalCommand::Clear => {
-            let goal = current_goal.ok_or_else(|| "当前 Session 没有 Agent Goal".to_string())?;
-            let result = clear_game_creator_agent_goal(
-                project_path,
-                parent_agent_id.to_string(),
-                session_id,
-                goal.goal_id,
-                goal.revision,
-            )?;
-            print_swarm_goal_mutation("已清理", &result, output)?;
-            Ok(None)
-        }
-    }
-}
-
-pub(super) fn print_swarm_goal_status(
-    session_id: &str,
-    goal: Option<&AgentGoalRecord>,
-    output: &mut W,
-) -> Result<(), String> {
-    let Some(goal) = goal else {
-        return writeln!(output, "[Goal] session={session_id} 当前尚未设置持久目标。")
-            .map_err(|error| format!("写入终端失败:{error}"));
-    };
-    writeln!(
-        output,
-        "[Goal] session={} goal={} run={} revision={} status={}",
-        goal.session_id, goal.goal_id, goal.run_id, goal.revision, goal.status
-    )
-    .and_then(|_| writeln!(output, "[Goal 目标] {}", goal.outcome))
-    .map_err(|error| format!("写入终端失败:{error}"))?;
-    for constraint in &goal.constraints {
-        writeln!(output, "[Goal 约束] {constraint}")
-            .map_err(|error| format!("写入终端失败:{error}"))?;
-    }
-    for verification in &goal.verification {
-        writeln!(output, "[Goal 完成标准] {verification}")
-            .map_err(|error| format!("写入终端失败:{error}"))?;
-    }
-    if let Some(error) = goal.error.as_deref() {
-        writeln!(output, "[Goal 错误] {error}")
-            .map_err(|write_error| format!("写入终端失败:{write_error}"))?;
-    }
-    Ok(())
-}
-
-pub(super) fn print_swarm_goal_mutation(
-    action: &str,
-    result: &AgentGoalMutationResult,
-    output: &mut W,
-) -> Result<(), String> {
-    writeln!(
-        output,
-        "[Goal {action}] goal={} run={} revision={} status={} providerInterrupted={}",
-        result.goal.goal_id,
-        result.goal.run_id,
-        result.goal.revision,
-        result.goal.status,
-        result.provider_interrupted
-    )
-    .map_err(|error| format!("写入终端失败:{error}"))?;
-    print_swarm_goal_status(&result.goal.session_id, Some(&result.goal), output)
-}
-
-pub(super) fn print_swarm_goal_error(output: &mut W, error: &str) -> Result<(), String> {
-    writeln!(output, "[Goal 失败] {error}")
-        .map_err(|write_error| format!("写入终端失败:{write_error}"))
-}
diff --git a/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/input.rs b/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/input.rs
deleted file mode 100644
index fdcc3ba13..000000000
--- a/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/input.rs
+++ /dev/null
@@ -1,390 +0,0 @@
-use super::*;
-
-#[derive(Debug, Eq, PartialEq)]
-pub(super) enum SwarmChatInput {
-    Help,
-    Agents,
-    Status,
-    History,
-    Compact,
-    Goal(SwarmGoalCommand),
-    InvalidGoal(String),
-    Resume,
-    Quit,
-    Message(String),
-}
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-pub(super) enum SwarmChatFlow {
-    Continue,
-    Exit,
-}
-
-pub(super) enum SwarmInputEvent {
-    Line(String),
-    Eof,
-    Error(String),
-}
-
-pub(super) enum SwarmPromptDecision {
-    Approve,
-    Reject,
-    Deferred,
-    InputClosed,
-    Quit,
-}
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-pub(super) enum SwarmNewRunLaunch<'a> {
-    ProjectSupervisor {
-        source: &'static str,
-        run_profile: &'a str,
-    },
-    ExplicitParentDebug,
-}
-
-impl SwarmNewRunLaunch<'_> {
-    pub(super) fn expected_parent_source(self) -> Option<&'static str> {
-        match self {
-            Self::ProjectSupervisor { .. } | Self::ExplicitParentDebug => None,
-        }
-    }
-}
-
-pub(super) fn resolve_swarm_new_run_launch<'a>(
-    parent_agent_id: &str,
-    run_profile: &'a str,
-    supervisor_source: &'static str,
-) -> Result, String> {
-    if !matches!(
-        run_profile,
-        AGENT_RUNTIME_RUN_PROFILE_STANDARD | AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
-    ) {
-        return Err(format!("不支持的 Agent Runtime Run Profile:{run_profile}"));
-    }
-    if parent_agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID {
-        if supervisor_source != AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE {
-            return Err(format!(
-                "不支持的 Project Supervisor source:{supervisor_source}"
-            ));
-        }
-        return Ok(SwarmNewRunLaunch::ProjectSupervisor {
-            source: supervisor_source,
-            run_profile,
-        });
-    }
-    if supervisor_source != AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE {
-        return Err(format!(
-            "受限 Supervisor source 仅支持 project-supervisor 总控入口:{supervisor_source}"
-        ));
-    }
-    if run_profile != AGENT_RUNTIME_RUN_PROFILE_STANDARD {
-        return Err("--autonomous-game-build 仅支持 project-supervisor 总控入口".to_string());
-    }
-    Ok(SwarmNewRunLaunch::ExplicitParentDebug)
-}
-
-pub(super) fn run_game_creator_swarm_chat_with_input(
-    root: &Path,
-    parent_agent_id: &str,
-    run_profile: &str,
-    supervisor_source: &'static str,
-    input: &Receiver,
-    output: &mut W,
-) -> Result<(), String> {
-    let parent_agent_id = parent_agent_id.trim();
-    if parent_agent_id.is_empty() {
-        return Err("parentAgentId 不能为空".to_string());
-    }
-    let new_run_launch =
-        resolve_swarm_new_run_launch(parent_agent_id, run_profile, supervisor_source)?;
-    let expected_parent_source = new_run_launch.expected_parent_source();
-    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.compact")?;
-    enforce_project_permission_policy(root, "agent.resume")?;
-    let _ = read_local_conversation_for_session_at(root, Some(parent_agent_id), None)?;
-    let existing_runtimes = read_game_creator_agent_runtimes_at(root)?;
-
-    writeln!(output, "Agent Swarm Chat")
-        .and_then(|_| writeln!(output, "项目:{}", root.display()))
-        .and_then(|_| writeln!(output, "父 Agent:{parent_agent_id}"))
-        .and_then(|_| writeln!(output, "输入 /help 查看命令。"))
-        .map_err(|error| format!("写入终端失败:{error}"))?;
-    print_conversation_history(root, parent_agent_id, output)?;
-
-    let active_conversation =
-        read_local_conversation_for_session_at(root, Some(parent_agent_id), None)?;
-    let active_session_id = active_conversation
-        .session_id
-        .as_deref()
-        .ok_or_else(|| "父 Agent 当前 Session 缺失".to_string())?;
-    let matching_parent_is_busy = swarm_parent_runtime(
-        parent_agent_id,
-        active_session_id,
-        run_profile,
-        expected_parent_source,
-        &existing_runtimes,
-    )
-    .is_some_and(runtime_is_busy);
-    if matching_parent_is_busy {
-        writeln!(
-            output,
-            "[恢复扫描] 检测到未收束 Runtime;输入 /resume 继续观察,新消息会进入该 run 的 steer 队列。"
-        )
-        .map_err(|error| format!("写入终端失败:{error}"))?;
-    }
-
-    loop {
-        write!(output, "\n你> ").map_err(|error| format!("写入终端失败:{error}"))?;
-        output
-            .flush()
-            .map_err(|error| format!("刷新终端失败:{error}"))?;
-        let Some(line) = receive_swarm_chat_line(input)? else {
-            writeln!(output, "\n已退出 Agent Swarm Chat。")
-                .map_err(|error| format!("写入终端失败:{error}"))?;
-            return Ok(());
-        };
-        let Some(command) = parse_swarm_chat_input(&line) else {
-            continue;
-        };
-        match command {
-            SwarmChatInput::Help => print_swarm_chat_help(output)?,
-            SwarmChatInput::Agents => print_swarm_agents(root, output)?,
-            SwarmChatInput::Status => print_swarm_status(root, output)?,
-            SwarmChatInput::History => print_conversation_history(root, parent_agent_id, output)?,
-            SwarmChatInput::Compact => {
-                handle_swarm_context_compaction(root, parent_agent_id, output)?
-            }
-            SwarmChatInput::Goal(command) => {
-                let mut observer = SwarmRuntimeObserver::seed(root)?;
-                let Some(observation) =
-                    handle_swarm_goal_command(root, parent_agent_id, command, output)?
-                else {
-                    continue;
-                };
-                let conversation_baseline = new_swarm_turn_conversation_baseline(
-                    observation.previous_message_count,
-                    &observation.run_id,
-                );
-                let outcome = wait_for_swarm_turn(
-                    root,
-                    parent_agent_id,
-                    &observation.session_id,
-                    run_profile,
-                    expected_parent_source,
-                    conversation_baseline,
-                    input,
-                    output,
-                    &mut observer,
-                    SWARM_CHAT_POLL_INTERVAL,
-                    SWARM_CHAT_SETTLE_WINDOW,
-                )?;
-                if outcome == SwarmTurnOutcome::Quit {
-                    return print_swarm_chat_exit(output);
-                }
-                print_turn_outcome(outcome, output)?;
-            }
-            SwarmChatInput::InvalidGoal(error) => print_swarm_goal_error(output, &error)?,
-            SwarmChatInput::Resume => {
-                let before =
-                    read_local_conversation_for_session_at(root, Some(parent_agent_id), None)?;
-                let session_id = before
-                    .session_id
-                    .as_deref()
-                    .ok_or_else(|| "父 Agent 当前 Session 缺失".to_string())?;
-                if handle_swarm_resume_turn(
-                    root,
-                    parent_agent_id,
-                    session_id,
-                    run_profile,
-                    expected_parent_source,
-                    before.messages.len(),
-                    input,
-                    output,
-                )? == SwarmChatFlow::Exit
-                {
-                    return Ok(());
-                }
-            }
-            SwarmChatInput::Quit => {
-                writeln!(
-                    output,
-                    "已退出 Agent Swarm Chat;后台 Runner 和已投递任务保持运行。"
-                )
-                .map_err(|error| format!("写入终端失败:{error}"))?;
-                return Ok(());
-            }
-            SwarmChatInput::Message(message) => {
-                let before =
-                    read_local_conversation_for_session_at(root, Some(parent_agent_id), None)?;
-                let session_id = before
-                    .session_id
-                    .as_deref()
-                    .ok_or_else(|| "父 Agent 当前 Session 缺失".to_string())?;
-                if handle_swarm_user_turn(
-                    root,
-                    parent_agent_id,
-                    session_id,
-                    run_profile,
-                    new_run_launch,
-                    &message,
-                    input,
-                    output,
-                )? == SwarmChatFlow::Exit
-                {
-                    return Ok(());
-                }
-            }
-        }
-    }
-}
-
-pub(super) fn receive_swarm_chat_line(
-    input: &Receiver,
-) -> Result, String> {
-    match input.recv() {
-        Ok(SwarmInputEvent::Line(line)) => Ok(Some(line)),
-        Ok(SwarmInputEvent::Eof) | Err(_) => Ok(None),
-        Ok(SwarmInputEvent::Error(error)) => Err(format!("读取终端输入失败:{error}")),
-    }
-}
-
-pub(super) fn prompt_swarm_decision(
-    root: &Path,
-    parent_agent_id: &str,
-    input: &Receiver,
-    output: &mut W,
-    prompt: &str,
-) -> Result {
-    write!(output, "{prompt}").map_err(|error| format!("写入终端失败:{error}"))?;
-    output
-        .flush()
-        .map_err(|error| format!("刷新终端失败:{error}"))?;
-    loop {
-        let Some(line) = receive_swarm_chat_line(input)? else {
-            return Ok(SwarmPromptDecision::InputClosed);
-        };
-        match line.to_ascii_lowercase().as_str() {
-            "approve" | "yes" | "y" | "批准" => return Ok(SwarmPromptDecision::Approve),
-            "reject" | "no" | "n" | "拒绝" => return Ok(SwarmPromptDecision::Reject),
-            "/quit" | "/exit" => return Ok(SwarmPromptDecision::Quit),
-            _ => {
-                if let Some(command) = parse_swarm_chat_input(&line) {
-                    match command {
-                        SwarmChatInput::Goal(command) => {
-                            let _ =
-                                handle_swarm_goal_command(root, parent_agent_id, command, output)?;
-                            return Ok(SwarmPromptDecision::Deferred);
-                        }
-                        SwarmChatInput::InvalidGoal(error) => {
-                            print_swarm_goal_error(output, &error)?;
-                        }
-                        SwarmChatInput::Compact => {
-                            handle_swarm_context_compaction(root, parent_agent_id, output)?;
-                            return Ok(SwarmPromptDecision::Deferred);
-                        }
-                        _ => {}
-                    }
-                }
-                write!(output, "请输入 approve 或 reject:")
-                    .map_err(|error| format!("写入终端失败:{error}"))?;
-                output
-                    .flush()
-                    .map_err(|error| format!("刷新终端失败:{error}"))?;
-            }
-        }
-    }
-}
-
-pub(super) fn print_swarm_chat_exit(output: &mut W) -> Result<(), String> {
-    writeln!(
-        output,
-        "已退出 Agent Swarm Chat;后台 Runner 和已投递任务保持运行。"
-    )
-    .map_err(|error| format!("写入终端失败:{error}"))
-}
-
-pub(super) fn parse_swarm_chat_input(input: &str) -> Option {
-    let input = input.trim();
-    if input.is_empty() {
-        return None;
-    }
-    if let Some(rest) = input.strip_prefix("/goal") {
-        if rest.is_empty() {
-            return Some(SwarmChatInput::Goal(SwarmGoalCommand::Status));
-        }
-        if !rest.chars().next().is_some_and(char::is_whitespace) {
-            return Some(SwarmChatInput::InvalidGoal(
-                "未知 /goal 命令;输入 /help 查看支持的 Goal 命令。".to_string(),
-            ));
-        }
-        return Some(parse_swarm_goal_command(rest.trim()));
-    }
-    Some(match input {
-        "/help" => SwarmChatInput::Help,
-        "/agents" => SwarmChatInput::Agents,
-        "/status" => SwarmChatInput::Status,
-        "/history" => SwarmChatInput::History,
-        "/compact" => SwarmChatInput::Compact,
-        "/resume" => SwarmChatInput::Resume,
-        "/quit" | "/exit" => SwarmChatInput::Quit,
-        value => SwarmChatInput::Message(value.to_string()),
-    })
-}
-
-pub(super) fn parse_swarm_goal_command(input: &str) -> SwarmChatInput {
-    if input.is_empty() || input == "status" {
-        return SwarmChatInput::Goal(SwarmGoalCommand::Status);
-    }
-    if input == "pause" {
-        return SwarmChatInput::Goal(SwarmGoalCommand::Pause);
-    }
-    if input == "resume" {
-        return SwarmChatInput::Goal(SwarmGoalCommand::Resume);
-    }
-    if input == "clear" {
-        return SwarmChatInput::Goal(SwarmGoalCommand::Clear);
-    }
-    if let Some(outcome) = input.strip_prefix("edit") {
-        if outcome.is_empty() {
-            return SwarmChatInput::InvalidGoal("用法:/goal edit <目标>".to_string());
-        }
-        if outcome.chars().next().is_some_and(char::is_whitespace) {
-            let outcome = outcome.trim();
-            return if outcome.is_empty() {
-                SwarmChatInput::InvalidGoal("用法:/goal edit <目标>".to_string())
-            } else {
-                SwarmChatInput::Goal(SwarmGoalCommand::Edit(outcome.to_string()))
-            };
-        }
-    }
-    for command in ["status", "pause", "resume", "clear"] {
-        if input
-            .strip_prefix(command)
-            .is_some_and(|rest| rest.chars().next().is_some_and(char::is_whitespace))
-        {
-            return SwarmChatInput::InvalidGoal(format!("/goal {command} 不接受额外参数"));
-        }
-    }
-    SwarmChatInput::Goal(SwarmGoalCommand::Start(input.to_string()))
-}
-
-pub(super) fn print_swarm_chat_help(output: &mut W) -> Result<(), String> {
-    writeln!(output, "/agents  查看静态 Agent 与动态 child")
-        .and_then(|_| writeln!(output, "/status  查看全部 Runtime 状态"))
-        .and_then(|_| writeln!(output, "/history 查看父 Agent 当前 Session 历史"))
-        .and_then(|_| writeln!(output, "/compact 压缩父 Agent 当前空闲 Session 历史"))
-        .and_then(|_| writeln!(output, "/resume 继续观察当前 Session 的未收束 Runtime"))
-        .and_then(|_| writeln!(output, "/goal <目标>      启动当前 Session 的持久 Goal"))
-        .and_then(|_| writeln!(output, "/goal             查看当前 Goal"))
-        .and_then(|_| writeln!(output, "/goal status      查看当前 Goal"))
-        .and_then(|_| writeln!(output, "/goal edit <目标> 编辑当前 Goal"))
-        .and_then(|_| writeln!(output, "/goal pause       暂停当前 Goal"))
-        .and_then(|_| writeln!(output, "/goal resume      恢复当前 Goal"))
-        .and_then(|_| writeln!(output, "/goal clear       清理当前 Goal"))
-        .and_then(|_| writeln!(output, "/help    查看命令"))
-        .and_then(|_| writeln!(output, "/quit    退出终端观察客户端"))
-        .map_err(|error| format!("写入终端失败:{error}"))
-}
diff --git a/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/observer.rs b/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/observer.rs
deleted file mode 100644
index f728282f7..000000000
--- a/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/observer.rs
+++ /dev/null
@@ -1,843 +0,0 @@
-use super::*;
-
-pub(super) const SWARM_CHAT_PLAN_STEP_LIMIT: usize = 8;
-
-#[derive(Default)]
-pub(super) struct SwarmRuntimeObserver {
-    pub(super) state_signatures: BTreeMap,
-    pub(super) seen_events: BTreeSet,
-    pub(super) handled_confirmations: BTreeSet,
-    pub(super) handled_user_input_requests: BTreeSet,
-    pub(super) user_input_response_ids: BTreeMap,
-    pub(super) response_streams: BTreeMap,
-    pub(super) open_response_line: Option,
-}
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub(super) struct SwarmResponseStreamIdentity {
-    pub(super) task_id: String,
-    pub(super) session_id: String,
-    pub(super) run_id: String,
-    pub(super) request_slot: String,
-    pub(super) applied_steer_cursor: u64,
-    pub(super) response_revision: u64,
-}
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub(super) struct SwarmResponseStreamCursor {
-    pub(super) identity: SwarmResponseStreamIdentity,
-    pub(super) session_id: String,
-    pub(super) sequence: u64,
-    pub(super) status: String,
-    pub(super) accumulated_text: String,
-    pub(super) printed_accumulated_text: Option,
-    pub(super) connected: bool,
-    pub(super) rejected_snapshot: Option,
-}
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub(super) struct SwarmResponseStreamLine {
-    pub(super) agent_id: String,
-    pub(super) identity: SwarmResponseStreamIdentity,
-}
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub(super) struct SwarmRejectedResponseStreamSnapshot {
-    pub(super) session_id: String,
-    pub(super) sequence: u64,
-    pub(super) status: String,
-    pub(super) accumulated_text: String,
-}
-
-pub(super) enum SwarmConfirmationResolution {
-    None,
-    Handled,
-    InputClosed,
-    Quit,
-}
-
-impl SwarmResponseStreamIdentity {
-    pub(super) fn from_stream(stream: &AgentRuntimeResponseStream) -> Self {
-        Self {
-            task_id: stream.task_id.clone(),
-            session_id: stream.session_id.clone(),
-            run_id: stream.run_id.clone(),
-            request_slot: stream.request_slot.clone(),
-            applied_steer_cursor: stream.applied_steer_cursor,
-            response_revision: stream.response_revision,
-        }
-    }
-}
-
-impl SwarmResponseStreamCursor {
-    pub(super) fn seeded(stream: &AgentRuntimeResponseStream) -> Self {
-        Self {
-            identity: SwarmResponseStreamIdentity::from_stream(stream),
-            session_id: stream.session_id.clone(),
-            sequence: stream.sequence,
-            status: stream.status.clone(),
-            accumulated_text: stream.accumulated_text.clone(),
-            printed_accumulated_text: stream.accumulated_text.is_empty().then(String::new),
-            connected: true,
-            rejected_snapshot: None,
-        }
-    }
-
-    pub(super) fn fresh(stream: &AgentRuntimeResponseStream) -> Self {
-        Self {
-            identity: SwarmResponseStreamIdentity::from_stream(stream),
-            session_id: stream.session_id.clone(),
-            sequence: stream.sequence,
-            status: stream.status.clone(),
-            accumulated_text: stream.accumulated_text.clone(),
-            printed_accumulated_text: None,
-            connected: true,
-            rejected_snapshot: None,
-        }
-    }
-}
-
-impl SwarmRejectedResponseStreamSnapshot {
-    pub(super) fn from_stream(stream: &AgentRuntimeResponseStream) -> Self {
-        Self {
-            session_id: stream.session_id.clone(),
-            sequence: stream.sequence,
-            status: stream.status.clone(),
-            accumulated_text: stream.accumulated_text.clone(),
-        }
-    }
-}
-
-pub(super) fn swarm_response_stream_is_printable(stream: &AgentRuntimeResponseStream) -> bool {
-    matches!(
-        stream.status.as_str(),
-        AGENT_RUNTIME_RESPONSE_STREAM_STATUS_STREAMING | AGENT_RUNTIME_RESPONSE_STREAM_STATUS_READY
-    )
-}
-
-pub(super) fn swarm_response_stream_identity_reset_reason(
-    previous: &SwarmResponseStreamIdentity,
-    current: &SwarmResponseStreamIdentity,
-) -> &'static str {
-    if previous.run_id != current.run_id {
-        "new-run"
-    } else if previous.session_id != current.session_id {
-        "new-session"
-    } else if previous.task_id != current.task_id {
-        "new-task"
-    } else if previous.applied_steer_cursor != current.applied_steer_cursor {
-        "new-steer-cursor"
-    } else if previous.request_slot != current.request_slot {
-        "new-request-slot"
-    } else {
-        "new-response-revision"
-    }
-}
-
-impl SwarmRuntimeObserver {
-    pub(super) fn seed(root: &Path) -> Result {
-        let mut observer = Self::default();
-        for runtime in read_game_creator_agent_runtimes_at(root)? {
-            observer.state_signatures.insert(
-                runtime.state.agent_id.clone(),
-                runtime_state_signature(&runtime.state, &runtime.task_queue),
-            );
-            if let Some(stream) = runtime.response_stream.as_ref() {
-                observer.response_streams.insert(
-                    runtime.state.agent_id.clone(),
-                    SwarmResponseStreamCursor::seeded(stream),
-                );
-            }
-            for event in runtime.recent_events {
-                observer.seen_events.insert(runtime_event_key(&event));
-            }
-        }
-        Ok(observer)
-    }
-
-    pub(super) fn print_changes(
-        &mut self,
-        runtimes: &[AgentRuntimeResult],
-        output: &mut W,
-    ) -> Result {
-        let mut changed = false;
-        for runtime in runtimes {
-            let has_runtime_history = !runtime.state.run_id.is_empty()
-                || runtime.task_queue.total > 0
-                || !runtime.recent_events.is_empty();
-            if has_runtime_history {
-                let signature = runtime_state_signature(&runtime.state, &runtime.task_queue);
-                if self.state_signatures.get(&runtime.state.agent_id) != Some(&signature) {
-                    changed = true;
-                    self.close_response_line(output)?;
-                    self.state_signatures
-                        .insert(runtime.state.agent_id.clone(), signature);
-                    print_runtime_state(&runtime.state, &runtime.task_queue, output)?;
-                }
-            }
-            for event in &runtime.recent_events {
-                if self.seen_events.insert(runtime_event_key(event)) {
-                    changed = true;
-                    self.close_response_line(output)?;
-                    writeln!(
-                        output,
-                        "[事件] {} {} {}/{} {}{}",
-                        event.agent_id,
-                        event.event_type,
-                        event.status,
-                        event.phase,
-                        event.summary,
-                        event
-                            .detail
-                            .as_deref()
-                            .map(|detail| format!(" | {detail}"))
-                            .unwrap_or_default()
-                    )
-                    .map_err(|error| format!("写入终端失败:{error}"))?;
-                }
-            }
-            changed |= self.observe_response_stream(
-                &runtime.state.agent_id,
-                runtime.response_stream.as_ref(),
-                output,
-            )?;
-        }
-        Ok(changed)
-    }
-
-    pub(super) fn observe_response_stream(
-        &mut self,
-        agent_id: &str,
-        stream: Option<&AgentRuntimeResponseStream>,
-        output: &mut W,
-    ) -> Result {
-        let previous = self.response_streams.remove(agent_id);
-        let Some(stream) = stream else {
-            let Some(mut cursor) = previous else {
-                return Ok(false);
-            };
-            let changed = cursor.connected;
-            if changed {
-                cursor.connected = false;
-                if self.response_line_matches(agent_id, &cursor.identity) {
-                    self.close_response_line(output)?;
-                }
-            }
-            self.response_streams.insert(agent_id.to_string(), cursor);
-            return Ok(changed);
-        };
-
-        let printable = swarm_response_stream_is_printable(stream);
-        let Some(previous) = previous else {
-            let reason = if stream.sequence == 0 && stream.accumulated_text.is_empty() {
-                "new-stream"
-            } else {
-                "reconnect"
-            };
-            self.print_response_stream_reset(agent_id, stream, reason, output)?;
-            let mut cursor = SwarmResponseStreamCursor::fresh(stream);
-            if printable {
-                self.print_response_stream_full(agent_id, stream, &mut cursor, output)?;
-            }
-            self.response_streams.insert(agent_id.to_string(), cursor);
-            return Ok(true);
-        };
-
-        let identity = SwarmResponseStreamIdentity::from_stream(stream);
-        if previous.identity != identity {
-            let reason = swarm_response_stream_identity_reset_reason(&previous.identity, &identity);
-            self.print_response_stream_reset(agent_id, stream, reason, output)?;
-            let mut cursor = SwarmResponseStreamCursor::fresh(stream);
-            if printable {
-                self.print_response_stream_full(agent_id, stream, &mut cursor, output)?;
-            }
-            self.response_streams.insert(agent_id.to_string(), cursor);
-            return Ok(true);
-        }
-
-        let exact_snapshot = previous.session_id == stream.session_id
-            && previous.sequence == stream.sequence
-            && previous.status == stream.status
-            && previous.accumulated_text == stream.accumulated_text;
-        let unchanged = previous.connected && exact_snapshot;
-        if unchanged {
-            self.response_streams.insert(agent_id.to_string(), previous);
-            return Ok(false);
-        }
-
-        let non_monotonic_reason = if previous.session_id != stream.session_id {
-            Some("identity-conflict")
-        } else if stream.sequence < previous.sequence {
-            Some("sequence-rollback")
-        } else if stream.sequence == previous.sequence && !exact_snapshot {
-            Some("sequence-conflict")
-        } else {
-            None
-        };
-        if let Some(reason) = non_monotonic_reason {
-            let rejected = SwarmRejectedResponseStreamSnapshot::from_stream(stream);
-            if previous.rejected_snapshot.as_ref() == Some(&rejected) {
-                self.response_streams.insert(agent_id.to_string(), previous);
-                return Ok(false);
-            }
-            self.print_response_stream_reset(agent_id, stream, reason, output)?;
-            let mut cursor = previous;
-            cursor.connected = false;
-            cursor.rejected_snapshot = Some(rejected);
-            self.response_streams.insert(agent_id.to_string(), cursor);
-            return Ok(true);
-        }
-
-        let prefix_continuation = stream
-            .accumulated_text
-            .strip_prefix(&previous.accumulated_text);
-        let reset_reason = if !previous.connected {
-            Some("reconnect")
-        } else if prefix_continuation.is_none() {
-            Some("non-prefix-correction")
-        } else {
-            None
-        };
-        if let Some(reason) = reset_reason {
-            self.print_response_stream_reset(agent_id, stream, reason, output)?;
-        }
-
-        let mut cursor = SwarmResponseStreamCursor::fresh(stream);
-        if printable {
-            let previous_printed = previous.printed_accumulated_text.as_deref();
-            let reset_requires_full = reset_reason.is_some_and(|reason| reason != "reconnect")
-                && previous_printed != Some(stream.accumulated_text.as_str());
-            if previous_printed == Some(stream.accumulated_text.as_str()) {
-                cursor.printed_accumulated_text = Some(stream.accumulated_text.clone());
-            } else if reset_requires_full {
-                self.print_response_stream_full(agent_id, stream, &mut cursor, output)?;
-            } else if let Some(suffix) = prefix_continuation
-                .filter(|_| previous_printed == Some(previous.accumulated_text.as_str()))
-            {
-                self.write_response_stream_chunk(agent_id, &identity, suffix, output)?;
-                cursor.printed_accumulated_text = Some(stream.accumulated_text.clone());
-            } else {
-                self.print_response_stream_full(agent_id, stream, &mut cursor, output)?;
-            }
-        } else {
-            cursor.printed_accumulated_text = previous
-                .printed_accumulated_text
-                .filter(|printed| printed == &stream.accumulated_text);
-            if self.response_line_matches(agent_id, &identity) {
-                self.close_response_line(output)?;
-            }
-        }
-        self.response_streams.insert(agent_id.to_string(), cursor);
-        Ok(true)
-    }
-
-    pub(super) fn print_response_stream_full(
-        &mut self,
-        agent_id: &str,
-        stream: &AgentRuntimeResponseStream,
-        cursor: &mut SwarmResponseStreamCursor,
-        output: &mut W,
-    ) -> Result<(), String> {
-        self.write_response_stream_chunk(
-            agent_id,
-            &cursor.identity,
-            &stream.accumulated_text,
-            output,
-        )?;
-        cursor.printed_accumulated_text = Some(stream.accumulated_text.clone());
-        Ok(())
-    }
-
-    pub(super) fn print_response_stream_reset(
-        &mut self,
-        agent_id: &str,
-        stream: &AgentRuntimeResponseStream,
-        reason: &str,
-        output: &mut W,
-    ) -> Result<(), String> {
-        self.close_response_line(output)?;
-        writeln!(
-            output,
-            "[回复流重置] agent={} run={} requestSlot={} revision={} sequence={} status={} chars={} reason={}",
-            agent_id,
-            stream.run_id,
-            stream.request_slot,
-            stream.response_revision,
-            stream.sequence,
-            stream.status,
-            stream.accumulated_text.chars().count(),
-            reason
-        )
-        .map_err(|error| format!("写入终端失败:{error}"))
-    }
-
-    pub(super) fn write_response_stream_chunk(
-        &mut self,
-        agent_id: &str,
-        identity: &SwarmResponseStreamIdentity,
-        chunk: &str,
-        output: &mut W,
-    ) -> Result<(), String> {
-        if chunk.is_empty() {
-            return Ok(());
-        }
-        if !self.response_line_matches(agent_id, identity) {
-            self.close_response_line(output)?;
-            write!(output, "Agent[{agent_id}]> {chunk}")
-                .map_err(|error| format!("写入终端失败:{error}"))?;
-            self.open_response_line = Some(SwarmResponseStreamLine {
-                agent_id: agent_id.to_string(),
-                identity: identity.clone(),
-            });
-        } else {
-            write!(output, "{chunk}").map_err(|error| format!("写入终端失败:{error}"))?;
-        }
-        output
-            .flush()
-            .map_err(|error| format!("刷新终端失败:{error}"))
-    }
-
-    pub(super) fn response_line_matches(
-        &self,
-        agent_id: &str,
-        identity: &SwarmResponseStreamIdentity,
-    ) -> bool {
-        self.open_response_line
-            .as_ref()
-            .is_some_and(|line| line.agent_id == agent_id && line.identity == *identity)
-    }
-
-    pub(super) fn close_response_line(&mut self, output: &mut W) -> Result<(), String> {
-        if self.open_response_line.take().is_some() {
-            writeln!(output).map_err(|error| format!("写入终端失败:{error}"))?;
-            output
-                .flush()
-                .map_err(|error| format!("刷新终端失败:{error}"))?;
-        }
-        Ok(())
-    }
-
-    pub(super) fn parent_reply_was_fully_streamed(
-        &self,
-        parent_agent_id: &str,
-        session_id: &str,
-        reply: &str,
-    ) -> bool {
-        self.response_streams
-            .get(parent_agent_id)
-            .is_some_and(|cursor| {
-                cursor.session_id == session_id
-                    && cursor.accumulated_text == reply
-                    && cursor.printed_accumulated_text.as_deref() == Some(reply)
-                    && matches!(
-                        cursor.status.as_str(),
-                        AGENT_RUNTIME_RESPONSE_STREAM_STATUS_STREAMING
-                            | AGENT_RUNTIME_RESPONSE_STREAM_STATUS_READY
-                            | AGENT_RUNTIME_RESPONSE_STREAM_STATUS_COMMITTED
-                    )
-            })
-    }
-
-    pub(super) fn resolve_confirmations(
-        &mut self,
-        root: &Path,
-        parent_agent_id: &str,
-        runtimes: &[AgentRuntimeResult],
-        input: &Receiver,
-        output: &mut W,
-    ) -> Result {
-        for runtime in runtimes {
-            if runtime.state.status != "waiting-for-confirmation" {
-                continue;
-            }
-            let Some(pending) = runtime.state.pending_tool_action.as_ref() else {
-                continue;
-            };
-            let key = format!(
-                "{}:{}:{}",
-                runtime.state.agent_id, runtime.state.run_id, pending.action_id
-            );
-            if self.handled_confirmations.contains(&key) {
-                continue;
-            }
-            self.close_response_line(output)?;
-            writeln!(
-                output,
-                "\n[待确认] agent={} run={} action={} tool={}",
-                runtime.state.agent_id, runtime.state.run_id, pending.action_id, pending.tool
-            )
-            .and_then(|_| {
-                if let Some(summary) = pending.input_summary.as_deref() {
-                    writeln!(output, "输入摘要:{summary}")
-                } else {
-                    Ok(())
-                }
-            })
-            .and_then(|_| write!(output, "输入 approve 或 reject:"))
-            .map_err(|error| format!("写入终端失败:{error}"))?;
-            let decision = prompt_swarm_decision(root, parent_agent_id, input, output, "")?;
-            let approved = match decision {
-                SwarmPromptDecision::Approve => true,
-                SwarmPromptDecision::Reject => false,
-                SwarmPromptDecision::Deferred => return Ok(SwarmConfirmationResolution::Handled),
-                SwarmPromptDecision::InputClosed => {
-                    return Ok(SwarmConfirmationResolution::InputClosed)
-                }
-                SwarmPromptDecision::Quit => return Ok(SwarmConfirmationResolution::Quit),
-            };
-            let project_path = root.display().to_string();
-            if approved {
-                confirm_game_creator_agent_runtime_task(
-                    project_path,
-                    runtime.state.agent_id.clone(),
-                    runtime.state.run_id.clone(),
-                    pending.action_id.clone(),
-                    "Agent Swarm Chat 终端批准".to_string(),
-                )?;
-                writeln!(output, "[已批准] {}", pending.action_id)
-                    .map_err(|error| format!("写入终端失败:{error}"))?;
-            } else {
-                reject_game_creator_agent_runtime_task(
-                    project_path,
-                    runtime.state.agent_id.clone(),
-                    runtime.state.run_id.clone(),
-                    pending.action_id.clone(),
-                    "Agent Swarm Chat 终端拒绝".to_string(),
-                )?;
-                writeln!(output, "[已拒绝] {}", pending.action_id)
-                    .map_err(|error| format!("写入终端失败:{error}"))?;
-            }
-            self.handled_confirmations.insert(key);
-            return Ok(SwarmConfirmationResolution::Handled);
-        }
-        Ok(SwarmConfirmationResolution::None)
-    }
-
-    pub(super) fn resolve_user_input_requests(
-        &mut self,
-        root: &Path,
-        parent_agent_id: &str,
-        runtimes: &[AgentRuntimeResult],
-        input: &Receiver,
-        output: &mut W,
-    ) -> Result {
-        for runtime in runtimes {
-            let Some(request) = runtime.user_input_request.as_ref() else {
-                continue;
-            };
-            if runtime.state.agent_id != parent_agent_id
-                || runtime.state.status != "waiting-for-user-input"
-            {
-                continue;
-            }
-            let key = format!(
-                "{}:{}:{}",
-                runtime.state.agent_id, runtime.state.run_id, request.request_id
-            );
-            if self.handled_user_input_requests.contains(&key) {
-                continue;
-            }
-            self.close_response_line(output)?;
-            writeln!(
-                output,
-                "\n[Needs input] agent={} run={} request={}",
-                runtime.state.agent_id, runtime.state.run_id, request.request_id
-            )
-            .map_err(|error| format!("写入终端失败:{error}"))?;
-            let mut answers = BTreeMap::new();
-            for question in &request.questions {
-                writeln!(output, "\n{}:{}", question.header, question.question)
-                    .map_err(|error| format!("写入终端失败:{error}"))?;
-                for (index, option) in question.options.iter().enumerate() {
-                    writeln!(
-                        output,
-                        "  {}. {} - {}",
-                        index + 1,
-                        option.label,
-                        option.description
-                    )
-                    .map_err(|error| format!("写入终端失败:{error}"))?;
-                }
-                loop {
-                    write!(
-                        output,
-                        "请选择 1-{},或直接输入其他答案:",
-                        question.options.len()
-                    )
-                    .map_err(|error| format!("写入终端失败:{error}"))?;
-                    output
-                        .flush()
-                        .map_err(|error| format!("刷新终端失败:{error}"))?;
-                    let Some(line) = receive_swarm_chat_line(input)? else {
-                        return Ok(SwarmConfirmationResolution::InputClosed);
-                    };
-                    if matches!(line.as_str(), "/quit" | "/exit") {
-                        return Ok(SwarmConfirmationResolution::Quit);
-                    }
-                    if line == "/status" {
-                        print_swarm_status(root, output)?;
-                        continue;
-                    }
-                    if line == "/history" {
-                        print_conversation_history(root, parent_agent_id, output)?;
-                        continue;
-                    }
-                    let answer = line
-                        .parse::()
-                        .ok()
-                        .and_then(|index| index.checked_sub(1))
-                        .and_then(|index| question.options.get(index))
-                        .map(|option| option.label.clone())
-                        .unwrap_or_else(|| line.trim().to_string());
-                    if answer.is_empty() {
-                        writeln!(output, "回答不能为空。")
-                            .map_err(|error| format!("写入终端失败:{error}"))?;
-                        continue;
-                    }
-                    answers.insert(question.id.clone(), answer);
-                    break;
-                }
-            }
-            let response_id = self
-                .user_input_response_ids
-                .entry(key.clone())
-                .or_insert_with(|| {
-                    format!("swarm-user-input-{}-{}", request.request_id, unix_millis())
-                })
-                .clone();
-            answer_game_creator_agent_runtime_user_input_at(
-                root,
-                &runtime.state.agent_id,
-                &runtime.state.run_id,
-                &request.action_id,
-                &request.request_id,
-                &response_id,
-                answers,
-            )?;
-            writeln!(output, "[已回答] {}", request.request_id)
-                .map_err(|error| format!("写入终端失败:{error}"))?;
-            self.handled_user_input_requests.insert(key);
-            return Ok(SwarmConfirmationResolution::Handled);
-        }
-        Ok(SwarmConfirmationResolution::None)
-    }
-}
-
-pub(super) fn print_runtime_state(
-    state: &AgentRuntimeState,
-    queue: &AgentRuntimeTaskQueueSummary,
-    output: &mut W,
-) -> Result<(), String> {
-    let relation = state
-        .parent_agent_id
-        .as_deref()
-        .map(|parent| {
-            format!(
-                " parent={parent} delegation={}",
-                state.delegation_id.as_deref().unwrap_or("unknown")
-            )
-        })
-        .unwrap_or_default();
-    writeln!(
-        output,
-        "[状态] {} {}/{} run={} queue={}/{}/{}/{}{} | {}",
-        state.agent_id,
-        state.status,
-        state.phase,
-        state.run_id,
-        queue.pending,
-        queue.running,
-        queue.waiting_for_confirmation,
-        queue.waiting_for_user_input,
-        relation,
-        state.current_action
-    )
-    .map_err(|error| format!("写入终端失败:{error}"))?;
-
-    let completed = state
-        .plan_steps
-        .iter()
-        .filter(|step| step.status == "completed")
-        .count();
-    let current_step = runtime_current_plan_step(state)
-        .map(|step| {
-            format!(
-                "#{} [{}] {}",
-                step.index.saturating_add(1),
-                runtime_cli_value(&step.status),
-                runtime_cli_value(&step.title)
-            )
-        })
-        .unwrap_or_else(|| "-".to_string());
-    writeln!(
-        output,
-        "[计划] revision={} completed={}/{} current={} | waiting={} | next={}",
-        state.plan_revision,
-        completed,
-        state.plan_steps.len(),
-        current_step,
-        runtime_cli_value(&state.waiting_on),
-        runtime_cli_value(&state.next_step)
-    )
-    .map_err(|error| format!("写入终端失败:{error}"))?;
-    if !state.plan_explanation.trim().is_empty() {
-        writeln!(
-            output,
-            "  [计划说明] {}",
-            runtime_cli_value(&state.plan_explanation)
-        )
-        .map_err(|error| format!("写入终端失败:{error}"))?;
-    }
-    writeln!(
-        output,
-        "[上下文] estimated={}/{} actual={}/{}/{} compaction={} last={}",
-        state.context_usage.estimated_input_tokens,
-        state.context_usage.auto_compact_token_limit,
-        state
-            .context_usage
-            .last_prompt_tokens
-            .map(|value| value.to_string())
-            .unwrap_or_else(|| "-".to_string()),
-        state
-            .context_usage
-            .last_completion_tokens
-            .map(|value| value.to_string())
-            .unwrap_or_else(|| "-".to_string()),
-        state
-            .context_usage
-            .last_total_tokens
-            .map(|value| value.to_string())
-            .unwrap_or_else(|| "-".to_string()),
-        state.context_usage.compaction_revision,
-        state
-            .context_usage
-            .last_compacted_at
-            .map(|value| value.to_string())
-            .unwrap_or_else(|| "-".to_string()),
-    )
-    .map_err(|error| format!("写入终端失败:{error}"))?;
-
-    for step in state.plan_steps.iter().take(SWARM_CHAT_PLAN_STEP_LIMIT) {
-        writeln!(
-            output,
-            "  [计划步骤] #{} [{}] {}",
-            step.index.saturating_add(1),
-            runtime_cli_value(&step.status),
-            runtime_cli_value(&step.title)
-        )
-        .map_err(|error| format!("写入终端失败:{error}"))?;
-    }
-    if state.plan_steps.len() > SWARM_CHAT_PLAN_STEP_LIMIT {
-        writeln!(
-            output,
-            "  [计划] 另有 {} 条步骤未显示",
-            state.plan_steps.len() - SWARM_CHAT_PLAN_STEP_LIMIT
-        )
-        .map_err(|error| format!("写入终端失败:{error}"))?;
-    }
-    Ok(())
-}
-
-pub(super) fn runtime_current_plan_step(
-    state: &AgentRuntimeState,
-) -> Option<&AgentRuntimePlanStep> {
-    state
-        .active_plan_step_index
-        .and_then(|active_index| {
-            state
-                .plan_steps
-                .iter()
-                .find(|step| step.index == active_index)
-                .or_else(|| state.plan_steps.get(active_index as usize))
-        })
-        .or_else(|| {
-            state.plan_steps.iter().find(|step| {
-                matches!(
-                    step.status.as_str(),
-                    "active" | "in_progress" | "running" | "waiting-for-confirmation"
-                )
-            })
-        })
-        .or_else(|| {
-            state
-                .plan_steps
-                .iter()
-                .find(|step| step.status == "pending")
-        })
-}
-
-pub(super) fn runtime_cli_value(value: &str) -> &str {
-    let value = value.trim();
-    if value.is_empty() {
-        "-"
-    } else {
-        value
-    }
-}
-
-pub(super) fn runtime_state_signature(
-    state: &AgentRuntimeState,
-    queue: &AgentRuntimeTaskQueueSummary,
-) -> String {
-    let completed_plan_steps = state
-        .plan_steps
-        .iter()
-        .filter(|step| step.status == "completed")
-        .count();
-    let current_plan_step = runtime_current_plan_step(state)
-        .map(|step| format!("{}:{}:{}", step.index, step.status, step.title))
-        .unwrap_or_default();
-    let mut signature = format!(
-        "{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}",
-        state.run_id,
-        state.status,
-        state.phase,
-        state.current_action,
-        state.updated_at,
-        queue.pending,
-        queue.running,
-        queue.waiting_for_confirmation,
-        queue.waiting_for_user_input,
-        queue.updated_at,
-        state.plan_revision,
-        state
-            .active_plan_step_index
-            .map(|index| index.to_string())
-            .unwrap_or_default(),
-        completed_plan_steps,
-        state.plan_steps.len(),
-        current_plan_step,
-        state.waiting_on,
-        state.next_step
-    );
-    signature.push(':');
-    signature.push_str(&state.plan_explanation);
-    signature.push(':');
-    signature.push_str(&format!(
-        "{}:{}:{:?}:{:?}:{}:{:?}",
-        state.context_usage.estimated_input_tokens,
-        state.context_usage.auto_compact_token_limit,
-        state.context_usage.last_prompt_tokens,
-        state.context_usage.last_completion_tokens,
-        state.context_usage.compaction_revision,
-        state.context_usage.last_compacted_at,
-    ));
-    signature
-}
-
-pub(super) fn runtime_event_key(event: &AgentRuntimeEvent) -> String {
-    format!(
-        "{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}",
-        event.agent_id,
-        event.task_id,
-        event.session_id,
-        event.run_id,
-        event.event_type,
-        event.action_id.as_deref().unwrap_or_default(),
-        event.status,
-        event.phase,
-        event.updated_at,
-        event.summary,
-        event.detail.as_deref().unwrap_or_default()
-    )
-}
diff --git a/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/report.rs b/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/report.rs
deleted file mode 100644
index a587ca7c0..000000000
--- a/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/report.rs
+++ /dev/null
@@ -1,285 +0,0 @@
-use super::*;
-
-pub(super) const SWARM_TURN_REPORT_PREFIX: &str = "[turn.report] ";
-pub(super) const SWARM_TURN_REPORT_SCHEMA_VERSION: &str = "game-creator-swarm-turn-report.v1";
-pub(super) const SWARM_TURN_FAILED_ERROR: &str = "swarm-turn-failed";
-pub(super) const SWARM_TURN_INCOMPLETE_ERROR: &str = "swarm-turn-incomplete";
-pub(super) const SWARM_TURN_RECONCILIATION_ERROR: &str = "swarm-turn-needs-reconciliation";
-
-#[derive(Debug, Eq, PartialEq)]
-pub(super) enum SwarmTurnOutcome {
-    Settled(SwarmTurnReport),
-    Failed {
-        agent_ids: Vec,
-        report: SwarmTurnReport,
-    },
-    Incomplete {
-        reasons: Vec,
-        report: SwarmTurnReport,
-    },
-    NeedsReconciliation {
-        agent_ids: Vec,
-        report: SwarmTurnReport,
-    },
-    Quit,
-}
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize)]
-#[serde(rename_all = "kebab-case")]
-pub(super) enum SwarmTurnReportOutcome {
-    Settled,
-    Failed,
-    Incomplete,
-    NeedsReconciliation,
-}
-
-#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
-#[serde(rename_all = "camelCase")]
-pub(super) struct SwarmTurnReport {
-    pub(super) schema_version: &'static str,
-    pub(super) outcome: SwarmTurnReportOutcome,
-    pub(super) parent_agent_id: String,
-    pub(super) session_id: String,
-    pub(super) parent_run_id: Option,
-    pub(super) runtime_count: usize,
-    pub(super) busy_runtime_count: usize,
-    pub(super) pending_task_count: u64,
-    pub(super) running_task_count: u64,
-    pub(super) waiting_for_confirmation_count: u64,
-    pub(super) waiting_for_user_input_count: u64,
-    pub(super) new_assistant_message_count: usize,
-    pub(super) final_reply_chars: usize,
-    pub(super) reconciliation_agent_count: usize,
-}
-
-#[derive(Default)]
-struct SwarmTurnRuntimeMetrics {
-    runtime_count: usize,
-    busy_runtime_count: usize,
-    pending_task_count: u64,
-    running_task_count: u64,
-    waiting_for_confirmation_count: u64,
-    waiting_for_user_input_count: u64,
-}
-
-struct SwarmTurnRuntimeSnapshot {
-    agent_id: String,
-    run_id: String,
-    status: String,
-    phase: String,
-    updated_at: u64,
-}
-
-fn runtime_state_belongs_to_turn(
-    state: &AgentRuntimeState,
-    parent_agent_id: &str,
-    session_id: &str,
-    parent_run_id: &str,
-) -> bool {
-    (state.agent_id == parent_agent_id
-        && state.session_id == session_id
-        && state.run_id == parent_run_id)
-        || (state.parent_agent_id.as_deref() == Some(parent_agent_id)
-            && state.parent_run_id.as_deref() == Some(parent_run_id))
-}
-
-fn runtime_task_belongs_to_turn(
-    task: &AgentRuntimeTaskRecord,
-    parent_agent_id: &str,
-    session_id: &str,
-    parent_run_id: &str,
-) -> bool {
-    (task.agent_id == parent_agent_id
-        && task.session_id == session_id
-        && task.run_id == parent_run_id)
-        || (task.parent_agent_id.as_deref() == Some(parent_agent_id)
-            && task.parent_run_id.as_deref() == Some(parent_run_id))
-}
-
-fn upsert_turn_runtime_snapshot(
-    snapshots: &mut Vec,
-    candidate: SwarmTurnRuntimeSnapshot,
-) {
-    if candidate.run_id.trim().is_empty() {
-        return;
-    }
-    if let Some(existing) = snapshots.iter_mut().find(|snapshot| {
-        snapshot.agent_id == candidate.agent_id && snapshot.run_id == candidate.run_id
-    }) {
-        if candidate.updated_at >= existing.updated_at {
-            *existing = candidate;
-        }
-    } else {
-        snapshots.push(candidate);
-    }
-}
-
-fn scoped_swarm_turn_runtime_metrics(
-    parent_agent_id: &str,
-    session_id: &str,
-    parent_run_id: Option<&str>,
-    runtimes: &[AgentRuntimeResult],
-) -> Result {
-    let Some(parent_run_id) = parent_run_id else {
-        return Ok(SwarmTurnRuntimeMetrics::default());
-    };
-    let mut snapshots = Vec::new();
-    for runtime in runtimes {
-        let journal_tasks = if runtime.task_path.trim().is_empty() {
-            None
-        } else {
-            Some(read_all_game_creator_agent_runtime_tasks(Path::new(
-                &runtime.task_path,
-            ))?)
-        };
-        let tasks = journal_tasks.as_deref().unwrap_or(&runtime.recent_tasks);
-        for task in tasks {
-            if runtime_task_belongs_to_turn(task, parent_agent_id, session_id, parent_run_id) {
-                upsert_turn_runtime_snapshot(
-                    &mut snapshots,
-                    SwarmTurnRuntimeSnapshot {
-                        agent_id: task.agent_id.clone(),
-                        run_id: task.run_id.clone(),
-                        status: task.status.clone(),
-                        phase: task.phase.clone(),
-                        updated_at: task.updated_at,
-                    },
-                );
-            }
-        }
-        if runtime_state_belongs_to_turn(&runtime.state, parent_agent_id, session_id, parent_run_id)
-        {
-            upsert_turn_runtime_snapshot(
-                &mut snapshots,
-                SwarmTurnRuntimeSnapshot {
-                    agent_id: runtime.state.agent_id.clone(),
-                    run_id: runtime.state.run_id.clone(),
-                    status: runtime.state.status.clone(),
-                    phase: runtime.state.phase.clone(),
-                    updated_at: runtime.state.updated_at,
-                },
-            );
-        }
-    }
-
-    let mut metrics = SwarmTurnRuntimeMetrics {
-        runtime_count: snapshots.len(),
-        ..SwarmTurnRuntimeMetrics::default()
-    };
-    for snapshot in snapshots {
-        if matches!(
-            snapshot.status.as_str(),
-            "pending"
-                | "running"
-                | "waiting-for-confirmation"
-                | "waiting-for-user-input"
-                | "cancelling"
-        ) || snapshot.phase == "needs-reconciliation"
-        {
-            metrics.busy_runtime_count += 1;
-        }
-        match snapshot.status.as_str() {
-            "pending" => metrics.pending_task_count += 1,
-            "running" => metrics.running_task_count += 1,
-            "waiting-for-confirmation" => metrics.waiting_for_confirmation_count += 1,
-            "waiting-for-user-input" => metrics.waiting_for_user_input_count += 1,
-            _ => {}
-        }
-    }
-    Ok(metrics)
-}
-
-pub(super) fn build_swarm_turn_report(
-    outcome: SwarmTurnReportOutcome,
-    parent_agent_id: &str,
-    session_id: &str,
-    expected_parent_run_id: Option<&str>,
-    runtimes: &[AgentRuntimeResult],
-    conversation_metrics: SwarmTurnConversationMetrics,
-    reconciliation_agent_count: usize,
-) -> Result {
-    let parent_run_id = expected_parent_run_id
-        .map(str::trim)
-        .filter(|run_id| !run_id.is_empty())
-        .map(str::to_string)
-        .or_else(|| {
-            runtimes
-                .iter()
-                .find(|runtime| {
-                    runtime.state.agent_id == parent_agent_id
-                        && runtime.state.session_id == session_id
-                })
-                .map(|runtime| runtime.state.run_id.trim())
-                .filter(|run_id| !run_id.is_empty())
-                .map(str::to_string)
-        });
-    let runtime_metrics = scoped_swarm_turn_runtime_metrics(
-        parent_agent_id,
-        session_id,
-        parent_run_id.as_deref(),
-        runtimes,
-    )?;
-    Ok(SwarmTurnReport {
-        schema_version: SWARM_TURN_REPORT_SCHEMA_VERSION,
-        outcome,
-        parent_agent_id: parent_agent_id.to_string(),
-        session_id: session_id.to_string(),
-        parent_run_id,
-        runtime_count: runtime_metrics.runtime_count,
-        busy_runtime_count: runtime_metrics.busy_runtime_count,
-        pending_task_count: runtime_metrics.pending_task_count,
-        running_task_count: runtime_metrics.running_task_count,
-        waiting_for_confirmation_count: runtime_metrics.waiting_for_confirmation_count,
-        waiting_for_user_input_count: runtime_metrics.waiting_for_user_input_count,
-        new_assistant_message_count: conversation_metrics.new_assistant_message_count,
-        final_reply_chars: conversation_metrics.final_reply_chars,
-        reconciliation_agent_count,
-    })
-}
-
-pub(super) fn print_turn_outcome(
-    outcome: SwarmTurnOutcome,
-    output: &mut W,
-) -> Result<(), String> {
-    match outcome {
-        SwarmTurnOutcome::Settled(report) => print_swarm_turn_report(&report, output),
-        SwarmTurnOutcome::Failed { agent_ids, report } => {
-            writeln!(
-                output,
-                "[已失败] 以下 Runtime 到达失败终态:{}",
-                agent_ids.join(", ")
-            )
-            .map_err(|error| format!("写入终端失败:{error}"))?;
-            print_swarm_turn_report(&report, output)
-        }
-        SwarmTurnOutcome::Incomplete { reasons, report } => {
-            writeln!(
-                output,
-                "[未完成] 当前 turn 未满足可信终态:{}",
-                reasons.join(", ")
-            )
-            .map_err(|error| format!("写入终端失败:{error}"))?;
-            print_swarm_turn_report(&report, output)
-        }
-        SwarmTurnOutcome::NeedsReconciliation { agent_ids, report } => {
-            writeln!(
-                output,
-                "[已阻断] 以下 Agent 需要人工 reconciliation:{}",
-                agent_ids.join(", ")
-            )
-            .map_err(|error| format!("写入终端失败:{error}"))?;
-            print_swarm_turn_report(&report, output)
-        }
-        SwarmTurnOutcome::Quit => Ok(()),
-    }
-}
-
-pub(super) fn print_swarm_turn_report(
-    report: &SwarmTurnReport,
-    output: &mut W,
-) -> Result<(), String> {
-    let json = serde_json::to_string(report)
-        .map_err(|error| format!("序列化 turn report 失败:{error}"))?;
-    writeln!(output, "{SWARM_TURN_REPORT_PREFIX}{json}")
-        .map_err(|error| format!("写入终端失败:{error}"))
-}
diff --git a/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/terminal_classification.rs b/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/terminal_classification.rs
deleted file mode 100644
index 40129d9ec..000000000
--- a/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/terminal_classification.rs
+++ /dev/null
@@ -1,803 +0,0 @@
-use super::*;
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-pub(super) enum SwarmTurnTerminalClassification {
-    Settled,
-    Failed,
-    Incomplete,
-}
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-pub(super) enum SwarmSpecialistFailureDisposition {
-    Recoverable,
-    Failed,
-    Incomplete,
-}
-
-#[derive(Default)]
-pub(super) struct SwarmTerminalFailureScan {
-    pub(super) failed_agents: Vec,
-    pub(super) incomplete_reasons: Vec,
-    pub(super) reconciliation_agents: Vec,
-}
-
-pub(super) fn swarm_parent_runtime<'a>(
-    parent_agent_id: &str,
-    session_id: &str,
-    run_profile: &str,
-    expected_source: Option<&str>,
-    runtimes: &'a [AgentRuntimeResult],
-) -> Option<&'a AgentRuntimeResult> {
-    runtimes.iter().find(|runtime| {
-        runtime.state.agent_id == parent_agent_id
-            && runtime.state.session_id == session_id
-            && runtime.state.run_profile == run_profile
-            && expected_source.is_none_or(|source| runtime.state.source == source)
-    })
-}
-
-pub(super) fn swarm_parent_runtime_for_run<'a>(
-    parent_agent_id: &str,
-    session_id: &str,
-    run_profile: &str,
-    expected_source: Option<&str>,
-    expected_run_id: &str,
-    runtimes: &'a [AgentRuntimeResult],
-) -> Option<&'a AgentRuntimeResult> {
-    swarm_parent_runtime(
-        parent_agent_id,
-        session_id,
-        run_profile,
-        expected_source,
-        runtimes,
-    )
-    .filter(|runtime| runtime.state.run_id == expected_run_id)
-}
-
-pub(super) fn swarm_parent_steer_target<'a>(
-    parent_agent_id: &str,
-    session_id: &str,
-    run_profile: &str,
-    expected_source: Option<&str>,
-    expected_run_id: Option<&str>,
-    runtimes: &'a [AgentRuntimeResult],
-) -> Option<&'a AgentRuntimeResult> {
-    swarm_parent_runtime(
-        parent_agent_id,
-        session_id,
-        run_profile,
-        expected_source,
-        runtimes,
-    )
-    .filter(|runtime| {
-        expected_run_id.is_none_or(|run_id| runtime.state.run_id == run_id)
-            && game_creator_agent_runtime_accepts_steer(&runtime.state)
-    })
-}
-
-pub(super) fn matching_pending_swarm_run_id<'a>(
-    runtime: &'a AgentRuntimeResult,
-    session_id: &str,
-    run_profile: &str,
-    expected_source: Option<&str>,
-    message: &str,
-) -> Option<&'a str> {
-    let message = message.trim();
-    runtime
-        .recent_tasks
-        .iter()
-        .rev()
-        .find(|task| {
-            task.session_id == session_id
-                && task.run_profile == run_profile
-                && expected_source.is_none_or(|source| task.source == source)
-                && task.status == "pending"
-                && task.phase == "queued"
-                && task.task.trim() == message
-        })
-        .map(|task| task.run_id.as_str())
-}
-
-pub(super) fn next_pending_swarm_task<'a>(
-    runtime: &'a AgentRuntimeResult,
-    session_id: &str,
-    run_profile: &str,
-    expected_source: Option<&str>,
-) -> Option<&'a AgentRuntimeTaskRecord> {
-    runtime.recent_tasks.iter().find(|task| {
-        task.session_id == session_id
-            && task.run_profile == run_profile
-            && expected_source.is_none_or(|source| task.source == source)
-            && task.status == "pending"
-            && task.phase == "queued"
-    })
-}
-
-pub(super) fn swarm_parent_task_for_run<'a>(
-    parent_agent_id: &str,
-    session_id: &str,
-    run_profile: &str,
-    expected_source: Option<&str>,
-    expected_run_id: &str,
-    runtimes: &'a [AgentRuntimeResult],
-) -> Option<&'a AgentRuntimeTaskRecord> {
-    swarm_parent_runtime(
-        parent_agent_id,
-        session_id,
-        run_profile,
-        expected_source,
-        runtimes,
-    )
-    .and_then(|runtime| {
-        runtime.recent_tasks.iter().find(|task| {
-            task.agent_id == parent_agent_id
-                && task.session_id == session_id
-                && task.run_profile == run_profile
-                && expected_source.is_none_or(|source| task.source == source)
-                && task.run_id == expected_run_id
-        })
-    })
-}
-
-pub(super) fn runtime_result_from_task_record(task: &AgentRuntimeTaskRecord) -> AgentRuntimeResult {
-    let mut state = default_game_creator_agent_runtime_state(&task.agent_id, &task.run_id);
-    state.task_id = task.task_id.clone();
-    state.session_id = task.session_id.clone();
-    state.source = task.source.clone();
-    state.run_profile = task.run_profile.clone();
-    state.run_profile_binding_fingerprint = task.run_profile_binding_fingerprint.clone();
-    state.parent_agent_id = task.parent_agent_id.clone();
-    state.parent_run_id = task.parent_run_id.clone();
-    state.delegation_id = task.delegation_id.clone();
-    state.goal_id = task.goal_id.clone();
-    state.goal_revision = task.goal_revision;
-    state.goal_status = task.goal_status.clone();
-    state.current_task = task.task.clone();
-    state.status = task.status.clone();
-    state.phase = task.phase.clone();
-    state.current_action = task.current_action.clone();
-    state.error = task.error.clone();
-    state.updated_at = task.updated_at;
-    AgentRuntimeResult {
-        state,
-        accepted_run_id: None,
-        session_path: String::new(),
-        event_path: String::new(),
-        task_path: String::new(),
-        task_queue: AgentRuntimeTaskQueueSummary::default(),
-        recent_events: Vec::new(),
-        recent_tasks: vec![task.clone()],
-        response_stream: None,
-        user_input_request: None,
-    }
-}
-
-pub(super) fn swarm_parent_runtime_snapshot_for_run(
-    root: &Path,
-    parent_agent_id: &str,
-    session_id: &str,
-    run_profile: &str,
-    expected_source: Option<&str>,
-    expected_run_id: &str,
-    runtimes: &[AgentRuntimeResult],
-) -> Result, String> {
-    if let Some(runtime) = swarm_parent_runtime_for_run(
-        parent_agent_id,
-        session_id,
-        run_profile,
-        expected_source,
-        expected_run_id,
-        runtimes,
-    ) {
-        return Ok(Some(runtime.clone()));
-    }
-    if let Some(task) = read_latest_game_creator_agent_runtime_task_by_run_id(
-        root,
-        parent_agent_id,
-        expected_run_id,
-    )? {
-        if task.agent_id != parent_agent_id
-            || task.session_id != session_id
-            || task.run_profile != run_profile
-            || expected_source.is_some_and(|source| task.source != source)
-            || task.run_id != expected_run_id
-        {
-            return Err("Swarm parent task journal 与目标 Session/run 不匹配".to_string());
-        }
-        return Ok(Some(runtime_result_from_task_record(&task)));
-    }
-    Ok(swarm_parent_task_for_run(
-        parent_agent_id,
-        session_id,
-        run_profile,
-        expected_source,
-        expected_run_id,
-        runtimes,
-    )
-    .map(runtime_result_from_task_record))
-}
-
-pub(super) fn swarm_turn_is_busy(
-    root: &Path,
-    parent_agent_id: &str,
-    session_id: &str,
-    run_profile: &str,
-    expected_source: Option<&str>,
-    expected_run_id: &str,
-    runtimes: &[AgentRuntimeResult],
-) -> Result {
-    let Some(parent) = swarm_parent_runtime_snapshot_for_run(
-        root,
-        parent_agent_id,
-        session_id,
-        run_profile,
-        expected_source,
-        expected_run_id,
-        runtimes,
-    )?
-    else {
-        return Ok(false);
-    };
-    Ok(matches!(
-        parent.state.status.as_str(),
-        "pending"
-            | "running"
-            | "waiting-for-confirmation"
-            | "waiting-for-user-input"
-            | "cancelling"
-    ) || parent.state.phase == "needs-reconciliation")
-}
-
-pub(super) fn swarm_current_runtimes_for_run(
-    parent_agent_id: &str,
-    session_id: &str,
-    run_profile: &str,
-    expected_source: Option<&str>,
-    expected_run_id: &str,
-    runtimes: &[AgentRuntimeResult],
-) -> Vec {
-    runtimes
-        .iter()
-        .filter(|runtime| {
-            (runtime.state.agent_id == parent_agent_id
-                && runtime.state.session_id == session_id
-                && runtime.state.run_profile == run_profile
-                && expected_source.is_none_or(|source| runtime.state.source == source)
-                && runtime.state.run_id == expected_run_id)
-                || (runtime.state.parent_agent_id.as_deref() == Some(parent_agent_id)
-                    && runtime.state.parent_run_id.as_deref() == Some(expected_run_id))
-        })
-        .cloned()
-        .collect()
-}
-
-pub(super) fn runtime_terminal_failure_kind(runtime: &AgentRuntimeResult) -> Option<&'static str> {
-    if runtime.state.phase == "needs-reconciliation" {
-        None
-    } else if runtime.state.phase == "budget-exhausted" {
-        Some("budget-exhausted")
-    } else if runtime.state.status == "cancelled" || runtime.state.phase == "cancelled" {
-        Some("cancelled")
-    } else if runtime.state.phase == "completion-contract-failed" {
-        Some("completion-contract-failed")
-    } else if runtime.state.status == "failed" || runtime.state.phase == "failed" {
-        Some("failed")
-    } else {
-        None
-    }
-}
-
-pub(super) fn parent_runtime_is_active(runtime: &AgentRuntimeResult) -> bool {
-    runtime.state.phase != "needs-reconciliation"
-        && (matches!(
-            runtime.state.status.as_str(),
-            "pending"
-                | "running"
-                | "waiting-for-confirmation"
-                | "waiting-for-user-input"
-                | "cancelling"
-        ) || runtime.recent_tasks.iter().any(|task| {
-            task.run_id == runtime.state.run_id
-                && matches!(
-                    task.status.as_str(),
-                    "pending"
-                        | "running"
-                        | "waiting-for-confirmation"
-                        | "waiting-for-user-input"
-                        | "cancelling"
-                )
-        }))
-}
-
-pub(super) fn static_delegate_delivery_has_repairable_contract(
-    delivery: &StaticDelegateDeliveryRecord,
-) -> bool {
-    delivery.repair_of_delegation_id.is_none()
-        && delivery.status != StaticDelegateDeliveryStatus::Suppressed
-        && (!delivery.acceptance_criteria.is_empty() || !delivery.expected_artifacts.is_empty())
-        && delivery.structured_result.as_ref().is_none_or(|result| {
-            result.contract_status == StaticDelegateContractStatus::NeedsRepair
-        })
-}
-
-pub(super) fn classify_failed_specialist(
-    parent: &AgentRuntimeResult,
-    child: &AgentRuntimeResult,
-    delivery: Option<&StaticDelegateDeliveryRecord>,
-    successful_repair: bool,
-) -> SwarmSpecialistFailureDisposition {
-    let delivery_matches = delivery.is_some_and(|delivery| {
-        child.state.source == "agent-delegate"
-            && child.state.parent_agent_id.as_deref() == Some(parent.state.agent_id.as_str())
-            && child.state.parent_run_id.as_deref() == Some(parent.state.run_id.as_str())
-            && child.state.delegation_id.as_deref() == Some(delivery.delegation_id.as_str())
-            && delivery.parent_agent_id == parent.state.agent_id
-            && delivery.parent_session_id == parent.state.session_id
-            && delivery.parent_run_id == parent.state.run_id
-            && delivery.target_agent_id == child.state.agent_id
-            && delivery.target_session_id == child.state.session_id
-            && delivery.target_run_id == child.state.run_id
-    });
-    if !delivery_matches {
-        return SwarmSpecialistFailureDisposition::Failed;
-    }
-    let delivery = delivery.expect("matching delivery exists");
-    if !static_delegate_delivery_has_repairable_contract(delivery) {
-        return SwarmSpecialistFailureDisposition::Failed;
-    }
-    if successful_repair {
-        return SwarmSpecialistFailureDisposition::Recoverable;
-    }
-    if parent_runtime_is_active(parent) {
-        SwarmSpecialistFailureDisposition::Recoverable
-    } else {
-        SwarmSpecialistFailureDisposition::Incomplete
-    }
-}
-
-pub(super) fn original_delivery_has_successful_repair(
-    original: &StaticDelegateDeliveryRecord,
-    claimed_deliveries: &[StaticDelegateDeliveryRecord],
-) -> bool {
-    original.repair_of_delegation_id.is_none()
-        && !original
-            .structured_result
-            .as_ref()
-            .is_some_and(|result| result.contract_status.is_unknown())
-        && claimed_deliveries.iter().any(|candidate| {
-            candidate.repair_of_delegation_id.as_deref() == Some(original.delegation_id.as_str())
-                && candidate.terminal_status.as_deref() == Some("completed")
-                && candidate.structured_result.as_ref().is_some_and(|result| {
-                    result.contract_status == StaticDelegateContractStatus::EvidenceReady
-                })
-        })
-}
-
-pub(super) fn scan_swarm_terminal_failures_at(
-    root: &Path,
-    parent_agent_id: &str,
-    session_id: &str,
-    run_profile: &str,
-    expected_source: Option<&str>,
-    expected_parent_run_id: &str,
-    runtimes: &[AgentRuntimeResult],
-) -> SwarmTerminalFailureScan {
-    let mut scan = SwarmTerminalFailureScan::default();
-    let canonical_parent = swarm_parent_runtime_for_run(
-        parent_agent_id,
-        session_id,
-        run_profile,
-        expected_source,
-        expected_parent_run_id,
-        runtimes,
-    );
-    let journal_parent_task = match read_latest_game_creator_agent_runtime_task_by_run_id(
-        root,
-        parent_agent_id,
-        expected_parent_run_id,
-    ) {
-        Ok(Some(task))
-            if task.agent_id == parent_agent_id
-                && task.session_id == session_id
-                && task.run_profile == run_profile
-                && expected_source.is_none_or(|source| task.source == source)
-                && task.run_id == expected_parent_run_id =>
-        {
-            Some(task)
-        }
-        Ok(Some(_)) => {
-            scan.reconciliation_agents.push(parent_agent_id.to_string());
-            return scan;
-        }
-        Ok(None) => None,
-        Err(_) => {
-            scan.reconciliation_agents.push(parent_agent_id.to_string());
-            return scan;
-        }
-    };
-    let parent_task = journal_parent_task.as_ref().or_else(|| {
-        swarm_parent_task_for_run(
-            parent_agent_id,
-            session_id,
-            run_profile,
-            expected_source,
-            expected_parent_run_id,
-            runtimes,
-        )
-    });
-    if canonical_parent.is_none()
-        && parent_task.is_none_or(|task| game_creator_agent_runtime_terminal_status(task).is_none())
-    {
-        return scan;
-    }
-    let parent = canonical_parent
-        .cloned()
-        .or_else(|| parent_task.map(runtime_result_from_task_record))
-        .expect("target parent runtime or terminal task exists");
-    let claimed_deliveries = match claimed_static_delegate_deliveries_at(
-        root,
-        &parent.state.agent_id,
-        &parent.state.run_id,
-    ) {
-        Ok(deliveries) => deliveries,
-        Err(_) => {
-            scan.reconciliation_agents
-                .push(parent.state.agent_id.clone());
-            return scan;
-        }
-    };
-    if let Some(kind) = runtime_terminal_failure_kind(&parent) {
-        scan.failed_agents
-            .push(format!("{}:{kind}", parent.state.agent_id));
-    }
-    let task_matches_parent = |task: &AgentRuntimeTaskRecord| {
-        task.source == "agent-delegate"
-            && task.parent_agent_id.as_deref() == Some(parent.state.agent_id.as_str())
-            && task.parent_run_id.as_deref() == Some(parent.state.run_id.as_str())
-    };
-    let mut specialist_agent_ids = runtimes
-        .iter()
-        .map(|runtime| runtime.state.agent_id.clone())
-        .filter(|agent_id| agent_id != &parent.state.agent_id)
-        .collect::>();
-    specialist_agent_ids.extend(
-        claimed_deliveries
-            .iter()
-            .map(|delivery| delivery.target_agent_id.clone()),
-    );
-
-    // recent_tasks remains a compatibility fallback for in-memory callers, while every
-    // discoverable specialist journal below overwrites it with append-order latest records.
-    let mut latest_child_tasks_by_identity =
-        BTreeMap::<(String, String), AgentRuntimeTaskRecord>::new();
-    for runtime in runtimes {
-        for task in runtime
-            .recent_tasks
-            .iter()
-            .filter(|task| task_matches_parent(task))
-        {
-            latest_child_tasks_by_identity
-                .insert((task.agent_id.clone(), task.run_id.clone()), task.clone());
-        }
-    }
-    for agent_id in specialist_agent_ids {
-        let path = game_creator_agent_runtime_task_path(root, &agent_id);
-        let tasks = match read_all_game_creator_agent_runtime_tasks(&path) {
-            Ok(tasks) => tasks,
-            Err(_) => {
-                scan.reconciliation_agents.push(agent_id);
-                continue;
-            }
-        };
-        for task in tasks.into_iter().filter(|task| task_matches_parent(task)) {
-            if task.agent_id != agent_id {
-                scan.reconciliation_agents.push(agent_id.clone());
-                continue;
-            }
-            latest_child_tasks_by_identity
-                .insert((task.agent_id.clone(), task.run_id.clone()), task);
-        }
-    }
-
-    let mut failed_children_by_identity = latest_child_tasks_by_identity
-        .into_iter()
-        .filter_map(|(identity, task)| {
-            let runtime = runtime_result_from_task_record(&task);
-            runtime_terminal_failure_kind(&runtime).map(|_| (identity, runtime))
-        })
-        .collect::>();
-    for runtime in runtimes.iter().filter(|runtime| {
-        runtime.state.source == "agent-delegate"
-            && runtime.state.parent_agent_id.as_deref() == Some(parent.state.agent_id.as_str())
-            && runtime.state.parent_run_id.as_deref() == Some(parent.state.run_id.as_str())
-    }) {
-        if runtime_terminal_failure_kind(runtime).is_some() {
-            failed_children_by_identity.insert(
-                (runtime.state.agent_id.clone(), runtime.state.run_id.clone()),
-                runtime.clone(),
-            );
-        }
-    }
-    for child in failed_children_by_identity.values() {
-        let Some(delegation_id) = child
-            .state
-            .delegation_id
-            .as_deref()
-            .filter(|value| !value.is_empty())
-        else {
-            scan.failed_agents.push(format!(
-                "{}:{}",
-                child.state.agent_id,
-                runtime_terminal_failure_kind(child).unwrap_or("failed")
-            ));
-            continue;
-        };
-        let delivery = match read_static_delegate_delivery_at(root, delegation_id) {
-            Ok(Some(delivery)) => delivery,
-            Ok(None) | Err(_) => {
-                scan.reconciliation_agents
-                    .push(child.state.agent_id.clone());
-                continue;
-            }
-        };
-        let successful_repair =
-            original_delivery_has_successful_repair(&delivery, &claimed_deliveries);
-        match classify_failed_specialist(&parent, child, Some(&delivery), successful_repair) {
-            SwarmSpecialistFailureDisposition::Recoverable => {}
-            SwarmSpecialistFailureDisposition::Failed => scan.failed_agents.push(format!(
-                "{}:{}",
-                child.state.agent_id,
-                runtime_terminal_failure_kind(child).unwrap_or("failed")
-            )),
-            SwarmSpecialistFailureDisposition::Incomplete => scan
-                .incomplete_reasons
-                .push(format!("repair-required:{}", child.state.agent_id)),
-        }
-    }
-    scan.failed_agents.sort();
-    scan.failed_agents.dedup();
-    scan.incomplete_reasons.sort();
-    scan.incomplete_reasons.dedup();
-    scan.reconciliation_agents.sort();
-    scan.reconciliation_agents.dedup();
-    scan
-}
-
-pub(super) fn swarm_unhandled_interaction_reasons(
-    parent_agent_id: &str,
-    runtimes: &[AgentRuntimeResult],
-    input_closed: bool,
-) -> Vec {
-    let mut reasons = Vec::new();
-    for runtime in runtimes {
-        let waiting_for_confirmation = runtime.state.status == "waiting-for-confirmation"
-            || runtime.state.pending_tool_action.is_some()
-            || runtime.task_queue.waiting_for_confirmation > 0;
-        let waiting_for_user_input = runtime.state.status == "waiting-for-user-input"
-            || runtime.user_input_request.is_some()
-            || runtime.task_queue.waiting_for_user_input > 0;
-        if input_closed && waiting_for_confirmation {
-            reasons.push(format!("pending-confirmation:{}", runtime.state.agent_id));
-        }
-        if waiting_for_user_input && (input_closed || runtime.state.agent_id != parent_agent_id) {
-            reasons.push(format!("pending-user-input:{}", runtime.state.agent_id));
-        }
-    }
-    reasons.sort();
-    reasons.dedup();
-    reasons
-}
-
-pub(super) fn parent_runtime_completed(runtime: &AgentRuntimeResult) -> bool {
-    runtime.state.phase == "completed"
-        && matches!(runtime.state.status.as_str(), "idle" | "completed")
-}
-
-pub(super) fn classify_swarm_turn_terminal(
-    parent: Option<&AgentRuntimeResult>,
-    conversation_metrics: SwarmTurnConversationMetrics,
-    failed_runtime_count: usize,
-    pending_interaction_count: usize,
-    completion_blocker_count: usize,
-) -> SwarmTurnTerminalClassification {
-    if failed_runtime_count > 0
-        || parent.is_some_and(|runtime| runtime_terminal_failure_kind(runtime).is_some())
-    {
-        return SwarmTurnTerminalClassification::Failed;
-    }
-    if parent.is_none_or(|runtime| !parent_runtime_completed(runtime))
-        || pending_interaction_count > 0
-        || completion_blocker_count > 0
-        || conversation_metrics.new_assistant_message_count != 1
-        || conversation_metrics.final_reply_chars == 0
-    {
-        return SwarmTurnTerminalClassification::Incomplete;
-    }
-    SwarmTurnTerminalClassification::Settled
-}
-
-pub(super) fn append_swarm_terminal_snapshot_reasons(
-    reasons: &mut Vec,
-    parent: Option<&AgentRuntimeResult>,
-    conversation_metrics: SwarmTurnConversationMetrics,
-) {
-    match parent {
-        None => reasons.push("parent-runtime-missing".to_string()),
-        Some(parent) if !parent_runtime_completed(parent) => reasons.push(format!(
-            "parent-not-completed:{}:{}",
-            parent.state.status, parent.state.phase
-        )),
-        Some(_) => {}
-    }
-    if conversation_metrics.new_assistant_message_count != 1 {
-        reasons.push(format!(
-            "assistant-count={}",
-            conversation_metrics.new_assistant_message_count
-        ));
-    } else if conversation_metrics.final_reply_chars == 0 {
-        reasons.push("assistant-empty".to_string());
-    }
-    reasons.sort();
-    reasons.dedup();
-}
-
-pub(super) fn swarm_parent_completion_contract_blockers_at(
-    root: &Path,
-    parent: &AgentRuntimeResult,
-) -> Vec {
-    let mut blockers = Vec::new();
-    let agent_id = parent.state.agent_id.as_str();
-    let run_id = parent.state.run_id.as_str();
-    if let Some(blocker) = structured_plan_completion_blocker(&parent.state) {
-        blockers.push(blocker.tool);
-    }
-    if parent.state.goal_id.is_some()
-        && !matches!(
-            parent.state.goal_status.as_deref(),
-            Some(AGENT_GOAL_STATUS_COMPLETED | AGENT_GOAL_STATUS_CLEARED)
-        )
-    {
-        blockers.push("runtime.goal".to_string());
-    }
-    if parent.state.pending_tool_action.is_some() {
-        blockers.push("runtime.pending_tool_action".to_string());
-    }
-    match crate::provider_retry::read_for_run_at(root, agent_id, run_id) {
-        Ok(None) => {}
-        Ok(Some(_)) | Err(_) => blockers.push("runtime.provider_retry".to_string()),
-    }
-    let provider_action_batch_path =
-        game_creator_agent_runtime_provider_action_batch_path(root, agent_id, run_id);
-    if provider_action_batch_path.exists()
-        || agent_runtime_json_sidecar_backup_path(&provider_action_batch_path).exists()
-    {
-        blockers.push("runtime.provider_action_batch".to_string());
-    }
-    let finalization_path = game_creator_agent_runtime_finalization_path(root, agent_id, run_id);
-    if finalization_path.exists()
-        || agent_runtime_json_sidecar_backup_path(&finalization_path).exists()
-    {
-        blockers.push("runtime.finalization".to_string());
-    }
-    if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID {
-        match resolve_supervisor_collaboration_policy_for_run_at(root, agent_id, run_id) {
-            Ok(resolution) => {
-                match read_supervisor_collaboration_state_at(root, agent_id, run_id) {
-                    Ok(state) => {
-                        if supervisor_collaboration_completion_gap(&resolution.policy, &state)
-                            .is_some()
-                        {
-                            blockers.push("runtime.collaboration_policy".to_string());
-                        }
-                    }
-                    Err(_) => blockers.push("runtime.collaboration_policy".to_string()),
-                }
-            }
-            Err(_) => blockers.push("runtime.collaboration_policy".to_string()),
-        }
-    }
-    if let Some(blocker) = process_session_completion_blocker_at(root, agent_id, run_id) {
-        blockers.push(blocker.tool);
-    }
-    if let Some(blocker) = isolated_join_completion_blocker_at(root, agent_id, run_id) {
-        blockers.push(blocker.tool);
-    }
-    if let Some(blocker) = static_delegate_completion_blocker_at(root, agent_id, run_id) {
-        blockers.push(blocker.tool);
-    }
-    if let Some(blocker) = project_verification_completion_blocker_at(root, agent_id, run_id, &[]) {
-        blockers.push(blocker.tool);
-    }
-    blockers.sort();
-    blockers.dedup();
-    blockers
-}
-
-pub(super) fn swarm_reconciliation_agents(runtimes: &[AgentRuntimeResult]) -> Vec {
-    runtimes
-        .iter()
-        .filter(|runtime| {
-            runtime.state.phase == "needs-reconciliation"
-                || (runtime.state.status == "waiting-for-confirmation"
-                    && runtime.state.pending_tool_action.is_none())
-                || (runtime.state.status == "waiting-for-user-input"
-                    && runtime.user_input_request.is_none())
-        })
-        .map(|runtime| runtime.state.agent_id.clone())
-        .collect()
-}
-
-pub(super) fn build_reconciliation_turn_outcome(
-    root: &Path,
-    parent_agent_id: &str,
-    session_id: &str,
-    conversation_baseline: &SwarmTurnConversationBaseline,
-    runtimes: &[AgentRuntimeResult],
-    mut agent_ids: Vec,
-) -> Result {
-    agent_ids.sort();
-    agent_ids.dedup();
-    let conversation_metrics =
-        read_turn_conversation_metrics(root, parent_agent_id, session_id, conversation_baseline)?;
-    let report = build_swarm_turn_report(
-        SwarmTurnReportOutcome::NeedsReconciliation,
-        parent_agent_id,
-        session_id,
-        Some(&conversation_baseline.parent_run_id),
-        runtimes,
-        conversation_metrics,
-        agent_ids.len(),
-    )?;
-    Ok(SwarmTurnOutcome::NeedsReconciliation { agent_ids, report })
-}
-
-pub(super) fn build_failed_turn_outcome(
-    root: &Path,
-    parent_agent_id: &str,
-    session_id: &str,
-    conversation_baseline: &SwarmTurnConversationBaseline,
-    runtimes: &[AgentRuntimeResult],
-    mut agent_ids: Vec,
-) -> Result {
-    agent_ids.sort();
-    agent_ids.dedup();
-    let conversation_metrics =
-        read_turn_conversation_metrics(root, parent_agent_id, session_id, conversation_baseline)?;
-    let report = build_swarm_turn_report(
-        SwarmTurnReportOutcome::Failed,
-        parent_agent_id,
-        session_id,
-        Some(&conversation_baseline.parent_run_id),
-        runtimes,
-        conversation_metrics,
-        0,
-    )?;
-    Ok(SwarmTurnOutcome::Failed { agent_ids, report })
-}
-
-pub(super) fn build_incomplete_turn_outcome(
-    root: &Path,
-    parent_agent_id: &str,
-    session_id: &str,
-    conversation_baseline: &SwarmTurnConversationBaseline,
-    runtimes: &[AgentRuntimeResult],
-    mut reasons: Vec,
-) -> Result {
-    reasons.sort();
-    reasons.dedup();
-    if reasons.is_empty() {
-        reasons.push("terminal-contract-not-proven".to_string());
-    }
-    let conversation_metrics =
-        read_turn_conversation_metrics(root, parent_agent_id, session_id, conversation_baseline)?;
-    let report = build_swarm_turn_report(
-        SwarmTurnReportOutcome::Incomplete,
-        parent_agent_id,
-        session_id,
-        Some(&conversation_baseline.parent_run_id),
-        runtimes,
-        conversation_metrics,
-        0,
-    )?;
-    Ok(SwarmTurnOutcome::Incomplete { reasons, report })
-}
diff --git a/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/tests.rs b/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/tests.rs
deleted file mode 100644
index 9c3c1aedf..000000000
--- a/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/tests.rs
+++ /dev/null
@@ -1,2417 +0,0 @@
-use super::*;
-
-fn runtime(status: &str, phase: &str, pending: u32) -> AgentRuntimeResult {
-    let state = serde_json::from_value::(serde_json::json!({
-        "agentId": "code-prototype",
-        "runId": "run-test",
-        "status": status,
-        "phase": phase,
-    }))
-    .expect("deserialize runtime fixture");
-    let mut task_queue = AgentRuntimeTaskQueueSummary::default();
-    task_queue.pending = pending;
-    AgentRuntimeResult {
-        state,
-        accepted_run_id: None,
-        session_path: String::new(),
-        event_path: String::new(),
-        task_path: String::new(),
-        task_queue,
-        recent_events: Vec::new(),
-        recent_tasks: Vec::new(),
-        response_stream: None,
-        user_input_request: None,
-    }
-}
-
-fn response_stream(
-    request_slot: &str,
-    response_revision: u64,
-    sequence: u64,
-    status: &str,
-    accumulated_text: &str,
-) -> AgentRuntimeResponseStream {
-    AgentRuntimeResponseStream {
-        schema_version: "game-creator-runtime-response-stream.v1".to_string(),
-        agent_id: "code-prototype".to_string(),
-        task_id: "code-prototype".to_string(),
-        session_id: "session-test".to_string(),
-        run_id: "run-test".to_string(),
-        request_kind: "final-reply".to_string(),
-        request_slot: request_slot.to_string(),
-        applied_steer_cursor: 0,
-        response_revision,
-        sequence,
-        status: status.to_string(),
-        accumulated_text: accumulated_text.to_string(),
-        finish_reason: None,
-        started_at: 100,
-        updated_at: 100 + sequence,
-    }
-}
-
-fn runtime_with_response_stream(stream: AgentRuntimeResponseStream) -> AgentRuntimeResult {
-    let mut snapshot = runtime("running", "response", 0);
-    snapshot.state.session_id = stream.session_id.clone();
-    snapshot.response_stream = Some(stream);
-    snapshot
-}
-
-#[test]
-fn new_supervisor_runs_fix_cli_source_and_select_requested_profile() {
-    for run_profile in [
-        AGENT_RUNTIME_RUN_PROFILE_STANDARD,
-        AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD,
-    ] {
-        assert_eq!(
-            resolve_swarm_new_run_launch(
-                GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
-                run_profile,
-                AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE,
-            )
-            .expect("resolve supervisor launch"),
-            SwarmNewRunLaunch::ProjectSupervisor {
-                source: AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE,
-                run_profile,
-            }
-        );
-    }
-}
-
-#[test]
-fn explicit_parent_debug_keeps_standard_profile_only() {
-    assert_eq!(
-        resolve_swarm_new_run_launch(
-            "code-prototype",
-            AGENT_RUNTIME_RUN_PROFILE_STANDARD,
-            AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE,
-        )
-        .expect("resolve explicit parent debug launch"),
-        SwarmNewRunLaunch::ExplicitParentDebug,
-    );
-    assert!(resolve_swarm_new_run_launch(
-        "code-prototype",
-        AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD,
-        AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE,
-    )
-    .expect_err("autonomous profile must stay on the supervisor root run")
-    .contains(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID));
-    assert!(resolve_swarm_new_run_launch(
-        GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
-        "unsupported",
-        AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE,
-    )
-    .is_err());
-}
-
-#[test]
-fn same_run_steer_preserves_bound_autonomous_profile() {
-    let root = std::env::temp_dir().join(format!(
-        "swarm-cli-profile-steer-{}-{}",
-        std::process::id(),
-        unix_millis()
-    ));
-    init_local_game_project_at(&root, "project-swarm-profile", "Swarm Profile Steer")
-        .expect("initialize profile steer project");
-    let run_id = "swarm-profile-steer-run";
-    let binding = bind_game_creator_agent_runtime_run_profile_at(
-        &root,
-        GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
-        run_id,
-        AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE,
-        Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD),
-        None,
-    )
-    .expect("bind autonomous supervisor profile");
-    let state = start_game_creator_agent_runtime_task_at(
-        &root,
-        GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
-        "生成一版可试玩项目",
-        run_id,
-        AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE,
-        "准备自主构建",
-        vec!["实现并验证最小可玩闭环".to_string()],
-    )
-    .expect("start autonomous supervisor runtime");
-
-    let steered = steer_game_creator_agent_runtime_task_at(
-        &root,
-        GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
-        &state.session_id,
-        run_id,
-        "swarm-profile-steer-1",
-        "保持当前目标并补充触屏操作",
-        "swarm-cli",
-    )
-    .expect("steer autonomous supervisor runtime");
-
-    assert_eq!(steered.runtime.state.run_id, run_id);
-    assert_eq!(
-        steered.runtime.state.run_profile,
-        AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
-    );
-    assert_eq!(
-        steered.runtime.state.run_profile_binding_fingerprint,
-        binding.binding_fingerprint
-    );
-    fs::remove_dir_all(root).ok();
-}
-
-#[test]
-fn cross_profile_steer_is_rejected_before_persistent_side_effects() {
-    let root = std::env::temp_dir().join(format!(
-        "swarm-cli-profile-mismatch-{}-{}",
-        std::process::id(),
-        unix_millis()
-    ));
-    init_local_game_project_at(&root, "project-swarm-mismatch", "Swarm Profile Mismatch")
-        .expect("initialize profile mismatch project");
-    let run_id = "swarm-profile-standard-run";
-    let state = start_game_creator_agent_runtime_task_at(
-        &root,
-        GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
-        "等待开发者确认的标准任务",
-        run_id,
-        AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE,
-        "准备标准模式运行",
-        vec!["等待确认".to_string()],
-    )
-    .expect("start standard supervisor runtime");
-    assert_eq!(
-        state.run_profile, AGENT_RUNTIME_RUN_PROFILE_STANDARD,
-        "fixture must stay standard"
-    );
-    let conversation_before = read_local_conversation_for_session_at(
-        &root,
-        Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID),
-        Some(&state.session_id),
-    )
-    .expect("read conversation before rejected steer");
-
-    let error = steer_game_creator_agent_runtime_task_for_profile_at(
-        &root,
-        GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
-        &state.session_id,
-        run_id,
-        "swarm-profile-mismatch-steer",
-        "切换为自主构建并继续",
-        Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD),
-        "swarm-cli",
-    )
-    .expect_err("cross-profile steer must fail closed");
-    assert!(error.contains("Run Profile"), "unexpected error: {error}");
-    assert!(!game_creator_agent_runtime_steer_ledger_path(
-        &root,
-        GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
-        run_id,
-    )
-    .exists());
-    let conversation_after = read_local_conversation_for_session_at(
-        &root,
-        Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID),
-        Some(&state.session_id),
-    )
-    .expect("read conversation after rejected steer");
-    assert_eq!(conversation_after.messages, conversation_before.messages);
-    let persisted =
-        read_game_creator_agent_runtime_at(&root, GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID)
-            .expect("read runtime after rejected steer");
-    assert_eq!(persisted.state.run_id, run_id);
-    assert_eq!(
-        persisted.state.run_profile,
-        AGENT_RUNTIME_RUN_PROFILE_STANDARD
-    );
-    fs::remove_dir_all(root).ok();
-}
-
-#[test]
-fn natural_language_is_not_preclassified_by_cli_keywords() {
-    for message in [
-        "你是谁",
-        "项目在哪里",
-        "实现登录页并运行测试",
-        "不要运行任何命令,只解释架构",
-        "你能做什么,并顺便修复这个问题",
-        "继续刚才的任务",
-    ] {
-        assert_eq!(
-            parse_swarm_chat_input(message),
-            Some(SwarmChatInput::Message(message.to_string())),
-            "{message} must enter the unified Agent interaction loop"
-        );
-    }
-}
-
-#[test]
-fn resume_is_an_explicit_control_command() {
-    assert_eq!(
-        parse_swarm_chat_input("/resume"),
-        Some(SwarmChatInput::Resume)
-    );
-}
-
-#[test]
-fn natural_language_resume_without_active_runtime_starts_a_new_run() {
-    assert_eq!(
-        normalize_interaction_action_without_active_runtime(AgentInteractionAction::Resume),
-        AgentInteractionAction::Execute
-    );
-    assert_eq!(
-        normalize_interaction_action_without_active_runtime(AgentInteractionAction::Reply(
-            "记得之前的工作".to_string()
-        )),
-        AgentInteractionAction::Reply("记得之前的工作".to_string())
-    );
-}
-
-#[test]
-fn parent_runtime_matching_is_scoped_to_requested_profile() {
-    let mut standard = runtime("running", "planning", 0);
-    standard.state.agent_id = GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string();
-    standard.state.session_id = "session-profile-match".to_string();
-    standard.state.run_id = "run-standard".to_string();
-    standard.state.run_profile = AGENT_RUNTIME_RUN_PROFILE_STANDARD.to_string();
-    let mut autonomous = standard.clone();
-    autonomous.state.run_id = "run-autonomous".to_string();
-    autonomous.state.run_profile = AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD.to_string();
-    let runtimes = vec![standard, autonomous];
-
-    assert_eq!(
-        swarm_parent_runtime(
-            GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
-            "session-profile-match",
-            AGENT_RUNTIME_RUN_PROFILE_STANDARD,
-            None,
-            &runtimes,
-        )
-        .map(|runtime| runtime.state.run_id.as_str()),
-        Some("run-standard")
-    );
-    assert_eq!(
-        swarm_parent_runtime(
-            GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
-            "session-profile-match",
-            AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD,
-            None,
-            &runtimes,
-        )
-        .map(|runtime| runtime.state.run_id.as_str()),
-        Some("run-autonomous")
-    );
-}
-
-#[test]
-fn completed_parent_with_pending_task_is_busy_but_not_a_steer_target() {
-    let session_id = "session-pending-after-completed";
-    let run_profile = AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD;
-    let mut completed = runtime("idle", "completed", 1);
-    completed.state.agent_id = GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string();
-    completed.state.session_id = session_id.to_string();
-    completed.state.run_id = "run-completed-before-pending".to_string();
-    completed.state.run_profile = run_profile.to_string();
-    completed.recent_tasks.push(
-        serde_json::from_value(serde_json::json!({
-            "agentId": GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
-            "taskId": GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
-            "sessionId": session_id,
-            "runId": "run-pending-after-completed",
-            "source": AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE,
-            "runProfile": run_profile,
-            "task": "继续补齐游戏功能",
-            "status": "pending",
-            "phase": "queued"
-        }))
-        .expect("deserialize pending task fixture"),
-    );
-    let runtimes = vec![completed];
-
-    assert!(runtime_is_busy(&runtimes[0]));
-    assert!(
-        swarm_parent_steer_target(
-            GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
-            session_id,
-            run_profile,
-            None,
-            None,
-            &runtimes,
-        )
-        .is_none(),
-        "queued work must not make the completed canonical run steerable"
-    );
-    assert_eq!(
-        matching_pending_swarm_run_id(
-            &runtimes[0],
-            session_id,
-            run_profile,
-            None,
-            "继续补齐游戏功能",
-        ),
-        Some("run-pending-after-completed")
-    );
-}
-
-#[test]
-fn new_turn_uses_accepted_run_id_instead_of_stale_canonical_state() {
-    let mut started = runtime("cancelled", "cancelled", 1);
-    started.state.agent_id = GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string();
-    started.state.run_id = "run-old-cancelled".to_string();
-    started.accepted_run_id = Some("run-new-pending".to_string());
-
-    assert_eq!(
-        accepted_swarm_run_id(&started, "run-requested"),
-        "run-new-pending"
-    );
-    started.accepted_run_id = None;
-    assert_eq!(
-        accepted_swarm_run_id(&started, "run-requested"),
-        "run-requested"
-    );
-}
-
-#[test]
-fn queued_start_returns_actual_accepted_run_id_after_collision() {
-    let root = std::env::temp_dir().join(format!(
-        "swarm-cli-accepted-run-id-{}-{}",
-        std::process::id(),
-        unix_millis()
-    ));
-    init_local_game_project_at(&root, "project-accepted-run-id", "Accepted run ID")
-        .expect("initialize accepted run ID project");
-    let runtime_lock = try_acquire_game_creator_agent_runtime_task_lock(
-        &root,
-        GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
-    )
-    .expect("acquire supervisor runtime lock")
-    .expect("supervisor runtime lock available");
-
-    let first = start_game_creator_supervisor_background_task_for_session_at(
-        &root,
-        None,
-        "第一条排队任务",
-        "run-collision",
-        AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE,
-        AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD,
-    )
-    .expect("queue first colliding run");
-    let second = start_game_creator_supervisor_background_task_for_session_at(
-        &root,
-        None,
-        "第二条排队任务",
-        "run-collision",
-        AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE,
-        AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD,
-    )
-    .expect("queue second colliding run");
-
-    assert_eq!(first.accepted_run_id.as_deref(), Some("run-collision"));
-    let second_run_id = second
-        .accepted_run_id
-        .as_deref()
-        .expect("second accepted run ID");
-    assert_ne!(second_run_id, "run-collision");
-    assert!(second_run_id.starts_with("run-collision-dup-"));
-    let serialized = serde_json::to_value(&second).expect("serialize queued start result");
-    assert_eq!(serialized["acceptedRunId"], second_run_id);
-    assert!(serialized.get("accepted_run_id").is_none());
-
-    drop(runtime_lock);
-    fs::remove_dir_all(root).ok();
-}
-
-#[test]
-fn completed_target_turn_settles_after_canonical_advances_to_next_run() {
-    let root = std::env::temp_dir().join(format!(
-        "swarm-cli-consecutive-snapshot-{}-{}",
-        std::process::id(),
-        unix_millis()
-    ));
-    init_local_game_project_at(
-        &root,
-        "project-consecutive-snapshot",
-        "Consecutive run snapshot",
-    )
-    .expect("initialize consecutive snapshot project");
-    let session_id = "session-consecutive-runs";
-    let run_profile = AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD;
-    let completed_task = serde_json::from_value::(serde_json::json!({
-        "agentId": GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
-        "taskId": GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
-        "sessionId": session_id,
-        "runId": "run-target-completed",
-        "source": AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE,
-        "runProfile": run_profile,
-        "task": "先完成这一轮",
-        "status": "completed",
-        "phase": "completed",
-        "currentAction": "本轮已经完成"
-    }))
-    .expect("deserialize completed target task");
-    let next_task = serde_json::from_value::(serde_json::json!({
-        "agentId": GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
-        "taskId": GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
-        "sessionId": session_id,
-        "runId": "run-next-running",
-        "source": AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE,
-        "runProfile": run_profile,
-        "task": "随后执行下一轮",
-        "status": "running",
-        "phase": "planning",
-        "currentAction": "下一轮正在执行"
-    }))
-    .expect("deserialize next running task");
-    let mut current = runtime("running", "planning", 0);
-    current.state.agent_id = GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string();
-    current.state.session_id = session_id.to_string();
-    current.state.run_id = "run-next-running".to_string();
-    current.state.run_profile = run_profile.to_string();
-    let task_path =
-        game_creator_agent_runtime_task_path(&root, GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID);
-    fs::create_dir_all(task_path.parent().expect("task journal parent"))
-        .expect("create task journal parent");
-    fs::write(
-        &task_path,
-        format!(
-            "{}\n{}\n",
-            serde_json::to_string(&completed_task).expect("serialize completed target task"),
-            serde_json::to_string(&next_task).expect("serialize next target task"),
-        ),
-    )
-    .expect("persist full task journal");
-    current.recent_tasks = vec![next_task];
-    let runtimes = vec![current];
-
-    assert!(!swarm_turn_is_busy(
-        &root,
-        GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
-        session_id,
-        run_profile,
-        None,
-        "run-target-completed",
-        &runtimes,
-    )
-    .expect("read completed target busy state"));
-    assert!(swarm_turn_is_busy(
-        &root,
-        GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
-        session_id,
-        run_profile,
-        None,
-        "run-next-running",
-        &runtimes,
-    )
-    .expect("read next target busy state"));
-    let snapshot = swarm_parent_runtime_snapshot_for_run(
-        &root,
-        GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
-        session_id,
-        run_profile,
-        None,
-        "run-target-completed",
-        &runtimes,
-    )
-    .expect("read target task journal")
-    .expect("recover completed target from task journal");
-    assert!(parent_runtime_completed(&snapshot));
-
-    fs::remove_dir_all(root).ok();
-}
-
-#[test]
-fn running_parent_remains_the_only_valid_swarm_steer_target() {
-    let session_id = "session-running-steer";
-    let run_profile = AGENT_RUNTIME_RUN_PROFILE_STANDARD;
-    let mut running = runtime("running", "waiting-for-provider-retry", 0);
-    running.state.agent_id = GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string();
-    running.state.session_id = session_id.to_string();
-    running.state.run_id = "run-running-steer".to_string();
-    running.state.run_profile = run_profile.to_string();
-    let runtimes = vec![running];
-
-    assert_eq!(
-        swarm_parent_steer_target(
-            GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
-            session_id,
-            run_profile,
-            None,
-            Some("run-running-steer"),
-            &runtimes,
-        )
-        .map(|runtime| runtime.state.run_id.as_str()),
-        Some("run-running-steer")
-    );
-    assert!(swarm_parent_steer_target(
-        GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
-        session_id,
-        run_profile,
-        None,
-        Some("another-run"),
-        &runtimes,
-    )
-    .is_none());
-}
-
-#[test]
-fn parses_chat_commands_without_stealing_normal_messages() {
-    assert_eq!(parse_swarm_chat_input("  "), None);
-    assert_eq!(
-        parse_swarm_chat_input("/agents"),
-        Some(SwarmChatInput::Agents)
-    );
-    assert_eq!(parse_swarm_chat_input("/exit"), Some(SwarmChatInput::Quit));
-    assert_eq!(
-        parse_swarm_chat_input("/status"),
-        Some(SwarmChatInput::Status)
-    );
-    assert_eq!(
-        parse_swarm_chat_input("/compact"),
-        Some(SwarmChatInput::Compact)
-    );
-    assert_eq!(
-        parse_swarm_chat_input("让策划和程序并行检查玩法"),
-        Some(SwarmChatInput::Message(
-            "让策划和程序并行检查玩法".to_string()
-        ))
-    );
-}
-
-#[test]
-fn parses_goal_commands_and_keeps_goal_namespace_out_of_messages() {
-    assert_eq!(
-        parse_swarm_chat_input("/goal"),
-        Some(SwarmChatInput::Goal(SwarmGoalCommand::Status))
-    );
-    assert_eq!(
-        parse_swarm_chat_input("/goal status"),
-        Some(SwarmChatInput::Goal(SwarmGoalCommand::Status))
-    );
-    assert_eq!(
-        parse_swarm_chat_input("/goal 完成可玩的战斗循环"),
-        Some(SwarmChatInput::Goal(SwarmGoalCommand::Start(
-            "完成可玩的战斗循环".to_string()
-        )))
-    );
-    assert_eq!(
-        parse_swarm_chat_input("/goal edit 增加键盘与触屏验收"),
-        Some(SwarmChatInput::Goal(SwarmGoalCommand::Edit(
-            "增加键盘与触屏验收".to_string()
-        )))
-    );
-    assert_eq!(
-        parse_swarm_chat_input("/goal pause"),
-        Some(SwarmChatInput::Goal(SwarmGoalCommand::Pause))
-    );
-    assert_eq!(
-        parse_swarm_chat_input("/goal resume"),
-        Some(SwarmChatInput::Goal(SwarmGoalCommand::Resume))
-    );
-    assert_eq!(
-        parse_swarm_chat_input("/goal clear"),
-        Some(SwarmChatInput::Goal(SwarmGoalCommand::Clear))
-    );
-
-    for invalid in [
-        "/goal-status",
-        "/goal/status",
-        "/goal edit",
-        "/goal pause now",
-    ] {
-        assert!(matches!(
-            parse_swarm_chat_input(invalid),
-            Some(SwarmChatInput::InvalidGoal(_))
-        ));
-        assert!(!matches!(
-            parse_swarm_chat_input(invalid),
-            Some(SwarmChatInput::Message(_))
-        ));
-    }
-}
-
-#[test]
-fn swarm_help_lists_the_complete_goal_control_surface() {
-    let mut output = Vec::new();
-    print_swarm_chat_help(&mut output).expect("print swarm help");
-    let output = String::from_utf8(output).expect("help output is utf-8");
-
-    for command in [
-        "/status",
-        "/compact",
-        "/goal <目标>",
-        "/goal status",
-        "/goal edit <目标>",
-        "/goal pause",
-        "/goal resume",
-        "/goal clear",
-    ] {
-        assert!(output.contains(command), "missing help command: {command}");
-    }
-}
-
-#[test]
-fn goal_status_prints_identity_outcome_and_completion_standard() {
-    let goal = AgentGoalRecord {
-        schema_version: AGENT_GOAL_SCHEMA_VERSION.to_string(),
-        project_id: "project-1".to_string(),
-        goal_id: "goal-1".to_string(),
-        agent_id: "project-supervisor".to_string(),
-        session_id: "session-1".to_string(),
-        run_id: "run-1".to_string(),
-        revision: 3,
-        status: AGENT_GOAL_STATUS_ACTIVE.to_string(),
-        outcome: "完成首个可玩版本".to_string(),
-        constraints: vec!["不新增平行 Runtime".to_string()],
-        verification: vec!["键盘与触屏均可完成一局".to_string()],
-        completion_evidence: Vec::new(),
-        response_fingerprint: None,
-        created_at: 1,
-        pause_requested_at: None,
-        paused_at: None,
-        completed_at: None,
-        cleared_at: None,
-        error: None,
-        updated_at: 2,
-    };
-    let mut output = Vec::new();
-    print_swarm_goal_status("session-1", Some(&goal), &mut output).expect("print goal status");
-    let output = String::from_utf8(output).expect("goal output is utf-8");
-
-    assert!(output.contains("goal=goal-1 run=run-1 revision=3 status=active"));
-    assert!(output.contains("[Goal 目标] 完成首个可玩版本"));
-    assert!(output.contains("[Goal 约束] 不新增平行 Runtime"));
-    assert!(output.contains("[Goal 完成标准] 键盘与触屏均可完成一局"));
-}
-
-#[test]
-fn swarm_stays_busy_for_active_queue_and_reconciliation() {
-    assert!(runtimes_are_busy(&[runtime("running", "planning", 0)]));
-    assert!(runtimes_are_busy(&[runtime("idle", "completed", 1)]));
-    assert!(runtimes_are_busy(&[runtime(
-        "failed",
-        "needs-reconciliation",
-        0
-    )]));
-    assert!(!runtimes_are_busy(&[runtime("idle", "completed", 0)]));
-    assert!(!runtimes_are_busy(&[runtime("failed", "failed", 0)]));
-}
-
-#[test]
-fn active_turn_eof_closes_input_once_without_requesting_quit() {
-    let mut input_closed = false;
-    let mut observer = SwarmRuntimeObserver::default();
-    let mut output = Vec::new();
-
-    mark_swarm_turn_input_closed(&mut input_closed, &mut observer, &mut output)
-        .expect("close active turn input");
-    mark_swarm_turn_input_closed(&mut input_closed, &mut observer, &mut output)
-        .expect("repeat closed input is idempotent");
-
-    assert!(input_closed);
-    let output = String::from_utf8(output).expect("input close output is utf-8");
-    assert_eq!(output.matches("[输入已关闭]").count(), 1);
-    assert!(output.contains("继续运行,等待可信终态"));
-    assert!(!output.contains("已退出 Agent Swarm Chat"));
-}
-
-#[test]
-fn active_turn_eof_keeps_observing_until_parent_completes() {
-    let root = std::env::temp_dir().join(format!(
-        "swarm-cli-eof-active-{}-{}",
-        std::process::id(),
-        unix_millis()
-    ));
-    init_local_game_project_at(&root, "project-swarm-eof", "Swarm EOF active turn")
-        .expect("initialize EOF project");
-    for group in GAME_CREATOR_AGENT_GROUP_DEFINITIONS {
-        for role in group.roles {
-            let idle = default_game_creator_agent_runtime_state(role.task_id, "run-eof-idle");
-            write_game_creator_agent_runtime_state(&root, &idle)
-                .expect("persist valid idle specialist state");
-        }
-    }
-    let parent_agent_id = GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID;
-    let before = append_local_conversation_message_at(
-        &root,
-        Some(parent_agent_id),
-        LocalConversationMessage {
-            role: "user".to_string(),
-            content: "继续完成当前项目".to_string(),
-            agent_id: Some(parent_agent_id.to_string()),
-        },
-    )
-    .expect("append turn user message");
-    let session_id = before.session_id.clone().expect("active parent session");
-    let mut parent = runtime("running", "planning", 0).state;
-    parent.agent_id = parent_agent_id.to_string();
-    parent.task_id = parent_agent_id.to_string();
-    parent.session_id = session_id.clone();
-    parent.run_id = "run-eof-active".to_string();
-    parent.source = "agent-background-task".to_string();
-    parent.current_task = "继续完成当前项目".to_string();
-    write_game_creator_agent_runtime_state(&root, &parent).expect("persist active parent");
-    let conversation_baseline =
-        new_swarm_turn_conversation_baseline(before.messages.len(), &parent.run_id);
-
-    let completion_root = root.clone();
-    let completion_session_id = session_id.clone();
-    let completion = std::thread::spawn(move || {
-        std::thread::sleep(Duration::from_millis(15));
-        append_local_conversation_message_for_session_at(
-            &completion_root,
-            Some(parent_agent_id),
-            Some(&completion_session_id),
-            LocalConversationMessage {
-                role: "assistant".to_string(),
-                content: "已完成可信终态".to_string(),
-                agent_id: Some(parent_agent_id.to_string()),
-            },
-        )
-        .expect("append terminal assistant message");
-        parent.status = "idle".to_string();
-        parent.phase = "completed".to_string();
-        parent.updated_at = unix_timestamp();
-        write_game_creator_agent_runtime_state(&completion_root, &parent)
-            .expect("persist completed parent");
-    });
-    let (tx, rx) = mpsc::channel();
-    tx.send(SwarmInputEvent::Eof).expect("send active turn EOF");
-    let mut observer = SwarmRuntimeObserver::default();
-    let mut output = Vec::new();
-
-    let outcome = wait_for_swarm_turn(
-        &root,
-        parent_agent_id,
-        &session_id,
-        AGENT_RUNTIME_RUN_PROFILE_STANDARD,
-        None,
-        conversation_baseline,
-        &rx,
-        &mut output,
-        &mut observer,
-        Duration::from_millis(2),
-        Duration::from_millis(8),
-    )
-    .expect("observe active turn after EOF");
-    completion.join().expect("join completion writer");
-
-    let output = String::from_utf8(output).expect("EOF turn output is utf-8");
-    let runtime_diagnostics = read_game_creator_agent_runtimes_at(&root)
-        .expect("read terminal runtime diagnostics")
-        .into_iter()
-        .filter(|runtime| runtime.state.phase == "needs-reconciliation")
-        .map(|runtime| {
-            format!(
-                "{}:{}",
-                runtime.state.agent_id,
-                runtime.state.error.unwrap_or_default()
-            )
-        })
-        .collect::>();
-    assert!(
-        matches!(outcome, SwarmTurnOutcome::Settled(_)),
-        "unexpected outcome: {outcome:?}; diagnostics={runtime_diagnostics:?}; output={output}"
-    );
-    assert!(output.contains("[输入已关闭]"));
-    assert!(output.contains("已完成可信终态"));
-    assert!(!output.contains("已退出 Agent Swarm Chat"));
-
-    fs::remove_dir_all(root).ok();
-}
-
-#[test]
-fn recovered_prebaseline_assistant_counts_once_without_duplicate_output() {
-    let root = std::env::temp_dir().join(format!(
-        "swarm-cli-recovered-assistant-{}-{}",
-        std::process::id(),
-        unix_millis()
-    ));
-    init_local_game_project_at(
-        &root,
-        "project-swarm-recovered-assistant",
-        "Swarm recovered assistant",
-    )
-    .expect("initialize recovered assistant project");
-    let parent_agent_id = GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID;
-    let conversation = append_local_conversation_message_at(
-        &root,
-        Some(parent_agent_id),
-        LocalConversationMessage {
-            role: "assistant".to_string(),
-            content: "已在恢复阶段持久化".to_string(),
-            agent_id: Some(parent_agent_id.to_string()),
-        },
-    )
-    .expect("append recovered assistant");
-    let session_id = conversation.session_id.expect("active parent session");
-    let mut baseline =
-        new_swarm_turn_conversation_baseline(conversation.messages.len(), "run-recovered");
-    baseline.recovered_assistant = Some(SwarmRecoveredAssistant {
-        run_id: "run-recovered".to_string(),
-        finalization_id: "finalization-recovered".to_string(),
-        message_id: "message-recovered".to_string(),
-        content: "已在恢复阶段持久化".to_string(),
-    });
-
-    let snapshot = read_turn_conversation_snapshot(&root, parent_agent_id, &session_id, &baseline)
-        .expect("read recovered conversation snapshot");
-    assert_eq!(snapshot.metrics.new_assistant_message_count, 1);
-    assert_eq!(
-        snapshot.metrics.final_reply_chars,
-        "已在恢复阶段持久化".chars().count()
-    );
-    assert_eq!(snapshot.final_reply.as_deref(), Some("已在恢复阶段持久化"));
-    assert!(snapshot.recovered_before_observation);
-
-    let mut output = Vec::new();
-    let mut observer = SwarmRuntimeObserver::default();
-    let metrics = print_new_parent_reply(
-        &root,
-        parent_agent_id,
-        &session_id,
-        &baseline,
-        &mut output,
-        &mut observer,
-    )
-    .expect("print recovered parent reply");
-    assert_eq!(metrics, snapshot.metrics);
-    let output = String::from_utf8(output).expect("recovered output is utf-8");
-    assert!(output.contains("父 Agent 回复已在恢复前持久化"));
-    assert!(!output.contains("已在恢复阶段持久化"));
-
-    fs::remove_dir_all(root).ok();
-}
-
-#[test]
-fn recovered_assistant_cannot_overlap_a_new_terminal_reply() {
-    let root = std::env::temp_dir().join(format!(
-        "swarm-cli-recovered-overlap-{}-{}",
-        std::process::id(),
-        unix_millis()
-    ));
-    init_local_game_project_at(
-        &root,
-        "project-swarm-recovered-overlap",
-        "Swarm recovered overlap",
-    )
-    .expect("initialize recovered overlap project");
-    let parent_agent_id = GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID;
-    let before =
-        read_local_conversation_for_session_at(root.as_path(), Some(parent_agent_id), None)
-            .expect("read initial conversation");
-    let session_id = before.session_id.expect("active parent session");
-    append_local_conversation_message_for_session_at(
-        &root,
-        Some(parent_agent_id),
-        Some(&session_id),
-        LocalConversationMessage {
-            role: "assistant".to_string(),
-            content: "baseline 后的新回复".to_string(),
-            agent_id: Some(parent_agent_id.to_string()),
-        },
-    )
-    .expect("append new terminal reply");
-    let mut baseline = new_swarm_turn_conversation_baseline(before.messages.len(), "run-overlap");
-    baseline.recovered_assistant = Some(SwarmRecoveredAssistant {
-        run_id: "run-overlap".to_string(),
-        finalization_id: "finalization-overlap".to_string(),
-        message_id: "message-overlap".to_string(),
-        content: "恢复回复".to_string(),
-    });
-
-    let error = read_turn_conversation_snapshot(&root, parent_agent_id, &session_id, &baseline)
-        .expect_err("recovered and new assistant replies must not be double counted");
-    assert!(error.contains("与 baseline 后的新回复重叠"));
-
-    fs::remove_dir_all(root).ok();
-}
-
-#[test]
-fn conversation_snapshot_scopes_consecutive_run_replies_by_message_id() {
-    let target_message_id = game_creator_agent_runtime_finalization_message_id(
-        GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
-        "session-consecutive",
-        "run-b",
-    );
-    let next_message_id = game_creator_agent_runtime_finalization_message_id(
-        GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
-        "session-consecutive",
-        "run-c",
-    );
-    let (metrics, reply) = summarize_scoped_new_assistant_messages(
-        [
-            (
-                "assistant",
-                "B 的最终回复",
-                Some(target_message_id.as_str()),
-            ),
-            ("assistant", "C 的最终回复", Some(next_message_id.as_str())),
-        ],
-        Some(&target_message_id),
-    );
-    assert_eq!(metrics.new_assistant_message_count, 1);
-    assert_eq!(metrics.final_reply_chars, "B 的最终回复".chars().count());
-    assert_eq!(reply, Some("B 的最终回复"));
-
-    let (missing_metrics, missing_reply) = summarize_scoped_new_assistant_messages(
-        [("assistant", "C 的最终回复", Some(next_message_id.as_str()))],
-        Some(&target_message_id),
-    );
-    assert_eq!(missing_metrics, SwarmTurnConversationMetrics::default());
-    assert_eq!(missing_reply, None);
-
-    let (legacy_metrics, legacy_reply) = summarize_scoped_new_assistant_messages(
-        [("assistant", "旧格式回复", None)],
-        Some(&target_message_id),
-    );
-    assert_eq!(legacy_metrics.new_assistant_message_count, 1);
-    assert_eq!(legacy_reply, Some("旧格式回复"));
-}
-
-#[test]
-fn wait_for_turn_keeps_consecutive_run_reply_and_report_scoped() {
-    let root = std::env::temp_dir().join(format!(
-        "swarm-cli-consecutive-wait-{}-{}",
-        std::process::id(),
-        unix_millis()
-    ));
-    init_local_game_project_at(&root, "project-consecutive-wait", "Consecutive wait")
-        .expect("initialize consecutive wait project");
-    let parent_agent_id = GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID;
-    let before = append_local_conversation_message_at(
-        &root,
-        Some(parent_agent_id),
-        LocalConversationMessage {
-            role: "user".to_string(),
-            content: "先完成 B".to_string(),
-            agent_id: None,
-        },
-    )
-    .expect("append B user turn");
-    let session_id = before.session_id.expect("active supervisor session");
-    let baseline = new_swarm_turn_conversation_baseline(before.messages.len(), "run-b");
-    for (run_id, reply) in [("run-b", "B 的最终回复"), ("run-c", "C 的最终回复")] {
-        append_local_conversation_message_for_session_idempotent_at(
-            &root,
-            Some(parent_agent_id),
-            Some(&session_id),
-            LocalConversationMessage {
-                role: "assistant".to_string(),
-                content: reply.to_string(),
-                agent_id: None,
-            },
-            &game_creator_agent_runtime_finalization_message_id(
-                parent_agent_id,
-                &session_id,
-                run_id,
-            ),
-        )
-        .expect("append consecutive assistant reply");
-    }
-
-    let task_path = game_creator_agent_runtime_task_path(&root, parent_agent_id);
-    fs::create_dir_all(task_path.parent().expect("consecutive journal parent"))
-        .expect("create consecutive journal parent");
-    let tasks = [("run-b", "先完成 B"), ("run-c", "再完成 C")]
-        .into_iter()
-        .map(|(run_id, task)| {
-            serde_json::from_value::(serde_json::json!({
-                "agentId": parent_agent_id,
-                "taskId": parent_agent_id,
-                "sessionId": session_id,
-                "runId": run_id,
-                "source": AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE,
-                "runProfile": AGENT_RUNTIME_RUN_PROFILE_STANDARD,
-                "task": task,
-                "status": "completed",
-                "phase": "completed",
-                "currentAction": "本轮已完成",
-                "updatedAt": if run_id == "run-b" { 100 } else { 200 }
-            }))
-            .expect("deserialize consecutive task")
-        })
-        .collect::>();
-    fs::write(
-        &task_path,
-        tasks
-            .iter()
-            .map(|task| serde_json::to_string(task).expect("serialize consecutive task"))
-            .collect::>()
-            .join("\n")
-            + "\n",
-    )
-    .expect("persist consecutive task journal");
-    let mut current = default_game_creator_agent_runtime_state(parent_agent_id, "run-c");
-    current.session_id = session_id.clone();
-    current.source = AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE.to_string();
-    current.run_profile = AGENT_RUNTIME_RUN_PROFILE_STANDARD.to_string();
-    current.status = "idle".to_string();
-    current.phase = "completed".to_string();
-    current.current_task = "再完成 C".to_string();
-    current.current_action = "C 已完成".to_string();
-    current.updated_at = 200;
-    write_game_creator_agent_runtime_state(&root, ¤t)
-        .expect("persist consecutive canonical state");
-
-    let (_tx, rx) = mpsc::channel();
-    let mut observer = SwarmRuntimeObserver::default();
-    let mut output = Vec::new();
-    let outcome = wait_for_swarm_turn(
-        &root,
-        parent_agent_id,
-        &session_id,
-        AGENT_RUNTIME_RUN_PROFILE_STANDARD,
-        None,
-        baseline,
-        &rx,
-        &mut output,
-        &mut observer,
-        Duration::from_millis(1),
-        Duration::ZERO,
-    )
-    .expect("settle B after canonical advanced to C");
-    let SwarmTurnOutcome::Settled(report) = outcome else {
-        panic!("B must settle independently: {outcome:?}");
-    };
-    assert_eq!(report.parent_run_id.as_deref(), Some("run-b"));
-    assert_eq!(report.new_assistant_message_count, 1);
-    assert_eq!(report.runtime_count, 1);
-    let output = String::from_utf8(output).expect("consecutive wait output is utf-8");
-    assert!(output.contains("B 的最终回复"));
-    assert!(!output.contains("C 的最终回复"));
-
-    fs::remove_dir_all(root).ok();
-}
-
-#[test]
-fn confirmation_prompt_propagates_eof_as_closed_input() {
-    let (tx, rx) = mpsc::channel();
-    tx.send(SwarmInputEvent::Eof).expect("send eof");
-    let mut output = Vec::new();
-
-    let decision = prompt_swarm_decision(
-        Path::new("."),
-        GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
-        &rx,
-        &mut output,
-        "confirm> ",
-    )
-    .expect("EOF is a turn input state, not an error");
-
-    assert!(matches!(decision, SwarmPromptDecision::InputClosed));
-}
-
-#[test]
-fn terminal_classifier_requires_completed_parent_unique_reply_and_clear_contract() {
-    let mut parent = runtime("idle", "completed", 0);
-    parent.state.agent_id = GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string();
-    parent.state.session_id = "session-terminal".to_string();
-    parent.state.run_id = "run-terminal".to_string();
-    let unique_reply = SwarmTurnConversationMetrics {
-        new_assistant_message_count: 1,
-        final_reply_chars: 12,
-    };
-
-    assert_eq!(
-        classify_swarm_turn_terminal(Some(&parent), unique_reply, 0, 0, 0),
-        SwarmTurnTerminalClassification::Settled
-    );
-    assert_eq!(
-        classify_swarm_turn_terminal(Some(&parent), unique_reply, 0, 1, 0),
-        SwarmTurnTerminalClassification::Incomplete
-    );
-    assert_eq!(
-        classify_swarm_turn_terminal(Some(&parent), unique_reply, 0, 0, 1),
-        SwarmTurnTerminalClassification::Incomplete
-    );
-    assert_eq!(
-        classify_swarm_turn_terminal(
-            Some(&parent),
-            SwarmTurnConversationMetrics::default(),
-            0,
-            0,
-            0,
-        ),
-        SwarmTurnTerminalClassification::Incomplete
-    );
-    assert_eq!(
-        classify_swarm_turn_terminal(
-            Some(&parent),
-            SwarmTurnConversationMetrics {
-                new_assistant_message_count: 2,
-                final_reply_chars: 12,
-            },
-            0,
-            0,
-            0,
-        ),
-        SwarmTurnTerminalClassification::Incomplete
-    );
-    assert_eq!(
-        classify_swarm_turn_terminal(None, unique_reply, 0, 0, 0),
-        SwarmTurnTerminalClassification::Incomplete
-    );
-}
-
-#[test]
-fn terminal_classifier_fails_parent_failure_cancel_and_budget_exhaustion() {
-    let metrics = SwarmTurnConversationMetrics {
-        new_assistant_message_count: 1,
-        final_reply_chars: 8,
-    };
-    for (status, phase) in [
-        ("failed", "failed"),
-        ("cancelled", "cancelled"),
-        ("failed", "budget-exhausted"),
-    ] {
-        let parent = runtime(status, phase, 0);
-        assert_eq!(
-            classify_swarm_turn_terminal(Some(&parent), metrics, 0, 0, 0),
-            SwarmTurnTerminalClassification::Failed,
-            "parent {status}/{phase} must fail closed"
-        );
-    }
-    let completed = runtime("idle", "completed", 0);
-    assert_eq!(
-        classify_swarm_turn_terminal(Some(&completed), metrics, 1, 0, 0),
-        SwarmTurnTerminalClassification::Failed
-    );
-}
-
-#[test]
-fn pending_interactions_never_form_a_settled_snapshot() {
-    let mut parent = runtime("waiting-for-user-input", "waiting-for-user-input", 0);
-    parent.state.agent_id = GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string();
-    let mut child = runtime("waiting-for-user-input", "waiting-for-user-input", 0);
-    child.state.agent_id = "code-prototype".to_string();
-    let mut confirmation = runtime("waiting-for-confirmation", "waiting-for-confirmation", 0);
-    confirmation.state.agent_id = "quality-review".to_string();
-
-    assert!(swarm_unhandled_interaction_reasons(
-        GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
-        &[parent.clone()],
-        false,
-    )
-    .is_empty());
-    let child_reasons = swarm_unhandled_interaction_reasons(
-        GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
-        &[child],
-        false,
-    );
-    assert_eq!(child_reasons, vec!["pending-user-input:code-prototype"]);
-    let closed_reasons = swarm_unhandled_interaction_reasons(
-        GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
-        &[parent, confirmation],
-        true,
-    );
-    assert!(closed_reasons
-        .iter()
-        .any(|reason| reason == "pending-user-input:project-supervisor"));
-    assert!(closed_reasons
-        .iter()
-        .any(|reason| reason == "pending-confirmation:quality-review"));
-}
-
-#[test]
-fn original_specialist_failure_is_recoverable_but_repair_failure_closes() {
-    let mut parent = runtime("running", "waiting-for-delegate-receipts", 0);
-    parent.state.agent_id = GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string();
-    parent.state.session_id = "session-parent".to_string();
-    parent.state.run_id = "run-parent".to_string();
-    let mut child = runtime("failed", "failed", 0);
-    child.state.agent_id = "code-prototype".to_string();
-    child.state.session_id = "session-child".to_string();
-    child.state.run_id = "run-child".to_string();
-    child.state.source = "agent-delegate".to_string();
-    child.state.parent_agent_id = Some(parent.state.agent_id.clone());
-    child.state.parent_run_id = Some(parent.state.run_id.clone());
-    child.state.delegation_id = Some("delivery-original".to_string());
-    let acceptance = vec!["交付可运行原型".to_string()];
-    let original = new_static_delegate_delivery_with_contract(
-        &parent.state.agent_id,
-        &parent.state.session_id,
-        &parent.state.run_id,
-        "action-original",
-        "delivery-original",
-        &child.state.agent_id,
-        &child.state.session_id,
-        &child.state.run_id,
-        &acceptance,
-        &[],
-        None,
-    );
-
-    assert_eq!(
-        classify_failed_specialist(&parent, &child, Some(&original), false),
-        SwarmSpecialistFailureDisposition::Recoverable
-    );
-    let mut completed_parent = parent.clone();
-    completed_parent.state.status = "idle".to_string();
-    completed_parent.state.phase = "completed".to_string();
-    assert_eq!(
-        classify_failed_specialist(&completed_parent, &child, Some(&original), false),
-        SwarmSpecialistFailureDisposition::Incomplete
-    );
-
-    child.state.run_id = "run-repair".to_string();
-    child.state.delegation_id = Some("delivery-repair".to_string());
-    let repair = new_static_delegate_delivery_with_contract(
-        &parent.state.agent_id,
-        &parent.state.session_id,
-        &parent.state.run_id,
-        "action-repair",
-        "delivery-repair",
-        &child.state.agent_id,
-        &child.state.session_id,
-        &child.state.run_id,
-        &acceptance,
-        &[],
-        Some("delivery-original"),
-    );
-    assert_eq!(
-        classify_failed_specialist(&parent, &child, Some(&repair), false),
-        SwarmSpecialistFailureDisposition::Failed
-    );
-
-    let mut successful_repair = repair.clone();
-    successful_repair.terminal_status = Some("completed".to_string());
-    let mut evidence_ready = StaticDelegateStructuredResult::default();
-    evidence_ready.contract_status = StaticDelegateContractStatus::EvidenceReady;
-    successful_repair.structured_result = Some(evidence_ready);
-    assert!(original_delivery_has_successful_repair(
-        &original,
-        &[successful_repair.clone()]
-    ));
-
-    let mut unknown_original = original.clone();
-    let mut unknown_result = StaticDelegateStructuredResult::default();
-    unknown_result.contract_status =
-        StaticDelegateContractStatus::Unknown("future-contract-status".to_string());
-    unknown_original.structured_result = Some(unknown_result);
-    assert!(
-        !original_delivery_has_successful_repair(&unknown_original, &[successful_repair]),
-        "a newer contract status must not be classified as already repaired"
-    );
-}
-
-#[test]
-fn observer_failure_scan_waits_for_original_repair_and_fails_repair_child() {
-    let root = std::env::temp_dir().join(format!(
-        "swarm-cli-repair-scan-{}-{}",
-        std::process::id(),
-        unix_millis()
-    ));
-    init_local_game_project_at(&root, "project-swarm-repair", "Swarm repair scan")
-        .expect("initialize repair scan project");
-    let mut parent = runtime("running", "waiting-for-delegate-receipts", 0);
-    parent.state.agent_id = GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string();
-    parent.state.session_id = "session-parent".to_string();
-    parent.state.run_id = "run-parent".to_string();
-    let mut child = runtime("failed", "failed", 0);
-    child.state.agent_id = "code-prototype".to_string();
-    child.state.session_id = "session-child".to_string();
-    child.state.run_id = "run-child".to_string();
-    child.state.source = "agent-delegate".to_string();
-    child.state.parent_agent_id = Some(parent.state.agent_id.clone());
-    child.state.parent_run_id = Some(parent.state.run_id.clone());
-    child.state.delegation_id = Some("delivery-original".to_string());
-    let acceptance = vec!["交付可运行原型".to_string()];
-    let original = new_static_delegate_delivery_with_contract(
-        &parent.state.agent_id,
-        &parent.state.session_id,
-        &parent.state.run_id,
-        "action-original",
-        "delivery-original",
-        &child.state.agent_id,
-        &child.state.session_id,
-        &child.state.run_id,
-        &acceptance,
-        &[],
-        None,
-    );
-    create_or_read_static_delegate_delivery_at(&root, &original)
-        .expect("persist original delivery");
-
-    let original_scan = scan_swarm_terminal_failures_at(
-        &root,
-        &parent.state.agent_id,
-        &parent.state.session_id,
-        AGENT_RUNTIME_RUN_PROFILE_STANDARD,
-        None,
-        &parent.state.run_id,
-        &[parent.clone(), child.clone()],
-    );
-    assert!(original_scan.failed_agents.is_empty());
-    assert!(original_scan.incomplete_reasons.is_empty());
-    assert!(original_scan.reconciliation_agents.is_empty());
-
-    child.state.run_id = "run-repair".to_string();
-    child.state.delegation_id = Some("delivery-repair".to_string());
-    let repair = new_static_delegate_delivery_with_contract(
-        &parent.state.agent_id,
-        &parent.state.session_id,
-        &parent.state.run_id,
-        "action-repair",
-        "delivery-repair",
-        &child.state.agent_id,
-        &child.state.session_id,
-        &child.state.run_id,
-        &acceptance,
-        &[],
-        Some("delivery-original"),
-    );
-    create_or_read_static_delegate_delivery_at(&root, &repair).expect("persist repair delivery");
-    let repair_scan = scan_swarm_terminal_failures_at(
-        &root,
-        &parent.state.agent_id,
-        &parent.state.session_id,
-        AGENT_RUNTIME_RUN_PROFILE_STANDARD,
-        None,
-        &parent.state.run_id,
-        &[parent.clone(), child],
-    );
-    assert_eq!(repair_scan.failed_agents, vec!["code-prototype:failed"]);
-    assert!(repair_scan.incomplete_reasons.is_empty());
-    assert!(repair_scan.reconciliation_agents.is_empty());
-
-    fs::remove_dir_all(root).ok();
-}
-
-#[test]
-fn failure_scan_reads_all_historical_runs_for_the_same_specialist() {
-    let root = std::env::temp_dir().join(format!(
-        "swarm-cli-historical-repair-scan-{}-{}",
-        std::process::id(),
-        unix_millis()
-    ));
-    init_local_game_project_at(&root, "project-historical-repair", "Historical repair scan")
-        .expect("initialize historical repair project");
-    let mut parent = runtime("running", "waiting-for-delegate-receipts", 0);
-    parent.state.agent_id = GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string();
-    parent.state.session_id = "session-historical-parent".to_string();
-    parent.state.run_id = "run-historical-parent".to_string();
-    let acceptance = vec!["交付可运行原型".to_string()];
-    let original = new_static_delegate_delivery_with_contract(
-        &parent.state.agent_id,
-        &parent.state.session_id,
-        &parent.state.run_id,
-        "action-historical-original",
-        "delivery-historical-original",
-        "code-prototype",
-        "session-historical-child",
-        "run-historical-original",
-        &acceptance,
-        &[],
-        None,
-    );
-    let repair = new_static_delegate_delivery_with_contract(
-        &parent.state.agent_id,
-        &parent.state.session_id,
-        &parent.state.run_id,
-        "action-historical-repair",
-        "delivery-historical-repair",
-        "code-prototype",
-        "session-historical-child",
-        "run-historical-repair",
-        &acceptance,
-        &[],
-        Some("delivery-historical-original"),
-    );
-    let quality_failure = new_static_delegate_delivery_with_contract(
-        &parent.state.agent_id,
-        &parent.state.session_id,
-        &parent.state.run_id,
-        "action-historical-quality",
-        "delivery-historical-quality",
-        "quality-review",
-        "session-historical-quality",
-        "run-historical-repair",
-        &acceptance,
-        &[],
-        Some("delivery-historical-quality-original"),
-    );
-    create_or_read_static_delegate_delivery_at(&root, &original)
-        .expect("persist historical original delivery");
-    create_or_read_static_delegate_delivery_at(&root, &repair)
-        .expect("persist historical repair delivery");
-    create_or_read_static_delegate_delivery_at(&root, &quality_failure)
-        .expect("persist historical quality delivery");
-
-    let original_task = serde_json::from_value::(serde_json::json!({
-        "agentId": "code-prototype",
-        "taskId": "code-prototype",
-        "sessionId": "session-historical-child",
-        "runId": "run-historical-original",
-        "source": "agent-delegate",
-        "runProfile": AGENT_RUNTIME_RUN_PROFILE_STANDARD,
-        "parentAgentId": GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
-        "parentRunId": "run-historical-parent",
-        "delegationId": "delivery-historical-original",
-        "task": "原始委派",
-        "status": "failed",
-        "phase": "failed",
-        "currentAction": "原始委派失败",
-        "updatedAt": 100
-    }))
-    .expect("deserialize historical original task");
-    let repair_task = serde_json::from_value::(serde_json::json!({
-        "agentId": "code-prototype",
-        "taskId": "code-prototype",
-        "sessionId": "session-historical-child",
-        "runId": "run-historical-repair",
-        "source": "agent-delegate",
-        "runProfile": AGENT_RUNTIME_RUN_PROFILE_STANDARD,
-        "parentAgentId": GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
-        "parentRunId": "run-historical-parent",
-        "delegationId": "delivery-historical-repair",
-        "task": "修复委派",
-        "status": "failed",
-        "phase": "completion-contract-failed",
-        "currentAction": "修复交付未通过合同",
-        "updatedAt": 200
-    }))
-    .expect("deserialize historical repair task");
-    let task_path = game_creator_agent_runtime_task_path(&root, "code-prototype");
-    fs::create_dir_all(task_path.parent().expect("specialist journal parent"))
-        .expect("create specialist journal parent");
-    fs::write(
-        &task_path,
-        format!(
-            "{}\n{}\n",
-            serde_json::to_string(&original_task).expect("serialize original task"),
-            serde_json::to_string(&repair_task).expect("serialize repair task"),
-        ),
-    )
-    .expect("persist specialist task journal");
-
-    let quality_task = serde_json::from_value::(serde_json::json!({
-        "agentId": "quality-review",
-        "taskId": "quality-review",
-        "sessionId": "session-historical-quality",
-        "runId": "run-historical-repair",
-        "source": "agent-delegate",
-        "runProfile": AGENT_RUNTIME_RUN_PROFILE_STANDARD,
-        "parentAgentId": GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
-        "parentRunId": "run-historical-parent",
-        "delegationId": "delivery-historical-quality",
-        "task": "质量修复复验",
-        "status": "failed",
-        "phase": "failed",
-        "currentAction": "质量修复失败",
-        "updatedAt": 210
-    }))
-    .expect("deserialize historical quality task");
-    let quality_task_path = game_creator_agent_runtime_task_path(&root, "quality-review");
-    fs::create_dir_all(quality_task_path.parent().expect("quality journal parent"))
-        .expect("create quality journal parent");
-    fs::write(
-        &quality_task_path,
-        format!(
-            "{}\n",
-            serde_json::to_string(&quality_task).expect("serialize quality task"),
-        ),
-    )
-    .expect("persist quality task journal");
-
-    let mut current_specialist = runtime("running", "planning", 0);
-    current_specialist.state.agent_id = "code-prototype".to_string();
-    current_specialist.state.session_id = "session-historical-child".to_string();
-    current_specialist.state.run_id = "run-historical-repair".to_string();
-    current_specialist.state.source = "agent-delegate".to_string();
-    current_specialist.state.parent_agent_id = Some(parent.state.agent_id.clone());
-    current_specialist.state.parent_run_id = Some(parent.state.run_id.clone());
-    current_specialist.state.updated_at = 50;
-    let mut current_quality = runtime("idle", "completed", 0);
-    current_quality.state.agent_id = "quality-review".to_string();
-    current_quality.state.run_id = "run-later-quality".to_string();
-    let scan = scan_swarm_terminal_failures_at(
-        &root,
-        &parent.state.agent_id,
-        &parent.state.session_id,
-        AGENT_RUNTIME_RUN_PROFILE_STANDARD,
-        None,
-        &parent.state.run_id,
-        &[parent.clone(), current_specialist, current_quality],
-    );
-    assert_eq!(
-        scan.failed_agents,
-        vec![
-            "code-prototype:completion-contract-failed",
-            "quality-review:failed",
-        ]
-    );
-    assert!(scan.incomplete_reasons.is_empty());
-    assert!(scan.reconciliation_agents.is_empty());
-
-    fs::remove_dir_all(root).ok();
-}
-
-#[test]
-fn failure_scan_ignores_cancelled_parent_and_children_from_another_run() {
-    let root = std::env::temp_dir().join(format!(
-        "swarm-cli-cross-run-failure-scan-{}-{}",
-        std::process::id(),
-        unix_millis()
-    ));
-    init_local_game_project_at(&root, "project-cross-run-scan", "Cross-run failure scan")
-        .expect("initialize cross-run failure scan project");
-    let session_id = "session-cross-run";
-    let run_profile = AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD;
-    let mut old_parent = runtime("cancelled", "cancelled", 1);
-    old_parent.state.agent_id = GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string();
-    old_parent.state.session_id = session_id.to_string();
-    old_parent.state.run_id = "run-old-cancelled".to_string();
-    old_parent.state.run_profile = run_profile.to_string();
-
-    let mut old_child = runtime("failed", "budget-exhausted", 0);
-    old_child.state.agent_id = "art-asset-plan".to_string();
-    old_child.state.session_id = "session-old-child".to_string();
-    old_child.state.run_id = "run-old-child".to_string();
-    old_child.state.source = "agent-delegate".to_string();
-    old_child.state.parent_agent_id = Some(old_parent.state.agent_id.clone());
-    old_child.state.parent_run_id = Some(old_parent.state.run_id.clone());
-
-    let runtimes = vec![old_parent.clone(), old_child];
-    let new_turn_scan = scan_swarm_terminal_failures_at(
-        &root,
-        &old_parent.state.agent_id,
-        session_id,
-        run_profile,
-        None,
-        "run-new-pending",
-        &runtimes,
-    );
-    assert!(new_turn_scan.failed_agents.is_empty());
-    assert!(new_turn_scan.incomplete_reasons.is_empty());
-    assert!(new_turn_scan.reconciliation_agents.is_empty());
-
-    let old_turn_scan = scan_swarm_terminal_failures_at(
-        &root,
-        &old_parent.state.agent_id,
-        session_id,
-        run_profile,
-        None,
-        &old_parent.state.run_id,
-        &runtimes,
-    );
-    assert!(old_turn_scan
-        .failed_agents
-        .iter()
-        .any(|agent| agent == "project-supervisor:cancelled"));
-    assert!(old_turn_scan
-        .failed_agents
-        .iter()
-        .any(|agent| agent == "art-asset-plan:budget-exhausted"));
-
-    fs::remove_dir_all(root).ok();
-}
-
-#[test]
-fn missing_confirmation_sidecar_is_reported_as_reconciliation() {
-    let broken = runtime("waiting-for-confirmation", "waiting-for-confirmation", 0);
-    assert_eq!(
-        swarm_reconciliation_agents(&[broken]),
-        vec!["code-prototype".to_string()]
-    );
-}
-
-#[test]
-fn turn_report_counts_runtime_and_conversation_snapshots() {
-    let mut parent = runtime("running", "response", 0);
-    parent.state.agent_id = "project-supervisor".to_string();
-    parent.state.session_id = "session-report".to_string();
-    parent.state.run_id = "run-parent".to_string();
-
-    let mut pending_child = runtime("pending", "queued", 0);
-    pending_child.state.agent_id = "child-code".to_string();
-    pending_child.state.run_id = "run-child-pending".to_string();
-    pending_child.state.parent_agent_id = Some("project-supervisor".to_string());
-    pending_child.state.parent_run_id = Some("run-parent".to_string());
-
-    let mut confirmation_child = runtime("waiting-for-confirmation", "waiting-for-confirmation", 0);
-    confirmation_child.state.agent_id = "child-design".to_string();
-    confirmation_child.state.run_id = "run-child-confirmation".to_string();
-    confirmation_child.state.parent_agent_id = Some("project-supervisor".to_string());
-    confirmation_child.state.parent_run_id = Some("run-parent".to_string());
-
-    let mut input_child = runtime("waiting-for-user-input", "waiting-for-user-input", 0);
-    input_child.state.agent_id = "child-test".to_string();
-    input_child.state.run_id = "run-child-input".to_string();
-    input_child.state.parent_agent_id = Some("project-supervisor".to_string());
-    input_child.state.parent_run_id = Some("run-parent".to_string());
-
-    let mut next_turn = runtime("running", "planning", 0);
-    next_turn.state.agent_id = "project-supervisor".to_string();
-    next_turn.state.session_id = "session-report".to_string();
-    next_turn.state.run_id = "run-next-turn".to_string();
-    let runtimes = vec![
-        parent,
-        pending_child,
-        confirmation_child,
-        input_child,
-        next_turn,
-    ];
-    let (conversation_metrics, final_reply) = summarize_new_assistant_messages([
-        ("user", "请继续"),
-        ("assistant", "阶段回复"),
-        ("tool", "PRIVATE_OBSERVATION"),
-        ("assistant", "最终🙂"),
-    ]);
-    assert_eq!(final_reply, Some("最终🙂"));
-
-    let report = build_swarm_turn_report(
-        SwarmTurnReportOutcome::NeedsReconciliation,
-        "project-supervisor",
-        "session-report",
-        Some("run-parent"),
-        &runtimes,
-        conversation_metrics,
-        1,
-    )
-    .expect("build scoped turn report");
-
-    assert_eq!(report.schema_version, SWARM_TURN_REPORT_SCHEMA_VERSION);
-    assert_eq!(report.outcome, SwarmTurnReportOutcome::NeedsReconciliation);
-    assert_eq!(report.parent_agent_id, "project-supervisor");
-    assert_eq!(report.session_id, "session-report");
-    assert_eq!(report.parent_run_id.as_deref(), Some("run-parent"));
-    assert_eq!(report.runtime_count, 4);
-    assert_eq!(report.busy_runtime_count, 4);
-    assert_eq!(report.pending_task_count, 1);
-    assert_eq!(report.running_task_count, 1);
-    assert_eq!(report.waiting_for_confirmation_count, 1);
-    assert_eq!(report.waiting_for_user_input_count, 1);
-    assert_eq!(report.new_assistant_message_count, 2);
-    assert_eq!(report.final_reply_chars, "最终🙂".chars().count());
-    assert_eq!(report.reconciliation_agent_count, 1);
-}
-
-#[test]
-fn turn_report_prefers_expected_run_over_stale_canonical_state() {
-    let root = std::env::temp_dir().join(format!(
-        "swarm-cli-report-full-journal-{}-{}",
-        std::process::id(),
-        unix_millis()
-    ));
-    init_local_game_project_at(&root, "project-report-journal", "Report full journal")
-        .expect("initialize report journal project");
-    let mut old_parent = runtime("cancelled", "cancelled", 1);
-    old_parent.state.agent_id = GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string();
-    old_parent.state.session_id = "session-report-stale".to_string();
-    old_parent.state.run_id = "run-old-cancelled".to_string();
-    let task_path =
-        game_creator_agent_runtime_task_path(&root, GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID);
-    fs::create_dir_all(task_path.parent().expect("report journal parent"))
-        .expect("create report journal parent");
-    let pending_task = serde_json::from_value::(serde_json::json!({
-        "agentId": GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
-        "taskId": GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
-        "sessionId": "session-report-stale",
-        "runId": "run-new-pending",
-        "source": AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE,
-        "runProfile": AGENT_RUNTIME_RUN_PROFILE_STANDARD,
-        "task": "等待下一轮",
-        "status": "pending",
-        "phase": "queued",
-        "currentAction": "等待 Runner",
-        "updatedAt": 200
-    }))
-    .expect("deserialize report pending task");
-    fs::write(
-        &task_path,
-        format!(
-            "{}\n",
-            serde_json::to_string(&pending_task).expect("serialize report pending task"),
-        ),
-    )
-    .expect("persist report task journal");
-    old_parent.task_path = task_path.to_string_lossy().into_owned();
-
-    let report = build_swarm_turn_report(
-        SwarmTurnReportOutcome::Failed,
-        GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
-        "session-report-stale",
-        Some("run-new-pending"),
-        &[old_parent],
-        SwarmTurnConversationMetrics::default(),
-        0,
-    )
-    .expect("build stale canonical turn report");
-
-    assert_eq!(report.parent_run_id.as_deref(), Some("run-new-pending"));
-    assert_eq!(report.runtime_count, 1);
-    assert_eq!(report.pending_task_count, 1);
-
-    fs::remove_dir_all(root).ok();
-}
-
-#[test]
-fn turn_report_json_is_single_line_and_omits_sensitive_bodies_and_paths() {
-    let sensitive_reply = concat!(
-        "PRIVATE_REPLY_BODY\n",
-        "/private/project/root ",
-        "prompt=DO_NOT_LEAK observation=DO_NOT_LEAK CREDENTIAL_SENTINEL"
-    );
-    let (conversation_metrics, _) =
-        summarize_new_assistant_messages([("assistant", sensitive_reply)]);
-    let report = build_swarm_turn_report(
-        SwarmTurnReportOutcome::Settled,
-        "project-supervisor",
-        "session-safe",
-        None,
-        &[],
-        conversation_metrics,
-        0,
-    )
-    .expect("build safe turn report");
-    let json = serde_json::to_string(&report).expect("serialize turn report");
-    let value = serde_json::from_str::(&json).expect("parse turn report");
-    let object = value.as_object().expect("turn report is an object");
-
-    assert_eq!(json.lines().count(), 1);
-    assert_eq!(object.len(), 14);
-    for key in [
-        "schemaVersion",
-        "outcome",
-        "parentAgentId",
-        "sessionId",
-        "parentRunId",
-        "runtimeCount",
-        "busyRuntimeCount",
-        "pendingTaskCount",
-        "runningTaskCount",
-        "waitingForConfirmationCount",
-        "waitingForUserInputCount",
-        "newAssistantMessageCount",
-        "finalReplyChars",
-        "reconciliationAgentCount",
-    ] {
-        assert!(object.contains_key(key), "turn report omitted {key}");
-    }
-    assert_eq!(
-        value["schemaVersion"],
-        serde_json::json!(SWARM_TURN_REPORT_SCHEMA_VERSION)
-    );
-    assert_eq!(value["outcome"], serde_json::json!("settled"));
-    assert_eq!(value["parentRunId"], serde_json::Value::Null);
-    assert_eq!(value["newAssistantMessageCount"], serde_json::json!(1));
-    assert_eq!(
-        value["finalReplyChars"],
-        serde_json::json!(sensitive_reply.chars().count())
-    );
-    for forbidden in [
-        "PRIVATE_REPLY_BODY",
-        "/private/project/root",
-        "DO_NOT_LEAK",
-        "CREDENTIAL_SENTINEL",
-    ] {
-        assert!(!json.contains(forbidden), "report leaked {forbidden}");
-    }
-}
-
-#[test]
-fn turn_outcome_prints_all_terminal_reports_but_not_quit() {
-    let metrics = SwarmTurnConversationMetrics {
-        new_assistant_message_count: 1,
-        final_reply_chars: 4,
-    };
-    let settled_report = build_swarm_turn_report(
-        SwarmTurnReportOutcome::Settled,
-        "project-supervisor",
-        "session-settled",
-        None,
-        &[],
-        metrics,
-        0,
-    )
-    .expect("build settled turn report");
-    let mut settled_output = Vec::new();
-    print_turn_outcome(
-        SwarmTurnOutcome::Settled(settled_report),
-        &mut settled_output,
-    )
-    .expect("print settled report");
-    let settled_output = String::from_utf8(settled_output).expect("settled output is utf-8");
-    assert_eq!(settled_output.lines().count(), 1);
-    assert!(settled_output.starts_with(SWARM_TURN_REPORT_PREFIX));
-    assert!(settled_output.contains("\"outcome\":\"settled\""));
-
-    let failed_report = build_swarm_turn_report(
-        SwarmTurnReportOutcome::Failed,
-        "project-supervisor",
-        "session-failed",
-        None,
-        &[],
-        metrics,
-        0,
-    )
-    .expect("build failed turn report");
-    let mut failed_output = Vec::new();
-    print_turn_outcome(
-        SwarmTurnOutcome::Failed {
-            agent_ids: vec!["project-supervisor:budget-exhausted".to_string()],
-            report: failed_report,
-        },
-        &mut failed_output,
-    )
-    .expect("print failed report");
-    let failed_output = String::from_utf8(failed_output).expect("failed output is utf-8");
-    assert!(failed_output.starts_with("[已失败]"));
-    assert!(failed_output.contains("\"outcome\":\"failed\""));
-
-    let incomplete_report = build_swarm_turn_report(
-        SwarmTurnReportOutcome::Incomplete,
-        "project-supervisor",
-        "session-incomplete",
-        None,
-        &[],
-        metrics,
-        0,
-    )
-    .expect("build incomplete turn report");
-    let mut incomplete_output = Vec::new();
-    print_turn_outcome(
-        SwarmTurnOutcome::Incomplete {
-            reasons: vec!["assistant-count=0".to_string()],
-            report: incomplete_report,
-        },
-        &mut incomplete_output,
-    )
-    .expect("print incomplete report");
-    let incomplete_output =
-        String::from_utf8(incomplete_output).expect("incomplete output is utf-8");
-    assert!(incomplete_output.starts_with("[未完成]"));
-    assert!(incomplete_output.contains("\"outcome\":\"incomplete\""));
-
-    let reconciliation_report = build_swarm_turn_report(
-        SwarmTurnReportOutcome::NeedsReconciliation,
-        "project-supervisor",
-        "session-reconciliation",
-        None,
-        &[],
-        metrics,
-        2,
-    )
-    .expect("build reconciliation turn report");
-    let mut reconciliation_output = Vec::new();
-    print_turn_outcome(
-        SwarmTurnOutcome::NeedsReconciliation {
-            agent_ids: vec!["code-prototype".to_string(), "external-runner".to_string()],
-            report: reconciliation_report,
-        },
-        &mut reconciliation_output,
-    )
-    .expect("print reconciliation report");
-    let reconciliation_output =
-        String::from_utf8(reconciliation_output).expect("reconciliation output is utf-8");
-    let lines = reconciliation_output.lines().collect::>();
-    assert_eq!(lines.len(), 2);
-    assert_eq!(
-        lines[0],
-        "[已阻断] 以下 Agent 需要人工 reconciliation:code-prototype, external-runner"
-    );
-    assert!(lines[1].starts_with(SWARM_TURN_REPORT_PREFIX));
-    assert!(lines[1].contains("\"outcome\":\"needs-reconciliation\""));
-    assert!(lines[1].contains("\"reconciliationAgentCount\":2"));
-
-    let mut quit_output = Vec::new();
-    print_turn_outcome(SwarmTurnOutcome::Quit, &mut quit_output).expect("ignore quit");
-    assert!(quit_output.is_empty());
-}
-
-#[test]
-fn blank_agent_snapshots_do_not_reset_the_settle_window() {
-    let mut blank = runtime("idle", "idle", 0);
-    blank.state.run_id.clear();
-    blank.state.updated_at = 100;
-    let mut observer = SwarmRuntimeObserver::default();
-    let mut output = Vec::new();
-    assert!(!observer
-        .print_changes(&[blank.clone()], &mut output)
-        .expect("observe first blank snapshot"));
-    blank.state.updated_at = 101;
-    assert!(!observer
-        .print_changes(&[blank], &mut output)
-        .expect("observe refreshed blank snapshot"));
-    assert!(output.is_empty());
-}
-
-#[test]
-fn runtime_plan_revision_and_current_step_change_state_signature() {
-    let mut snapshot = runtime("running", "planning", 0);
-    snapshot.state.updated_at = 100;
-    snapshot.task_queue.updated_at = 100;
-    snapshot.state.plan_revision = 1;
-    snapshot.state.plan_steps = vec![
-        AgentRuntimePlanStep {
-            index: 0,
-            title: "读取现有 CLI".to_string(),
-            status: "in_progress".to_string(),
-            detail: None,
-            updated_at: 100,
-        },
-        AgentRuntimePlanStep {
-            index: 1,
-            title: "补充计划展示".to_string(),
-            status: "pending".to_string(),
-            detail: None,
-            updated_at: 100,
-        },
-    ];
-    snapshot.state.active_plan_step_index = Some(0);
-
-    let initial = runtime_state_signature(&snapshot.state, &snapshot.task_queue);
-    snapshot.state.plan_revision = 2;
-    let revised = runtime_state_signature(&snapshot.state, &snapshot.task_queue);
-    assert_ne!(initial, revised);
-
-    snapshot.state.plan_steps[0].title = "核对现有 CLI".to_string();
-    let current_step_changed = runtime_state_signature(&snapshot.state, &snapshot.task_queue);
-    assert_ne!(revised, current_step_changed);
-
-    snapshot.state.plan_steps[0].status = "completed".to_string();
-    snapshot.state.plan_steps[1].status = "in_progress".to_string();
-    snapshot.state.active_plan_step_index = Some(1);
-    let advanced = runtime_state_signature(&snapshot.state, &snapshot.task_queue);
-    assert_ne!(current_step_changed, advanced);
-}
-
-#[test]
-fn runtime_plan_output_is_bounded_and_omits_private_observations() {
-    let mut snapshot = runtime("running", "planning", 0);
-    snapshot.state.plan_revision = 7;
-    snapshot.state.plan_explanation = "已完成读取,进入验证".to_string();
-    snapshot.state.current_action = "展示持久计划".to_string();
-    snapshot.state.waiting_on = "开发者确认".to_string();
-    snapshot.state.next_step = "运行 focused cargo test".to_string();
-    snapshot.state.observations = vec![
-        "PRIVATE_OBSERVATION_SENTINEL".to_string(),
-        "PRIVATE_DETAIL_SENTINEL".to_string(),
-    ];
-    snapshot.state.plan_steps = (0..10)
-        .map(|index| AgentRuntimePlanStep {
-            index,
-            title: format!("计划步骤 {}", index + 1),
-            status: match index {
-                0 | 1 => "completed",
-                2 => "in_progress",
-                _ => "pending",
-            }
-            .to_string(),
-            detail: Some(format!("PRIVATE_STEP_DETAIL_{index}")),
-            updated_at: 100,
-        })
-        .collect();
-    snapshot.state.active_plan_step_index = Some(2);
-
-    let mut output = Vec::new();
-    print_runtime_state(&snapshot.state, &snapshot.task_queue, &mut output)
-        .expect("print runtime plan progress");
-    let output = String::from_utf8(output).expect("runtime output is utf-8");
-
-    assert!(output.contains(
-            "[计划] revision=7 completed=2/10 current=#3 [in_progress] 计划步骤 3 | waiting=开发者确认 | next=运行 focused cargo test"
-        ));
-    assert!(output.contains("[计划说明] 已完成读取,进入验证"));
-    assert_eq!(output.matches("[计划步骤]").count(), 8);
-    assert!(output.contains("[计划步骤] #8 [pending] 计划步骤 8"));
-    assert!(output.contains("另有 2 条步骤未显示"));
-    assert!(!output.contains("计划步骤 9"));
-    assert!(!output.contains("PRIVATE_OBSERVATION_SENTINEL"));
-    assert!(!output.contains("PRIVATE_DETAIL_SENTINEL"));
-    assert!(!output.contains("PRIVATE_STEP_DETAIL"));
-}
-
-#[test]
-fn response_stream_prints_only_monotonic_utf8_suffixes() {
-    let mut observer = SwarmRuntimeObserver::default();
-    let mut output = Vec::new();
-    let mut snapshot = runtime_with_response_stream(response_stream(
-        "slot-1",
-        7,
-        0,
-        AGENT_RUNTIME_RESPONSE_STREAM_STATUS_STREAMING,
-        "",
-    ));
-
-    assert!(observer
-        .print_changes(&[snapshot.clone()], &mut output)
-        .expect("observe empty response stream"));
-    snapshot.response_stream = Some(response_stream(
-        "slot-1",
-        7,
-        1,
-        AGENT_RUNTIME_RESPONSE_STREAM_STATUS_STREAMING,
-        "你",
-    ));
-    assert!(observer
-        .print_changes(&[snapshot.clone()], &mut output)
-        .expect("observe first utf-8 suffix"));
-    snapshot.response_stream = Some(response_stream(
-        "slot-1",
-        7,
-        2,
-        AGENT_RUNTIME_RESPONSE_STREAM_STATUS_READY,
-        "你好🙂",
-    ));
-    assert!(observer
-        .print_changes(&[snapshot.clone()], &mut output)
-        .expect("observe second utf-8 suffix"));
-    assert!(!observer
-        .print_changes(&[snapshot], &mut output)
-        .expect("ignore duplicate snapshot"));
-    observer
-        .close_response_line(&mut output)
-        .expect("close response line");
-
-    let output = String::from_utf8(output).expect("stream output is utf-8");
-    assert!(output.contains("Agent[code-prototype]> 你好🙂"));
-    assert_eq!(output.matches("Agent[code-prototype]>").count(), 1);
-    assert!(!output.contains("你你好"));
-}
-
-#[test]
-fn response_stream_resets_for_non_prefix_and_new_request_slot() {
-    let mut observer = SwarmRuntimeObserver::default();
-    let mut output = Vec::new();
-    let mut snapshot = runtime_with_response_stream(response_stream(
-        "slot-1",
-        9,
-        1,
-        AGENT_RUNTIME_RESPONSE_STREAM_STATUS_STREAMING,
-        "旧稿",
-    ));
-    observer
-        .print_changes(&[snapshot.clone()], &mut output)
-        .expect("observe initial stream");
-
-    snapshot.response_stream = Some(response_stream(
-        "slot-1",
-        9,
-        2,
-        AGENT_RUNTIME_RESPONSE_STREAM_STATUS_READY,
-        "修正版",
-    ));
-    observer
-        .print_changes(&[snapshot.clone()], &mut output)
-        .expect("observe non-prefix correction");
-    snapshot.response_stream = Some(response_stream(
-        "slot-2",
-        9,
-        1,
-        AGENT_RUNTIME_RESPONSE_STREAM_STATUS_READY,
-        "最终版",
-    ));
-    observer
-        .print_changes(&[snapshot], &mut output)
-        .expect("observe new request slot");
-    observer
-        .close_response_line(&mut output)
-        .expect("close response line");
-
-    let output = String::from_utf8(output).expect("stream output is utf-8");
-    assert!(output.contains("reason=non-prefix-correction"));
-    assert!(output.contains("reason=new-request-slot"));
-    assert_eq!(output.matches("旧稿").count(), 1);
-    assert_eq!(output.matches("修正版").count(), 1);
-    assert_eq!(output.matches("最终版").count(), 1);
-}
-
-#[test]
-fn response_stream_resets_sequence_for_same_run_steer_cursor() {
-    let mut observer = SwarmRuntimeObserver::default();
-    let mut output = Vec::new();
-    let mut initial = response_stream(
-        "slot-1",
-        9,
-        4,
-        AGENT_RUNTIME_RESPONSE_STREAM_STATUS_STREAMING,
-        "纠偏前回复",
-    );
-    initial.applied_steer_cursor = 1;
-    observer
-        .print_changes(&[runtime_with_response_stream(initial)], &mut output)
-        .expect("observe pre-steer stream");
-
-    let mut steered = response_stream(
-        "slot-1",
-        9,
-        1,
-        AGENT_RUNTIME_RESPONSE_STREAM_STATUS_READY,
-        "纠偏后回复",
-    );
-    steered.applied_steer_cursor = 2;
-    observer
-        .print_changes(&[runtime_with_response_stream(steered)], &mut output)
-        .expect("observe same-run stream after steer");
-    observer
-        .close_response_line(&mut output)
-        .expect("close steered response line");
-
-    let output = String::from_utf8(output).expect("steer output is utf-8");
-    assert!(output.contains("reason=new-steer-cursor"));
-    assert!(!output.contains("reason=sequence-rollback"));
-    assert_eq!(output.matches("纠偏前回复").count(), 1);
-    assert_eq!(output.matches("纠偏后回复").count(), 1);
-}
-
-#[test]
-fn response_stream_reconnects_without_repeating_body_and_rejects_sequence_rollback() {
-    let mut observer = SwarmRuntimeObserver::default();
-    let mut output = Vec::new();
-    let mut snapshot = runtime_with_response_stream(response_stream(
-        "slot-1",
-        11,
-        3,
-        AGENT_RUNTIME_RESPONSE_STREAM_STATUS_STREAMING,
-        "已输出",
-    ));
-    observer
-        .print_changes(&[snapshot.clone()], &mut output)
-        .expect("observe initial stream");
-
-    snapshot.response_stream = None;
-    assert!(observer
-        .print_changes(&[snapshot.clone()], &mut output)
-        .expect("observe disconnect"));
-    snapshot.response_stream = Some(response_stream(
-        "slot-1",
-        11,
-        3,
-        AGENT_RUNTIME_RESPONSE_STREAM_STATUS_STREAMING,
-        "已输出",
-    ));
-    assert!(observer
-        .print_changes(&[snapshot.clone()], &mut output)
-        .expect("observe reconnect"));
-
-    snapshot.response_stream = Some(response_stream(
-        "slot-1",
-        11,
-        2,
-        AGENT_RUNTIME_RESPONSE_STREAM_STATUS_STREAMING,
-        "回退正文",
-    ));
-    assert!(observer
-        .print_changes(&[snapshot.clone()], &mut output)
-        .expect("report sequence rollback"));
-    assert!(!observer
-        .print_changes(&[snapshot], &mut output)
-        .expect("deduplicate repeated rollback"));
-
-    let cursor = observer
-        .response_streams
-        .get("code-prototype")
-        .expect("response cursor");
-    assert_eq!(cursor.sequence, 3);
-    assert_eq!(cursor.accumulated_text, "已输出");
-    assert_eq!(cursor.printed_accumulated_text.as_deref(), Some("已输出"));
-
-    let recovered = runtime_with_response_stream(response_stream(
-        "slot-1",
-        11,
-        4,
-        AGENT_RUNTIME_RESPONSE_STREAM_STATUS_READY,
-        "已输出继续",
-    ));
-    assert!(observer
-        .print_changes(&[recovered], &mut output)
-        .expect("resume from accepted high-water mark"));
-    observer
-        .close_response_line(&mut output)
-        .expect("close recovered response line");
-
-    let output = String::from_utf8(output).expect("stream output is utf-8");
-    assert!(output.contains("reason=reconnect"));
-    assert!(output.contains("reason=sequence-rollback"));
-    assert_eq!(output.matches("已输出").count(), 1);
-    assert_eq!(output.matches("继续").count(), 1);
-    assert!(!output.contains("回退正文"));
-}
-
-#[test]
-fn settled_parent_reply_is_not_repeated_after_complete_stream() {
-    let mut observer = SwarmRuntimeObserver::default();
-    let mut output = Vec::new();
-    let mut snapshot = runtime_with_response_stream(response_stream(
-        "slot-1",
-        13,
-        4,
-        AGENT_RUNTIME_RESPONSE_STREAM_STATUS_READY,
-        "权威最终回复",
-    ));
-    observer
-        .print_changes(&[snapshot.clone()], &mut output)
-        .expect("observe complete stream");
-    snapshot.response_stream = Some(response_stream(
-        "slot-1",
-        13,
-        5,
-        AGENT_RUNTIME_RESPONSE_STREAM_STATUS_COMMITTED,
-        "权威最终回复",
-    ));
-    observer
-        .print_changes(&[snapshot], &mut output)
-        .expect("observe committed stream without printing body");
-    observer
-        .close_response_line(&mut output)
-        .expect("close response line");
-    print_settled_parent_reply(
-        "code-prototype",
-        "session-test",
-        Some("权威最终回复"),
-        &observer,
-        &mut output,
-    )
-    .expect("settle streamed reply");
-    let (conversation_metrics, _) =
-        summarize_new_assistant_messages([("assistant", "权威最终回复")]);
-    let report = build_swarm_turn_report(
-        SwarmTurnReportOutcome::Settled,
-        "code-prototype",
-        "session-test",
-        None,
-        &[],
-        conversation_metrics,
-        0,
-    )
-    .expect("build streamed reply turn report");
-    print_turn_outcome(SwarmTurnOutcome::Settled(report), &mut output)
-        .expect("print settled report after stream");
-
-    let output = String::from_utf8(output).expect("settle output is utf-8");
-    assert_eq!(output.matches("权威最终回复").count(), 1);
-    assert!(output.contains("父 Agent 回复已完整流式输出"));
-    assert!(output.contains(SWARM_TURN_REPORT_PREFIX));
-
-    let mut next_run_stream = response_stream(
-        "slot-next",
-        14,
-        1,
-        AGENT_RUNTIME_RESPONSE_STREAM_STATUS_READY,
-        "下一轮回复",
-    );
-    next_run_stream.run_id = "run-next".to_string();
-    let mut next_run_observer = SwarmRuntimeObserver::default();
-    let mut next_run_output = Vec::new();
-    next_run_observer
-        .print_changes(
-            &[runtime_with_response_stream(next_run_stream)],
-            &mut next_run_output,
-        )
-        .expect("observe next run stream");
-    print_settled_parent_reply_for_run(
-        "code-prototype",
-        "session-test",
-        Some("run-target"),
-        Some("上一轮最终回复"),
-        &next_run_observer,
-        &mut next_run_output,
-    )
-    .expect("next run stream must not suppress target reply");
-    let next_run_output = String::from_utf8(next_run_output).expect("next run output is utf-8");
-    assert!(next_run_output.contains("Agent> 上一轮最终回复"));
-
-    let mut fallback = Vec::new();
-    print_settled_parent_reply(
-        "code-prototype",
-        "session-test",
-        Some("未流过的权威回复"),
-        &SwarmRuntimeObserver::default(),
-        &mut fallback,
-    )
-    .expect("print authoritative fallback");
-    let fallback = String::from_utf8(fallback).expect("fallback output is utf-8");
-    assert!(fallback.contains("Agent> 未流过的权威回复"));
-}
-
-#[test]
-fn response_stream_status_reports_only_status_sequence_and_char_count() {
-    let body = "PRIVATE_RESPONSE_BODY";
-    let stream = response_stream(
-        "slot-private",
-        17,
-        8,
-        AGENT_RUNTIME_RESPONSE_STREAM_STATUS_READY,
-        body,
-    );
-    let mut output = Vec::new();
-    print_runtime_response_stream_status(Some(&stream), &mut output)
-        .expect("print response stream status");
-    let output = String::from_utf8(output).expect("status output is utf-8");
-
-    assert_eq!(
-        output.trim(),
-        format!(
-            "[回复流] status=ready sequence=8 chars={}",
-            body.chars().count()
-        )
-    );
-    assert!(!output.contains(body));
-    assert!(!output.contains("slot-private"));
-}
-
-#[test]
-fn input_channel_preserves_lines_and_eof() {
-    let (tx, rx) = mpsc::channel();
-    tx.send(SwarmInputEvent::Line("hello swarm".to_string()))
-        .expect("send line");
-    tx.send(SwarmInputEvent::Eof).expect("send eof");
-    assert_eq!(
-        receive_swarm_chat_line(&rx).expect("read line").as_deref(),
-        Some("hello swarm")
-    );
-    assert_eq!(receive_swarm_chat_line(&rx).expect("read eof"), None);
-}
-
-#[test]
-fn confirmation_prompt_defers_to_bare_goal_status_without_deciding_action() {
-    let root = std::env::temp_dir().join(format!(
-        "swarm-goal-confirmation-{}-{}",
-        std::process::id(),
-        unix_millis()
-    ));
-    init_local_game_project_at(&root, "project-1", "Goal 确认提示测试")
-        .expect("initialize Goal prompt project");
-    let (tx, rx) = mpsc::channel();
-    tx.send(SwarmInputEvent::Line("/goal".to_string()))
-        .expect("send bare Goal status");
-    let mut output = Vec::new();
-
-    let decision = prompt_swarm_decision(
-        &root,
-        GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
-        &rx,
-        &mut output,
-        "",
-    )
-    .expect("handle Goal status during confirmation");
-    assert!(matches!(decision, SwarmPromptDecision::Deferred));
-    let output = String::from_utf8(output).expect("prompt output is utf-8");
-    assert!(output.contains("当前尚未设置持久目标"));
-    assert!(!output.contains("[已批准]"));
-    assert!(!output.contains("[已拒绝]"));
-
-    fs::remove_dir_all(root).ok();
-}
-
-#[test]
-fn event_deduplication_keeps_phase_and_detail_changes() {
-    let base = serde_json::json!({
-        "agentId": "code-prototype",
-        "taskId": "task-1",
-        "sessionId": "session-1",
-        "runId": "run-1",
-        "eventType": "observation",
-        "status": "running",
-        "phase": "action",
-        "summary": "工具观察",
-        "detail": "第一条",
-        "updatedAt": 100,
-    });
-    let first =
-        serde_json::from_value::(base.clone()).expect("deserialize first event");
-    let mut changed = base;
-    changed["phase"] = serde_json::json!("observation");
-    changed["detail"] = serde_json::json!("第二条");
-    let second =
-        serde_json::from_value::(changed).expect("deserialize second event");
-    assert_ne!(runtime_event_key(&first), runtime_event_key(&second));
-}
diff --git a/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/turn_dispatch.rs b/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/turn_dispatch.rs
deleted file mode 100644
index 0254f6301..000000000
--- a/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/turn_dispatch.rs
+++ /dev/null
@@ -1,590 +0,0 @@
-use super::*;
-
-pub(super) fn handle_swarm_user_turn(
-    root: &Path,
-    parent_agent_id: &str,
-    session_id: &str,
-    run_profile: &str,
-    new_run_launch: SwarmNewRunLaunch<'_>,
-    message: &str,
-    input: &Receiver,
-    output: &mut W,
-) -> Result {
-    let expected_parent_source = new_run_launch.expected_parent_source();
-    let before =
-        read_local_conversation_for_session_at(root, Some(parent_agent_id), Some(session_id))?;
-    let active_goal_run_id =
-        if let Some(goal) = read_game_creator_agent_goal_at(root, parent_agent_id, session_id)? {
-            match goal.status.as_str() {
-                AGENT_GOAL_STATUS_ACTIVE => Some(goal.run_id),
-                AGENT_GOAL_STATUS_PAUSE_REQUESTED | AGENT_GOAL_STATUS_PAUSED => {
-                    print_swarm_goal_error(output, "当前 Goal 已暂停;请先输入 /goal resume。")?;
-                    return Ok(SwarmChatFlow::Continue);
-                }
-                AGENT_GOAL_STATUS_CLEARING => {
-                    print_swarm_goal_error(output, "当前 Goal 正在清理,暂不接受新消息。")?;
-                    return Ok(SwarmChatFlow::Continue);
-                }
-                AGENT_GOAL_STATUS_NEEDS_RECONCILIATION => {
-                    print_swarm_goal_error(
-                        output,
-                        "当前 Goal 需要人工 reconciliation,暂不接受新消息。",
-                    )?;
-                    return Ok(SwarmChatFlow::Continue);
-                }
-                AGENT_GOAL_STATUS_COMPLETED | AGENT_GOAL_STATUS_CLEARED => None,
-                status => {
-                    print_swarm_goal_error(
-                        output,
-                        &format!("当前 Goal 状态未知,已阻止发送:{status}"),
-                    )?;
-                    return Ok(SwarmChatFlow::Continue);
-                }
-            }
-        } else {
-            None
-        };
-
-    let mut runtimes = read_game_creator_agent_runtimes_at(root)?;
-    if let Some(runtime) = swarm_parent_steer_target(
-        parent_agent_id,
-        session_id,
-        run_profile,
-        expected_parent_source,
-        active_goal_run_id.as_deref(),
-        &runtimes,
-    ) {
-        return steer_and_wait_for_swarm_turn(
-            root,
-            parent_agent_id,
-            session_id,
-            run_profile,
-            expected_parent_source,
-            &runtime.state.run_id,
-            message,
-            before.messages.len(),
-            input,
-            output,
-            if active_goal_run_id.is_some() {
-                "Goal 已追加"
-            } else {
-                "运行中输入已排队"
-            },
-        );
-    }
-
-    let pending_message_is_latest = before
-        .messages
-        .last()
-        .is_some_and(|item| item.role == "user" && item.content.trim() == message.trim());
-    let matching_pending_run_id = pending_message_is_latest
-        .then(|| {
-            swarm_parent_runtime(
-                parent_agent_id,
-                session_id,
-                run_profile,
-                expected_parent_source,
-                &runtimes,
-            )
-        })
-        .flatten()
-        .and_then(|runtime| {
-            matching_pending_swarm_run_id(
-                runtime,
-                session_id,
-                run_profile,
-                expected_parent_source,
-                message,
-            )
-        })
-        .map(str::to_string);
-    let parent_has_queued_work = swarm_parent_runtime(
-        parent_agent_id,
-        session_id,
-        run_profile,
-        expected_parent_source,
-        &runtimes,
-    )
-    .is_some_and(runtime_is_busy);
-    if parent_has_queued_work {
-        require_external_agent_runner_for_cli_runtime_write(root)?;
-        resume_game_creator_agent_background_tasks_at(root)?;
-        runtimes = read_game_creator_agent_runtimes_at(root)?;
-
-        if let Some(run_id) = matching_pending_run_id {
-            writeln!(
-                output,
-                "[恢复] 该消息已在 run={run_id} 落盘,继续观察原任务,不重复追加。"
-            )
-            .map_err(|error| format!("写入终端失败:{error}"))?;
-            let conversation_baseline = new_swarm_turn_conversation_baseline(
-                before.messages.len().saturating_sub(1),
-                &run_id,
-            );
-            return wait_and_print_swarm_turn(
-                root,
-                parent_agent_id,
-                session_id,
-                run_profile,
-                expected_parent_source,
-                conversation_baseline,
-                input,
-                output,
-            );
-        }
-
-        if let Some(runtime) = swarm_parent_steer_target(
-            parent_agent_id,
-            session_id,
-            run_profile,
-            expected_parent_source,
-            active_goal_run_id.as_deref(),
-            &runtimes,
-        ) {
-            return steer_and_wait_for_swarm_turn(
-                root,
-                parent_agent_id,
-                session_id,
-                run_profile,
-                expected_parent_source,
-                &runtime.state.run_id,
-                message,
-                before.messages.len(),
-                input,
-                output,
-                if active_goal_run_id.is_some() {
-                    "Goal 已恢复并追加"
-                } else {
-                    "排队任务已恢复,输入已追加"
-                },
-            );
-        }
-    }
-
-    if let Some(goal_run_id) = active_goal_run_id {
-        print_swarm_goal_error(
-            output,
-            &format!(
-                "当前 Goal run={goal_run_id} 没有可追加的运行态;请先输入 /resume 检查恢复结果。"
-            ),
-        )?;
-        return Ok(SwarmChatFlow::Continue);
-    }
-
-    let action = if swarm_turn_uses_interaction_kernel(parent_agent_id, expected_parent_source) {
-        let Some(runtime_lock) =
-            try_acquire_game_creator_agent_runtime_task_lock(root, parent_agent_id)?
-        else {
-            let current_runtimes = read_game_creator_agent_runtimes_at(root)?;
-            if let Some(runtime) = swarm_parent_steer_target(
-                parent_agent_id,
-                session_id,
-                run_profile,
-                expected_parent_source,
-                None,
-                ¤t_runtimes,
-            ) {
-                return steer_and_wait_for_swarm_turn(
-                    root,
-                    parent_agent_id,
-                    session_id,
-                    run_profile,
-                    expected_parent_source,
-                    &runtime.state.run_id,
-                    message,
-                    before.messages.len(),
-                    input,
-                    output,
-                    "运行中输入已排队",
-                );
-            }
-            return Err(format!(
-                "Agent 交互锁已被占用但没有可识别的活动 Runtime:{parent_agent_id}"
-            ));
-        };
-        let action_result =
-            decide_interaction_action(root, parent_agent_id, session_id, message, output);
-        let persistence_result = match &action_result {
-            Ok(AgentInteractionAction::Reply(reply)) => {
-                persist_swarm_reply(root, parent_agent_id, session_id, message, reply)
-            }
-            Ok(AgentInteractionAction::ProjectLocation) => {
-                let reply = format!("当前 CLI 会话绑定的项目目录是:{}", root.display());
-                writeln!(output, "陶泥儿> {reply}")
-                    .map_err(|error| format!("写入终端失败:{error}"))
-                    .and_then(|_| {
-                        persist_swarm_reply(root, parent_agent_id, session_id, message, &reply)
-                    })
-            }
-            _ => Ok(()),
-        };
-        spawn_next_game_creator_agent_background_task_drain_with_lock(
-            root,
-            parent_agent_id,
-            runtime_lock,
-        );
-        persistence_result?;
-        action_result?
-    } else {
-        AgentInteractionAction::Execute
-    };
-    let action = normalize_interaction_action_without_active_runtime(action);
-    writeln!(output, "[意图] {}", action.label())
-        .map_err(|error| format!("写入终端失败:{error}"))?;
-    match action {
-        AgentInteractionAction::Reply(reply) => {
-            debug_assert!(!reply.trim().is_empty());
-            Ok(SwarmChatFlow::Continue)
-        }
-        AgentInteractionAction::ProjectLocation => Ok(SwarmChatFlow::Continue),
-        AgentInteractionAction::Execute => start_and_wait_for_swarm_turn(
-            root,
-            parent_agent_id,
-            session_id,
-            run_profile,
-            new_run_launch,
-            message,
-            before.messages.len(),
-            input,
-            output,
-        ),
-        AgentInteractionAction::Resume => handle_swarm_resume_turn(
-            root,
-            parent_agent_id,
-            session_id,
-            run_profile,
-            expected_parent_source,
-            before.messages.len(),
-            input,
-            output,
-        ),
-    }
-}
-
-pub(super) fn swarm_turn_uses_interaction_kernel(
-    parent_agent_id: &str,
-    _expected_parent_source: Option<&str>,
-) -> bool {
-    game_creator_agent_uses_interaction_kernel(parent_agent_id)
-}
-
-pub(super) fn normalize_interaction_action_without_active_runtime(
-    action: AgentInteractionAction,
-) -> AgentInteractionAction {
-    match action {
-        AgentInteractionAction::Resume => AgentInteractionAction::Execute,
-        action => action,
-    }
-}
-
-fn decide_interaction_action(
-    root: &Path,
-    parent_agent_id: &str,
-    session_id: &str,
-    message: &str,
-    output: &mut W,
-) -> Result {
-    writeln!(output, "[决策] Agent 正在判断直接回复或调用持久能力。")
-        .map_err(|error| format!("写入终端失败:{error}"))?;
-    let mut printed_chars = 0usize;
-    let mut reply_started = false;
-    let mut protocol_buffered = false;
-    let action =
-        tauri::async_runtime::block_on(decide_game_creator_agent_interaction_turn_for_session_at(
-            root,
-            parent_agent_id,
-            session_id,
-            message,
-            |delta| {
-                if protocol_buffered {
-                    return;
-                }
-                let trimmed = delta.accumulated_text.trim_start();
-                if !reply_started && (trimmed.starts_with('{') || trimmed.starts_with("```")) {
-                    protocol_buffered = true;
-                    return;
-                }
-                if delta.accumulated_text.len() <= printed_chars {
-                    return;
-                }
-                if !reply_started {
-                    let _ = write!(output, "陶泥儿> ");
-                    reply_started = true;
-                }
-                let chunk = &delta.accumulated_text[printed_chars..];
-                let _ = write!(output, "{chunk}");
-                let _ = output.flush();
-                printed_chars = delta.accumulated_text.len();
-            },
-        ))?;
-    if let AgentInteractionAction::Reply(reply) = &action {
-        if reply.len() > printed_chars {
-            if !reply_started {
-                write!(output, "陶泥儿> ").map_err(|error| format!("写入终端失败:{error}"))?;
-            }
-            write!(output, "{}", &reply[printed_chars..])
-                .map_err(|error| format!("写入终端失败:{error}"))?;
-        }
-        writeln!(output).map_err(|error| format!("写入终端失败:{error}"))?;
-    } else if reply_started {
-        writeln!(output).map_err(|error| format!("写入终端失败:{error}"))?;
-    }
-    Ok(action)
-}
-
-fn persist_swarm_reply(
-    root: &Path,
-    parent_agent_id: &str,
-    session_id: &str,
-    message: &str,
-    reply: &str,
-) -> Result<(), String> {
-    with_agent_conversation_session_lane_at(
-        root,
-        parent_agent_id,
-        "Swarm interaction 回复落盘",
-        || {
-            append_local_conversation_message_for_session_at(
-                root,
-                Some(parent_agent_id),
-                Some(session_id),
-                LocalConversationMessage {
-                    role: "user".to_string(),
-                    content: message.to_string(),
-                    agent_id: Some(parent_agent_id.to_string()),
-                },
-            )?;
-            append_local_conversation_message_for_session_at(
-                root,
-                Some(parent_agent_id),
-                Some(session_id),
-                LocalConversationMessage {
-                    role: "assistant".to_string(),
-                    content: reply.to_string(),
-                    agent_id: Some(parent_agent_id.to_string()),
-                },
-            )?;
-            Ok(())
-        },
-    )
-}
-
-fn steer_and_wait_for_swarm_turn(
-    root: &Path,
-    parent_agent_id: &str,
-    session_id: &str,
-    run_profile: &str,
-    expected_parent_source: Option<&str>,
-    run_id: &str,
-    message: &str,
-    previous_message_count: usize,
-    input: &Receiver,
-    output: &mut W,
-    label: &str,
-) -> Result {
-    require_external_agent_runner_for_cli_runtime_write(root)?;
-    let steer_id = format!("swarm-steer-{}", unix_millis());
-    let steer_source = expected_parent_source.map(str::to_string);
-    let result = tauri::async_runtime::block_on(steer_game_creator_agent_runtime_task(
-        root.display().to_string(),
-        parent_agent_id.to_string(),
-        session_id.to_string(),
-        run_id.to_string(),
-        steer_id.clone(),
-        message.to_string(),
-        Some(run_profile.to_string()),
-        steer_source,
-    ))?;
-    writeln!(
-        output,
-        "[{label}] run={} steer={} providerInterrupted={}",
-        run_id, steer_id, result.provider_interrupted
-    )
-    .map_err(|error| format!("写入终端失败:{error}"))?;
-    let conversation_baseline =
-        new_swarm_turn_conversation_baseline(previous_message_count, run_id);
-    wait_and_print_swarm_turn(
-        root,
-        parent_agent_id,
-        session_id,
-        run_profile,
-        expected_parent_source,
-        conversation_baseline,
-        input,
-        output,
-    )
-}
-
-fn start_and_wait_for_swarm_turn(
-    root: &Path,
-    parent_agent_id: &str,
-    session_id: &str,
-    run_profile: &str,
-    new_run_launch: SwarmNewRunLaunch<'_>,
-    task: &str,
-    previous_message_count: usize,
-    input: &Receiver,
-    output: &mut W,
-) -> Result {
-    require_external_agent_runner_for_cli_runtime_write(root)?;
-    let requested_run_id = format!("swarm-{parent_agent_id}-{}", unix_millis());
-    let started = match new_run_launch {
-        SwarmNewRunLaunch::ProjectSupervisor {
-            source,
-            run_profile,
-        } => start_game_creator_supervisor_background_task_for_session_at(
-            root,
-            Some(session_id),
-            task,
-            &requested_run_id,
-            source,
-            run_profile,
-        )?,
-        SwarmNewRunLaunch::ExplicitParentDebug => start_game_creator_agent_runtime_task(
-            root.display().to_string(),
-            parent_agent_id.to_string(),
-            Some(session_id.to_string()),
-            task.to_string(),
-            requested_run_id.clone(),
-        )?,
-    };
-    let accepted_run_id = accepted_swarm_run_id(&started, &requested_run_id);
-    writeln!(
-        output,
-        "[已投递] agent={} session={} run={}",
-        parent_agent_id, started.state.session_id, accepted_run_id
-    )
-    .map_err(|error| format!("写入终端失败:{error}"))?;
-    let conversation_baseline =
-        new_swarm_turn_conversation_baseline(previous_message_count, accepted_run_id);
-    wait_and_print_swarm_turn(
-        root,
-        parent_agent_id,
-        session_id,
-        run_profile,
-        new_run_launch.expected_parent_source(),
-        conversation_baseline,
-        input,
-        output,
-    )
-}
-
-pub(super) fn accepted_swarm_run_id(
-    started: &AgentRuntimeResult,
-    requested_run_id: &str,
-) -> String {
-    started
-        .accepted_run_id
-        .as_deref()
-        .map(str::trim)
-        .filter(|run_id| !run_id.is_empty())
-        .unwrap_or(requested_run_id)
-        .to_string()
-}
-
-pub(super) fn handle_swarm_resume_turn(
-    root: &Path,
-    parent_agent_id: &str,
-    session_id: &str,
-    run_profile: &str,
-    expected_parent_source: Option<&str>,
-    previous_message_count: usize,
-    input: &Receiver,
-    output: &mut W,
-) -> Result {
-    require_external_agent_runner_for_cli_runtime_write(root)?;
-    let resumed = resume_game_creator_agent_background_tasks_at(root)?;
-    if !resumed.is_empty() {
-        writeln!(output, "[恢复扫描] 已检查 {} 个 Runtime。", resumed.len())
-            .map_err(|error| format!("写入终端失败:{error}"))?;
-    }
-    let current_runtimes = read_game_creator_agent_runtimes_at(root)?;
-    let Some(parent) = swarm_parent_runtime(
-        parent_agent_id,
-        session_id,
-        run_profile,
-        expected_parent_source,
-        ¤t_runtimes,
-    ) else {
-        writeln!(output, "[恢复] 当前 Session 没有可恢复的运行任务。")
-            .map_err(|error| format!("写入终端失败:{error}"))?;
-        return Ok(SwarmChatFlow::Continue);
-    };
-    let pending = next_pending_swarm_task(parent, session_id, run_profile, expected_parent_source);
-    let (target_run_id, target_status, target_phase) =
-        if game_creator_agent_runtime_accepts_steer(&parent.state)
-            || parent.state.status == "pending"
-        {
-            (
-                parent.state.run_id.as_str(),
-                parent.state.status.as_str(),
-                parent.state.phase.as_str(),
-            )
-        } else if let Some(task) = pending {
-            (
-                task.run_id.as_str(),
-                task.status.as_str(),
-                task.phase.as_str(),
-            )
-        } else {
-            writeln!(output, "[恢复] 当前 Session 没有可恢复的运行任务。")
-                .map_err(|error| format!("写入终端失败:{error}"))?;
-            return Ok(SwarmChatFlow::Continue);
-        };
-    writeln!(
-        output,
-        "[恢复] 继续观察 run={} status={} phase={}",
-        target_run_id, target_status, target_phase
-    )
-    .map_err(|error| format!("写入终端失败:{error}"))?;
-    let mut conversation_baseline =
-        new_swarm_turn_conversation_baseline(previous_message_count, target_run_id);
-    capture_recovered_swarm_assistant_at(
-        root,
-        parent_agent_id,
-        session_id,
-        &mut conversation_baseline,
-    )?;
-    wait_and_print_swarm_turn(
-        root,
-        parent_agent_id,
-        session_id,
-        run_profile,
-        expected_parent_source,
-        conversation_baseline,
-        input,
-        output,
-    )
-}
-
-fn wait_and_print_swarm_turn(
-    root: &Path,
-    parent_agent_id: &str,
-    session_id: &str,
-    run_profile: &str,
-    expected_parent_source: Option<&str>,
-    conversation_baseline: SwarmTurnConversationBaseline,
-    input: &Receiver,
-    output: &mut W,
-) -> Result {
-    let mut observer = SwarmRuntimeObserver::seed(root)?;
-    let outcome = wait_for_swarm_turn(
-        root,
-        parent_agent_id,
-        session_id,
-        run_profile,
-        expected_parent_source,
-        conversation_baseline,
-        input,
-        output,
-        &mut observer,
-        SWARM_CHAT_POLL_INTERVAL,
-        SWARM_CHAT_SETTLE_WINDOW,
-    )?;
-    if outcome == SwarmTurnOutcome::Quit {
-        print_swarm_chat_exit(output)?;
-        return Ok(SwarmChatFlow::Exit);
-    }
-    print_turn_outcome(outcome, output)?;
-    Ok(SwarmChatFlow::Continue)
-}
diff --git a/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/turn_wait.rs b/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/turn_wait.rs
deleted file mode 100644
index e1d820bd6..000000000
--- a/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/turn_wait.rs
+++ /dev/null
@@ -1,427 +0,0 @@
-use super::*;
-
-pub(super) const SWARM_CHAT_POLL_INTERVAL: Duration = Duration::from_millis(250);
-pub(super) const SWARM_CHAT_SETTLE_WINDOW: Duration = Duration::from_millis(1_500);
-
-#[derive(Debug, Eq, PartialEq)]
-pub(super) struct SwarmTurnObservation {
-    pub(super) outcome: SwarmTurnOutcome,
-    pub(super) input_closed: bool,
-}
-
-pub(super) fn wait_for_swarm_turn(
-    root: &Path,
-    parent_agent_id: &str,
-    session_id: &str,
-    run_profile: &str,
-    expected_parent_source: Option<&str>,
-    conversation_baseline: SwarmTurnConversationBaseline,
-    input: &Receiver,
-    output: &mut W,
-    observer: &mut SwarmRuntimeObserver,
-    poll_interval: Duration,
-    settle_window: Duration,
-) -> Result {
-    let mut stable_since: Option = None;
-    let mut recovery_scan_required = true;
-    let mut last_runner_check = Instant::now();
-    let mut input_closed = false;
-    loop {
-        let runtimes = read_game_creator_agent_runtimes_at(root)?;
-        let turn_runtimes = swarm_current_runtimes_for_run(
-            parent_agent_id,
-            session_id,
-            run_profile,
-            expected_parent_source,
-            &conversation_baseline.parent_run_id,
-            &runtimes,
-        );
-        let changed = observer.print_changes(&turn_runtimes, output)?;
-        if changed {
-            stable_since = None;
-        }
-        let mut reconciliation = swarm_reconciliation_agents(&turn_runtimes);
-        if !reconciliation.is_empty() {
-            observer.close_response_line(output)?;
-            return build_reconciliation_turn_outcome(
-                root,
-                parent_agent_id,
-                session_id,
-                &conversation_baseline,
-                &runtimes,
-                reconciliation,
-            );
-        }
-        if !input_closed {
-            match observer.resolve_confirmations(
-                root,
-                parent_agent_id,
-                &turn_runtimes,
-                input,
-                output,
-            )? {
-                SwarmConfirmationResolution::Handled => {
-                    stable_since = None;
-                    recovery_scan_required = true;
-                    continue;
-                }
-                SwarmConfirmationResolution::InputClosed => {
-                    mark_swarm_turn_input_closed(&mut input_closed, observer, output)?;
-                    stable_since = None;
-                    continue;
-                }
-                SwarmConfirmationResolution::Quit => return Ok(SwarmTurnOutcome::Quit),
-                SwarmConfirmationResolution::None => {}
-            }
-            match observer.resolve_user_input_requests(
-                root,
-                parent_agent_id,
-                &turn_runtimes,
-                input,
-                output,
-            )? {
-                SwarmConfirmationResolution::Handled => {
-                    stable_since = None;
-                    recovery_scan_required = true;
-                    continue;
-                }
-                SwarmConfirmationResolution::InputClosed => {
-                    mark_swarm_turn_input_closed(&mut input_closed, observer, output)?;
-                    stable_since = None;
-                    continue;
-                }
-                SwarmConfirmationResolution::Quit => return Ok(SwarmTurnOutcome::Quit),
-                SwarmConfirmationResolution::None => {}
-            }
-        }
-        let pending_interactions =
-            swarm_unhandled_interaction_reasons(parent_agent_id, &turn_runtimes, input_closed);
-        if !pending_interactions.is_empty() {
-            observer.close_response_line(output)?;
-            return build_incomplete_turn_outcome(
-                root,
-                parent_agent_id,
-                session_id,
-                &conversation_baseline,
-                &runtimes,
-                pending_interactions,
-            );
-        }
-        let failure_scan = scan_swarm_terminal_failures_at(
-            root,
-            parent_agent_id,
-            session_id,
-            run_profile,
-            expected_parent_source,
-            &conversation_baseline.parent_run_id,
-            &runtimes,
-        );
-        if !failure_scan.reconciliation_agents.is_empty() {
-            observer.close_response_line(output)?;
-            return build_reconciliation_turn_outcome(
-                root,
-                parent_agent_id,
-                session_id,
-                &conversation_baseline,
-                &runtimes,
-                failure_scan.reconciliation_agents,
-            );
-        }
-        if !failure_scan.failed_agents.is_empty() {
-            observer.close_response_line(output)?;
-            return build_failed_turn_outcome(
-                root,
-                parent_agent_id,
-                session_id,
-                &conversation_baseline,
-                &runtimes,
-                failure_scan.failed_agents,
-            );
-        }
-        if !failure_scan.incomplete_reasons.is_empty() {
-            observer.close_response_line(output)?;
-            return build_incomplete_turn_outcome(
-                root,
-                parent_agent_id,
-                session_id,
-                &conversation_baseline,
-                &runtimes,
-                failure_scan.incomplete_reasons,
-            );
-        }
-        if last_runner_check.elapsed() >= Duration::from_secs(2) {
-            let runner = read_external_agent_runner_status();
-            last_runner_check = Instant::now();
-            if swarm_turn_is_busy(
-                root,
-                parent_agent_id,
-                session_id,
-                run_profile,
-                expected_parent_source,
-                &conversation_baseline.parent_run_id,
-                &runtimes,
-            )? && (!runner.enabled || !runner.running)
-            {
-                reconciliation.push("external-runner".to_string());
-                observer.close_response_line(output)?;
-                return build_reconciliation_turn_outcome(
-                    root,
-                    parent_agent_id,
-                    session_id,
-                    &conversation_baseline,
-                    &runtimes,
-                    reconciliation,
-                );
-            }
-        }
-        if swarm_turn_is_busy(
-            root,
-            parent_agent_id,
-            session_id,
-            run_profile,
-            expected_parent_source,
-            &conversation_baseline.parent_run_id,
-            &runtimes,
-        )? {
-            stable_since = None;
-            recovery_scan_required = true;
-        } else {
-            let since = stable_since.get_or_insert_with(Instant::now);
-            if since.elapsed() >= settle_window {
-                if recovery_scan_required {
-                    observer.close_response_line(output)?;
-                    writeln!(
-                        output,
-                        "[收束] Runtime 已空闲,检查待发布的 receipt / join。"
-                    )
-                    .map_err(|error| format!("写入终端失败:{error}"))?;
-                    output
-                        .flush()
-                        .map_err(|error| format!("刷新终端失败:{error}"))?;
-                    resume_game_creator_agent_background_tasks_at(root)?;
-                    recovery_scan_required = false;
-                    stable_since = Some(Instant::now());
-                    continue;
-                }
-                let conversation_metrics = read_turn_conversation_metrics(
-                    root,
-                    parent_agent_id,
-                    session_id,
-                    &conversation_baseline,
-                )?;
-                let parent_runtime_is_current = swarm_parent_runtime_for_run(
-                    parent_agent_id,
-                    session_id,
-                    run_profile,
-                    expected_parent_source,
-                    &conversation_baseline.parent_run_id,
-                    &runtimes,
-                )
-                .is_some();
-                let parent_runtime = swarm_parent_runtime_snapshot_for_run(
-                    root,
-                    parent_agent_id,
-                    session_id,
-                    run_profile,
-                    expected_parent_source,
-                    &conversation_baseline.parent_run_id,
-                    &runtimes,
-                )?;
-                let completion_blockers = if parent_runtime_is_current {
-                    parent_runtime
-                        .as_ref()
-                        .map(|parent| swarm_parent_completion_contract_blockers_at(root, parent))
-                        .unwrap_or_else(|| vec!["parent-runtime-missing".to_string()])
-                } else if parent_runtime
-                    .as_ref()
-                    .is_some_and(parent_runtime_completed)
-                {
-                    Vec::new()
-                } else {
-                    vec!["parent-runtime-missing".to_string()]
-                };
-                match classify_swarm_turn_terminal(
-                    parent_runtime.as_ref(),
-                    conversation_metrics,
-                    0,
-                    0,
-                    completion_blockers.len(),
-                ) {
-                    SwarmTurnTerminalClassification::Settled => {
-                        let printed_metrics = print_new_parent_reply(
-                            root,
-                            parent_agent_id,
-                            session_id,
-                            &conversation_baseline,
-                            output,
-                            observer,
-                        )?;
-                        if printed_metrics != conversation_metrics {
-                            return build_incomplete_turn_outcome(
-                                root,
-                                parent_agent_id,
-                                session_id,
-                                &conversation_baseline,
-                                &runtimes,
-                                vec!["conversation-changed-before-settle".to_string()],
-                            );
-                        }
-                        let report = build_swarm_turn_report(
-                            SwarmTurnReportOutcome::Settled,
-                            parent_agent_id,
-                            session_id,
-                            Some(&conversation_baseline.parent_run_id),
-                            &runtimes,
-                            conversation_metrics,
-                            0,
-                        )?;
-                        return Ok(SwarmTurnOutcome::Settled(report));
-                    }
-                    SwarmTurnTerminalClassification::Failed => {
-                        return build_failed_turn_outcome(
-                            root,
-                            parent_agent_id,
-                            session_id,
-                            &conversation_baseline,
-                            &runtimes,
-                            vec![format!(
-                                "{}:{}",
-                                parent_agent_id,
-                                parent_runtime
-                                    .as_ref()
-                                    .map(|runtime| runtime.state.phase.as_str())
-                                    .unwrap_or("missing")
-                            )],
-                        );
-                    }
-                    SwarmTurnTerminalClassification::Incomplete => {
-                        let mut reasons = completion_blockers;
-                        append_swarm_terminal_snapshot_reasons(
-                            &mut reasons,
-                            parent_runtime.as_ref(),
-                            conversation_metrics,
-                        );
-                        return build_incomplete_turn_outcome(
-                            root,
-                            parent_agent_id,
-                            session_id,
-                            &conversation_baseline,
-                            &runtimes,
-                            reasons,
-                        );
-                    }
-                }
-            }
-        }
-        if input_closed {
-            std::thread::sleep(poll_interval);
-            continue;
-        }
-        match input.recv_timeout(poll_interval) {
-            Ok(SwarmInputEvent::Line(line)) => {
-                let Some(command) = parse_swarm_chat_input(&line) else {
-                    continue;
-                };
-                observer.close_response_line(output)?;
-                match command {
-                    SwarmChatInput::Quit => return Ok(SwarmTurnOutcome::Quit),
-                    SwarmChatInput::Help => print_swarm_chat_help(output)?,
-                    SwarmChatInput::Agents => print_swarm_agents(root, output)?,
-                    SwarmChatInput::Status => print_swarm_status(root, output)?,
-                    SwarmChatInput::History => {
-                        print_conversation_history(root, parent_agent_id, output)?
-                    }
-                    SwarmChatInput::Compact => {
-                        handle_swarm_context_compaction(root, parent_agent_id, output)?
-                    }
-                    SwarmChatInput::Goal(command) => {
-                        let _ = handle_swarm_goal_command(root, parent_agent_id, command, output)?;
-                        stable_since = None;
-                        recovery_scan_required = true;
-                    }
-                    SwarmChatInput::InvalidGoal(error) => print_swarm_goal_error(output, &error)?,
-                    SwarmChatInput::Resume => {
-                        writeln!(output, "[恢复] 当前已经在观察这个 Runtime。")
-                            .map_err(|error| format!("写入终端失败:{error}"))?;
-                    }
-                    SwarmChatInput::Message(message) => {
-                        if let Some(parent) = swarm_parent_steer_target(
-                            parent_agent_id,
-                            session_id,
-                            run_profile,
-                            expected_parent_source,
-                            Some(&conversation_baseline.parent_run_id),
-                            &runtimes,
-                        ) {
-                            let steer_id = format!("swarm-steer-{}", unix_millis());
-                            let result = tauri::async_runtime::block_on(
-                                steer_game_creator_agent_runtime_task(
-                                    root.display().to_string(),
-                                    parent_agent_id.to_string(),
-                                    parent.state.session_id.clone(),
-                                    parent.state.run_id.clone(),
-                                    steer_id.clone(),
-                                    message,
-                                    Some(run_profile.to_string()),
-                                    None,
-                                ),
-                            )?;
-                            writeln!(
-                                output,
-                                "[已追加] run={} steer={} providerInterrupted={}",
-                                parent.state.run_id, steer_id, result.provider_interrupted
-                            )
-                            .map_err(|error| format!("写入终端失败:{error}"))?;
-                        } else {
-                            writeln!(output, "[暂未发送] 子 Agent 尚未收束,请稍后重发。")
-                                .map_err(|error| format!("写入终端失败:{error}"))?;
-                        }
-                        stable_since = None;
-                        recovery_scan_required = true;
-                    }
-                }
-            }
-            Ok(SwarmInputEvent::Eof) | Err(RecvTimeoutError::Disconnected) => {
-                mark_swarm_turn_input_closed(&mut input_closed, observer, output)?;
-            }
-            Ok(SwarmInputEvent::Error(error)) => {
-                observer.close_response_line(output)?;
-                return Err(format!("读取终端输入失败:{error}"));
-            }
-            Err(RecvTimeoutError::Timeout) => {}
-        }
-    }
-}
-
-pub(super) fn runtimes_are_busy(runtimes: &[AgentRuntimeResult]) -> bool {
-    runtimes.iter().any(runtime_is_busy)
-}
-
-pub(super) fn runtime_is_busy(runtime: &AgentRuntimeResult) -> bool {
-    matches!(
-        runtime.state.status.as_str(),
-        "pending"
-            | "running"
-            | "waiting-for-confirmation"
-            | "waiting-for-user-input"
-            | "cancelling"
-    ) || runtime.state.phase == "needs-reconciliation"
-        || runtime.task_queue.pending > 0
-        || runtime.task_queue.running > 0
-        || runtime.task_queue.waiting_for_confirmation > 0
-        || runtime.task_queue.waiting_for_user_input > 0
-}
-
-pub(super) fn mark_swarm_turn_input_closed(
-    input_closed: &mut bool,
-    observer: &mut SwarmRuntimeObserver,
-    output: &mut W,
-) -> Result<(), String> {
-    if *input_closed {
-        return Ok(());
-    }
-    *input_closed = true;
-    observer.close_response_line(output)?;
-    writeln!(output, "[输入已关闭] 当前 turn 继续运行,等待可信终态。")
-        .map_err(|error| format!("写入终端失败:{error}"))
-}
diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/project.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/project.rs
index 6e9abb9e8..f70834828 100644
--- a/apps/ai-game-creator-shell/src-tauri/src/tests/project.rs
+++ b/apps/ai-game-creator-shell/src-tauri/src/tests/project.rs
@@ -1929,58 +1929,6 @@ async fn generate_local_game_draft_fails_after_max_passes_without_final_artifact
     fs::remove_dir_all(root).ok();
 }
 
-#[test]
-fn canvas_sync_suggestion_is_media_type_aware() {
-    let art_group = GAME_CREATOR_AGENT_GROUP_DEFINITIONS
-        .iter()
-        .find(|definition| definition.id == "art")
-        .copied()
-        .expect("art group");
-    let audio_group = GAME_CREATOR_AGENT_GROUP_DEFINITIONS
-        .iter()
-        .find(|definition| definition.id == "audio")
-        .copied()
-        .expect("audio group");
-    let art_role = ART_AGENT_ROLES
-        .iter()
-        .find(|role| role.id == "asset")
-        .copied()
-        .expect("art asset role");
-    let audio_role = AUDIO_AGENT_ROLES
-        .iter()
-        .find(|role| role.id == "sfx")
-        .copied()
-        .expect("audio sfx role");
-    let art_brief = AgentRoleBrief {
-        group_definition: art_group,
-        role_definition: art_role,
-        markdown: String::new(),
-        relative_path: ".agent/passes/pass-1/groups/art/asset.md".to_string(),
-        memory_relative_path: agent_role_memory_relative_path(art_group, art_role),
-        status: "completed".to_string(),
-        tool_id: art_role.tool_id.to_string(),
-        summary: String::new(),
-    };
-    let audio_brief = AgentRoleBrief {
-        group_definition: audio_group,
-        role_definition: audio_role,
-        markdown: String::new(),
-        relative_path: ".agent/passes/pass-1/groups/audio/sfx.md".to_string(),
-        memory_relative_path: agent_role_memory_relative_path(audio_group, audio_role),
-        status: "completed".to_string(),
-        tool_id: audio_role.tool_id.to_string(),
-        summary: String::new(),
-    };
-    let input_paths = vec![".agent/manifest.json".to_string()];
-    let image_canvas_assets = vec!["image/png".to_string()];
-    let audio_canvas_assets = vec!["audio/wav".to_string()];
-
-    assert!(suggested_canvas_tool_call(&art_brief, &input_paths, &image_canvas_assets).is_none());
-    assert!(suggested_canvas_tool_call(&audio_brief, &input_paths, &image_canvas_assets).is_some());
-    assert!(suggested_canvas_tool_call(&audio_brief, &input_paths, &audio_canvas_assets).is_none());
-    assert!(suggested_canvas_tool_call(&art_brief, &input_paths, &audio_canvas_assets).is_some());
-}
-
 #[test]
 fn init_local_game_project_creates_manifest_and_dirs() {
     let root = unique_project_path();
diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/provider.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/provider.rs
index 77e12f38c..aeb59c559 100644
--- a/apps/ai-game-creator-shell/src-tauri/src/tests/provider.rs
+++ b/apps/ai-game-creator-shell/src-tauri/src/tests/provider.rs
@@ -6461,31 +6461,7 @@ async fn agent_loop_writes_spec_findings_and_retries_generator() {
         .iter()
         .any(|step| step["agent"] == "数值组 / Difficulty"));
     assert!(steps.iter().any(|step| step["agent"] == "美术组 / Asset"));
-    assert!(steps.iter().any(|step| {
-        step["agent"] == "美术组 / Asset"
-            && step["toolCalls"]
-                .as_array()
-                .unwrap()
-                .iter()
-                .any(|tool_call| {
-                    tool_call["toolId"] == "agent.tool.suggest.canvas.project_sync"
-                        && tool_call["status"] == "suggested"
-                })
-    }));
     assert!(steps.iter().any(|step| step["agent"] == "音乐组 / SFX"));
-    assert!(steps.iter().any(|step| {
-        step["agent"] == "音乐组 / SFX"
-            && step["toolCalls"]
-                .as_array()
-                .unwrap()
-                .iter()
-                .any(|tool_call| {
-                    tool_call["toolId"] == "agent.tool.suggest.canvas.project_sync"
-                        && tool_call["summary"]
-                            .as_str()
-                            .is_some_and(|summary| summary.contains("/sync-canvas-project"))
-                })
-    }));
     assert!(steps.iter().any(|step| step["agent"] == "程序组 / Code"));
     assert!(steps.iter().any(|step| step["agent"] == "运营组 / Publish"));
     assert!(steps
diff --git a/apps/ai-game-creator-shell/src/App.tsx b/apps/ai-game-creator-shell/src/App.tsx
index 2403d3913..ab0f9c102 100644
--- a/apps/ai-game-creator-shell/src/App.tsx
+++ b/apps/ai-game-creator-shell/src/App.tsx
@@ -40,7 +40,6 @@ import type {
   LocalGameProjectRevisionStatus,
   LocalPreviewResult,
   LocalPreviewStatus,
-  LocalProjectFileResult,
   LocalProjectKind,
   PendingUiConfirmation,
   ProjectPermissionPolicyView,
@@ -67,19 +66,13 @@ import {
   writeRecentWorkspace,
 } from './features/app-shell/model';
 import { WorkspaceLauncherShell } from './features/app-shell/WorkspaceLauncher';
-import {
-  projectAgentRuntimeSummaries,
-  summarizeAgentRunCompletionForChat,
-} from './features/project-summary/agentPresentation';
+import { projectAgentRuntimeSummaries } from './features/project-summary/agentPresentation';
 import {
   isAbsoluteProjectPath,
   projectPathHasControlCharacter,
 } from './features/project-summary/projectSummary';
-import { parseAgentRunTrace } from './features/project-workspace/agentRunTrace';
 import { importDesignFiles } from './features/project-workspace/importDesignFiles';
-import { parseRememberInput } from './features/project-workspace/memoryCommands';
 import {
-  isAgentTraceFilePath,
   needsInitializedChatProject,
   resolveChatProjectPath,
 } from './features/project-workspace/projectCommandPolicy';
@@ -146,17 +139,9 @@ function isPersistableDirectCodexConversationMessage(message: ChatMessage) {
  */
 
 export { AuthenticatedClient } from './app/AuthenticatedClient';
-export {
-  deriveAgentStatusCards,
-  summarizeAgentAudit,
-  summarizeAgentRunTrace,
-} from './features/project-summary/agentPresentation';
+export { deriveAgentStatusCards } from './features/project-summary/agentPresentation';
 export { isAbsoluteProjectPath } from './features/project-summary/projectSummary';
-export {
-  needsInitializedChatProject,
-  parseRememberInput,
-  resolveChatProjectPath,
-};
+export { needsInitializedChatProject, resolveChatProjectPath };
 
 export function WorkspaceLauncher(props: WorkspaceLauncherProps) {
   return ;
@@ -857,7 +842,7 @@ export function App({
         localProjectPathRef.current === initialProjectPath &&
         !planningStartMode
       ) {
-        void refreshAgentRunTrace(initialProjectPath);
+        void refreshAgentRuntimes(initialProjectPath);
       }
     });
     // Initial project opening is guarded by initialProjectOpenedRef.
@@ -1512,7 +1497,7 @@ export function App({
       }
       void loadProjectConversation(openedProject.projectPath);
       if (!directProjectMode) {
-        void refreshAgentRunTrace(openedProject.projectPath);
+        void refreshAgentRuntimes(openedProject.projectPath);
       }
     } catch (error) {
       if (projectScopeVersionRef.current !== projectScopeVersion) {
@@ -1730,9 +1715,8 @@ export function App({
    *
    * `activate_local_game_preview` 是 Rust 侧的「这条预览还活着、且属于这个项目」闸门:
    * 它只核对内存 registry 里的状态并回传可用的 loopback 地址,前端据此进客户端运行视图。
-   * 同一个动作过去由工作台壳的 `/preview open` 聊天命令承担,那条命令链随 Supervisor
-   * 前端链路一起退役,行为改由「运行」入口承接,结果经 DirectProject 聊天的 `announce`
-   * 交给聊天自己的消息流。返回 `null` 表示没有可复用的活体预览,调用方照旧重启预览。
+   * 行为由「运行」入口承接,结果经 DirectProject 聊天的 `announce` 交给聊天自己的消息流。
+   * 返回 `null` 表示没有可复用的活体预览,调用方照旧重启预览。
    */
   async function activateRunningPreview(
     invoke: TauriInvoke,
@@ -1795,7 +1779,7 @@ export function App({
       updateClientPreview(previewResult);
       void refreshManifest(nextProjectPath);
       if (!directProjectMode) {
-        void refreshAgentRunTrace(nextProjectPath);
+        void refreshAgentRuntimes(nextProjectPath);
       }
       if (announceToChat) {
         announceProjectChatMessage(
@@ -1810,36 +1794,6 @@ export function App({
     }
   }
 
-  async function loadAgentRunTraceFile(
-    relativePath: string,
-    nextProjectPath = resolveChatProjectPath(localProject) ?? '',
-  ) {
-    const invoke = resolveTauriInvoke();
-    if (!invoke) {
-      return null;
-    }
-    if (!nextProjectPath) {
-      return null;
-    }
-
-    try {
-      const result = await invoke(
-        'read_local_project_file',
-        {
-          projectPath: nextProjectPath,
-          relativePath,
-          commandId: isAgentTraceFilePath(relativePath)
-            ? 'agent.trace_read'
-            : 'file.read',
-        },
-      );
-      const trace = parseAgentRunTrace(result.content);
-      return summarizeAgentRunCompletionForChat(trace);
-    } catch {
-      return null;
-    }
-  }
-
   function rememberAgentRuntimeState(runtime: AgentRuntimeState | null) {
     if (!runtime) {
       return;
@@ -1955,21 +1909,6 @@ export function App({
     }
   }
 
-  async function refreshAgentRunTrace(
-    nextProjectPath = resolveChatProjectPath(localProject) ?? '',
-  ) {
-    if (!nextProjectPath) {
-      await refreshAgentRuntimes(nextProjectPath);
-      return null;
-    }
-    const summary = await loadAgentRunTraceFile(
-      '.agent/run.latest.json',
-      nextProjectPath,
-    );
-    await refreshAgentRuntimes(nextProjectPath);
-    return summary;
-  }
-
   const professionalResultCandidates = taskRowsFromManifest(manifest).map(
     (task) => ({
       agentId: agentConversationId(task),
diff --git a/apps/ai-game-creator-shell/src/features/project-summary/agentPresentation.ts b/apps/ai-game-creator-shell/src/features/project-summary/agentPresentation.ts
index 77f8045af..1f7d05317 100644
--- a/apps/ai-game-creator-shell/src/features/project-summary/agentPresentation.ts
+++ b/apps/ai-game-creator-shell/src/features/project-summary/agentPresentation.ts
@@ -1,24 +1,14 @@
 import {
-  GAME_CREATION_AGENT_CAPABILITIES,
-  GAME_CREATION_APP_COMMANDS,
   type GameCreationAgentRunStep,
   type GameCreationAgentRunTrace,
-  type GameCreationAgentToolCallTrace,
-  type GameCreationAppAgentGroup,
   type GameCreationAppManifest,
-  type GameCreationAppTaskState,
   type GameCreationAppTaskStatus,
-  selectGameCreationAppReadyTasks,
 } from '../../../../../packages/shared/src/contracts/gameCreationApp';
 import type {
   AgentRuntimeState,
-  AgentRuntimeTaskQueueSummary,
-  AgentRuntimeTaskRecord,
   AgentStatusCard,
   AgentTaskGraphState,
   GameCreatorLlmConfigStatus,
-  LocalProjectFileEntry,
-  ProjectPermissionPolicy,
 } from '../../app/types';
 import type { ProjectAgentRuntimeSummary } from '../../view/project-development';
 import {
@@ -34,18 +24,6 @@ import {
   projectRuntimeVisibleError,
   taskRowsFromManifest,
 } from '../agent-runtime';
-import {
-  formatAgentRunStatus,
-  formatProjectPolicyCommandList,
-  formatTraceRepairRoutes,
-  formatTraceTaskId,
-  formatTraceTaskIds,
-  isSafeProjectRelativePath,
-  previewStatusLabels,
-  readableArtifactPathFromAgentRunTrace,
-  taskGroupLabels,
-  taskStatusLabels,
-} from './projectSummary';
 
 export function taskStatusFromTraceStep(
   stepStatus: string | undefined,
@@ -357,316 +335,6 @@ export function projectAgentRuntimeSummaries(
   });
 }
 
-export function sameStringArray(left: string[], right: string[]) {
-  return (
-    left.length === right.length &&
-    left.every((value, index) => value === right[index])
-  );
-}
-
-export function sameAgentRuntimeTaskQueue(
-  left: AgentRuntimeTaskQueueSummary | null,
-  right: AgentRuntimeTaskQueueSummary | null,
-) {
-  if (!left || !right) {
-    return left === right;
-  }
-  return (
-    left.total === right.total &&
-    left.pending === right.pending &&
-    left.running === right.running &&
-    (left.waitingForConfirmation ?? 0) ===
-      (right.waitingForConfirmation ?? 0) &&
-    (left.waitingForUserInput ?? 0) === (right.waitingForUserInput ?? 0) &&
-    (left.cancelled ?? 0) === (right.cancelled ?? 0) &&
-    left.completed === right.completed &&
-    left.failed === right.failed &&
-    left.latestRunId === right.latestRunId &&
-    left.updatedAt === right.updatedAt
-  );
-}
-
-export function sameAgentRuntimeTasks(
-  left: AgentRuntimeTaskRecord[],
-  right: AgentRuntimeTaskRecord[],
-) {
-  return (
-    left.length === right.length &&
-    left.every((task, index) => {
-      const other = right[index];
-      return (
-        other &&
-        task.runId === other.runId &&
-        task.source === other.source &&
-        task.parentAgentId === other.parentAgentId &&
-        task.parentRunId === other.parentRunId &&
-        task.delegationId === other.delegationId &&
-        task.status === other.status &&
-        task.phase === other.phase &&
-        task.task === other.task &&
-        task.currentAction === other.currentAction &&
-        task.terminalDetail === other.terminalDetail &&
-        task.error === other.error &&
-        task.updatedAt === other.updatedAt
-      );
-    })
-  );
-}
-
-export function hasProjectFile(files: LocalProjectFileEntry[], path: string) {
-  return files.some((file) => file.kind === 'file' && file.path === path);
-}
-
-export function hasTracePath(
-  trace: GameCreationAgentRunTrace | null,
-  path: string,
-) {
-  if (!trace) {
-    return false;
-  }
-  return (
-    trace.artifacts.some((artifact) => artifact.path === path) ||
-    trace.steps.some(
-      (step) =>
-        step.inputPaths.includes(path) || step.outputPaths.includes(path),
-    )
-  );
-}
-
-export function formatAuditStatus(
-  ok: boolean,
-  okLabel = '通过',
-  failLabel = '待补',
-) {
-  return ok ? okLabel : failLabel;
-}
-
-export function summarizeAuditLoopTrace(
-  trace: GameCreationAgentRunTrace | null,
-) {
-  if (!trace) {
-    return '- Loop trace:待生成 · 还没有 .agent/run.latest.json';
-  }
-
-  const hasEvaluatorPassed = trace.steps.some(
-    (step) => step.agent === 'Evaluator' && step.status === 'passed',
-  );
-  const failed =
-    trace.status === 'failed' ||
-    Boolean(trace.error) ||
-    trace.stopReason === 'failed' ||
-    trace.stopReason === 'max-passes-exhausted';
-  const passed =
-    !failed &&
-    hasEvaluatorPassed &&
-    ['passed', 'preview-running', 'preview-stopped'].includes(trace.status);
-  const needsRevision =
-    !failed &&
-    (trace.status === 'needs-revision' ||
-      trace.stopReason === 'evaluator-needs-revision');
-  const label = failed
-    ? '未通过'
-    : passed
-      ? '通过'
-      : needsRevision
-        ? '需返工'
-        : trace.status === 'running'
-          ? '运行中'
-          : trace.status === 'artifacts-written'
-            ? '待自检'
-            : hasEvaluatorPassed
-              ? '待核对'
-              : '缺步骤';
-
-  return `- Loop trace:${label} · run ${trace.runId},${trace.passes}/${trace.maxPasses} 轮,${trace.toolCallCount}/${trace.maxToolCalls} 次工具调用,${trace.stopReason}`;
-}
-
-export function summarizeAgentAudit(
-  nextManifest: GameCreationAppManifest,
-  nextProjectPath: string,
-  files: LocalProjectFileEntry[],
-  trace: GameCreationAgentRunTrace | null,
-  commandLogContent = '',
-) {
-  const tasks = taskRowsFromManifest(nextManifest);
-  const groups = new Set(tasks.map((task) => task.group));
-  const groupIds = Object.keys(taskGroupLabels) as GameCreationAppAgentGroup[];
-  const configuredGroups = groupIds.filter((group) => groups.has(group));
-  const traceGroups = new Set(
-    trace?.steps
-      .map((step) => step.group)
-      .filter((group): group is GameCreationAppAgentGroup => Boolean(group)) ??
-      [],
-  );
-  const collaborationGroups = groupIds.filter((group) =>
-    traceGroups.has(group),
-  );
-  const completedCount = tasks.filter(
-    (task) => task.status === 'completed',
-  ).length;
-  const readyTasks = selectGameCreationAppReadyTasks({ tasks });
-  const readySummary =
-    readyTasks.length > 0
-      ? readyTasks
-          .slice(0, 3)
-          .map((task) => `${taskGroupLabels[task.group]} / ${task.role}`)
-          .join(';')
-      : '暂无';
-  const traceAgents = new Set(trace?.steps.map((step) => step.agent) ?? []);
-  const hasCoreLoop =
-    trace !== null &&
-    ['Planner', 'Orchestrator', 'Generator', 'Evaluator'].every((agent) =>
-      traceAgents.has(agent),
-    );
-  const finalArtifacts = [
-    'game/index.html',
-    'game/game_design.md',
-    'game/balance.json',
-    'assets/manifest.art.json',
-    'assets/manifest.audio.json',
-    'exports/README.md',
-  ].filter((path) => hasProjectFile(files, path) || hasTracePath(trace, path));
-  const hasShortMemory =
-    hasProjectFile(files, 'memory/session.md') ||
-    hasTracePath(trace, 'memory/session.md');
-  const hasLongMemory =
-    hasProjectFile(files, 'memory/project.md') ||
-    hasTracePath(trace, 'memory/project.md');
-  const hasBlackboardMemory =
-    hasProjectFile(files, 'memory/blackboard.md') ||
-    hasTracePath(trace, 'memory/blackboard.md');
-  const hasAgentPrivateMemory =
-    files.some((file) => file.path.startsWith('memory/agents/')) ||
-    (trace?.artifacts.some((artifact) =>
-      artifact.path.startsWith('memory/agents/'),
-    ) ??
-      false) ||
-    (trace?.steps.some((step) =>
-      [...step.inputPaths, ...step.outputPaths].some((path) =>
-        path.startsWith('memory/agents/'),
-      ),
-    ) ??
-      false);
-  const hasCanvasAsset = nextManifest.assets.some(
-    (asset) => asset.source.kind === 'canvas',
-  );
-  const suggestedCanvasSync =
-    trace?.steps.some((step) =>
-      step.toolCalls.some((toolCall) =>
-        toolCall.toolId.includes('canvas.project_sync'),
-      ),
-    ) ?? false;
-  const commandRuns = nextManifest.commandRuns ?? [];
-  const hasPermissionPending = commandLogContent.includes('permission.pending');
-  const hasPermissionDecision =
-    commandLogContent.includes('permission.confirm') ||
-    commandLogContent.includes('permission.cancel');
-  const hasAutoPermissionLog = commandLogContent.includes('command.auto');
-  const hasCommandLog =
-    (hasPermissionPending && hasPermissionDecision) || hasAutoPermissionLog;
-  const preview = nextManifest.preview;
-  const previewSummary =
-    preview?.status === 'running' && preview.url
-      ? preview.url
-      : preview
-        ? previewStatusLabels[preview.status]
-        : '未启动';
-
-  return [
-    'Agent v1 审计:',
-    `目录:${nextProjectPath}`,
-    `- 用户面:通过 · 普通窗口只保留聊天、上传和确认卡片;开发面板只在 dev 模式出现`,
-    `- 能力/命令契约:通过 · ${GAME_CREATION_AGENT_CAPABILITIES.length} 项能力,${GAME_CREATION_APP_COMMANDS.length} 个内置命令`,
-    `- 6 组任务配置:${formatAuditStatus(configuredGroups.length === groupIds.length)} · ${configuredGroups
-      .map((group) => taskGroupLabels[group])
-      .join('、')};角色任务 ${tasks.length} 个`,
-    `- 6 组协作证据:${formatAuditStatus(
-      collaborationGroups.length === groupIds.length,
-      '通过',
-      trace ? '缺组' : '待生成',
-    )} · ${
-      collaborationGroups.length > 0
-        ? collaborationGroups.map((group) => taskGroupLabels[group]).join('、')
-        : '还没有 Agent run trace'
-    }`,
-    `- 任务拆分/编排:${formatAuditStatus(tasks.length > 0)} · 已完成 ${completedCount}/${tasks.length};下一步 ${readySummary}`,
-    summarizeAuditLoopTrace(trace),
-    `- Planner/Orchestrator/Generator/Evaluator:${formatAuditStatus(
-      hasCoreLoop,
-      '通过',
-      trace ? '缺步骤' : '待生成',
-    )}`,
-    `- 返工路由/Carry-over:${formatAuditStatus(
-      Boolean(
-        trace &&
-          (trace.taskGraph.repairRoutes.length > 0 ||
-            trace.taskGraph.carriedTaskIds.length > 0 ||
-            trace.passPlans.some((plan) => plan.mode === 'repair')),
-      ),
-      '通过',
-      trace ? '待触发' : '待生成',
-    )}`,
-    `- 记忆:${formatAuditStatus(
-      hasShortMemory &&
-        hasLongMemory &&
-        hasBlackboardMemory &&
-        hasAgentPrivateMemory,
-    )} · session ${hasShortMemory ? '有' : '无'},project ${
-      hasLongMemory ? '有' : '无'
-    },blackboard ${hasBlackboardMemory ? '有' : '无'},agent ${
-      hasAgentPrivateMemory ? '有' : '无'
-    }`,
-    `- 本地产物:${formatAuditStatus(finalArtifacts.length > 0)} · ${
-      finalArtifacts.join(',') || '无'
-    }`,
-    `- 本地 HTTP 预览:${formatAuditStatus(
-      preview?.status === 'running',
-      '通过',
-      '待启动',
-    )} · ${previewSummary}`,
-    `- 画板回流:${formatAuditStatus(
-      hasCanvasAsset,
-      '通过',
-      suggestedCanvasSync ? '待同步' : '待接入资产',
-    )} · ${
-      hasCanvasAsset
-        ? `${nextManifest.assets.filter((asset) => asset.source.kind === 'canvas').length} 个 canvas 资产`
-        : suggestedCanvasSync
-          ? '建议 /sync-canvas-project <画板项目ID>'
-          : '暂无 canvas 来源资产'
-    }`,
-    `- 权限 Gate/命令日志:${formatAuditStatus(hasCommandLog)} · ${
-      hasPermissionPending && hasPermissionDecision
-        ? '.agent/logs/command.log 含 pending/decision'
-        : hasAutoPermissionLog
-          ? '.agent/logs/command.log 含 auto 权限记录'
-          : commandRuns.length > 0 ||
-              hasProjectFile(files, '.agent/logs/command.log')
-            ? '缺 permission.pending 或确认/取消记录'
-            : '无命令记录'
-    }`,
-    '完整 trace:/trace',
-  ].join('\n');
-}
-
-export function isMissingAgentRunTraceError(message: string) {
-  return (
-    message.includes('.agent/run.latest.json') &&
-    (message.includes('No such file') ||
-      message.includes('os error 2') ||
-      message.includes('找不到'))
-  );
-}
-
-export function formatAgentRunControlError(action: string, message: string) {
-  if (!isMissingAgentRunTraceError(message)) {
-    return message;
-  }
-  return action === 'status'
-    ? '暂无最近 Agent run。先生成一次游戏草案后再查看状态。'
-    : '暂无可控制的 Agent run。先生成一次游戏草案后再操作。';
-}
-
 export function formatCodexRuntimeCapabilities(
   status: Pick<
     GameCreatorLlmConfigStatus,
@@ -681,34 +349,6 @@ export function formatCodexRuntimeCapabilities(
   ].join(',');
 }
 
-export function llmStatusForAgentCard(
-  status: GameCreatorLlmConfigStatus | null,
-  agent: Pick,
-) {
-  return (
-    status?.agents?.find(
-      (candidate) =>
-        candidate.agentId === agent.taskId || candidate.agentId === agent.id,
-    ) ?? null
-  );
-}
-
-export function formatAgentCardLlmStatus(
-  status: GameCreatorLlmConfigStatus | null,
-  agent: AgentStatusCard,
-) {
-  const agentStatus = llmStatusForAgentCard(status, agent);
-  if (!agentStatus) {
-    return null;
-  }
-  return [
-    `智能服务:${agentStatus.configured ? '已连接' : '未就绪'}`,
-    `流式${agentStatus.stream ? '开' : '关'}`,
-    `联网检索${agentStatus.webSearchEnabled ? '开' : '关'}`,
-    `账号状态${visibleLlmCredentialState(agentStatus.accountCredentialState)}`,
-  ].join(' · ');
-}
-
 export function formatAgentCardRuntimeStatus(agent: AgentStatusCard) {
   if (!agent.runtimeStatus) {
     return null;
@@ -744,296 +384,3 @@ export function formatAgentCardRuntimeStatus(agent: AgentStatusCard) {
     .filter(Boolean)
     .join(' · ');
 }
-
-export function formatAgentPolicySummary(policy: ProjectPermissionPolicy) {
-  const entries = Object.entries(policy.agentPolicies ?? {});
-  if (entries.length === 0) {
-    return '无 Agent 独立策略';
-  }
-  return entries
-    .slice(0, 4)
-    .map(
-      ([agentId, agentPolicy]) =>
-        `${agentId} 拒绝:${formatProjectPolicyCommandList(
-          agentPolicy.deniedCommands,
-        )} 确认:${formatProjectPolicyCommandList(agentPolicy.confirmCommands)}`,
-    )
-    .join(';');
-}
-
-function visibleLlmCredentialState(state: string | undefined) {
-  switch (state) {
-    case 'ready':
-    case 'available':
-      return '已就绪';
-    case 'login_required':
-      return '需要登录';
-    case 'permission_denied':
-      return '权限不足';
-    case 'revoked':
-      return '需要重新授权';
-    default:
-      return '暂不可用';
-  }
-}
-
-export const agentRoleMemoryFileNames: Record = {
-  Director: 'director.md',
-  Gameplay: 'gameplay.md',
-  Difficulty: 'difficulty.md',
-  Asset: 'asset.md',
-  Polish: 'polish.md',
-  SFX: 'sfx.md',
-  Code: 'code.md',
-  Review: 'review.md',
-  Preview: 'preview.md',
-  Playtest: 'playtest.md',
-  Publish: 'publish.md',
-};
-
-export function agentMemoryReadDraftFromTask(task: GameCreationAppTaskState) {
-  const fileName = agentRoleMemoryFileNames[task.role];
-  if (!fileName) {
-    return null;
-  }
-  return {
-    task,
-    path: `memory/agents/${task.group}/${fileName}`,
-  };
-}
-
-export function agentMemoryReadDraftsFromManifest(
-  nextManifest: GameCreationAppManifest,
-) {
-  return taskRowsFromManifest(nextManifest)
-    .map(agentMemoryReadDraftFromTask)
-    .filter((draft): draft is NonNullable => draft !== null);
-}
-
-export function agentConversationReadDraftFromTask(
-  task: GameCreationAppTaskState,
-) {
-  return {
-    task,
-    path: `.agent/conversations/agents/${agentConversationId(task)}.jsonl`,
-  };
-}
-
-export function agentConversationReadDraftsFromManifest(
-  nextManifest: GameCreationAppManifest,
-) {
-  return taskRowsFromManifest(nextManifest).map(
-    agentConversationReadDraftFromTask,
-  );
-}
-
-export function formatTraceTaskWaves(
-  waves: string[][],
-  tasks: GameCreationAppTaskState[],
-) {
-  return (
-    waves
-      .map((wave) =>
-        wave.map((taskId) => formatTraceTaskId(taskId, tasks)).join(' + '),
-      )
-      .join(' / ') || 'none'
-  );
-}
-
-export function summarizeSuggestedToolCalls(trace: GameCreationAgentRunTrace) {
-  const suggestedToolCalls = trace.steps
-    .flatMap((step) => step.toolCalls)
-    .filter(
-      (toolCall) =>
-        toolCall.status === 'suggested' ||
-        toolCall.toolId.startsWith('agent.tool.suggest.'),
-    );
-  const lines = suggestedToolCalls
-    .slice(0, 5)
-    .map((toolCall) => `- ${toolCall.toolId}: ${toolCall.summary}`);
-  if (suggestedToolCalls.length > lines.length) {
-    lines.push(`- 还有 ${suggestedToolCalls.length - lines.length} 个建议命令`);
-  }
-  return lines.join('\n');
-}
-
-export function commandDraftFromSuggestedToolCall(
-  toolCall: GameCreationAgentToolCallTrace,
-) {
-  if (
-    toolCall.toolId.includes('canvas.project_sync') &&
-    (toolCall.status === 'suggested' ||
-      toolCall.toolId.startsWith('agent.tool.suggest.'))
-  ) {
-    return '/sync-canvas-project ';
-  }
-  return null;
-}
-
-export function commandDraftFromAgentRunTrace(
-  trace: GameCreationAgentRunTrace,
-) {
-  return (
-    trace.steps
-      .flatMap((step) => step.toolCalls)
-      .map(commandDraftFromSuggestedToolCall)
-      .find((commandDraft) => commandDraft !== null) ?? null
-  );
-}
-
-export function readablePassArtifactsFromAgentRunTrace(
-  trace: GameCreationAgentRunTrace,
-) {
-  return trace.artifacts.filter(
-    (artifact) =>
-      artifact.path.startsWith('.agent/passes/') &&
-      isSafeProjectRelativePath(artifact.path),
-  );
-}
-
-export function summarizeLlmConversation(trace: GameCreationAgentRunTrace) {
-  const llmSteps = trace.steps.filter((step) =>
-    step.toolCalls.some((toolCall) => toolCall.toolId.startsWith('llm.')),
-  );
-  const visibleLlmSteps = llmSteps.slice(-6);
-  const lines = visibleLlmSteps.map((step) => {
-    const toolIds = step.toolCalls
-      .filter((toolCall) => toolCall.toolId.startsWith('llm.'))
-      .map((toolCall) => toolCall.toolId)
-      .join(', ');
-    return `- ${step.agent} #${step.pass} · ${step.status} · ${step.phase} · ${toolIds}`;
-  });
-  if (llmSteps.length > visibleLlmSteps.length) {
-    lines.push(
-      `- 还有 ${llmSteps.length - visibleLlmSteps.length} 个较早 LLM 步骤`,
-    );
-  }
-  return lines.join('\n');
-}
-
-export function summarizeAgentRunTrace(trace: GameCreationAgentRunTrace) {
-  const llmConversation = summarizeLlmConversation(trace);
-  const visibleRecentSteps = trace.steps.slice(-5);
-  const recentStepLines = visibleRecentSteps.map(
-    (step) => `- ${step.agent} #${step.pass} · ${step.status} · ${step.phase}`,
-  );
-  if (trace.steps.length > visibleRecentSteps.length) {
-    recentStepLines.push(
-      `- 还有 ${trace.steps.length - visibleRecentSteps.length} 个较早步骤`,
-    );
-  }
-  const recentSteps = recentStepLines.join('\n');
-  const taskCounts = trace.taskGraph.tasks.reduce<
-    Record
-  >(
-    (current, task) => {
-      current[task.status] += 1;
-      return current;
-    },
-    {
-      pending: 0,
-      running: 0,
-      'waiting-for-confirmation': 0,
-      completed: 0,
-      failed: 0,
-    },
-  );
-  const taskSummary = (
-    [
-      'completed',
-      'waiting-for-confirmation',
-      'running',
-      'pending',
-      'failed',
-    ] as const
-  )
-    .filter((status) => taskCounts[status] > 0)
-    .map((status) => `${taskStatusLabels[status]} ${taskCounts[status]}`)
-    .join(',');
-  const repair = formatTraceRepairRoutes(
-    trace.taskGraph.repairRoutes,
-    trace.taskGraph.tasks,
-  );
-  const suggestedTools = summarizeSuggestedToolCalls(trace);
-  const allPassPlans = trace.passPlans ?? [];
-  const visiblePassPlans = allPassPlans.slice(-3);
-  const passPlanLines = visiblePassPlans.map((plan) => {
-    const waves = formatTraceTaskWaves(
-      plan.dependencyWaves,
-      trace.taskGraph.tasks,
-    );
-    return [
-      `- pass ${plan.pass} · ${plan.mode}`,
-      `active ${formatTraceTaskIds(plan.activeTaskIds, trace.taskGraph.tasks)}`,
-      `carry ${formatTraceTaskIds(plan.carriedTaskIds, trace.taskGraph.tasks)}`,
-      `waves ${waves}`,
-      plan.repairFocus.length > 0
-        ? `repair ${plan.repairFocus.join(';')}`
-        : null,
-      plan.repairRoutes.length > 0
-        ? `routes ${formatTraceRepairRoutes(plan.repairRoutes, trace.taskGraph.tasks)}`
-        : null,
-    ]
-      .filter(Boolean)
-      .join(' · ');
-  });
-  if (allPassPlans.length > visiblePassPlans.length) {
-    passPlanLines.push(
-      `- 还有 ${allPassPlans.length - visiblePassPlans.length} 个较早轮次`,
-    );
-  }
-  const passPlans = passPlanLines.join('\n');
-  const visibleArtifacts = trace.artifacts.slice(-5);
-  const artifactLines = visibleArtifacts.map(
-    (artifact) => `- ${artifact.path} · ${artifact.checksum}`,
-  );
-  if (trace.artifacts.length > visibleArtifacts.length) {
-    artifactLines.push(
-      `- 还有 ${trace.artifacts.length - visibleArtifacts.length} 个较早产物`,
-    );
-  }
-  const artifacts = artifactLines.join('\n');
-
-  return [
-    `Run:${trace.runId}`,
-    `状态:${formatAgentRunStatus(trace)}`,
-    `工具调用:${trace.toolCallCount}/${trace.maxToolCalls}`,
-    `下一步:${trace.nextStep}`,
-    llmConversation ? `LLM 对话:\n${llmConversation}` : null,
-    taskSummary ? `任务:${taskSummary}` : null,
-    `active 任务:${formatTraceTaskIds(
-      trace.taskGraph.activeTaskIds,
-      trace.taskGraph.tasks,
-    )}`,
-    `carry-over 任务:${formatTraceTaskIds(
-      trace.taskGraph.carriedTaskIds,
-      trace.taskGraph.tasks,
-    )}`,
-    trace.taskGraph.repairFocus.length > 0
-      ? `返工焦点:${trace.taskGraph.repairFocus.join(';')}`
-      : null,
-    repair ? `返工路线:${repair}` : null,
-    suggestedTools ? `建议命令:\n${suggestedTools}` : null,
-    passPlans ? `编排轮次:\n${passPlans}` : null,
-    artifacts ? `产物快照:\n${artifacts}` : null,
-    recentSteps ? `最近步骤:\n${recentSteps}` : null,
-  ]
-    .filter(Boolean)
-    .join('\n');
-}
-
-export function summarizeAgentRunCompletionForChat(
-  trace: GameCreationAgentRunTrace,
-) {
-  const suggestedCommand = commandDraftFromAgentRunTrace(trace);
-  const readableArtifactPath = readableArtifactPathFromAgentRunTrace(trace);
-  const artifactReadCommand = readableArtifactPath
-    ? `/read ${readableArtifactPath}`
-    : undefined;
-  return {
-    text: `${summarizeAgentRunTrace(trace)}\n完整 trace:/trace`,
-    draftCommand: suggestedCommand ?? artifactReadCommand,
-    draftCommandLabel:
-      suggestedCommand || !artifactReadCommand ? undefined : '读取首个产物',
-  };
-}
diff --git a/apps/ai-game-creator-shell/src/features/project-summary/agentRunSummaries.ts b/apps/ai-game-creator-shell/src/features/project-summary/agentRunSummaries.ts
deleted file mode 100644
index 9d037d9d8..000000000
--- a/apps/ai-game-creator-shell/src/features/project-summary/agentRunSummaries.ts
+++ /dev/null
@@ -1,311 +0,0 @@
-import {
-  type GameCreationAgentRunTrace,
-  type GameCreationAppManifest,
-  type GameCreationAppTaskState,
-  selectGameCreationAppReadyTasks,
-} from '../../../../../packages/shared/src/contracts/gameCreationApp';
-import {
-  type AgentRunHistoryItem,
-  type AgentStatusCard,
-} from '../../app/types';
-import { taskRowsFromManifest } from '../agent-runtime';
-import {
-  formatAgentRunStatus,
-  formatTraceRepairRoutes,
-  isAgentReviewStep,
-  isAgentRunTracePassed,
-} from './agentTrace';
-import { isSafeProjectRelativePath } from './projectPath';
-import { previewStatusLabels } from './projectSummaryConstants';
-
-export function summarizeAgentRunBudget(
-  trace: GameCreationAgentRunTrace | null,
-) {
-  if (!trace) {
-    return {
-      text: '运行预算:\n- 最近 Run:暂无\n- 建议:/next',
-      draftCommand: '/next',
-      draftCommandLabel: '查看下一步',
-    };
-  }
-
-  const remainingPasses = Math.max(trace.maxPasses - trace.passes, 0);
-  const remainingToolCalls = Math.max(
-    trace.maxToolCalls - trace.toolCallCount,
-    0,
-  );
-  const blocked =
-    trace.lifecycleStatus === 'killed' ||
-    trace.status === 'failed' ||
-    trace.status === 'needs-revision' ||
-    trace.stopReason === 'max-passes-exhausted' ||
-    remainingPasses === 0 ||
-    remainingToolCalls === 0;
-  const draftCommand = blocked
-    ? '/review'
-    : isAgentRunTracePassed(trace)
-      ? '/publish'
-      : '/trace';
-
-  return {
-    text: [
-      '运行预算:',
-      `- Run:${trace.runId} · ${formatAgentRunStatus(trace)}`,
-      `- 轮次:已用 ${trace.passes}/${trace.maxPasses} · 剩余 ${remainingPasses}`,
-      `- 工具调用:已用 ${trace.toolCallCount}/${trace.maxToolCalls} · 剩余 ${remainingToolCalls}`,
-      `- 下一步:${trace.nextStep}`,
-      `- 建议:${draftCommand}`,
-    ].join('\n'),
-    draftCommand,
-    draftCommandLabel:
-      draftCommand === '/review'
-        ? '查看评审'
-        : draftCommand === '/publish'
-          ? '查看发布准备'
-          : '查看 trace',
-  };
-}
-
-export function summarizeAgentReviewState(
-  trace: GameCreationAgentRunTrace | null,
-) {
-  if (!trace) {
-    return {
-      text: '评审状态:暂无最近 trace',
-      draftCommand: '/next',
-      draftCommandLabel: '查看下一步',
-    };
-  }
-
-  const reviewSteps = trace.steps.filter(isAgentReviewStep);
-  const visibleReviewSteps = reviewSteps.slice(-3);
-  const reviewStepLines = visibleReviewSteps.map((step) => {
-    const outputPaths = step.outputPaths.filter(isSafeProjectRelativePath);
-    return [
-      `- ${step.agent} #${step.pass} · ${step.status} · ${step.summary}`,
-      outputPaths.length > 0 ? `输出 ${outputPaths.join(', ')}` : null,
-    ]
-      .filter(Boolean)
-      .join(' · ');
-  });
-  if (reviewSteps.length > visibleReviewSteps.length) {
-    reviewStepLines.push(
-      `- 还有 ${reviewSteps.length - visibleReviewSteps.length} 个较早评审步骤`,
-    );
-  }
-  const repair = formatTraceRepairRoutes(
-    trace.taskGraph.repairRoutes,
-    trace.taskGraph.tasks,
-  );
-  const needsResume =
-    trace.lifecycleStatus === 'killed' ||
-    trace.status === 'failed' ||
-    trace.status === 'needs-revision' ||
-    trace.stopReason === 'max-passes-exhausted';
-  const evaluatorState = isAgentRunTracePassed(trace)
-    ? '通过'
-    : needsResume
-      ? '需返工'
-      : '未通过';
-
-  return {
-    text: [
-      '评审状态:',
-      `- Run:${trace.runId} · ${formatAgentRunStatus(trace)}`,
-      `- Evaluator:${evaluatorState}`,
-      `- 返工焦点:${
-        trace.taskGraph.repairFocus.length > 0
-          ? trace.taskGraph.repairFocus.join(';')
-          : '暂无'
-      }`,
-      `- 返工路线:${repair || '暂无'}`,
-      `- 下一步:${trace.nextStep}`,
-      '- 评审记录:/read .agent/findings.md',
-      reviewStepLines.length > 0
-        ? `- 最近评审步骤:\n${reviewStepLines.join('\n')}`
-        : '- 最近评审步骤:暂无',
-    ].join('\n'),
-    draftCommand: needsResume ? '/agent-resume ' : '/read .agent/findings.md',
-    draftCommandLabel: needsResume ? '继续修复' : '读取评审记录',
-  };
-}
-
-export function summarizeProjectContextSources(
-  nextManifest: GameCreationAppManifest,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  const tasks = taskRowsFromManifest(nextManifest);
-  const llmInputPaths = trace
-    ? Array.from(
-        new Set(
-          trace.steps
-            .filter((step) =>
-              step.toolCalls.some((toolCall) =>
-                toolCall.toolId.startsWith('llm.'),
-              ),
-            )
-            .flatMap((step) => step.inputPaths)
-            .filter(isSafeProjectRelativePath),
-        ),
-      ).slice(0, 8)
-    : [];
-  const inputPathLines = llmInputPaths.map(
-    (path) => `- ${path}:/read ${path}`,
-  );
-  const draftCommand =
-    llmInputPaths.length > 0
-      ? `/read ${llmInputPaths[0]}`
-      : '/memory blackboard';
-
-  return {
-    text: [
-      '上下文来源:',
-      '- 项目对话:/history',
-      '- 短期记忆:/memory short',
-      '- 长期记忆:/memory long',
-      '- 项目黑板:/memory blackboard',
-      `- Agent 对话:${tasks.length} 个 · /agent-conversations`,
-      `- Agent 私有记忆:${tasks.length} 个 · /agent-memories`,
-      '- 项目 manifest:/read .agent/manifest.json',
-      trace ? `- 最近 Run:${trace.runId} · /trace` : '- 最近 Run:暂无',
-      inputPathLines.length > 0
-        ? `最近 LLM 输入:\n${inputPathLines.join('\n')}`
-        : '最近 LLM 输入:暂无',
-    ].join('\n'),
-    draftCommand,
-    draftCommandLabel: llmInputPaths.length > 0 ? '读取首个上下文' : '查看黑板',
-  };
-}
-
-export function summarizeProjectTimeline(
-  nextManifest: GameCreationAppManifest,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  const commandRuns = (nextManifest.commandRuns ?? []).slice(-5);
-  const commandLines = commandRuns.map((commandRun) => {
-    const logSuffix = isSafeProjectRelativePath(commandRun.logPath)
-      ? ` · 日志 /read ${commandRun.logPath}`
-      : '';
-    return `- 命令 ${commandRun.commandId} · ${
-      commandRun.status === 'completed' ? '完成' : '失败'
-    }${logSuffix}`;
-  });
-  const visibleSteps = trace?.steps.slice(-6) ?? [];
-  const stepLines = visibleSteps.map((step) => {
-    const outputPaths = step.outputPaths
-      .filter(isSafeProjectRelativePath)
-      .slice(0, 3);
-    return [
-      `- ${step.agent} #${step.pass} / ${step.phase} · ${step.status} · ${step.summary}`,
-      outputPaths.length > 0 ? `输出 ${outputPaths.join(', ')}` : null,
-    ]
-      .filter(Boolean)
-      .join(' · ');
-  });
-  const latestSafeLogPath = [...commandRuns]
-    .reverse()
-    .find((commandRun) =>
-      isSafeProjectRelativePath(commandRun.logPath),
-    )?.logPath;
-  const draftCommand = latestSafeLogPath
-    ? `/read ${latestSafeLogPath}`
-    : trace
-      ? '/trace'
-      : '/history';
-
-  return {
-    text: [
-      '项目时间线:',
-      `- Run:${trace ? `${trace.runId} · ${formatAgentRunStatus(trace)}` : '暂无最近 run'}`,
-      commandLines.length > 0
-        ? `- 最近命令:\n${commandLines.join('\n')}`
-        : '- 最近命令:暂无',
-      stepLines.length > 0
-        ? `- 最近步骤:\n${stepLines.join('\n')}`
-        : '- 最近步骤:暂无',
-    ].join('\n'),
-    draftCommand,
-    draftCommandLabel: latestSafeLogPath
-      ? '读取最近日志'
-      : trace
-        ? '查看 trace'
-        : '查看历史',
-  };
-}
-
-export function summarizeProjectHandoff(
-  nextManifest: GameCreationAppManifest,
-  nextProjectPath: string,
-  trace: GameCreationAgentRunTrace | null,
-  history: AgentRunHistoryItem[],
-  agents: AgentStatusCard[],
-) {
-  const tasks = taskRowsFromManifest(nextManifest);
-  const completedCount = tasks.filter(
-    (task) => task.status === 'completed',
-  ).length;
-  const manifestTasksById = new Map(tasks.map((task) => [task.id, task]));
-  const traceTasksById = new Map(
-    trace?.taskGraph.tasks.map((task) => [task.id, task]) ?? [],
-  );
-  const traceReadyTasks =
-    trace?.taskGraph.readyTaskIds
-      .map(
-        (taskId) => traceTasksById.get(taskId) ?? manifestTasksById.get(taskId),
-      )
-      .filter((task): task is GameCreationAppTaskState => Boolean(task)) ?? [];
-  const readyTasks =
-    traceReadyTasks.length > 0
-      ? traceReadyTasks
-      : selectGameCreationAppReadyTasks({ tasks });
-  const failedTasks = tasks.filter((task) => task.status === 'failed');
-  const sourceCounts = nextManifest.assets.reduce(
-    (counts, asset) => {
-      counts[asset.source.kind] += 1;
-      return counts;
-    },
-    { uploaded: 0, generated: 0, canvas: 0 },
-  );
-  const preview = nextManifest.preview;
-  const previewSummary =
-    preview?.status === 'running' && preview.url
-      ? `运行中 ${preview.url}`
-      : preview
-        ? previewStatusLabels[preview.status]
-        : '未启动';
-  const commandRuns = nextManifest.commandRuns ?? [];
-  const latestCommandRun = commandRuns[commandRuns.length - 1];
-  const evidenceAgentCount = agents.filter(
-    (agent) => agent.hasRecentEvidence,
-  ).length;
-  const activeAgentCount = agents.filter(
-    (agent) => agent.taskGraphState === 'active',
-  ).length;
-  const runSummary = trace
-    ? `${trace.runId} · ${formatAgentRunStatus(trace)} · next ${trace.nextStep}`
-    : history[0]
-      ? `无 latest,最近历史 ${history[0].trace.runId} · ${formatAgentRunStatus(history[0].trace)}`
-      : '暂无 run';
-  const readySummary =
-    readyTasks.length > 0
-      ? readyTasks
-          .slice(0, 3)
-          .map((task) => `${task.group}/${task.role} ${task.title}`)
-          .join(';')
-      : '暂无';
-  const failedSummary =
-    failedTasks.length > 0
-      ? failedTasks
-          .slice(0, 3)
-          .map((task) => `${task.group}/${task.role} ${task.title}`)
-          .join(';')
-      : '暂无';
-
-  return `项目交接:\n- 项目:${nextManifest.name}\n- 目录:${nextProjectPath}\n- Run:${runSummary}\n- 任务:完成 ${completedCount}/${tasks.length} · ready ${readyTasks.length} · 失败 ${failedTasks.length}\n- Ready:${readySummary}\n- 失败项:${failedSummary}\n- 资产:${nextManifest.assets.length} 个 · 上传 ${sourceCounts.uploaded} / 生成 ${sourceCounts.generated} / 画板 ${sourceCounts.canvas}\n- 预览:${previewSummary}\n- Agent:${evidenceAgentCount}/${agents.length} 有运行证据 · active ${activeAgentCount}\n- 历史:已加载 ${history.length} 个 run\n- 最近命令:${
-    latestCommandRun
-      ? `${latestCommandRun.commandId} · ${
-          latestCommandRun.status === 'completed' ? '完成' : '失败'
-        }`
-      : '暂无'
-  }`;
-}
diff --git a/apps/ai-game-creator-shell/src/features/project-summary/agentTrace.ts b/apps/ai-game-creator-shell/src/features/project-summary/agentTrace.ts
deleted file mode 100644
index 2a619e13a..000000000
--- a/apps/ai-game-creator-shell/src/features/project-summary/agentTrace.ts
+++ /dev/null
@@ -1,96 +0,0 @@
-import {
-  type GameCreationAgentRepairRouteTrace,
-  type GameCreationAgentRunStep,
-  type GameCreationAgentRunTrace,
-  type GameCreationAppTaskState,
-} from '../../../../../packages/shared/src/contracts/gameCreationApp';
-import { isSafeProjectRelativePath } from './projectPath';
-import { taskGroupLabels } from './projectSummaryConstants';
-
-export function isAgentRunTracePassed(trace: GameCreationAgentRunTrace | null) {
-  return (
-    trace?.status === 'passed' ||
-    trace?.status === 'artifacts-written' ||
-    trace?.stopReason === 'evaluator-passed'
-  );
-}
-
-export function isAgentReviewStep(step: GameCreationAgentRunStep) {
-  return (
-    step.agent.toLowerCase().includes('evaluator') ||
-    step.phase === 'evaluation' ||
-    step.phase === 'evaluate' ||
-    step.taskId === 'quality-review'
-  );
-}
-
-export function isPlaytestTraceStep(step: GameCreationAgentRunStep) {
-  return (
-    step.agent.toLowerCase().includes('playtest') ||
-    step.phase === 'playtest' ||
-    step.taskId === 'preview-playtest' ||
-    step.toolCalls.some(
-      (toolCall) =>
-        toolCall.toolId === 'game.static_smoke' ||
-        toolCall.toolId.startsWith('preview.'),
-    )
-  );
-}
-
-export function formatAgentRunStatus(trace: GameCreationAgentRunTrace) {
-  const status = trace.lifecycleStatus
-    ? `${trace.status} / ${trace.lifecycleStatus}`
-    : trace.status;
-  return `${status} · ${trace.passes}/${trace.maxPasses} 轮 · ${trace.stopReason}`;
-}
-
-export function formatTraceTaskId(
-  taskId: string,
-  tasks: GameCreationAppTaskState[],
-) {
-  const task = tasks.find((candidate) => candidate.id === taskId);
-  if (!task) {
-    return taskId;
-  }
-  return `${taskGroupLabels[task.group]} / ${task.role} ${task.title}(${task.id})`;
-}
-
-export function formatTraceTaskIds(
-  taskIds: string[],
-  tasks: GameCreationAppTaskState[],
-) {
-  return taskIds.length > 0
-    ? taskIds.map((taskId) => formatTraceTaskId(taskId, tasks)).join(', ')
-    : 'none';
-}
-
-export function formatTraceRepairRoutes(
-  routes: GameCreationAgentRepairRouteTrace[],
-  tasks: GameCreationAppTaskState[],
-) {
-  const visibleRoutes = routes
-    .slice(0, 3)
-    .map(
-      (route) => `${route.reason}: ${formatTraceTaskIds(route.taskIds, tasks)}`,
-    );
-  if (routes.length > visibleRoutes.length) {
-    visibleRoutes.push(`还有 ${routes.length - visibleRoutes.length} 条路线`);
-  }
-  return visibleRoutes.join(';');
-}
-
-export function readableArtifactPathFromAgentRunTrace(
-  trace: GameCreationAgentRunTrace,
-) {
-  return trace.artifacts.find((artifact) =>
-    isSafeProjectRelativePath(artifact.path),
-  )?.path;
-}
-
-export function readableArtifactsFromAgentRunTrace(
-  trace: GameCreationAgentRunTrace,
-) {
-  return trace.artifacts.filter((artifact) =>
-    isSafeProjectRelativePath(artifact.path),
-  );
-}
diff --git a/apps/ai-game-creator-shell/src/features/project-summary/chatCommandMetadata.ts b/apps/ai-game-creator-shell/src/features/project-summary/chatCommandMetadata.ts
deleted file mode 100644
index df190e75e..000000000
--- a/apps/ai-game-creator-shell/src/features/project-summary/chatCommandMetadata.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-export function missingChatCommandArgumentMessage(prompt: string) {
-  switch (prompt) {
-    case '/project':
-      return '格式:/project /绝对路径';
-    case '/generate':
-    case '/draft':
-      return '格式:/generate 创作想法';
-    case '/diff':
-      return '格式:/diff checkpoint-id';
-    case '/restore':
-      return '格式:/restore checkpoint-id';
-    case '/policy-deny':
-      return '格式:/policy-deny file.write';
-    case '/policy-allow':
-      return '格式:/policy-allow file.write';
-    case '/policy-confirm':
-      return '格式:/policy-confirm project.index';
-    case '/policy-auto':
-      return '格式:/policy-auto project.index';
-    case '/agent-policy-deny':
-      return '格式:/agent-policy-deny design-director file.read';
-    case '/agent-policy-allow':
-      return '格式:/agent-policy-allow design-director file.read';
-    case '/agent-policy-confirm':
-      return '格式:/agent-policy-confirm design-director memory.write';
-    case '/agent-policy-auto':
-      return '格式:/agent-policy-auto design-director memory.write';
-    case '/read':
-      return '格式:/read game/index.html';
-    case '/asset-register':
-      return '格式:/asset-register assets/hero.png [kind] [mediaType]';
-    case '/remember':
-      return '请提供要追加的记忆内容。';
-    case '/memory-set':
-      return '请提供要保存的记忆内容。';
-    case '/canvas':
-    case '/sync-canvas-project':
-      return '请提供画板项目 ID。';
-    case '/generate-art':
-      return '请提供美术生成提示词。';
-    case '/import-canvas-asset':
-      return '格式:/import-canvas-asset assets/hero.png 画板项目ID 资源ID|object:资产对象ID';
-    case '/import-canvas-export':
-      return '格式:/import-canvas-export /绝对/画板素材.zip 画板项目ID';
-    default:
-      return null;
-  }
-}
diff --git a/apps/ai-game-creator-shell/src/features/project-summary/projectArtifactSummaries.ts b/apps/ai-game-creator-shell/src/features/project-summary/projectArtifactSummaries.ts
deleted file mode 100644
index ce4906830..000000000
--- a/apps/ai-game-creator-shell/src/features/project-summary/projectArtifactSummaries.ts
+++ /dev/null
@@ -1,368 +0,0 @@
-import {
-  type LocalProjectCheckpointResult,
-  type LocalProjectCheckpointSummary,
-  type LocalProjectDiffResult,
-  type LocalProjectExportPackageResult,
-  type LocalProjectExportPackagesResult,
-  type LocalProjectFileEntry,
-  type LocalProjectFileResult,
-  type LocalProjectIndexResult,
-  type ProjectAssetDraft,
-  type ProjectFileActionDraft,
-  type ProjectPermissionPolicyView,
-} from '../../app/types';
-import {
-  commonAgentRunSupportReadDrafts,
-  commonProjectArtifactReadDrafts,
-  commonProjectInternalReadDrafts,
-  commonProjectLogReadDrafts,
-} from './projectSummaryConstants';
-
-export function summarizeProjectFiles(files: LocalProjectFileEntry[]) {
-  if (files.length === 0) {
-    return '本地项目还没有文件。';
-  }
-
-  const visibleFiles = files.slice(0, 40);
-  const lines = visibleFiles.map((file) =>
-    file.kind === 'directory' ? `- ${file.path}/` : `- ${file.path}`,
-  );
-  if (files.length > visibleFiles.length) {
-    lines.push(`- 还有 ${files.length - visibleFiles.length} 项`);
-  }
-  return `本地项目文件:\n${lines.join('\n')}`;
-}
-
-export function summarizeProjectIndex(result: LocalProjectIndexResult) {
-  const visibleFiles = result.files.slice(0, 12);
-  const lines = visibleFiles.map((file) => `- ${file.path} · ${file.size}B`);
-  if (result.files.length > visibleFiles.length) {
-    lines.push(`- 还有 ${result.files.length - visibleFiles.length} 项`);
-  }
-  return [
-    `索引:${result.fileCount} 个文件,${result.totalBytes}B`,
-    `路径:${result.indexPath}`,
-    lines.join('\n'),
-  ]
-    .filter(Boolean)
-    .join('\n');
-}
-
-export function summarizeProjectCheckpoint(
-  result: LocalProjectCheckpointResult,
-) {
-  return [
-    `已保存 checkpoint:${result.checkpointId}`,
-    `文件:${result.fileCount} 个,${result.totalBytes}B`,
-    `路径:${result.checkpointPath}`,
-  ].join('\n');
-}
-
-export function summarizeProjectExportPackage(
-  result: LocalProjectExportPackageResult,
-) {
-  return [
-    `已导出本地试玩包:${result.packageRelativePath}`,
-    `文件:${result.fileCount} 个,${result.totalBytes}B`,
-    `路径:${result.packagePath}`,
-  ].join('\n');
-}
-
-export function summarizeProjectExportPackages(
-  result: LocalProjectExportPackagesResult,
-) {
-  if (result.packages.length === 0) {
-    return '本地试玩包:暂无。输入 /export 导出当前可试玩原型。';
-  }
-  const visiblePackages = result.packages.slice(0, 8);
-  const lines = visiblePackages.map(
-    (item) => `- ${item.packageRelativePath} · ${item.totalBytes}B`,
-  );
-  if (result.packages.length > visiblePackages.length) {
-    lines.push(
-      `- 还有 ${result.packages.length - visiblePackages.length} 个更早试玩包`,
-    );
-  }
-  return `本地试玩包:\n${lines.join('\n')}`;
-}
-
-export function checkpointIdFromManifestPath(path: string) {
-  const match = path.match(/^\.agent\/checkpoints\/([^/]+)\/manifest\.json$/);
-  return match?.[1] ?? null;
-}
-
-export function isCheckpointManifestFile(file: LocalProjectFileEntry) {
-  return file.kind === 'file' && checkpointIdFromManifestPath(file.path);
-}
-
-export function sortCheckpointManifestFiles(files: LocalProjectFileEntry[]) {
-  return files.filter(isCheckpointManifestFile).sort((left, right) => {
-    const modifiedDelta = (right.modifiedAt ?? 0) - (left.modifiedAt ?? 0);
-    return modifiedDelta || right.path.localeCompare(left.path);
-  });
-}
-
-export function summarizeProjectCheckpoints(
-  checkpoints: LocalProjectCheckpointSummary[],
-  hiddenCount: number,
-) {
-  if (checkpoints.length === 0) {
-    return '还没有 checkpoint。输入 /checkpoint 保存当前项目快照。';
-  }
-  const lines = checkpoints.map((checkpoint) =>
-    [
-      `- ${checkpoint.checkpointId}`,
-      `${checkpoint.fileCount} 个文件`,
-      `${checkpoint.totalBytes}B`,
-      checkpoint.createdAt ? `createdAt ${checkpoint.createdAt}` : null,
-      `/diff ${checkpoint.checkpointId}`,
-      `/restore ${checkpoint.checkpointId}`,
-    ]
-      .filter(Boolean)
-      .join(' · '),
-  );
-  if (hiddenCount > 0) {
-    lines.push(`- 还有 ${hiddenCount} 个更早 checkpoint`);
-  }
-  return `最近 checkpoint:\n${lines.join('\n')}`;
-}
-
-export function checkpointSummaryFromManifest(
-  file: LocalProjectFileEntry,
-  content: string,
-): LocalProjectCheckpointSummary {
-  const fallbackId = checkpointIdFromManifestPath(file.path) ?? file.path;
-  try {
-    const parsed: unknown = JSON.parse(content);
-    const data =
-      parsed && typeof parsed === 'object'
-        ? (parsed as {
-            checkpointId?: unknown;
-            createdAt?: unknown;
-            files?: unknown;
-          })
-        : {};
-    const files = Array.isArray(data.files) ? data.files : [];
-    const manifestFileCount = (data as { fileCount?: unknown }).fileCount;
-    const fileCount =
-      files.length > 0
-        ? files.length
-        : typeof manifestFileCount === 'number' && manifestFileCount >= 0
-          ? manifestFileCount
-          : 0;
-    const totalBytes = files.reduce((sum, item) => {
-      if (!item || typeof item !== 'object') {
-        return sum;
-      }
-      const size = (item as { size?: unknown }).size;
-      return sum + (typeof size === 'number' && size > 0 ? size : 0);
-    }, 0);
-    const manifestTotalBytes = (data as { totalBytes?: unknown }).totalBytes;
-    const resolvedTotalBytes =
-      totalBytes > 0
-        ? totalBytes
-        : typeof manifestTotalBytes === 'number' && manifestTotalBytes >= 0
-          ? manifestTotalBytes
-          : 0;
-    return {
-      checkpointId:
-        typeof data.checkpointId === 'string' && data.checkpointId.trim()
-          ? data.checkpointId
-          : fallbackId,
-      path: file.path,
-      fileCount,
-      totalBytes: resolvedTotalBytes,
-      createdAt:
-        typeof data.createdAt === 'number' || typeof data.createdAt === 'string'
-          ? String(data.createdAt)
-          : '',
-      modifiedAt: file.modifiedAt,
-    };
-  } catch {
-    return {
-      checkpointId: fallbackId,
-      path: file.path,
-      fileCount: 0,
-      totalBytes: 0,
-      createdAt: '',
-      modifiedAt: file.modifiedAt,
-    };
-  }
-}
-
-export function summarizeProjectDiff(result: LocalProjectDiffResult) {
-  const section = (label: string, files: Array<{ path: string }>) => {
-    if (files.length === 0) {
-      return null;
-    }
-    const visibleFiles = files.slice(0, 20);
-    const lines = visibleFiles.map((file) => `- ${file.path}`);
-    if (files.length > visibleFiles.length) {
-      lines.push(`- 还有 ${files.length - visibleFiles.length} 项`);
-    }
-    return `${label}:\n${lines.join('\n')}`;
-  };
-  return (
-    [
-      `checkpoint:${result.checkpointId}`,
-      section('新增', result.added),
-      section('变更', result.changed),
-      section('删除', result.deleted),
-    ]
-      .filter(Boolean)
-      .join('\n') || '无差异。'
-  );
-}
-
-export function summarizeProjectPolicy(view: ProjectPermissionPolicyView) {
-  const agentPolicies = view.policy.agentPolicies ?? {};
-  const agentPolicyLines = Object.entries(agentPolicies)
-    .slice(0, 8)
-    .map(
-      ([agentId, policy]) =>
-        `Agent ${agentId}:拒绝 ${formatProjectPolicyCommandList(
-          policy.deniedCommands,
-        )};确认 ${formatProjectPolicyCommandList(policy.confirmCommands)}`,
-    );
-  if (Object.keys(agentPolicies).length > agentPolicyLines.length) {
-    agentPolicyLines.push(
-      `Agent 策略还有 ${Object.keys(agentPolicies).length - agentPolicyLines.length} 项`,
-    );
-  }
-  return [
-    `策略:${view.path}`,
-    `拒绝:${formatProjectPolicyCommandList(view.policy.deniedCommands)}`,
-    `确认:${formatProjectPolicyCommandList(view.policy.confirmCommands)}`,
-    ...agentPolicyLines,
-  ].join('\n');
-}
-
-export function formatProjectPolicyCommandList(values: string[]) {
-  if (values.length === 0) {
-    return '无';
-  }
-  const visibleValues = values.slice(0, 12);
-  return [
-    visibleValues.join('、'),
-    values.length > visibleValues.length
-      ? `还有 ${values.length - visibleValues.length} 项`
-      : null,
-  ]
-    .filter(Boolean)
-    .join('、');
-}
-
-export function formatCanvasAssetSource(source: {
-  canvasProjectId: string;
-  canvasAssetId: string;
-  canvasAssetObjectId?: string;
-}) {
-  const assetReference = source.canvasAssetObjectId
-    ? `object:${source.canvasAssetObjectId}`
-    : source.canvasAssetId;
-  return `${source.canvasProjectId} / ${assetReference || '未提供资产 ID'}`;
-}
-
-export function summarizeProjectFileContent(result: LocalProjectFileResult) {
-  const limit = 4000;
-  const content =
-    result.content.length > limit
-      ? `${result.content.slice(0, limit)}\n...已截断 ${
-          result.content.length - limit
-        } 字符`
-      : result.content;
-
-  const visibleContent = content || '空文件';
-  let longestBacktickRun = 0;
-  for (const match of visibleContent.matchAll(/`+/gu)) {
-    longestBacktickRun = Math.max(longestBacktickRun, match[0].length);
-  }
-  const fence = '`'.repeat(Math.max(3, longestBacktickRun + 1));
-  return `文件:${result.path}\n\n${fence}text\n${visibleContent}\n${fence}`;
-}
-
-export function inferProjectFileAssetDraft(
-  localPath: string,
-): ProjectAssetDraft {
-  const extension = localPath.split('.').pop()?.toLowerCase() ?? '';
-  if (
-    ['png', 'jpg', 'jpeg', 'webp', 'gif', 'svg', 'avif'].includes(extension)
-  ) {
-    const normalizedExtension = extension === 'jpg' ? 'jpeg' : extension;
-    return {
-      localPath,
-      kind: 'image',
-      mediaType:
-        extension === 'svg' ? 'image/svg+xml' : `image/${normalizedExtension}`,
-    };
-  }
-  if (['mp3', 'wav', 'ogg', 'm4a', 'flac'].includes(extension)) {
-    return {
-      localPath,
-      kind: 'audio',
-      mediaType: extension === 'm4a' ? 'audio/mp4' : `audio/${extension}`,
-    };
-  }
-  if (['mp4', 'webm', 'mov'].includes(extension)) {
-    return {
-      localPath,
-      kind: 'video',
-      mediaType: extension === 'mov' ? 'video/quicktime' : `video/${extension}`,
-    };
-  }
-  if (extension === 'json') {
-    return { localPath, kind: 'document', mediaType: 'application/json' };
-  }
-  if (extension === 'html') {
-    return { localPath, kind: 'document', mediaType: 'text/html' };
-  }
-  if (['txt', 'md', 'csv'].includes(extension)) {
-    return { localPath, kind: 'document', mediaType: 'text/plain' };
-  }
-  // 判不出内容类型就写 `unknown`(→「待归类」),与 Rust 侧 `uploaded_asset_kind` 同口径:
-  // `/asset-register` 的 kind 会原样进 `register_local_asset` 的严格解析,写 `data` / `asset`
-  // 这类非 canonical 值只会被判成"认不出"并留痕,不能靠它表达"这是数据 / 这是资产"。
-  return { localPath, kind: 'unknown', mediaType: 'application/octet-stream' };
-}
-
-export function projectAssetDraftCommand(draft: ProjectAssetDraft) {
-  return `/asset-register ${draft.localPath} ${draft.kind} ${draft.mediaType}`;
-}
-
-export function projectFileActionDrafts(
-  localPath: string,
-): ProjectFileActionDraft {
-  return {
-    readCommand: `/read ${localPath}`,
-    assetCommand: projectAssetDraftCommand(
-      inferProjectFileAssetDraft(localPath),
-    ),
-  };
-}
-
-export function summarizeCommonProjectArtifactReadDrafts() {
-  return `常用生成产物:\n${commonProjectArtifactReadDrafts
-    .map(
-      (artifact) =>
-        `- ${artifact.label} · ${artifact.path} · /read ${artifact.path}`,
-    )
-    .join('\n')}`;
-}
-
-export function summarizeCommonProjectLogReadDrafts() {
-  return `常用日志读取命令:\n${commonProjectLogReadDrafts
-    .map((log) => `- ${log.label} · ${log.path} · /read ${log.path}`)
-    .join('\n')}`;
-}
-
-export function summarizeAgentRunSupportFileReadDrafts() {
-  return `Agent 运行辅助文件读取命令:\n${commonAgentRunSupportReadDrafts
-    .map((file) => `- ${file.label} · ${file.path} · /read ${file.path}`)
-    .join('\n')}`;
-}
-
-export function summarizeProjectInternalReadDrafts() {
-  return `项目内部真相源读取命令:\n${commonProjectInternalReadDrafts
-    .map((file) => `- ${file.label} · ${file.path} · /read ${file.path}`)
-    .join('\n')}`;
-}
diff --git a/apps/ai-game-creator-shell/src/features/project-summary/projectAssetSummaries.ts b/apps/ai-game-creator-shell/src/features/project-summary/projectAssetSummaries.ts
deleted file mode 100644
index 3afb4fde7..000000000
--- a/apps/ai-game-creator-shell/src/features/project-summary/projectAssetSummaries.ts
+++ /dev/null
@@ -1,282 +0,0 @@
-import {
-  type GameCreationAppAssetSourceKind,
-  type GameCreationAppManifest,
-  isGameCreationAppAssetAudioKind,
-  isGameCreationAppAssetVisualKind,
-  selectGameCreationAppReadyTasks,
-} from '../../../../../packages/shared/src/contracts/gameCreationApp';
-import { taskRowsFromManifest } from '../agent-runtime';
-import { isSafeProjectRelativePath } from './projectPath';
-import {
-  assetSourceKindLabels,
-  taskGroupLabels,
-  taskStatusLabels,
-} from './projectSummaryConstants';
-
-export function summarizeProjectAssets(nextManifest: GameCreationAppManifest) {
-  if (nextManifest.assets.length === 0) {
-    return '本地项目还没有登记资产。';
-  }
-
-  const visibleAssets = nextManifest.assets.slice(0, 20);
-  const lines = visibleAssets.map(
-    (asset) =>
-      `- ${asset.kind} · ${asset.localPath} · ${asset.source.kind}${
-        asset.source.canvasProjectId
-          ? ` · 画板 ${asset.source.canvasProjectId}`
-          : ''
-      }`,
-  );
-  if (nextManifest.assets.length > visibleAssets.length) {
-    lines.push(
-      `- 还有 ${nextManifest.assets.length - visibleAssets.length} 个资产`,
-    );
-  }
-  return `本地项目资产:\n${lines.join('\n')}`;
-}
-
-export function summarizeProjectAssetCredits(
-  nextManifest: GameCreationAppManifest,
-) {
-  if (nextManifest.assets.length === 0) {
-    return {
-      text: [
-        '素材署名:',
-        '- 当前资产:暂无登记资产',
-        '- 来源清单:暂无',
-        '- 需要确认:上传素材授权;生成素材模型;画板资源来源',
-        '- 建议:/assets',
-      ].join('\n'),
-      draftCommand: '/assets',
-      draftCommandLabel: '查看资产',
-    };
-  }
-
-  const sourceCounts = nextManifest.assets.reduce(
-    (counts, asset) => {
-      counts[asset.source.kind] += 1;
-      return counts;
-    },
-    { uploaded: 0, generated: 0, canvas: 0 } satisfies Record<
-      GameCreationAppAssetSourceKind,
-      number
-    >,
-  );
-  const visibleAssets = nextManifest.assets.slice(0, 10);
-  const lines = visibleAssets.map((asset) => {
-    const sourceLabel = assetSourceKindLabels[asset.source.kind];
-    const sourceDetail =
-      asset.source.kind === 'canvas'
-        ? `画板 ${asset.source.canvasProjectId ?? '未记录'}`
-        : asset.source.kind === 'generated'
-          ? `生成${asset.source.model ? ` ${asset.source.model}` : ''}`
-          : '用户上传';
-    return `- ${asset.localPath} · ${asset.mediaType} · ${sourceLabel} · ${sourceDetail}`;
-  });
-  if (nextManifest.assets.length > visibleAssets.length) {
-    lines.push(
-      `- 还有 ${nextManifest.assets.length - visibleAssets.length} 个资产`,
-    );
-  }
-  const sourceSummary = (
-    Object.keys(sourceCounts) as GameCreationAppAssetSourceKind[]
-  )
-    .filter((source) => sourceCounts[source] > 0)
-    .map((source) => `${assetSourceKindLabels[source]} ${sourceCounts[source]}`)
-    .join(' / ');
-
-  return {
-    text: [
-      '素材署名:',
-      `- 当前资产:${nextManifest.assets.length} 个`,
-      `- 来源分布:${sourceSummary || '暂无'}`,
-      `- 来源清单:\n${lines.join('\n')}`,
-      '- 需要确认:上传素材授权;生成素材模型;画板资源来源;本地试玩包保留来源口径',
-      '- 参考:/assets;/art;/audio;/listing',
-      '- 建议:/assets',
-    ].join('\n'),
-    draftCommand: '/assets',
-    draftCommandLabel: '查看资产',
-  };
-}
-
-export function isProjectAudioAsset(
-  asset: GameCreationAppManifest['assets'][number],
-) {
-  const mediaType = asset.mediaType.toLowerCase();
-  return (
-    mediaType.startsWith('audio/') ||
-    isGameCreationAppAssetAudioKind(asset.kind)
-  );
-}
-
-export function isProjectVisualAsset(
-  asset: GameCreationAppManifest['assets'][number],
-) {
-  const mediaType = asset.mediaType.toLowerCase();
-  return (
-    mediaType.startsWith('image/') ||
-    mediaType.startsWith('video/') ||
-    mediaType === 'application/vnd.genarrative.image-sequence' ||
-    isGameCreationAppAssetVisualKind(asset.kind)
-  );
-}
-
-export function summarizeProjectVisualAssets(
-  nextManifest: GameCreationAppManifest,
-) {
-  const visualAssets = nextManifest.assets.filter(isProjectVisualAsset);
-  if (visualAssets.length === 0) {
-    return {
-      text: [
-        '美术素材:暂无登记图片、视频或序列帧。',
-        '可先生成首版美术,或同步已有画板项目资源。',
-      ].join('\n'),
-      draftCommand: '/generate-art 首版核心美术素材',
-      draftCommandLabel: '生成美术',
-    };
-  }
-
-  const sourceCounts = visualAssets.reduce(
-    (counts, asset) => {
-      counts[asset.source.kind] += 1;
-      return counts;
-    },
-    { uploaded: 0, generated: 0, canvas: 0 } satisfies Record<
-      GameCreationAppAssetSourceKind,
-      number
-    >,
-  );
-  const visibleAssets = visualAssets.slice(0, 8);
-  const lines = visibleAssets.map(
-    (asset) =>
-      `- ${asset.localPath} · ${asset.mediaType} · ${
-        assetSourceKindLabels[asset.source.kind]
-      }${
-        asset.source.canvasProjectId
-          ? ` · 画板 ${asset.source.canvasProjectId}`
-          : ''
-      }`,
-  );
-  if (visualAssets.length > visibleAssets.length) {
-    lines.push(
-      `- 还有 ${visualAssets.length - visibleAssets.length} 个美术素材`,
-    );
-  }
-  const hasCanvasVisualAsset = visualAssets.some(
-    (asset) => asset.source.kind === 'canvas',
-  );
-
-  return {
-    text: [
-      `美术素材:${visualAssets.length} 个`,
-      `来源:${(Object.keys(sourceCounts) as GameCreationAppAssetSourceKind[])
-        .filter((source) => sourceCounts[source] > 0)
-        .map(
-          (source) =>
-            `${assetSourceKindLabels[source]} ${sourceCounts[source]}`,
-        )
-        .join('、')}`,
-      hasCanvasVisualAsset
-        ? '画板来源:已接入'
-        : '画板来源:暂无 · 建议 /generate-art 首版核心美术素材',
-      lines.join('\n'),
-    ].join('\n'),
-    draftCommand: hasCanvasVisualAsset
-      ? '/read assets/manifest.art.json'
-      : '/generate-art 首版核心美术素材',
-    draftCommandLabel: hasCanvasVisualAsset ? '读美术清单' : '生成美术',
-  };
-}
-
-export function summarizeProjectAudioAssets(
-  nextManifest: GameCreationAppManifest,
-) {
-  const audioAssets = nextManifest.assets.filter(isProjectAudioAsset);
-  if (audioAssets.length === 0) {
-    return {
-      text: [
-        '音频素材:暂无登记音频。',
-        '可先登记项目内音效,或把已有画板音频作为素材导入。',
-      ].join('\n'),
-      draftCommand: '/asset-register assets/audio/sfx.wav audio audio/wav',
-      draftCommandLabel: '登记音效',
-    };
-  }
-
-  const sourceCounts = audioAssets.reduce(
-    (counts, asset) => {
-      counts[asset.source.kind] += 1;
-      return counts;
-    },
-    { uploaded: 0, generated: 0, canvas: 0 } satisfies Record<
-      GameCreationAppAssetSourceKind,
-      number
-    >,
-  );
-  const visibleAssets = audioAssets.slice(0, 8);
-  const lines = visibleAssets.map(
-    (asset) =>
-      `- ${asset.localPath} · ${asset.mediaType} · ${
-        assetSourceKindLabels[asset.source.kind]
-      }${
-        asset.source.canvasProjectId
-          ? ` · 画板 ${asset.source.canvasProjectId}`
-          : ''
-      }`,
-  );
-  if (audioAssets.length > visibleAssets.length) {
-    lines.push(
-      `- 还有 ${audioAssets.length - visibleAssets.length} 个音频素材`,
-    );
-  }
-
-  return {
-    text: [
-      `音频素材:${audioAssets.length} 个`,
-      `来源:${(Object.keys(sourceCounts) as GameCreationAppAssetSourceKind[])
-        .filter((source) => sourceCounts[source] > 0)
-        .map(
-          (source) =>
-            `${assetSourceKindLabels[source]} ${sourceCounts[source]}`,
-        )
-        .join('、')}`,
-      lines.join('\n'),
-    ].join('\n'),
-    draftCommand: '/read assets/manifest.audio.json',
-    draftCommandLabel: '读音频清单',
-  };
-}
-
-export function firstReadableProjectAssetPath(
-  nextManifest: GameCreationAppManifest,
-) {
-  return nextManifest.assets.find((asset) =>
-    isSafeProjectRelativePath(asset.localPath),
-  )?.localPath;
-}
-
-export function summarizeProjectTasks(nextManifest: GameCreationAppManifest) {
-  const tasks = taskRowsFromManifest(nextManifest);
-  if (tasks.length === 0) {
-    return '还没有任务拆分。';
-  }
-  const readyTasks = selectGameCreationAppReadyTasks({
-    tasks,
-  });
-  const readySummary =
-    readyTasks.length > 0
-      ? `\n下一步:${readyTasks
-          .map((task) => `${taskGroupLabels[task.group]} / ${task.role}`)
-          .join(';')}`
-      : '\n下一步:等待确认或暂无可执行任务';
-
-  return `任务拆分:\n${tasks
-    .map(
-      (task) =>
-        `- ${taskGroupLabels[task.group]} / ${task.role}:${task.title} · ${
-          taskStatusLabels[task.status]
-        } -> ${task.artifacts.join(', ')}`,
-    )
-    .join('\n')}${readySummary}`;
-}
diff --git a/apps/ai-game-creator-shell/src/features/project-summary/projectDeliverySummaries.ts b/apps/ai-game-creator-shell/src/features/project-summary/projectDeliverySummaries.ts
deleted file mode 100644
index 0e3d4091c..000000000
--- a/apps/ai-game-creator-shell/src/features/project-summary/projectDeliverySummaries.ts
+++ /dev/null
@@ -1,661 +0,0 @@
-import {
-  type GameCreationAgentRunTrace,
-  type GameCreationAppAgentGroup,
-  type GameCreationAppManifest,
-  type GameCreationAppTaskState,
-  selectGameCreationAppReadyTasks,
-} from '../../../../../packages/shared/src/contracts/gameCreationApp';
-import { taskRowsFromManifest } from '../agent-runtime';
-import {
-  formatAgentRunStatus,
-  isAgentRunTracePassed,
-  readableArtifactsFromAgentRunTrace,
-} from './agentTrace';
-import {
-  isProjectAudioAsset,
-  isProjectVisualAsset,
-} from './projectAssetSummaries';
-import {
-  previewStatusLabels,
-  taskGroupLabels,
-  taskStatusLabels,
-} from './projectSummaryConstants';
-
-export function summarizeProjectReleaseNotes(
-  nextManifest: GameCreationAppManifest,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  const preview = nextManifest.preview;
-  const previewRunning = preview?.status === 'running' && preview.url;
-  const previewSummary = previewRunning
-    ? `运行中 ${preview.url}`
-    : preview
-      ? previewStatusLabels[preview.status]
-      : '未启动';
-  const tracePassed = isAgentRunTracePassed(trace);
-  const blockedTrace =
-    trace?.lifecycleStatus === 'killed' ||
-    trace?.status === 'failed' ||
-    trace?.status === 'needs-revision' ||
-    trace?.stopReason === 'max-passes-exhausted';
-  const goal =
-    nextManifest.goal?.trim() ||
-    trace?.goal?.trim() ||
-    trace?.taskGraph.goal?.trim() ||
-    '暂无';
-  const artifacts = trace ? readableArtifactsFromAgentRunTrace(trace) : [];
-  const artifactSummary =
-    artifacts.length > 0
-      ? artifacts
-          .slice(0, 4)
-          .map((artifact) => artifact.path)
-          .join(';')
-      : '暂无';
-  const visualAssetCount =
-    nextManifest.assets.filter(isProjectVisualAsset).length;
-  const audioAssetCount =
-    nextManifest.assets.filter(isProjectAudioAsset).length;
-  const hasPublishReadme =
-    trace?.artifacts.some(
-      (artifact) => artifact.path === 'exports/README.md',
-    ) ?? false;
-
-  let draftCommand = '/media-kit';
-  let draftCommandLabel = '准备资料包';
-  if (!trace) {
-    draftCommand = '/next';
-    draftCommandLabel = '查看下一步';
-  } else if (blockedTrace) {
-    draftCommand = '/review';
-    draftCommandLabel = '查看评审';
-  } else if (!tracePassed) {
-    draftCommand = '/trace';
-    draftCommandLabel = '查看 trace';
-  } else if (!previewRunning) {
-    draftCommand = '/run';
-    draftCommandLabel = '启动试玩';
-  } else if (hasPublishReadme) {
-    draftCommand = '/read exports/README.md';
-    draftCommandLabel = '读发布说明';
-  }
-
-  return {
-    text: [
-      '试玩更新说明:',
-      `- 项目:${nextManifest.name}`,
-      `- 一句话:${goal}`,
-      `- 当前版本:本地 Web 原型 · 小范围试玩 · 预览 ${previewSummary}`,
-      `- Run:${
-        trace
-          ? `${trace.runId} · ${formatAgentRunStatus(trace)}`
-          : '暂无最近 run'
-      }`,
-      `- 本轮变化:${tracePassed ? '可试玩版本已通过 Evaluator' : trace ? '仍需返工或复查' : '待生成首个版本'}`,
-      `- 主要产物:${artifactSummary}`,
-      `- 素材变化:视觉素材 ${visualAssetCount} 个;音频素材 ${audioAssetCount} 个`,
-      '- 玩家可见说明:玩法目标;操作方式;胜负 / 重开反馈;当前已知限制',
-      '- 已知限制:本地原型;不承诺账号、云存档、排行榜、付费或长期兼容',
-      '- 搭配:/changes;/media-kit;/post;/store;/share',
-      '- 边界:只准备试玩更新说明;不读取文件;不启动或打开预览;不导出试玩包;不上传云端;不发布作品;不写项目',
-      `- 建议:${draftCommand}`,
-    ].join('\n'),
-    draftCommand,
-    draftCommandLabel,
-  };
-}
-
-export function summarizeProjectKnownIssues(
-  nextManifest: GameCreationAppManifest,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  const preview = nextManifest.preview;
-  const previewRunning = preview?.status === 'running' && preview.url;
-  const previewSummary = previewRunning
-    ? `运行中 ${preview.url}`
-    : preview
-      ? previewStatusLabels[preview.status]
-      : '未启动';
-  const tracePassed = isAgentRunTracePassed(trace);
-  const blockedTrace =
-    trace?.lifecycleStatus === 'killed' ||
-    trace?.status === 'failed' ||
-    trace?.status === 'needs-revision' ||
-    trace?.stopReason === 'max-passes-exhausted';
-  const traceTasks = trace?.taskGraph.tasks ?? [];
-  const taskSource =
-    traceTasks.length > 0 ? traceTasks : taskRowsFromManifest(nextManifest);
-  const failedTasks = taskSource.filter((task) => task.status === 'failed');
-  const latestFailedTask = failedTasks[0] ?? null;
-  const knownIssueSummary = latestFailedTask
-    ? `失败任务 ${failedTasks.length} 个:${taskGroupLabels[latestFailedTask.group]} / ${latestFailedTask.role} ${latestFailedTask.title}(${latestFailedTask.id})`
-    : blockedTrace && trace
-      ? `最近 run 需返工:${trace.status} / ${trace.stopReason}`
-      : '暂无明确失败任务;仍按早期原型标注限制';
-
-  let draftCommand = '/share';
-  let draftCommandLabel = '准备交付';
-  if (!trace) {
-    draftCommand = '/next';
-    draftCommandLabel = '查看下一步';
-  } else if (blockedTrace) {
-    draftCommand = '/review';
-    draftCommandLabel = '查看评审';
-  } else if (!tracePassed) {
-    draftCommand = '/trace';
-    draftCommandLabel = '查看 trace';
-  } else if (!previewRunning) {
-    draftCommand = '/run';
-    draftCommandLabel = '启动试玩';
-  }
-
-  return {
-    text: [
-      '已知问题清单:',
-      `- 项目:${nextManifest.name}`,
-      `- 当前状态:${
-        trace
-          ? `${trace.runId} · ${formatAgentRunStatus(trace)}`
-          : '暂无最近 run'
-      };预览 ${previewSummary}`,
-      `- 已知问题:${knownIssueSummary}`,
-      '- 试玩限制:本地 Web 原型;小范围 5-10 分钟试玩;不承诺账号、云存档、排行榜、付费或长期兼容',
-      '- 反馈入口:客户端问题由自动诊断提示;整体体验走 /feedback;版本变化走 /release-notes',
-      '- 发送前检查:可试玩状态先 /run;交付口径看 /share;对外资料看 /media-kit',
-      '- 边界:只准备已知问题清单;不读取文件;不启动或打开预览;不导出试玩包;不上传云端;不发布作品;不写项目',
-      `- 建议:${draftCommand}`,
-    ].join('\n'),
-    draftCommand,
-    draftCommandLabel,
-  };
-}
-
-export function summarizeProjectAcceptanceCriteria(
-  nextManifest: GameCreationAppManifest,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  const tasks = taskRowsFromManifest(nextManifest);
-  const manifestTasksById = new Map(tasks.map((task) => [task.id, task]));
-  const traceTasksById = new Map(
-    trace?.taskGraph.tasks.map((task) => [task.id, task]) ?? [],
-  );
-  const selectedTasks: Array<{
-    task: GameCreationAppTaskState;
-    marker: string;
-  }> = [];
-  const seenTaskIds = new Set();
-  const addTask = (taskId: string, marker: string) => {
-    if (seenTaskIds.has(taskId)) {
-      return;
-    }
-    const task = traceTasksById.get(taskId) ?? manifestTasksById.get(taskId);
-    if (!task) {
-      return;
-    }
-    selectedTasks.push({ task, marker });
-    seenTaskIds.add(task.id);
-  };
-
-  trace?.taskGraph.activeTaskIds.forEach((taskId) => addTask(taskId, 'active'));
-  trace?.taskGraph.carriedTaskIds.forEach((taskId) => addTask(taskId, 'carry'));
-  trace?.taskGraph.readyTaskIds.forEach((taskId) => addTask(taskId, 'ready'));
-  tasks
-    .filter((task) => task.status === 'failed')
-    .forEach((task) => addTask(task.id, '失败'));
-
-  if (selectedTasks.length === 0) {
-    selectGameCreationAppReadyTasks({ tasks }).forEach((task) =>
-      addTask(task.id, 'ready'),
-    );
-  }
-
-  if (selectedTasks.length === 0) {
-    tasks
-      .filter((task) => task.status !== 'completed')
-      .slice(0, 3)
-      .forEach((task) => addTask(task.id, taskStatusLabels[task.status]));
-  }
-
-  const visibleTasks = selectedTasks.slice(0, 6);
-  const taskLines = visibleTasks.map(({ task, marker }) => {
-    const criteria =
-      task.acceptanceCriteria.length > 0
-        ? task.acceptanceCriteria.join(';')
-        : '暂无';
-    const artifacts =
-      task.artifacts.length > 0
-        ? task.artifacts.slice(0, 3).join(', ')
-        : '暂无';
-    return `- ${marker}:${taskGroupLabels[task.group]} / ${task.role} ${task.title}(${task.id}) · ${taskStatusLabels[task.status]} · 验收:${criteria} · 产物:${artifacts}`;
-  });
-  if (selectedTasks.length > visibleTasks.length) {
-    taskLines.push(
-      `- 还有 ${selectedTasks.length - visibleTasks.length} 个任务`,
-    );
-  }
-
-  return {
-    text: [
-      '当前验收标准:',
-      trace ? `- Run:${trace.runId} · ${formatAgentRunStatus(trace)}` : null,
-      taskLines.length > 0 ? taskLines.join('\n') : '- 暂无待验收任务',
-    ]
-      .filter(Boolean)
-      .join('\n'),
-    draftCommand: '/tasks',
-    draftCommandLabel: '查看任务',
-  };
-}
-
-export function summarizeProjectTodoList(
-  nextManifest: GameCreationAppManifest,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  const manifestTasks = taskRowsFromManifest(nextManifest);
-  const traceTasks = trace?.taskGraph.tasks ?? [];
-  const taskSource = traceTasks.length > 0 ? traceTasks : manifestTasks;
-  const manifestTasksById = new Map(
-    manifestTasks.map((task) => [task.id, task]),
-  );
-  const taskSourceById = new Map(taskSource.map((task) => [task.id, task]));
-  const selectedTasks: Array<{
-    task: GameCreationAppTaskState;
-    marker: string;
-  }> = [];
-  const seenTaskIds = new Set();
-  const addTask = (taskId: string, marker: string) => {
-    if (seenTaskIds.has(taskId)) {
-      return;
-    }
-    const task = taskSourceById.get(taskId) ?? manifestTasksById.get(taskId);
-    if (!task) {
-      return;
-    }
-    selectedTasks.push({ task, marker });
-    seenTaskIds.add(task.id);
-  };
-
-  taskSource
-    .filter((task) => task.status === 'failed')
-    .forEach((task) => addTask(task.id, '失败'));
-  trace?.taskGraph.activeTaskIds.forEach((taskId) => addTask(taskId, 'active'));
-  trace?.taskGraph.carriedTaskIds.forEach((taskId) => addTask(taskId, 'carry'));
-  trace?.taskGraph.readyTaskIds.forEach((taskId) => addTask(taskId, 'ready'));
-
-  if (selectedTasks.length === 0) {
-    selectGameCreationAppReadyTasks({ tasks: manifestTasks }).forEach((task) =>
-      addTask(task.id, 'ready'),
-    );
-  }
-
-  if (selectedTasks.length === 0) {
-    taskSource
-      .filter((task) => task.status !== 'completed')
-      .slice(0, 5)
-      .forEach((task) => addTask(task.id, taskStatusLabels[task.status]));
-  }
-
-  const visibleTasks = selectedTasks.slice(0, 5);
-  const taskLines = visibleTasks.map(({ task, marker }, index) => {
-    const acceptance =
-      task.acceptanceCriteria.length > 0 ? task.acceptanceCriteria[0] : '暂无';
-    const artifact = task.artifacts[0] ?? '暂无';
-    return `- ${index + 1}. ${marker}:${taskGroupLabels[task.group]} / ${task.role} ${task.title}(${task.id}) · ${taskStatusLabels[task.status]} · 验收:${acceptance} · 产物:${artifact}`;
-  });
-  if (selectedTasks.length > visibleTasks.length) {
-    taskLines.push(
-      `- 还有 ${selectedTasks.length - visibleTasks.length} 个候选任务`,
-    );
-  }
-
-  const blockedTrace =
-    trace?.lifecycleStatus === 'killed' ||
-    trace?.status === 'failed' ||
-    trace?.status === 'needs-revision' ||
-    trace?.stopReason === 'max-passes-exhausted';
-  let draftCommand = selectedTasks.length > 0 ? '/tasks' : '/next';
-  let draftCommandLabel = selectedTasks.length > 0 ? '查看任务' : '查看下一步';
-  if (blockedTrace) {
-    draftCommand = '/review';
-    draftCommandLabel = '查看评审';
-  }
-
-  return {
-    text: [
-      '下一轮小步:',
-      `- 项目:${nextManifest.name}`,
-      trace ? `- Run:${trace.runId} · ${formatAgentRunStatus(trace)}` : null,
-      trace?.nextStep ? `- 编排下一步:${trace.nextStep}` : null,
-      taskLines.length > 0
-        ? `- 小步清单:\n${taskLines.join('\n')}`
-        : '- 小步清单:暂无待处理任务',
-      '- 边界:只整理下一步;不读取任务文件;不启动 run;不修改项目',
-      `- 建议:${draftCommand}`,
-    ]
-      .filter(Boolean)
-      .join('\n'),
-    draftCommand,
-    draftCommandLabel,
-  };
-}
-
-export function summarizeProjectNextRoundPlan(
-  nextManifest: GameCreationAppManifest,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  const manifestTasks = taskRowsFromManifest(nextManifest);
-  const traceTasks = trace?.taskGraph.tasks ?? [];
-  const taskSource = traceTasks.length > 0 ? traceTasks : manifestTasks;
-  const manifestTasksById = new Map(
-    manifestTasks.map((task) => [task.id, task]),
-  );
-  const taskSourceById = new Map(taskSource.map((task) => [task.id, task]));
-  const selectedTasks: Array<{
-    task: GameCreationAppTaskState;
-    marker: string;
-  }> = [];
-  const seenTaskIds = new Set();
-  const addTask = (taskId: string, marker: string) => {
-    if (seenTaskIds.has(taskId)) {
-      return;
-    }
-    const task = taskSourceById.get(taskId) ?? manifestTasksById.get(taskId);
-    if (!task) {
-      return;
-    }
-    selectedTasks.push({ task, marker });
-    seenTaskIds.add(task.id);
-  };
-
-  taskSource
-    .filter((task) => task.status === 'failed')
-    .forEach((task) => addTask(task.id, '失败'));
-  trace?.taskGraph.activeTaskIds.forEach((taskId) => addTask(taskId, 'active'));
-  trace?.taskGraph.carriedTaskIds.forEach((taskId) => addTask(taskId, 'carry'));
-  trace?.taskGraph.readyTaskIds.forEach((taskId) => addTask(taskId, 'ready'));
-
-  if (selectedTasks.length === 0) {
-    selectGameCreationAppReadyTasks({ tasks: manifestTasks }).forEach((task) =>
-      addTask(task.id, 'ready'),
-    );
-  }
-
-  if (selectedTasks.length === 0) {
-    taskSource
-      .filter((task) => task.status !== 'completed')
-      .slice(0, 6)
-      .forEach((task) => addTask(task.id, taskStatusLabels[task.status]));
-  }
-
-  const groups: GameCreationAppAgentGroup[] = [
-    'design',
-    'art',
-    'code',
-    'balance',
-    'audio',
-    'publishing',
-  ];
-  const groupLines = groups.flatMap((group) => {
-    const groupTasks = selectedTasks.filter(({ task }) => task.group === group);
-    return groupTasks.slice(0, 2).map(({ task, marker }) => {
-      const acceptance = task.acceptanceCriteria[0] ?? '暂无';
-      return `- ${taskGroupLabels[group]}:${marker} · ${task.role} ${task.title}(${task.id}) · 验收:${acceptance}`;
-    });
-  });
-  const selectedGroups = groups.filter((group) =>
-    selectedTasks.some(({ task }) => task.group === group),
-  );
-  const idleGroups = groups.filter((group) => !selectedGroups.includes(group));
-  const firstTask = selectedTasks[0]?.task ?? null;
-  const blockedTrace =
-    trace?.lifecycleStatus === 'killed' ||
-    trace?.status === 'failed' ||
-    trace?.status === 'needs-revision' ||
-    trace?.stopReason === 'max-passes-exhausted';
-  const draftCommand = blockedTrace
-    ? '/review'
-    : firstTask
-      ? `/agent-resume 下一轮计划:${taskGroupLabels[firstTask.group]} / ${firstTask.role} ${firstTask.title}`
-      : '/next';
-  const draftCommandLabel = blockedTrace
-    ? '查看评审'
-    : firstTask
-      ? '继续执行计划'
-      : '查看下一步';
-
-  return {
-    text: [
-      '下一轮分工计划:',
-      `- 项目:${nextManifest.name}`,
-      trace ? `- Run:${trace.runId} · ${formatAgentRunStatus(trace)}` : null,
-      trace?.nextStep ? `- 编排焦点:${trace.nextStep}` : null,
-      selectedGroups.length > 0
-        ? `- 协作顺序:${selectedGroups
-            .map((group) => taskGroupLabels[group])
-            .join(' -> ')}`
-        : '- 协作顺序:暂无',
-      groupLines.length > 0
-        ? `- 分工:\n${groupLines.join('\n')}`
-        : '- 分工:暂无待接手任务',
-      idleGroups.length > 0
-        ? `- 空档组:${idleGroups
-            .map((group) => taskGroupLabels[group])
-            .join('、')}`
-        : '- 空档组:暂无',
-      '- 边界:只整理下一轮分工;不读取任务文件;不启动 run;不修改项目',
-      `- 建议:${draftCommand}`,
-    ]
-      .filter(Boolean)
-      .join('\n'),
-    draftCommand,
-    draftCommandLabel,
-  };
-}
-
-export function summarizeProjectSpecSheet(
-  nextManifest: GameCreationAppManifest,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  const tasks = taskRowsFromManifest(nextManifest);
-  const taskArtifactPaths = new Set(tasks.flatMap((task) => task.artifacts));
-  const tracePathEntries =
-    trace?.steps.flatMap((step) => [...step.inputPaths, ...step.outputPaths]) ??
-    [];
-  const tracePaths = new Set([
-    ...(trace?.artifacts.map((artifact) => artifact.path) ?? []),
-    ...tracePathEntries,
-  ]);
-  const specItems = [
-    { label: 'Planner 规格', path: '.agent/spec.md', group: 'design' },
-    { label: '玩法设计', path: 'game/game_design.md', group: 'design' },
-    { label: '数值表', path: 'game/balance.json', group: 'balance' },
-    { label: '美术清单', path: 'assets/manifest.art.json', group: 'art' },
-    { label: '音频清单', path: 'assets/manifest.audio.json', group: 'audio' },
-    { label: '发布说明', path: 'exports/README.md', group: 'publishing' },
-  ] as const;
-  const itemLines = specItems.map((item) => {
-    const groupTasks = tasks.filter((task) => task.group === item.group);
-    const completedCount = groupTasks.filter(
-      (task) => task.status === 'completed',
-    ).length;
-    const status = tracePaths.has(item.path)
-      ? '已出现在最近 run'
-      : taskArtifactPaths.has(item.path)
-        ? '任务声明'
-        : '待补齐';
-    const taskSummary =
-      groupTasks.length > 0
-        ? ` · ${taskGroupLabels[item.group]}任务 ${completedCount}/${groupTasks.length}`
-        : '';
-    return `- ${item.label}:${item.path} · ${status}${taskSummary}`;
-  });
-  const firstReadablePath =
-    specItems.find((item) => tracePaths.has(item.path))?.path ??
-    specItems.find((item) => taskArtifactPaths.has(item.path))?.path ??
-    null;
-  const draftCommand = firstReadablePath
-    ? `/read ${firstReadablePath}`
-    : '/next';
-
-  return {
-    text: [
-      '创作规格包:',
-      `- 项目:${nextManifest.name}`,
-      `- 目标:${nextManifest.goal || trace?.goal || '暂无'}`,
-      trace ? `- Run:${trace.runId} · ${formatAgentRunStatus(trace)}` : null,
-      `- 规格清单:\n${itemLines.join('\n')}`,
-      '- 关联:/goal;/rules;/balance;/art;/audio;/publish',
-      '- 边界:只整理规格产物状态;不读取规格文件;不启动预览;不写项目',
-      `- 建议:${draftCommand}`,
-    ]
-      .filter(Boolean)
-      .join('\n'),
-    draftCommand,
-    draftCommandLabel: firstReadablePath ? '读取规格' : '查看下一步',
-  };
-}
-
-export function summarizeProjectGroupProgress(
-  nextManifest: GameCreationAppManifest,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  const tasks = taskRowsFromManifest(nextManifest);
-  const traceTasks = trace?.taskGraph.tasks ?? [];
-  const taskSource = traceTasks.length > 0 ? traceTasks : tasks;
-  const activeTaskIds = new Set(trace?.taskGraph.activeTaskIds ?? []);
-  const carriedTaskIds = new Set(trace?.taskGraph.carriedTaskIds ?? []);
-  const readyTaskIds = new Set(
-    trace
-      ? trace.taskGraph.readyTaskIds
-      : selectGameCreationAppReadyTasks({ tasks }).map((task) => task.id),
-  );
-  const groups: GameCreationAppAgentGroup[] = [
-    'design',
-    'art',
-    'code',
-    'balance',
-    'audio',
-    'publishing',
-  ];
-  const lines = groups.map((group) => {
-    const groupTasks = taskSource.filter((task) => task.group === group);
-    const completedCount = groupTasks.filter(
-      (task) => task.status === 'completed',
-    ).length;
-    const failedCount = groupTasks.filter(
-      (task) => task.status === 'failed',
-    ).length;
-    const activeCount = groupTasks.filter((task) =>
-      activeTaskIds.has(task.id),
-    ).length;
-    const carriedCount = groupTasks.filter((task) =>
-      carriedTaskIds.has(task.id),
-    ).length;
-    const readyTasks = groupTasks.filter((task) => readyTaskIds.has(task.id));
-    const nextTask =
-      readyTasks[0] ??
-      groupTasks.find((task) => activeTaskIds.has(task.id)) ??
-      null;
-    const nextSummary = nextTask
-      ? `${nextTask.role} ${nextTask.title}`
-      : '暂无';
-
-    return `- ${taskGroupLabels[group]}:完成 ${completedCount}/${groupTasks.length} · active ${activeCount} · carry ${carriedCount} · ready ${readyTasks.length} · 失败 ${failedCount} · 下一步 ${nextSummary}`;
-  });
-  const latestPassPlan = trace?.passPlans.slice(-1)[0] ?? null;
-
-  return {
-    text: [
-      '专业组进度:',
-      trace ? `- Run:${trace.runId} · ${formatAgentRunStatus(trace)}` : null,
-      latestPassPlan
-        ? `- 最近编排:第 ${latestPassPlan.pass} 轮 · ${latestPassPlan.mode} · ${latestPassPlan.summary}`
-        : null,
-      lines.join('\n'),
-    ]
-      .filter(Boolean)
-      .join('\n'),
-    draftCommand: '/tasks',
-    draftCommandLabel: '查看任务',
-  };
-}
-
-export function summarizeProjectBalanceState(
-  nextManifest: GameCreationAppManifest,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  const manifestTasks = taskRowsFromManifest(nextManifest);
-  const traceTasks = trace?.taskGraph.tasks ?? [];
-  const tasks = manifestTasks.map(
-    (task) => traceTasks.find((traceTask) => traceTask.id === task.id) ?? task,
-  );
-  const balanceTasks = tasks.filter(
-    (task) => task.group === 'balance' || task.id.startsWith('balance-'),
-  );
-  const readyTaskIds = new Set(
-    trace
-      ? trace.taskGraph.readyTaskIds
-      : selectGameCreationAppReadyTasks({ tasks }).map((task) => task.id),
-  );
-  const activeTaskIds = new Set(trace?.taskGraph.activeTaskIds ?? []);
-  const carriedTaskIds = new Set(trace?.taskGraph.carriedTaskIds ?? []);
-  const balanceArtifact =
-    trace?.artifacts.find(
-      (artifact) => artifact.path === 'game/balance.json',
-    ) ?? null;
-  const latestBalanceStep =
-    trace?.steps
-      .filter(
-        (step) =>
-          step.group === 'balance' || step.taskId?.startsWith('balance-'),
-      )
-      .slice(-1)[0] ?? null;
-  const taskLines = balanceTasks.map((task) => {
-    const markers = [taskStatusLabels[task.status]];
-    if (readyTaskIds.has(task.id)) {
-      markers.push('ready');
-    }
-    if (activeTaskIds.has(task.id)) {
-      markers.push('active');
-    }
-    if (carriedTaskIds.has(task.id)) {
-      markers.push('carry');
-    }
-    return `- ${task.id}:${task.role} ${task.title} · ${markers.join(' / ')}`;
-  });
-  const criteriaLines = balanceTasks.flatMap((task) =>
-    task.acceptanceCriteria.map((criterion) => `- ${task.id}:${criterion}`),
-  );
-
-  const draftCommand = balanceArtifact
-    ? '/read game/balance.json'
-    : trace
-      ? '/agent-resume 数值调整:前 30 秒更易上手;得分反馈更明显;失败后重开节奏更快'
-      : '/next';
-
-  return {
-    text: [
-      '数值状态:',
-      `- 项目:${nextManifest.name}`,
-      taskLines.length > 0
-        ? `- 数值任务:\n${taskLines.join('\n')}`
-        : '- 数值任务:暂无',
-      criteriaLines.length > 0
-        ? `- 数值口径:\n${criteriaLines.join('\n')}`
-        : '- 数值口径:暂无',
-      `- 数值表:${balanceArtifact ? 'game/balance.json · 已生成' : 'game/balance.json · 待生成'}`,
-      `- 最近数值步骤:${
-        latestBalanceStep
-          ? `${latestBalanceStep.agent} #${latestBalanceStep.pass} · ${latestBalanceStep.status} · ${latestBalanceStep.summary}`
-          : '暂无'
-      }`,
-      '- 试玩关联:/playtest;/feedback',
-      `- 建议:${draftCommand}`,
-    ].join('\n'),
-    draftCommand,
-    draftCommandLabel: balanceArtifact
-      ? '读数值表'
-      : trace
-        ? '填写数值反馈'
-        : '查看下一步',
-  };
-}
diff --git a/apps/ai-game-creator-shell/src/features/project-summary/projectGuidanceSummaries.ts b/apps/ai-game-creator-shell/src/features/project-summary/projectGuidanceSummaries.ts
deleted file mode 100644
index d5f8b35ee..000000000
--- a/apps/ai-game-creator-shell/src/features/project-summary/projectGuidanceSummaries.ts
+++ /dev/null
@@ -1,271 +0,0 @@
-import {
-  type GameCreationAgentRunTrace,
-  type GameCreationAppAssetSourceKind,
-  type GameCreationAppManifest,
-  selectGameCreationAppReadyTasks,
-} from '../../../../../packages/shared/src/contracts/gameCreationApp';
-import { type AgentStatusCard } from '../../app/types';
-import { taskRowsFromManifest } from '../agent-runtime';
-import { isAgentRunTracePassed } from './agentTrace';
-import {
-  isProjectAudioAsset,
-  isProjectVisualAsset,
-} from './projectAssetSummaries';
-import { assetSourceKindLabels } from './projectSummaryConstants';
-
-export function summarizeNextProjectActions(
-  nextManifest: GameCreationAppManifest,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  const suggestions: Array<{ label: string; command?: string }> = [];
-  const addSuggestion = (label: string, command?: string) => {
-    if (command && suggestions.some((item) => item.command === command)) {
-      return;
-    }
-    suggestions.push({ label, command });
-  };
-
-  const preview = nextManifest.preview;
-  if (!trace) {
-    addSuggestion('直接输入一句游戏需求,确认后生成首版原型');
-  } else if (
-    trace.lifecycleStatus === 'killed' ||
-    trace.status === 'failed' ||
-    trace.stopReason === 'max-passes-exhausted'
-  ) {
-    addSuggestion('补充说明并继续最近 run', '/agent-resume ');
-  } else if (
-    trace.status === 'passed' ||
-    trace.status === 'artifacts-written' ||
-    trace.stopReason === 'evaluator-passed'
-  ) {
-    addSuggestion(
-      preview?.status === 'running' && preview.url
-        ? '打开当前本地预览'
-        : '运行自检并启动本地预览',
-      preview?.status === 'running' && preview.url ? '/open-preview' : '/run',
-    );
-    addSuggestion('导出本地试玩包', '/export');
-    addSuggestion('查看本地试玩包', '/exports');
-  } else {
-    addSuggestion('查看最近 loop 进展', '/trace');
-  }
-  addSuggestion('查看创作目标', '/goal');
-  addSuggestion('查看普通用户操作导引', '/guide');
-  addSuggestion('查看项目进度', '/progress');
-  addSuggestion('查看创作规格包', '/spec');
-  addSuggestion('查看本轮 MVP 范围', '/mvp');
-  addSuggestion('查看试玩定位与卖点', '/pitch');
-  addSuggestion('准备 30 秒试玩讲解稿', '/demo');
-  addSuggestion('查看玩法操作与规则', '/rules');
-  addSuggestion('查看新手引导检查', '/tutorial');
-  addSuggestion('查看移动试玩检查', '/mobile');
-  addSuggestion('准备兼容性说明', '/compatibility');
-  addSuggestion('查看可读性与无障碍检查', '/accessibility');
-  addSuggestion('查看本地化与文案检查', '/localization');
-  addSuggestion('查看性能与加载检查', '/performance');
-  addSuggestion('查看试玩前打磨清单', '/polish');
-  addSuggestion('查看数值与难度口径', '/balance');
-  addSuggestion('查看当前阻塞项', '/blockers');
-  addSuggestion('查看试玩就绪度', '/ready');
-  addSuggestion('查看验证证据台账', '/evidence');
-  addSuggestion('查看 Agent 智能服务状态', '/llm-routes');
-  addSuggestion('查看任务依赖链', '/deps');
-  addSuggestion('准备下一轮改版说明', '/revise');
-  addSuggestion('查看隐私与导出边界', '/privacy');
-  addSuggestion('查看首批试玩对象', '/audience');
-  addSuggestion('准备试玩邀请文案', '/invite');
-  addSuggestion('准备试玩问卷问题', '/survey');
-  addSuggestion('准备封面与缩略图检查', '/cover');
-  addSuggestion('准备宣传截图清单', '/screenshots');
-  addSuggestion('准备试玩短视频脚本', '/trailer');
-  addSuggestion('准备试玩常见问答', '/faq');
-  addSuggestion('准备社区发布文案', '/post');
-  addSuggestion('准备上架资料清单', '/store');
-  addSuggestion('准备媒体资料包清单', '/media-kit');
-  addSuggestion('准备试玩更新说明', '/release-notes');
-  addSuggestion('准备已知问题清单', '/known-issues');
-
-  const readyTasks = selectGameCreationAppReadyTasks({
-    tasks: taskRowsFromManifest(nextManifest),
-  });
-  if (readyTasks.length > 0) {
-    addSuggestion(`查看 ${readyTasks.length} 个 ready 任务`, '/tasks');
-    addSuggestion('查看当前任务验收标准', '/criteria');
-    addSuggestion('查看专业组进度', '/groups');
-  } else {
-    addSuggestion('查看任务拆分和等待项', '/tasks');
-    addSuggestion('查看当前任务验收标准', '/criteria');
-    addSuggestion('查看专业组进度', '/groups');
-  }
-  addSuggestion('查看质量检查清单', '/qa');
-  addSuggestion('查看最近生成变更', '/changes');
-  addSuggestion('查看下一轮分工计划', '/plan');
-  addSuggestion('查看下一轮小步清单', '/todo');
-
-  if (nextManifest.assets.length > 0) {
-    addSuggestion(`查看 ${nextManifest.assets.length} 个本地资产`, '/assets');
-    addSuggestion('查看素材署名与来源', '/credits');
-    addSuggestion(
-      nextManifest.assets.some(isProjectVisualAsset)
-        ? '查看美术素材'
-        : '生成或同步美术素材',
-      '/art',
-    );
-    addSuggestion(
-      nextManifest.assets.some(isProjectAudioAsset)
-        ? '查看音频素材'
-        : '登记或导入音频素材',
-      '/audio',
-    );
-  } else {
-    addSuggestion(
-      '登记本地素材或同步画板资源',
-      '/asset-register assets/hero.png image image/png',
-    );
-    addSuggestion('同步已有画板项目资源', '/sync-canvas-project ');
-    addSuggestion('查看素材署名与来源', '/credits');
-    addSuggestion('生成或同步美术素材', '/art');
-    addSuggestion('登记或导入音频素材', '/audio');
-  }
-
-  if (trace) {
-    addSuggestion('查看发布准备清单', '/publish');
-    addSuggestion('准备作品页文案清单', '/listing');
-    addSuggestion('查看试玩状态', '/playtest');
-    addSuggestion('准备手动测试计划', '/test-plan');
-    addSuggestion('准备试玩反馈', '/feedback');
-    addSuggestion('准备复玩观察清单', '/retention');
-    addSuggestion('准备试玩交付清单', '/share');
-    addSuggestion('查看最近 run 预算', '/budget');
-    addSuggestion('查看评审和返工焦点', '/review');
-    addSuggestion('查看生成上下文来源', '/context');
-    addSuggestion('查看项目活动时间线', '/timeline');
-    addSuggestion('查看最近 trace 摘要', '/trace');
-    addSuggestion('列出最近 Run 产物', '/run-artifacts');
-    addSuggestion('列出 Agent 轮次产物', '/passes');
-    addSuggestion('列出 Agent 运行辅助文件', '/run-files');
-  }
-  addSuggestion('打开产物命令列表', '/artifacts');
-  addSuggestion('列出内部真相源读取命令', '/internals');
-  addSuggestion('打开日志命令列表', '/logs');
-
-  const firstCommand = suggestions.find((item) => item.command);
-  return {
-    text: `下一步建议:\n${suggestions
-      .map(
-        (item) => `- ${item.label}${item.command ? `:${item.command}` : ''}`,
-      )
-      .join('\n')}`,
-    draftCommand: firstCommand?.command,
-    draftCommandLabel: firstCommand?.label,
-  };
-}
-
-export function summarizeProjectUserGuide(
-  nextManifest: GameCreationAppManifest,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  const preview = nextManifest.preview;
-  const previewRunning = preview?.status === 'running' && preview.url;
-  const tracePassed = isAgentRunTracePassed(trace);
-  const blockedTrace =
-    trace?.lifecycleStatus === 'killed' ||
-    trace?.status === 'failed' ||
-    trace?.status === 'needs-revision' ||
-    trace?.stopReason === 'max-passes-exhausted';
-  const exported = (nextManifest.commandRuns ?? []).some(
-    (commandRun) =>
-      commandRun.commandId === 'project.export_package' &&
-      commandRun.status === 'completed',
-  );
-
-  let stage = '未开始';
-  let nextAction = '先确认一句游戏目标,再生成首版原型。';
-  let recommendedCommands = ['/brief', '/mvp', '/next'];
-
-  if (blockedTrace) {
-    stage = '需修复';
-    nextAction = '先看评审和阻塞,再把修复说明交回 agent。';
-    recommendedCommands = ['/review', '/todo', '/plan'];
-  } else if (exported) {
-    stage = '已导出';
-    nextAction = '先检查试玩包和交付材料,再发给测试者。';
-    recommendedCommands = ['/exports', '/share', '/listing'];
-  } else if (previewRunning) {
-    stage = '可预览';
-    nextAction = '先打开本地预览试玩一轮,再记录反馈。';
-    recommendedCommands = ['/open-preview', '/test-plan', '/feedback'];
-  } else if (tracePassed) {
-    stage = '可导出';
-    nextAction = '先运行自检并启动本地预览,通过后再导出试玩包。';
-    recommendedCommands = ['/run', '/test-plan', '/share'];
-  } else if (trace) {
-    stage = '生成中/待验收';
-    nextAction = '先看最近 loop 和下一轮小步,再决定是否继续。';
-    recommendedCommands = ['/trace', '/todo', '/plan'];
-  }
-
-  const draftCommand = recommendedCommands[0];
-
-  return {
-    text: [
-      '使用导引:',
-      `- 项目:${nextManifest.name}`,
-      `- 当前阶段:${stage}`,
-      `- 现在先做:${nextAction}`,
-      `- 推荐命令:${recommendedCommands.join(' / ')}`,
-      '- 边界:只给操作导引;不读取文件;不启动 run;不启动预览;不写项目',
-    ].join('\n'),
-    draftCommand,
-    draftCommandLabel: '执行导引建议',
-  };
-}
-
-export function summarizeMainProjectHeader(
-  nextManifest: GameCreationAppManifest,
-  agents: AgentStatusCard[],
-) {
-  const tasks = taskRowsFromManifest(nextManifest);
-  const completedCount = tasks.filter(
-    (task) => task.status === 'completed',
-  ).length;
-  const readyTaskIds = new Set(
-    selectGameCreationAppReadyTasks({ tasks }).map((task) => task.id),
-  );
-  for (const agent of agents) {
-    if (agent.taskGraphState === 'ready') {
-      readyTaskIds.add(agent.taskId);
-    }
-  }
-  const sourceCounts = nextManifest.assets.reduce(
-    (counts, asset) => {
-      counts[asset.source.kind] += 1;
-      return counts;
-    },
-    {
-      uploaded: 0,
-      generated: 0,
-      canvas: 0,
-    } satisfies Record,
-  );
-  const sourceSummary = (
-    Object.keys(sourceCounts) as GameCreationAppAssetSourceKind[]
-  )
-    .filter((source) => sourceCounts[source] > 0)
-    .map((source) => `${assetSourceKindLabels[source]} ${sourceCounts[source]}`)
-    .join(' / ');
-  const commandRuns = nextManifest.commandRuns ?? [];
-  const latestCommandRun = commandRuns[commandRuns.length - 1];
-  return [
-    `任务:已完成 ${completedCount}/${tasks.length} · ready ${readyTaskIds.size}`,
-    `资产:${nextManifest.assets.length} 个${
-      sourceSummary ? ` · ${sourceSummary}` : ''
-    }`,
-    latestCommandRun
-      ? `最近命令:${latestCommandRun.commandId} ${
-          latestCommandRun.status === 'completed' ? '完成' : '失败'
-        }`
-      : '最近命令:暂无',
-  ].join(' · ');
-}
diff --git a/apps/ai-game-creator-shell/src/features/project-summary/projectOverviewSummaries.ts b/apps/ai-game-creator-shell/src/features/project-summary/projectOverviewSummaries.ts
deleted file mode 100644
index 5860bfd3a..000000000
--- a/apps/ai-game-creator-shell/src/features/project-summary/projectOverviewSummaries.ts
+++ /dev/null
@@ -1,562 +0,0 @@
-import {
-  type GameCreationAgentRunTrace,
-  type GameCreationAppManifest,
-  type GameCreationAppTaskStatus,
-  selectGameCreationAppReadyTasks,
-} from '../../../../../packages/shared/src/contracts/gameCreationApp';
-import { taskRowsFromManifest } from '../agent-runtime';
-import {
-  formatAgentRunStatus,
-  isAgentRunTracePassed,
-  isPlaytestTraceStep,
-} from './agentTrace';
-import {
-  isProjectAudioAsset,
-  isProjectVisualAsset,
-} from './projectAssetSummaries';
-import {
-  previewStatusLabels,
-  taskGroupLabels,
-  taskStatusLabels,
-} from './projectSummaryConstants';
-
-export function summarizeProjectStatus(
-  nextManifest: GameCreationAppManifest,
-  nextProjectPath: string,
-) {
-  const tasks = taskRowsFromManifest(nextManifest);
-  const counts = tasks.reduce>(
-    (current, task) => {
-      current[task.status] += 1;
-      return current;
-    },
-    {
-      pending: 0,
-      running: 0,
-      'waiting-for-confirmation': 0,
-      completed: 0,
-      failed: 0,
-    },
-  );
-  const taskSummary = (
-    [
-      'completed',
-      'waiting-for-confirmation',
-      'running',
-      'pending',
-      'failed',
-    ] as const
-  )
-    .filter((status) => counts[status] > 0)
-    .map((status) => `${taskStatusLabels[status]} ${counts[status]}`)
-    .join(',');
-  const preview = nextManifest.preview;
-  const commandRuns = nextManifest.commandRuns ?? [];
-  const latestCommandRun = commandRuns[commandRuns.length - 1];
-  const previewSummary =
-    preview?.status === 'running' && preview.url
-      ? `运行中 ${preview.url}`
-      : preview
-        ? previewStatusLabels[preview.status]
-        : '未启动';
-
-  return [
-    `项目:${nextManifest.name}`,
-    `目录:${nextProjectPath}`,
-    `任务:${taskSummary || '无任务'}`,
-    `资产:${nextManifest.assets.length} 个`,
-    `预览:${previewSummary}`,
-    latestCommandRun
-      ? `最近命令:${latestCommandRun.commandId} · ${
-          latestCommandRun.status === 'completed' ? '完成' : '失败'
-        }`
-      : null,
-  ]
-    .filter(Boolean)
-    .join('\n');
-}
-
-export function summarizeProjectBrief(
-  nextManifest: GameCreationAppManifest,
-  nextProjectPath: string,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  const tasks = taskRowsFromManifest(nextManifest);
-  const completedCount = tasks.filter(
-    (task) => task.status === 'completed',
-  ).length;
-  const failedCount = tasks.filter((task) => task.status === 'failed').length;
-  const readyCount = selectGameCreationAppReadyTasks({ tasks }).length;
-  const sourceCounts = nextManifest.assets.reduce(
-    (counts, asset) => {
-      counts[asset.source.kind] += 1;
-      return counts;
-    },
-    { uploaded: 0, generated: 0, canvas: 0 },
-  );
-  const assetSummary =
-    nextManifest.assets.length > 0
-      ? `${nextManifest.assets.length} 个 · 上传 ${sourceCounts.uploaded} / 生成 ${sourceCounts.generated} / 画板 ${sourceCounts.canvas}`
-      : '暂无';
-  const preview = nextManifest.preview;
-  const previewSummary =
-    preview?.status === 'running' && preview.url
-      ? `运行中 ${preview.url}`
-      : preview
-        ? previewStatusLabels[preview.status]
-        : '未启动';
-  const commandRuns = nextManifest.commandRuns ?? [];
-  const latestCommandRun = commandRuns[commandRuns.length - 1];
-  const runSummary = trace
-    ? `${trace.runId} · ${trace.status}${
-        trace.lifecycleStatus ? ` / ${trace.lifecycleStatus}` : ''
-      } · ${trace.passes}/${trace.maxPasses} 轮 · ${trace.stopReason}`
-    : '暂无最近 run';
-
-  return `项目简报:\n- 项目:${nextManifest.name}\n- 目录:${nextProjectPath}\n- 任务:完成 ${completedCount}/${tasks.length} · ready ${readyCount} · 失败 ${failedCount}\n- 资产:${assetSummary}\n- 最近 Run:${runSummary}\n- 预览:${previewSummary}\n- 最近命令:${
-    latestCommandRun
-      ? `${latestCommandRun.commandId} · ${
-          latestCommandRun.status === 'completed' ? '完成' : '失败'
-        }`
-      : '暂无'
-  }`;
-}
-
-export function summarizeProjectGoal(
-  nextManifest: GameCreationAppManifest,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  const manifestGoal = nextManifest.goal?.trim() || '';
-  const runGoal = trace?.goal?.trim() || '';
-  const taskGraphGoal = trace?.taskGraph.goal?.trim() || '';
-  const draftCommand = trace ? '/agent-resume 细化目标:' : '/next';
-
-  return {
-    text: [
-      '创作目标:',
-      `- 项目:${nextManifest.name}`,
-      `- Manifest:${manifestGoal || '暂无'}`,
-      trace ? `- Run:${trace.runId} · ${formatAgentRunStatus(trace)}` : null,
-      `- Run 目标:${runGoal || '暂无'}`,
-      `- 任务图目标:${taskGraphGoal || '暂无'}`,
-      '- 上下文:/context',
-      `- 建议:${draftCommand}`,
-    ]
-      .filter(Boolean)
-      .join('\n'),
-    draftCommand,
-    draftCommandLabel: trace ? '补充目标' : '查看下一步',
-  };
-}
-
-export function summarizeProjectProgress(
-  nextManifest: GameCreationAppManifest,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  const manifestTasks = taskRowsFromManifest(nextManifest);
-  const traceTasks = trace?.taskGraph.tasks ?? [];
-  const traceTasksById = new Map(traceTasks.map((task) => [task.id, task]));
-  const tasks = manifestTasks.map(
-    (task) => traceTasksById.get(task.id) ?? task,
-  );
-  const taskIds = new Set(tasks.map((task) => task.id));
-  for (const task of traceTasks) {
-    if (!taskIds.has(task.id)) {
-      tasks.push(task);
-      taskIds.add(task.id);
-    }
-  }
-  const completedCount = tasks.filter(
-    (task) => task.status === 'completed',
-  ).length;
-  const failedCount = tasks.filter((task) => task.status === 'failed').length;
-  const readyCount =
-    trace?.taskGraph.readyTaskIds.filter((taskId) => taskIds.has(taskId))
-      .length ?? selectGameCreationAppReadyTasks({ tasks }).length;
-  const progressPercent =
-    tasks.length > 0 ? Math.round((completedCount / tasks.length) * 100) : 0;
-  const preview = nextManifest.preview;
-  const previewRunning = preview?.status === 'running' && preview.url;
-  const previewSummary = previewRunning
-    ? `运行中 ${preview.url}`
-    : preview
-      ? previewStatusLabels[preview.status]
-      : '未启动';
-  const commandRuns = nextManifest.commandRuns ?? [];
-  const staticSmokePassed = commandRuns.some(
-    (commandRun) =>
-      commandRun.commandId === 'game.static_smoke' &&
-      commandRun.status === 'completed',
-  );
-  const exported = commandRuns.some(
-    (commandRun) =>
-      commandRun.commandId === 'project.export_package' &&
-      commandRun.status === 'completed',
-  );
-  const visualAssetCount =
-    nextManifest.assets.filter(isProjectVisualAsset).length;
-  const audioAssetCount =
-    nextManifest.assets.filter(isProjectAudioAsset).length;
-  const tracePassed = isAgentRunTracePassed(trace);
-  const blockedTrace =
-    trace?.lifecycleStatus === 'killed' ||
-    trace?.status === 'failed' ||
-    trace?.status === 'needs-revision' ||
-    trace?.stopReason === 'max-passes-exhausted';
-
-  let phase = '准备生成';
-  let draftCommand = '/guide';
-  let draftCommandLabel = '查看导引';
-
-  if (blockedTrace) {
-    phase = '需修复';
-    draftCommand = '/review';
-    draftCommandLabel = '查看评审';
-  } else if (exported) {
-    phase = '已导出';
-    draftCommand = '/share';
-    draftCommandLabel = '准备交付';
-  } else if (previewRunning) {
-    phase = '试玩中';
-    draftCommand = '/test-plan';
-    draftCommandLabel = '准备测试';
-  } else if (tracePassed) {
-    phase = '已生成';
-    draftCommand = '/run';
-    draftCommandLabel = '启动预览';
-  } else if (trace) {
-    phase = '生成中/待验收';
-    draftCommand = readyCount > 0 ? '/todo' : '/trace';
-    draftCommandLabel = readyCount > 0 ? '查看小步' : '查看 trace';
-  }
-
-  return {
-    text: [
-      '项目进度:',
-      `- 项目:${nextManifest.name}`,
-      `- 当前阶段:${phase}`,
-      `- 任务完成度:${completedCount}/${tasks.length} · ${progressPercent}% · ready ${readyCount} · 失败 ${failedCount}`,
-      trace
-        ? `- 最近 Run:${trace.runId} · ${formatAgentRunStatus(trace)}`
-        : '- 最近 Run:暂无',
-      `- 预览:${previewSummary}`,
-      `- 素材:共 ${nextManifest.assets.length} 个 · 美术 ${visualAssetCount} · 音频 ${audioAssetCount}`,
-      `- 交付:自检 ${staticSmokePassed ? '已通过' : '未通过'} · 试玩包 ${exported ? '已导出' : '未导出'}`,
-      '- 边界:只整理项目进度;不读取文件;不启动 run;不启动预览;不导出试玩包;不写项目',
-      `- 建议:${draftCommand}`,
-    ].join('\n'),
-    draftCommand,
-    draftCommandLabel,
-  };
-}
-
-export function summarizeProjectMvpScope(
-  nextManifest: GameCreationAppManifest,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  const tasks = taskRowsFromManifest(nextManifest);
-  const completedCount = tasks.filter(
-    (task) => task.status === 'completed',
-  ).length;
-  const failedCount = tasks.filter((task) => task.status === 'failed').length;
-  const readyCount = selectGameCreationAppReadyTasks({ tasks }).length;
-  const preview = nextManifest.preview;
-  const previewRunning = preview?.status === 'running' && preview.url;
-  const previewSummary = previewRunning
-    ? `运行中 ${preview.url}`
-    : preview
-      ? previewStatusLabels[preview.status]
-      : '未启动';
-  const tracePassed = isAgentRunTracePassed(trace);
-  const blockedTrace =
-    trace?.lifecycleStatus === 'killed' ||
-    trace?.status === 'failed' ||
-    trace?.status === 'needs-revision' ||
-    trace?.stopReason === 'max-passes-exhausted';
-  const exported = (nextManifest.commandRuns ?? []).some(
-    (commandRun) =>
-      commandRun.commandId === 'project.export_package' &&
-      commandRun.status === 'completed',
-  );
-  const goal =
-    nextManifest.goal?.trim() ||
-    trace?.goal?.trim() ||
-    trace?.taskGraph.goal?.trim() ||
-    '暂无';
-
-  let draftCommand = '/next';
-  let draftCommandLabel = '查看下一步';
-  if (blockedTrace) {
-    draftCommand = '/review';
-    draftCommandLabel = '查看评审';
-  } else if (trace && !tracePassed) {
-    draftCommand = readyCount > 0 ? '/criteria' : '/trace';
-    draftCommandLabel = readyCount > 0 ? '查看验收' : '查看 trace';
-  } else if (tracePassed && !previewRunning) {
-    draftCommand = '/run';
-    draftCommandLabel = '启动预览';
-  } else if (tracePassed && !exported) {
-    draftCommand = '/export';
-    draftCommandLabel = '导出试玩包';
-  } else if (exported) {
-    draftCommand = '/exports';
-    draftCommandLabel = '查看试玩包';
-  }
-
-  return {
-    text: [
-      'MVP 范围:',
-      `- 项目:${nextManifest.name}`,
-      `- 目标:${goal}`,
-      `- MVP 内:可运行 Web 原型;基础输入 / 胜负 / 重开;本地预览;本地试玩包`,
-      trace
-        ? `- 当前状态:最近 run ${trace.runId} · ${formatAgentRunStatus(trace)}`
-        : '- 当前状态:暂无最近 run',
-      `- 任务:完成 ${completedCount}/${tasks.length} · ready ${readyCount} · 失败 ${failedCount}`,
-      `- 预览:${previewSummary}`,
-      `- 资产:${nextManifest.assets.length} 个`,
-      `- 试玩包:${exported ? '已导出' : tracePassed ? '待导出' : '待原型通过'}`,
-      '- 先不做:云同步;Unity/Godot;插件市场;任意 shell;深度资产精修',
-      `- 建议:${draftCommand}`,
-    ].join('\n'),
-    draftCommand,
-    draftCommandLabel,
-  };
-}
-
-export function summarizeProjectPitch(
-  nextManifest: GameCreationAppManifest,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  const preview = nextManifest.preview;
-  const previewRunning = preview?.status === 'running' && preview.url;
-  const previewSummary = previewRunning
-    ? `运行中 ${preview.url}`
-    : preview
-      ? previewStatusLabels[preview.status]
-      : '未启动';
-  const tracePassed = isAgentRunTracePassed(trace);
-  const blockedTrace =
-    trace?.lifecycleStatus === 'killed' ||
-    trace?.status === 'failed' ||
-    trace?.status === 'needs-revision' ||
-    trace?.stopReason === 'max-passes-exhausted';
-  const goal =
-    nextManifest.goal?.trim() ||
-    trace?.goal?.trim() ||
-    trace?.taskGraph.goal?.trim() ||
-    '暂无';
-
-  let draftCommand = '/mvp';
-  let draftCommandLabel = '查看 MVP 范围';
-  if (blockedTrace) {
-    draftCommand = '/review';
-    draftCommandLabel = '查看评审';
-  } else if (!trace) {
-    draftCommand = '/mvp';
-    draftCommandLabel = '查看 MVP 范围';
-  } else if (!tracePassed) {
-    draftCommand = '/trace';
-    draftCommandLabel = '查看 trace';
-  } else if (previewRunning) {
-    draftCommand = '/open-preview';
-    draftCommandLabel = '打开预览';
-  } else {
-    draftCommand = '/run';
-    draftCommandLabel = '启动预览';
-  }
-
-  return {
-    text: [
-      '试玩定位:',
-      `- 项目:${nextManifest.name}`,
-      `- 一句话:${goal}`,
-      '- 核心乐趣:快速验证目标、操作反馈、胜负结果和重开节奏',
-      `- 当前可演示:${
-        tracePassed
-          ? `最近 run 已通过${trace?.runId ? ` ${trace.runId}` : ''}`
-          : trace
-            ? `最近 run 未通过 ${trace.status} / ${trace.stopReason}`
-            : '暂无最近 run'
-      };预览 ${previewSummary}`,
-      '- 讲给测试者:先说明目标,再说明操作,然后看 30 秒内是否能理解胜负和重开',
-      '- 不承诺:云发布;深度美术精修;账号体系;排行榜;长期运营包装',
-      '- 参考:/mvp;/rules;/playtest;/listing',
-      `- 建议:${draftCommand}`,
-    ].join('\n'),
-    draftCommand,
-    draftCommandLabel,
-  };
-}
-
-export function summarizeProjectDemoScript(
-  nextManifest: GameCreationAppManifest,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  const preview = nextManifest.preview;
-  const previewRunning = preview?.status === 'running' && preview.url;
-  const previewSummary = previewRunning
-    ? `运行中 ${preview.url}`
-    : preview
-      ? previewStatusLabels[preview.status]
-      : '未启动';
-  const tracePassed = isAgentRunTracePassed(trace);
-  const blockedTrace =
-    trace?.lifecycleStatus === 'killed' ||
-    trace?.status === 'failed' ||
-    trace?.status === 'needs-revision' ||
-    trace?.stopReason === 'max-passes-exhausted';
-  const goal =
-    nextManifest.goal?.trim() ||
-    trace?.goal?.trim() ||
-    trace?.taskGraph.goal?.trim() ||
-    '暂无';
-  const latestPlaytestStep =
-    trace?.steps.filter(isPlaytestTraceStep).slice(-1)[0] ?? null;
-
-  let draftCommand = '/test-plan';
-  let draftCommandLabel = '准备测试计划';
-  if (blockedTrace) {
-    draftCommand = '/review';
-    draftCommandLabel = '查看评审';
-  } else if (trace && !tracePassed) {
-    draftCommand = '/trace';
-    draftCommandLabel = '查看 trace';
-  } else if (previewRunning) {
-    draftCommand = '/open-preview';
-    draftCommandLabel = '打开预览';
-  } else if (tracePassed) {
-    draftCommand = '/run';
-    draftCommandLabel = '启动预览';
-  }
-
-  return {
-    text: [
-      '试玩讲解稿:',
-      `- 项目:${nextManifest.name}`,
-      `- 30 秒开场:这是《${nextManifest.name}》,目标是${goal}`,
-      '- 讲解顺序:目标 -> 操作 -> 反馈 -> 胜负 -> 重开',
-      '- 口播稿:先看目标提示,尝试移动/点击完成核心动作;看到得分、受击或状态反馈后,继续到胜利或失败;结束后确认能否一键重开',
-      `- 当前演示状态:${
-        tracePassed
-          ? `最近 run 已通过${trace?.runId ? ` ${trace.runId}` : ''}`
-          : trace
-            ? `最近 run 未通过 ${trace.status} / ${trace.stopReason}`
-            : '暂无最近 run'
-      };预览 ${previewSummary}`,
-      `- 最近试玩证据:${
-        latestPlaytestStep
-          ? `${latestPlaytestStep.agent} #${latestPlaytestStep.pass} · ${latestPlaytestStep.status} · ${latestPlaytestStep.summary}`
-          : '暂无'
-      }`,
-      '- 收反馈:操作是否明白;节奏是否太快;胜负是否清楚;视觉 / 音效是否帮助理解',
-      '- 边界:只准备试玩讲解;不启动预览;不导出试玩包;不发布作品',
-      '- 参考:/rules;/test-plan;/feedback;/share',
-      `- 建议:${draftCommand}`,
-    ].join('\n'),
-    draftCommand,
-    draftCommandLabel,
-  };
-}
-
-export function summarizeProjectControlGuide(
-  nextManifest: GameCreationAppManifest,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  const manifestTasks = taskRowsFromManifest(nextManifest);
-  const traceTasks = trace?.taskGraph.tasks ?? [];
-  const tasks = manifestTasks.map(
-    (task) => traceTasks.find((traceTask) => traceTask.id === task.id) ?? task,
-  );
-  const tasksById = new Map(tasks.map((task) => [task.id, task]));
-  const readyTaskIds = new Set(
-    trace
-      ? trace.taskGraph.readyTaskIds
-      : selectGameCreationAppReadyTasks({ tasks }).map((task) => task.id),
-  );
-  const activeTaskIds = new Set(trace?.taskGraph.activeTaskIds ?? []);
-  const carriedTaskIds = new Set(trace?.taskGraph.carriedTaskIds ?? []);
-  const formatTaskLine = (taskId: string) => {
-    const task = tasksById.get(taskId);
-    if (!task) {
-      return null;
-    }
-    const markers = [taskStatusLabels[task.status]];
-    if (readyTaskIds.has(task.id)) {
-      markers.push('ready');
-    }
-    if (activeTaskIds.has(task.id)) {
-      markers.push('active');
-    }
-    if (carriedTaskIds.has(task.id)) {
-      markers.push('carry');
-    }
-    return `- ${task.id}:${taskGroupLabels[task.group]} / ${task.role} ${task.title} · ${markers.join(' / ')}`;
-  };
-  const taskLines = [
-    'design-foundation',
-    'code-prototype',
-    'preview-readiness',
-    'preview-playtest',
-  ]
-    .map(formatTaskLine)
-    .filter(Boolean);
-  const hasDesignArtifact =
-    trace?.artifacts.some(
-      (artifact) => artifact.path === 'game/game_design.md',
-    ) ?? false;
-  const hasGameArtifact =
-    trace?.artifacts.some(
-      (artifact) =>
-        artifact.path === 'game/index.html' || artifact.path === 'game/',
-    ) ?? false;
-  const latestControlStep =
-    trace?.steps
-      .filter(
-        (step) =>
-          step.taskId === 'code-prototype' ||
-          step.taskId === 'preview-readiness' ||
-          step.taskId === 'preview-playtest' ||
-          step.group === 'code' ||
-          step.phase === 'generate' ||
-          step.phase === 'playtest' ||
-          step.toolCalls.some(
-            (toolCall) =>
-              toolCall.toolId === 'game.static_smoke' ||
-              toolCall.toolId.startsWith('preview.'),
-          ),
-      )
-      .slice(-1)[0] ?? null;
-  const draftCommand = hasDesignArtifact
-    ? '/read game/game_design.md'
-    : trace
-      ? '/agent-resume 操作说明:在首屏明确移动/点击操作、胜负目标、失败后重开方式'
-      : '/next';
-
-  return {
-    text: [
-      '玩法操作:',
-      `- 项目:${nextManifest.name}`,
-      `- 目标:${nextManifest.goal ?? trace?.goal ?? trace?.taskGraph.goal ?? '暂无'}`,
-      '- 核心口径:目标;操作;胜负;重开;本地预览',
-      `- 规则来源:game/game_design.md · ${hasDesignArtifact ? '已生成' : '未见 trace 产物'}`,
-      `- 原型入口:game/index.html · ${hasGameArtifact ? '已生成' : '未见 trace 产物'}`,
-      taskLines.length > 0
-        ? `- 任务状态:\n${taskLines.join('\n')}`
-        : '- 任务状态:暂无',
-      `- 最近程序/试玩步骤:${
-        latestControlStep
-          ? `${latestControlStep.agent} #${latestControlStep.pass} · ${latestControlStep.status} · ${latestControlStep.summary}`
-          : '暂无'
-      }`,
-      '- 相关命令:/mvp;/playtest;/feedback',
-      `- 建议:${draftCommand}`,
-    ].join('\n'),
-    draftCommand,
-    draftCommandLabel: hasDesignArtifact
-      ? '读取玩法设计'
-      : trace
-        ? '补充操作说明'
-        : '查看下一步',
-  };
-}
diff --git a/apps/ai-game-creator-shell/src/features/project-summary/projectPath.ts b/apps/ai-game-creator-shell/src/features/project-summary/projectPath.ts
index d8a54f66a..5808f0235 100644
--- a/apps/ai-game-creator-shell/src/features/project-summary/projectPath.ts
+++ b/apps/ai-game-creator-shell/src/features/project-summary/projectPath.ts
@@ -37,15 +37,3 @@ export function projectPathsMatchForInvalidation(
   }
   return normalize(eventPath) === normalize(activePath);
 }
-
-export function isSafeProjectRelativePath(value: string) {
-  const path = value.trim();
-  return (
-    !!path &&
-    !isAbsoluteProjectPath(path) &&
-    !projectPathHasControlCharacter(path) &&
-    !path.includes('\\') &&
-    !path.includes(':') &&
-    path.split('/').every((part) => part && part !== '.' && part !== '..')
-  );
-}
diff --git a/apps/ai-game-creator-shell/src/features/project-summary/projectPlanningSummaries.ts b/apps/ai-game-creator-shell/src/features/project-summary/projectPlanningSummaries.ts
deleted file mode 100644
index 02e69c3d5..000000000
--- a/apps/ai-game-creator-shell/src/features/project-summary/projectPlanningSummaries.ts
+++ /dev/null
@@ -1,463 +0,0 @@
-import {
-  type GameCreationAgentRunTrace,
-  type GameCreationAppAssetSourceKind,
-  type GameCreationAppManifest,
-  type GameCreationAppTaskState,
-  selectGameCreationAppReadyTasks,
-} from '../../../../../packages/shared/src/contracts/gameCreationApp';
-import { taskRowsFromManifest } from '../agent-runtime';
-import {
-  formatAgentRunStatus,
-  formatTraceTaskId,
-  isAgentReviewStep,
-  isAgentRunTracePassed,
-  isPlaytestTraceStep,
-  readableArtifactsFromAgentRunTrace,
-} from './agentTrace';
-import {
-  isProjectAudioAsset,
-  isProjectVisualAsset,
-} from './projectAssetSummaries';
-import {
-  assetSourceKindLabels,
-  previewStatusLabels,
-} from './projectSummaryConstants';
-
-export function summarizeProjectEvidenceLedger(
-  nextManifest: GameCreationAppManifest,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  const commandRuns = nextManifest.commandRuns ?? [];
-  const preview = nextManifest.preview;
-  const previewRunning = preview?.status === 'running' && preview.url;
-  const previewSummary = previewRunning
-    ? `运行中 ${preview.url}`
-    : preview
-      ? previewStatusLabels[preview.status]
-      : '未启动';
-  const tracePassed = isAgentRunTracePassed(trace);
-  const staticSmokePassed =
-    commandRuns.some(
-      (commandRun) =>
-        commandRun.commandId === 'game.static_smoke' &&
-        commandRun.status === 'completed',
-    ) ||
-    Boolean(
-      trace?.steps.some((step) =>
-        step.toolCalls.some(
-          (toolCall) =>
-            toolCall.toolId === 'game.static_smoke' && toolCall.status === 'ok',
-        ),
-      ),
-    );
-  const latestExportCommand =
-    [...commandRuns]
-      .reverse()
-      .find(
-        (commandRun) => commandRun.commandId === 'project.export_package',
-      ) ?? null;
-  const latestFailedCommand =
-    [...commandRuns]
-      .reverse()
-      .find((commandRun) => commandRun.status === 'failed') ?? null;
-  const latestReviewStep =
-    trace?.steps.filter(isAgentReviewStep).slice(-1)[0] ?? null;
-  const latestPlaytestStep =
-    trace?.steps.filter(isPlaytestTraceStep).slice(-1)[0] ?? null;
-  const readableArtifacts = trace
-    ? readableArtifactsFromAgentRunTrace(trace)
-    : [];
-  const hasGameEntry =
-    trace?.artifacts.some((artifact) => artifact.path === 'game/index.html') ??
-    false;
-  const hasVisualAsset = nextManifest.assets.some(isProjectVisualAsset);
-  const hasAudioAsset = nextManifest.assets.some(isProjectAudioAsset);
-  const evidenceLines = [
-    trace
-      ? `- Run trace:已有 ${trace.runId} · ${formatAgentRunStatus(trace)}`
-      : '- Run trace:缺失',
-    `- Evaluator:${
-      tracePassed
-        ? '通过'
-        : trace
-          ? `未通过 ${trace.status} / ${trace.stopReason}`
-          : '缺失'
-    }${latestReviewStep ? ` · ${latestReviewStep.summary}` : ''}`,
-    `- 静态自检:${staticSmokePassed ? '已有通过证据' : '缺失通过证据'}`,
-    `- 预览:${previewSummary}`,
-    `- 试玩包:${
-      latestExportCommand?.status === 'completed'
-        ? '已有导出记录'
-        : latestExportCommand?.status === 'failed'
-          ? '最近导出失败'
-          : '缺失'
-    }`,
-    `- 入口产物:${hasGameEntry ? 'game/index.html 已在 trace 产物中' : '缺失 trace 产物证据'}`,
-    `- 资产:${nextManifest.assets.length} 个 · 美术 ${
-      hasVisualAsset ? '有' : '缺'
-    } · 音频 ${hasAudioAsset ? '有' : '可后补'}`,
-    `- 可读产物:${readableArtifacts.length} 个`,
-    latestPlaytestStep
-      ? `- 最近试玩:${latestPlaytestStep.agent} #${latestPlaytestStep.pass} · ${latestPlaytestStep.status} · ${latestPlaytestStep.summary}`
-      : '- 最近试玩:暂无',
-    latestFailedCommand
-      ? `- 最近失败命令:${latestFailedCommand.commandId}`
-      : '- 最近失败命令:暂无',
-  ];
-  const gaps: Array<{ text: string; command: string }> = [];
-  if (!trace) {
-    gaps.push({ text: '缺少最近 run trace', command: '/next' });
-  } else if (!tracePassed) {
-    gaps.push({ text: 'Evaluator 尚未通过', command: '/review' });
-  }
-  if (!staticSmokePassed) {
-    gaps.push({ text: '缺少静态自检通过证据', command: '/run' });
-  }
-  if (!previewRunning) {
-    gaps.push({ text: '本地预览未运行', command: '/run' });
-  }
-  if (tracePassed && latestExportCommand?.status !== 'completed') {
-    gaps.push({ text: '缺少本地试玩包导出记录', command: '/export' });
-  }
-  if (!hasVisualAsset) {
-    gaps.push({ text: '缺少可复用美术素材', command: '/art' });
-  }
-  if (latestFailedCommand) {
-    gaps.push({ text: '存在失败命令需要查看日志', command: '/logs' });
-  }
-  const firstGap = gaps[0] ?? null;
-
-  return {
-    text: [
-      '验证证据台账:',
-      `- 项目:${nextManifest.name}`,
-      ...evidenceLines,
-      gaps.length > 0
-        ? `- 缺口:\n${gaps
-            .map((gap) => `- ${gap.text} · 建议 ${gap.command}`)
-            .join('\n')}`
-        : '- 缺口:暂无关键缺口',
-      '- 边界:只整理当前已加载证据;不读取文件;不启动或打开预览;不导出试玩包;不写项目',
-      `- 建议:${firstGap?.command ?? '/ready'}`,
-    ].join('\n'),
-    draftCommand: firstGap?.command ?? '/ready',
-    draftCommandLabel: firstGap ? '补齐首个证据缺口' : '查看就绪度',
-  };
-}
-
-export function summarizeProjectDependencyMap(
-  nextManifest: GameCreationAppManifest,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  const manifestTasks = taskRowsFromManifest(nextManifest);
-  const traceTasks = trace?.taskGraph.tasks ?? [];
-  const taskSource = traceTasks.length > 0 ? traceTasks : manifestTasks;
-  const manifestTasksById = new Map(
-    manifestTasks.map((task) => [task.id, task]),
-  );
-  const taskSourceById = new Map(taskSource.map((task) => [task.id, task]));
-  const resolvedTask = (taskId: string) =>
-    taskSourceById.get(taskId) ?? manifestTasksById.get(taskId) ?? null;
-  const completedTaskIds = new Set(
-    taskSource
-      .filter((task) => task.status === 'completed')
-      .map((task) => task.id),
-  );
-  const readyTaskIds =
-    trace?.taskGraph.readyTaskIds ??
-    selectGameCreationAppReadyTasks({ tasks: manifestTasks }).map(
-      (task) => task.id,
-    );
-  const activeTaskIds = trace?.taskGraph.activeTaskIds ?? [];
-  const carriedTaskIds = trace?.taskGraph.carriedTaskIds ?? [];
-  const blockedTasks = taskSource
-    .filter((task) => task.status !== 'completed')
-    .map((task) => ({
-      task,
-      missingDependencies: task.dependencies.filter(
-        (dependencyId) => !completedTaskIds.has(dependencyId),
-      ),
-    }))
-    .filter((entry) => entry.missingDependencies.length > 0);
-  const readyLines = readyTaskIds
-    .map((taskId) => resolvedTask(taskId))
-    .filter((task): task is GameCreationAppTaskState => Boolean(task))
-    .slice(0, 4)
-    .map((task) => {
-      const dependencies =
-        task.dependencies.length > 0
-          ? task.dependencies
-              .map((dependencyId) =>
-                formatTraceTaskId(dependencyId, taskSource),
-              )
-              .join(';')
-          : '无';
-      return `- ${formatTraceTaskId(task.id, taskSource)} · 依赖:${dependencies}`;
-    });
-  const blockedLines = blockedTasks.slice(0, 5).map((entry) => {
-    const missing = entry.missingDependencies
-      .map((dependencyId) => formatTraceTaskId(dependencyId, taskSource))
-      .join(';');
-    return `- ${formatTraceTaskId(entry.task.id, taskSource)} · 等待:${missing}`;
-  });
-  if (blockedTasks.length > blockedLines.length) {
-    blockedLines.push(
-      `- 还有 ${blockedTasks.length - blockedLines.length} 个等待依赖的任务`,
-    );
-  }
-
-  let draftCommand = '/tasks';
-  let draftCommandLabel = '查看任务';
-  if (activeTaskIds.length > 0 || carriedTaskIds.length > 0) {
-    draftCommand = '/todo';
-    draftCommandLabel = '查看小步清单';
-  } else if (readyTaskIds.length > 0) {
-    draftCommand = '/criteria';
-    draftCommandLabel = '查看验收标准';
-  } else if (blockedTasks.length === 0) {
-    draftCommand = '/next';
-    draftCommandLabel = '查看下一步';
-  }
-
-  return {
-    text: [
-      '任务依赖链:',
-      trace ? `- Run:${trace.runId} · ${formatAgentRunStatus(trace)}` : null,
-      `- 状态:active ${activeTaskIds.length} / carry ${carriedTaskIds.length} / ready ${readyTaskIds.length} / 等待依赖 ${blockedTasks.length}`,
-      readyLines.length > 0
-        ? `- 可执行任务:\n${readyLines.join('\n')}`
-        : '- 可执行任务:暂无',
-      blockedLines.length > 0
-        ? `- 依赖等待:\n${blockedLines.join('\n')}`
-        : '- 依赖等待:暂无',
-      '- 边界:只整理任务依赖;不读取任务文件;不启动 run;不修改项目',
-      `- 建议:${draftCommand}`,
-    ]
-      .filter(Boolean)
-      .join('\n'),
-    draftCommand,
-    draftCommandLabel,
-  };
-}
-
-export function summarizeProjectRevisionDraft(
-  nextManifest: GameCreationAppManifest,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  const manifestTasks = taskRowsFromManifest(nextManifest);
-  const traceTasks = trace?.taskGraph.tasks ?? [];
-  const taskSource = traceTasks.length > 0 ? traceTasks : manifestTasks;
-  const manifestTasksById = new Map(
-    manifestTasks.map((task) => [task.id, task]),
-  );
-  const taskSourceById = new Map(taskSource.map((task) => [task.id, task]));
-  const resolvedTask = (taskId: string) =>
-    taskSourceById.get(taskId) ?? manifestTasksById.get(taskId) ?? null;
-  const failedTasks = taskSource.filter((task) => task.status === 'failed');
-  const activeTasks =
-    trace?.taskGraph.activeTaskIds
-      .map(resolvedTask)
-      .filter((task): task is GameCreationAppTaskState => Boolean(task)) ?? [];
-  const carriedTasks =
-    trace?.taskGraph.carriedTaskIds
-      .map(resolvedTask)
-      .filter((task): task is GameCreationAppTaskState => Boolean(task)) ?? [];
-  const readyTasks =
-    trace?.taskGraph.readyTaskIds
-      .map(resolvedTask)
-      .filter((task): task is GameCreationAppTaskState => Boolean(task)) ??
-    selectGameCreationAppReadyTasks({ tasks: manifestTasks });
-  const commandRuns = nextManifest.commandRuns ?? [];
-  const latestExportCommand =
-    [...commandRuns]
-      .reverse()
-      .find(
-        (commandRun) => commandRun.commandId === 'project.export_package',
-      ) ?? null;
-  const preview = nextManifest.preview;
-  const previewRunning = preview?.status === 'running' && preview.url;
-  const tracePassed = isAgentRunTracePassed(trace);
-  const blockedTrace =
-    trace?.lifecycleStatus === 'killed' ||
-    trace?.status === 'failed' ||
-    trace?.status === 'needs-revision' ||
-    trace?.stopReason === 'max-passes-exhausted';
-  const latestReviewStep =
-    trace?.steps.filter(isAgentReviewStep).slice(-1)[0] ?? null;
-  const latestPlaytestStep =
-    trace?.steps.filter(isPlaytestTraceStep).slice(-1)[0] ?? null;
-  const revisionItems: string[] = [];
-  const addRevisionItem = (item: string) => {
-    if (!revisionItems.includes(item)) {
-      revisionItems.push(item);
-    }
-  };
-
-  if (!trace) {
-    addRevisionItem('明确首版核心玩法、可玩目标和验收口径');
-  } else {
-    if (trace.taskGraph.repairFocus.length > 0) {
-      addRevisionItem(
-        `处理返工焦点:${trace.taskGraph.repairFocus.join(';')}`,
-      );
-    }
-    if (failedTasks.length > 0) {
-      addRevisionItem(
-        `修复失败任务:${formatTraceTaskId(failedTasks[0]!.id, taskSource)}`,
-      );
-    }
-    if (activeTasks.length > 0) {
-      addRevisionItem(
-        `继续 active 任务:${formatTraceTaskId(activeTasks[0]!.id, taskSource)}`,
-      );
-    }
-    if (carriedTasks.length > 0) {
-      addRevisionItem(
-        `承接 carry 任务:${formatTraceTaskId(carriedTasks[0]!.id, taskSource)}`,
-      );
-    }
-    if (readyTasks.length > 0) {
-      addRevisionItem(
-        `推进 ready 任务:${formatTraceTaskId(readyTasks[0]!.id, taskSource)}`,
-      );
-    }
-    if (blockedTrace && latestReviewStep) {
-      addRevisionItem(`按评审修复:${latestReviewStep.summary}`);
-    }
-    if (latestPlaytestStep && latestPlaytestStep.status !== 'completed') {
-      addRevisionItem(`补试玩问题:${latestPlaytestStep.summary}`);
-    }
-    if (tracePassed && !previewRunning) {
-      addRevisionItem('补齐本地试玩:启动预览并验证首屏');
-    }
-    if (tracePassed && latestExportCommand?.status !== 'completed') {
-      addRevisionItem('交付:导出本地试玩包');
-    }
-  }
-
-  if (revisionItems.length === 0) {
-    addRevisionItem('做一轮小步打磨,优先提升可玩性和交付清晰度');
-  }
-
-  const keepItem = tracePassed
-    ? '保留当前已通过的核心玩法和可运行入口'
-    : '保留当前创作目标、已有任务拆分和已生成资产';
-  const adjustItems = revisionItems.slice(0, 2);
-  const addItems: string[] = [];
-  if (tracePassed && !previewRunning) {
-    addItems.push('补一次本地预览验证');
-  }
-  if (tracePassed && latestExportCommand?.status !== 'completed') {
-    addItems.push('补导出本地试玩包');
-  }
-  if (addItems.length === 0) {
-    addItems.push('补清楚下一轮验收证据');
-  }
-  const acceptanceItem = '通过 /ready、/qa 和 /changes 复查';
-  const draftCommand = `/agent-resume 改版说明:保留${keepItem};调整${adjustItems.join(
-    ';',
-  )};新增${addItems.join(';')};验收${acceptanceItem}`;
-
-  return {
-    text: [
-      '改版草稿:',
-      `- 项目:${nextManifest.name}`,
-      trace ? `- Run:${trace.runId} · ${formatAgentRunStatus(trace)}` : null,
-      latestReviewStep
-        ? `- 最近评审:${latestReviewStep.agent} #${latestReviewStep.pass} · ${latestReviewStep.status} · ${latestReviewStep.summary}`
-        : '- 最近评审:暂无',
-      latestPlaytestStep
-        ? `- 最近试玩:${latestPlaytestStep.agent} #${latestPlaytestStep.pass} · ${latestPlaytestStep.status} · ${latestPlaytestStep.summary}`
-        : '- 最近试玩:暂无',
-      `- 保留项:${keepItem}`,
-      `- 调整项:${adjustItems.join(';')}`,
-      `- 新增项:${addItems.join(';')}`,
-      `- 验收口径:${acceptanceItem}`,
-      `- 优先依据:\n${revisionItems.map((item) => `- ${item}`).join('\n')}`,
-      '- 参考命令:/ready;/deps;/qa;/changes',
-      `- 草稿:${draftCommand}`,
-      '- 边界:只准备改版说明;不继续 run;不读取文件;不启动预览;不导出试玩包;不写项目',
-      `- 建议:${draftCommand}`,
-    ]
-      .filter(Boolean)
-      .join('\n'),
-    draftCommand,
-    draftCommandLabel: '填入改版说明',
-  };
-}
-
-export function summarizeProjectPrivacyBoundary(
-  nextManifest: GameCreationAppManifest,
-  projectPath: string,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  const sourceCounts = nextManifest.assets.reduce(
-    (counts, asset) => {
-      counts[asset.source.kind] += 1;
-      return counts;
-    },
-    {
-      uploaded: 0,
-      generated: 0,
-      canvas: 0,
-    } satisfies Record,
-  );
-  const sourceSummary =
-    (Object.keys(sourceCounts) as GameCreationAppAssetSourceKind[])
-      .filter((source) => sourceCounts[source] > 0)
-      .map(
-        (source) => `${assetSourceKindLabels[source]} ${sourceCounts[source]}`,
-      )
-      .join(' / ') || '暂无';
-  const commandRuns = nextManifest.commandRuns ?? [];
-  const latestExportCommand =
-    [...commandRuns]
-      .reverse()
-      .find(
-        (commandRun) => commandRun.commandId === 'project.export_package',
-      ) ?? null;
-  const preview = nextManifest.preview;
-  const previewRunning = preview?.status === 'running' && preview.url;
-  const previewSummary = previewRunning
-    ? `本机预览 ${preview.url}`
-    : preview
-      ? previewStatusLabels[preview.status]
-      : '未启动';
-  const draftCommand =
-    latestExportCommand?.status === 'completed'
-      ? '/exports'
-      : nextManifest.assets.length > 0
-        ? '/credits'
-        : '/config';
-  const draftCommandLabel =
-    draftCommand === '/exports'
-      ? '查看试玩包'
-      : draftCommand === '/credits'
-        ? '查看素材来源'
-        : '打开配置';
-
-  return {
-    text: [
-      '隐私与导出边界:',
-      `- 项目:${nextManifest.name}`,
-      `- 本地目录:${projectPath}`,
-      '- 智能服务凭据由服务端按登录账号管理;不进入客户端、manifest、trace、聊天、导出包或项目文件',
-      `- 本地预览:${previewSummary};仅限 127.0.0.1 本机访问`,
-      `- 试玩包:${
-        latestExportCommand?.status === 'completed' ? '最近已导出' : '尚未导出'
-      };只应包含 game/**、assets/** 和 exports/README.md`,
-      `- 内部文件:.agent/**、memory/**、日志、trace、配置和密钥不得进入试玩包`,
-      `- 素材来源:${nextManifest.assets.length} 个;${sourceSummary}`,
-      `- Trace:${
-        trace
-          ? `${trace.runId} · ${trace.artifacts.length} 个内部产物记录`
-          : '暂无最近 run'
-      };只通过 /trace 或 /internals 查看,不作为交付内容`,
-      '- 交付前建议:/credits;/ready;/export;/exports',
-      '- 边界:只整理隐私与交付口径;不读取文件;不导出;不启动预览;不写项目',
-      `- 建议:${draftCommand}`,
-    ].join('\n'),
-    draftCommand,
-    draftCommandLabel,
-  };
-}
diff --git a/apps/ai-game-creator-shell/src/features/project-summary/projectPlaytestSummaries.ts b/apps/ai-game-creator-shell/src/features/project-summary/projectPlaytestSummaries.ts
deleted file mode 100644
index 53c928a77..000000000
--- a/apps/ai-game-creator-shell/src/features/project-summary/projectPlaytestSummaries.ts
+++ /dev/null
@@ -1,786 +0,0 @@
-import {
-  type GameCreationAgentRunTrace,
-  type GameCreationAppManifest,
-  type GameCreationAppTaskState,
-} from '../../../../../packages/shared/src/contracts/gameCreationApp';
-import { taskRowsFromManifest } from '../agent-runtime';
-import {
-  formatAgentRunStatus,
-  isAgentRunTracePassed,
-  isPlaytestTraceStep,
-} from './agentTrace';
-import {
-  isProjectAudioAsset,
-  isProjectVisualAsset,
-} from './projectAssetSummaries';
-import {
-  assetSourceKindLabels,
-  previewStatusLabels,
-  taskGroupLabels,
-  taskStatusLabels,
-} from './projectSummaryConstants';
-
-export function summarizeProjectAudienceGuide(
-  nextManifest: GameCreationAppManifest,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  const manifestTasks = taskRowsFromManifest(nextManifest);
-  const traceTasks = trace?.taskGraph.tasks ?? [];
-  const taskSource = traceTasks.length > 0 ? traceTasks : manifestTasks;
-  const previewReadinessTask = taskSource.find(
-    (task) => task.id === 'preview-readiness',
-  );
-  const previewPlaytestTask = taskSource.find(
-    (task) => task.id === 'preview-playtest',
-  );
-  const previewTaskLines = [previewReadinessTask, previewPlaytestTask]
-    .filter((task): task is GameCreationAppTaskState => Boolean(task))
-    .map(
-      (task) =>
-        `- ${task.id}:${taskGroupLabels[task.group]} / ${task.role} ${task.title} · ${taskStatusLabels[task.status]}`,
-    );
-  const preview = nextManifest.preview;
-  const previewRunning = preview?.status === 'running' && preview.url;
-  const previewSummary = previewRunning
-    ? `运行中 ${preview.url}`
-    : preview
-      ? previewStatusLabels[preview.status]
-      : '未启动';
-  const tracePassed = isAgentRunTracePassed(trace);
-  const blockedTrace =
-    trace?.lifecycleStatus === 'killed' ||
-    trace?.status === 'failed' ||
-    trace?.status === 'needs-revision' ||
-    trace?.stopReason === 'max-passes-exhausted';
-  const goal =
-    nextManifest.goal?.trim() ||
-    trace?.goal?.trim() ||
-    trace?.taskGraph.goal?.trim() ||
-    '暂无';
-  const latestPlaytestStep =
-    trace?.steps.filter(isPlaytestTraceStep).slice(-1)[0] ?? null;
-
-  let draftCommand = '/feedback';
-  let draftCommandLabel = '准备反馈';
-  if (!trace) {
-    draftCommand = '/next';
-    draftCommandLabel = '查看下一步';
-  } else if (blockedTrace) {
-    draftCommand = '/review';
-    draftCommandLabel = '查看评审';
-  } else if (!tracePassed) {
-    draftCommand = '/trace';
-    draftCommandLabel = '查看 trace';
-  } else if (!previewRunning) {
-    draftCommand = '/run';
-    draftCommandLabel = '启动试玩';
-  }
-
-  return {
-    text: [
-      '首批试玩对象:',
-      `- 项目:${nextManifest.name}`,
-      `- 目标:${goal}`,
-      `- 当前状态:${
-        tracePassed
-          ? `最近 run 已通过${trace?.runId ? ` ${trace.runId}` : ''}`
-          : trace
-            ? `最近 run 未通过 ${trace.status} / ${trace.stopReason}`
-            : '暂无最近 run'
-      };预览 ${previewSummary}`,
-      '- 先测人群:创作者自测 1 轮;熟悉目标的同事 1-2 人;完全没看过项目的人 3-5 人;至少 1 位移动/触屏用户',
-      '- 第一批测试者:3-5 人;每人 5-10 分钟;先看能否独立理解',
-      '- 观察重点:30 秒能否理解目标;输入是否顺;胜负/重开是否明确;难度是否过早劝退;视觉/音效是否干扰',
-      previewTaskLines.length > 0
-        ? `- 试玩任务:\n${previewTaskLines.join('\n')}`
-        : '- 试玩任务:暂无',
-      `- 最近试玩证据:${
-        latestPlaytestStep
-          ? `${latestPlaytestStep.agent} #${latestPlaytestStep.pass} · ${latestPlaytestStep.status} · ${latestPlaytestStep.summary}`
-          : '暂无'
-      }`,
-      '- 暂不面向:公开发布、付费用户、大规模投放、儿童/无障碍等强承诺场景',
-      '- 参考:/playtest;/test-plan;/feedback;/share',
-      '- 边界:只整理首批试玩对象;不读取文件;不启动或打开预览;不导出试玩包;不写项目',
-      `- 建议:${draftCommand}`,
-    ].join('\n'),
-    draftCommand,
-    draftCommandLabel,
-  };
-}
-
-export function summarizeProjectPlaytestInvite(
-  nextManifest: GameCreationAppManifest,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  const preview = nextManifest.preview;
-  const previewRunning = preview?.status === 'running' && preview.url;
-  const previewSummary = previewRunning
-    ? `运行中 ${preview.url}`
-    : preview
-      ? previewStatusLabels[preview.status]
-      : '未启动';
-  const tracePassed = isAgentRunTracePassed(trace);
-  const blockedTrace =
-    trace?.lifecycleStatus === 'killed' ||
-    trace?.status === 'failed' ||
-    trace?.status === 'needs-revision' ||
-    trace?.stopReason === 'max-passes-exhausted';
-  const goal =
-    nextManifest.goal?.trim() ||
-    trace?.goal?.trim() ||
-    trace?.taskGraph.goal?.trim() ||
-    '暂无';
-
-  let draftCommand = '/feedback';
-  let draftCommandLabel = '准备反馈';
-  if (!trace) {
-    draftCommand = '/next';
-    draftCommandLabel = '查看下一步';
-  } else if (blockedTrace) {
-    draftCommand = '/review';
-    draftCommandLabel = '查看评审';
-  } else if (!tracePassed) {
-    draftCommand = '/trace';
-    draftCommandLabel = '查看 trace';
-  } else if (!previewRunning) {
-    draftCommand = '/run';
-    draftCommandLabel = '启动试玩';
-  }
-
-  return {
-    text: [
-      '试玩邀请:',
-      `- 项目:${nextManifest.name}`,
-      `- 目标:${goal}`,
-      `- 当前状态:${
-        tracePassed
-          ? `最近 run 已通过${trace?.runId ? ` ${trace.runId}` : ''}`
-          : trace
-            ? `最近 run 未通过 ${trace.status} / ${trace.stopReason}`
-            : '暂无最近 run'
-      };预览 ${previewSummary}`,
-      '- 邀请对象:先发 3-5 人;优先熟人/同事/没看过项目的人;暂不公开发布或大规模投放',
-      '- 邀请文案:我做了一个早期 Web 小游戏原型,想请你花 5-10 分钟试玩。重点不是评价完成度,而是看 30 秒内能否理解目标、操作是否顺、胜负和重开是否清楚。试玩后请反馈:哪里没看懂、哪里卡住、还想不想再来一局。',
-      previewRunning
-        ? '- 发送前:本地预览已运行,可配合 /open-preview'
-        : '- 发送前:先 /run 启动本地预览,再把本地试玩方式发给测试者',
-      '- 收反馈:让测试者按 /feedback 的三类模板回收;需要交付包时再看 /share',
-      '- 参考:/audience;/test-plan;/feedback;/share',
-      '- 边界:只准备邀请文案;不读取文件;不启动或打开预览;不导出试玩包;不写项目',
-      `- 建议:${draftCommand}`,
-    ].join('\n'),
-    draftCommand,
-    draftCommandLabel,
-  };
-}
-
-export function summarizeProjectBugReport(
-  nextManifest: GameCreationAppManifest,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  const preview = nextManifest.preview;
-  const previewRunning = preview?.status === 'running' && preview.url;
-  const previewSummary = previewRunning
-    ? `运行中 ${preview.url}`
-    : preview
-      ? previewStatusLabels[preview.status]
-      : '未启动';
-  const tracePassed = isAgentRunTracePassed(trace);
-  const blockedTrace =
-    trace?.lifecycleStatus === 'killed' ||
-    trace?.status === 'failed' ||
-    trace?.status === 'needs-revision' ||
-    trace?.stopReason === 'max-passes-exhausted';
-  const latestPlaytestStep =
-    trace?.steps.filter(isPlaytestTraceStep).slice(-1)[0] ?? null;
-
-  let draftCommand = '/agent-resume 缺陷修复:';
-  let draftCommandLabel = '填写缺陷修复';
-  if (!trace) {
-    draftCommand = '/next';
-    draftCommandLabel = '查看下一步';
-  } else if (blockedTrace) {
-    draftCommand = '/review';
-    draftCommandLabel = '查看评审';
-  } else if (!tracePassed) {
-    draftCommand = '/trace';
-    draftCommandLabel = '查看 trace';
-  } else if (!previewRunning) {
-    draftCommand = '/run';
-    draftCommandLabel = '启动试玩';
-  }
-
-  return {
-    text: [
-      '缺陷记录:',
-      `- 项目:${nextManifest.name}`,
-      trace ? `- Run:${trace.runId} · ${formatAgentRunStatus(trace)}` : null,
-      `- 复现入口:预览 ${previewSummary}${previewRunning ? ' · /open-preview' : ' · 建议 /run'}`,
-      `- 最近试玩证据:${
-        latestPlaytestStep
-          ? `${latestPlaytestStep.agent} #${latestPlaytestStep.pass} · ${latestPlaytestStep.status} · ${latestPlaytestStep.summary}`
-          : '暂无'
-      }`,
-      '- 记录模板:问题一句话;复现步骤 1/2/3;期望结果;实际结果;设备/输入方式;严重度 阻断/高/中/低;附件 截图/录屏/日志时间点',
-      '- 优先级口径:阻断无法进入首局;高影响胜负或重开;中影响理解或手感;低为包装和文字问题',
-      '- 转修复草稿:/agent-resume 缺陷修复:现象…;复现…;期望…;实际…',
-      '- 参考:/test-plan;/feedback;/review;/logs',
-      '- 边界:只准备缺陷记录模板;不读取文件;不启动或打开预览;不导出试玩包;不写项目',
-      `- 建议:${draftCommand}`,
-    ]
-      .filter(Boolean)
-      .join('\n'),
-    draftCommand,
-    draftCommandLabel,
-  };
-}
-
-export function summarizeProjectPlaytestSurvey(
-  nextManifest: GameCreationAppManifest,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  const preview = nextManifest.preview;
-  const previewRunning = preview?.status === 'running' && preview.url;
-  const previewSummary = previewRunning
-    ? `运行中 ${preview.url}`
-    : preview
-      ? previewStatusLabels[preview.status]
-      : '未启动';
-  const tracePassed = isAgentRunTracePassed(trace);
-  const blockedTrace =
-    trace?.lifecycleStatus === 'killed' ||
-    trace?.status === 'failed' ||
-    trace?.status === 'needs-revision' ||
-    trace?.stopReason === 'max-passes-exhausted';
-  const goal =
-    nextManifest.goal?.trim() ||
-    trace?.goal?.trim() ||
-    trace?.taskGraph.goal?.trim() ||
-    '暂无';
-
-  let draftCommand = '/invite';
-  let draftCommandLabel = '准备邀请';
-  if (!trace) {
-    draftCommand = '/next';
-    draftCommandLabel = '查看下一步';
-  } else if (blockedTrace) {
-    draftCommand = '/review';
-    draftCommandLabel = '查看评审';
-  } else if (!tracePassed) {
-    draftCommand = '/trace';
-    draftCommandLabel = '查看 trace';
-  } else if (!previewRunning) {
-    draftCommand = '/run';
-    draftCommandLabel = '启动试玩';
-  }
-
-  return {
-    text: [
-      '试玩问卷:',
-      `- 项目:${nextManifest.name}`,
-      `- 目标:${goal}`,
-      `- 当前状态:${
-        tracePassed
-          ? `最近 run 已通过${trace?.runId ? ` ${trace.runId}` : ''}`
-          : trace
-            ? `最近 run 未通过 ${trace.status} / ${trace.stopReason}`
-            : '暂无最近 run'
-      };预览 ${previewSummary}`,
-      '- 使用场景:发给首批 3-5 位测试者;每人 5-10 分钟;先自由玩一局再回答',
-      '- 问题清单:1. 30 秒内你觉得目标是什么?2. 第一次操作哪里最卡?3. 胜负/重开是否清楚?4. 难度/节奏感觉如何?5. 最想保留和最想改的各一项?',
-      '- 记录格式:每题 1-5 分 + 一句话;补充设备、输入方式、是否愿意再玩一局',
-      '- 追踪方式:客户端问题由自动诊断提示;整体反馈走 /feedback;下一轮改动走 /revise',
-      '- 参考:/invite;/audience;/feedback',
-      '- 边界:只准备试玩问卷;不读取文件;不启动或打开预览;不导出试玩包;不写项目',
-      `- 建议:${draftCommand}`,
-    ].join('\n'),
-    draftCommand,
-    draftCommandLabel,
-  };
-}
-
-export function summarizeProjectCoverChecklist(
-  nextManifest: GameCreationAppManifest,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  const preview = nextManifest.preview;
-  const previewRunning = preview?.status === 'running' && preview.url;
-  const previewSummary = previewRunning
-    ? `运行中 ${preview.url}`
-    : preview
-      ? previewStatusLabels[preview.status]
-      : '未启动';
-  const tracePassed = isAgentRunTracePassed(trace);
-  const blockedTrace =
-    trace?.lifecycleStatus === 'killed' ||
-    trace?.status === 'failed' ||
-    trace?.status === 'needs-revision' ||
-    trace?.stopReason === 'max-passes-exhausted';
-  const visualAssets = nextManifest.assets.filter(isProjectVisualAsset);
-  const coverCandidate =
-    visualAssets.find(
-      (asset) =>
-        asset.source.kind === 'canvas' || asset.source.kind === 'generated',
-    ) ??
-    visualAssets[0] ??
-    null;
-  const canvasOrGeneratedCount = visualAssets.filter(
-    (asset) =>
-      asset.source.kind === 'canvas' || asset.source.kind === 'generated',
-  ).length;
-  const coverCandidateSummary = coverCandidate
-    ? `${coverCandidate.localPath} · ${coverCandidate.mediaType} · ${assetSourceKindLabels[coverCandidate.source.kind]}`
-    : '暂无 · 先用 /screenshots 或 /art 准备';
-
-  let draftCommand = coverCandidate ? '/listing' : '/art';
-  let draftCommandLabel = coverCandidate ? '准备作品页' : '查看美术素材';
-  if (!trace) {
-    draftCommand = '/next';
-    draftCommandLabel = '查看下一步';
-  } else if (blockedTrace) {
-    draftCommand = '/review';
-    draftCommandLabel = '查看评审';
-  } else if (!tracePassed) {
-    draftCommand = '/trace';
-    draftCommandLabel = '查看 trace';
-  } else if (!previewRunning) {
-    draftCommand = '/run';
-    draftCommandLabel = '启动试玩';
-  }
-
-  return {
-    text: [
-      '封面与缩略图:',
-      `- 项目:${nextManifest.name}`,
-      `- 当前状态:${
-        tracePassed
-          ? `最近 run 已通过${trace?.runId ? ` ${trace.runId}` : ''}`
-          : trace
-            ? `最近 run 未通过 ${trace.status} / ${trace.stopReason}`
-            : '暂无最近 run'
-      };预览 ${previewSummary}`,
-      `- 可用素材:视觉素材 ${visualAssets.length} 个;画板/生成候选 ${canvasOrGeneratedCount} 个;总资产 ${nextManifest.assets.length} 个`,
-      `- 封面候选:${coverCandidateSummary}`,
-      '- 用途尺寸:作品页封面 16:9;社区缩略图 1:1;移动首屏 9:16',
-      '- 选择口径:优先展示核心玩法状态;避免内部路径、调试面板、密钥配置或纯空场景',
-      '- 补齐路径:有可试玩时先 /screenshots;缺美术时 /art;作品页文案走 /listing',
-      '- 参考:/screenshots;/listing;/media-kit;/credits',
-      '- 边界:只准备封面与缩略图检查;不截屏;不裁剪;不读取文件;不启动或打开预览;不导出试玩包;不上传云端;不发布作品;不写项目',
-      `- 建议:${draftCommand}`,
-    ].join('\n'),
-    draftCommand,
-    draftCommandLabel,
-  };
-}
-
-export function summarizeProjectScreenshotChecklist(
-  nextManifest: GameCreationAppManifest,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  const preview = nextManifest.preview;
-  const previewRunning = preview?.status === 'running' && preview.url;
-  const previewSummary = previewRunning
-    ? `运行中 ${preview.url}`
-    : preview
-      ? previewStatusLabels[preview.status]
-      : '未启动';
-  const tracePassed = isAgentRunTracePassed(trace);
-  const blockedTrace =
-    trace?.lifecycleStatus === 'killed' ||
-    trace?.status === 'failed' ||
-    trace?.status === 'needs-revision' ||
-    trace?.stopReason === 'max-passes-exhausted';
-  const visualAssetCount =
-    nextManifest.assets.filter(isProjectVisualAsset).length;
-
-  let draftCommand = '/listing';
-  let draftCommandLabel = '准备作品页';
-  if (!trace) {
-    draftCommand = '/next';
-    draftCommandLabel = '查看下一步';
-  } else if (blockedTrace) {
-    draftCommand = '/review';
-    draftCommandLabel = '查看评审';
-  } else if (!tracePassed) {
-    draftCommand = '/trace';
-    draftCommandLabel = '查看 trace';
-  } else if (!previewRunning) {
-    draftCommand = '/run';
-    draftCommandLabel = '启动试玩';
-  }
-
-  return {
-    text: [
-      '宣传截图:',
-      `- 项目:${nextManifest.name}`,
-      `- 当前状态:${
-        tracePassed
-          ? `最近 run 已通过${trace?.runId ? ` ${trace.runId}` : ''}`
-          : trace
-            ? `最近 run 未通过 ${trace.status} / ${trace.stopReason}`
-            : '暂无最近 run'
-      };预览 ${previewSummary}`,
-      `- 可用素材:视觉素材 ${visualAssetCount} 个;总资产 ${nextManifest.assets.length} 个`,
-      '- 截图目标:封面一张;核心操作一张;胜负/重开一张;移动或窄屏一张;异常/空状态不作为首批宣传图',
-      '- 拍摄顺序:先确认 /run 可试玩;进入第一局 10-30 秒;截核心交互;再截结算或失败反馈',
-      '- 命名建议:exports/screenshots/cover.png;gameplay.png;result.png;mobile.png',
-      '- 文案搭配:每张图只配一句卖点;作品页标题和标签继续走 /listing',
-      '- 参考:/listing;/publish;/share;/credits',
-      '- 边界:只准备截图清单;不截屏;不读取文件;不启动或打开预览;不导出试玩包;不写项目',
-      `- 建议:${draftCommand}`,
-    ].join('\n'),
-    draftCommand,
-    draftCommandLabel,
-  };
-}
-
-export function summarizeProjectTrailerScript(
-  nextManifest: GameCreationAppManifest,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  const preview = nextManifest.preview;
-  const previewRunning = preview?.status === 'running' && preview.url;
-  const previewSummary = previewRunning
-    ? `运行中 ${preview.url}`
-    : preview
-      ? previewStatusLabels[preview.status]
-      : '未启动';
-  const tracePassed = isAgentRunTracePassed(trace);
-  const blockedTrace =
-    trace?.lifecycleStatus === 'killed' ||
-    trace?.status === 'failed' ||
-    trace?.status === 'needs-revision' ||
-    trace?.stopReason === 'max-passes-exhausted';
-  const goal =
-    nextManifest.goal?.trim() ||
-    trace?.goal?.trim() ||
-    trace?.taskGraph.goal?.trim() ||
-    '暂无';
-  const visualAssetCount =
-    nextManifest.assets.filter(isProjectVisualAsset).length;
-  const audioAssetCount =
-    nextManifest.assets.filter(isProjectAudioAsset).length;
-
-  let draftCommand = '/share';
-  let draftCommandLabel = '准备交付';
-  if (!trace) {
-    draftCommand = '/next';
-    draftCommandLabel = '查看下一步';
-  } else if (blockedTrace) {
-    draftCommand = '/review';
-    draftCommandLabel = '查看评审';
-  } else if (!tracePassed) {
-    draftCommand = '/trace';
-    draftCommandLabel = '查看 trace';
-  } else if (!previewRunning) {
-    draftCommand = '/run';
-    draftCommandLabel = '启动试玩';
-  }
-
-  return {
-    text: [
-      '试玩短视频:',
-      `- 项目:${nextManifest.name}`,
-      `- 目标:${goal}`,
-      `- 当前状态:${
-        tracePassed
-          ? `最近 run 已通过${trace?.runId ? ` ${trace.runId}` : ''}`
-          : trace
-            ? `最近 run 未通过 ${trace.status} / ${trace.stopReason}`
-            : '暂无最近 run'
-      };预览 ${previewSummary}`,
-      `- 可用素材:视觉素材 ${visualAssetCount} 个;音频素材 ${audioAssetCount} 个;总资产 ${nextManifest.assets.length} 个`,
-      '- 15 秒结构:0-3 秒首屏目标;3-8 秒核心操作;8-12 秒胜负 / 重开;12-15 秒结尾 CTA',
-      '- 镜头清单:标题 / 目标提示;玩家第一次操作;得分或失败反馈;重开按钮;结尾试玩邀请',
-      '- 口播节奏:一句玩法目标;一句操作说明;一句邀请试玩和反馈',
-      '- 录制提示:先确认 /run 可试玩;横屏或竖屏只选一种;不露内部路径、调试面板或密钥配置',
-      '- 参考:/screenshots;/listing;/share;/publish',
-      '- 边界:只准备试玩短视频脚本;不录屏;不读取文件;不启动或打开预览;不导出试玩包;不写项目',
-      `- 建议:${draftCommand}`,
-    ].join('\n'),
-    draftCommand,
-    draftCommandLabel,
-  };
-}
-
-export function summarizeProjectPlaytestFaq(
-  nextManifest: GameCreationAppManifest,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  const preview = nextManifest.preview;
-  const previewRunning = preview?.status === 'running' && preview.url;
-  const previewSummary = previewRunning
-    ? `运行中 ${preview.url}`
-    : preview
-      ? previewStatusLabels[preview.status]
-      : '未启动';
-  const tracePassed = isAgentRunTracePassed(trace);
-  const blockedTrace =
-    trace?.lifecycleStatus === 'killed' ||
-    trace?.status === 'failed' ||
-    trace?.status === 'needs-revision' ||
-    trace?.stopReason === 'max-passes-exhausted';
-  const goal =
-    nextManifest.goal?.trim() ||
-    trace?.goal?.trim() ||
-    trace?.taskGraph.goal?.trim() ||
-    '暂无';
-
-  let draftCommand = '/share';
-  let draftCommandLabel = '准备交付';
-  if (!trace) {
-    draftCommand = '/next';
-    draftCommandLabel = '查看下一步';
-  } else if (blockedTrace) {
-    draftCommand = '/review';
-    draftCommandLabel = '查看评审';
-  } else if (!tracePassed) {
-    draftCommand = '/trace';
-    draftCommandLabel = '查看 trace';
-  } else if (!previewRunning) {
-    draftCommand = '/run';
-    draftCommandLabel = '启动试玩';
-  }
-
-  return {
-    text: [
-      '试玩 FAQ:',
-      `- 项目:${nextManifest.name}`,
-      `- 目标:${goal}`,
-      `- 当前状态:${
-        tracePassed
-          ? `最近 run 已通过${trace?.runId ? ` ${trace.runId}` : ''}`
-          : trace
-            ? `最近 run 未通过 ${trace.status} / ${trace.stopReason}`
-            : '暂无最近 run'
-      };预览 ${previewSummary}`,
-      '- 问答清单:1. 这是什么?2. 怎么开始和重开?3. 需要反馈什么?4. 打不开或卡住怎么办?5. 能不能转发或公开?',
-      '- 回答口径:早期本地 Web 原型;5-10 分钟试玩;重点反馈目标理解、操作手感、难度、bug 和还想不想再玩',
-      '- 测试者提醒:先自由玩一局;不要评价完成度;问卷走 /survey;客户端问题由自动诊断提示',
-      '- 交付搭配:/invite;/share;/screenshots;/trailer',
-      '- 边界:只准备试玩常见问答;不读取文件;不启动或打开预览;不导出试玩包;不写项目',
-      `- 建议:${draftCommand}`,
-    ].join('\n'),
-    draftCommand,
-    draftCommandLabel,
-  };
-}
-
-export function summarizeProjectCommunityPost(
-  nextManifest: GameCreationAppManifest,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  const preview = nextManifest.preview;
-  const previewRunning = preview?.status === 'running' && preview.url;
-  const previewSummary = previewRunning
-    ? `运行中 ${preview.url}`
-    : preview
-      ? previewStatusLabels[preview.status]
-      : '未启动';
-  const tracePassed = isAgentRunTracePassed(trace);
-  const blockedTrace =
-    trace?.lifecycleStatus === 'killed' ||
-    trace?.status === 'failed' ||
-    trace?.status === 'needs-revision' ||
-    trace?.stopReason === 'max-passes-exhausted';
-  const goal =
-    nextManifest.goal?.trim() ||
-    trace?.goal?.trim() ||
-    trace?.taskGraph.goal?.trim() ||
-    '暂无';
-  const visualAssetCount =
-    nextManifest.assets.filter(isProjectVisualAsset).length;
-
-  let draftCommand = '/store';
-  let draftCommandLabel = '准备上架';
-  if (!trace) {
-    draftCommand = '/next';
-    draftCommandLabel = '查看下一步';
-  } else if (blockedTrace) {
-    draftCommand = '/review';
-    draftCommandLabel = '查看评审';
-  } else if (!tracePassed) {
-    draftCommand = '/trace';
-    draftCommandLabel = '查看 trace';
-  } else if (!previewRunning) {
-    draftCommand = '/run';
-    draftCommandLabel = '启动试玩';
-  }
-
-  return {
-    text: [
-      '社区发布文案:',
-      `- 项目:${nextManifest.name}`,
-      `- 一句话:${goal}`,
-      `- 当前状态:${
-        tracePassed
-          ? `最近 run 已通过${trace?.runId ? ` ${trace.runId}` : ''}`
-          : trace
-            ? `最近 run 未通过 ${trace.status} / ${trace.stopReason}`
-            : '暂无最近 run'
-      };预览 ${previewSummary}`,
-      `- 素材准备:视觉素材 ${visualAssetCount} 个;配图走 /screenshots;短视频走 /trailer`,
-      `- 短文案:我做了一个早期 Web 小游戏原型《${nextManifest.name}》,核心目标是${goal}。想找 3-5 位朋友试玩 5 分钟,重点看能不能理解目标、操作顺不顺、还想不想再来一局。`,
-      '- 长文案结构:一句玩法目标;一张截图或短视频;试玩方式;希望收到的三类反馈;已知限制',
-      '- 标签建议:#Web小游戏 #原型试玩 #AI游戏创作 #本地试玩',
-      '- CTA:愿意试玩请回复;遇到问题按 /faq 的口径反馈,客户端问题会自动提示',
-      '- 参考:/faq;/screenshots;/trailer;/store;/share',
-      '- 边界:只准备社区发布文案;不上传云端;不发布作品;不读取文件;不启动或打开预览;不导出试玩包;不写项目',
-      `- 建议:${draftCommand}`,
-    ].join('\n'),
-    draftCommand,
-    draftCommandLabel,
-  };
-}
-
-export function summarizeProjectStoreChecklist(
-  nextManifest: GameCreationAppManifest,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  const preview = nextManifest.preview;
-  const previewRunning = preview?.status === 'running' && preview.url;
-  const previewSummary = previewRunning
-    ? `运行中 ${preview.url}`
-    : preview
-      ? previewStatusLabels[preview.status]
-      : '未启动';
-  const tracePassed = isAgentRunTracePassed(trace);
-  const blockedTrace =
-    trace?.lifecycleStatus === 'killed' ||
-    trace?.status === 'failed' ||
-    trace?.status === 'needs-revision' ||
-    trace?.stopReason === 'max-passes-exhausted';
-  const goal =
-    nextManifest.goal?.trim() ||
-    trace?.goal?.trim() ||
-    trace?.taskGraph.goal?.trim() ||
-    '暂无';
-  const visualAssetCount =
-    nextManifest.assets.filter(isProjectVisualAsset).length;
-  const audioAssetCount =
-    nextManifest.assets.filter(isProjectAudioAsset).length;
-  const hasPublishReadme =
-    trace?.artifacts.some(
-      (artifact) => artifact.path === 'exports/README.md',
-    ) ?? false;
-
-  let draftCommand = '/listing';
-  let draftCommandLabel = '准备作品页';
-  if (!trace) {
-    draftCommand = '/next';
-    draftCommandLabel = '查看下一步';
-  } else if (blockedTrace) {
-    draftCommand = '/review';
-    draftCommandLabel = '查看评审';
-  } else if (!tracePassed) {
-    draftCommand = '/trace';
-    draftCommandLabel = '查看 trace';
-  } else if (!previewRunning) {
-    draftCommand = '/run';
-    draftCommandLabel = '启动试玩';
-  } else if (hasPublishReadme) {
-    draftCommand = '/read exports/README.md';
-    draftCommandLabel = '读发布说明';
-  }
-
-  return {
-    text: [
-      '上架资料:',
-      `- 项目:${nextManifest.name}`,
-      `- 一句话:${goal}`,
-      `- 当前状态:${
-        tracePassed
-          ? `最近 run 已通过${trace?.runId ? ` ${trace.runId}` : ''}`
-          : trace
-            ? `最近 run 未通过 ${trace.status} / ${trace.stopReason}`
-            : '暂无最近 run'
-      };预览 ${previewSummary}`,
-      `- 资产概况:视觉 ${visualAssetCount} 个;音频 ${audioAssetCount} 个;总资产 ${nextManifest.assets.length} 个`,
-      `- 必备资料:作品页文案 /listing;宣传截图 /screenshots;素材署名 /credits;隐私边界 /privacy;试玩包 /export`,
-      `- 发布说明:${hasPublishReadme ? 'exports/README.md · 已生成' : '待生成 · 先看 /publish 或 /listing'}`,
-      '- 首发范围:本地 Web 原型;小规模试玩;免费体验;不承诺账号、云存档、排行榜或付费',
-      '- 上架前检查:30 秒玩法可懂;首屏不空白;重开清楚;截图不含内部路径;素材来源可说明',
-      '- 参考:/publish;/listing;/screenshots;/credits;/privacy;/share',
-      '- 边界:只准备上架资料清单;不上传云端;不发布作品;不读取文件;不启动或打开预览;不导出试玩包;不写项目',
-      `- 建议:${draftCommand}`,
-    ].join('\n'),
-    draftCommand,
-    draftCommandLabel,
-  };
-}
-
-export function summarizeProjectMediaKit(
-  nextManifest: GameCreationAppManifest,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  const preview = nextManifest.preview;
-  const previewRunning = preview?.status === 'running' && preview.url;
-  const previewSummary = previewRunning
-    ? `运行中 ${preview.url}`
-    : preview
-      ? previewStatusLabels[preview.status]
-      : '未启动';
-  const tracePassed = isAgentRunTracePassed(trace);
-  const blockedTrace =
-    trace?.lifecycleStatus === 'killed' ||
-    trace?.status === 'failed' ||
-    trace?.status === 'needs-revision' ||
-    trace?.stopReason === 'max-passes-exhausted';
-  const goal =
-    nextManifest.goal?.trim() ||
-    trace?.goal?.trim() ||
-    trace?.taskGraph.goal?.trim() ||
-    '暂无';
-  const visualAssetCount =
-    nextManifest.assets.filter(isProjectVisualAsset).length;
-  const audioAssetCount =
-    nextManifest.assets.filter(isProjectAudioAsset).length;
-  const hasPublishReadme =
-    trace?.artifacts.some(
-      (artifact) => artifact.path === 'exports/README.md',
-    ) ?? false;
-
-  let draftCommand = '/screenshots';
-  let draftCommandLabel = '准备截图';
-  if (!trace) {
-    draftCommand = '/next';
-    draftCommandLabel = '查看下一步';
-  } else if (blockedTrace) {
-    draftCommand = '/review';
-    draftCommandLabel = '查看评审';
-  } else if (!tracePassed) {
-    draftCommand = '/trace';
-    draftCommandLabel = '查看 trace';
-  } else if (!previewRunning) {
-    draftCommand = '/run';
-    draftCommandLabel = '启动试玩';
-  } else if (hasPublishReadme) {
-    draftCommand = '/read exports/README.md';
-    draftCommandLabel = '读发布说明';
-  }
-
-  return {
-    text: [
-      '媒体资料包:',
-      `- 项目:${nextManifest.name}`,
-      `- 一句话:${goal}`,
-      `- 当前状态:${
-        tracePassed
-          ? `最近 run 已通过${trace?.runId ? ` ${trace.runId}` : ''}`
-          : trace
-            ? `最近 run 未通过 ${trace.status} / ${trace.stopReason}`
-            : '暂无最近 run'
-      };预览 ${previewSummary}`,
-      `- 素材概况:视觉素材 ${visualAssetCount} 个;音频素材 ${audioAssetCount} 个;总资产 ${nextManifest.assets.length} 个;发布说明 ${hasPublishReadme ? 'exports/README.md · 已生成' : '待生成'}`,
-      '- 资料清单:作品页 /listing;宣传截图 /screenshots;短视频 /trailer;FAQ /faq;社区文案 /post;上架清单 /store',
-      '- 缺口优先级:先跑 /run 确认可试玩;再补 /screenshots 和 /trailer;最后整理 /post 与 /store',
-      '- 打包顺序:1. 确认首屏和核心玩法;2. 准备截图 / 视频 / FAQ;3. 汇总署名、隐私和发布说明',
-      '- 参考:/screenshots;/trailer;/listing;/faq;/post;/store;/share',
-      '- 边界:只准备媒体资料包清单;不截屏;不录屏;不读取文件;不启动或打开预览;不导出试玩包;不上传云端;不发布作品;不写项目',
-      `- 建议:${draftCommand}`,
-    ].join('\n'),
-    draftCommand,
-    draftCommandLabel,
-  };
-}
diff --git a/apps/ai-game-creator-shell/src/features/project-summary/projectQualitySummaries.ts b/apps/ai-game-creator-shell/src/features/project-summary/projectQualitySummaries.ts
deleted file mode 100644
index d95a6428f..000000000
--- a/apps/ai-game-creator-shell/src/features/project-summary/projectQualitySummaries.ts
+++ /dev/null
@@ -1,1078 +0,0 @@
-import {
-  type GameCreationAgentRunTrace,
-  type GameCreationAppManifest,
-  type GameCreationAppTaskState,
-  selectGameCreationAppReadyTasks,
-} from '../../../../../packages/shared/src/contracts/gameCreationApp';
-import { taskRowsFromManifest } from '../agent-runtime';
-import {
-  formatAgentRunStatus,
-  isAgentRunTracePassed,
-  readableArtifactsFromAgentRunTrace,
-} from './agentTrace';
-import {
-  isProjectAudioAsset,
-  isProjectVisualAsset,
-} from './projectAssetSummaries';
-import {
-  previewStatusLabels,
-  taskGroupLabels,
-  taskStatusLabels,
-} from './projectSummaryConstants';
-
-export function summarizeProjectTutorialGuide(
-  nextManifest: GameCreationAppManifest,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  const preview = nextManifest.preview;
-  const previewRunning = preview?.status === 'running' && preview.url;
-  const previewSummary = previewRunning
-    ? `运行中 ${preview.url}`
-    : preview
-      ? previewStatusLabels[preview.status]
-      : '未启动';
-  const tracePassed = isAgentRunTracePassed(trace);
-  const goal =
-    nextManifest.goal?.trim() ||
-    trace?.goal?.trim() ||
-    trace?.taskGraph.goal?.trim() ||
-    '暂无';
-  const manifestTasks = taskRowsFromManifest(nextManifest);
-  const traceTasks = trace?.taskGraph.tasks ?? [];
-  const tasks = manifestTasks.map(
-    (task) => traceTasks.find((traceTask) => traceTask.id === task.id) ?? task,
-  );
-  const playtestTask =
-    tasks.find((task) => task.id === 'preview-playtest') ?? null;
-  const hasGameArtifact =
-    trace?.artifacts.some(
-      (artifact) =>
-        artifact.path === 'game/index.html' || artifact.path === 'game/',
-    ) ?? false;
-  const latestTutorialStep =
-    trace?.steps
-      .filter(
-        (step) =>
-          step.taskId === 'design-foundation' ||
-          step.taskId === 'preview-readiness' ||
-          step.taskId === 'preview-playtest' ||
-          step.phase === 'generate' ||
-          step.phase === 'playtest' ||
-          step.toolCalls.some(
-            (toolCall) =>
-              toolCall.toolId === 'game.static_smoke' ||
-              toolCall.toolId.startsWith('preview.'),
-          ),
-      )
-      .slice(-1)[0] ?? null;
-
-  let draftCommand = '/rules';
-  let draftCommandLabel = '查看玩法规则';
-  if (trace && !tracePassed) {
-    draftCommand = '/review';
-    draftCommandLabel = '查看评审';
-  } else if (tracePassed && !hasGameArtifact) {
-    draftCommand =
-      '/agent-resume 新手引导:在首屏加入目标、操作、反馈、失败重开提示';
-    draftCommandLabel = '补充新手引导';
-  } else if (previewRunning) {
-    draftCommand = '/open-preview';
-    draftCommandLabel = '打开预览';
-  } else if (tracePassed) {
-    draftCommand = '/run';
-    draftCommandLabel = '启动预览';
-  }
-
-  return {
-    text: [
-      '新手引导:',
-      `- 项目:${nextManifest.name}`,
-      `- 首屏目标:${goal}`,
-      '- 首局 30 秒:看到目标;尝试操作;收到反馈;理解失败/胜利;能重开',
-      `- 当前证据:原型入口 ${hasGameArtifact ? '已生成' : '未见 trace 产物'};${
-        tracePassed
-          ? `最近 run 已通过${trace?.runId ? ` ${trace.runId}` : ''}`
-          : trace
-            ? `最近 run 未通过 ${trace.status} / ${trace.stopReason}`
-            : '暂无最近 run'
-      };预览 ${previewSummary}`,
-      `- 试玩任务:${
-        playtestTask
-          ? `${taskGroupLabels[playtestTask.group]} / ${playtestTask.role} ${playtestTask.title} · ${taskStatusLabels[playtestTask.status]}`
-          : '暂无'
-      }`,
-      `- 最近引导证据:${
-        latestTutorialStep
-          ? `${latestTutorialStep.agent} #${latestTutorialStep.pass} · ${latestTutorialStep.status} · ${latestTutorialStep.summary}`
-          : '暂无'
-      }`,
-      '- 需要补齐:首屏目标提示;操作提示;碰撞/得分反馈;失败或胜利提示;重开按钮',
-      '- 参考:/rules;/playtest;/feedback;/pitch',
-      `- 建议:${draftCommand}`,
-    ].join('\n'),
-    draftCommand,
-    draftCommandLabel,
-  };
-}
-
-export function summarizeProjectMobilePlaytestGuide(
-  nextManifest: GameCreationAppManifest,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  const preview = nextManifest.preview;
-  const previewRunning = preview?.status === 'running' && preview.url;
-  const previewSummary = previewRunning
-    ? `运行中 ${preview.url}`
-    : preview
-      ? previewStatusLabels[preview.status]
-      : '未启动';
-  const tracePassed = isAgentRunTracePassed(trace);
-  const goal =
-    nextManifest.goal?.trim() ||
-    trace?.goal?.trim() ||
-    trace?.taskGraph.goal?.trim() ||
-    '暂无';
-  const manifestTasks = taskRowsFromManifest(nextManifest);
-  const traceTasks = trace?.taskGraph.tasks ?? [];
-  const tasks = manifestTasks.map(
-    (task) => traceTasks.find((traceTask) => traceTask.id === task.id) ?? task,
-  );
-  const relevantTasks = ['code-prototype', 'preview-playtest']
-    .map((taskId) => tasks.find((task) => task.id === taskId))
-    .filter((task): task is NonNullable => Boolean(task));
-  const taskLines = relevantTasks.map(
-    (task) =>
-      `- ${task.id}:${taskGroupLabels[task.group]} / ${task.role} ${task.title} · ${taskStatusLabels[task.status]}`,
-  );
-  const hasGameArtifact =
-    trace?.artifacts.some(
-      (artifact) =>
-        artifact.path === 'game/index.html' || artifact.path === 'game/',
-    ) ?? false;
-  const latestMobileStep =
-    trace?.steps
-      .filter(
-        (step) =>
-          step.taskId === 'code-prototype' ||
-          step.taskId === 'preview-readiness' ||
-          step.taskId === 'preview-playtest' ||
-          step.group === 'code' ||
-          step.phase === 'generate' ||
-          step.phase === 'playtest' ||
-          step.toolCalls.some(
-            (toolCall) =>
-              toolCall.toolId === 'game.static_smoke' ||
-              toolCall.toolId.startsWith('preview.'),
-          ),
-      )
-      .slice(-1)[0] ?? null;
-
-  let draftCommand = '/rules';
-  let draftCommandLabel = '查看玩法规则';
-  if (!trace) {
-    draftCommand = '/next';
-    draftCommandLabel = '查看下一步';
-  } else if (!tracePassed) {
-    draftCommand = '/review';
-    draftCommandLabel = '查看评审';
-  } else if (tracePassed && !hasGameArtifact) {
-    draftCommand =
-      '/agent-resume 移动试玩:补充触屏操作、响应式画布、横竖屏提示、重开按钮';
-    draftCommandLabel = '补充移动试玩';
-  } else if (previewRunning) {
-    draftCommand = '/open-preview';
-    draftCommandLabel = '打开预览';
-  } else {
-    draftCommand = '/run';
-    draftCommandLabel = '启动预览';
-  }
-
-  return {
-    text: [
-      '移动试玩:',
-      `- 项目:${nextManifest.name}`,
-      `- 目标:${goal}`,
-      '- 输入方式:键盘 / 触屏都应能完成核心循环',
-      `- 当前证据:原型入口 ${hasGameArtifact ? '已生成' : '未见 trace 产物'};${
-        tracePassed
-          ? `最近 run 已通过${trace?.runId ? ` ${trace.runId}` : ''}`
-          : trace
-            ? `最近 run 未通过 ${trace.status} / ${trace.stopReason}`
-            : '暂无最近 run'
-      };预览 ${previewSummary}`,
-      '- 移动检查:触屏操作;响应式画布;横竖屏提示;按钮尺寸;失败/胜利重开',
-      taskLines.length > 0
-        ? `- 关联任务:\n${taskLines.join('\n')}`
-        : '- 关联任务:暂无',
-      `- 最近移动相关步骤:${
-        latestMobileStep
-          ? `${latestMobileStep.agent} #${latestMobileStep.pass} · ${latestMobileStep.status} · ${latestMobileStep.summary}`
-          : '暂无'
-      }`,
-      '- 参考:/rules;/tutorial;/playtest;/feedback',
-      `- 建议:${draftCommand}`,
-    ].join('\n'),
-    draftCommand,
-    draftCommandLabel,
-  };
-}
-
-export function summarizeProjectCompatibilityNotes(
-  nextManifest: GameCreationAppManifest,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  const preview = nextManifest.preview;
-  const previewRunning = preview?.status === 'running' && preview.url;
-  const previewSummary = previewRunning
-    ? `运行中 ${preview.url}`
-    : preview
-      ? previewStatusLabels[preview.status]
-      : '未启动';
-  const tracePassed = isAgentRunTracePassed(trace);
-  const blockedTrace =
-    trace?.lifecycleStatus === 'killed' ||
-    trace?.status === 'failed' ||
-    trace?.status === 'needs-revision' ||
-    trace?.stopReason === 'max-passes-exhausted';
-  const goal =
-    nextManifest.goal?.trim() ||
-    trace?.goal?.trim() ||
-    trace?.taskGraph.goal?.trim() ||
-    '暂无';
-  const commandRuns = nextManifest.commandRuns ?? [];
-  const staticSmokePassed =
-    commandRuns.some(
-      (commandRun) =>
-        commandRun.commandId === 'game.static_smoke' &&
-        commandRun.status === 'completed',
-    ) ||
-    Boolean(
-      trace?.steps.some((step) =>
-        step.toolCalls.some(
-          (toolCall) =>
-            toolCall.toolId === 'game.static_smoke' && toolCall.status === 'ok',
-        ),
-      ),
-    );
-  const inputSummary = tracePassed
-    ? '键盘优先;触屏按 /mobile 复查'
-    : '待原型通过后复查键盘 / 触屏';
-
-  let draftCommand = '/mobile';
-  let draftCommandLabel = '查看移动试玩';
-  if (!trace) {
-    draftCommand = '/next';
-    draftCommandLabel = '查看下一步';
-  } else if (blockedTrace) {
-    draftCommand = '/review';
-    draftCommandLabel = '查看评审';
-  } else if (!tracePassed) {
-    draftCommand = '/trace';
-    draftCommandLabel = '查看 trace';
-  } else if (!previewRunning) {
-    draftCommand = '/run';
-    draftCommandLabel = '启动试玩';
-  }
-
-  return {
-    text: [
-      '兼容性说明:',
-      `- 项目:${nextManifest.name}`,
-      `- 目标:${goal}`,
-      `- 当前状态:${
-        tracePassed
-          ? `最近 run 已通过${trace?.runId ? ` ${trace.runId}` : ''}`
-          : trace
-            ? `最近 run 未通过 ${trace.status} / ${trace.stopReason}`
-            : '暂无最近 run'
-      };预览 ${previewSummary}`,
-      `- 自检:${staticSmokePassed ? 'game.static_smoke 已通过' : '未见静态自检通过'}`,
-      `- 输入兼容:${inputSummary}`,
-      '- 推荐环境:桌面 Chrome / Edge 最新版;本机 127.0.0.1 预览;移动浏览器只做早期体验',
-      '- 不承诺:旧浏览器、低端设备、离线模式、云存档、账号同步、手柄或多端数据一致',
-      '- 反馈口径:设备 / 浏览器 / 输入方式 / 截图或录屏;客户端检测到问题时会自动提示',
-      '- 参考:/mobile;/accessibility;/performance;/known-issues',
-      '- 边界:只准备兼容性说明;不读取文件;不启动或打开预览;不导出试玩包;不上传云端;不发布作品;不写项目',
-      `- 建议:${draftCommand}`,
-    ].join('\n'),
-    draftCommand,
-    draftCommandLabel,
-  };
-}
-
-export function summarizeProjectAccessibilityGuide(
-  nextManifest: GameCreationAppManifest,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  const preview = nextManifest.preview;
-  const previewRunning = preview?.status === 'running' && preview.url;
-  const previewSummary = previewRunning
-    ? `运行中 ${preview.url}`
-    : preview
-      ? previewStatusLabels[preview.status]
-      : '未启动';
-  const tracePassed = isAgentRunTracePassed(trace);
-  const goal =
-    nextManifest.goal?.trim() ||
-    trace?.goal?.trim() ||
-    trace?.taskGraph.goal?.trim() ||
-    '暂无';
-  const manifestTasks = taskRowsFromManifest(nextManifest);
-  const traceTasks = trace?.taskGraph.tasks ?? [];
-  const tasks = manifestTasks.map(
-    (task) => traceTasks.find((traceTask) => traceTask.id === task.id) ?? task,
-  );
-  const relevantTasks = [
-    'code-prototype',
-    'quality-review',
-    'preview-readiness',
-    'preview-playtest',
-  ]
-    .map((taskId) => tasks.find((task) => task.id === taskId))
-    .filter((task): task is NonNullable => Boolean(task));
-  const taskLines = relevantTasks.map(
-    (task) =>
-      `- ${task.id}:${taskGroupLabels[task.group]} / ${task.role} ${task.title} · ${taskStatusLabels[task.status]}`,
-  );
-  const hasGameArtifact =
-    trace?.artifacts.some(
-      (artifact) =>
-        artifact.path === 'game/index.html' || artifact.path === 'game/',
-    ) ?? false;
-  const latestAccessibilityStep =
-    trace?.steps
-      .filter(
-        (step) =>
-          step.taskId === 'code-prototype' ||
-          step.taskId === 'quality-review' ||
-          step.taskId === 'preview-readiness' ||
-          step.taskId === 'preview-playtest' ||
-          step.group === 'code' ||
-          step.phase === 'evaluation' ||
-          step.phase === 'playtest' ||
-          step.toolCalls.some(
-            (toolCall) =>
-              toolCall.toolId === 'game.static_smoke' ||
-              toolCall.toolId === 'agent.evaluate' ||
-              toolCall.toolId.startsWith('preview.'),
-          ),
-      )
-      .slice(-1)[0] ?? null;
-
-  let draftCommand = '/rules';
-  let draftCommandLabel = '查看玩法规则';
-  if (!trace) {
-    draftCommand = '/next';
-    draftCommandLabel = '查看下一步';
-  } else if (!tracePassed) {
-    draftCommand = '/review';
-    draftCommandLabel = '查看评审';
-  } else if (tracePassed && !hasGameArtifact) {
-    draftCommand =
-      '/agent-resume 可读性与无障碍:补充文字对比、清晰按钮标签、键盘等价操作、非颜色唯一反馈、静音可玩';
-    draftCommandLabel = '补充无障碍';
-  } else if (previewRunning) {
-    draftCommand = '/open-preview';
-    draftCommandLabel = '打开预览';
-  } else {
-    draftCommand = '/run';
-    draftCommandLabel = '启动预览';
-  }
-
-  return {
-    text: [
-      '可读性与无障碍:',
-      `- 项目:${nextManifest.name}`,
-      `- 目标:${goal}`,
-      '- 检查范围:文字可读;颜色对比;按钮/状态命名;键盘等价操作;可见焦点;非颜色唯一反馈;静音可玩',
-      `- 当前证据:原型入口 ${hasGameArtifact ? '已生成' : '未见 trace 产物'};${
-        tracePassed
-          ? `最近 run 已通过${trace?.runId ? ` ${trace.runId}` : ''}`
-          : trace
-            ? `最近 run 未通过 ${trace.status} / ${trace.stopReason}`
-            : '暂无最近 run'
-      };预览 ${previewSummary}`,
-      '- 补齐项:文字对比;清晰按钮标签;键盘等价操作;非颜色唯一反馈;静音可玩',
-      taskLines.length > 0
-        ? `- 关联任务:\n${taskLines.join('\n')}`
-        : '- 关联任务:暂无',
-      `- 最近无障碍相关步骤:${
-        latestAccessibilityStep
-          ? `${latestAccessibilityStep.agent} #${latestAccessibilityStep.pass} · ${latestAccessibilityStep.status} · ${latestAccessibilityStep.summary}`
-          : '暂无'
-      }`,
-      '- 参考:/rules;/mobile;/tutorial;/qa;/playtest',
-      `- 建议:${draftCommand}`,
-    ].join('\n'),
-    draftCommand,
-    draftCommandLabel,
-  };
-}
-
-export function summarizeProjectLocalizationChecklist(
-  nextManifest: GameCreationAppManifest,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  const preview = nextManifest.preview;
-  const previewRunning = preview?.status === 'running' && preview.url;
-  const previewSummary = previewRunning
-    ? `运行中 ${preview.url}`
-    : preview
-      ? previewStatusLabels[preview.status]
-      : '未启动';
-  const tracePassed = isAgentRunTracePassed(trace);
-  const goal =
-    nextManifest.goal?.trim() ||
-    trace?.goal?.trim() ||
-    trace?.taskGraph.goal?.trim() ||
-    '暂无';
-  const manifestTasks = taskRowsFromManifest(nextManifest);
-  const traceTasks = trace?.taskGraph.tasks ?? [];
-  const tasks = manifestTasks.map(
-    (task) => traceTasks.find((traceTask) => traceTask.id === task.id) ?? task,
-  );
-  const relevantTasks = [
-    'design-foundation',
-    'code-prototype',
-    'quality-review',
-    'publish-package',
-  ]
-    .map((taskId) => tasks.find((task) => task.id === taskId))
-    .filter((task): task is NonNullable => Boolean(task));
-  const taskLines = relevantTasks.map(
-    (task) =>
-      `- ${task.id}:${taskGroupLabels[task.group]} / ${task.role} ${task.title} · ${taskStatusLabels[task.status]}`,
-  );
-  const hasPublishArtifact =
-    trace?.artifacts.some(
-      (artifact) => artifact.path === 'exports/README.md',
-    ) ?? false;
-  const latestCopyStep =
-    trace?.steps
-      .filter(
-        (step) =>
-          step.group === 'design' ||
-          step.group === 'code' ||
-          step.group === 'publishing' ||
-          step.phase === 'evaluation' ||
-          step.taskId === 'design-foundation' ||
-          step.taskId === 'quality-review' ||
-          step.taskId === 'publish-package',
-      )
-      .slice(-1)[0] ?? null;
-
-  let draftCommand = '/next';
-  let draftCommandLabel = '查看下一步';
-  if (trace && !tracePassed) {
-    draftCommand = '/review';
-    draftCommandLabel = '查看评审';
-  } else if (hasPublishArtifact) {
-    draftCommand = '/read exports/README.md';
-    draftCommandLabel = '读发布说明';
-  } else if (tracePassed) {
-    draftCommand =
-      '/agent-resume 本地化与文案:统一标题、按钮、状态提示、失败胜利文案、发布简介';
-    draftCommandLabel = '补充文案';
-  }
-
-  return {
-    text: [
-      '本地化与文案:',
-      `- 项目:${nextManifest.name}`,
-      `- 目标:${goal}`,
-      '- 默认语言:简体中文;首版不承诺多语言',
-      '- 文案范围:标题;目标提示;操作按钮;状态反馈;失败/胜利;重开;发布简介',
-      `- 当前证据:${
-        tracePassed
-          ? `最近 run 已通过${trace?.runId ? ` ${trace.runId}` : ''}`
-          : trace
-            ? `最近 run 未通过 ${trace.status} / ${trace.stopReason}`
-            : '暂无最近 run'
-      };预览 ${previewSummary};发布说明 ${
-        hasPublishArtifact ? '已生成' : '未见 trace 产物'
-      }`,
-      taskLines.length > 0
-        ? `- 关联任务:\n${taskLines.join('\n')}`
-        : '- 关联任务:暂无',
-      '- 检查口径:短句优先;动词一致;玩家术语统一;错误提示可复现;UI 文案避免开发解释',
-      '- 暂不做:英日韩等多语言包;自动翻译;地区化素材;语音本地化;商店长文案 A/B',
-      `- 最近文案相关步骤:${
-        latestCopyStep
-          ? `${latestCopyStep.agent} #${latestCopyStep.pass} · ${latestCopyStep.status} · ${latestCopyStep.summary}`
-          : '暂无'
-      }`,
-      '- 参考:/rules;/tutorial;/listing;/faq;/known-issues',
-      '- 边界:只整理本地化与文案检查;不读取文件;不启动或打开预览;不导出试玩包;不上传云端;不发布作品;不写项目',
-      `- 建议:${draftCommand}`,
-    ].join('\n'),
-    draftCommand,
-    draftCommandLabel,
-  };
-}
-
-export function summarizeProjectPerformanceCheck(
-  nextManifest: GameCreationAppManifest,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  const preview = nextManifest.preview;
-  const previewRunning = preview?.status === 'running' && preview.url;
-  const previewSummary = previewRunning
-    ? `运行中 ${preview.url}`
-    : preview
-      ? previewStatusLabels[preview.status]
-      : '未启动';
-  const tracePassed = isAgentRunTracePassed(trace);
-  const goal =
-    nextManifest.goal?.trim() ||
-    trace?.goal?.trim() ||
-    trace?.taskGraph.goal?.trim() ||
-    '暂无';
-  const artifacts = trace ? readableArtifactsFromAgentRunTrace(trace) : [];
-  const totalArtifactBytes = artifacts.reduce(
-    (total, artifact) => total + artifact.sizeBytes,
-    0,
-  );
-  const visibleArtifacts = artifacts.slice(0, 6);
-  const artifactLines = visibleArtifacts.map(
-    (artifact) =>
-      `- ${artifact.path} · ${artifact.sizeBytes}B · ${artifact.checksum}`,
-  );
-  if (artifacts.length > visibleArtifacts.length) {
-    artifactLines.push(
-      `- 还有 ${artifacts.length - visibleArtifacts.length} 个产物`,
-    );
-  }
-  const manifestTasks = taskRowsFromManifest(nextManifest);
-  const traceTasks = trace?.taskGraph.tasks ?? [];
-  const tasks = manifestTasks.map(
-    (task) => traceTasks.find((traceTask) => traceTask.id === task.id) ?? task,
-  );
-  const relevantTasks = [
-    'code-prototype',
-    'preview-readiness',
-    'preview-playtest',
-  ]
-    .map((taskId) => tasks.find((task) => task.id === taskId))
-    .filter((task): task is NonNullable => Boolean(task));
-  const taskLines = relevantTasks.map(
-    (task) =>
-      `- ${task.id}:${taskGroupLabels[task.group]} / ${task.role} ${task.title} · ${taskStatusLabels[task.status]}`,
-  );
-  const hasGameArtifact =
-    trace?.artifacts.some(
-      (artifact) =>
-        artifact.path === 'game/index.html' || artifact.path === 'game/',
-    ) ?? false;
-  const latestPerformanceStep =
-    trace?.steps
-      .filter(
-        (step) =>
-          step.taskId === 'code-prototype' ||
-          step.taskId === 'preview-readiness' ||
-          step.taskId === 'preview-playtest' ||
-          step.group === 'code' ||
-          step.phase === 'generate' ||
-          step.phase === 'playtest' ||
-          step.toolCalls.some(
-            (toolCall) =>
-              toolCall.toolId === 'game.static_smoke' ||
-              toolCall.toolId.startsWith('preview.'),
-          ),
-      )
-      .slice(-1)[0] ?? null;
-
-  let draftCommand = '/next';
-  let draftCommandLabel = '查看下一步';
-  if (trace && !tracePassed) {
-    draftCommand = '/review';
-    draftCommandLabel = '查看评审';
-  } else if (artifacts.length > 0) {
-    draftCommand = '/run-artifacts';
-    draftCommandLabel = '列出 Run 产物';
-  } else if (previewRunning) {
-    draftCommand = '/open-preview';
-    draftCommandLabel = '打开预览';
-  } else if (tracePassed) {
-    draftCommand = '/run';
-    draftCommandLabel = '启动预览';
-  }
-
-  return {
-    text: [
-      '性能与加载:',
-      `- 项目:${nextManifest.name}`,
-      `- 目标:${goal}`,
-      `- 当前证据:${
-        tracePassed
-          ? `最近 run 已通过${trace?.runId ? ` ${trace.runId}` : ''}`
-          : trace
-            ? `最近 run 未通过 ${trace.status} / ${trace.stopReason}`
-            : '暂无最近 run'
-      };预览 ${previewSummary};入口 ${hasGameArtifact ? '已生成' : '未见 trace 产物'};产物 ${artifacts.length} 个 / ${totalArtifactBytes}B;资产 ${nextManifest.assets.length} 个`,
-      '- 检查范围:入口 HTML 自包含;首屏不空白;素材体积;主循环稳定;无远程依赖;预览启动',
-      artifactLines.length > 0
-        ? `- 关键产物:\n${artifactLines.join('\n')}`
-        : '- 关键产物:暂无',
-      taskLines.length > 0
-        ? `- 关联任务:\n${taskLines.join('\n')}`
-        : '- 关联任务:暂无',
-      `- 最近性能相关步骤:${
-        latestPerformanceStep
-          ? `${latestPerformanceStep.agent} #${latestPerformanceStep.pass} · ${latestPerformanceStep.status} · ${latestPerformanceStep.summary}`
-          : '暂无'
-      }`,
-      '- 参考:/run-artifacts;/playtest;/qa;/export',
-      `- 建议:${draftCommand}`,
-    ].join('\n'),
-    draftCommand,
-    draftCommandLabel,
-  };
-}
-
-export function summarizeProjectPolishChecklist(
-  nextManifest: GameCreationAppManifest,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  const preview = nextManifest.preview;
-  const previewRunning = preview?.status === 'running' && preview.url;
-  const previewSummary = previewRunning
-    ? `运行中 ${preview.url}`
-    : preview
-      ? previewStatusLabels[preview.status]
-      : '未启动';
-  const tracePassed = isAgentRunTracePassed(trace);
-  const goal =
-    nextManifest.goal?.trim() ||
-    trace?.goal?.trim() ||
-    trace?.taskGraph.goal?.trim() ||
-    '暂无';
-  const visualAssetCount =
-    nextManifest.assets.filter(isProjectVisualAsset).length;
-  const audioAssetCount =
-    nextManifest.assets.filter(isProjectAudioAsset).length;
-  const latestSmokeRun =
-    [...(nextManifest.commandRuns ?? [])]
-      .reverse()
-      .find((run) => run.commandId === 'game.static_smoke') ?? null;
-  const smokeSummary = latestSmokeRun
-    ? latestSmokeRun.status === 'completed'
-      ? '已通过'
-      : '失败'
-    : '暂无';
-  const manifestTasks = taskRowsFromManifest(nextManifest);
-  const traceTasks = trace?.taskGraph.tasks ?? [];
-  const tasks = manifestTasks.map(
-    (task) => traceTasks.find((traceTask) => traceTask.id === task.id) ?? task,
-  );
-  const polishTaskIds = [
-    'art-polish',
-    'audio-asset-plan',
-    'code-prototype',
-    'quality-review',
-    'preview-readiness',
-    'preview-playtest',
-    'publish-package',
-  ];
-  const taskLines = polishTaskIds
-    .map((taskId) => tasks.find((task) => task.id === taskId))
-    .filter((task): task is NonNullable => Boolean(task))
-    .map(
-      (task) =>
-        `- ${task.id}:${taskGroupLabels[task.group]} / ${task.role} ${task.title} · ${taskStatusLabels[task.status]}`,
-    );
-  const latestPolishStep =
-    trace?.steps
-      .filter(
-        (step) =>
-          (step.taskId && polishTaskIds.includes(step.taskId)) ||
-          step.phase === 'evaluation' ||
-          step.phase === 'playtest' ||
-          step.group === 'art' ||
-          step.group === 'code' ||
-          step.group === 'publishing' ||
-          step.toolCalls.some(
-            (toolCall) =>
-              toolCall.toolId === 'agent.evaluate' ||
-              toolCall.toolId === 'game.static_smoke' ||
-              toolCall.toolId.startsWith('preview.'),
-          ),
-      )
-      .slice(-1)[0] ?? null;
-
-  let draftCommand =
-    '/agent-resume 打磨:补齐新手引导、触屏操作、可读性、性能、素材署名和试玩反馈';
-  let draftCommandLabel = '补充打磨';
-  if (!trace) {
-    draftCommand = '/next';
-    draftCommandLabel = '查看下一步';
-  } else if (!tracePassed) {
-    draftCommand = '/review';
-    draftCommandLabel = '查看评审';
-  } else if (previewRunning) {
-    draftCommand = '/feedback';
-    draftCommandLabel = '准备反馈';
-  }
-
-  return {
-    text: [
-      '试玩前打磨:',
-      `- 项目:${nextManifest.name}`,
-      `- 目标:${goal}`,
-      `- 当前证据:${
-        tracePassed
-          ? `最近 run 已通过${trace?.runId ? ` ${trace.runId}` : ''}`
-          : trace
-            ? `最近 run 未通过 ${trace.status} / ${trace.stopReason}`
-            : '暂无最近 run'
-      };预览 ${previewSummary};自检 ${smokeSummary};资产 ${
-        nextManifest.assets.length
-      } 个(美术 ${visualAssetCount} / 音频 ${audioAssetCount})`,
-      '- 打磨范围:新手引导;移动试玩;可读性与无障碍;性能与加载;美术 / 音频素材;试玩反馈',
-      '- 推荐顺序:/tutorial -> /mobile -> /accessibility -> /performance -> /credits -> /feedback',
-      taskLines.length > 0
-        ? `- 关联任务:\n${taskLines.join('\n')}`
-        : '- 关联任务:暂无',
-      `- 最近打磨相关步骤:${
-        latestPolishStep
-          ? `${latestPolishStep.agent} #${latestPolishStep.pass} · ${latestPolishStep.status} · ${latestPolishStep.summary}`
-          : '暂无'
-      }`,
-      '- 边界:只整理试玩前打磨清单;不读取文件;不启动预览;不导出试玩包;不写项目',
-      `- 建议:${draftCommand}`,
-    ].join('\n'),
-    draftCommand,
-    draftCommandLabel,
-  };
-}
-
-export function summarizeProjectRisks(
-  nextManifest: GameCreationAppManifest,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  const risks: Array<{ text: string; command?: string }> = [];
-  const addRisk = (text: string, command?: string) => {
-    if (command && risks.some((risk) => risk.command === command)) {
-      return;
-    }
-    risks.push({ text, command });
-  };
-
-  const tasks = taskRowsFromManifest(nextManifest);
-  const failedTasks = tasks.filter((task) => task.status === 'failed');
-  const readyTasks = selectGameCreationAppReadyTasks({ tasks });
-  const commandRuns = nextManifest.commandRuns ?? [];
-  const latestCommandRun = commandRuns[commandRuns.length - 1];
-  const preview = nextManifest.preview;
-  const tracePassed =
-    trace?.status === 'passed' ||
-    trace?.status === 'artifacts-written' ||
-    trace?.stopReason === 'evaluator-passed';
-  const hasCanvasImageAsset = nextManifest.assets.some(
-    (asset) =>
-      asset.source.kind === 'canvas' &&
-      (asset.mediaType.startsWith('image/') ||
-        asset.mediaType === 'application/vnd.genarrative.image-sequence'),
-  );
-
-  if (!trace) {
-    addRisk('暂无最近 Agent run,当前项目还缺少生成闭环证据。', '/next');
-  } else if (
-    trace.lifecycleStatus === 'killed' ||
-    trace.status === 'failed' ||
-    trace.stopReason === 'max-passes-exhausted'
-  ) {
-    addRisk(
-      `最近 run 未完成:${trace.status} / ${trace.stopReason}。`,
-      '/agent-resume ',
-    );
-  } else if (!tracePassed) {
-    addRisk(
-      `最近 run 尚未通过 Evaluator:${trace.status} / ${trace.stopReason}。`,
-      '/trace',
-    );
-  }
-
-  if (failedTasks.length > 0) {
-    addRisk(`有 ${failedTasks.length} 个任务处于失败状态。`, '/tasks');
-  }
-
-  if (latestCommandRun?.status === 'failed') {
-    addRisk(`最近命令 ${latestCommandRun.commandId} 失败。`, '/logs');
-  }
-
-  if (tracePassed && !(preview?.status === 'running' && preview.url)) {
-    addRisk('最近 run 已通过,但当前本地预览未运行。', '/run');
-  }
-
-  if (readyTasks.length > 0) {
-    addRisk(`还有 ${readyTasks.length} 个 ready 任务等待处理。`, '/tasks');
-  }
-
-  if (nextManifest.assets.length === 0) {
-    addRisk(
-      '暂无本地资产,首版原型可能缺少可复用素材。',
-      '/asset-register assets/hero.png image image/png',
-    );
-  } else if (!hasCanvasImageAsset) {
-    addRisk(
-      '暂无画板来源图片资产,美术组可能只能先使用占位素材。',
-      '/sync-canvas-project ',
-    );
-  }
-
-  const firstAction = risks.find((risk) => risk.command);
-  return {
-    text:
-      risks.length > 0
-        ? `项目风险:\n${risks
-            .map(
-              (risk) =>
-                `- ${risk.text}${risk.command ? ` 建议:${risk.command}` : ''}`,
-            )
-            .join('\n')}`
-        : '项目风险:\n- 暂未发现需要立即处理的风险。',
-    draftCommand: firstAction?.command,
-    draftCommandLabel: firstAction ? '处理首个风险' : undefined,
-  };
-}
-
-export function summarizeProjectBlockers(
-  nextManifest: GameCreationAppManifest,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  const blockers: Array<{ text: string; command?: string }> = [];
-  const addBlocker = (text: string, command?: string) => {
-    if (command && blockers.some((blocker) => blocker.command === command)) {
-      return;
-    }
-    blockers.push({ text, command });
-  };
-
-  const manifestTasks = taskRowsFromManifest(nextManifest);
-  const traceTasks = trace?.taskGraph.tasks ?? [];
-  const taskSource = traceTasks.length > 0 ? traceTasks : manifestTasks;
-  const taskSourceById = new Map(taskSource.map((task) => [task.id, task]));
-  const failedTasks = taskSource.filter((task) => task.status === 'failed');
-  const readyTasks =
-    trace?.taskGraph.readyTaskIds
-      .map((taskId) => taskSourceById.get(taskId))
-      .filter((task): task is GameCreationAppTaskState => Boolean(task)) ??
-    selectGameCreationAppReadyTasks({ tasks: manifestTasks });
-  const commandRuns = nextManifest.commandRuns ?? [];
-  const latestCommandRun = commandRuns[commandRuns.length - 1] ?? null;
-  const latestExportCommand =
-    [...commandRuns]
-      .reverse()
-      .find(
-        (commandRun) => commandRun.commandId === 'project.export_package',
-      ) ?? null;
-  const preview = nextManifest.preview;
-  const previewRunning = preview?.status === 'running' && preview.url;
-  const tracePassed = isAgentRunTracePassed(trace);
-  const hasVisualAsset = nextManifest.assets.some(isProjectVisualAsset);
-
-  if (!trace) {
-    addBlocker('暂无最近 Agent run,缺少可验原型证据。', '/next');
-  } else if (
-    trace.lifecycleStatus === 'killed' ||
-    trace.status === 'failed' ||
-    trace.status === 'needs-revision' ||
-    trace.stopReason === 'max-passes-exhausted'
-  ) {
-    addBlocker(
-      `最近 run 阻塞:${trace.status} / ${trace.stopReason}。`,
-      '/review',
-    );
-  } else if (!tracePassed) {
-    addBlocker(
-      `最近 run 尚未通过:${trace.status} / ${trace.stopReason}。`,
-      '/trace',
-    );
-  }
-
-  if (failedTasks.length > 0) {
-    const firstFailed = failedTasks[0];
-    if (firstFailed) {
-      addBlocker(
-        `失败任务 ${failedTasks.length} 个:${firstFailed.title}(${firstFailed.id})。`,
-        '/tasks',
-      );
-    }
-  }
-
-  if (latestCommandRun?.status === 'failed') {
-    addBlocker(`最近命令失败:${latestCommandRun.commandId}。`, '/logs');
-  }
-
-  if (tracePassed && !previewRunning) {
-    addBlocker('原型已通过,但本地预览未运行。', '/run');
-  }
-
-  if (tracePassed && latestExportCommand?.status !== 'completed') {
-    addBlocker('原型已通过,但本地试玩包尚未导出。', '/export');
-  }
-
-  if (readyTasks.length > 0) {
-    const firstReady = readyTasks[0];
-    if (firstReady) {
-      addBlocker(
-        `ready 任务 ${readyTasks.length} 个:${taskGroupLabels[firstReady.group]} / ${firstReady.role} ${firstReady.title}(${firstReady.id})。`,
-        '/todo',
-      );
-    }
-  }
-
-  if (!hasVisualAsset) {
-    addBlocker('暂无可用美术素材,首版试玩可能只能使用占位。', '/art');
-  }
-
-  const firstAction = blockers.find((blocker) => blocker.command);
-  const blockerLines =
-    blockers.length > 0
-      ? blockers.map(
-          (blocker) =>
-            `- ${blocker.text}${blocker.command ? ` 建议:${blocker.command}` : ''}`,
-        )
-      : ['- 暂未发现会阻断 MVP 试玩的事项。'];
-
-  return {
-    text: [
-      '当前阻塞项:',
-      `- 项目:${nextManifest.name}`,
-      trace ? `- Run:${trace.runId} · ${formatAgentRunStatus(trace)}` : null,
-      ...blockerLines,
-      '- 边界:只整理阻塞项;不读取文件;不启动预览;不导出试玩包;不写项目',
-      `- 建议:${firstAction?.command ?? '/next'}`,
-    ]
-      .filter(Boolean)
-      .join('\n'),
-    draftCommand: firstAction?.command ?? '/next',
-    draftCommandLabel: firstAction ? '处理首个阻塞' : '查看下一步',
-  };
-}
-
-export function summarizeProjectPlaytestReadiness(
-  nextManifest: GameCreationAppManifest,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  const manifestTasks = taskRowsFromManifest(nextManifest);
-  const traceTasks = trace?.taskGraph.tasks ?? [];
-  const taskSource = traceTasks.length > 0 ? traceTasks : manifestTasks;
-  const taskSourceById = new Map(taskSource.map((task) => [task.id, task]));
-  const failedTasks = taskSource.filter((task) => task.status === 'failed');
-  const readyTasks =
-    trace?.taskGraph.readyTaskIds
-      .map((taskId) => taskSourceById.get(taskId))
-      .filter((task): task is GameCreationAppTaskState => Boolean(task)) ??
-    selectGameCreationAppReadyTasks({ tasks: manifestTasks });
-  const commandRuns = nextManifest.commandRuns ?? [];
-  const latestStaticSmokeCommand =
-    [...commandRuns]
-      .reverse()
-      .find((commandRun) => commandRun.commandId === 'game.static_smoke') ??
-    null;
-  const latestExportCommand =
-    [...commandRuns]
-      .reverse()
-      .find(
-        (commandRun) => commandRun.commandId === 'project.export_package',
-      ) ?? null;
-  const preview = nextManifest.preview;
-  const previewRunning = preview?.status === 'running' && preview.url;
-  const previewSummary = previewRunning
-    ? `运行中 ${preview.url}`
-    : preview
-      ? previewStatusLabels[preview.status]
-      : '未启动';
-  const tracePassed = isAgentRunTracePassed(trace);
-  const blockedTrace =
-    trace?.lifecycleStatus === 'killed' ||
-    trace?.status === 'failed' ||
-    trace?.status === 'needs-revision' ||
-    trace?.stopReason === 'max-passes-exhausted';
-  const staticSmokePassed =
-    latestStaticSmokeCommand?.status === 'completed' ||
-    Boolean(
-      trace?.steps.some((step) =>
-        step.toolCalls.some(
-          (toolCall) =>
-            toolCall.toolId === 'game.static_smoke' && toolCall.status === 'ok',
-        ),
-      ),
-    );
-  const exportReady = latestExportCommand?.status === 'completed';
-  const hasVisualAsset = nextManifest.assets.some(isProjectVisualAsset);
-  const hasAudioAsset = nextManifest.assets.some(isProjectAudioAsset);
-
-  let draftCommand = '/share';
-  let draftCommandLabel = '准备交付';
-  if (!trace) {
-    draftCommand = '/next';
-    draftCommandLabel = '查看下一步';
-  } else if (blockedTrace) {
-    draftCommand = '/review';
-    draftCommandLabel = '查看评审';
-  } else if (!tracePassed) {
-    draftCommand = '/trace';
-    draftCommandLabel = '查看 trace';
-  } else if (failedTasks.length > 0) {
-    draftCommand = '/tasks';
-    draftCommandLabel = '查看任务';
-  } else if (!staticSmokePassed || !previewRunning) {
-    draftCommand = '/run';
-    draftCommandLabel = '启动试玩';
-  } else if (!exportReady) {
-    draftCommand = '/export';
-    draftCommandLabel = '导出试玩包';
-  } else if (readyTasks.length > 0) {
-    draftCommand = '/todo';
-    draftCommandLabel = '查看小步清单';
-  } else if (!hasVisualAsset) {
-    draftCommand = '/art';
-    draftCommandLabel = '查看美术';
-  }
-
-  const verdict =
-    tracePassed && previewRunning && staticSmokePassed && exportReady
-      ? '可交给测试者'
-      : tracePassed
-        ? '接近可测,先补齐预览 / 自检 / 试玩包'
-        : trace
-          ? '暂不建议交付,先处理最近 run'
-          : '暂不建议交付,先生成可验原型';
-
-  return {
-    text: [
-      '试玩就绪度:',
-      `- 项目:${nextManifest.name}`,
-      trace ? `- Run:${trace.runId} · ${formatAgentRunStatus(trace)}` : null,
-      `- 原型:${
-        tracePassed
-          ? '最近 run 已通过'
-          : trace
-            ? `最近 run 未通过 ${trace.status} / ${trace.stopReason}`
-            : '暂无最近 run'
-      }`,
-      `- 预览:${previewSummary}`,
-      `- 自检:${
-        staticSmokePassed
-          ? '已通过'
-          : latestStaticSmokeCommand
-            ? `${latestStaticSmokeCommand.commandId} ${latestStaticSmokeCommand.status}`
-            : '暂无'
-      }`,
-      `- 试玩包:${exportReady ? '已导出' : '未导出'}`,
-      `- 任务:失败 ${failedTasks.length} / ready ${readyTasks.length}`,
-      `- 素材:美术 ${hasVisualAsset ? '已有' : '缺少'} / 音频 ${
-        hasAudioAsset ? '已有' : '可后补'
-      }`,
-      `- 结论:${verdict}`,
-      '- 边界:只判断就绪度;不读取文件;不启动预览;不导出试玩包;不写项目',
-      `- 建议:${draftCommand}`,
-    ]
-      .filter(Boolean)
-      .join('\n'),
-    draftCommand,
-    draftCommandLabel,
-  };
-}
diff --git a/apps/ai-game-creator-shell/src/features/project-summary/projectReadinessSummaries.ts b/apps/ai-game-creator-shell/src/features/project-summary/projectReadinessSummaries.ts
deleted file mode 100644
index 189a705f0..000000000
--- a/apps/ai-game-creator-shell/src/features/project-summary/projectReadinessSummaries.ts
+++ /dev/null
@@ -1,811 +0,0 @@
-import {
-  type GameCreationAgentRunTrace,
-  type GameCreationAppAssetSourceKind,
-  type GameCreationAppManifest,
-  selectGameCreationAppReadyTasks,
-} from '../../../../../packages/shared/src/contracts/gameCreationApp';
-import { taskRowsFromManifest } from '../agent-runtime';
-import {
-  formatAgentRunStatus,
-  isAgentReviewStep,
-  isAgentRunTracePassed,
-  isPlaytestTraceStep,
-  readableArtifactsFromAgentRunTrace,
-} from './agentTrace';
-import {
-  isProjectAudioAsset,
-  isProjectVisualAsset,
-} from './projectAssetSummaries';
-import { isSafeProjectRelativePath } from './projectPath';
-import {
-  previewStatusLabels,
-  taskGroupLabels,
-  taskStatusLabels,
-} from './projectSummaryConstants';
-
-export function summarizeProjectPublishReadiness(
-  nextManifest: GameCreationAppManifest,
-  nextProjectPath: string,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  const tasks = taskRowsFromManifest(nextManifest);
-  const completedCount = tasks.filter(
-    (task) => task.status === 'completed',
-  ).length;
-  const failedCount = tasks.filter((task) => task.status === 'failed').length;
-  const readyCount = selectGameCreationAppReadyTasks({ tasks }).length;
-  const tracePassed = isAgentRunTracePassed(trace);
-  const preview = nextManifest.preview;
-  const previewRunning = preview?.status === 'running' && preview.url;
-  const previewSummary = previewRunning
-    ? `运行中 ${preview.url}`
-    : preview
-      ? previewStatusLabels[preview.status]
-      : '未启动';
-  const sourceCounts = nextManifest.assets.reduce(
-    (counts, asset) => {
-      counts[asset.source.kind] += 1;
-      return counts;
-    },
-    { uploaded: 0, generated: 0, canvas: 0 } satisfies Record<
-      GameCreationAppAssetSourceKind,
-      number
-    >,
-  );
-  const audioAssetCount =
-    nextManifest.assets.filter(isProjectAudioAsset).length;
-  const commandRuns = nextManifest.commandRuns ?? [];
-  const latestCommandRun = commandRuns[commandRuns.length - 1] ?? null;
-  const hasPublishReadme =
-    trace?.artifacts.some(
-      (artifact) => artifact.path === 'exports/README.md',
-    ) ?? false;
-
-  let draftCommand = '/export';
-  let draftCommandLabel = '导出试玩包';
-  if (!trace) {
-    draftCommand = '/next';
-    draftCommandLabel = '查看下一步';
-  } else if (
-    trace.lifecycleStatus === 'killed' ||
-    trace.status === 'failed' ||
-    trace.stopReason === 'max-passes-exhausted'
-  ) {
-    draftCommand = '/agent-resume ';
-    draftCommandLabel = '继续最近 run';
-  } else if (!tracePassed) {
-    draftCommand = '/trace';
-    draftCommandLabel = '查看 trace';
-  } else if (!previewRunning) {
-    draftCommand = '/run';
-    draftCommandLabel = '启动预览';
-  }
-
-  return {
-    text: [
-      '发布准备:',
-      `- 项目:${nextManifest.name}`,
-      `- 目录:${nextProjectPath}`,
-      `- 原型:${
-        tracePassed
-          ? `最近 run 已通过 ${trace?.runId ?? ''}`.trim()
-          : trace
-            ? `最近 run 未通过 ${trace.status} / ${trace.stopReason}`
-            : '暂无最近 run'
-      }`,
-      `- 预览:${previewSummary}${previewRunning ? '' : ' · 建议 /run'}`,
-      `- 任务:完成 ${completedCount}/${tasks.length} · ready ${readyCount} · 失败 ${failedCount}`,
-      `- 资产:${nextManifest.assets.length} 个 · 上传 ${sourceCounts.uploaded} / 生成 ${sourceCounts.generated} / 画板 ${sourceCounts.canvas}`,
-      `- 音频:${audioAssetCount > 0 ? `${audioAssetCount} 个` : '暂无 · 建议 /audio'}`,
-      `- 包装:${
-        hasPublishReadme
-          ? '最近 Run 包含 exports/README.md · /read exports/README.md'
-          : '可用 /artifacts 查看发布说明草稿'
-      }`,
-      `- 试玩包:${
-        tracePassed
-          ? '可执行 /export 生成本地 ZIP'
-          : '等待最近 run 通过后再导出'
-      }`,
-      latestCommandRun?.status === 'failed'
-        ? `- 阻塞:最近命令 ${latestCommandRun.commandId} 失败 · /logs`
-        : null,
-    ]
-      .filter(Boolean)
-      .join('\n'),
-    draftCommand,
-    draftCommandLabel,
-  };
-}
-
-export function summarizeProjectListingDraft(
-  nextManifest: GameCreationAppManifest,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  const manifestTasks = taskRowsFromManifest(nextManifest);
-  const traceTasks = trace?.taskGraph.tasks ?? [];
-  const tasks = manifestTasks.map(
-    (task) => traceTasks.find((traceTask) => traceTask.id === task.id) ?? task,
-  );
-  const publishingTasks = tasks.filter(
-    (task) => task.group === 'publishing' || task.id.startsWith('publish-'),
-  );
-  const readyTaskIds = new Set(
-    trace
-      ? trace.taskGraph.readyTaskIds
-      : selectGameCreationAppReadyTasks({ tasks }).map((task) => task.id),
-  );
-  const activeTaskIds = new Set(trace?.taskGraph.activeTaskIds ?? []);
-  const carriedTaskIds = new Set(trace?.taskGraph.carriedTaskIds ?? []);
-  const taskLines = publishingTasks.map((task) => {
-    const markers = [taskStatusLabels[task.status]];
-    if (readyTaskIds.has(task.id)) {
-      markers.push('ready');
-    }
-    if (activeTaskIds.has(task.id)) {
-      markers.push('active');
-    }
-    if (carriedTaskIds.has(task.id)) {
-      markers.push('carry');
-    }
-    return `- ${task.id}:${task.role} ${task.title} · ${markers.join(' / ')}`;
-  });
-  const visualAssets = nextManifest.assets.filter(isProjectVisualAsset);
-  const usableVisualAssetCount = visualAssets.filter(
-    (asset) =>
-      asset.source.kind === 'canvas' || asset.source.kind === 'generated',
-  ).length;
-  const hasPublishReadme =
-    trace?.artifacts.some(
-      (artifact) => artifact.path === 'exports/README.md',
-    ) ?? false;
-  const latestPublishingStep =
-    trace?.steps
-      .filter(
-        (step) =>
-          step.group === 'publishing' ||
-          step.taskId?.startsWith('publish-') ||
-          step.outputPaths.includes('exports/README.md'),
-      )
-      .slice(-1)[0] ?? null;
-  const goal =
-    nextManifest.goal?.trim() ||
-    trace?.goal?.trim() ||
-    trace?.taskGraph.goal?.trim() ||
-    '暂无';
-  const blockedTrace =
-    trace?.lifecycleStatus === 'killed' ||
-    trace?.status === 'failed' ||
-    trace?.status === 'needs-revision' ||
-    trace?.stopReason === 'max-passes-exhausted';
-
-  let draftCommand = '/publish';
-  let draftCommandLabel = '查看发布准备';
-  if (hasPublishReadme) {
-    draftCommand = '/read exports/README.md';
-    draftCommandLabel = '读发布说明';
-  } else if (blockedTrace) {
-    draftCommand = '/review';
-    draftCommandLabel = '查看评审';
-  } else if (!trace) {
-    draftCommand = '/next';
-    draftCommandLabel = '查看下一步';
-  } else if (visualAssets.length === 0) {
-    draftCommand = '/art';
-    draftCommandLabel = '补齐美术素材';
-  }
-
-  return {
-    text: [
-      '作品页草稿:',
-      `- 标题:${nextManifest.name}`,
-      `- 一句话卖点:${goal}`,
-      taskLines.length > 0
-        ? `- 发布任务:\n${taskLines.join('\n')}`
-        : '- 发布任务:暂无',
-      `- 封面素材:${
-        visualAssets.length > 0
-          ? `${visualAssets.length} 个视觉素材 · 可用 ${usableVisualAssetCount} 个画板 / 生成来源`
-          : '暂无 · 建议 /art'
-      }`,
-      `- 说明文案:${
-        hasPublishReadme
-          ? 'exports/README.md · 已生成'
-          : '待从发布包装生成 · /publish'
-      }`,
-      '- 标签口径:玩法类型;视觉风格;难度 / 节奏;本地可玩',
-      `- 最近运营步骤:${
-        latestPublishingStep
-          ? `${latestPublishingStep.agent} #${latestPublishingStep.pass} · ${latestPublishingStep.status} · ${latestPublishingStep.summary}`
-          : '暂无'
-      }`,
-      '- 边界:只整理作品页文案和封面需求;不上传云端;不发布作品',
-      `- 建议:${draftCommand}`,
-    ].join('\n'),
-    draftCommand,
-    draftCommandLabel,
-  };
-}
-
-export function summarizeProjectPlaytestState(
-  nextManifest: GameCreationAppManifest,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  const tasks = taskRowsFromManifest(nextManifest);
-  const traceTasks = trace?.taskGraph.tasks ?? [];
-  const playtestTask =
-    traceTasks.find((task) => task.id === 'preview-playtest') ??
-    tasks.find((task) => task.id === 'preview-playtest') ??
-    null;
-  const readyTaskIds = new Set(
-    trace
-      ? trace.taskGraph.readyTaskIds
-      : selectGameCreationAppReadyTasks({ tasks }).map((task) => task.id),
-  );
-  const preview = nextManifest.preview;
-  const previewRunning = preview?.status === 'running' && preview.url;
-  const previewSummary = previewRunning
-    ? `运行中 ${preview.url}`
-    : preview
-      ? previewStatusLabels[preview.status]
-      : '未启动';
-  const tracePassed = isAgentRunTracePassed(trace);
-  const blockedTrace =
-    trace?.lifecycleStatus === 'killed' ||
-    trace?.status === 'failed' ||
-    trace?.status === 'needs-revision' ||
-    trace?.stopReason === 'max-passes-exhausted';
-  const latestPlaytestStep =
-    trace?.steps.filter(isPlaytestTraceStep).slice(-1)[0] ?? null;
-
-  let draftCommand = '/next';
-  let draftCommandLabel = '查看下一步';
-  if (blockedTrace) {
-    draftCommand = '/review';
-    draftCommandLabel = '查看评审';
-  } else if (trace && !tracePassed) {
-    draftCommand = '/trace';
-    draftCommandLabel = '查看 trace';
-  } else if (previewRunning) {
-    draftCommand = '/open-preview';
-    draftCommandLabel = '打开预览';
-  } else if (tracePassed) {
-    draftCommand = '/run';
-    draftCommandLabel = '启动试玩';
-  }
-
-  const taskMarkers: string[] = [];
-  if (playtestTask) {
-    taskMarkers.push(taskStatusLabels[playtestTask.status]);
-    if (readyTaskIds.has(playtestTask.id)) {
-      taskMarkers.push('ready');
-    }
-    if (trace?.taskGraph.activeTaskIds.includes(playtestTask.id)) {
-      taskMarkers.push('active');
-    }
-    if (trace?.taskGraph.carriedTaskIds.includes(playtestTask.id)) {
-      taskMarkers.push('carry');
-    }
-  }
-
-  return {
-    text: [
-      '试玩状态:',
-      `- 原型:${
-        tracePassed
-          ? `最近 run 已通过 ${trace?.runId ?? ''}`.trim()
-          : trace
-            ? `最近 run 未通过 ${trace.status} / ${trace.stopReason}`
-            : '暂无最近 run'
-      }`,
-      `- 预览:${previewSummary}${
-        previewRunning ? ' · 建议 /open-preview' : ' · 建议 /run'
-      }`,
-      `- Playtest 任务:${
-        playtestTask
-          ? `${taskGroupLabels[playtestTask.group]} / ${playtestTask.role} ${playtestTask.title} · ${taskMarkers.join(' / ')}`
-          : '暂无 preview-playtest 任务'
-      }`,
-      `- 最近试玩步骤:${
-        latestPlaytestStep
-          ? `${latestPlaytestStep.agent} #${latestPlaytestStep.pass} · ${latestPlaytestStep.status} · ${latestPlaytestStep.summary}`
-          : '暂无'
-      }`,
-      '- 试玩日志:/read .agent/logs/preview.log',
-    ].join('\n'),
-    draftCommand,
-    draftCommandLabel,
-  };
-}
-
-export function summarizeProjectManualTestPlan(
-  nextManifest: GameCreationAppManifest,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  const tasks = taskRowsFromManifest(nextManifest);
-  const traceTasks = trace?.taskGraph.tasks ?? [];
-  const taskRows = tasks.map(
-    (task) => traceTasks.find((traceTask) => traceTask.id === task.id) ?? task,
-  );
-  const taskLines = ['preview-readiness', 'preview-playtest']
-    .map((taskId) => taskRows.find((task) => task.id === taskId))
-    .filter((task): task is NonNullable => Boolean(task))
-    .map(
-      (task) =>
-        `- ${task.id}:${taskGroupLabels[task.group]} / ${task.role} ${task.title} · ${taskStatusLabels[task.status]}`,
-    );
-  const preview = nextManifest.preview;
-  const previewRunning = preview?.status === 'running' && preview.url;
-  const previewSummary = previewRunning
-    ? `运行中 ${preview.url}`
-    : preview
-      ? previewStatusLabels[preview.status]
-      : '未启动';
-  const tracePassed = isAgentRunTracePassed(trace);
-  const blockedTrace =
-    trace?.lifecycleStatus === 'killed' ||
-    trace?.status === 'failed' ||
-    trace?.status === 'needs-revision' ||
-    trace?.stopReason === 'max-passes-exhausted';
-  const goal =
-    nextManifest.goal?.trim() ||
-    trace?.goal?.trim() ||
-    trace?.taskGraph.goal?.trim() ||
-    '暂无';
-  const hasGameArtifact =
-    trace?.artifacts.some(
-      (artifact) =>
-        artifact.path === 'game/index.html' || artifact.path === 'game/',
-    ) ?? false;
-  const latestPlaytestStep =
-    trace?.steps.filter(isPlaytestTraceStep).slice(-1)[0] ?? null;
-
-  let draftCommand = '/next';
-  let draftCommandLabel = '查看下一步';
-  if (blockedTrace) {
-    draftCommand = '/review';
-    draftCommandLabel = '查看评审';
-  } else if (trace && !tracePassed) {
-    draftCommand = '/trace';
-    draftCommandLabel = '查看 trace';
-  } else if (previewRunning) {
-    draftCommand = '/open-preview';
-    draftCommandLabel = '打开预览';
-  } else if (tracePassed) {
-    draftCommand = '/run';
-    draftCommandLabel = '启动试玩';
-  }
-
-  return {
-    text: [
-      '手动测试计划:',
-      `- 项目:${nextManifest.name}`,
-      `- 目标:${goal}`,
-      `- 当前证据:${
-        tracePassed
-          ? `最近 run 已通过${trace?.runId ? ` ${trace.runId}` : ''}`
-          : trace
-            ? `最近 run 未通过 ${trace.status} / ${trace.stopReason}`
-            : '暂无最近 run'
-      };预览 ${previewSummary};入口 ${hasGameArtifact ? '已生成' : '未见 trace 产物'}`,
-      '- 用例:\n1. 启动预览:/run 后确认首屏不空白\n2. 30 秒理解:目标、操作、得分/失败和重开可见\n3. 输入验证:键盘/点击/触屏至少一种可完成核心动作\n4. 结局验证:胜利或失败后可重开\n5. 回归检查:/mobile;/accessibility;/performance;/audio',
-      taskLines.length > 0
-        ? `- 关联任务:\n${taskLines.join('\n')}`
-        : '- 关联任务:暂无',
-      `- 最近试玩证据:${
-        latestPlaytestStep
-          ? `${latestPlaytestStep.agent} #${latestPlaytestStep.pass} · ${latestPlaytestStep.status} · ${latestPlaytestStep.summary}`
-          : '暂无'
-      }`,
-      '- 记录反馈:/feedback',
-      `- 建议:${draftCommand}`,
-    ].join('\n'),
-    draftCommand,
-    draftCommandLabel,
-  };
-}
-
-export function summarizeProjectFeedbackPrompt(
-  nextManifest: GameCreationAppManifest,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  const preview = nextManifest.preview;
-  const previewRunning = preview?.status === 'running' && preview.url;
-  const previewSummary = previewRunning
-    ? `运行中 ${preview.url}`
-    : preview
-      ? previewStatusLabels[preview.status]
-      : '未启动';
-  const tracePassed = isAgentRunTracePassed(trace);
-  const blockedTrace =
-    trace?.lifecycleStatus === 'killed' ||
-    trace?.status === 'failed' ||
-    trace?.status === 'needs-revision' ||
-    trace?.stopReason === 'max-passes-exhausted';
-
-  let draftCommand = '/next';
-  let draftCommandLabel = '查看下一步';
-  if (blockedTrace) {
-    draftCommand = '/review';
-    draftCommandLabel = '查看评审';
-  } else if (tracePassed && !previewRunning) {
-    draftCommand = '/run';
-    draftCommandLabel = '启动预览';
-  } else if (trace) {
-    draftCommand = '/agent-resume 试玩反馈:';
-    draftCommandLabel = '填写试玩反馈';
-  }
-
-  return {
-    text: [
-      '试玩反馈:',
-      `- 项目:${nextManifest.name}`,
-      trace ? `- Run:${trace.runId} · ${formatAgentRunStatus(trace)}` : null,
-      `- 预览:${previewSummary}${previewRunning ? '' : ' · 建议 /run'}`,
-      '- 反馈方向:操作手感;胜负目标;难度;视觉 / 音效;重开路径',
-      '- 反馈模板:/agent-resume 试玩反馈:保留…;调整…;新增…',
-      '- 参考:/playtest;/qa;/changes',
-      `- 建议:${draftCommand}`,
-    ]
-      .filter(Boolean)
-      .join('\n'),
-    draftCommand,
-    draftCommandLabel,
-  };
-}
-
-export function summarizeProjectRetentionSignals(
-  nextManifest: GameCreationAppManifest,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  const preview = nextManifest.preview;
-  const previewRunning = preview?.status === 'running' && preview.url;
-  const previewSummary = previewRunning
-    ? `运行中 ${preview.url}`
-    : preview
-      ? previewStatusLabels[preview.status]
-      : '未启动';
-  const tracePassed = isAgentRunTracePassed(trace);
-  const blockedTrace =
-    trace?.lifecycleStatus === 'killed' ||
-    trace?.status === 'failed' ||
-    trace?.status === 'needs-revision' ||
-    trace?.stopReason === 'max-passes-exhausted';
-  const commandRuns = nextManifest.commandRuns ?? [];
-  const staticSmokePassed =
-    commandRuns.some(
-      (commandRun) =>
-        commandRun.commandId === 'game.static_smoke' &&
-        commandRun.status === 'completed',
-    ) ||
-    Boolean(
-      trace?.steps.some((step) =>
-        step.toolCalls.some(
-          (toolCall) =>
-            toolCall.toolId === 'game.static_smoke' && toolCall.status === 'ok',
-        ),
-      ),
-    );
-  const latestExportCommand =
-    [...commandRuns]
-      .reverse()
-      .find(
-        (commandRun) => commandRun.commandId === 'project.export_package',
-      ) ?? null;
-  const packageSummary =
-    latestExportCommand?.status === 'completed'
-      ? '最近导出完成'
-      : latestExportCommand?.status === 'failed'
-        ? '最近导出失败'
-        : tracePassed
-          ? '待导出'
-          : '等待原型通过';
-  const goal =
-    nextManifest.goal?.trim() ||
-    trace?.goal?.trim() ||
-    trace?.taskGraph.goal?.trim() ||
-    '暂无';
-  const latestPlaytestStep =
-    trace?.steps.filter(isPlaytestTraceStep).slice(-1)[0] ?? null;
-  const hasReleaseNotes =
-    trace?.artifacts.some(
-      (artifact) => artifact.path === 'exports/README.md',
-    ) ?? false;
-
-  let draftCommand = '/next';
-  let draftCommandLabel = '查看下一步';
-  if (blockedTrace) {
-    draftCommand = '/review';
-    draftCommandLabel = '查看评审';
-  } else if (trace && !tracePassed) {
-    draftCommand = '/trace';
-    draftCommandLabel = '查看 trace';
-  } else if (tracePassed && !previewRunning) {
-    draftCommand = '/run';
-    draftCommandLabel = '启动试玩';
-  } else if (trace) {
-    draftCommand = '/feedback';
-    draftCommandLabel = '准备反馈';
-  }
-
-  return {
-    text: [
-      '复玩观察:',
-      `- 项目:${nextManifest.name}`,
-      `- 目标:${goal}`,
-      `- 当前状态:${
-        tracePassed
-          ? `最近 run 已通过${trace?.runId ? ` ${trace.runId}` : ''}`
-          : trace
-            ? `最近 run 未通过 ${trace.status} / ${trace.stopReason}`
-            : '暂无最近 run'
-      };预览 ${previewSummary};自检 ${
-        staticSmokePassed ? '已通过' : '未见通过'
-      };试玩包 ${packageSummary}`,
-      '- 首轮样本:3-5 名测试者;每人 5-10 分钟;先不解释玩法,观察是否能自己完成首局',
-      '- 复玩信号:是否主动重开;失败后是否理解原因;第二局是否更快进入目标;是否愿意换难度/角色/关卡;是否能说出想保留的一点',
-      `- 资产与包装:素材 ${nextManifest.assets.length} 个;发布说明 ${
-        hasReleaseNotes ? '已生成' : '未见 trace 产物'
-      }`,
-      `- 最近试玩证据:${
-        latestPlaytestStep
-          ? `${latestPlaytestStep.agent} #${latestPlaytestStep.pass} · ${latestPlaytestStep.status} · ${latestPlaytestStep.summary}`
-          : '暂无'
-      }`,
-      '- 记录模板:保留 1 项;调弱/调强 1 项;新增 1 项;必须修 1 项;是否愿意再玩一局',
-      '- 暂不做:真实埋点;留存报表;用户画像;A/B 实验;排行榜或账号留存',
-      '- 参考:/playtest;/feedback;/survey;/share;/known-issues',
-      '- 边界:只准备复玩观察清单;不读取文件;不启动或打开预览;不导出试玩包;不上传云端;不发布作品;不写项目',
-      `- 建议:${draftCommand}`,
-    ].join('\n'),
-    draftCommand,
-    draftCommandLabel,
-  };
-}
-
-export function summarizeProjectShareHandoff(
-  nextManifest: GameCreationAppManifest,
-  nextProjectPath: string,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  const tracePassed = isAgentRunTracePassed(trace);
-  const blockedTrace =
-    trace?.lifecycleStatus === 'killed' ||
-    trace?.status === 'failed' ||
-    trace?.status === 'needs-revision' ||
-    trace?.stopReason === 'max-passes-exhausted';
-  const preview = nextManifest.preview;
-  const previewRunning = preview?.status === 'running' && preview.url;
-  const previewSummary = previewRunning
-    ? `运行中 ${preview.url}`
-    : preview
-      ? previewStatusLabels[preview.status]
-      : '未启动';
-  const commandRuns = nextManifest.commandRuns ?? [];
-  const latestExportCommand =
-    [...commandRuns]
-      .reverse()
-      .find(
-        (commandRun) => commandRun.commandId === 'project.export_package',
-      ) ?? null;
-  const packageSummary =
-    latestExportCommand?.status === 'completed'
-      ? '最近导出完成 · /exports'
-      : latestExportCommand?.status === 'failed'
-        ? '最近导出失败 · /logs'
-        : tracePassed
-          ? '待导出 · /export'
-          : '等待最近 run 通过';
-
-  let draftCommand = '/next';
-  let draftCommandLabel = '查看下一步';
-  if (blockedTrace) {
-    draftCommand = '/review';
-    draftCommandLabel = '查看评审';
-  } else if (!trace) {
-    draftCommand = '/next';
-    draftCommandLabel = '查看下一步';
-  } else if (!tracePassed) {
-    draftCommand = '/trace';
-    draftCommandLabel = '查看 trace';
-  } else if (latestExportCommand?.status === 'completed') {
-    draftCommand = '/exports';
-    draftCommandLabel = '查看试玩包';
-  } else {
-    draftCommand = '/export';
-    draftCommandLabel = '导出试玩包';
-  }
-
-  return {
-    text: [
-      '试玩交付:',
-      `- 项目:${nextManifest.name}`,
-      `- 目录:${nextProjectPath}`,
-      `- 原型:${
-        tracePassed
-          ? `最近 run 已通过 ${trace?.runId ?? ''}`.trim()
-          : trace
-            ? `最近 run 未通过 ${trace.status} / ${trace.stopReason}`
-            : '暂无最近 run'
-      }`,
-      `- 本地预览:${previewSummary}${previewRunning ? ' · /open-preview' : ' · /run'}`,
-      `- 本地试玩包:${packageSummary}`,
-      '- 给测试者:玩法目标 / 操作 / 胜负 / 重开口径见 /rules',
-      '- 反馈收集:/feedback',
-      '- 交付边界:本地 ZIP 和本地预览;不上传云端;不生成公开分享链接',
-      `- 建议:${draftCommand}`,
-    ].join('\n'),
-    draftCommand,
-    draftCommandLabel,
-  };
-}
-
-export function summarizeProjectQualityCheck(
-  nextManifest: GameCreationAppManifest,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  const tasks = taskRowsFromManifest(nextManifest);
-  const completedCount = tasks.filter(
-    (task) => task.status === 'completed',
-  ).length;
-  const failedTasks = tasks.filter((task) => task.status === 'failed');
-  const readyCount = selectGameCreationAppReadyTasks({ tasks }).length;
-  const commandRuns = nextManifest.commandRuns ?? [];
-  const latestCommandRun = commandRuns[commandRuns.length - 1] ?? null;
-  const preview = nextManifest.preview;
-  const previewRunning = preview?.status === 'running' && preview.url;
-  const tracePassed = isAgentRunTracePassed(trace);
-  const blockedTrace =
-    trace?.lifecycleStatus === 'killed' ||
-    trace?.status === 'failed' ||
-    trace?.status === 'needs-revision' ||
-    trace?.stopReason === 'max-passes-exhausted';
-  const staticSmokePassed =
-    commandRuns.some(
-      (commandRun) =>
-        commandRun.commandId === 'game.static_smoke' &&
-        commandRun.status === 'completed',
-    ) ||
-    Boolean(
-      trace?.steps.some((step) =>
-        step.toolCalls.some(
-          (toolCall) =>
-            toolCall.toolId === 'game.static_smoke' && toolCall.status === 'ok',
-        ),
-      ),
-    );
-  const latestReviewStep =
-    trace?.steps.filter(isAgentReviewStep).slice(-1)[0] ?? null;
-  const latestPlaytestStep =
-    trace?.steps.filter(isPlaytestTraceStep).slice(-1)[0] ?? null;
-  const evaluatorSummary = !trace
-    ? '暂无'
-    : tracePassed
-      ? '通过'
-      : blockedTrace
-        ? '需返工'
-        : '未通过';
-  const playtestSummary = previewRunning
-    ? `预览运行中 ${preview.url}`
-    : tracePassed
-      ? '待启动预览'
-      : '等待原型通过';
-
-  let draftCommand = '/publish';
-  let draftCommandLabel = '查看发布准备';
-  if (!trace) {
-    draftCommand = '/next';
-    draftCommandLabel = '查看下一步';
-  } else if (blockedTrace) {
-    draftCommand = '/review';
-    draftCommandLabel = '查看评审';
-  } else if (failedTasks.length > 0) {
-    draftCommand = '/tasks';
-    draftCommandLabel = '查看任务';
-  } else if (!tracePassed) {
-    draftCommand = '/trace';
-    draftCommandLabel = '查看 trace';
-  } else if (!staticSmokePassed || !previewRunning) {
-    draftCommand = '/playtest';
-    draftCommandLabel = '查看试玩状态';
-  }
-
-  return {
-    text: [
-      '质量检查:',
-      trace ? `- Run:${trace.runId} · ${formatAgentRunStatus(trace)}` : null,
-      `- Evaluator:${evaluatorSummary}${
-        latestReviewStep ? ` · ${latestReviewStep.summary}` : ''
-      }`,
-      `- 任务:完成 ${completedCount}/${tasks.length} · ready ${readyCount} · 失败 ${failedTasks.length}`,
-      `- 静态自检:${staticSmokePassed ? '通过' : '未运行'}`,
-      `- 试玩:${playtestSummary}`,
-      `- 最近试玩步骤:${
-        latestPlaytestStep
-          ? `${latestPlaytestStep.agent} #${latestPlaytestStep.pass} · ${latestPlaytestStep.status} · ${latestPlaytestStep.summary}`
-          : '暂无'
-      }`,
-      `- 产物:${trace ? `${trace.artifacts.length} 个` : '暂无'}`,
-      latestCommandRun?.status === 'failed'
-        ? `- 阻塞:最近命令 ${latestCommandRun.commandId} 失败 · /logs`
-        : null,
-      `- 建议:${draftCommand}`,
-    ]
-      .filter(Boolean)
-      .join('\n'),
-    draftCommand,
-    draftCommandLabel,
-  };
-}
-
-export function summarizeProjectRecentChanges(
-  nextManifest: GameCreationAppManifest,
-  trace: GameCreationAgentRunTrace | null,
-) {
-  if (!trace) {
-    return {
-      text: '最近变更:\n- 最近 Run:暂无\n- 建议:/next',
-      draftCommand: '/next',
-      draftCommandLabel: '查看下一步',
-    };
-  }
-
-  const artifacts = readableArtifactsFromAgentRunTrace(trace);
-  const visibleArtifacts = artifacts.slice(0, 6);
-  const artifactLines = visibleArtifacts.map(
-    (artifact) =>
-      `- ${artifact.path} · ${artifact.sizeBytes}B · ${artifact.checksum}`,
-  );
-  if (artifacts.length > visibleArtifacts.length) {
-    artifactLines.push(
-      `- 还有 ${artifacts.length - visibleArtifacts.length} 个产物`,
-    );
-  }
-  const outputPathLines = trace.steps
-    .slice(-4)
-    .map((step) => {
-      const outputPaths = step.outputPaths
-        .filter(isSafeProjectRelativePath)
-        .slice(0, 3);
-      if (outputPaths.length === 0) {
-        return null;
-      }
-      return `- ${step.agent} #${step.pass} · ${step.status} · ${outputPaths.join(', ')}`;
-    })
-    .filter(Boolean);
-  const commandRuns = nextManifest.commandRuns ?? [];
-  const latestCommandRun = commandRuns[commandRuns.length - 1] ?? null;
-  const preferredArtifact =
-    artifacts.find((artifact) => artifact.path === 'game/index.html') ??
-    artifacts[0] ??
-    null;
-  const draftCommand = preferredArtifact
-    ? `/read ${preferredArtifact.path}`
-    : '/run-artifacts';
-
-  return {
-    text: [
-      '最近变更:',
-      `- Run:${trace.runId} · ${formatAgentRunStatus(trace)}`,
-      `- 可验产物:${artifacts.length} 个`,
-      artifactLines.length > 0
-        ? `- 关键产物:\n${artifactLines.join('\n')}`
-        : '- 关键产物:暂无可读取产物',
-      outputPathLines.length > 0
-        ? `- 最近输出:\n${outputPathLines.join('\n')}`
-        : '- 最近输出:暂无',
-      `- 当前资产:${nextManifest.assets.length} 个`,
-      latestCommandRun
-        ? `- 最近命令:${latestCommandRun.commandId} · ${
-            latestCommandRun.status === 'completed' ? '完成' : '失败'
-          }`
-        : '- 最近命令:暂无',
-      '- 全部产物:/run-artifacts',
-      '- Trace:/trace',
-      '- 真实差异:/checkpoints 后 /diff checkpoint-id',
-      `- 建议:${draftCommand}`,
-    ].join('\n'),
-    draftCommand,
-    draftCommandLabel: preferredArtifact ? '读取首个产物' : '列出 Run 产物',
-  };
-}
diff --git a/apps/ai-game-creator-shell/src/features/project-summary/projectSummary.ts b/apps/ai-game-creator-shell/src/features/project-summary/projectSummary.ts
index 97b981535..eefb6306d 100644
--- a/apps/ai-game-creator-shell/src/features/project-summary/projectSummary.ts
+++ b/apps/ai-game-creator-shell/src/features/project-summary/projectSummary.ts
@@ -1,139 +1,5 @@
-export {
-  summarizeAgentReviewState,
-  summarizeAgentRunBudget,
-  summarizeProjectContextSources,
-  summarizeProjectHandoff,
-  summarizeProjectTimeline,
-} from './agentRunSummaries';
-export {
-  formatAgentRunStatus,
-  formatTraceRepairRoutes,
-  formatTraceTaskId,
-  formatTraceTaskIds,
-  isAgentReviewStep,
-  isAgentRunTracePassed,
-  isPlaytestTraceStep,
-  readableArtifactPathFromAgentRunTrace,
-  readableArtifactsFromAgentRunTrace,
-} from './agentTrace';
-export { missingChatCommandArgumentMessage } from './chatCommandMetadata';
-export {
-  checkpointIdFromManifestPath,
-  checkpointSummaryFromManifest,
-  formatCanvasAssetSource,
-  formatProjectPolicyCommandList,
-  inferProjectFileAssetDraft,
-  isCheckpointManifestFile,
-  projectAssetDraftCommand,
-  projectFileActionDrafts,
-  sortCheckpointManifestFiles,
-  summarizeAgentRunSupportFileReadDrafts,
-  summarizeCommonProjectArtifactReadDrafts,
-  summarizeCommonProjectLogReadDrafts,
-  summarizeProjectCheckpoint,
-  summarizeProjectCheckpoints,
-  summarizeProjectDiff,
-  summarizeProjectExportPackage,
-  summarizeProjectExportPackages,
-  summarizeProjectFileContent,
-  summarizeProjectFiles,
-  summarizeProjectIndex,
-  summarizeProjectInternalReadDrafts,
-  summarizeProjectPolicy,
-} from './projectArtifactSummaries';
-export {
-  firstReadableProjectAssetPath,
-  isProjectAudioAsset,
-  isProjectVisualAsset,
-  summarizeProjectAssetCredits,
-  summarizeProjectAssets,
-  summarizeProjectAudioAssets,
-  summarizeProjectTasks,
-  summarizeProjectVisualAssets,
-} from './projectAssetSummaries';
-export {
-  summarizeProjectAcceptanceCriteria,
-  summarizeProjectBalanceState,
-  summarizeProjectGroupProgress,
-  summarizeProjectKnownIssues,
-  summarizeProjectNextRoundPlan,
-  summarizeProjectReleaseNotes,
-  summarizeProjectSpecSheet,
-  summarizeProjectTodoList,
-} from './projectDeliverySummaries';
-export {
-  summarizeMainProjectHeader,
-  summarizeNextProjectActions,
-  summarizeProjectUserGuide,
-} from './projectGuidanceSummaries';
-export {
-  summarizeProjectBrief,
-  summarizeProjectControlGuide,
-  summarizeProjectDemoScript,
-  summarizeProjectGoal,
-  summarizeProjectMvpScope,
-  summarizeProjectPitch,
-  summarizeProjectProgress,
-  summarizeProjectStatus,
-} from './projectOverviewSummaries';
 export {
   isAbsoluteProjectPath,
-  isSafeProjectRelativePath,
   projectPathHasControlCharacter,
   projectPathsMatchForInvalidation,
 } from './projectPath';
-export {
-  summarizeProjectDependencyMap,
-  summarizeProjectEvidenceLedger,
-  summarizeProjectPrivacyBoundary,
-  summarizeProjectRevisionDraft,
-} from './projectPlanningSummaries';
-export {
-  summarizeProjectAudienceGuide,
-  summarizeProjectBugReport,
-  summarizeProjectCommunityPost,
-  summarizeProjectCoverChecklist,
-  summarizeProjectMediaKit,
-  summarizeProjectPlaytestFaq,
-  summarizeProjectPlaytestInvite,
-  summarizeProjectPlaytestSurvey,
-  summarizeProjectScreenshotChecklist,
-  summarizeProjectStoreChecklist,
-  summarizeProjectTrailerScript,
-} from './projectPlaytestSummaries';
-export {
-  summarizeProjectAccessibilityGuide,
-  summarizeProjectBlockers,
-  summarizeProjectCompatibilityNotes,
-  summarizeProjectLocalizationChecklist,
-  summarizeProjectMobilePlaytestGuide,
-  summarizeProjectPerformanceCheck,
-  summarizeProjectPlaytestReadiness,
-  summarizeProjectPolishChecklist,
-  summarizeProjectRisks,
-  summarizeProjectTutorialGuide,
-} from './projectQualitySummaries';
-export {
-  summarizeProjectFeedbackPrompt,
-  summarizeProjectListingDraft,
-  summarizeProjectManualTestPlan,
-  summarizeProjectPlaytestState,
-  summarizeProjectPublishReadiness,
-  summarizeProjectQualityCheck,
-  summarizeProjectRecentChanges,
-  summarizeProjectRetentionSignals,
-  summarizeProjectShareHandoff,
-} from './projectReadinessSummaries';
-export {
-  agentTaskGraphStateLabels,
-  assetSourceKindLabels,
-  capabilityAreaLabels,
-  chatCommandHelp,
-  commonAgentRunSupportReadDrafts,
-  commonProjectArtifactReadDrafts,
-  commonProjectInternalReadDrafts,
-  commonProjectLogReadDrafts,
-  previewStatusLabels,
-  taskGroupLabels,
-  taskStatusLabels,
-} from './projectSummaryConstants';
diff --git a/apps/ai-game-creator-shell/src/features/project-summary/projectSummaryConstants.ts b/apps/ai-game-creator-shell/src/features/project-summary/projectSummaryConstants.ts
deleted file mode 100644
index c2f43bc5c..000000000
--- a/apps/ai-game-creator-shell/src/features/project-summary/projectSummaryConstants.ts
+++ /dev/null
@@ -1,215 +0,0 @@
-import {
-  GAME_CREATION_AGENT_CAPABILITIES,
-  type GameCreationAppAgentGroup,
-  type GameCreationAppAssetSourceKind,
-  type GameCreationAppPreviewStatus,
-  type GameCreationAppTaskStatus,
-} from '../../../../../packages/shared/src/contracts/gameCreationApp';
-import { type AgentTaskGraphState } from '../../app/types';
-
-export const commonProjectArtifactReadDrafts = [
-  { label: '读入口', path: 'game/index.html' },
-  { label: '读设计', path: 'game/game_design.md' },
-  { label: '读数值', path: 'game/balance.json' },
-  { label: '读美术清单', path: 'assets/manifest.art.json' },
-  { label: '读音频清单', path: 'assets/manifest.audio.json' },
-  { label: '读发布说明', path: 'exports/README.md' },
-] as const;
-
-export const commonProjectLogReadDrafts = [
-  { label: '读命令日志', path: '.agent/logs/command.log' },
-  { label: '读预览日志', path: '.agent/logs/preview.log' },
-  { label: '读 Agent 日志', path: '.agent/logs/agent.log' },
-] as const;
-
-export const commonAgentRunSupportReadDrafts = [
-  { label: '读输出流', path: '.agent/output.jsonl' },
-  { label: '读活动流', path: '.agent/activity.jsonl' },
-  { label: '读上下文包', path: '.agent/context.bundle.json' },
-] as const;
-
-export const commonProjectInternalReadDrafts = [
-  { label: '读 manifest', path: '.agent/manifest.json' },
-  { label: '读 run 指针', path: '.agent/run.latest.json' },
-  { label: '读规格', path: '.agent/spec.md' },
-  { label: '读评审', path: '.agent/findings.md' },
-  { label: '读权限策略', path: '.agent/policy.json' },
-  { label: '读项目索引', path: '.agent/project.index.json' },
-  { label: '读本地索引流', path: '.agent/agent.db' },
-  { label: '读项目对话', path: '.agent/conversations/project.jsonl' },
-] as const;
-
-export const taskGroupLabels: Record = {
-  design: '设计实现组',
-  art: '美术组',
-  code: '程序组',
-  balance: '数值组',
-  audio: '音乐组',
-  publishing: '运营组',
-};
-
-export const taskStatusLabels: Record = {
-  pending: '待处理',
-  running: '运行中',
-  'waiting-for-confirmation': '待确认',
-  completed: '已完成',
-  failed: '失败',
-};
-
-export const agentTaskGraphStateLabels: Record = {
-  active: '本轮 active',
-  carried: 'carry-over',
-  ready: 'ready',
-};
-
-export const previewStatusLabels: Record =
-  {
-    stopped: '未启动',
-    starting: '启动中',
-    running: '运行中',
-    failed: '失败',
-  };
-
-export const assetSourceKindLabels: Record<
-  GameCreationAppAssetSourceKind,
-  string
-> = {
-  uploaded: '上传',
-  generated: '生成',
-  canvas: '画板',
-};
-
-export const capabilityAreaLabels: Record<
-  (typeof GAME_CREATION_AGENT_CAPABILITIES)[number]['area'],
-  string
-> = {
-  user: '用户入口',
-  'agent-runtime': 'Agent Runtime',
-  'local-runtime': '本地运行',
-  'dev-runtime': '开发支撑',
-};
-
-export const chatCommandHelp = [
-  '直接输入普通文本:和主聊天 Agent 对话',
-  '/generate 创作想法:生成本地游戏草案',
-  '/project /绝对路径:设置本地项目目录',
-  '/config:打开运行时配置',
-  '/llm-status:检查 LLM 配置',
-  '/llm-routes:查看 Agent 智能服务状态',
-  '/capabilities:查看 Agent 能力清单',
-  '/audit:审计当前项目的 Agent 能力证据',
-  '/status:查看项目状态',
-  '/brief:生成当前项目简报',
-  '/goal:查看创作目标',
-  '/progress:查看项目进度',
-  '/spec:查看创作规格包',
-  '/mvp:查看本轮最小可玩范围',
-  '/pitch:查看试玩定位与卖点',
-  '/demo:准备 30 秒试玩讲解稿',
-  '/rules:查看玩法操作与规则',
-  '/tutorial:查看新手引导检查',
-  '/mobile:查看移动试玩检查',
-  '/compatibility:准备兼容性说明',
-  '/accessibility:查看可读性与无障碍检查',
-  '/localization:查看本地化与文案检查',
-  '/performance:查看性能与加载检查',
-  '/polish:查看试玩前打磨清单',
-  '/risks:查看当前项目风险',
-  '/blockers:查看当前阻塞项',
-  '/ready:查看试玩就绪度',
-  '/evidence:查看当前验证证据台账',
-  '/deps:查看任务依赖链',
-  '/revise:准备下一轮改版说明草稿',
-  '/privacy:查看隐私与导出边界',
-  '/audience:查看首批试玩对象',
-  '/invite:准备试玩邀请文案',
-  '/survey:准备试玩问卷问题',
-  '/cover:准备封面与缩略图检查',
-  '/screenshots:准备宣传截图清单',
-  '/trailer:准备试玩短视频脚本',
-  '/faq:准备试玩常见问答',
-  '/post:准备社区发布文案',
-  '/store:准备上架资料清单',
-  '/media-kit:准备媒体资料包清单',
-  '/release-notes:准备试玩更新说明',
-  '/known-issues:准备已知问题清单',
-  '/criteria:查看当前任务验收标准',
-  '/groups:查看专业组进度',
-  '/balance:查看数值与难度口径',
-  '/budget:查看最近 run 预算',
-  '/qa:查看质量检查清单',
-  '/changes:查看最近生成变更',
-  '/review:查看 Evaluator 评审和返工焦点',
-  '/context:查看生成上下文来源',
-  '/timeline:查看项目活动时间线',
-  '/handoff:生成当前项目交接摘要',
-  '/next:查看下一步建议',
-  '/guide:查看普通用户操作导引',
-  '/plan:查看下一轮分工计划',
-  '/todo:查看下一轮小步清单',
-  '/publish:查看发布准备清单',
-  '/listing:准备作品页文案清单',
-  '/playtest:查看试玩状态与下一步',
-  '/test-plan:准备手动测试计划',
-  '/feedback:准备试玩反馈和修改说明',
-  '/retention:准备首轮复玩/留存观察清单',
-  '/share:准备试玩交付清单',
-  '/open-project:在系统文件管理器中显示项目目录',
-  '/switch-project:回到首页项目组切换工作区',
-  '/index:刷新本地项目索引',
-  '/checkpoint:保存本地项目快照',
-  '/checkpoints:列出最近 checkpoint',
-  '/diff checkpoint-id:对比 checkpoint',
-  '/restore checkpoint-id:回滚项目文件到 checkpoint',
-  '/policy:查看项目权限策略',
-  '/policy-deny 命令:拒绝项目内某个内置命令',
-  '/policy-allow 命令:移除项目内某个命令拒绝项',
-  '/policy-confirm 命令:执行前每次确认',
-  '/policy-auto 命令:恢复自动执行',
-  '/agent-policy-deny Agent 命令:拒绝某个 Agent 调用工具',
-  '/agent-policy-allow Agent 命令:移除某个 Agent 的拒绝项',
-  '/agent-policy-confirm Agent 命令:某个 Agent 调用工具前要求确认',
-  '/agent-policy-auto Agent 命令:恢复某个 Agent 自动执行',
-  '/tasks:查看任务拆分',
-  '/agents:查看每个 Agent 的当前状态',
-  '/agent-conversations:列出 Agent 对话读取命令',
-  '/agent-memories:列出 Agent 私有记忆读取命令',
-  '/trace 或 /loop:查看最近一次 Agent loop trace',
-  '/agent-status:查看最近 run 生命周期',
-  '/agent-kill:标记最近 run 为 killed',
-  '/agent-retry:用最近 run 目标重新运行一次',
-  '/agent-resume [说明]:带说明继续运行最近 run 目标',
-  '/history:重新读取当前项目对话历史',
-  '/files:列出本地项目文件',
-  '/assets:列出本地项目资产',
-  '/credits:查看素材署名与来源',
-  '/art:查看美术素材与下一步草稿',
-  '/audio:查看音频素材与下一步草稿',
-  '/artifacts:列出常用生成产物读取命令',
-  '/run-artifacts:列出最近 Run 产物读取命令',
-  '/passes:列出 Agent 轮次产物读取命令',
-  '/runs:列出已加载 Run 历史读取命令',
-  '/run-files:列出 Agent 运行辅助文件读取命令',
-  '/internals:列出项目内部真相源读取命令',
-  '/logs:列出常用日志读取命令',
-  '/asset-register 路径 [kind] [mediaType]:登记项目内已有资产',
-  '/read 路径:读取本地项目内文本文件',
-  '/run:运行自检,启动本地 HTTP 预览并载入客户端运行视图',
-  '/export:导出本地试玩包',
-  '/exports:列出本地试玩包',
-  '/preview:启动本地 HTTP 预览并载入客户端运行视图',
-  '/open-preview:打开当前本地预览',
-  '/preview-status:查看预览状态',
-  '/preview-stop:停止预览',
-  '/memory [short|long|blackboard]:查看短期、长期或黑板记忆',
-  '/remember [short|long|blackboard] 内容:追加短期、长期或黑板记忆',
-  '/memory-set [short|long|blackboard] 内容:覆盖保存对应记忆',
-  '/forget-memory [short|long|blackboard]:删除对应记忆',
-  '/commands:查看可运行的受限命令白名单',
-  '/smoke:运行静态入口自检',
-  '/canvas 画板项目ID:打开本机画板项目',
-  '/sync-canvas-project 画板项目ID:同步画板项目资源到本地资产',
-  '/generate-art 提示词:通过平台 External Editor API 生成首版美术素材',
-  '/import-canvas-asset 本地路径 画板项目ID 资源ID|object:资产对象ID:登记画板来源资产',
-  '/import-canvas-export /绝对/导出.zip 画板项目ID:导入画板素材导出包',
-];
diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/agentRunTrace.ts b/apps/ai-game-creator-shell/src/features/project-workspace/agentRunTrace.ts
deleted file mode 100644
index f6fed0f77..000000000
--- a/apps/ai-game-creator-shell/src/features/project-workspace/agentRunTrace.ts
+++ /dev/null
@@ -1,104 +0,0 @@
-import {
-  createGameCreationAppSeedTasks,
-  GAME_CREATION_AGENT_RUN_MAX_PASSES,
-  GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
-  GAME_CREATION_AGENT_TOOL_CALL_MAX,
-  type GameCreationAgentRunTrace,
-} from '../../../../../packages/shared/src/contracts/gameCreationApp';
-
-function normalizeTraceArray(value: T[] | null | undefined, fallback: T[]) {
-  if (value == null) {
-    return fallback;
-  }
-  if (!Array.isArray(value)) {
-    throw new Error('Agent run trace 格式不正确');
-  }
-  return value;
-}
-
-export function parseAgentRunTrace(content: string): GameCreationAgentRunTrace {
-  const parsed = JSON.parse(content) as GameCreationAgentRunTrace;
-  if (
-    parsed.schemaVersion !== GAME_CREATION_AGENT_RUN_SCHEMA_VERSION ||
-    !Array.isArray(parsed.steps)
-  ) {
-    throw new Error('Agent run trace 格式不正确');
-  }
-  parsed.artifacts = normalizeTraceArray(parsed.artifacts, []);
-  parsed.taskGraph ??= {
-    goal: parsed.goal,
-    readyTaskIds: [],
-    activeTaskIds: [],
-    carriedTaskIds: [],
-    repairFocus: [],
-    repairRoutes: [],
-    tasks: createGameCreationAppSeedTasks(),
-  };
-  if (typeof parsed.taskGraph !== 'object' || Array.isArray(parsed.taskGraph)) {
-    throw new Error('Agent run trace 格式不正确');
-  }
-  parsed.steps.forEach((step) => {
-    step.inputPaths = normalizeTraceArray(step.inputPaths, []);
-    step.outputPaths = normalizeTraceArray(step.outputPaths, []);
-    step.toolCalls = normalizeTraceArray(step.toolCalls, []);
-  });
-  parsed.maxPasses ??= GAME_CREATION_AGENT_RUN_MAX_PASSES;
-  parsed.toolCallCount ??= parsed.steps.reduce(
-    (count, step) => count + step.toolCalls.length,
-    0,
-  );
-  parsed.maxToolCalls ??= GAME_CREATION_AGENT_TOOL_CALL_MAX;
-  parsed.stopReason ??= parsed.status;
-  parsed.taskGraph.readyTaskIds = normalizeTraceArray(
-    parsed.taskGraph.readyTaskIds,
-    [],
-  );
-  parsed.taskGraph.activeTaskIds = normalizeTraceArray(
-    parsed.taskGraph.activeTaskIds,
-    [],
-  );
-  parsed.taskGraph.carriedTaskIds = normalizeTraceArray(
-    parsed.taskGraph.carriedTaskIds,
-    [],
-  );
-  parsed.taskGraph.repairFocus = normalizeTraceArray(
-    parsed.taskGraph.repairFocus,
-    [],
-  );
-  parsed.taskGraph.repairRoutes = normalizeTraceArray(
-    parsed.taskGraph.repairRoutes,
-    [],
-  );
-  parsed.taskGraph.repairRoutes.forEach((route) => {
-    route.taskIds = normalizeTraceArray(route.taskIds, []);
-  });
-  parsed.taskGraph.tasks = normalizeTraceArray(
-    parsed.taskGraph.tasks,
-    createGameCreationAppSeedTasks(),
-  );
-  parsed.passPlans = normalizeTraceArray(parsed.passPlans, []);
-  parsed.passPlans.forEach((plan) => {
-    plan.activeTaskIds = normalizeTraceArray(plan.activeTaskIds, []);
-    plan.carriedTaskIds = normalizeTraceArray(plan.carriedTaskIds, []);
-    plan.dependencyWaves = normalizeTraceArray(plan.dependencyWaves, []);
-    plan.dependencyWaves.forEach((wave) => {
-      if (!Array.isArray(wave)) {
-        throw new Error('Agent run trace 格式不正确');
-      }
-    });
-    plan.repairFocus = normalizeTraceArray(plan.repairFocus, []);
-    plan.repairRoutes = normalizeTraceArray(plan.repairRoutes, []);
-    plan.repairRoutes.forEach((route) => {
-      route.taskIds = normalizeTraceArray(route.taskIds, []);
-    });
-  });
-  return parsed;
-}
-
-export function gameDraftStartedMessage() {
-  return [
-    '开始调用 LLM:Planner 正在整理规格。',
-    '随后 Orchestrator 编排 6 组角色 brief,Generator 生成代码和资产清单,Evaluator 做质量评审。',
-    '完成后会把 run trace 和本地产物摘要发回这里。',
-  ].join('\n');
-}
diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/chatPromptPolish.ts b/apps/ai-game-creator-shell/src/features/project-workspace/chatPromptPolish.ts
index d30127da5..d426335ac 100644
--- a/apps/ai-game-creator-shell/src/features/project-workspace/chatPromptPolish.ts
+++ b/apps/ai-game-creator-shell/src/features/project-workspace/chatPromptPolish.ts
@@ -68,8 +68,7 @@ export function chatPromptDraftKey(
  * 发送前提醒判据(全部满足才提醒):
  * 1. 提醒没有被用户在偏好里关掉(本机 localStorage);
  * 2. 当前草稿指纹不等于「本轮已确认草稿」指纹 —— 即本轮还没有润色过、也没有选过「使用原文提交」;
- * 3. 纯文本(trim 后)长度达到 {@link CHAT_PROMPT_POLISH_REMINDER_MIN_TEXT_LENGTH};
- * 4. 草稿不是以 `/` 开头的命令 —— 命令走直通路径,不参与提醒。
+ * 3. 纯文本(trim 后)长度达到 {@link CHAT_PROMPT_POLISH_REMINDER_MIN_TEXT_LENGTH}。
  */
 export function shouldRemindChatPromptPolish({
   content,
@@ -89,9 +88,6 @@ export function shouldRemindChatPromptPolish({
   if (text.length < CHAT_PROMPT_POLISH_REMINDER_MIN_TEXT_LENGTH) {
     return false;
   }
-  if (text.startsWith('/')) {
-    return false;
-  }
   return chatPromptDraftKey(content) !== acknowledgedDraftKey;
 }
 
diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/memoryCommands.ts b/apps/ai-game-creator-shell/src/features/project-workspace/memoryCommands.ts
deleted file mode 100644
index f803aa0cf..000000000
--- a/apps/ai-game-creator-shell/src/features/project-workspace/memoryCommands.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import type { MemoryScope } from '../../app/types';
-
-export function memoryScopeLabel(scope: MemoryScope) {
-  if (scope === 'short') {
-    return '短期';
-  }
-  return scope === 'blackboard' ? '黑板' : '长期';
-}
-
-export function memoryScopePath(scope: MemoryScope) {
-  if (scope === 'short') {
-    return 'memory/session.md';
-  }
-  return scope === 'blackboard' ? 'memory/blackboard.md' : 'memory/project.md';
-}
-
-export function parseMemoryScope(value: string): MemoryScope {
-  const scope = value.trim().toLowerCase();
-  if (scope === 'short' || scope === '短期' || scope === 'session') {
-    return 'short';
-  }
-  if (scope === 'blackboard' || scope === '黑板') {
-    return 'blackboard';
-  }
-  return 'long';
-}
-
-export function parseRememberInput(value: string): {
-  scope: MemoryScope;
-  content: string;
-} {
-  const input = value.trim();
-  const [first = '', ...rest] = input.split(/\s+/);
-  const lower = first.toLowerCase();
-  if (
-    [
-      'short',
-      '短期',
-      'session',
-      'long',
-      '长期',
-      'project',
-      'blackboard',
-      '黑板',
-    ].includes(lower)
-  ) {
-    return {
-      scope: parseMemoryScope(first),
-      content: rest.join(' ').trim(),
-    };
-  }
-  return { scope: 'long', content: input };
-}
diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/projectCommandPolicy.ts b/apps/ai-game-creator-shell/src/features/project-workspace/projectCommandPolicy.ts
index 152837459..ff92e5353 100644
--- a/apps/ai-game-creator-shell/src/features/project-workspace/projectCommandPolicy.ts
+++ b/apps/ai-game-creator-shell/src/features/project-workspace/projectCommandPolicy.ts
@@ -18,14 +18,6 @@ export function resolveChatProjectPath(
   return projectPath;
 }
 
-export function isAgentTraceFilePath(value: string) {
-  const path = value.trim();
-  return (
-    path === '.agent/run.latest.json' ||
-    (path.startsWith('.agent/runs/') && path.endsWith('.json'))
-  );
-}
-
 export function needsInitializedChatProject(
   commandId: GameCreationAppCommandDescriptor['id'],
 ) {
diff --git a/apps/ai-game-creator-shell/src/view/project-development/chat/controller/useDirectProjectChatController.ts b/apps/ai-game-creator-shell/src/view/project-development/chat/controller/useDirectProjectChatController.ts
index d368e8bb5..f20a51515 100644
--- a/apps/ai-game-creator-shell/src/view/project-development/chat/controller/useDirectProjectChatController.ts
+++ b/apps/ai-game-creator-shell/src/view/project-development/chat/controller/useDirectProjectChatController.ts
@@ -330,12 +330,6 @@ export function useDirectProjectChatController({
       }
       return false;
     }
-    const prompt = directCodexContentToPromptText(content, assets).trim();
-    if (prompt === '/history') {
-      clearPendingInput();
-      void reloadHistory();
-      return true;
-    }
     const clientTurnId = createDirectProjectTurnId();
     const userItem = directCodexUserItemFromContent(
       content,
@@ -613,49 +607,6 @@ export function useDirectProjectChatController({
     }
   }
 
-  async function reloadHistory() {
-    const nextProjectPath = projectPath;
-    const invoke = resolveTauriInvoke();
-    if (!nextProjectPath || !invoke || historyLoadingRef.current) return;
-    const allowed = await ensureConversationReadAllowed({
-      projectPath: nextProjectPath,
-      onConfirmed: () => {
-        void reloadHistory();
-      },
-    });
-    if (!allowed || projectPathRef.current !== nextProjectPath) return;
-    historyLoadingRef.current = true;
-    try {
-      const pages = await readDirectHistoryPages({
-        existingEntries: directEntries,
-        beforeItemId: null,
-        readSlice: (beforeItemId) =>
-          invoke(
-            'read_direct_project_history_slice',
-            {
-              projectPath: nextProjectPath,
-              limit: CONVERSATION_INITIAL_VISIBLE_COUNT,
-              ...(beforeItemId ? { beforeItemId } : {}),
-            },
-          ),
-      });
-      if (projectPathRef.current !== nextProjectPath) return;
-      mergeHistoryPages(pages);
-      if (pages.error) throw pages.error;
-      setStatusNotice('已读取项目对话历史');
-    } catch (error) {
-      if (projectPathRef.current === nextProjectPath) {
-        setStatusNotice(
-          `读取项目对话历史失败:${
-            error instanceof Error ? error.message : String(error)
-          }`,
-        );
-      }
-    } finally {
-      historyLoadingRef.current = false;
-    }
-  }
-
   async function loadEarlierHistory() {
     const nextProjectPath = projectPath;
     if (
@@ -715,7 +666,6 @@ export function useDirectProjectChatController({
     loadEarlierHistory,
     localMessages,
     queuedTurns,
-    reloadHistory,
     removeAttachment,
     startInitialTurn: startTurn,
     statusNotice,
diff --git a/apps/ai-game-creator-shell/tests/ChatMarkdownMessage.test.tsx b/apps/ai-game-creator-shell/tests/ChatMarkdownMessage.test.tsx
index 2b4768e04..dd92a0023 100644
--- a/apps/ai-game-creator-shell/tests/ChatMarkdownMessage.test.tsx
+++ b/apps/ai-game-creator-shell/tests/ChatMarkdownMessage.test.tsx
@@ -236,13 +236,16 @@ describe('ChatMarkdownMessage', () => {
 
   it('用户消息保持纯文本,不解析 Markdown', () => {
     const { container } = render(
-      ,
+      ,
     );
 
     expect(container.querySelector('strong')).toBeNull();
     expect(container.querySelector('code')).toBeNull();
     expect(container.textContent).toContain('**不要解析**');
-    expect(container.textContent).toContain('`/read game`');
+    expect(container.textContent).toContain('`game/index.html`');
   });
 
   it('不输出原始 HTML、可点击链接或图片节点', () => {
diff --git a/apps/ai-game-creator-shell/tests/agentRunTrace.test.ts b/apps/ai-game-creator-shell/tests/agentRunTrace.test.ts
deleted file mode 100644
index 8f7d8183f..000000000
--- a/apps/ai-game-creator-shell/tests/agentRunTrace.test.ts
+++ /dev/null
@@ -1,155 +0,0 @@
-import { describe, expect, it } from 'vitest';
-
-import {
-  createGameCreationAppSeedTasks,
-  GAME_CREATION_AGENT_RUN_MAX_PASSES,
-  GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
-  GAME_CREATION_AGENT_TOOL_CALL_MAX,
-} from '../../../packages/shared/src/contracts/gameCreationApp';
-import { formatAgentRunStatus } from '../src/features/project-summary/agentTrace';
-import { parseAgentRunTrace } from '../src/features/project-workspace/agentRunTrace';
-
-const TRACE_FORMAT_ERROR = 'Agent run trace 格式不正确';
-
-/**
- * 旧 trace(`876529e66` 之前的 App 命令聊天会在面板里渲染它):步骤没有路径与工具数组、
- * 缺 `artifacts` / `taskGraph` 以外的归一化字段。这里保留同一份夹具,只是改成直接断言
- * 归一化函数的输出,不再渲染整个 App。
- */
-function legacyTraceFixture(): Record {
-  return {
-    schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
-    runId: 'run-legacy-trace',
-    commandId: 'game.generate_draft',
-    status: 'running',
-    passes: 1,
-    stopReason: 'planning',
-    goal: '做一个厨房弹幕游戏',
-    coordination: 'legacy',
-    steps: [legacyStep()],
-    taskGraph: {
-      goal: '做一个厨房弹幕游戏',
-      activeTaskIds: ['code-prototype'],
-      tasks: createGameCreationAppSeedTasks(),
-    },
-    passPlans: [
-      {
-        pass: 1,
-        mode: 'repair',
-        summary: '旧 pass plan',
-        activeTaskIds: ['code-prototype'],
-      },
-    ],
-    nextStep: 'continue',
-    error: null,
-    updatedAt: 1,
-  };
-}
-
-function legacyStep(overrides: Record = {}) {
-  return {
-    pass: 1,
-    agent: 'Generator',
-    phase: 'generate',
-    taskId: 'code-prototype',
-    group: 'code',
-    role: 'Code',
-    status: 'completed',
-    summary: '旧 trace 没有路径和工具数组',
-    ...overrides,
-  };
-}
-
-function traceJson(overrides: Record = {}) {
-  return JSON.stringify({ ...legacyTraceFixture(), ...overrides });
-}
-
-describe('Agent run trace 归一化与展示', () => {
-  it('normalizes a legacy trace before it reaches the renderers', () => {
-    const trace = parseAgentRunTrace(traceJson());
-
-    expect(trace.steps[0]!.inputPaths).toEqual([]);
-    expect(trace.steps[0]!.outputPaths).toEqual([]);
-    expect(trace.steps[0]!.toolCalls).toEqual([]);
-    expect(trace.artifacts).toEqual([]);
-    expect(trace.maxPasses).toBe(GAME_CREATION_AGENT_RUN_MAX_PASSES);
-    expect(trace.maxToolCalls).toBe(GAME_CREATION_AGENT_TOOL_CALL_MAX);
-    expect(trace.toolCallCount).toBe(0);
-    expect(trace.taskGraph.readyTaskIds).toEqual([]);
-    expect(trace.taskGraph.carriedTaskIds).toEqual([]);
-    expect(trace.taskGraph.repairFocus).toEqual([]);
-    expect(trace.taskGraph.repairRoutes).toEqual([]);
-    expect(trace.passPlans[0]!.carriedTaskIds).toEqual([]);
-    expect(trace.passPlans[0]!.dependencyWaves).toEqual([]);
-    expect(trace.passPlans[0]!.repairFocus).toEqual([]);
-    expect(trace.passPlans[0]!.repairRoutes).toEqual([]);
-  });
-
-  it('counts tool calls from the steps when the legacy trace omits the total', () => {
-    const trace = parseAgentRunTrace(
-      traceJson({
-        steps: [
-          legacyStep({ toolCalls: [{ toolId: 'file.read' }] }),
-          legacyStep({
-            taskId: 'quality-review',
-            toolCalls: [
-              { toolId: 'game.static_smoke' },
-              { toolId: 'game.run_local' },
-            ],
-          }),
-        ],
-      }),
-    );
-
-    expect(trace.toolCallCount).toBe(3);
-  });
-
-  it('falls back to the seed task graph when the trace predates task graphs', () => {
-    const fixture = legacyTraceFixture();
-    delete fixture.taskGraph;
-
-    const trace = parseAgentRunTrace(JSON.stringify(fixture));
-
-    expect(trace.taskGraph.goal).toBe('做一个厨房弹幕游戏');
-    expect(trace.taskGraph.tasks.length).toBe(
-      createGameCreationAppSeedTasks().length,
-    );
-    expect(trace.taskGraph.activeTaskIds).toEqual([]);
-  });
-
-  it('rejects malformed traces instead of rendering a half-parsed panel', () => {
-    expect(() => parseAgentRunTrace(traceJson({ steps: null }))).toThrow(
-      TRACE_FORMAT_ERROR,
-    );
-    expect(() =>
-      parseAgentRunTrace(traceJson({ schemaVersion: 'legacy' })),
-    ).toThrow(TRACE_FORMAT_ERROR);
-    expect(() => parseAgentRunTrace(traceJson({ artifacts: {} }))).toThrow(
-      TRACE_FORMAT_ERROR,
-    );
-    expect(() => parseAgentRunTrace(traceJson({ taskGraph: [] }))).toThrow(
-      TRACE_FORMAT_ERROR,
-    );
-    expect(() =>
-      parseAgentRunTrace(
-        traceJson({
-          passPlans: [
-            { pass: 1, mode: 'repair', dependencyWaves: [['a'], 'b'] },
-          ],
-        }),
-      ),
-    ).toThrow(TRACE_FORMAT_ERROR);
-  });
-
-  it('formats the run status with and without a lifecycle status', () => {
-    const running = parseAgentRunTrace(traceJson());
-    expect(formatAgentRunStatus(running)).toBe('running · 1/3 轮 · planning');
-
-    const passed = parseAgentRunTrace(
-      traceJson({ status: 'passed', passes: 2, lifecycleStatus: 'completed' }),
-    );
-    expect(formatAgentRunStatus(passed)).toBe(
-      'passed / completed · 2/3 轮 · planning',
-    );
-  });
-});
diff --git a/apps/ai-game-creator-shell/tests/agentSwarmTestEntry.test.ts b/apps/ai-game-creator-shell/tests/agentSwarmTestEntry.test.ts
deleted file mode 100644
index 5498797fd..000000000
--- a/apps/ai-game-creator-shell/tests/agentSwarmTestEntry.test.ts
+++ /dev/null
@@ -1,1915 +0,0 @@
-import { spawn } from 'node:child_process';
-import {
-  lstat,
-  mkdir,
-  mkdtemp,
-  readdir,
-  readFile,
-  realpath,
-  rm,
-  symlink,
-  writeFile,
-} from 'node:fs/promises';
-import os from 'node:os';
-import path from 'node:path';
-import { fileURLToPath } from 'node:url';
-import { deflateSync } from 'node:zlib';
-
-import { describe, expect, it } from 'vitest';
-
-import {
-  appIdentifier,
-  buildCargoCliArguments,
-  buildMissingConfigWizardArguments,
-  canPromptForMissingRuntimeConfig,
-  childExitWithTimeout,
-  cleanupSwarmTestProject,
-  cleanupSwarmTestRuntimeConfig,
-  configFileName,
-  defaultRealSwarmTestTask,
-  defaultRuntimeConfigDirCandidates,
-  discoverRuntimeConfigDir,
-  hasConfiguredEditorApiKey,
-  hasGeneratedGameEntry,
-  hasIncompleteArtifactMarker,
-  inspectSwarmProjectArtifacts,
-  localConfigFileName,
-  nextSwarmAutoPilotReply,
-  parseRunnerShutdownOutput,
-  parseSettledSwarmTurnReport,
-  parseSwarmTestArguments,
-  prepareSwarmTestProject,
-  prepareSwarmTestRuntimeConfig,
-  removeDirectoryWithTimeout,
-  requiredSwarmManifestTaskIds,
-  resolveSwarmTestTimeoutMs,
-  runnerEndpointFileName,
-  shouldStartPersistentPreview,
-  swarmAutoPilotShouldCloseInput,
-  swarmAutoPilotSitsAtPrompt,
-  terminateChildTree,
-  testProjectPrefix,
-  testProjectSentinelName,
-  testProjectSentinelSchema,
-  testRuntimeConfigPrefix,
-  testRuntimeConfigSentinelName,
-  testRuntimeConfigSentinelSchema,
-  ungeneratedGameEntryMarker,
-  validatePngBytes,
-  validatePreviewUrl,
-  validateSwarmProjectArtifacts,
-} from '../scripts/agent-swarm-test-chat.mjs';
-import {
-  buildGameCreatorWizardConfig,
-  gameCreatorProviderPresets,
-  normalizeWizardBaseUrl,
-  parseConfigWizardArguments,
-  readGameCreatorWizardConfigState,
-  resolveGameCreatorAppConfigDir,
-  writeGameCreatorConfigAtomically,
-  writeGameCreatorWizardConfig,
-} from '../scripts/game-creator-config-wizard.mjs';
-
-const appRoot = path.resolve(fileURLToPath(new URL('..', import.meta.url)));
-
-// Windows 下「私有路径加固」每次都会拉起真实 powershell.exe 去设置 DACL(本机实测约 0.6 秒 / 次)。
-// 只校验配置分层、隔离与清理语义的用例统一传入这份 no-op 桩;真正覆盖私有 ACL 的用例才走真实实现。
-const skippedWindowsAclOptions =
-  process.platform === 'win32' ? { secureWindowsPath: async () => {} } : {};
-
-async function withTemporaryRoot(
-  run: (root: string) => Promise,
-): Promise {
-  const root = await mkdtemp(
-    path.join(os.tmpdir(), 'genarrative-swarm-entry-test-'),
-  );
-  try {
-    return await run(root);
-  } finally {
-    await rm(root, { recursive: true, force: true });
-  }
-}
-
-async function pathExists(targetPath: string): Promise {
-  return lstat(targetPath).then(
-    () => true,
-    (error: NodeJS.ErrnoException) => {
-      if (error.code === 'ENOENT') return false;
-      throw error;
-    },
-  );
-}
-
-const minimumFormalArtifactContents: Record = {
-  'memory/project.md':
-    '# Project\n\n原创项目目标、世界观、资源与单位命名均已确定。\n',
-  'game/game_design.md':
-    '# Game design\n\n玩家放置原创守卫完成波次,胜利后进入下一关,并可随时重新开始。\n',
-  'game/balance.json': '{"speed":1,"waves":3}\n',
-  'assets/manifest.art.json':
-    '{"assets":[{"path":"assets/art-spritesheet.png"}]}\n',
-  'assets/manifest.audio.json': '{"assets":[],"status":"planned"}\n',
-  'game/index.html':
-    '\n',
-  'exports/README.md':
-    '# Export\n\n项目已完成静态检查与桌面、移动双视口试玩。\n',
-};
-
-const fixturePngSignature = Buffer.from([
-  0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
-]);
-
-function fixtureCrc32(bytes: Buffer): number {
-  let value = 0xffffffff;
-  for (const byte of bytes) {
-    value ^= byte;
-    for (let bit = 0; bit < 8; bit += 1) {
-      value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
-    }
-  }
-  return (value ^ 0xffffffff) >>> 0;
-}
-
-function fixturePngChunk(type: string, data: Buffer): Buffer {
-  const typeBytes = Buffer.from(type, 'ascii');
-  const chunk = Buffer.alloc(12 + data.length);
-  chunk.writeUInt32BE(data.length, 0);
-  typeBytes.copy(chunk, 4);
-  data.copy(chunk, 8);
-  chunk.writeUInt32BE(
-    fixtureCrc32(Buffer.concat([typeBytes, data])),
-    8 + data.length,
-  );
-  return chunk;
-}
-
-function fixturePng(
-  width: number,
-  height: number,
-  { invalidFilter = false, trailingCompressedBytes = false } = {},
-): Buffer {
-  const ihdr = Buffer.alloc(13);
-  ihdr.writeUInt32BE(width, 0);
-  ihdr.writeUInt32BE(height, 4);
-  ihdr[8] = 1;
-  ihdr[9] = 0;
-  const rowBytes = Math.ceil(width / 8);
-  const scanlines = Buffer.alloc((rowBytes + 1) * height);
-  let pseudoRandom = 0x12345678;
-  for (let row = 0; row < height; row += 1) {
-    const rowOffset = row * (rowBytes + 1);
-    scanlines[rowOffset] = invalidFilter && row === 0 ? 5 : 0;
-    for (let column = 0; column < rowBytes; column += 1) {
-      pseudoRandom = (Math.imul(pseudoRandom, 1664525) + 1013904223) >>> 0;
-      scanlines[rowOffset + column + 1] = pseudoRandom >>> 24;
-    }
-  }
-  return Buffer.concat([
-    fixturePngSignature,
-    fixturePngChunk('IHDR', ihdr),
-    fixturePngChunk(
-      'IDAT',
-      trailingCompressedBytes
-        ? Buffer.concat([deflateSync(scanlines), Buffer.from('junk')])
-        : deflateSync(scanlines),
-    ),
-    fixturePngChunk('IEND', Buffer.alloc(0)),
-  ]);
-}
-
-function fixtureIndexedPng({
-  includePalette = true,
-  duplicatePalette = false,
-  unknownCriticalChunk = false,
-} = {}): Buffer {
-  const ihdr = Buffer.alloc(13);
-  ihdr.writeUInt32BE(1, 0);
-  ihdr.writeUInt32BE(1, 4);
-  ihdr[8] = 8;
-  ihdr[9] = 3;
-  const palette = fixturePngChunk('PLTE', Buffer.from([0, 0, 0]));
-  return Buffer.concat([
-    fixturePngSignature,
-    fixturePngChunk('IHDR', ihdr),
-    ...(includePalette ? [palette] : []),
-    ...(duplicatePalette ? [palette] : []),
-    ...(unknownCriticalChunk ? [fixturePngChunk('ABCD', Buffer.alloc(0))] : []),
-    fixturePngChunk('IDAT', deflateSync(Buffer.from([0, 0]))),
-    fixturePngChunk('IEND', Buffer.alloc(0)),
-  ]);
-}
-
-async function writeMinimumFormalArtifacts(root: string): Promise {
-  for (const [relativePath, content] of Object.entries(
-    minimumFormalArtifactContents,
-  )) {
-    const targetPath = path.join(root, ...relativePath.split('/'));
-    await mkdir(path.dirname(targetPath), { recursive: true });
-    await writeFile(targetPath, content);
-  }
-  const revision = 8;
-  const reportPath =
-    '.agent/runtime/browser-validations/publish-package/test-run/8/validation.json';
-  const desktopPath =
-    '.agent/runtime/browser-validations/publish-package/test-run/8/desktop.png';
-  const mobilePath =
-    '.agent/runtime/browser-validations/publish-package/test-run/8/mobile.png';
-  await mkdir(path.join(root, '.agent', 'runtime'), { recursive: true });
-  await writeFile(
-    path.join(root, '.agent', 'manifest.json'),
-    `${JSON.stringify({
-      tasks: requiredSwarmManifestTaskIds.map((id) => ({
-        id,
-        status: 'completed',
-      })),
-    })}\n`,
-  );
-  await writeFile(
-    path.join(root, '.agent', 'runtime', 'project-revision.json'),
-    `${JSON.stringify({ revision })}\n`,
-  );
-  const evidenceDirectory = path.dirname(path.join(root, reportPath));
-  await mkdir(evidenceDirectory, { recursive: true });
-  await writeFile(path.join(root, desktopPath), fixturePng(1280, 720));
-  await writeFile(path.join(root, mobilePath), fixturePng(390, 844));
-  await writeFile(
-    path.join(root, reportPath),
-    `${JSON.stringify({
-      passed: true,
-      playtest: { passed: true },
-      viewportResults: [
-        { viewport: 'desktop', passed: true },
-        { viewport: 'mobile', passed: true },
-      ],
-    })}\n`,
-  );
-  await writeFile(
-    path.join(root, '.agent', 'agent.db'),
-    [
-      JSON.stringify({
-        recordType: 'agent.runtime.command.run_limited',
-        commandId: 'game.static_smoke',
-        status: 'completed',
-        revision,
-      }),
-      JSON.stringify({
-        recordType: 'agent.runtime.preview.validation',
-        passed: true,
-        playtestPassed: true,
-        revision,
-        reportPath,
-        screenshots: [desktopPath, mobilePath],
-        updatedAt: 1,
-      }),
-      '',
-    ].join('\n'),
-  );
-}
-
-async function writeReadyTaskExactlyOnceEvidence(
-  root: string,
-  parentRunId: string,
-): Promise {
-  const databasePath = path.join(root, '.agent', 'agent.db');
-  const database = await readFile(databasePath, 'utf8');
-  const records: Array> = [];
-  for (const taskId of requiredSwarmManifestTaskIds) {
-    const runId = `autonomous-ready-${taskId}-fixture`;
-    const taskDirectory = path.join(root, '.agent', 'runtime', 'tasks');
-    await mkdir(taskDirectory, { recursive: true });
-    await writeFile(
-      path.join(taskDirectory, `${taskId}.jsonl`),
-      `${JSON.stringify({
-        agentId: taskId,
-        taskId,
-        runId,
-        source: 'agent-ready-task-scheduler',
-        runProfile: 'autonomous-game-build',
-        parentAgentId: 'project-supervisor',
-        parentRunId,
-        status: 'completed',
-        phase: 'completed',
-      })}\n`,
-    );
-    records.push(
-      {
-        recordType: 'agent.runtime.background_task',
-        agentId: taskId,
-        taskId,
-        runId,
-        source: 'agent-ready-task-scheduler',
-      },
-      {
-        recordType: 'agent.runtime.background_task.completed',
-        agentId: taskId,
-        taskId,
-        runId,
-        source: 'agent-ready-task-scheduler',
-      },
-      {
-        recordType: 'agent.runtime.autonomous_ready_task.manifest_projected',
-        agentId: taskId,
-        taskId,
-        runId,
-        source: 'agent-ready-task-scheduler',
-        parentAgentId: 'project-supervisor',
-        parentRunId,
-        terminalPhase: 'completed',
-        manifestStatus: 'completed',
-      },
-    );
-  }
-  await writeFile(
-    databasePath,
-    `${database.trimEnd()}\n${records.map((record) => JSON.stringify(record)).join('\n')}\n`,
-  );
-}
-
-describe('terminal configuration wizard arguments', () => {
-  it('parses the supported options and documented defaults', () => {
-    const configDir = path.resolve('fixture-config');
-
-    expect(parseConfigWizardArguments([])).toEqual({
-      configDir: null,
-      configureOnly: false,
-      help: false,
-    });
-    expect(
-      parseConfigWizardArguments([
-        '--config-dir',
-        configDir,
-        '--configure-only',
-        '--help',
-      ]),
-    ).toEqual({ configDir, configureOnly: true, help: true });
-    expect(parseConfigWizardArguments(['-h']).help).toBe(true);
-  });
-
-  it.each([
-    ['separate API key argument', ['--api-key', 'fixture-secret'], 'API Key'],
-    ['inline API key argument', ['--api-key=fixture-secret'], 'API Key'],
-    ['relative config directory', ['--config-dir', 'relative'], '绝对路径'],
-    ['missing config directory', ['--config-dir'], '缺少目录路径'],
-    [
-      'duplicate config directory',
-      ['--config-dir', '/first', '--config-dir', '/second'],
-      '只能指定一次',
-    ],
-    ['unknown option', ['--unknown'], '未知选项'],
-  ])('rejects %s', (_label, args, marker) => {
-    expect(() => parseConfigWizardArguments(args)).toThrow(marker);
-  });
-});
-
-describe('terminal configuration wizard AppData paths', () => {
-  it.each([
-    {
-      label: 'Linux XDG config',
-      context: {
-        platform: 'linux',
-        environment: { XDG_CONFIG_HOME: '/fixture/xdg' },
-        homeDirectory: '/fixture/home',
-      },
-      candidate: path.posix.join('/fixture/xdg', appIdentifier),
-    },
-    {
-      label: 'macOS Application Support',
-      context: {
-        platform: 'darwin',
-        environment: {},
-        homeDirectory: '/Users/fixture',
-      },
-      candidate: path.posix.join(
-        '/Users/fixture',
-        'Library',
-        'Application Support',
-        appIdentifier,
-      ),
-    },
-    {
-      label: 'Windows roaming AppData',
-      context: {
-        platform: 'win32',
-        environment: {
-          APPDATA: 'C:\\Users\\fixture\\AppData\\Roaming',
-          LOCALAPPDATA: 'C:\\Users\\fixture\\AppData\\Local',
-        },
-        homeDirectory: 'C:\\Users\\fixture',
-      },
-      candidate: path.win32.join(
-        'C:\\Users\\fixture\\AppData\\Roaming',
-        appIdentifier,
-      ),
-    },
-  ])(
-    'resolves the GUI-compatible $label directory',
-    ({ context, candidate }) => {
-      expect(resolveGameCreatorAppConfigDir(context)).toBe(candidate);
-    },
-  );
-
-  it('uses an explicit absolute directory and rejects a relative one', () => {
-    const explicitConfigDir = path.resolve('explicit-config');
-
-    expect(resolveGameCreatorAppConfigDir({ explicitConfigDir })).toBe(
-      explicitConfigDir,
-    );
-    expect(() =>
-      resolveGameCreatorAppConfigDir({ explicitConfigDir: 'relative-config' }),
-    ).toThrow('绝对路径');
-  });
-});
-
-describe('terminal configuration wizard providers', () => {
-  it('publishes the supported provider presets exactly', () => {
-    expect(gameCreatorProviderPresets).toEqual([
-      {
-        id: 'openai',
-        label: 'OpenAI',
-        baseUrl: 'https://api.openai.com/v1',
-        model: 'gpt-4.1',
-        apiKind: 'openai_responses',
-      },
-      {
-        id: 'deepseek',
-        label: 'DeepSeek',
-        baseUrl: 'https://api.deepseek.com',
-        model: 'deepseek-chat',
-        apiKind: 'openai_chat',
-      },
-      {
-        id: 'anthropic',
-        label: 'Anthropic',
-        baseUrl: 'https://api.anthropic.com',
-        model: 'claude-3-5-sonnet-latest',
-        apiKind: 'anthropic',
-      },
-      {
-        id: 'ark',
-        label: '火山 Ark',
-        baseUrl: 'https://ark.cn-beijing.volces.com/api/v3',
-        model: 'doubao-seed-1-6',
-        apiKind: 'openai_chat',
-      },
-      {
-        id: 'custom',
-        label: '自定义',
-        baseUrl: '',
-        model: '',
-        apiKind: 'openai_chat',
-      },
-    ]);
-    expect(Object.isFrozen(gameCreatorProviderPresets)).toBe(true);
-  });
-});
-
-describe('terminal configuration wizard config merge', () => {
-  it('updates the default LLM while retaining per-Agent and Editor API config', () => {
-    const existingConfig = {
-      schemaVersion: 'game-creator-config.v2',
-      agentMode: 'codex_app_server',
-      llm: {
-        apiKey: 'old-fixture-secret',
-        model: 'old-model',
-        maxRetries: 2,
-      },
-      agentLlm: {
-        'project-supervisor': { providerId: 'supervisor-provider' },
-      },
-      editorApi: {
-        apiKey: 'fixture-editor-secret',
-        baseUrl: 'https://editor.example.test/v1',
-      },
-    };
-    const originalConfig = structuredClone(existingConfig);
-
-    const merged = buildGameCreatorWizardConfig(existingConfig, {
-      apiKey: '  new-fixture-secret  ',
-      baseUrl: 'https://llm.example.test/v1/',
-      model: '  fixture-model  ',
-      apiKind: 'openai_chat',
-    });
-
-    expect(merged).toEqual({
-      ...originalConfig,
-      agentMode: 'provider',
-      llm: {
-        apiKey: 'new-fixture-secret',
-        model: 'fixture-model',
-        maxRetries: 2,
-        baseUrl: 'https://llm.example.test/v1',
-        apiKind: 'openai_chat',
-        reasoningEffort: 'high',
-      },
-    });
-    expect(merged.agentLlm).toEqual(originalConfig.agentLlm);
-    expect(merged.editorApi).toEqual(originalConfig.editorApi);
-    expect(existingConfig).toEqual(originalConfig);
-  });
-
-  it('uses provider-default reasoning for Anthropic', () => {
-    expect(
-      buildGameCreatorWizardConfig(
-        { llm: { webSearchEnabled: true } },
-        {
-          apiKey: 'fixture-secret',
-          baseUrl: 'https://api.anthropic.com/',
-          model: 'claude-fixture',
-          apiKind: 'anthropic',
-        },
-      ).llm,
-    ).toMatchObject({ reasoningEffort: 'default', webSearchEnabled: false });
-  });
-});
-
-describe('terminal configuration wizard URL safety', () => {
-  it.each([
-    ['https://provider.example.test/v1///', 'https://provider.example.test/v1'],
-    ['http://127.0.0.1:8080/v1/', 'http://127.0.0.1:8080/v1'],
-    ['http://localhost:8080/', 'http://localhost:8080'],
-    ['http://[::1]:8080/v1/', 'http://[::1]:8080/v1'],
-  ])('accepts %s', (input, expected) => {
-    expect(normalizeWizardBaseUrl(input)).toBe(expected);
-  });
-
-  it.each([
-    '',
-    'not-a-url',
-    'http://provider.example.test/v1',
-    'ftp://provider.example.test/v1',
-    'https://user:password@provider.example.test/v1',
-    'https://provider.example.test/v1?token=fixture',
-    'https://provider.example.test/v1#fragment',
-  ])('rejects unsafe Base URL %s', (input) => {
-    expect(() => normalizeWizardBaseUrl(input)).toThrowError();
-  });
-});
-
-describe('terminal configuration wizard persistence', () => {
-  it('atomically replaces a private AppData config file', async () => {
-    await withTemporaryRoot(async (root) => {
-      const configDir = path.join(root, 'nested', appIdentifier);
-      const configPath = path.join(configDir, configFileName);
-      const firstConfig = { llm: { model: 'first-fixture-model' } };
-      const finalConfig = {
-        llm: { model: 'final-fixture-model', apiKey: 'fixture-secret' },
-        agentLlm: { 'project-supervisor': { model: 'agent-fixture-model' } },
-      };
-
-      await expect(
-        writeGameCreatorConfigAtomically(
-          configPath,
-          firstConfig,
-          skippedWindowsAclOptions,
-        ),
-      ).resolves.toBe(configPath);
-      const firstMetadata = await lstat(configPath);
-      await expect(
-        writeGameCreatorConfigAtomically(
-          configPath,
-          finalConfig,
-          skippedWindowsAclOptions,
-        ),
-      ).resolves.toBe(configPath);
-
-      const [directoryMetadata, finalMetadata, entries, contents] =
-        await Promise.all([
-          lstat(configDir),
-          lstat(configPath),
-          readdir(configDir),
-          readFile(configPath, 'utf8'),
-        ]);
-      expect(JSON.parse(contents)).toEqual(finalConfig);
-      expect(contents.endsWith('\n')).toBe(true);
-      expect(entries).toEqual([configFileName]);
-      expect(finalMetadata.isFile()).toBe(true);
-      expect(finalMetadata.isSymbolicLink()).toBe(false);
-      if (process.platform !== 'win32') {
-        expect(directoryMetadata.mode & 0o077).toBe(0);
-        expect(finalMetadata.mode & 0o077).toBe(0);
-        expect([finalMetadata.dev, finalMetadata.ino]).not.toEqual([
-          firstMetadata.dev,
-          firstMetadata.ino,
-        ]);
-      }
-    });
-  });
-
-  it('moves the default LLM to primary config and lets later GUI saves win', async () => {
-    await withTemporaryRoot(async (root) => {
-      const configDir = path.join(root, appIdentifier);
-      const primaryPath = path.join(configDir, configFileName);
-      const localPath = path.join(configDir, localConfigFileName);
-      await mkdir(configDir);
-      await writeFile(
-        primaryPath,
-        '{"llm":{"model":"primary","requestTimeoutMs":12345},"editorApi":{"apiKey":"canvas"}}\n',
-      );
-      await writeFile(
-        localPath,
-        '{"llm":{"model":"stale-local","stream":true},"agentLlm":{"planner":{"model":"planner"}}}\n',
-      );
-
-      const state = await readGameCreatorWizardConfigState(
-        configDir,
-        skippedWindowsAclOptions,
-      );
-      expect(state.configPath).toBe(primaryPath);
-      expect(state.effectiveConfig.llm.model).toBe('stale-local');
-      const wizardConfig = buildGameCreatorWizardConfig(state.writeConfig, {
-        apiKey: 'wizard-key',
-        baseUrl: 'https://provider.example.test/v1',
-        model: 'wizard-model',
-        apiKind: 'openai_chat',
-      });
-      await writeGameCreatorWizardConfig(
-        state,
-        wizardConfig,
-        skippedWindowsAclOptions,
-      );
-
-      const sanitizedLocal = JSON.parse(await readFile(localPath, 'utf8'));
-      expect(sanitizedLocal.llm).toBeUndefined();
-      expect(sanitizedLocal.agentLlm.planner.model).toBe('planner');
-      const afterWizard = await readGameCreatorWizardConfigState(
-        configDir,
-        skippedWindowsAclOptions,
-      );
-      expect(afterWizard.effectiveConfig.llm).toMatchObject({
-        apiKey: 'wizard-key',
-        model: 'wizard-model',
-        stream: true,
-        requestTimeoutMs: 12345,
-      });
-
-      const guiConfig = JSON.parse(await readFile(primaryPath, 'utf8'));
-      guiConfig.llm = {
-        ...guiConfig.llm,
-        apiKey: 'gui-key',
-        model: 'gui-model',
-      };
-      await writeGameCreatorConfigAtomically(
-        primaryPath,
-        guiConfig,
-        skippedWindowsAclOptions,
-      );
-      const afterGui = await readGameCreatorWizardConfigState(
-        configDir,
-        skippedWindowsAclOptions,
-      );
-      expect(afterGui.effectiveConfig.llm.apiKey).toBe('gui-key');
-      expect(afterGui.effectiveConfig.llm.model).toBe('gui-model');
-    });
-  });
-});
-
-describe('Swarm test argument parsing', () => {
-  it('returns the documented defaults', () => {
-    expect(parseSwarmTestArguments([])).toEqual({
-      configDir: null,
-      projectDir: null,
-      keepProject: false,
-      openBrowser: true,
-      task: null,
-      timeoutMinutes: null,
-      dryRun: false,
-      help: false,
-    });
-  });
-
-  it('parses every supported option', () => {
-    const configDir = path.resolve('fixture-config');
-    const projectDir = path.resolve('fixture-project');
-
-    expect(
-      parseSwarmTestArguments([
-        '--config-dir',
-        configDir,
-        '--project-dir',
-        projectDir,
-        '--keep-project',
-        '--no-open',
-        '--task',
-        '生成一款可试玩的塔防游戏',
-        '--timeout-minutes',
-        '75',
-        '--dry-run',
-        '--help',
-      ]),
-    ).toEqual({
-      configDir,
-      projectDir,
-      keepProject: true,
-      openBrowser: false,
-      task: '生成一款可试玩的塔防游戏',
-      timeoutMinutes: 75,
-      dryRun: true,
-      help: true,
-    });
-    expect(parseSwarmTestArguments(['-h']).help).toBe(true);
-    expect(() => parseSwarmTestArguments(['--task', ''])).toThrowError();
-  });
-
-  it('auto-answers only the confirmation and choice prompts', () => {
-    expect(
-      nextSwarmAutoPilotReply(
-        '[待确认] agent=project-supervisor run=r action=a tool=agent.delegate\n输入 approve 或 reject:',
-      ),
-    ).toBe('approve');
-    expect(nextSwarmAutoPilotReply('请选择 1-3,或直接输入其他答案:')).toBe(
-      '1',
-    );
-    // 提示词已经被上一轮消费掉、不在缓冲末尾时不得重复应答。
-    expect(
-      nextSwarmAutoPilotReply('输入 approve 或 reject:\n[已批准] action-1\n'),
-    ).toBeNull();
-    expect(
-      nextSwarmAutoPilotReply('[状态] project-supervisor running'),
-    ).toBeNull();
-  });
-
-  it('closes the driven stdin only at the prompt that follows submission', () => {
-    // CLI 先打印提示符再读行:第一个「你>」是用来读我们这条任务的,此时本轮还没
-    // 开始跑。在那里收 stdin,确认卡弹出来时已经是 EOF,自动应答器根本没机会回
-    // approve——实测就这样把一轮跑成了 pending-confirmation。
-    expect(swarmAutoPilotSitsAtPrompt('你> ')).toBe(true);
-    expect(swarmAutoPilotShouldCloseInput('你> ', 0)).toBe(false);
-    expect(swarmAutoPilotShouldCloseInput('你> ', 1)).toBe(true);
-    // 总控判定直接回复那条路径没有 turn 回执,同样靠这个提示符收口。
-    expect(swarmAutoPilotShouldCloseInput('[意图] reply\n\n你> ', 1)).toBe(
-      true,
-    );
-    // 提示符不在缓冲末尾、或本轮还在推进时不得收 stdin。
-    expect(
-      swarmAutoPilotSitsAtPrompt(
-        '你> [决策] Agent 正在判断直接回复或调用持久能力。',
-      ),
-    ).toBe(false);
-    expect(
-      swarmAutoPilotSitsAtPrompt('[状态] project-supervisor running/planning'),
-    ).toBe(false);
-  });
-
-  it('keeps persistent preview only for manual chat mode', () => {
-    expect(shouldStartPersistentPreview(parseSwarmTestArguments([]))).toBe(
-      true,
-    );
-    expect(
-      shouldStartPersistentPreview(
-        parseSwarmTestArguments(['--task', '生成一款可试玩的塔防游戏']),
-      ),
-    ).toBe(false);
-  });
-
-  it('applies a bounded default only to non-interactive tasks', () => {
-    expect(resolveSwarmTestTimeoutMs(parseSwarmTestArguments([]))).toBeNull();
-    expect(
-      resolveSwarmTestTimeoutMs(
-        parseSwarmTestArguments(['--task', '生成原创塔防游戏']),
-      ),
-    ).toBe(50 * 60_000);
-    expect(
-      resolveSwarmTestTimeoutMs(
-        parseSwarmTestArguments(['--timeout-minutes', '12']),
-      ),
-    ).toBe(12 * 60_000);
-  });
-
-  it.each([
-    {
-      label: 'duplicate config directory',
-      args: ['--config-dir', '/first', '--config-dir', '/second'],
-      marker: '--config-dir',
-    },
-    {
-      label: 'duplicate project directory',
-      args: ['--project-dir', '/first', '--project-dir', '/second'],
-      marker: '--project-dir',
-    },
-    {
-      label: 'missing config directory',
-      args: ['--config-dir'],
-      marker: '--config-dir',
-    },
-    {
-      label: 'missing project directory',
-      args: ['--project-dir', '--dry-run'],
-      marker: '--project-dir',
-    },
-    {
-      label: 'duplicate timeout',
-      args: ['--timeout-minutes', '10', '--timeout-minutes', '20'],
-      marker: '--timeout-minutes',
-    },
-    {
-      label: 'invalid timeout',
-      args: ['--timeout-minutes', '0'],
-      marker: '1-1440',
-    },
-    {
-      label: 'unknown option',
-      args: ['--unsupported'],
-      marker: '--unsupported',
-    },
-  ])('rejects $label', ({ args, marker }) => {
-    expect(() => parseSwarmTestArguments(args)).toThrow(marker);
-  });
-});
-
-describe('runtime config directory candidates', () => {
-  it('uses an absolute Linux XDG config root', () => {
-    expect(
-      defaultRuntimeConfigDirCandidates({
-        platform: 'linux',
-        environment: { XDG_CONFIG_HOME: '/fixture/xdg' },
-        homeDirectory: '/fixture/home',
-      }),
-    ).toEqual([path.posix.join('/fixture/xdg', appIdentifier)]);
-  });
-
-  it('falls back to the Linux home config root', () => {
-    expect(
-      defaultRuntimeConfigDirCandidates({
-        platform: 'linux',
-        environment: { XDG_CONFIG_HOME: 'relative-xdg' },
-        homeDirectory: '/fixture/home',
-      }),
-    ).toEqual([path.posix.join('/fixture/home', '.config', appIdentifier)]);
-  });
-
-  it('uses the macOS Application Support directory', () => {
-    expect(
-      defaultRuntimeConfigDirCandidates({
-        platform: 'darwin',
-        environment: {},
-        homeDirectory: '/Users/fixture',
-      }),
-    ).toEqual([
-      path.posix.join(
-        '/Users/fixture',
-        'Library',
-        'Application Support',
-        appIdentifier,
-      ),
-    ]);
-  });
-
-  it('uses both Windows roaming and local AppData directories', () => {
-    const appData = 'C:\\Users\\fixture\\AppData\\Roaming';
-    const localAppData = 'C:\\Users\\fixture\\AppData\\Local';
-
-    expect(
-      defaultRuntimeConfigDirCandidates({
-        platform: 'win32',
-        environment: {
-          APPDATA: appData,
-          LOCALAPPDATA: localAppData,
-        },
-        homeDirectory: 'C:\\Users\\fixture',
-      }),
-    ).toEqual([
-      path.win32.join(appData, appIdentifier),
-      path.win32.join(localAppData, appIdentifier),
-    ]);
-  });
-});
-
-describe('runtime config discovery', () => {
-  it('passes an empty explicit config directory through the TTY wizard only', () => {
-    const explicitConfigDir = path.resolve('empty-explicit-config');
-    const argumentsForWizard =
-      buildMissingConfigWizardArguments(explicitConfigDir);
-
-    expect(argumentsForWizard.slice(1)).toEqual([
-      '--configure-only',
-      '--config-dir',
-      explicitConfigDir,
-    ]);
-    expect(argumentsForWizard[0]).toMatch(/game-creator-config-wizard\.mjs$/u);
-    expect(canPromptForMissingRuntimeConfig(true, true)).toBe(true);
-    expect(canPromptForMissingRuntimeConfig(false, true)).toBe(false);
-    expect(canPromptForMissingRuntimeConfig(true, false)).toBe(false);
-  });
-
-  it('discovers an explicitly selected config directory', async () => {
-    await withTemporaryRoot(async (root) => {
-      const configDir = path.join(root, 'explicit-config');
-      await mkdir(configDir);
-      await writeFile(path.join(configDir, configFileName), '{}\n');
-
-      await expect(
-        discoverRuntimeConfigDir(configDir, {
-          platform: 'linux',
-          environment: { XDG_CONFIG_HOME: path.join(root, 'unused') },
-          homeDirectory: path.join(root, 'unused-home'),
-        }),
-      ).resolves.toBe(await realpath(configDir));
-    });
-  });
-
-  it('discovers a config directory from an isolated XDG root', async () => {
-    await withTemporaryRoot(async (root) => {
-      const xdgRoot = path.join(root, 'xdg');
-      const configDir = path.join(xdgRoot, appIdentifier);
-      await mkdir(configDir, { recursive: true });
-      await writeFile(path.join(configDir, configFileName), '{}\n');
-
-      await expect(
-        discoverRuntimeConfigDir(null, {
-          platform: 'linux',
-          environment: { XDG_CONFIG_HOME: xdgRoot },
-          homeDirectory: path.join(root, 'unused-home'),
-        }),
-      ).resolves.toBe(await realpath(configDir));
-    });
-  });
-
-  it.skipIf(process.platform === 'win32')(
-    'rejects symlinked and non-file config entries',
-    async () => {
-      await withTemporaryRoot(async (root) => {
-        const target = path.join(root, 'config-target.json');
-        await writeFile(target, '{}\n');
-
-        const symlinkConfigDir = path.join(root, 'symlink-config');
-        await mkdir(symlinkConfigDir);
-        await symlink(target, path.join(symlinkConfigDir, configFileName));
-        await expect(
-          discoverRuntimeConfigDir(symlinkConfigDir),
-        ).rejects.toThrow(configFileName);
-
-        const directoryConfigDir = path.join(root, 'directory-config');
-        await mkdir(path.join(directoryConfigDir, configFileName), {
-          recursive: true,
-        });
-        await expect(
-          discoverRuntimeConfigDir(directoryConfigDir),
-        ).rejects.toThrow(configFileName);
-      });
-    },
-  );
-
-  it('rejects a relative explicit config directory', async () => {
-    await expect(discoverRuntimeConfigDir('relative-config')).rejects.toThrow(
-      '--config-dir',
-    );
-  });
-});
-
-describe('isolated Swarm runtime config', () => {
-  it('privately copies only active config files and removes the owned directory', async () => {
-    await withTemporaryRoot(async (root) => {
-      const sourceConfigDir = path.join(root, 'source-config');
-      await mkdir(sourceConfigDir);
-      await writeFile(
-        path.join(sourceConfigDir, configFileName),
-        '{"llm":{"apiKey":"fixture-credential"}}\n',
-        { mode: 0o600 },
-      );
-      await writeFile(
-        path.join(sourceConfigDir, localConfigFileName),
-        '{"llm":{"model":"fixture-model"}}\n',
-        { mode: 0o600 },
-      );
-      await writeFile(
-        path.join(sourceConfigDir, runnerEndpointFileName),
-        '{"mustNotCopy":true}\n',
-      );
-      await writeFile(
-        path.join(sourceConfigDir, 'agent-runner.lock'),
-        'must-not-copy\n',
-      );
-      await writeFile(
-        path.join(sourceConfigDir, `.${configFileName}.previous`),
-        'must-not-copy\n',
-      );
-      const sourceMetadata = await lstat(
-        path.join(sourceConfigDir, configFileName),
-      );
-
-      const runtimeConfig = await prepareSwarmTestRuntimeConfig(
-        sourceConfigDir,
-        root,
-        skippedWindowsAclOptions,
-      );
-      const runtimeEntries = (await readdir(runtimeConfig.path)).sort();
-      const runtimeDirectoryMetadata = await lstat(runtimeConfig.path);
-      const primaryMetadata = await lstat(
-        path.join(runtimeConfig.path, configFileName),
-      );
-      const localMetadata = await lstat(
-        path.join(runtimeConfig.path, localConfigFileName),
-      );
-      const sentinel = JSON.parse(
-        await readFile(
-          path.join(runtimeConfig.path, testRuntimeConfigSentinelName),
-          'utf8',
-        ),
-      );
-
-      expect(runtimeConfig.sourcePath).toBe(await realpath(sourceConfigDir));
-      expect(path.basename(runtimeConfig.path)).toMatch(
-        new RegExp(`^${testRuntimeConfigPrefix}`),
-      );
-      expect(runtimeEntries).toEqual(
-        [
-          configFileName,
-          localConfigFileName,
-          testRuntimeConfigSentinelName,
-        ].sort(),
-      );
-      expect(
-        await readFile(path.join(runtimeConfig.path, configFileName), 'utf8'),
-      ).toBe('{"llm":{"apiKey":"fixture-credential"}}\n');
-      expect(
-        await readFile(
-          path.join(runtimeConfig.path, localConfigFileName),
-          'utf8',
-        ),
-      ).toBe('{"llm":{"model":"fixture-model"}}\n');
-      expect(sentinel).toEqual({
-        schemaVersion: testRuntimeConfigSentinelSchema,
-        token: runtimeConfig.sentinelToken,
-      });
-      if (process.platform !== 'win32') {
-        expect(runtimeDirectoryMetadata.mode & 0o077).toBe(0);
-        expect(primaryMetadata.mode & 0o077).toBe(0);
-        expect(localMetadata.mode & 0o077).toBe(0);
-        expect([primaryMetadata.dev, primaryMetadata.ino]).not.toEqual([
-          sourceMetadata.dev,
-          sourceMetadata.ino,
-        ]);
-      }
-      expect(
-        await pathExists(path.join(runtimeConfig.path, runnerEndpointFileName)),
-      ).toBe(false);
-
-      await expect(cleanupSwarmTestRuntimeConfig(runtimeConfig)).resolves.toBe(
-        true,
-      );
-      expect(await pathExists(runtimeConfig.path)).toBe(false);
-      expect(await pathExists(sourceConfigDir)).toBe(true);
-      expect(
-        await readFile(path.join(sourceConfigDir, configFileName), 'utf8'),
-      ).toBe('{"llm":{"apiKey":"fixture-credential"}}\n');
-    });
-  });
-
-  it('rejects a symlinked local override without leaving a temporary directory', async () => {
-    if (process.platform === 'win32') return;
-    await withTemporaryRoot(async (root) => {
-      const sourceConfigDir = path.join(root, 'source-config');
-      const localTarget = path.join(root, 'local-target.json');
-      await mkdir(sourceConfigDir);
-      await writeFile(path.join(sourceConfigDir, configFileName), '{}\n');
-      await writeFile(localTarget, '{}\n');
-      await symlink(
-        localTarget,
-        path.join(sourceConfigDir, localConfigFileName),
-      );
-
-      await expect(
-        prepareSwarmTestRuntimeConfig(
-          sourceConfigDir,
-          root,
-          skippedWindowsAclOptions,
-        ),
-      ).rejects.toThrow(localConfigFileName);
-      expect(
-        (await readdir(root)).filter((entry) =>
-          entry.startsWith(testRuntimeConfigPrefix),
-        ),
-      ).toEqual([]);
-    });
-  });
-
-  it('refuses to delete an isolated config while its Runner endpoint exists', async () => {
-    await withTemporaryRoot(async (root) => {
-      const sourceConfigDir = path.join(root, 'source-config');
-      await mkdir(sourceConfigDir);
-      await writeFile(path.join(sourceConfigDir, configFileName), '{}\n');
-      const runtimeConfig = await prepareSwarmTestRuntimeConfig(
-        sourceConfigDir,
-        root,
-        skippedWindowsAclOptions,
-      );
-      const endpointPath = path.join(
-        runtimeConfig.path,
-        runnerEndpointFileName,
-      );
-      await writeFile(endpointPath, '{}\n');
-
-      await expect(
-        cleanupSwarmTestRuntimeConfig(runtimeConfig),
-      ).rejects.toThrow('Runner');
-      expect(await pathExists(runtimeConfig.path)).toBe(true);
-
-      await rm(endpointPath);
-      await expect(cleanupSwarmTestRuntimeConfig(runtimeConfig)).resolves.toBe(
-        true,
-      );
-    });
-  });
-});
-
-describe('Swarm test project ownership', () => {
-  it('creates a sentinel-owned project and removes it during cleanup', async () => {
-    await withTemporaryRoot(async (root) => {
-      const project = await prepareSwarmTestProject(null, root);
-      const sentinelPath = path.join(project.path, testProjectSentinelName);
-      const sentinelMetadata = await lstat(sentinelPath);
-      const sentinel = JSON.parse(await readFile(sentinelPath, 'utf8'));
-
-      expect(project.owned).toBe(true);
-      expect(project.sentinelToken).toEqual(expect.any(String));
-      expect(path.basename(project.path)).toMatch(
-        new RegExp(`^${testProjectPrefix}`),
-      );
-      expect(sentinelMetadata.isFile()).toBe(true);
-      expect(sentinelMetadata.isSymbolicLink()).toBe(false);
-      expect(sentinel).toEqual({
-        schemaVersion: testProjectSentinelSchema,
-        token: project.sentinelToken,
-      });
-
-      await expect(cleanupSwarmTestProject(project)).resolves.toBe(true);
-      expect(await pathExists(project.path)).toBe(false);
-    });
-  });
-
-  it('refuses cleanup after the sentinel identity is changed', async () => {
-    await withTemporaryRoot(async (root) => {
-      const project = await prepareSwarmTestProject(null, root);
-      await writeFile(
-        path.join(project.path, testProjectSentinelName),
-        `${JSON.stringify({
-          schemaVersion: testProjectSentinelSchema,
-          token: 'changed-token',
-        })}\n`,
-      );
-
-      await expect(cleanupSwarmTestProject(project)).rejects.toThrowError();
-      expect(await pathExists(project.path)).toBe(true);
-    });
-  });
-
-  it('keeps an explicit empty directory unowned', async () => {
-    await withTemporaryRoot(async (root) => {
-      const explicitProjectDir = path.join(root, 'explicit-project');
-      await mkdir(explicitProjectDir);
-
-      const project = await prepareSwarmTestProject(explicitProjectDir, root);
-
-      expect(project).toEqual({
-        path: await realpath(explicitProjectDir),
-        owned: false,
-        sentinelToken: null,
-      });
-      await expect(cleanupSwarmTestProject(project)).resolves.toBe(false);
-      expect(await readdir(explicitProjectDir)).toEqual([]);
-    });
-  });
-
-  it('rejects a non-empty uninitialized explicit directory', async () => {
-    await withTemporaryRoot(async (root) => {
-      const explicitProjectDir = path.join(root, 'uninitialized-project');
-      await mkdir(explicitProjectDir);
-      await writeFile(path.join(explicitProjectDir, 'existing.txt'), 'fixture');
-
-      await expect(
-        prepareSwarmTestProject(explicitProjectDir, root),
-      ).rejects.toThrow('--project-dir');
-    });
-  });
-});
-
-describe('cargo CLI argument construction', () => {
-  it('uses the shell manifest and separates cargo from application arguments', () => {
-    const cliArguments = ['--config-dir', 'fixture-config', '--llm-status'];
-    const cargoArguments = buildCargoCliArguments(cliArguments);
-
-    // Assert the separator invariants rather than fixed positions: cargo flags
-    // may be added before `--`, but everything after it must reach the CLI
-    // unchanged, and the manifest must stay this shell's own.
-    const separatorIndex = cargoArguments.indexOf('--');
-    expect(cargoArguments[0]).toBe('run');
-    expect(separatorIndex).toBeGreaterThan(0);
-    expect(cargoArguments.slice(separatorIndex + 1)).toEqual(cliArguments);
-
-    const manifestIndex = cargoArguments.indexOf('--manifest-path');
-    expect(manifestIndex).toBeGreaterThan(0);
-    expect(manifestIndex).toBeLessThan(separatorIndex);
-    expect(path.isAbsolute(cargoArguments[manifestIndex + 1])).toBe(true);
-    expect(path.relative(appRoot, cargoArguments[manifestIndex + 1])).toBe(
-      path.join('src-tauri', 'Cargo.toml'),
-    );
-  });
-
-  it('keeps cargo build chatter out of the run output', () => {
-    // The dead-code warnings are reprinted on every spawn; without --quiet they
-    // bury the swarm output this script exists to surface.
-    const cargoArguments = buildCargoCliArguments(['--llm-status']);
-    const separatorIndex = cargoArguments.indexOf('--');
-
-    expect(cargoArguments.slice(0, separatorIndex)).toContain('--quiet');
-  });
-
-  it('parses the idle Runner shutdown marker', () => {
-    expect(parseRunnerShutdownOutput('runner.stopped=true\n')).toBe(true);
-    expect(parseRunnerShutdownOutput('runner.stopped=false\n')).toBe(false);
-    expect(() => parseRunnerShutdownOutput('runner.status=unknown\n')).toThrow(
-      'stopped',
-    );
-  });
-});
-
-describe('bounded process-tree termination', () => {
-  it('kills a POSIX process group after its leader exits with stdio still open', async () => {
-    if (process.platform === 'win32') return;
-    const child = spawn(
-      process.execPath,
-      [
-        '-e',
-        `const { spawn } = require('node:child_process');
-const grandchild = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)'], {
-  stdio: 'inherit',
-});
-grandchild.once('spawn', () => {
-  process.stdout.write('grandchild-ready\\n');
-  setTimeout(() => process.exit(0), 50);
-});
-grandchild.once('error', () => process.exit(2));`,
-      ],
-      { detached: true, stdio: ['ignore', 'pipe', 'pipe'] },
-    );
-    try {
-      child.stdout.setEncoding('utf8');
-      const grandchildReady = new Promise((resolve, reject) => {
-        let output = '';
-        child.stdout.on('data', (chunk) => {
-          output += chunk;
-          if (output.includes('grandchild-ready')) resolve();
-        });
-        child.once('error', reject);
-      });
-      child.stderr.resume();
-      const leaderExited = new Promise((resolve, reject) => {
-        child.once('error', reject);
-        child.once('exit', () => resolve());
-      });
-      await Promise.all([leaderExited, grandchildReady]);
-      expect(child.exitCode).toBe(0);
-      const startedAt = Date.now();
-      await expect(
-        childExitWithTimeout(child, 20, 'fixture process tree', {
-          graceMs: 50,
-          forceWaitMs: 500,
-        }),
-      ).rejects.toMatchObject({ code: 'AGC_CHILD_TIMEOUT' });
-      expect(Date.now() - startedAt).toBeLessThan(2_000);
-    } finally {
-      await terminateChildTree(child, 'SIGKILL', true);
-    }
-  });
-
-  it('bounds recursive cleanup in an independently terminable child', async () => {
-    if (process.platform === 'win32') return;
-    const target = await mkdtemp(
-      path.join(os.tmpdir(), 'genarrative-bounded-cleanup-test-'),
-    );
-    try {
-      await expect(
-        removeDirectoryWithTimeout(target, {
-          timeoutMs: 20,
-          childProgram: 'setInterval(() => {}, 1000);',
-        }),
-      ).rejects.toMatchObject({ code: 'AGC_CHILD_TIMEOUT' });
-      expect(await pathExists(target)).toBe(true);
-    } finally {
-      await rm(target, { recursive: true, force: true });
-    }
-  });
-});
-
-describe('non-interactive Swarm turn report validation', () => {
-  const settledReport = {
-    schemaVersion: 'game-creator-swarm-turn-report.v1',
-    outcome: 'settled',
-    parentAgentId: 'project-supervisor',
-    sessionId: 'agent-session-project-supervisor',
-    parentRunId: 'swarm-project-supervisor-fixture',
-    runtimeCount: 5,
-    busyRuntimeCount: 0,
-    pendingTaskCount: 0,
-    runningTaskCount: 0,
-    waitingForConfirmationCount: 0,
-    waitingForUserInputCount: 0,
-    newAssistantMessageCount: 1,
-    finalReplyChars: 128,
-    reconciliationAgentCount: 0,
-  };
-  const outputFor = (...reports: unknown[]) =>
-    [
-      '[状态] 正在收束',
-      ...reports.map((report) => `[turn.report] ${JSON.stringify(report)}`),
-      '[完成] 本轮结束',
-    ].join('\n');
-
-  it('accepts exactly one settled report with no remaining work', () => {
-    expect(parseSettledSwarmTurnReport(outputFor(settledReport))).toEqual(
-      settledReport,
-    );
-  });
-
-  it.each([
-    ['missing', '[状态] 没有报告'],
-    ['duplicate', outputFor(settledReport, settledReport)],
-    ['malformed JSON', '[turn.report] {invalid'],
-    ['non-object JSON', '[turn.report] []'],
-    [
-      'incomplete shape',
-      outputFor(
-        (({ finalReplyChars: _removed, ...report }) => report)(settledReport),
-      ),
-    ],
-    ['unknown shape', outputFor({ ...settledReport, unexpected: true })],
-  ])('rejects a %s report', (_label, output) => {
-    expect(() => parseSettledSwarmTurnReport(output)).toThrowError();
-  });
-
-  it.each(['failed', 'incomplete', 'needs-reconciliation'])(
-    'rejects the %s outcome',
-    (outcome) => {
-      expect(() =>
-        parseSettledSwarmTurnReport(outputFor({ ...settledReport, outcome })),
-      ).toThrow(`outcome=${outcome}`);
-    },
-  );
-
-  it.each([
-    'busyRuntimeCount',
-    'pendingTaskCount',
-    'runningTaskCount',
-    'waitingForConfirmationCount',
-    'waitingForUserInputCount',
-    'reconciliationAgentCount',
-  ])('rejects non-zero %s', (field) => {
-    expect(() =>
-      parseSettledSwarmTurnReport(outputFor({ ...settledReport, [field]: 1 })),
-    ).toThrow(field);
-  });
-
-  it.each([
-    ['no Runtime', { runtimeCount: 0 }],
-    ['no assistant reply', { newAssistantMessageCount: 0 }],
-    ['multiple assistant replies', { newAssistantMessageCount: 2 }],
-    ['empty final reply', { finalReplyChars: 0 }],
-    ['implausibly large final reply', { finalReplyChars: 1_000_001 }],
-    ['fractional count', { pendingTaskCount: 0.5 }],
-    ['missing parent run', { parentRunId: null }],
-  ])('rejects %s', (_label, overrides) => {
-    expect(() =>
-      parseSettledSwarmTurnReport(
-        outputFor({ ...settledReport, ...overrides }),
-      ),
-    ).toThrowError();
-  });
-});
-
-describe('generated game entry validation', () => {
-  it('rejects the initializer placeholder and accepts generated HTML', async () => {
-    await withTemporaryRoot(async (root) => {
-      const gameDir = path.join(root, 'game');
-      const gameEntry = path.join(gameDir, 'index.html');
-      await mkdir(gameDir);
-
-      expect(await hasGeneratedGameEntry(root)).toBe(false);
-      await writeFile(
-        gameEntry,
-        `
${ungeneratedGameEntryMarker}
`, - ); - expect(await hasGeneratedGameEntry(root)).toBe(false); - - await writeFile(gameEntry, ''); - expect(await hasGeneratedGameEntry(root)).toBe(true); - }); - }); -}); - -describe('formal Swarm project artifact validation', () => { - it('reports every missing required artifact by relative path', async () => { - await withTemporaryRoot(async (root) => { - const inspection = await inspectSwarmProjectArtifacts(root); - - expect(inspection.valid).toBe(false); - expect(inspection.invalidPaths).toEqual([ - ...Object.keys(minimumFormalArtifactContents), - '.agent/manifest.json', - '.agent/runtime/project-revision.json', - '.agent/agent.db', - ]); - const validationError = await validateSwarmProjectArtifacts(root).then( - () => null, - (error: Error) => error, - ); - expect(validationError).toBeInstanceOf(Error); - for (const relativePath of Object.keys(minimumFormalArtifactContents)) { - expect(validationError?.message).toContain(relativePath); - } - - await mkdir(path.join(root, 'exports')); - await writeFile(path.join(root, 'exports', 'README.md'), ' \n'); - const emptyInspection = await inspectSwarmProjectArtifacts(root); - expect(emptyInspection.issues).toContainEqual({ - path: 'exports/README.md', - reason: '空文件', - }); - - if (process.platform !== 'win32') { - const target = path.join(root, 'project-target.md'); - await mkdir(path.join(root, 'memory')); - await writeFile(target, '# Outside artifact path\n'); - await symlink(target, path.join(root, 'memory', 'project.md')); - const symlinkInspection = await inspectSwarmProjectArtifacts(root); - expect(symlinkInspection.issues).toContainEqual({ - path: 'memory/project.md', - reason: '不是无符号链接普通文件', - }); - } - }); - }); - - it('rejects each malformed JSON artifact', async () => { - await withTemporaryRoot(async (root) => { - await writeMinimumFormalArtifacts(root); - const invalidJsonPaths = [ - 'game/balance.json', - 'assets/manifest.art.json', - 'assets/manifest.audio.json', - ]; - await Promise.all( - invalidJsonPaths.map((relativePath) => - writeFile(path.join(root, ...relativePath.split('/')), '{invalid'), - ), - ); - - const inspection = await inspectSwarmProjectArtifacts(root); - - expect(inspection.invalidPaths).toEqual(invalidJsonPaths); - expect(inspection.issues).toEqual( - invalidJsonPaths.map((relativePath) => ({ - path: relativePath, - reason: 'JSON 无法解析', - })), - ); - }); - }); - - it('rejects placeholder Markdown and empty JSON objects', async () => { - await withTemporaryRoot(async (root) => { - await writeMinimumFormalArtifacts(root); - const projectMemoryPath = path.join(root, 'memory', 'project.md'); - await writeFile(projectMemoryPath, '# TODO\n\n待补充项目说明。\n'); - let inspection = await inspectSwarmProjectArtifacts(root); - expect(inspection.issues).toContainEqual({ - path: 'memory/project.md', - reason: '仍包含占位标记', - }); - - await writeFile( - projectMemoryPath, - minimumFormalArtifactContents['memory/project.md'], - ); - const gameEntryPath = path.join(root, 'game', 'index.html'); - await writeFile( - gameEntryPath, - '\n', - ); - inspection = await inspectSwarmProjectArtifacts(root); - expect(inspection.issues).toContainEqual({ - path: 'game/index.html', - reason: '仍包含占位标记', - }); - - await writeFile( - gameEntryPath, - minimumFormalArtifactContents['game/index.html'], - ); - await writeFile(path.join(root, 'game', 'balance.json'), '{}\n'); - inspection = await inspectSwarmProjectArtifacts(root); - expect(inspection.issues).toContainEqual({ - path: 'game/balance.json', - reason: 'JSON 必须是非空对象', - }); - }); - }); - - it.each([ - ['TODO', 'TODO: 补齐导出说明'], - ['TBD', 'TBD - export notes'], - ['placeholder', 'This is a placeholder document.'], - ['coming soon', 'Release notes are coming soon.'], - ['lorem ipsum', 'Lorem ipsum dolor sit amet.'], - ['待补充', '导出说明待补充。'], - ['待完善', '移动端结论待完善。'], - ['占位', '本文件仅供流程占位。'], - ['尚未完成', '最终试玩尚未完成。'], - ['稍后补充', '截图说明稍后补充。'], - ['待填写', '版本信息待填写。'], - ['待验证', '桌面视口待验证。'], - ['待复核', '最终结论待复核。'], - ['待确认', '发布范围待确认。'], - ['待定', '交付日期待定。'], - ['unchecked checklist', '- [ ] 补齐移动端试玩记录'], - ])('rejects exports/README.md containing %s', async (_label, marker) => { - expect(hasIncompleteArtifactMarker(`# Export\n\n${marker}\n`)).toBe(true); - - await withTemporaryRoot(async (root) => { - await writeMinimumFormalArtifacts(root); - await writeFile( - path.join(root, 'exports', 'README.md'), - `# Export\n\n当前导出流程记录如下。\n\n${marker}\n`, - ); - - const inspection = await inspectSwarmProjectArtifacts(root); - - expect(inspection.issues).toContainEqual({ - path: 'exports/README.md', - reason: '仍包含占位标记', - }); - }); - }); - - it('accepts complete formal Markdown and checked task lists', async () => { - const completeReadme = [ - '# Export', - '', - '- [x] 桌面视口试玩验证已经通过', - '- [X] 移动视口试玩验证已经通过', - '', - '版本信息已经填写,静态检查、人工复核和发布范围确认均已完成。', - '', - ].join('\n'); - expect(hasIncompleteArtifactMarker(completeReadme)).toBe(false); - - await withTemporaryRoot(async (root) => { - await writeMinimumFormalArtifacts(root); - await writeFile(path.join(root, 'exports', 'README.md'), completeReadme); - - await expect(validateSwarmProjectArtifacts(root)).resolves.toMatchObject({ - valid: true, - }); - }); - }); - - it('requires all 16 manifest tasks and browser evidence for the current revision', async () => { - await withTemporaryRoot(async (root) => { - await writeMinimumFormalArtifacts(root); - const manifestPath = path.join(root, '.agent', 'manifest.json'); - const manifest = JSON.parse(await readFile(manifestPath, 'utf8')); - manifest.tasks[0].status = 'running'; - await writeFile(manifestPath, `${JSON.stringify(manifest)}\n`); - - let inspection = await inspectSwarmProjectArtifacts(root); - expect(inspection.issues).toContainEqual({ - path: '.agent/manifest.json', - reason: '固定 16 个正式任务未全部且仅完成一次', - }); - - manifest.tasks[0].status = 'completed'; - await writeFile(manifestPath, `${JSON.stringify(manifest)}\n`); - await writeFile( - path.join(root, '.agent', 'runtime', 'project-revision.json'), - '{"revision":9}\n', - ); - inspection = await inspectSwarmProjectArtifacts(root); - expect(inspection.issues).toContainEqual({ - path: '.agent/agent.db', - reason: '缺少当前 revision 的桌面与移动试玩通过凭证', - }); - }); - }); - - it('binds all 16 task lifecycles exactly once to the settled parent run', async () => { - await withTemporaryRoot(async (root) => { - const parentRunId = 'swarm-project-supervisor-exactly-once'; - await writeMinimumFormalArtifacts(root); - await writeReadyTaskExactlyOnceEvidence(root, parentRunId); - - await expect( - validateSwarmProjectArtifacts(root, { parentRunId }), - ).resolves.toMatchObject({ valid: true }); - - const databasePath = path.join(root, '.agent', 'agent.db'); - const baselineDatabase = await readFile(databasePath, 'utf8'); - const failedThenCompletedTaskId = requiredSwarmManifestTaskIds[0]; - await writeFile( - databasePath, - `${baselineDatabase}${JSON.stringify({ - recordType: 'agent.runtime.background_task.failed', - agentId: failedThenCompletedTaskId, - taskId: failedThenCompletedTaskId, - runId: `autonomous-ready-${failedThenCompletedTaskId}-fixture`, - source: 'agent-ready-task-scheduler', - })}\n`, - ); - const failedThenCompleted = await inspectSwarmProjectArtifacts(root, { - parentRunId, - }); - expect(failedThenCompleted.issues).toContainEqual({ - path: `.agent/runtime/tasks/${failedThenCompletedTaskId}.jsonl`, - reason: `正式任务 ${failedThenCompletedTaskId} 未在当前父 Run 中恰好启动并完成一次`, - }); - await writeFile(databasePath, baselineDatabase); - - const duplicateTaskId = requiredSwarmManifestTaskIds[0]; - await writeFile( - databasePath, - `${baselineDatabase}${JSON.stringify({ - recordType: 'agent.runtime.background_task', - agentId: duplicateTaskId, - taskId: duplicateTaskId, - runId: `autonomous-ready-${duplicateTaskId}-fixture`, - source: 'agent-ready-task-scheduler', - })}\n`, - ); - const duplicate = await inspectSwarmProjectArtifacts(root, { - parentRunId, - }); - expect(duplicate.issues).toContainEqual({ - path: `.agent/runtime/tasks/${duplicateTaskId}.jsonl`, - reason: `正式任务 ${duplicateTaskId} 未在当前父 Run 中恰好启动并完成一次`, - }); - - const secondRunTaskId = requiredSwarmManifestTaskIds[1]; - const journalPath = path.join( - root, - '.agent', - 'runtime', - 'tasks', - `${secondRunTaskId}.jsonl`, - ); - await writeFile( - journalPath, - `${await readFile(journalPath, 'utf8')}${JSON.stringify({ - agentId: secondRunTaskId, - taskId: secondRunTaskId, - runId: `autonomous-ready-${secondRunTaskId}-second-attempt`, - source: 'agent-ready-task-scheduler', - runProfile: 'autonomous-game-build', - parentAgentId: 'project-supervisor', - parentRunId, - status: 'completed', - phase: 'completed', - })}\n`, - ); - const secondRun = await inspectSwarmProjectArtifacts(root, { - parentRunId, - }); - expect(secondRun.invalidPaths).toContain( - `.agent/runtime/tasks/${secondRunTaskId}.jsonl`, - ); - }); - }); - - it('rejects a stale static smoke record from an older revision', async () => { - await withTemporaryRoot(async (root) => { - await writeMinimumFormalArtifacts(root); - const databasePath = path.join(root, '.agent', 'agent.db'); - const records = (await readFile(databasePath, 'utf8')) - .trim() - .split('\n') - .map((line) => JSON.parse(line)); - records[0].revision = 7; - await writeFile( - databasePath, - `${records.map((record) => JSON.stringify(record)).join('\n')}\n`, - ); - - const inspection = await inspectSwarmProjectArtifacts(root); - expect(inspection.issues).toContainEqual({ - path: '.agent/agent.db', - reason: '缺少当前 revision 的静态检查通过凭证', - }); - }); - }); - - it('requires valid image files only when the editor API key is configured', async () => { - await withTemporaryRoot(async (root) => { - await writeMinimumFormalArtifacts(root); - const configDir = path.join(root, 'config'); - await mkdir(configDir); - await writeFile( - path.join(configDir, configFileName), - '{"editorApi":{"apiKey":" "}}\n', - ); - expect(await hasConfiguredEditorApiKey(configDir)).toBe(false); - await expect(validateSwarmProjectArtifacts(root)).resolves.toMatchObject({ - valid: true, - requireEditorImages: false, - }); - - await writeFile( - path.join(configDir, localConfigFileName), - '{"editorApi":{"apiKey":"fixture-editor-key"}}\n', - ); - expect(await hasConfiguredEditorApiKey(configDir)).toBe(true); - await expect( - validateSwarmProjectArtifacts(root, { requireEditorImages: true }), - ).rejects.toThrow('assets/ui-prototype.png'); - - await writeFile( - path.join(root, 'assets', 'ui-prototype.png'), - Buffer.from('not-an-image'), - ); - await writeFile( - path.join(root, 'assets', 'art-spritesheet.png'), - Buffer.from([0xff, 0xd8, 0xff, 0xe0]), - ); - await expect( - validateSwarmProjectArtifacts(root, { requireEditorImages: true }), - ).rejects.toThrow('assets/ui-prototype.png'); - - await writeFile( - path.join(root, 'assets', 'ui-prototype.png'), - Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]), - ); - await expect( - validateSwarmProjectArtifacts(root, { requireEditorImages: true }), - ).rejects.toThrow('assets/ui-prototype.png'); - - const headerOnlyPng = Buffer.alloc(1_024); - fixturePngSignature.copy(headerOnlyPng); - headerOnlyPng.writeUInt32BE(13, 8); - headerOnlyPng.write('IHDR', 12, 'ascii'); - headerOnlyPng.writeUInt32BE(1_600, 16); - headerOnlyPng.writeUInt32BE(900, 20); - await writeFile( - path.join(root, 'assets', 'ui-prototype.png'), - headerOnlyPng, - ); - await expect( - validateSwarmProjectArtifacts(root, { requireEditorImages: true }), - ).rejects.toThrow('assets/ui-prototype.png'); - - await writeFile( - path.join(root, 'assets', 'ui-prototype.png'), - fixturePng(1_600, 900), - ); - await writeFile( - path.join(root, 'assets', 'art-spritesheet.png'), - fixturePng(1_024, 1_024), - ); - await expect( - validateSwarmProjectArtifacts(root, { requireEditorImages: true }), - ).resolves.toMatchObject({ valid: true, requireEditorImages: true }); - }); - }); - - it('validates chunk CRC, zlib scanlines, filters, and screenshot PNGs', async () => { - const validPng = fixturePng(1_600, 900); - expect(validatePngBytes(validPng)).toEqual({ width: 1_600, height: 900 }); - - const crcCorrupted = Buffer.from(validPng); - crcCorrupted[42] ^= 0xff; - expect(() => validatePngBytes(crcCorrupted)).toThrow('CRC'); - expect(() => validatePngBytes(validPng.subarray(0, -1))).toThrowError(); - expect(() => - validatePngBytes(fixturePng(320, 180, { invalidFilter: true })), - ).toThrow('filter byte'); - expect(() => - validatePngBytes(fixturePng(320, 180, { trailingCompressedBytes: true })), - ).toThrow('zlib'); - expect(() => - validatePngBytes(fixtureIndexedPng({ includePalette: false })), - ).toThrow('PLTE'); - expect(() => - validatePngBytes(fixtureIndexedPng({ duplicatePalette: true })), - ).toThrow('PLTE'); - expect(() => - validatePngBytes(fixtureIndexedPng({ unknownCriticalChunk: true })), - ).toThrow('critical chunk'); - expect(validatePngBytes(fixtureIndexedPng())).toEqual({ - width: 1, - height: 1, - }); - - await withTemporaryRoot(async (root) => { - await writeMinimumFormalArtifacts(root); - const desktopScreenshot = path.join( - root, - '.agent', - 'runtime', - 'browser-validations', - 'publish-package', - 'test-run', - '8', - 'desktop.png', - ); - const fakeScreenshot = Buffer.alloc(2_048); - fixturePngSignature.copy(fakeScreenshot); - await writeFile(desktopScreenshot, fakeScreenshot); - - const inspection = await inspectSwarmProjectArtifacts(root); - expect(inspection.issues).toContainEqual({ - path: '.agent/agent.db', - reason: '缺少 desktop 试玩截图', - }); - }); - }); - - it('rejects non-PNG bytes at the fixed PNG artifact paths', async () => { - await withTemporaryRoot(async (root) => { - await writeMinimumFormalArtifacts(root); - await writeFile( - path.join(root, 'assets', 'ui-prototype.png'), - Buffer.from('RIFF\x04\x00\x00\x00WEBP', 'binary'), - ); - await writeFile( - path.join(root, 'assets', 'art-spritesheet.png'), - Buffer.from('GIF89a', 'ascii'), - ); - - await expect( - validateSwarmProjectArtifacts(root, { requireEditorImages: true }), - ).rejects.toThrow('assets/ui-prototype.png'); - }); - }); -}); - -describe('preview URL validation', () => { - it('accepts an HTTP URL on the numeric loopback host', () => { - const previewUrl = 'http://127.0.0.1:4173/'; - - expect(validatePreviewUrl(previewUrl)).toBe(previewUrl); - }); - - it.each([ - 'https://127.0.0.1:4173/', - 'http://localhost:4173/', - 'http://[::1]:4173/', - 'http://192.0.2.1:4173/', - 'http://127.0.0.1.example:4173/', - 'http://127.0.0.1/', - 'http://127.0.0.1:4173/play', - 'http://127.0.0.1:4173/?mode=test', - 'http://127.0.0.1:4173/#ready', - 'http://user@127.0.0.1:4173/', - 'file:///fixture/game/index.html', - 'not-a-url', - ])('rejects %s', (previewUrl) => { - expect(() => validatePreviewUrl(previewUrl)).toThrowError(); - }); -}); - -describe('package script registration', () => { - it('registers the root and app config and test commands exactly', async () => { - const [rootPackage, appPackage, checkConfigSource] = await Promise.all( - [ - new URL('../../../package.json', import.meta.url), - new URL('../package.json', import.meta.url), - new URL('../scripts/check-config.mjs', import.meta.url), - ].map(async (packageUrl) => - packageUrl.pathname.endsWith('.json') - ? JSON.parse(await readFile(packageUrl, 'utf8')) - : readFile(packageUrl, 'utf8'), - ), - ); - - expect(rootPackage.scripts?.['agc:config']).toBe( - 'npm --prefix apps/ai-game-creator-shell run config --', - ); - expect(rootPackage.scripts?.['agc:test']).toBe( - 'npm --prefix apps/ai-game-creator-shell run agent-runtime:supervisor-autonomous-playable-lane-defense-deterministic-e2e --', - ); - expect(rootPackage.scripts?.['agc:test:chat']).toBe( - 'npm --prefix apps/ai-game-creator-shell run test:chat --', - ); - expect(rootPackage.scripts?.['agc:test:chat:manual']).toBe( - 'npm --prefix apps/ai-game-creator-shell run test:chat:manual --', - ); - expect(appPackage.scripts?.['test:chat']).toBe( - `node scripts/agent-swarm-test-chat.mjs --task "${defaultRealSwarmTestTask}" --no-open`, - ); - expect(appPackage.scripts?.config).toBe( - 'node scripts/game-creator-config-wizard.mjs', - ); - expect(appPackage.scripts?.['test:chat:manual']).toBe( - 'node scripts/agent-swarm-test-chat.mjs', - ); - expect(checkConfigSource).toMatch( - /packageConfig\.scripts\?\.config\s*!==\s*'node scripts\/game-creator-config-wizard\.mjs'/u, - ); - expect(checkConfigSource).toMatch( - /rootPackageConfig\.scripts\?\.\['agc:config'\]\s*!==\s*'npm --prefix apps\/ai-game-creator-shell run config --'/u, - ); - const wrapperSource = await readFile( - new URL('../scripts/run-cli-with-config.mjs', import.meta.url), - 'utf8', - ); - expect(wrapperSource).toContain('resolveGameCreatorAppConfigDir'); - expect(wrapperSource).toContain( - "'--config-dir', resolveGameCreatorAppConfigDir()", - ); - }); -}); diff --git a/apps/ai-game-creator-shell/tests/agentTraceSummary.test.ts b/apps/ai-game-creator-shell/tests/agentTraceSummary.test.ts deleted file mode 100644 index a49f3db21..000000000 --- a/apps/ai-game-creator-shell/tests/agentTraceSummary.test.ts +++ /dev/null @@ -1,284 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { - createGameCreationAppSeedTasks, - GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, - type GameCreationAgentRunTrace, -} from '../../../packages/shared/src/contracts/gameCreationApp'; -import { summarizeAgentRunTrace } from '../src/App'; - -describe('AI 游戏创作 Agent loop 摘要', () => { - it('shows repair loop state in the chat trace summary', () => { - const tasks = createGameCreationAppSeedTasks(); - tasks[0]!.status = 'completed'; - tasks[1]!.status = 'completed'; - - const trace: GameCreationAgentRunTrace = { - schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, - runId: 'run-test', - commandId: 'game.generate_draft', - status: 'needs-revision', - lifecycleStatus: 'pending', - passes: 2, - maxPasses: 3, - toolCallCount: 12, - maxToolCalls: 128, - stopReason: 'max-passes-exhausted', - goal: '做一个弹幕厨房游戏', - coordination: 'Planner -> Orchestrator -> Generator -> Evaluator', - steps: [ - ...Array.from({ length: 7 }, (_, index) => ({ - pass: 1, - agent: `LLM-${index + 1}`, - phase: 'llm', - taskId: null, - group: null, - role: null, - status: 'completed', - inputPaths: [], - outputPaths: [], - summary: `LLM 调用 ${index + 1}`, - toolCalls: [ - { - toolId: `llm.call.${index + 1}`, - status: 'ok', - inputPaths: [], - outputPaths: [], - summary: `LLM 工具 ${index + 1}`, - }, - ], - })), - { - pass: 2, - agent: 'Orchestrator', - phase: 'plan', - taskId: 'code-director', - group: 'code', - role: 'Code', - status: 'completed', - inputPaths: ['.agent/findings.md'], - outputPaths: ['.agent/passes/pass-2/task-graph.json'], - summary: '重跑程序链路及下游发布包装', - toolCalls: [], - }, - ...Array.from({ length: 4 }, (_, index) => ({ - pass: 2, - agent: `Bridge-${index + 1}`, - phase: 'handoff', - taskId: null, - group: null, - role: null, - status: 'completed', - inputPaths: [], - outputPaths: [], - summary: `中间步骤 ${index + 1}`, - toolCalls: [], - })), - { - pass: 2, - agent: '美术组 / Asset', - phase: 'role-brief', - taskId: 'art-asset-plan', - group: 'art', - role: 'Asset', - status: 'completed', - inputPaths: ['.agent/manifest.json'], - outputPaths: ['.agent/passes/pass-2/groups/art/asset.md'], - summary: '需要回流画板角色素材', - toolCalls: [ - { - toolId: 'agent.role.brief.art.asset', - status: 'completed', - inputPaths: ['.agent/manifest.json'], - outputPaths: ['.agent/passes/pass-2/groups/art/asset.md'], - summary: '规划角色资产', - }, - { - toolId: 'agent.tool.suggest.canvas.project_sync', - status: 'suggested', - inputPaths: ['.agent/manifest.json'], - outputPaths: [], - summary: - '项目还没有画板回流资产;建议用户确认 /sync-canvas-project <画板项目ID>。', - }, - ...Array.from({ length: 5 }, (_, index) => ({ - toolId: `agent.tool.suggest.extra.${index + 1}`, - status: 'suggested', - inputPaths: ['.agent/manifest.json'], - outputPaths: [], - summary: `额外建议命令 ${index + 1}`, - })), - ], - }, - ], - artifacts: [ - { - path: '.agent/older-artifact.json', - sizeBytes: 64, - checksum: 'fnv1a64:older', - }, - { - path: '.agent/passes/pass-2/task-graph.json', - sizeBytes: 128, - checksum: 'fnv1a64:test', - }, - ...Array.from({ length: 4 }, (_, index) => ({ - path: `.agent/passes/pass-2/artifact-${index + 1}.json`, - sizeBytes: 128 + index, - checksum: `fnv1a64:artifact-${index + 1}`, - })), - ], - taskGraph: { - goal: '做一个弹幕厨房游戏', - readyTaskIds: ['code-director'], - activeTaskIds: ['code-director', 'quality-review', 'publish-package'], - carriedTaskIds: ['design-director'], - repairFocus: ['gameHtml 缺少 canvas'], - repairRoutes: [ - { - issue: 'gameHtml 缺少 canvas', - taskIds: [ - 'code-director', - 'quality-review', - 'preview-readiness', - 'publish-package', - ], - reason: 'code-repair+dependency-impact', - }, - { - issue: '缺少输入绑定', - taskIds: ['code-director'], - reason: 'input-binding', - }, - { - issue: '缺少胜负条件', - taskIds: ['quality-review'], - reason: 'win-condition', - }, - { - issue: '缺少发布说明', - taskIds: ['publish-package'], - reason: 'publish-readme', - }, - ], - tasks, - }, - passPlans: [ - { - pass: 1, - mode: 'initial', - summary: '第 1 轮全量调度', - activeTaskIds: ['design-director', 'code-director'], - carriedTaskIds: [], - dependencyWaves: [['design-director'], ['code-director']], - repairFocus: [], - repairRoutes: [], - }, - { - pass: 2, - mode: 'repair', - summary: '第 2 轮重跑程序链路及下游发布包装', - activeTaskIds: ['code-director', 'quality-review', 'publish-package'], - carriedTaskIds: ['design-director'], - dependencyWaves: [ - ['code-director'], - ['quality-review'], - ['publish-package'], - ], - repairFocus: ['gameHtml 缺少 canvas'], - repairRoutes: [ - { - issue: 'gameHtml 缺少 canvas', - taskIds: [ - 'code-director', - 'quality-review', - 'preview-readiness', - 'publish-package', - ], - reason: 'code-repair+dependency-impact', - }, - { - issue: '缺少输入绑定', - taskIds: ['code-director'], - reason: 'input-binding', - }, - { - issue: '缺少胜负条件', - taskIds: ['quality-review'], - reason: 'win-condition', - }, - { - issue: '缺少发布说明', - taskIds: ['publish-package'], - reason: 'publish-readme', - }, - ], - }, - { - pass: 3, - mode: 'repair', - summary: '第 3 轮复核', - activeTaskIds: ['quality-review'], - carriedTaskIds: ['design-director'], - dependencyWaves: [['quality-review']], - repairFocus: [], - repairRoutes: [], - }, - { - pass: 4, - mode: 'repair', - summary: '第 4 轮收尾', - activeTaskIds: ['publish-package'], - carriedTaskIds: [], - dependencyWaves: [['publish-package']], - repairFocus: [], - repairRoutes: [], - }, - ], - nextStep: 'repair-next-pass', - error: null, - updatedAt: 1, - }; - - const summary = summarizeAgentRunTrace(trace); - - expect(summary).toContain( - 'needs-revision / pending · 2/3 轮 · max-passes-exhausted', - ); - expect(summary).toContain('工具调用:12/128'); - expect(summary).toContain('任务:已完成 2'); - expect(summary).toContain( - 'active 任务:程序组 / Director 拆解程序实现(code-director), 程序组 / Review 执行质量评审(quality-review), 运营组 / Publish 整理发布包装(publish-package)', - ); - expect(summary).toContain( - 'carry-over 任务:设计实现组 / Director 拆解创作方向(design-director)', - ); - expect(summary).toContain('返工焦点:gameHtml 缺少 canvas'); - expect(summary).toContain( - '返工路线:code-repair+dependency-impact: 程序组 / Director 拆解程序实现(code-director), 程序组 / Review 执行质量评审(quality-review), 程序组 / Preview 执行静态自检(preview-readiness), 运营组 / Publish 整理发布包装(publish-package)', - ); - expect(summary).toContain('还有 1 条路线'); - expect(summary).not.toContain('publish-readme:'); - expect(summary).toContain('建议命令:'); - expect(summary).toContain('agent.tool.suggest.canvas.project_sync'); - expect(summary).toContain('/sync-canvas-project <画板项目ID>'); - expect(summary).toContain('agent.tool.suggest.extra.4'); - expect(summary).not.toContain('agent.tool.suggest.extra.5'); - expect(summary).toContain('还有 1 个建议命令'); - expect(summary).toContain('编排轮次:'); - expect(summary).toContain( - 'pass 2 · repair · active 程序组 / Director 拆解程序实现(code-director), 程序组 / Review 执行质量评审(quality-review), 运营组 / Publish 整理发布包装(publish-package) · carry 设计实现组 / Director 拆解创作方向(design-director) · waves 程序组 / Director 拆解程序实现(code-director) / 程序组 / Review 执行质量评审(quality-review) / 运营组 / Publish 整理发布包装(publish-package) · repair gameHtml 缺少 canvas · routes code-repair+dependency-impact: 程序组 / Director 拆解程序实现(code-director), 程序组 / Review 执行质量评审(quality-review), 程序组 / Preview 执行静态自检(preview-readiness), 运营组 / Publish 整理发布包装(publish-package)', - ); - expect(summary).not.toContain('pass 1 · initial'); - expect(summary).toContain('还有 1 个较早轮次'); - expect(summary).toContain('.agent/passes/pass-2/task-graph.json'); - expect(summary).not.toContain('.agent/older-artifact.json'); - expect(summary).toContain('还有 1 个较早产物'); - expect(summary).not.toContain('Orchestrator #2 · completed · plan'); - expect(summary).toContain('Bridge-1 #2 · completed · handoff'); - expect(summary).toContain('还有 8 个较早步骤'); - expect(summary).toContain('LLM-2 #1 · completed · llm · llm.call.2'); - expect(summary).not.toContain('LLM-1 #1 · completed · llm · llm.call.1'); - expect(summary).toContain('还有 1 个较早 LLM 步骤'); - }); -}); diff --git a/apps/ai-game-creator-shell/tests/appSurface/harness.ts b/apps/ai-game-creator-shell/tests/appSurface/harness.ts index a1ca82d1b..9322d1f5f 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/harness.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/harness.ts @@ -136,7 +136,6 @@ import { deriveAgentStatusCards, WorkspaceLauncher, } from '../../src/App'; -import { projectNameFromPath } from '../../src/features/agent-runtime/model'; import ProjectDevelopmentView from '../../src/view/project-development'; const testAuthUser: AuthUser = { @@ -314,11 +313,6 @@ async function setComposerText(element: HTMLElement, value: string) { await settleComposer(); } -async function submitChat(value: string) { - await setComposerText(screen.getByLabelText('创作想法'), value); - fireEvent.click(screen.getByRole('button', { name: '发送' })); -} - // 同一张资源卡上的按钮变少了:卡片本体只有「选中资源」与媒体播放钮两个(原先右上角 // 那个 @ 引用圆钮已挪进选中工具条)。选中按钮的可访问名模板固定为 // `选中资源:<分类标签> <资源文件名>`;分类标签可能自带空格(例如 `UI 交互`), @@ -1394,14 +1388,6 @@ function createProjectChatRuntimeHarness({ }; } -async function openMainProject(projectPath: string) { - await submitChat(`/project ${projectPath}`); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText(`已打开:${projectNameFromPath(projectPath)}`), - ).not.toBeNull(); -} - export function installResizeObserverStub() { let observerCount = 0; let observerDisconnected = false; @@ -1552,7 +1538,6 @@ export { it, mockRoleAgentReply, nativeClipboardMock, - openMainProject, openResourceFilterPanel, pickProjectFromLauncher, planningResponseStream, @@ -1568,7 +1553,6 @@ export { roleAgentMockReply, screen, setComposerText, - submitChat, TEST_LOCAL_PROJECT_PATH, testAuthUser, vi, diff --git a/apps/ai-game-creator-shell/tests/appSurface/project-conversation.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/project-conversation.suite.ts index bc3191d9a..61c65cead 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/project-conversation.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/project-conversation.suite.ts @@ -13,7 +13,6 @@ import { renderAppAt, roleAgentMockReply, screen, - submitChat, vi, waitFor, within, @@ -79,163 +78,6 @@ export function registerProjectConversationTests() { ); }); - it.skip('loads project conversation history after opening from chat command', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return emptyProjectPolicy(); - } - if (command === 'read_local_conversation') { - return { - path: '/tmp/authorized-game/.agent/conversations/project.jsonl', - agentId: null, - messages: [ - { - schemaVersion: 'game-creator-conversation.v1', - role: 'user', - content: '历史需求:保留弹幕厨房', - agentId: null, - updatedAt: 1, - }, - { - schemaVersion: 'game-creator-conversation.v1', - role: 'assistant', - content: '历史回复:继续做第二版', - agentId: null, - updatedAt: 2, - }, - ], - }; - } - if (command === 'read_local_project_file') { - throw new Error('missing trace'); - } - if (command === 'list_local_project_files') { - return { projectPath: String(args?.projectPath ?? ''), files: [] }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - expect(await screen.findByText('历史需求:保留弹幕厨房')).not.toBeNull(); - expect(screen.getByText('历史回复:继续做第二版')).not.toBeNull(); - expect( - screen.queryByText('已设置本地项目:/tmp/authorized-game'), - ).toBeNull(); - expect(invoke).toHaveBeenCalledWith('read_local_conversation', { - projectPath: '/tmp/authorized-game', - agentId: null, - }); - expect(invoke).not.toHaveBeenCalledWith( - 'append_local_conversation_message', - expect.anything(), - ); - }); - - it.skip('reloads project conversation history from chat on demand', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return emptyProjectPolicy(); - } - if (command === 'chat_with_game_creator_agent') { - return { - replyText: `主聊天回复:${String(args?.prompt ?? '')}`, - }; - } - if (command === 'read_local_conversation') { - return { - path: '/tmp/authorized-game/.agent/conversations/project.jsonl', - agentId: null, - messages: [ - { - schemaVersion: 'game-creator-conversation.v1', - role: 'user', - content: '重载历史需求', - agentId: null, - updatedAt: 1, - }, - { - schemaVersion: 'game-creator-conversation.v1', - role: 'assistant', - content: '重载历史回复', - agentId: null, - updatedAt: 2, - }, - ], - }; - } - if (command === 'read_local_project_file') { - throw new Error('missing trace'); - } - if (command === 'list_local_project_files') { - return { projectPath: String(args?.projectPath ?? ''), files: [] }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect(await screen.findByText('重载历史需求')).not.toBeNull(); - - await submitChat('临时未保存的输入'); - expect(await screen.findByText('临时未保存的输入')).not.toBeNull(); - await submitChat('/history'); - - expect(await screen.findByText('重载历史回复')).not.toBeNull(); - expect(screen.queryByText('临时未保存的输入')).toBeNull(); - expect(screen.getByText('已读取项目对话历史:2 条')).not.toBeNull(); - - await submitChat('另一条临时未保存的输入'); - expect(await screen.findByText('另一条临时未保存的输入')).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '历史' })); - - expect(await screen.findByText('重载历史回复')).not.toBeNull(); - expect(screen.queryByText('另一条临时未保存的输入')).toBeNull(); - expect(invoke).toHaveBeenCalledWith('read_local_conversation', { - projectPath: '/tmp/authorized-game', - agentId: null, - }); - }); - it.skip('requires confirmation before reading a specific agent conversation when policy asks for it', async () => { const manifest = createGameCreationAppManifest( 'local-project-draft', diff --git a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts index 119b1463f..75667a54d 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts @@ -32,12 +32,9 @@ import { act, cleanup, createGameCreationAppManifest, - createGameCreationAppSeedTasks, expect, findResourceSelectButton, fireEvent, - GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, - type GameCreationAgentRunTrace, getResourceSelectButton, installResizeObserverStub, it, @@ -6732,169 +6729,6 @@ export function registerProjectAgentStatusTests() { ).toBe(false); }); - it.skip('confirms before refreshing agents when trace read policy requires it', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const trace: GameCreationAgentRunTrace = { - schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, - runId: 'run-agent-refresh-confirm', - commandId: 'game.generate_draft', - status: 'running', - passes: 1, - maxPasses: 3, - toolCallCount: 1, - maxToolCalls: 128, - stopReason: 'running', - goal: '做一个厨房弹幕游戏', - coordination: 'Planner', - steps: [], - artifacts: [], - taskGraph: { - goal: '做一个厨房弹幕游戏', - readyTaskIds: [], - activeTaskIds: [], - carriedTaskIds: [], - repairFocus: [], - repairRoutes: [], - tasks: createGameCreationAppSeedTasks(), - }, - passPlans: [], - nextStep: 'continue', - error: null, - updatedAt: 1, - }; - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_local_conversation') { - return { - path: '/tmp/authorized-game/.agent/conversations/project.jsonl', - agentId: null, - messages: [], - }; - } - if (command === 'read_project_permission_policy') { - return { - path: '.agent/policy.json', - policy: { - deniedCommands: [], - confirmCommands: ['agent.trace_read'], - }, - }; - } - if (command === 'read_local_project_file') { - return { - path: '.agent/run.latest.json', - absolutePath: `${String(args?.projectPath ?? '')}/.agent/run.latest.json`, - content: JSON.stringify(trace), - }; - } - if (command === 'list_local_project_files') { - return { projectPath: String(args?.projectPath ?? ''), files: [] }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game'); - invoke.mockClear(); - - fireEvent.click(screen.getByRole('button', { name: '刷新 Agent' })); - - expect(await screen.findByText('agent.trace_read')).not.toBeNull(); - expect(invoke).not.toHaveBeenCalledWith( - 'read_local_project_file', - expect.anything(), - ); - - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - await waitFor(() => { - expect(invoke).toHaveBeenCalledWith('read_local_project_file', { - projectPath: '/tmp/authorized-game', - relativePath: '.agent/run.latest.json', - commandId: 'agent.trace_read', - }); - }); - }); - - it.skip('cancels agent run trace refresh policy confirmation from the panel', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_local_conversation') { - return { - path: '/tmp/authorized-game/.agent/conversations/project.jsonl', - agentId: null, - messages: [], - }; - } - if (command === 'read_project_permission_policy') { - return { - path: '.agent/policy.json', - policy: { - deniedCommands: [], - confirmCommands: ['agent.trace_read'], - }, - }; - } - if (command === 'read_local_project_file') { - throw new Error('should wait for trace confirmation'); - } - if (command === 'list_local_project_files') { - return { projectPath: String(args?.projectPath ?? ''), files: [] }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game'); - invoke.mockClear(); - - fireEvent.click(screen.getByRole('button', { name: '刷新 Agent' })); - - const traceReadCommand = await screen.findByText('agent.trace_read'); - fireEvent.click( - within( - traceReadCommand.closest('.pending-command') as HTMLElement, - ).getByRole('button', { name: '取消' }), - ); - - expect( - await screen.findByText('run: 已取消读取 Agent trace'), - ).not.toBeNull(); - expect(invoke).not.toHaveBeenCalledWith( - 'read_local_project_file', - expect.anything(), - ); - }); - /** * 资源总览(main 态)必须为**每个非空栏目**挂载真实卡片本体。 * diff --git a/apps/ai-game-creator-shell/tests/chatPromptPolish.test.tsx b/apps/ai-game-creator-shell/tests/chatPromptPolish.test.tsx index c179df4fc..4e9456d6d 100644 --- a/apps/ai-game-creator-shell/tests/chatPromptPolish.test.tsx +++ b/apps/ai-game-creator-shell/tests/chatPromptPolish.test.tsx @@ -203,15 +203,6 @@ describe('发送前提醒判据', () => { reminderDisabled: false, }), ).toBe(false); - const command = `/${'长'.repeat(60)}`; - expect( - shouldRemindChatPromptPolish({ - content: textContent(command), - prompt: command, - acknowledgedDraftKey: null, - reminderDisabled: false, - }), - ).toBe(false); }); test('changes the draft key when the references change', () => { @@ -494,22 +485,12 @@ describe('聊天输入区 AI 润色与发送前提醒', () => { }); }); - test('does not hold short prompts or slash commands', () => { + test('does not hold short prompts', () => { const shortSubmit = renderComposer({ initialText: '做个跳跃游戏' }); fireEvent.click(sendButton()); expect(screen.queryByRole('dialog', { name: '发送前提醒' })).toBeNull(); expect(shortSubmit).toHaveBeenCalledWith({ content: textContent('做个跳跃游戏'), }); - - cleanup(); - const commandSubmit = renderComposer({ - initialText: `/${'命令'.repeat(40)}`, - }); - fireEvent.click(sendButton()); - expect(screen.queryByRole('dialog', { name: '发送前提醒' })).toBeNull(); - expect(commandSubmit).toHaveBeenCalledWith({ - content: textContent(`/${'命令'.repeat(40)}`), - }); }); }); diff --git a/apps/ai-game-creator-shell/tests/rememberCommand.test.ts b/apps/ai-game-creator-shell/tests/rememberCommand.test.ts index 95f4ed246..0ab3b9fd8 100644 --- a/apps/ai-game-creator-shell/tests/rememberCommand.test.ts +++ b/apps/ai-game-creator-shell/tests/rememberCommand.test.ts @@ -9,11 +9,10 @@ import { deriveAgentStatusCards, isAbsoluteProjectPath, needsInitializedChatProject, - parseRememberInput, resolveChatProjectPath, } from '../src/App'; -describe('AI 游戏创作聊天记忆命令', () => { +describe('AI 游戏创作项目路径与 Agent 状态卡', () => { it('recognizes local project absolute paths across desktop platforms', () => { expect(isAbsoluteProjectPath('/tmp/game')).toBe(true); expect(isAbsoluteProjectPath('C:\\Games\\demo')).toBe(true); @@ -21,34 +20,7 @@ describe('AI 游戏创作聊天记忆命令', () => { expect(isAbsoluteProjectPath('relative-game')).toBe(false); }); - it('defaults /remember to long memory and supports short and blackboard scopes', () => { - expect(parseRememberInput('主角喜欢反弹弹幕')).toEqual({ - scope: 'long', - content: '主角喜欢反弹弹幕', - }); - expect(parseRememberInput('short 本轮先修输入手感')).toEqual({ - scope: 'short', - content: '本轮先修输入手感', - }); - expect(parseRememberInput('长期 保留厨房主题')).toEqual({ - scope: 'long', - content: '保留厨房主题', - }); - expect(parseRememberInput('project 覆盖后的长期设定')).toEqual({ - scope: 'long', - content: '覆盖后的长期设定', - }); - expect(parseRememberInput('blackboard 共享美术约束')).toEqual({ - scope: 'blackboard', - content: '共享美术约束', - }); - expect(parseRememberInput('黑板 统一使用俯视角')).toEqual({ - scope: 'blackboard', - content: '统一使用俯视角', - }); - }); - - it('requires an initialized local project path before chat memory commands', () => { + it('requires an initialized local project path', () => { expect(resolveChatProjectPath(null)).toBeNull(); expect(resolveChatProjectPath({ projectPath: '/tmp/game' })).toBe( '/tmp/game', @@ -57,17 +29,9 @@ describe('AI 游戏创作聊天记忆命令', () => { expect( resolveChatProjectPath({ projectPath: '/tmp/bad\u0007game' }), ).toBeNull(); - expect(parseRememberInput('long')).toEqual({ - scope: 'long', - content: '', - }); - expect(parseRememberInput('short 覆盖本轮上下文')).toEqual({ - scope: 'short', - content: '覆盖本轮上下文', - }); }); - it('requires a project before chat commands write or run local artifacts', () => { + it('requires a project before local artifacts are written or run', () => { expect(needsInitializedChatProject('game.generate_draft')).toBe(true); expect(needsInitializedChatProject('asset.upload')).toBe(true); expect(needsInitializedChatProject('agent.kill')).toBe(true); From f4294660d2e5694676d23f7ced437b0529219417 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Wed, 23 Sep 2026 11:13:03 +0800 Subject: [PATCH 03/20] =?UTF-8?q?=E9=80=80=E5=BD=B9AGC=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E5=AF=B9=E8=AF=9D=E6=96=9C=E6=9D=A0=E5=91=BD=E4=BB=A4=E4=B8=8E?= =?UTF-8?q?=E7=BB=88=E7=AB=AFswarm=20chat=E5=85=A5=E5=8F=A3=EF=BC=9A?= =?UTF-8?q?=E7=9C=9F=E5=AE=9EE2E=E3=80=81=E8=84=9A=E6=9C=AC=E4=B8=8E?= =?UTF-8?q?=E9=97=A8=E7=A6=81=E6=B8=85=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除 scripts/agent-swarm-test-chat.mjs、agent-runtime-deterministic-playable-e2e.mjs、deterministic-lane-defense-provider.mjs - 删除 user-input、supervisor-autonomous-playable-lane-defense、supervisor-swarm-autonomous-chat、static-isolated 与 collaboration-policy-mixed 套件 - 删除仅服务终端入口的交互式 CLI harness 管道与其进程清理、残留校验逻辑 - 清理 collaboration-assertions、evidence 模板与校验、self-test 中随之失效的混合与静态隔离断言及占位字段 - 移除 check-config.mjs 中钉住已删脚本、命令与套件的断言 - 删除 root 与应用 package.json 的 agc:test*、agc:chat、agc:swarm、各 *-real-e2e 与 test:chat* 脚本 - game-creator-config-wizard.mjs 改为从 channel-identity.mjs 取应用标识 --- apps/ai-game-creator-shell/package.json | 10 - ...ent-runtime-deterministic-playable-e2e.mjs | 2030 ----------- .../assertions/core.mjs | 63 +- .../scripts/agent-runtime-real-e2e/entry.mjs | 378 +- .../harness/app-data.mjs | 332 +- .../agent-runtime-real-e2e/harness/config.mjs | 10 - .../harness/process.mjs | 407 +-- .../harness/project.mjs | 35 +- .../harness/reporting.mjs | 19 +- .../harness/runtime.mjs | 13 +- .../agent-runtime-real-e2e/runtime-state.mjs | 278 -- .../suites/self-test.mjs | 959 +----- .../suites/supervisor-autonomous-playable.mjs | 1890 ---------- .../suites/supervisor-swarm.mjs | 1 - .../suites/supervisor-swarm/chat-session.mjs | 108 - .../collaboration-assertions.mjs | 606 ---- .../supervisor-swarm/collaboration-policy.mjs | 530 +-- .../supervisor-swarm/evidence-schema.mjs | 63 - .../supervisor-swarm/evidence-validation.mjs | 605 +--- .../suites/supervisor-swarm/execution.mjs | 140 +- .../supervisor-swarm/partial-evidence.mjs | 415 +-- .../supervisor-swarm/protocol-validation.mjs | 339 +- .../supervisor-swarm/repair-recovery.mjs | 202 +- .../suites/supervisor-swarm/setup.mjs | 209 +- .../suites/supervisor-swarm/shared.mjs | 22 - .../supervisor-swarm/suite-selection.mjs | 48 +- .../suites/user-input.mjs | 720 ---- .../scripts/agent-swarm-test-chat.mjs | 2149 ------------ .../scripts/check-config.mjs | 102 +- .../deterministic-lane-defense-provider.mjs | 3040 ----------------- .../scripts/game-creator-config-wizard.mjs | 77 +- package.json | 11 - 32 files changed, 260 insertions(+), 15551 deletions(-) delete mode 100644 apps/ai-game-creator-shell/scripts/agent-runtime-deterministic-playable-e2e.mjs delete mode 100644 apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-autonomous-playable.mjs delete mode 100644 apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/chat-session.mjs delete mode 100644 apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/user-input.mjs delete mode 100644 apps/ai-game-creator-shell/scripts/agent-swarm-test-chat.mjs delete mode 100644 apps/ai-game-creator-shell/scripts/deterministic-lane-defense-provider.mjs diff --git a/apps/ai-game-creator-shell/package.json b/apps/ai-game-creator-shell/package.json index 385f52277..c9c7a2d3a 100644 --- a/apps/ai-game-creator-shell/package.json +++ b/apps/ai-game-creator-shell/package.json @@ -15,20 +15,10 @@ "skill-pack:test": "node --test scripts/check-skill-pack.test.mjs", "llm-status": "node scripts/run-cli-with-config.mjs --llm-status", "agent-task": "node scripts/run-cli-with-config.mjs --agent-task", - "chat": "node scripts/run-cli-with-config.mjs --swarm-chat", - "swarm": "node scripts/run-cli-with-config.mjs --swarm-chat", "config": "node scripts/game-creator-config-wizard.mjs", - "test:chat": "node scripts/agent-swarm-test-chat.mjs --task \"制作一个可直接试玩的原创植物塔防小游戏:玩家选择并放置原创守卫阻挡敌人,完成波次后可以进入下一关并重新开始。主题、单位名称与视觉语言必须原创,不使用任何现有游戏角色、单位名、Logo 或受保护视觉语言。请自主完成正式产物、静态检查和双视口试玩验证。\" --no-open", - "test:chat:manual": "node scripts/agent-swarm-test-chat.mjs", "agent-run": "node scripts/run-cli-with-config.mjs --agent-run", "agent-run:smoke": "node scripts/smoke-agent-run-local-provider.mjs", "agent-runtime:real-e2e": "node scripts/agent-runtime-real-e2e.mjs", - "agent-runtime:collaboration-policy-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-collaboration-policy-mixed-recovery", - "agent-runtime:mixed-swarm-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-static-isolated-autonomous-chat", - "agent-runtime:supervisor-swarm-autonomous-chat-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-autonomous-chat", - "agent-runtime:supervisor-autonomous-playable-lane-defense-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-autonomous-playable-lane-defense", - "agent-runtime:supervisor-autonomous-playable-lane-defense-deterministic-e2e": "node scripts/agent-runtime-deterministic-playable-e2e.mjs", - "agent-runtime:supervisor-autonomous-playable-lane-defense-deterministic-self-test": "node scripts/agent-runtime-deterministic-playable-e2e.mjs --self-test", "agent-runtime:supervisor-swarm-transient-retry-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-transient-retry", "agent-runtime:supervisor-swarm-final-reply-transient-retry-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-final-reply-transient-retry", "agent-runtime:supervisor-swarm-tool-plan-handoff-runner-kill-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-tool-plan-handoff-runner-kill", diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-deterministic-playable-e2e.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-deterministic-playable-e2e.mjs deleted file mode 100644 index ef18c3780..000000000 --- a/apps/ai-game-creator-shell/scripts/agent-runtime-deterministic-playable-e2e.mjs +++ /dev/null @@ -1,2030 +0,0 @@ -import { spawn } from 'node:child_process'; -import { createHash, randomUUID } from 'node:crypto'; -import fs from 'node:fs/promises'; -import os from 'node:os'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; - -import { - createDeterministicLaneDefenseRouter, - deterministicLaneDefenseCanonicalHtml, - deterministicLaneDefenseInitialHtml, - deterministicLaneDefenseModel, - deterministicManifestReadyAgentIds, - hiddenCanvasCss, - startDeterministicLaneDefenseProvider, - visibleCanvasCss, -} from './deterministic-lane-defense-provider.mjs'; -import { withLoopbackNoProxy } from './llm-transient-fault-proxy.mjs'; - -const appRoot = path.resolve(fileURLToPath(new URL('..', import.meta.url))); -const repoRoot = path.resolve(appRoot, '../..'); -const realE2eScript = path.join(appRoot, 'scripts/agent-runtime-real-e2e.mjs'); -const suite = 'supervisor-autonomous-playable-lane-defense'; -const wrapperSuite = - 'supervisor-autonomous-playable-lane-defense-deterministic'; -const configFileName = 'game-creator.config.json'; -const configSentinelName = '.deterministic-provider-e2e.json'; -const configSentinelSchema = 'genarrative-deterministic-provider-e2e-config.v1'; -const platformSessionFixtureEnv = 'GENARRATIVE_AGC_PLATFORM_SESSION_FIXTURE'; -const platformSessionFixtureName = '.deterministic-platform-session.json'; -const platformSessionFixtureSchema = - 'genarrative-agc-platform-session-fixture.v1'; -const outputLimit = 32 * 1024 * 1024; - -function hashValue(value) { - return createHash('sha256').update(value).digest('hex'); -} - -function assert(condition, code) { - if (condition) return; - const error = new Error(code); - error.code = code; - throw error; -} - -function parseArguments(args) { - let keepProject = false; - let selfTest = false; - for (const arg of args) { - if (arg === '--keep-project') keepProject = true; - else if (arg === '--self-test') selfTest = true; - else throw new Error('unknown-argument'); - } - assert(!(keepProject && selfTest), 'self-test-keep-project-conflict'); - return { keepProject, selfTest }; -} - -function deterministicRuntimeConfig(apiKey, provider) { - return { - agentMode: 'provider', - editorApi: { - apiKey, - baseUrl: provider.editorBaseUrl, - }, - llm: { - apiKey, - baseUrl: provider.baseUrl, - model: deterministicLaneDefenseModel, - apiKind: 'openai_chat', - reasoningEffort: 'max', - stream: false, - requestTimeoutMs: 30_000, - maxRetries: 0, - retryBackoffMs: 100, - }, - agentLlm: { - 'project-supervisor': { - reasoningEffort: 'max', - }, - }, - }; -} - -function deterministicPlatformSessionFixture(apiKey, provider) { - return { - schemaVersion: platformSessionFixtureSchema, - userId: 'deterministic-e2e-user', - accessToken: apiKey, - apiBaseUrl: provider.editorBaseUrl, - generation: 1, - }; -} - -function appendBounded(current, chunk) { - const combined = Buffer.concat([current, chunk]); - if (combined.length > outputLimit) throw new Error('child-output-too-large'); - return combined; -} - -function runChild(args, environment) { - return new Promise((resolve) => { - const child = spawn(process.execPath, args, { - cwd: appRoot, - env: environment, - stdio: ['ignore', 'pipe', 'pipe'], - }); - let stdout = Buffer.alloc(0); - let stderr = Buffer.alloc(0); - let outputError = null; - child.stdout.on('data', (chunk) => { - try { - stdout = appendBounded(stdout, chunk); - } catch (error) { - outputError = error; - child.kill('SIGTERM'); - } - }); - child.stderr.on('data', (chunk) => { - try { - stderr = appendBounded(stderr, chunk); - } catch (error) { - outputError = error; - child.kill('SIGTERM'); - } - }); - child.once('error', (error) => - resolve({ code: null, signal: null, error, stdout, stderr }), - ); - child.once('close', (code, signal) => - resolve({ code, signal, error: outputError, stdout, stderr }), - ); - }); -} - -function safeChildDiagnostic(result) { - return { - exitCode: Number.isInteger(result.code) ? result.code : null, - signal: typeof result.signal === 'string' ? result.signal : null, - errorCode: - typeof result.error?.code === 'string' ? result.error.code : null, - stdoutBytes: result.stdout.length, - stdoutSha256: hashValue(result.stdout), - stderrBytes: result.stderr.length, - stderrSha256: hashValue(result.stderr), - }; -} - -function parseChildReport(result) { - try { - const report = JSON.parse(result.stdout.toString('utf8')); - assert(report && typeof report === 'object', 'child-report-root-invalid'); - return report; - } catch (error) { - if (error?.code) throw error; - const wrapped = new Error('child-report-json-invalid'); - wrapped.code = 'child-report-json-invalid'; - throw wrapped; - } -} - -function expectedProviderStats(stats) { - const manifestReadyTasks = manifestReadyTaskEvidence(stats); - const projectSupervisorRuntimePlanningCount = - Number.isInteger(stats.byAgent?.['project-supervisor']?.planning) && - Number.isInteger(stats.interactionExecuteCount) - ? stats.byAgent['project-supervisor'].planning - - stats.interactionExecuteCount - : null; - return ( - Number.isInteger(stats.requestCount) && - stats.requestCount >= 40 && - stats.planningRequestCount + - stats.finalReplyRequestCount + - (stats.contextCompactionRequestCount ?? 0) + - (stats.imageInspectionRequestCount ?? 0) === - stats.requestCount && - stats.finalReplyRequestCount === 0 && - stats.interactionExecuteCount === 1 && - stats.goalContractCount === 1 && - stats.actionHistoryCount === 1 && - stats.acceptanceUpdateCount === 1 && - stats.delegateActionCount === 0 && - stats.runStatusCount >= 1 && - stats.sourceWriteCount >= 7 && - stats.staticSmokeCount >= 4 && - stats.previewValidationCount >= 2 && - manifestReadyTasks.exactlyOnce && - stats.manifestReadyTaskFileWriteCount >= 7 && - stats.manifestReadyTaskPreviewValidationCount >= 1 && - stats.manifestReadyTaskCanvasGenerationCount >= - stats.canvasGenerationRequestCount && - stats.manifestReadyTaskCanvasGenerationCount <= - stats.canvasGenerationRequestCount + 16 && - stats.imageInspectionRequestCount === 1 && - stats.canvasGenerationRequestCount === 3 && - stats.canvasDownloadRequestCount === 3 && - stats.canvasSliceDownloadRequestCount === 4 && - stats.canvasGeneratedAspectRatios?.['16:9'] === 1 && - stats.canvasGeneratedAspectRatios?.['1:1'] === 2 && - stats.canonicalCodeRunCount === 1 && - stats.canonicalCodeAssetListCount === 1 && - stats.unexpectedRequestCount === 0 && - Object.keys(stats.rejectionCodes ?? {}).length === 0 && - projectSupervisorRuntimePlanningCount >= 5 && - stats.byAgent?.['project-supervisor']?.finalReply === 0 && - stats.byAgent?.['project-supervisor']?.projectMutation === 0 && - stats.byAgent?.['code-prototype']?.planning >= 5 && - stats.byAgent?.['code-prototype']?.finalReply === 0 && - stats.byAgent?.['code-prototype']?.projectMutation === 1 && - stats.byAgent?.['quality-review']?.finalReply === 0 && - stats.byAgent?.['quality-review']?.projectMutation === 0 - ); -} - -function manifestReadyTaskEvidence(stats) { - const rawCounts = stats?.readyTaskCountsByAgent; - const byAgent = - rawCounts && typeof rawCounts === 'object' && !Array.isArray(rawCounts) - ? Object.fromEntries( - Object.entries(rawCounts) - .sort(([left], [right]) => left.localeCompare(right)) - .map(([agentId, counts]) => [ - agentId, - { - run: counts?.run ?? null, - completion: counts?.completion ?? null, - }, - ]), - ) - : {}; - const actualAgentIds = Object.keys(byAgent); - const expectedAgentIds = [...deterministicManifestReadyAgentIds].sort(); - const exactAgentSet = - actualAgentIds.length === expectedAgentIds.length && - actualAgentIds.every( - (agentId, index) => agentId === expectedAgentIds[index], - ); - const runTotal = Object.values(byAgent).reduce( - (total, counts) => total + (Number.isInteger(counts.run) ? counts.run : 0), - 0, - ); - const completionTotal = Object.values(byAgent).reduce( - (total, counts) => - total + (Number.isInteger(counts.completion) ? counts.completion : 0), - 0, - ); - const perAgentExactlyOnce = - exactAgentSet && - expectedAgentIds.every( - (agentId) => - byAgent[agentId]?.run === 1 && byAgent[agentId]?.completion === 1, - ); - return { - expectedAgentIds, - byAgent, - runTotal, - completionTotal, - exactAgentSet, - perAgentExactlyOnce, - exactlyOnce: - perAgentExactlyOnce && - runTotal === deterministicManifestReadyAgentIds.length && - completionTotal === deterministicManifestReadyAgentIds.length && - stats?.manifestReadyTaskRunCount === - deterministicManifestReadyAgentIds.length && - stats?.manifestReadyTaskCompletionCount === - deterministicManifestReadyAgentIds.length, - }; -} - -const requiredZeroChildEvidenceFields = Object.freeze([ - 'activeRunnerKillCount', - 'approveInputCount', - 'answerInputCount', - 'steerInputCount', - 'turnReportWaitingForConfirmationCount', - 'turnReportWaitingForUserInputCount', - 'turnReportReconciliationAgentCount', - 'providerLifecycleFailedCount', - 'openProviderLifecycleCount', - 'pendingActionCount', - 'confirmationSidecarCount', - 'userInputSidecarCount', - 'providerActionBatchSidecarCount', - 'providerRetrySidecarCount', - 'providerHandoffSidecarCount', - 'toolPlanHandoffSidecarCount', - 'finalizationJournalCount', - 'reconciliationResidueCount', -]); - -function expectedChildReport(report, options, providerStats) { - const evidence = report?.evidence; - const providerRequestCount = providerStats?.requestCount; - const runtimeProviderRequestCount = - Number.isInteger(providerRequestCount) && - Number.isInteger(providerStats?.interactionExecuteCount) && - Number.isInteger(providerStats?.imageInspectionRequestCount) - ? providerRequestCount - - providerStats.interactionExecuteCount - - providerStats.imageInspectionRequestCount - : null; - return ( - report?.status === 'PASS' && - report?.suite === suite && - report?.errorCount === 0 && - Array.isArray(report?.blocked) && - report.blocked.length === 0 && - report?.cleanup?.performed === !options.keepProject && - report?.cleanup?.kept === options.keepProject && - evidence?.evidenceCompleteness === 'complete' && - evidence?.dedicatedZeroInterventionPath === true && - evidence?.stdinTaskCount === 1 && - evidence?.stdinEndedAfterTask === true && - evidence?.turnReportOutcome === 'settled' && - evidence?.parentTaskStatus === 'completed' && - evidence?.parentRuntimeStatus === 'idle' && - evidence?.parentRuntimePhase === 'completed' && - evidence?.laneDefensePlaytestPassed === true && - evidence?.laneDefenseAssertionCount === 37 && - evidence?.laneDefensePassedAssertionCount === 37 && - evidence?.browserValidationPassed === true && - evidence?.staticSmokePassed === true && - evidence?.gameIndexChanged === true && - Number.isInteger(evidence?.projectRevisionDelta) && - evidence.projectRevisionDelta > 0 && - evidence?.finalSupervisorAssistantCount === 1 && - evidence?.professionalAssistantCount >= 3 && - evidence?.providerRequestIdentityCount === runtimeProviderRequestCount && - evidence?.providerLifecycleStartedCount === runtimeProviderRequestCount && - evidence?.providerLifecycleTerminalCount === runtimeProviderRequestCount && - evidence?.providerLifecycleCompletedCount === runtimeProviderRequestCount && - requiredZeroChildEvidenceFields.every((field) => evidence?.[field] === 0) - ); -} - -function syntheticPayload(agentId, runId, tools, extraContext = '') { - return { - model: deterministicLaneDefenseModel, - stream: false, - messages: [ - { - role: 'user', - content: `- templateAgentId: ${agentId}\n- runId: ${runId}\n${extraContext}`, - }, - ], - tools: tools.map((name) => ({ type: 'function', function: { name } })), - }; -} - -function syntheticInteractionExecutePayload() { - return { - model: deterministicLaneDefenseModel, - stream: false, - reasoning_effort: 'max', - messages: [ - { - role: 'system', - content: '你现在位于统一的 Agent interaction loop。', - }, - { - role: 'user', - content: '用户这轮输入:\n制作一个三消经营游戏', - }, - ], - tools: ['runtime_execute', 'runtime_resume', 'project_location'].map( - (name) => ({ type: 'function', function: { name } }), - ), - }; -} - -function syntheticUiPrototypeInspectionPayload() { - return { - model: deterministicLaneDefenseModel, - stream: false, - messages: [ - { - role: 'system', - content: '你是游戏界面视觉检查 Agent。只分析画面。', - }, - { - role: 'user', - content: [ - { - type: 'text', - text: '请逐项检查 informationHud、gameplaySurface、objectiveEntities、primaryControls、failureRestartFlow、responsiveLayout、implementationClarity、originalTheme,并只返回严格 JSON。', - }, - { - type: 'image_url', - image_url: { url: 'data:image/png;base64,AA==' }, - }, - ], - }, - ], - }; -} - -function responseFunctionNames(response) { - return response.choices[0].message.tool_calls.map( - (call) => call.function.name, - ); -} - -function responseFunctionCalls(response) { - return response.choices[0].message.tool_calls.map((call) => ({ - name: call.function.name, - arguments: JSON.parse(call.function.arguments), - })); -} - -async function runSelfTest() { - const html = deterministicLaneDefenseInitialHtml(); - assert([...html].length <= 8_000, 'self-test-html-source-budget-invalid'); - assert( - html.includes(hiddenCanvasCss) && - !html.includes(visibleCanvasCss) && - html.includes('playable-web-game-state.v1') && - html.includes('data-playtest-id="next-level"') && - html.includes('Goal: defend the garden and win every wave.') && - html.includes('requestAnimationFrame'), - 'self-test-html-contract-invalid', - ); - const canonicalHtml = deterministicLaneDefenseCanonicalHtml(); - assert( - canonicalHtml.includes(visibleCanvasCss) && - canonicalHtml.includes("atlasArt.src='../assets/art-spritesheet.png'") && - [ - 'player.png', - 'blocks-and-targets.png', - 'obstacles-and-scene.png', - 'feedback-effects.png', - ].every((fileName) => - canonicalHtml.includes(`../assets/art-spritesheet-slices/${fileName}`), - ) && - (canonicalHtml.match(/ctx\.drawImage\(/g) ?? []).length >= 5, - 'self-test-canonical-visible-art-contract-invalid', - ); - const apiKey = `deterministic-self-test-${randomUUID()}`; - const runtimeConfig = deterministicRuntimeConfig(apiKey, { - baseUrl: 'http://127.0.0.1:41001/v1', - editorBaseUrl: 'http://127.0.0.1:41002', - }); - assert( - runtimeConfig.agentMode === 'provider' && - runtimeConfig.llm.apiKind === 'openai_chat' && - runtimeConfig.llm.reasoningEffort === 'max' && - runtimeConfig.agentLlm?.['project-supervisor']?.reasoningEffort === - 'max' && - runtimeConfig.llm.apiKey === apiKey && - runtimeConfig.editorApi.apiKey === apiKey, - 'self-test-provider-runtime-config-invalid', - ); - const captureProviderErrorCode = (operation) => { - try { - operation(); - return null; - } catch (error) { - return error?.code ?? null; - } - }; - const allTools = [ - 'update_agent_plan', - 'respond_to_user', - 'runtime_tool_agent_action_history', - 'runtime_tool_agent_acceptance_update', - 'runtime_tool_agent_run_status', - 'runtime_tool_command_run_limited', - 'runtime_tool_file_patch', - 'runtime_tool_file_write', - 'runtime_tool_preview_validate', - 'runtime_tool_task_list', - ]; - const manifestReadyTools = [ - ...allTools, - 'runtime_tool_asset_list', - 'runtime_tool_file_read', - ]; - const rootRunId = 'parent-run'; - const contractFingerprint = 'a'.repeat(64); - const contractContext = `[Root Goal Contract]\n${JSON.stringify({ contractFingerprint })}`; - const observationContext = (observations) => - `${contractContext}\n已有工具观察:\n${JSON.stringify(observations)}`; - const goalContractTools = ['runtime_tool_agent_goal_contract']; - const goalContractRouter = createDeterministicLaneDefenseRouter({ apiKey }); - goalContractRouter.route({ - authorization: `Bearer ${apiKey}`, - payload: syntheticPayload( - 'project-supervisor', - 'goal-contract-order-run', - goalContractTools, - ), - }); - assert( - captureProviderErrorCode(() => - goalContractRouter.route({ - authorization: `Bearer ${apiKey}`, - payload: syntheticPayload( - 'project-supervisor', - 'goal-contract-order-run', - goalContractTools, - ), - }), - ) === 'provider-goal-contract-out-of-order', - 'self-test-goal-contract-duplicate-not-rejected', - ); - - const router = createDeterministicLaneDefenseRouter({ apiKey }); - const route = (agentId, runId, tools = allTools, extraContext = '') => - router.route({ - authorization: `Bearer ${apiKey}`, - payload: syntheticPayload(agentId, runId, tools, extraContext), - }); - const interactionExecuteResponse = router.route({ - authorization: `Bearer ${apiKey}`, - payload: syntheticInteractionExecutePayload(), - }); - const interactionExecuteCalls = responseFunctionCalls( - interactionExecuteResponse, - ); - const interactionExecuteStats = router.getStats(); - assert( - interactionExecuteCalls.length === 1 && - interactionExecuteCalls[0]?.name === 'runtime_execute' && - Object.keys(interactionExecuteCalls[0]?.arguments ?? {}).length === 0 && - interactionExecuteStats.requestCount === 1 && - interactionExecuteStats.planningRequestCount === 1 && - interactionExecuteStats.interactionExecuteCount === 1 && - interactionExecuteStats.manifestReadyTaskRunCount === 0 && - interactionExecuteStats.manifestReadyTaskCompletionCount === 0 && - Object.keys(interactionExecuteStats.readyTaskCountsByAgent).length === 0, - 'self-test-interaction-max-execute-sequence-invalid', - ); - const visualInspectionResponse = router.route({ - authorization: `Bearer ${apiKey}`, - payload: syntheticUiPrototypeInspectionPayload(), - }); - const visualInspection = JSON.parse( - visualInspectionResponse.choices[0].message.content, - ); - assert( - Object.keys(visualInspection?.checks ?? {}).join(',') === - 'informationHud,gameplaySurface,objectiveEntities,primaryControls,failureRestartFlow,responsiveLayout,implementationClarity,originalTheme' && - Object.values(visualInspection.checks).every((value) => value === true) && - Array.isArray(visualInspection.issues) && - visualInspection.issues.length === 0 && - router.getStats().imageInspectionRequestCount === 1 && - router.getStats().unexpectedRequestCount === 0, - 'self-test-ui-prototype-inspection-request-not-routed', - ); - const goalContractResponse = route( - 'project-supervisor', - rootRunId, - goalContractTools, - ); - const goalContractCalls = responseFunctionCalls(goalContractResponse); - const acceptanceNodes = - goalContractCalls[0]?.arguments?.input?.acceptanceNodes ?? []; - assert( - goalContractCalls.length === 1 && - goalContractCalls[0]?.name === 'runtime_tool_agent_goal_contract' && - goalContractCalls[0]?.arguments?.input?.outcome?.includes( - '可完整游玩的原创植物塔防网页游戏', - ) && - acceptanceNodes.length === 2 && - acceptanceNodes[0]?.criterionId === 'static-current-revision' && - JSON.stringify(acceptanceNodes[0]?.requiredEvidence) === - JSON.stringify(['tool:command.run_limited']) && - acceptanceNodes[1]?.criterionId === 'playable-current-revision' && - JSON.stringify(acceptanceNodes[1]?.requiredEvidence) === - JSON.stringify(['tool:preview.validate']) && - JSON.stringify(acceptanceNodes[1]?.dependsOn) === - JSON.stringify(['static-current-revision']), - 'self-test-goal-contract-invalid', - ); - assert( - responseFunctionNames( - route( - 'project-supervisor', - rootRunId, - ['runtime_tool_task_list', 'runtime_tool_agent_run_status'], - `${contractContext}\n当前正式 manifest DAG 仍有专业 task 在运行`, - ), - ).join(',') === 'runtime_tool_task_list,runtime_tool_agent_run_status', - 'self-test-manifest-wait-invalid', - ); - - const visualReadyAgentIds = new Set([ - 'art-director', - 'design-foundation', - 'art-asset-plan', - ]); - const canvasCallsByAgent = new Map(); - const callBatchesByAgent = new Map(); - for (const agentId of deterministicManifestReadyAgentIds) { - const runId = `manifest-ready-${agentId}`; - const extraContext = - `处理 manifest ready 任务:${agentId}\n` + `任务 ID:${agentId}`; - const readyTools = visualReadyAgentIds.has(agentId) - ? [ - ...manifestReadyTools, - 'runtime_tool_canvas_asset_generate', - 'runtime_tool_image_inspect', - ] - : manifestReadyTools; - let terminalCompletionCount = 0; - for (let attempt = 0; attempt < 8; attempt += 1) { - const attemptTools = - agentId === 'art-director' && attempt >= 2 - ? ['respond_to_user'] - : readyTools; - const response = route(agentId, runId, attemptTools, extraContext); - const calls = responseFunctionCalls(response); - const batches = callBatchesByAgent.get(agentId) ?? []; - batches.push(calls.map((call) => call.name)); - callBatchesByAgent.set(agentId, batches); - const canvasCall = calls.find( - (call) => call.name === 'runtime_tool_canvas_asset_generate', - ); - if (canvasCall) { - assert( - !canvasCallsByAgent.has(agentId), - `self-test-canvas-generation-duplicate:${agentId}`, - ); - canvasCallsByAgent.set(agentId, canvasCall); - } - if (calls.some((call) => call.name === 'respond_to_user')) { - terminalCompletionCount += 1; - break; - } - } - assert( - terminalCompletionCount === 1, - `self-test-manifest-ready-terminal-invalid:${agentId}`, - ); - } - const artDirectorAssetCall = canvasCallsByAgent.get('art-director'); - const designFoundationAssetCall = canvasCallsByAgent.get('design-foundation'); - const artAssetPlanAssetCall = canvasCallsByAgent.get('art-asset-plan'); - assert( - JSON.stringify(callBatchesByAgent.get('art-asset-plan')) === - JSON.stringify([ - ['runtime_tool_asset_list'], - ['runtime_tool_file_write'], - ['runtime_tool_canvas_asset_generate'], - ['runtime_tool_file_read', 'runtime_tool_asset_list'], - ['update_agent_plan', 'respond_to_user'], - ]), - 'self-test-art-asset-plan-revision-stages-invalid', - ); - assert( - JSON.stringify(callBatchesByAgent.get('code-prototype')) === - JSON.stringify([ - [ - 'runtime_tool_file_read', - 'runtime_tool_file_read', - 'runtime_tool_file_read', - ], - [ - 'runtime_tool_file_read', - 'runtime_tool_file_read', - 'runtime_tool_file_read', - 'runtime_tool_asset_list', - ], - ['runtime_tool_file_write'], - ['runtime_tool_command_run_limited'], - ['update_agent_plan', 'respond_to_user'], - ]), - 'self-test-code-prototype-asset-audit-sequence-invalid', - ); - assert( - canvasCallsByAgent.size === 3 && - artDirectorAssetCall?.arguments?.input?.outputPath === - 'assets/art-spec.png' && - artDirectorAssetCall.arguments.input.aspectRatio === '1:1' && - artDirectorAssetCall.arguments.input.assetKind === 'icon-spec' && - designFoundationAssetCall?.arguments?.input?.outputPath === - 'assets/ui-prototype.png' && - designFoundationAssetCall.arguments.input.aspectRatio === '16:9' && - designFoundationAssetCall.arguments.input.assetKind === 'ui-design' && - artAssetPlanAssetCall?.arguments?.input?.outputPath === - 'assets/art-spritesheet.png' && - artAssetPlanAssetCall.arguments.input.aspectRatio === '1:1' && - artAssetPlanAssetCall.arguments.input.assetKind === 'icon-spritesheet', - 'self-test-visual-assets-invalid', - ); - - const visualRecoveryRouter = createDeterministicLaneDefenseRouter({ apiKey }); - const visualRecoveryRunId = 'visual-recovery-design-foundation'; - const visualRecoveryTools = [ - ...manifestReadyTools, - 'runtime_tool_canvas_asset_generate', - 'runtime_tool_image_inspect', - ]; - const visualRecoveryRoute = ( - tools = visualRecoveryTools, - extraContext = '', - ) => - visualRecoveryRouter.route({ - authorization: `Bearer ${apiKey}`, - payload: syntheticPayload( - 'design-foundation', - visualRecoveryRunId, - tools, - `处理 manifest ready 任务:design-foundation\n任务 ID:design-foundation${extraContext}`, - ), - }); - visualRecoveryRoute(); - visualRecoveryRoute(); - const visualRecoveryCanvasResponse = visualRecoveryRoute(); - const visualRecoveryInitialInspectResponse = visualRecoveryRoute(); - assert( - responseFunctionNames(visualRecoveryCanvasResponse).join(',') === - 'runtime_tool_canvas_asset_generate' && - responseFunctionNames(visualRecoveryInitialInspectResponse).includes( - 'runtime_tool_image_inspect', - ), - 'self-test-ui-prototype-initial-inspection-sequence-invalid', - ); - const failedInspectObservation = { - tool: 'image.inspect', - status: 'failed', - summary: '视觉模型调用失败', - detail: 'upstream-422', - }; - const prematureCompletionResponse = visualRecoveryRoute( - ['update_agent_plan', 'respond_to_user'], - `\n已有工具观察:\n${JSON.stringify([failedInspectObservation])}`, - ); - assert( - responseFunctionNames(prematureCompletionResponse).includes( - 'respond_to_user', - ), - 'self-test-ui-prototype-premature-completion-fixture-invalid', - ); - const visualBlockedObservation = { - tool: 'runtime.visual_asset', - status: 'blocked', - summary: 'UI 原型视觉检查尚未通过', - detail: - 'expectedPath=assets/ui-prototype.png · requiredInspection=image.inspect', - }; - const blockedRetryResponse = visualRecoveryRoute( - visualRecoveryTools, - `\n已有工具观察:\n${JSON.stringify([visualBlockedObservation])}`, - ); - assert( - responseFunctionNames(blockedRetryResponse).join(',') === - 'runtime_tool_image_inspect', - 'self-test-ui-prototype-visual-block-did-not-retry-inspection', - ); - const failedRetryResponse = visualRecoveryRoute( - visualRecoveryTools, - `\n已有工具观察:\n${JSON.stringify([failedInspectObservation])}`, - ); - assert( - responseFunctionNames(failedRetryResponse).join(',') === - 'runtime_tool_image_inspect', - 'self-test-ui-prototype-failed-inspection-did-not-retry', - ); - const successfulInspectObservation = { - tool: 'image.inspect', - status: 'ok', - summary: '视觉检查已完成,共分析 1 张图片', - detail: null, - }; - const visualRecoveryFinalResponse = visualRecoveryRoute( - ['update_agent_plan', 'respond_to_user'], - `\n已有工具观察:\n${JSON.stringify([successfulInspectObservation])}`, - ); - const visualRecoveryStats = visualRecoveryRouter.getStats(); - assert( - responseFunctionNames(visualRecoveryFinalResponse).includes( - 'respond_to_user', - ) && - visualRecoveryStats.manifestReadyTaskRunCount === 1 && - visualRecoveryStats.manifestReadyTaskCompletionCount === 1 && - visualRecoveryStats.manifestReadyTaskCanvasGenerationCount === 1 && - visualRecoveryStats.byAgent?.['design-foundation']?.projectMutation === - 3 && - visualRecoveryStats.unexpectedRequestCount === 0, - 'self-test-ui-prototype-visual-recovery-not-exactly-once', - ); - - const finalStaticResponse = route( - 'project-supervisor', - rootRunId, - ['runtime_tool_command_run_limited'], - `${contractContext}\n已有工具观察:\nseedTaskCounts: completed=16 running=0 pending=0 waiting=0 failed=0 total=16`, - ); - assert( - responseFunctionNames(finalStaticResponse).join(',') === - 'runtime_tool_command_run_limited', - 'self-test-final-static-not-exclusive', - ); - const successfulFinalStaticObservation = { - tool: 'command.run_limited', - status: 'ok', - summary: 'game.static_smoke 已完成', - detail: null, - }; - const finalPreviewResponse = route( - 'project-supervisor', - rootRunId, - ['runtime_tool_preview_validate'], - observationContext([successfulFinalStaticObservation]), - ); - assert( - responseFunctionNames(finalPreviewResponse).join(',') === - 'runtime_tool_preview_validate', - 'self-test-final-preview-not-exclusive', - ); - const successfulFinalPreviewObservation = { - tool: 'preview.validate', - status: 'ok', - summary: '桌面与移动视口真实试玩已通过', - detail: null, - }; - const finalHistoryResponse = route( - 'project-supervisor', - rootRunId, - ['runtime_tool_agent_action_history'], - observationContext([successfulFinalPreviewObservation]), - ); - const finalHistoryCalls = responseFunctionCalls(finalHistoryResponse); - assert( - finalHistoryCalls.length === 1 && - finalHistoryCalls[0]?.name === 'runtime_tool_agent_action_history' && - JSON.stringify(finalHistoryCalls[0]?.arguments?.input) === - JSON.stringify({ - runId: null, - actionId: null, - tool: null, - status: 'ok', - limit: 10, - }), - 'self-test-final-action-history-invalid', - ); - const staticActionId = `action-${'1'.repeat(24)}`; - const previewActionId = `action-${'2'.repeat(24)}`; - const actionHistoryDetail = JSON.stringify({ - runId: rootRunId, - count: 2, - truncated: false, - outputTruncated: false, - actions: [ - { - agentId: 'project-supervisor', - runId: rootRunId, - actionId: staticActionId, - tool: 'command.run_limited', - status: 'ok', - }, - { - agentId: 'project-supervisor', - runId: rootRunId, - actionId: previewActionId, - tool: 'preview.validate', - status: 'ok', - }, - ], - }); - const finalAcceptanceResponse = route( - 'project-supervisor', - rootRunId, - ['runtime_tool_agent_acceptance_update'], - observationContext([ - { - tool: 'agent.action_history', - status: 'ok', - summary: '已读取当前 Agent 的 2 条终态动作', - detail: actionHistoryDetail, - }, - ]), - ); - const finalAcceptanceCalls = responseFunctionCalls(finalAcceptanceResponse); - const acceptanceInput = finalAcceptanceCalls[0]?.arguments?.input; - assert( - finalAcceptanceCalls.length === 1 && - finalAcceptanceCalls[0]?.name === - 'runtime_tool_agent_acceptance_update' && - acceptanceInput?.contractFingerprint === contractFingerprint && - acceptanceInput?.evaluations?.length === 2 && - acceptanceInput.evaluations[0]?.criterionId === - 'static-current-revision' && - acceptanceInput.evaluations[0]?.evidence?.[0]?.actionId === - staticActionId && - acceptanceInput.evaluations[1]?.criterionId === - 'playable-current-revision' && - acceptanceInput.evaluations[1]?.evidence?.[0]?.actionId === - previewActionId, - 'self-test-final-acceptance-update-invalid', - ); - const finalRespondResponse = route( - 'project-supervisor', - rootRunId, - ['respond_to_user'], - observationContext([ - { - tool: 'agent.acceptance_update', - status: 'ok', - summary: 'Acceptance Graph 已更新', - detail: null, - }, - ]), - ); - assert( - responseFunctionNames(finalRespondResponse).join(',') === 'respond_to_user', - 'self-test-final-respond-not-exclusive', - ); - - const planRejectionRouter = createDeterministicLaneDefenseRouter({ apiKey }); - planRejectionRouter.route({ - authorization: `Bearer ${apiKey}`, - payload: syntheticPayload( - 'project-supervisor', - 'plan-rejection-run', - goalContractTools, - ), - }); - const planRejectionResponse = planRejectionRouter.route({ - authorization: `Bearer ${apiKey}`, - payload: syntheticPayload( - 'project-supervisor', - 'plan-rejection-run', - ['respond_to_user', 'runtime_tool_file_patch', 'runtime_tool_file_write'], - '当前父 run 已进入只编排模式;本轮只允许收束或安全编排。', - ), - }); - const planRejectionWaitResponse = planRejectionRouter.route({ - authorization: `Bearer ${apiKey}`, - payload: syntheticPayload( - 'project-supervisor', - 'plan-rejection-run', - ['runtime_tool_task_list', 'runtime_tool_agent_run_status'], - '当前正式 manifest DAG 仍有专业 task 在运行', - ), - }); - const planRejectionStats = planRejectionRouter.getStats(); - assert( - responseFunctionNames(planRejectionResponse).join(',') === - 'respond_to_user' && - responseFunctionNames(planRejectionWaitResponse).join(',') === - 'runtime_tool_task_list,runtime_tool_agent_run_status' && - planRejectionStats.byAgent?.['project-supervisor']?.projectMutation === - 0 && - planRejectionStats.delegateActionCount === 0 && - planRejectionStats.runStatusCount === 1 && - planRejectionStats.unexpectedRequestCount === 0, - 'self-test-supervisor-plan-rejection-did-not-converge-read-only', - ); - - const earlyFinalReplyRouter = createDeterministicLaneDefenseRouter({ - apiKey, - }); - assert( - captureProviderErrorCode(() => - earlyFinalReplyRouter.route({ - authorization: `Bearer ${apiKey}`, - payload: syntheticPayload( - 'project-supervisor', - 'early-final-reply-run', - [], - '给用户一个正常中文回复', - ), - }), - ) === 'provider-parent-final-reply-before-acceptance', - 'self-test-parent-final-reply-bypassed-acceptance', - ); - - const acceptanceGuardRunId = 'acceptance-guard-run'; - const acceptanceGuardRouter = createDeterministicLaneDefenseRouter({ - apiKey, - }); - const acceptanceGuardRoute = (tools, extraContext = '') => - acceptanceGuardRouter.route({ - authorization: `Bearer ${apiKey}`, - payload: syntheticPayload( - 'project-supervisor', - acceptanceGuardRunId, - tools, - extraContext, - ), - }); - acceptanceGuardRoute(goalContractTools); - acceptanceGuardRoute( - ['runtime_tool_command_run_limited'], - `${contractContext}\n已有工具观察:\nseedTaskCounts: completed=16 running=0 pending=0 waiting=0 failed=0 total=16`, - ); - acceptanceGuardRoute( - ['runtime_tool_preview_validate'], - observationContext([successfulFinalStaticObservation]), - ); - acceptanceGuardRoute( - ['runtime_tool_agent_action_history'], - observationContext([successfulFinalPreviewObservation]), - ); - const acceptanceGuardDetail = JSON.parse(actionHistoryDetail); - acceptanceGuardDetail.runId = acceptanceGuardRunId; - for (const action of acceptanceGuardDetail.actions) { - action.runId = acceptanceGuardRunId; - } - const actionHistoryObservationFor = (detail) => ({ - tool: 'agent.action_history', - status: 'ok', - summary: '已读取当前 Agent 的 2 条终态动作', - detail: JSON.stringify(detail), - }); - const ambiguousContractContext = `[Root Goal Contract]\n{"contractFingerprint":"${contractFingerprint}","nested":{"contractFingerprint":"${'b'.repeat(64)}"}}`; - assert( - captureProviderErrorCode(() => - acceptanceGuardRoute( - ['runtime_tool_agent_acceptance_update'], - `${ambiguousContractContext}\n已有工具观察:\n${JSON.stringify([ - actionHistoryObservationFor(acceptanceGuardDetail), - ])}`, - ), - ) === 'provider-goal-contract-fingerprint-missing', - 'self-test-ambiguous-goal-contract-fingerprint-accepted', - ); - const invalidActionHistoryDetail = structuredClone(acceptanceGuardDetail); - invalidActionHistoryDetail.actions[0].actionId = 'action-invalid'; - assert( - captureProviderErrorCode(() => - acceptanceGuardRoute( - ['runtime_tool_agent_acceptance_update'], - `${contractContext}\n已有工具观察:\n${JSON.stringify([ - actionHistoryObservationFor(invalidActionHistoryDetail), - ])}`, - ), - ) === 'provider-final-acceptance-evidence-missing', - 'self-test-invalid-action-history-id-accepted', - ); - assert( - responseFunctionNames( - acceptanceGuardRoute( - ['runtime_tool_agent_acceptance_update'], - `${contractContext}\n已有工具观察:\n${JSON.stringify([ - actionHistoryObservationFor(acceptanceGuardDetail), - ])}`, - ), - ).join(',') === 'runtime_tool_agent_acceptance_update', - 'self-test-acceptance-stage-advanced-after-rejected-history', - ); - - const stats = router.getStats(); - const manifestReadyTasks = manifestReadyTaskEvidence(stats); - assert( - manifestReadyTasks.exactlyOnce && - stats.goalContractCount === 1 && - stats.actionHistoryCount === 1 && - stats.acceptanceUpdateCount === 1 && - stats.interactionExecuteCount === 1 && - stats.delegateActionCount === 0 && - stats.imageInspectionRequestCount === 1 && - stats.manifestReadyTaskCanvasGenerationCount === 3 && - stats.byAgent?.['project-supervisor']?.projectMutation === 0 && - stats.byAgent?.['code-prototype']?.projectMutation === 1 && - stats.byAgent?.['quality-review']?.projectMutation === 0, - 'self-test-manifest-ready-exactly-once-invalid', - ); - - const missingReadyTaskStats = structuredClone(stats); - const missingAgentId = deterministicManifestReadyAgentIds.at(-1); - delete missingReadyTaskStats.readyTaskCountsByAgent[missingAgentId]; - missingReadyTaskStats.manifestReadyTaskRunCount -= 1; - missingReadyTaskStats.manifestReadyTaskCompletionCount -= 1; - assert( - !manifestReadyTaskEvidence(missingReadyTaskStats).exactlyOnce, - 'self-test-manifest-ready-missing-accepted', - ); - - const duplicateReadyTaskStats = structuredClone(stats); - const duplicateAgentId = deterministicManifestReadyAgentIds[0]; - duplicateReadyTaskStats.readyTaskCountsByAgent[duplicateAgentId].completion += - 1; - duplicateReadyTaskStats.manifestReadyTaskCompletionCount += 1; - assert( - !manifestReadyTaskEvidence(duplicateReadyTaskStats).exactlyOnce, - 'self-test-manifest-ready-duplicate-accepted', - ); - - const retryObservationContext = (observations) => - `\n已有工具观察:\n${JSON.stringify(observations)}`; - const staleObservation = { - tool: 'runtime.verification', - status: 'blocked', - summary: '最终回复生成期间项目 revision 已变化', - detail: 'responseRevision=1, currentRevision=2', - }; - const successfulSmokeObservation = { - tool: 'command.run_limited', - status: 'ok', - summary: 'game.static_smoke 已完成', - detail: null, - }; - const blockedSmokeObservation = { - tool: 'command.run_limited', - status: 'blocked', - summary: '仓库规范或启动上下文已漂移,旧动作未执行', - detail: - 'repositoryContextDrift=true · 请在同一 run 下一轮 planning 重新确认适用规范', - }; - const projectLockSmokeObservations = [ - { - tool: 'command.run_limited', - status: 'failed', - summary: 'game.static_smoke 无法取得项目验证锁', - detail: '项目正在被其他写操作占用:$PROJECT_ROOT/.agent/project.lock', - }, - { - tool: 'command.run_limited', - status: 'failed', - summary: 'game.static_smoke 无法取得项目验证锁', - detail: '项目正在被其他写操作占用:$PROJECT_ROOT\\.agent\\project.lock', - }, - ]; - const projectRevisionDriftSmokeObservation = { - tool: 'command.run_limited', - status: 'blocked', - summary: '并行项目变更使旧动作过期,旧动作未执行', - detail: - 'projectRevisionDrift=true · expectedRevision=4 · currentRevision=5 · replanRequired=true', - }; - const projectRevisionDriftCanvasObservation = { - tool: 'canvas.asset_generate', - status: 'blocked', - summary: '并行项目变更使旧动作过期,旧动作未执行', - detail: - 'projectRevisionDrift=true · expectedRevision=5 · currentRevision=6 · replanRequired=true', - }; - const successfulProjectMutationObservation = { - tool: 'project.patchset', - status: 'ok', - summary: 'project.patchset 已原子应用 2 项变更', - detail: 'checkpointId=checkpoint-concurrent-ready-batch', - }; - const missingArtAssetObservation = { - tool: 'runtime.visual_asset', - status: 'blocked', - summary: '首版美术素材图尚未按正式视觉流程生成并登记,不能完成任务', - detail: '[redacted sensitive context]', - }; - const successfulCanvasMutationObservation = { - tool: 'canvas.asset_generate', - status: 'ok', - summary: '素材画布图片已生成并登记', - detail: null, - }; - - const artAssetRecoveryAgentId = 'art-asset-plan'; - const artAssetRecoveryRunId = 'art-asset-visual-recovery-ready-run'; - const artAssetRecoveryTools = [ - ...manifestReadyTools, - 'runtime_tool_canvas_asset_generate', - ]; - const artAssetRecoveryRouter = createDeterministicLaneDefenseRouter({ - apiKey, - }); - const artAssetRecoveryRoute = (extraContext = '') => - artAssetRecoveryRouter.route({ - authorization: `Bearer ${apiKey}`, - payload: syntheticPayload( - artAssetRecoveryAgentId, - artAssetRecoveryRunId, - artAssetRecoveryTools, - `处理 manifest ready 任务:${artAssetRecoveryAgentId}\n任务 ID:${artAssetRecoveryAgentId}${extraContext}`, - ), - }); - for (let attempt = 0; attempt < 8; attempt += 1) { - if ( - responseFunctionNames(artAssetRecoveryRoute()).includes('respond_to_user') - ) { - break; - } - } - const artAssetRegenerationResponse = artAssetRecoveryRoute( - retryObservationContext([missingArtAssetObservation]), - ); - const artAssetReverificationResponse = artAssetRecoveryRoute( - retryObservationContext([successfulCanvasMutationObservation]), - ); - const artAssetRefinalizationResponse = artAssetRecoveryRoute( - retryObservationContext([successfulSmokeObservation]), - ); - const artAssetRecoveryStats = artAssetRecoveryRouter.getStats(); - assert( - responseFunctionNames(artAssetRegenerationResponse).join(',') === - 'runtime_tool_canvas_asset_generate' && - responseFunctionNames(artAssetReverificationResponse).join(',') === - 'runtime_tool_command_run_limited' && - responseFunctionNames(artAssetRefinalizationResponse).includes( - 'respond_to_user', - ) && - artAssetRecoveryStats.readyTaskCountsByAgent[artAssetRecoveryAgentId] - ?.completion === 1 && - artAssetRecoveryStats.manifestReadyTaskCanvasGenerationCount === 2 && - artAssetRecoveryStats.unexpectedRequestCount === 0, - 'self-test-art-asset-visual-recovery-not-exactly-once', - ); - - const artAssetRevisionDriftRunId = 'art-asset-revision-drift-ready-run'; - const artAssetRevisionDriftRouter = createDeterministicLaneDefenseRouter({ - apiKey, - }); - const artAssetRevisionDriftRoute = (extraContext = '') => - artAssetRevisionDriftRouter.route({ - authorization: `Bearer ${apiKey}`, - payload: syntheticPayload( - artAssetRecoveryAgentId, - artAssetRevisionDriftRunId, - artAssetRecoveryTools, - `处理 manifest ready 任务:${artAssetRecoveryAgentId}\n任务 ID:${artAssetRecoveryAgentId}${extraContext}`, - ), - }); - const artAssetInitialListResponse = artAssetRevisionDriftRoute(); - const artAssetManifestWriteResponse = artAssetRevisionDriftRoute(); - const artAssetInitialCanvasResponse = artAssetRevisionDriftRoute(); - const artAssetRetryCanvasResponse = artAssetRevisionDriftRoute( - retryObservationContext([projectRevisionDriftCanvasObservation]), - ); - const artAssetPostRetryReadResponse = artAssetRevisionDriftRoute( - retryObservationContext([successfulCanvasMutationObservation]), - ); - const artAssetRevisionDriftFinalizationResponse = - artAssetRevisionDriftRoute(); - const artAssetRevisionDriftStats = artAssetRevisionDriftRouter.getStats(); - assert( - responseFunctionNames(artAssetInitialListResponse).join(',') === - 'runtime_tool_asset_list' && - responseFunctionNames(artAssetManifestWriteResponse).join(',') === - 'runtime_tool_file_write' && - responseFunctionNames(artAssetInitialCanvasResponse).join(',') === - 'runtime_tool_canvas_asset_generate' && - responseFunctionNames(artAssetRetryCanvasResponse).join(',') === - 'runtime_tool_canvas_asset_generate' && - responseFunctionNames(artAssetPostRetryReadResponse).join(',') === - 'runtime_tool_file_read,runtime_tool_asset_list' && - responseFunctionNames(artAssetRevisionDriftFinalizationResponse).includes( - 'respond_to_user', - ) && - artAssetRevisionDriftStats.readyTaskCountsByAgent[artAssetRecoveryAgentId] - ?.run === 1 && - artAssetRevisionDriftStats.readyTaskCountsByAgent[artAssetRecoveryAgentId] - ?.completion === 1 && - artAssetRevisionDriftStats.manifestReadyTaskCanvasGenerationCount === 2 && - artAssetRevisionDriftStats.unexpectedRequestCount === 0, - 'self-test-art-asset-revision-drift-recovery-not-exactly-once', - ); - - const artAssetRevisionDriftLimitRunId = - 'art-asset-revision-drift-limit-ready-run'; - const artAssetRevisionDriftLimitRouter = createDeterministicLaneDefenseRouter( - { apiKey }, - ); - const artAssetRevisionDriftLimitRoute = (extraContext = '') => - artAssetRevisionDriftLimitRouter.route({ - authorization: `Bearer ${apiKey}`, - payload: syntheticPayload( - artAssetRecoveryAgentId, - artAssetRevisionDriftLimitRunId, - artAssetRecoveryTools, - `处理 manifest ready 任务:${artAssetRecoveryAgentId}\n任务 ID:${artAssetRecoveryAgentId}${extraContext}`, - ), - }); - artAssetRevisionDriftLimitRoute(); - artAssetRevisionDriftLimitRoute(); - artAssetRevisionDriftLimitRoute(); - for (let retry = 0; retry < 16; retry += 1) { - assert( - responseFunctionNames( - artAssetRevisionDriftLimitRoute( - retryObservationContext([projectRevisionDriftCanvasObservation]), - ), - ).join(',') === 'runtime_tool_canvas_asset_generate', - `self-test-art-asset-revision-drift-retry-invalid:${retry}`, - ); - } - const artAssetRevisionDriftLimitCode = captureProviderErrorCode(() => - artAssetRevisionDriftLimitRoute( - retryObservationContext([projectRevisionDriftCanvasObservation]), - ), - ); - const artAssetRevisionDriftLimitStats = - artAssetRevisionDriftLimitRouter.getStats(); - assert( - artAssetRevisionDriftLimitCode === - 'provider-ready-art-canvas-retry-exhausted' && - artAssetRevisionDriftLimitStats.readyTaskCountsByAgent[ - artAssetRecoveryAgentId - ]?.run === 1 && - artAssetRevisionDriftLimitStats.readyTaskCountsByAgent[ - artAssetRecoveryAgentId - ]?.completion === 0 && - artAssetRevisionDriftLimitStats.manifestReadyTaskCanvasGenerationCount === - 17, - 'self-test-art-asset-revision-drift-limit-invalid', - ); - - const readOnlyCommandRouter = createDeterministicLaneDefenseRouter({ - apiKey, - }); - const readOnlyCommandRunId = 'read-only-command-only-ready-run'; - const readOnlyCommandRoute = (tools) => - readOnlyCommandRouter.route({ - authorization: `Bearer ${apiKey}`, - payload: syntheticPayload( - 'quality-review', - readOnlyCommandRunId, - tools, - '处理 manifest ready 任务:quality-review\n任务 ID:quality-review', - ), - }); - readOnlyCommandRoute(manifestReadyTools); - readOnlyCommandRoute(manifestReadyTools); - const readOnlyCommandCode = captureProviderErrorCode(() => - readOnlyCommandRoute(['runtime_tool_command_run_limited']), - ); - const readOnlyCommandStats = readOnlyCommandRouter.getStats(); - assert( - readOnlyCommandCode === - 'provider-ready-finalization-tools-invalid:quality-review:runtime_tool_command_run_limited' && - readOnlyCommandStats.readyTaskCountsByAgent['quality-review'] - ?.completion === 0 && - readOnlyCommandStats.manifestReadyTaskStaticSmokeCount === 0, - 'self-test-read-only-command-only-not-rejected', - ); - - const preCompletionRouter = createDeterministicLaneDefenseRouter({ apiKey }); - const preCompletionAgentId = 'preview-readiness'; - const preCompletionRunId = 'pre-completion-transient-ready-run'; - const preCompletionRoute = (tools, extraContext = '') => - preCompletionRouter.route({ - authorization: `Bearer ${apiKey}`, - payload: syntheticPayload( - preCompletionAgentId, - preCompletionRunId, - tools, - `处理 manifest ready 任务:${preCompletionAgentId}\n任务 ID:${preCompletionAgentId}${extraContext}`, - ), - }); - preCompletionRoute(manifestReadyTools); - for (let retry = 0; retry < 16; retry += 1) { - assert( - responseFunctionNames( - preCompletionRoute( - ['runtime_tool_command_run_limited'], - retryObservationContext([blockedSmokeObservation]), - ), - ).join(',') === 'runtime_tool_command_run_limited', - `self-test-ready-precompletion-transient-invalid:${retry}`, - ); - } - const preCompletionLimitCode = captureProviderErrorCode(() => - preCompletionRoute( - ['runtime_tool_command_run_limited'], - retryObservationContext([blockedSmokeObservation]), - ), - ); - const preCompletionStats = preCompletionRouter.getStats(); - assert( - preCompletionLimitCode === - `provider-ready-precompletion-verification-exhausted:${preCompletionAgentId}` && - preCompletionStats.readyTaskCountsByAgent[preCompletionAgentId] - ?.completion === 0 && - preCompletionStats.manifestReadyTaskStaticSmokeCount === 17, - 'self-test-ready-precompletion-limit-invalid', - ); - - const projectLockRouter = createDeterministicLaneDefenseRouter({ apiKey }); - const projectLockRunId = 'project-lock-transient-ready-run'; - const projectLockRoute = (tools, extraContext = '') => - projectLockRouter.route({ - authorization: `Bearer ${apiKey}`, - payload: syntheticPayload( - preCompletionAgentId, - projectLockRunId, - tools, - `处理 manifest ready 任务:${preCompletionAgentId}\n任务 ID:${preCompletionAgentId}${extraContext}`, - ), - }); - projectLockRoute(manifestReadyTools); - for (const observation of projectLockSmokeObservations) { - assert( - responseFunctionNames( - projectLockRoute( - ['runtime_tool_command_run_limited'], - retryObservationContext([observation]), - ), - ).join(',') === 'runtime_tool_command_run_limited', - 'self-test-ready-project-lock-path-separator-not-retried', - ); - } - - const duplicateRouter = createDeterministicLaneDefenseRouter({ apiKey }); - const duplicateRoute = (runId, extraContext = '') => - duplicateRouter.route({ - authorization: `Bearer ${apiKey}`, - payload: syntheticPayload( - duplicateAgentId, - runId, - manifestReadyTools, - `处理 manifest ready 任务:${duplicateAgentId}\n任务 ID:${duplicateAgentId}${extraContext}`, - ), - }); - duplicateRoute('duplicate-ready-run'); - duplicateRoute('duplicate-ready-run'); - let duplicateTerminalCode = null; - try { - duplicateRoute('duplicate-ready-run'); - } catch (error) { - duplicateTerminalCode = error?.code ?? null; - } - let duplicateRunCode = null; - try { - duplicateRoute('second-ready-run'); - } catch (error) { - duplicateRunCode = error?.code ?? null; - } - assert( - duplicateTerminalCode === - `provider-ready-run-terminal-duplicate:${duplicateAgentId}` && - duplicateRunCode === - `provider-ready-agent-run-duplicate:${duplicateAgentId}`, - 'self-test-manifest-ready-provider-duplicate-not-rejected', - ); - - const retryAgentId = 'preview-readiness'; - const retryRunId = 'retry-ready-run'; - const retryRouter = createDeterministicLaneDefenseRouter({ apiKey }); - const retryRoute = (extraContext = '') => - retryRouter.route({ - authorization: `Bearer ${apiKey}`, - payload: syntheticPayload( - retryAgentId, - retryRunId, - manifestReadyTools, - `处理 manifest ready 任务:${retryAgentId}\n任务 ID:${retryAgentId}${extraContext}`, - ), - }); - for (let attempt = 0; attempt < 8; attempt += 1) { - if (responseFunctionNames(retryRoute()).includes('respond_to_user')) break; - } - for (let retry = 0; retry < 16; retry += 1) { - assert( - responseFunctionNames( - retryRoute(retryObservationContext([staleObservation])), - ).join(',') === 'runtime_tool_command_run_limited', - `self-test-ready-retry-verification-call-invalid:${retry}`, - ); - if (retry === 0) { - let missingObservationCode = null; - try { - retryRoute(); - } catch (error) { - missingObservationCode = error?.code ?? null; - } - assert( - missingObservationCode === - `provider-ready-retry-verification-invalid:${retryAgentId}`, - 'self-test-ready-retry-missing-observation-accepted', - ); - } - assert( - responseFunctionNames( - retryRoute(retryObservationContext([successfulSmokeObservation])), - ).includes('respond_to_user'), - `self-test-ready-retry-finalization-invalid:${retry}`, - ); - } - let retryLimitCode = null; - try { - retryRoute(retryObservationContext([staleObservation])); - } catch (error) { - retryLimitCode = error?.code ?? null; - } - const retryStats = retryRouter.getStats(); - assert( - retryLimitCode === - `provider-ready-run-terminal-duplicate:${retryAgentId}` && - retryStats.readyTaskCountsByAgent[retryAgentId]?.run === 1 && - retryStats.readyTaskCountsByAgent[retryAgentId]?.completion === 1, - 'self-test-ready-retry-limit-or-exactly-once-invalid', - ); - - const verifiedDeliveryRepairRouter = createDeterministicLaneDefenseRouter({ - apiKey, - }); - const verifiedDeliveryRepairRunId = 'verified-delivery-repair-ready-run'; - const verifiedDeliveryRepairRoute = (tools, extraContext = '') => - verifiedDeliveryRepairRouter.route({ - authorization: `Bearer ${apiKey}`, - payload: syntheticPayload( - retryAgentId, - verifiedDeliveryRepairRunId, - tools, - `处理 manifest ready 任务:${retryAgentId}\n任务 ID:${retryAgentId}${extraContext}`, - ), - }); - for (let attempt = 0; attempt < 8; attempt += 1) { - if ( - responseFunctionNames( - verifiedDeliveryRepairRoute(manifestReadyTools), - ).includes('respond_to_user') - ) { - break; - } - } - const rejectedVerificationPlan = verifiedDeliveryRepairRoute( - manifestReadyTools, - retryObservationContext([staleObservation]), - ); - const verifiedDeliveryRepairResponse = verifiedDeliveryRepairRoute( - ['respond_to_user'], - `${retryObservationContext([staleObservation])}\n上一条输出不符合工具计划协议:当前 revision 已通过验证。本次修复的原生工具目录只保留 respond_to_user;必须立即调用它交付专业合同结论。`, - ); - const verifiedDeliveryRepairStats = verifiedDeliveryRepairRouter.getStats(); - assert( - responseFunctionNames(rejectedVerificationPlan).join(',') === - 'runtime_tool_command_run_limited' && - responseFunctionNames(verifiedDeliveryRepairResponse).join(',') === - 'respond_to_user' && - verifiedDeliveryRepairStats.readyTaskCountsByAgent[retryAgentId] - ?.completion === 1 && - verifiedDeliveryRepairStats.unexpectedRequestCount === 0, - 'self-test-ready-verified-delivery-repair-invalid', - ); - - const transientRetryRouter = createDeterministicLaneDefenseRouter({ apiKey }); - const transientRetryRunId = 'transient-retry-ready-run'; - const transientRetryRoute = (extraContext = '') => - transientRetryRouter.route({ - authorization: `Bearer ${apiKey}`, - payload: syntheticPayload( - retryAgentId, - transientRetryRunId, - manifestReadyTools, - `处理 manifest ready 任务:${retryAgentId}\n任务 ID:${retryAgentId}${extraContext}`, - ), - }); - for (let attempt = 0; attempt < 8; attempt += 1) { - if ( - responseFunctionNames(transientRetryRoute()).includes('respond_to_user') - ) { - break; - } - } - transientRetryRoute(retryObservationContext([staleObservation])); - assert( - responseFunctionNames( - transientRetryRoute(retryObservationContext([blockedSmokeObservation])), - ).join(',') === 'runtime_tool_command_run_limited' && - responseFunctionNames( - transientRetryRoute( - retryObservationContext([successfulSmokeObservation]), - ), - ).includes('respond_to_user') && - transientRetryRouter.getStats().readyTaskCountsByAgent[retryAgentId] - ?.completion === 1, - 'self-test-ready-transient-verification-retry-invalid', - ); - - const standaloneRetryAgentId = 'design-foundation'; - const standaloneRetryRunId = 'standalone-retry-ready-run'; - const standaloneRetryRouter = createDeterministicLaneDefenseRouter({ - apiKey, - }); - const standaloneRetryRoute = (extraContext = '') => - standaloneRetryRouter.route({ - authorization: `Bearer ${apiKey}`, - payload: syntheticPayload( - standaloneRetryAgentId, - standaloneRetryRunId, - manifestReadyTools, - `处理 manifest ready 任务:${standaloneRetryAgentId}\n任务 ID:${standaloneRetryAgentId}${extraContext}`, - ), - }); - for (let attempt = 0; attempt < 8; attempt += 1) { - if ( - responseFunctionNames(standaloneRetryRoute()).includes('respond_to_user') - ) { - break; - } - } - const standaloneReplayResponse = standaloneRetryRoute( - retryObservationContext([successfulSmokeObservation]), - ); - const standaloneReplayDuplicateCode = captureProviderErrorCode(() => - standaloneRetryRoute(retryObservationContext([successfulSmokeObservation])), - ); - assert( - responseFunctionNames(standaloneReplayResponse).includes( - 'respond_to_user', - ) && - standaloneReplayDuplicateCode === - `provider-ready-run-terminal-duplicate:${standaloneRetryAgentId}` && - standaloneRetryRouter.getStats().readyTaskCountsByAgent[ - standaloneRetryAgentId - ]?.completion === 1, - 'self-test-ready-standalone-verification-replay-invalid', - ); - - const concurrentMutationAgentId = 'design-foundation'; - const concurrentMutationRunId = 'concurrent-mutation-ready-run'; - const concurrentMutationRouter = createDeterministicLaneDefenseRouter({ - apiKey, - }); - const concurrentMutationRoute = (extraContext = '') => - concurrentMutationRouter.route({ - authorization: `Bearer ${apiKey}`, - payload: syntheticPayload( - concurrentMutationAgentId, - concurrentMutationRunId, - manifestReadyTools, - `处理 manifest ready 任务:${concurrentMutationAgentId}\n任务 ID:${concurrentMutationAgentId}${extraContext}`, - ), - }); - for (let attempt = 0; attempt < 8; attempt += 1) { - if ( - responseFunctionNames(concurrentMutationRoute()).includes( - 'respond_to_user', - ) - ) { - break; - } - } - assert( - responseFunctionNames( - concurrentMutationRoute( - retryObservationContext([successfulProjectMutationObservation]), - ), - ).join(',') === 'runtime_tool_command_run_limited' && - responseFunctionNames( - concurrentMutationRoute( - retryObservationContext([successfulSmokeObservation]), - ), - ).includes('respond_to_user') && - concurrentMutationRouter.getStats().readyTaskCountsByAgent[ - concurrentMutationAgentId - ]?.completion === 1, - 'self-test-ready-concurrent-mutation-reverification-invalid', - ); - - const repairedWriterAgentId = 'balance-seed'; - const repairedWriterRunId = 'repaired-writer-ready-run'; - const repairedWriterRouter = createDeterministicLaneDefenseRouter({ apiKey }); - const repairedWriterRoute = (tools = manifestReadyTools, extraContext = '') => - repairedWriterRouter.route({ - authorization: `Bearer ${apiKey}`, - payload: syntheticPayload( - repairedWriterAgentId, - repairedWriterRunId, - tools, - `处理 manifest ready 任务:${repairedWriterAgentId}\n任务 ID:${repairedWriterAgentId}${extraContext}`, - ), - }); - for (let attempt = 0; attempt < 8; attempt += 1) { - if ( - responseFunctionNames(repairedWriterRoute()).includes('respond_to_user') - ) { - break; - } - } - const repairedWriterMutation = repairedWriterRoute( - ['runtime_tool_file_write'], - retryObservationContext([successfulSmokeObservation]), - ); - const repairedWriterVerification = repairedWriterRoute( - manifestReadyTools, - retryObservationContext([successfulProjectMutationObservation]), - ); - const repairedWriterFinalization = repairedWriterRoute( - manifestReadyTools, - retryObservationContext([successfulSmokeObservation]), - ); - const repairedWriterDuplicateCode = captureProviderErrorCode(() => - repairedWriterRoute( - manifestReadyTools, - retryObservationContext([successfulSmokeObservation]), - ), - ); - assert( - responseFunctionNames(repairedWriterMutation).join(',') === - 'runtime_tool_file_write' && - responseFunctionNames(repairedWriterVerification).join(',') === - 'runtime_tool_command_run_limited' && - responseFunctionNames(repairedWriterFinalization).includes( - 'respond_to_user', - ) && - repairedWriterDuplicateCode === - `provider-ready-run-terminal-duplicate:${repairedWriterAgentId}` && - repairedWriterRouter.getStats().readyTaskCountsByAgent[ - repairedWriterAgentId - ]?.completion === 1, - 'self-test-ready-repaired-writer-reverification-invalid', - ); - - const projectRevisionDriftAgentId = 'preview-readiness'; - const projectRevisionDriftRunId = 'project-revision-drift-ready-run'; - const projectRevisionDriftRouter = createDeterministicLaneDefenseRouter({ - apiKey, - }); - const projectRevisionDriftRoute = (extraContext = '') => - projectRevisionDriftRouter.route({ - authorization: `Bearer ${apiKey}`, - payload: syntheticPayload( - projectRevisionDriftAgentId, - projectRevisionDriftRunId, - manifestReadyTools, - `处理 manifest ready 任务:${projectRevisionDriftAgentId}\n任务 ID:${projectRevisionDriftAgentId}${extraContext}`, - ), - }); - for (let attempt = 0; attempt < 8; attempt += 1) { - if ( - responseFunctionNames(projectRevisionDriftRoute()).includes( - 'respond_to_user', - ) - ) { - break; - } - } - assert( - responseFunctionNames( - projectRevisionDriftRoute( - retryObservationContext([projectRevisionDriftSmokeObservation]), - ), - ).join(',') === 'runtime_tool_command_run_limited' && - responseFunctionNames( - projectRevisionDriftRoute( - retryObservationContext([successfulSmokeObservation]), - ), - ).includes('respond_to_user') && - projectRevisionDriftRouter.getStats().readyTaskCountsByAgent[ - projectRevisionDriftAgentId - ]?.completion === 1, - 'self-test-ready-project-revision-drift-retry-invalid', - ); - - const readOnlyRetryAgentId = 'quality-review'; - const readOnlyRetryRunId = 'read-only-retry-ready-run'; - const readOnlyRetryRouter = createDeterministicLaneDefenseRouter({ apiKey }); - const readOnlyRetryRoute = (extraContext = '') => - readOnlyRetryRouter.route({ - authorization: `Bearer ${apiKey}`, - payload: syntheticPayload( - readOnlyRetryAgentId, - readOnlyRetryRunId, - manifestReadyTools, - `处理 manifest ready 任务:${readOnlyRetryAgentId}\n任务 ID:${readOnlyRetryAgentId}${extraContext}`, - ), - }); - for (let attempt = 0; attempt < 8; attempt += 1) { - if ( - responseFunctionNames(readOnlyRetryRoute()).includes('respond_to_user') - ) { - break; - } - } - assert( - responseFunctionNames( - readOnlyRetryRoute(retryObservationContext([staleObservation])), - ).includes('respond_to_user') && - readOnlyRetryRouter.getStats().manifestReadyTaskStaticSmokeCount === 0, - 'self-test-read-only-retry-used-forbidden-verification', - ); - assert( - stats.goalContractCount === 1 && - stats.actionHistoryCount === 1 && - stats.acceptanceUpdateCount === 1 && - stats.delegateActionCount === 0 && - stats.manifestReadyTaskCanvasGenerationCount === 3 && - stats.byAgent?.['project-supervisor']?.projectMutation === 0 && - stats.byAgent?.['code-prototype']?.projectMutation === 1 && - stats.byAgent?.['quality-review']?.projectMutation === 0 && - stats.unexpectedRequestCount === 0, - 'self-test-provider-core-stats-invalid', - ); - - const syntheticRuntimeProviderRequestCount = - stats.requestCount - - stats.interactionExecuteCount - - stats.imageInspectionRequestCount; - - const syntheticChildReport = { - status: 'PASS', - suite, - errorCount: 0, - blocked: [], - cleanup: { performed: true, kept: false }, - evidence: { - ...Object.fromEntries( - requiredZeroChildEvidenceFields.map((field) => [field, 0]), - ), - evidenceCompleteness: 'complete', - dedicatedZeroInterventionPath: true, - stdinTaskCount: 1, - stdinEndedAfterTask: true, - turnReportOutcome: 'settled', - parentTaskStatus: 'completed', - parentRuntimeStatus: 'idle', - parentRuntimePhase: 'completed', - laneDefensePlaytestPassed: true, - laneDefenseAssertionCount: 37, - laneDefensePassedAssertionCount: 37, - browserValidationPassed: true, - staticSmokePassed: true, - gameIndexChanged: true, - projectRevisionDelta: 2, - finalSupervisorAssistantCount: 1, - professionalAssistantCount: 3, - providerRequestIdentityCount: syntheticRuntimeProviderRequestCount, - providerLifecycleStartedCount: syntheticRuntimeProviderRequestCount, - providerLifecycleTerminalCount: syntheticRuntimeProviderRequestCount, - providerLifecycleCompletedCount: syntheticRuntimeProviderRequestCount, - }, - }; - const incompletePlaytestReport = structuredClone(syntheticChildReport); - incompletePlaytestReport.evidence.laneDefensePassedAssertionCount = 36; - const manualInputReport = structuredClone(syntheticChildReport); - manualInputReport.evidence.approveInputCount = 1; - const residualSidecarReport = structuredClone(syntheticChildReport); - residualSidecarReport.evidence.providerHandoffSidecarCount = 1; - assert( - expectedChildReport(syntheticChildReport, { keepProject: false }, stats) && - !expectedChildReport( - incompletePlaytestReport, - { keepProject: false }, - stats, - ) && - !expectedChildReport(manualInputReport, { keepProject: false }, stats) && - !expectedChildReport( - residualSidecarReport, - { keepProject: false }, - stats, - ), - 'self-test-child-hard-gates-invalid', - ); - - const rootPackage = JSON.parse( - await fs.readFile(path.join(repoRoot, 'package.json'), 'utf8'), - ); - const shellPackage = JSON.parse( - await fs.readFile(path.join(appRoot, 'package.json'), 'utf8'), - ); - assert( - shellPackage.scripts?.[ - 'agent-runtime:supervisor-autonomous-playable-lane-defense-deterministic-e2e' - ] === 'node scripts/agent-runtime-deterministic-playable-e2e.mjs' && - rootPackage.scripts?.[ - 'ai-game-creator-shell:agent-runtime:supervisor-autonomous-playable-lane-defense-deterministic-e2e' - ] === - 'npm --prefix apps/ai-game-creator-shell run agent-runtime:supervisor-autonomous-playable-lane-defense-deterministic-e2e --', - 'self-test-package-command-invalid', - ); - return { - status: 'PASS', - suite: `${wrapperSuite}-self-test`, - providerUsed: false, - htmlChars: [...html].length, - providerStats: stats, - manifestReadyTasks, - manifestReadyFailureSamplesValidated: ['missing', 'duplicate'], - manifestReadyProviderDuplicateRejections: { - terminal: duplicateTerminalCode, - run: duplicateRunCode, - }, - terminalRetryContractsValidated: { - readOnlyCommandOnlyRejected: true, - preCompletionTransientRetryLimit: 16, - projectLockPathSeparatorsRetried: ['/', '\\'], - }, - providerRuntimeModeValidated: true, - interactionExecutionValidated: { - reasoningEffort: 'max', - requestCount: stats.interactionExecuteCount, - responseTools: interactionExecuteCalls.map((call) => call.name), - readyTaskRunCountBeforeRuntime: - interactionExecuteStats.manifestReadyTaskRunCount, - readyTaskCompletionCountBeforeRuntime: - interactionExecuteStats.manifestReadyTaskCompletionCount, - }, - goalContractValidated: true, - finalizationSequenceValidated: [ - 'command.run_limited', - 'preview.validate', - 'agent.action_history', - 'agent.acceptance_update', - 'respond_to_user', - ], - canvasArtifactsValidated: [ - 'assets/art-spec.png', - 'assets/ui-prototype.png', - 'assets/art-spritesheet.png', - ], - uiPrototypeInspectionValidated: true, - uiPrototypeVisualRecoveryExactlyOnceValidated: true, - artAssetVisualRecoveryExactlyOnceValidated: true, - artAssetRevisionDriftRecoveryExactlyOnceValidated: true, - artAssetRevisionDriftRetryLimitValidated: 16, - supervisorPlanRejectionMode: 'respond-only-then-read-only-wait', - childHardGatesValidated: true, - packageCommandsRegistered: true, - }; -} - -async function runE2e(options) { - const token = randomUUID(); - const apiKey = `deterministic-runtime-${randomUUID()}`; - const configDir = await fs.mkdtemp( - path.join(os.tmpdir(), 'genarrative-deterministic-provider-config-'), - ); - let provider = null; - let childResult = null; - let childReport = null; - let configRemoved = false; - let providerStats = null; - let failureCode = null; - try { - if (process.platform !== 'win32') await fs.chmod(configDir, 0o700); - await fs.writeFile( - path.join(configDir, configSentinelName), - `${JSON.stringify({ schemaVersion: configSentinelSchema, token })}\n`, - { flag: 'wx', mode: 0o600 }, - ); - provider = await startDeterministicLaneDefenseProvider({ apiKey }); - const config = deterministicRuntimeConfig(apiKey, provider); - const fixturePath = path.join(configDir, platformSessionFixtureName); - await fs.writeFile( - fixturePath, - `${JSON.stringify(deterministicPlatformSessionFixture(apiKey, provider))}\n`, - { flag: 'wx', mode: 0o600 }, - ); - await fs.writeFile( - path.join(configDir, configFileName), - `${JSON.stringify(config)}\n`, - { flag: 'wx', mode: 0o600 }, - ); - const childArgs = [ - realE2eScript, - '--suite', - suite, - '--config-dir', - configDir, - ]; - if (options.keepProject) childArgs.push('--keep-project'); - childResult = await runChild( - childArgs, - withLoopbackNoProxy({ - ...process.env, - NO_COLOR: '1', - [platformSessionFixtureEnv]: fixturePath, - GENARRATIVE_AGC_DEBUG_PROVIDER_E2E: '1', - }), - ); - childReport = parseChildReport(childResult); - } catch (error) { - failureCode = error?.code ?? 'deterministic-e2e-unexpected-error'; - } finally { - if (provider) { - try { - await provider.stop(); - providerStats = provider.getStats(); - } catch { - failureCode ??= 'deterministic-provider-stop-failed'; - } - } - try { - const sentinel = JSON.parse( - await fs.readFile(path.join(configDir, configSentinelName), 'utf8'), - ); - assert( - sentinel.schemaVersion === configSentinelSchema && - sentinel.token === token, - 'deterministic-config-sentinel-invalid', - ); - await fs.rm(configDir, { recursive: true, force: false }); - configRemoved = true; - } catch (error) { - failureCode ??= error?.code ?? 'deterministic-config-cleanup-failed'; - } - } - - const childPassed = - childResult?.code === 0 && - childResult?.signal === null && - !childResult?.error && - expectedChildReport(childReport, options, providerStats); - const providerPassed = - providerStats?.stopped === true && expectedProviderStats(providerStats); - const manifestReadyTasks = manifestReadyTaskEvidence(providerStats); - const status = - !failureCode && childPassed && providerPassed && configRemoved - ? 'PASS' - : 'FAIL'; - if (status !== 'PASS' && !failureCode) { - failureCode = !childPassed - ? 'deterministic-child-e2e-failed' - : !providerPassed - ? 'deterministic-provider-contract-failed' - : 'deterministic-config-not-cleaned'; - } - return { - status, - suite: wrapperSuite, - providerMode: 'deterministic-loopback-openai-chat', - delegatedSuite: suite, - child: childReport, - provider: providerStats, - manifestReadyTasks, - finalizationContract: { - goalContractCount: providerStats?.goalContractCount ?? null, - actionHistoryCount: providerStats?.actionHistoryCount ?? null, - acceptanceUpdateCount: providerStats?.acceptanceUpdateCount ?? null, - delegateActionCount: providerStats?.delegateActionCount ?? null, - supervisorProjectMutationCount: - providerStats?.byAgent?.['project-supervisor']?.projectMutation ?? null, - }, - cleanup: { - providerStopped: providerStats?.stopped === true, - configRemoved, - projectKept: options.keepProject, - }, - childDiagnostic: childResult ? safeChildDiagnostic(childResult) : null, - failureCode, - }; -} - -const options = parseArguments(process.argv.slice(2)); -const report = options.selfTest ? await runSelfTest() : await runE2e(options); -process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); -process.exitCode = report.status === 'PASS' ? 0 : 1; diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/assertions/core.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/assertions/core.mjs index f91c928e8..e2e02ff7f 100644 --- a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/assertions/core.mjs +++ b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/assertions/core.mjs @@ -1,69 +1,8 @@ import { createHash } from '../dependencies.mjs'; import { isPlainObject } from '../harness/config.mjs'; -import { - interactiveCliOutput, - writeInteractiveCliLine, -} from '../harness/process.mjs'; -import { - shutdownWaiters, - state, - userInputAnswerText, -} from '../runtime-state.mjs'; +import { shutdownWaiters, state } from '../runtime-state.mjs'; import { disposableProjectPathVariants } from './runtime.mjs'; -export async function answerRemainingInteractiveQuestions(session) { - let answeredPromptCount = 1; - const deadline = Date.now() + 60_000; - while (Date.now() < deadline) { - const output = interactiveCliOutput(session); - if (output.includes(`[\u5df2\u56de\u7b54] ${state.userInput.requestId}`)) - return; - const promptCount = output.split('或直接输入其他答案:').length - 1; - while (answeredPromptCount < promptCount && answeredPromptCount < 3) { - writeInteractiveCliLine(session, userInputAnswerText); - answeredPromptCount += 1; - } - if (output.includes('[待确认]')) { - throw codedError('user-input-unexpected-tool-confirmation'); - } - if (session.closed) throw codedError('user-input-cli-closed-before-answer'); - await sleep(100); - } - throw codedError('user-input-answer-timeout'); -} - -export function parseSingleSwarmTurnReport(output, codePrefix) { - const reportLines = output - .split(/\r?\n/u) - .filter((line) => line.startsWith('[turn.report] ')); - assert(reportLines.length === 1, `${codePrefix}-turn-report-count-invalid`); - const report = JSON.parse(reportLines[0].slice('[turn.report] '.length)); - assert( - isPlainObject(report) && - JSON.stringify(Object.keys(report).sort()) === - JSON.stringify( - [ - 'schemaVersion', - 'outcome', - 'parentAgentId', - 'sessionId', - 'parentRunId', - 'runtimeCount', - 'busyRuntimeCount', - 'pendingTaskCount', - 'runningTaskCount', - 'waitingForConfirmationCount', - 'waitingForUserInputCount', - 'newAssistantMessageCount', - 'finalReplyChars', - 'reconciliationAgentCount', - ].sort(), - ), - `${codePrefix}-turn-report-shape-invalid`, - ); - return report; -} - export function isFailedTask(task) { return ( ['failed', 'cancelled', 'budget-exhausted'].includes(task.status) || diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/entry.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/entry.mjs index 8b20293b9..56c6f0231 100644 --- a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/entry.mjs +++ b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/entry.mjs @@ -20,15 +20,6 @@ import { stopOwnedIsolatedRunner, } from './harness/app-data.mjs'; import { loadConfig, parseArguments } from './harness/config.mjs'; -import { - activeInteractiveCliSessions, - captureOwnedProcessCleanupSnapshot, - closeInteractiveCli, - destroyInteractiveCliOutputStreams, - interactiveCliOutput, - verifyOwnedProcessCleanupSnapshot, - waitForInteractiveCliStdioClose, -} from './harness/process.mjs'; import { checkPrerequisites } from './harness/project.mjs'; import { buildSummary, @@ -48,8 +39,6 @@ import { state, steerInstruction, StreamingSecretScanner, - userInputAnswerCanary, - userInputAnswerText, } from './runtime-state.mjs'; import { collectPartialContextCompactionEvidence, @@ -101,29 +90,14 @@ import { isSteerRunnerKillSuite, runSteerRunnerKillE2e, } from './suites/steer-runner-kill.mjs'; -import { - collectPartialSupervisorAutonomousPlayableEvidence, - emptySupervisorAutonomousPlayableEvidence, - isSupervisorAutonomousPlayableLaneDefenseSuite, - runSupervisorAutonomousPlayableLaneDefenseE2e, -} from './suites/supervisor-autonomous-playable.mjs'; import { collectPartialSupervisorSwarmEvidence, emptySupervisorSwarmEvidence, - isSupervisorSwarmInteractiveChatSuite, isSupervisorSwarmSuite, isSupervisorSwarmTransientRetrySuite, - recordSupervisorSwarmChatSessionFailureDiagnostic, runSupervisorSwarmE2e, - supervisorSwarmChatSessionFailureEvidence, supervisorSwarmToolPlanHandoffProxyNeedsCleanup, } from './suites/supervisor-swarm.mjs'; -import { - collectPartialUserInputEvidence, - emptyUserInputEvidence, - isUserInputRuntimeSuite, - runUserInputRuntimeE2e, -} from './suites/user-input.mjs'; import { collectPartialWebSearchEvidence, emptyWebSearchEvidence, @@ -175,13 +149,9 @@ if (selfTestRequested) { if (isContextCompactionSuite()) { state.evidence = emptyContextCompactionEvidence(); } - if (isUserInputRuntimeSuite()) state.evidence = emptyUserInputEvidence(); if (isScopedAgentsSuite()) state.evidence = emptyScopedAgentsEvidence(); if (isProjectSkillSuite()) state.evidence = emptyProjectSkillEvidence(); if (isParallelReadSuite()) state.evidence = emptyParallelReadEvidence(); - if (isSupervisorAutonomousPlayableLaneDefenseSuite()) { - state.evidence = emptySupervisorAutonomousPlayableEvidence(); - } if (isSupervisorSwarmSuite()) { state.evidence = emptySupervisorSwarmEvidence(); } @@ -192,11 +162,9 @@ if (selfTestRequested) { if ( isWebSearchSuite() || isContextCompactionSuite() || - isUserInputRuntimeSuite() || isScopedAgentsSuite() || isProjectSkillSuite() || isParallelReadSuite() || - isSupervisorAutonomousPlayableLaneDefenseSuite() || isSupervisorSwarmSuite() || isSteerRunnerKillSuite() ) { @@ -208,9 +176,8 @@ if (selfTestRequested) { state.transcriptScanner = new StreamingSecretScanner(state.secrets); state.config = await checkPrerequisites(loaded.config); - const required = isSupervisorAutonomousPlayableLaneDefenseSuite() - ? ['llmConfigured', 'chromeAvailable'] - : isProcessSessionSuite() || isIsolatedRunnerSuite() + const required = + isProcessSessionSuite() || isIsolatedRunnerSuite() ? ['llmConfigured'] : ['llmConfigured', 'chromeAvailable']; if (state.suite === 'full') { @@ -232,16 +199,12 @@ if (selfTestRequested) { await runWebSearchE2e(); } else if (isContextCompactionSuite()) { await runContextCompactionE2e(); - } else if (isUserInputRuntimeSuite()) { - await runUserInputRuntimeE2e(); } else if (isScopedAgentsSuite()) { await runScopedAgentsE2e(); } else if (isProjectSkillSuite()) { await runProjectSkillE2e(); } else if (isParallelReadSuite()) { await runParallelReadE2e(); - } else if (isSupervisorAutonomousPlayableLaneDefenseSuite()) { - await runSupervisorAutonomousPlayableLaneDefenseE2e(); } else if (isSupervisorSwarmSuite()) { await runSupervisorSwarmE2e(); } else if (isProcessSessionSuite()) { @@ -262,107 +225,6 @@ if (selfTestRequested) { recordError(error?.code ?? 'unexpected-error', error); } finally { state.cleanupInProgress = true; - const stateTrackedInteractiveCliSessions = new Set( - [ - state.userInputCliSession, - state.supervisorAutonomousPlayableCliSession, - state.supervisorSwarmCliSession, - ].filter(Boolean), - ); - const interactiveCliSessions = [ - ...new Set([ - ...stateTrackedInteractiveCliSessions, - ...activeInteractiveCliSessions, - ]), - ]; - const supervisorAutonomousPlayableCliSession = - state.supervisorAutonomousPlayableCliSession; - if (isUserInputRuntimeSuite() && state.userInputCliSession) { - try { - await closeInteractiveCli(state.userInputCliSession); - } catch (error) { - state.status = 'FAIL'; - recordError('user-input-cli-cleanup-failed', error); - } - state.userInputCliSession = null; - } - if ( - isSupervisorAutonomousPlayableLaneDefenseSuite() && - state.supervisorAutonomousPlayableCliSession - ) { - try { - await closeInteractiveCli(state.supervisorAutonomousPlayableCliSession); - } catch (error) { - state.status = 'FAIL'; - recordError('supervisor-autonomous-playable-cli-cleanup-failed', error); - } - state.supervisorAutonomousPlayableCliSession = null; - } - if ( - isSupervisorSwarmInteractiveChatSuite() && - state.supervisorSwarmCliSession - ) { - try { - if ( - state.status !== 'PASS' && - state.supervisorSwarmCliSession.closed && - !state.supervisorSwarm.chatSessionFailureDiagnostic - ) { - recordSupervisorSwarmChatSessionFailureDiagnostic( - state.supervisorSwarmCliSession, - ); - } - await closeInteractiveCli(state.supervisorSwarmCliSession); - } catch (error) { - state.status = 'FAIL'; - recordError( - 'supervisor-swarm-autonomous-chat-cli-cleanup-failed', - error, - ); - } - state.supervisorSwarmCliSession = null; - } - for (const session of interactiveCliSessions) { - if (stateTrackedInteractiveCliSessions.has(session)) continue; - try { - await closeInteractiveCli(session); - } catch (error) { - state.status = 'FAIL'; - recordError('interactive-cli-cleanup-failed', error); - } - } - if ( - isSupervisorAutonomousPlayableLaneDefenseSuite() && - state.isolatedRunner.appDataDir - ) { - try { - const runnerPid = state.isolatedRunner.current?.pid ?? null; - const helperPid = - state.isolatedRunner.current?.killHandle?.child?.pid ?? null; - state.supervisorAutonomousPlayable.ownedProcessCleanupSnapshot = - await captureOwnedProcessCleanupSnapshot({ - runnerPid, - helperPids: Number.isSafeInteger(helperPid) ? [helperPid] : [], - rootPids: [ - ...interactiveCliSessions.map((session) => session.child?.pid), - ...[...activeCommandChildren].map((child) => child.pid), - ].filter((pid) => Number.isSafeInteger(pid) && pid > 0), - }); - const observed = - state.supervisorAutonomousPlayable.ownedProcessCleanupSnapshot - .observedCounts; - assert( - observed.runner === 1 && observed.helper === 1, - 'supervisor-autonomous-playable-owned-process-snapshot-incomplete', - ); - } catch (error) { - state.status = 'FAIL'; - recordError( - 'supervisor-autonomous-playable-owned-process-snapshot-failed', - error, - ); - } - } if (isIsolatedRunnerSuite() && state.isolatedRunner.appDataDir) { try { await stopOwnedIsolatedRunner(); @@ -380,25 +242,6 @@ if (selfTestRequested) { ).catch(() => {}); } } - for (const session of interactiveCliSessions) { - try { - await waitForInteractiveCliStdioClose(session, 10_000); - } catch (error) { - destroyInteractiveCliOutputStreams(session); - state.status = 'FAIL'; - recordError( - error?.code === 'interactive-cli-stdio-close-timeout' - ? error.code - : 'interactive-cli-stdio-cleanup-failed', - error, - ); - } - } - if (supervisorAutonomousPlayableCliSession) { - state.supervisorAutonomousPlayable.cliOutput = interactiveCliOutput( - supervisorAutonomousPlayableCliSession, - ); - } if (isIsolatedRunnerSuite() && state.isolatedRunner.appDataDir) { if (state.isolatedRunner.stopped) { try { @@ -500,28 +343,6 @@ if (selfTestRequested) { state.status = 'FAIL'; recordError('web-search-formal-config-cli-call-detected'); } - } else if (isUserInputRuntimeSuite()) { - state.evidence.userInputRunnerStopped = state.isolatedRunner.stopped; - state.evidence.userInputAppDataCleanupPerformed = - state.isolatedRunner.cleanupPerformed; - state.evidence.userInputRunnerKillMethod = killMethod; - state.evidence.userInputRunnerPidfdClaimCount = - state.isolatedRunner.pidfdClaimCount; - state.evidence.userInputRunnerPidfdSignalCount = - state.isolatedRunner.pidfdSignalCount; - state.evidence.formalConfigCliCallCount = - state.isolatedRunner.sourceConfigCliCallCount; - state.evidence.sourceRunnerEndpointUnchanged = - state.isolatedRunner.sourceRunnerEndpointUnchanged; - state.evidence.sourceConfigHardlinkCount = - state.isolatedRunner.configLinks.length; - state.evidence.sourceConfigLinksVerified = - state.isolatedRunner.sourceConfigLinksVerified; - state.evidence.isolatedAppDataUsed = true; - if (state.isolatedRunner.sourceConfigCliCallCount > 0) { - state.status = 'FAIL'; - recordError('user-input-formal-config-cli-call-detected'); - } } else if (isScopedAgentsSuite()) { state.evidence.scopedAgentsRunnerStopped = state.isolatedRunner.stopped; state.evidence.scopedAgentsAppDataCleanupPerformed = @@ -588,28 +409,6 @@ if (selfTestRequested) { state.status = 'FAIL'; recordError('parallel-read-formal-config-cli-call-detected'); } - } else if (isSupervisorAutonomousPlayableLaneDefenseSuite()) { - state.evidence.supervisorAutonomousPlayableRunnerStopped = - state.isolatedRunner.stopped; - state.evidence.supervisorAutonomousPlayableAppDataCleanupPerformed = - state.isolatedRunner.cleanupPerformed; - state.evidence.formalConfigCliCallCount = - state.isolatedRunner.sourceConfigCliCallCount; - state.evidence.sourceRunnerEndpointUnchanged = - state.isolatedRunner.sourceRunnerEndpointUnchanged; - state.evidence.sourceAppDataDirectoryUntouched = - state.isolatedRunner.sourceAppDataDirectoryUntouched; - state.evidence.sourceConfigReplicaCount = - state.isolatedRunner.configLinks.length; - state.evidence.sourceConfigReplicasVerified = - state.isolatedRunner.sourceConfigLinksVerified; - state.evidence.isolatedAppDataUsed = true; - if (state.isolatedRunner.sourceConfigCliCallCount > 0) { - state.status = 'FAIL'; - recordError( - 'supervisor-autonomous-playable-formal-config-cli-call-detected', - ); - } } else if (isSupervisorSwarmSuite()) { state.evidence.supervisorSwarmRunnerStopped = state.isolatedRunner.stopped; @@ -719,61 +518,6 @@ if (selfTestRequested) { ); } } - if ( - isSupervisorAutonomousPlayableLaneDefenseSuite() && - state.isolatedRunner.appDataDir - ) { - const snapshot = - state.supervisorAutonomousPlayable.ownedProcessCleanupSnapshot; - if (snapshot) { - try { - const cleanup = await verifyOwnedProcessCleanupSnapshot(snapshot); - state.evidence.ownedProcessIdentityCaptured = true; - state.evidence.ownedRunnerObservedCount = - snapshot.observedCounts.runner; - state.evidence.ownedHelperObservedCount = - snapshot.observedCounts.helper; - state.evidence.ownedNodeDescendantObservedCount = - snapshot.observedCounts.node; - state.evidence.ownedBrowserDescendantObservedCount = - snapshot.observedCounts.browser; - state.evidence.ownedCommandDescendantObservedCount = - snapshot.observedCounts.command; - state.evidence.ownedRunnerResidualCount = - cleanup.residualCounts.runner; - state.evidence.ownedHelperResidualCount = - cleanup.residualCounts.helper; - state.evidence.ownedNodeDescendantResidualCount = - cleanup.residualCounts.node; - state.evidence.ownedBrowserDescendantResidualCount = - cleanup.residualCounts.browser; - state.evidence.ownedCommandDescendantResidualCount = - cleanup.residualCounts.command; - state.evidence.activeCommandChildrenAfterCleanup = - cleanup.activeCommandChildCount; - state.evidence.activeInteractiveCliSessionsAfterCleanup = - cleanup.activeInteractiveCliSessionCount; - state.evidence.ownedProcessCleanupPassed = cleanup.clean; - if (!cleanup.clean) { - state.status = 'FAIL'; - recordError( - 'supervisor-autonomous-playable-owned-process-residual-detected', - ); - } - } catch (error) { - state.status = 'FAIL'; - recordError( - 'supervisor-autonomous-playable-owned-process-verification-failed', - error, - ); - } - } else { - state.status = 'FAIL'; - recordError( - 'supervisor-autonomous-playable-owned-process-snapshot-missing', - ); - } - } if ( isSteerRunnerKillSuite() && state.projectRoot && @@ -836,20 +580,6 @@ if (selfTestRequested) { recordError('context-compaction-partial-evidence-read-failed', error); } } - if ( - isUserInputRuntimeSuite() && - state.projectRoot && - state.status !== 'PASS' - ) { - try { - state.evidence = { - ...state.evidence, - ...(await collectPartialUserInputEvidence()), - }; - } catch (error) { - recordError('user-input-partial-evidence-read-failed', error); - } - } if (isScopedAgentsSuite() && state.projectRoot && state.status !== 'PASS') { try { state.evidence = { @@ -880,24 +610,6 @@ if (selfTestRequested) { recordError('parallel-read-partial-evidence-read-failed', error); } } - if ( - isSupervisorAutonomousPlayableLaneDefenseSuite() && - state.projectRoot && - state.status !== 'PASS' && - state.evidence.evidenceCompleteness !== 'complete' - ) { - try { - state.evidence = { - ...state.evidence, - ...(await collectPartialSupervisorAutonomousPlayableEvidence()), - }; - } catch (error) { - recordError( - 'supervisor-autonomous-playable-partial-evidence-read-failed', - error, - ); - } - } if ( isSupervisorSwarmSuite() && state.projectRoot && @@ -1062,23 +774,6 @@ if (selfTestRequested) { report = JSON.stringify(summary, null, 2); } } - if (isUserInputRuntimeSuite()) { - state.userInput.reportLeakCount = countExactSecrets( - Buffer.from(report), - [ - userInputAnswerCanary, - userInputAnswerText, - ...state.userInput.privateValues, - ].filter(isNonEmptyString), - ); - state.evidence.userInputReportLeakCount = state.userInput.reportLeakCount; - if (state.userInput.reportLeakCount > 0) { - state.status = 'FAIL'; - recordError('user-input-private-body-report-leak-detected'); - summary = buildSummary(); - report = JSON.stringify(summary, null, 2); - } - } if (isScopedAgentsSuite()) { state.scopedAgents.reportLeakCount = countExactSecrets( Buffer.from(report), @@ -1121,20 +816,6 @@ if (selfTestRequested) { report = JSON.stringify(summary, null, 2); } } - if (isSupervisorAutonomousPlayableLaneDefenseSuite()) { - state.supervisorAutonomousPlayable.reportLeakCount = countExactSecrets( - Buffer.from(report), - state.supervisorAutonomousPlayable.privateValues, - ); - state.evidence.supervisorAutonomousPlayableReportLeakCount = - state.supervisorAutonomousPlayable.reportLeakCount; - if (state.supervisorAutonomousPlayable.reportLeakCount > 0) { - state.status = 'FAIL'; - recordError('supervisor-autonomous-playable-report-body-leak-detected'); - summary = buildSummary(); - report = JSON.stringify(summary, null, 2); - } - } if (isSupervisorSwarmSuite()) { state.supervisorSwarm.reportLeakCount = countExactSecrets( Buffer.from(report), @@ -1164,11 +845,9 @@ if (selfTestRequested) { if ( isWebSearchSuite() || isContextCompactionSuite() || - isUserInputRuntimeSuite() || isScopedAgentsSuite() || isProjectSkillSuite() || isParallelReadSuite() || - isSupervisorAutonomousPlayableLaneDefenseSuite() || isSupervisorSwarmSuite() || isSteerRunnerKillSuite() ) { @@ -1211,16 +890,6 @@ if (selfTestRequested) { const remainingWebSearchReportLeakCount = isWebSearchSuite() ? countExactSecrets(Buffer.from(report), webSearchPrivateLeakValues()) : 0; - const remainingUserInputReportLeakCount = isUserInputRuntimeSuite() - ? countExactSecrets( - Buffer.from(report), - [ - userInputAnswerCanary, - userInputAnswerText, - ...state.userInput.privateValues, - ].filter(isNonEmptyString), - ) - : 0; const remainingScopedAgentsReportLeakCount = isScopedAgentsSuite() ? countExactSecrets(Buffer.from(report), state.scopedAgents.privateValues) : 0; @@ -1230,13 +899,6 @@ if (selfTestRequested) { const remainingParallelReadReportLeakCount = isParallelReadSuite() ? countExactSecrets(Buffer.from(report), state.parallelRead.privateValues) : 0; - const remainingSupervisorAutonomousPlayableReportLeakCount = - isSupervisorAutonomousPlayableLaneDefenseSuite() - ? countExactSecrets( - Buffer.from(report), - state.supervisorAutonomousPlayable.privateValues, - ) - : 0; const remainingSupervisorSwarmReportLeakCount = isSupervisorSwarmSuite() ? countExactSecrets( Buffer.from(report), @@ -1246,11 +908,9 @@ if (selfTestRequested) { const remainingFormalConfigPathReportLeakCount = isWebSearchSuite() || isContextCompactionSuite() || - isUserInputRuntimeSuite() || isScopedAgentsSuite() || isProjectSkillSuite() || isParallelReadSuite() || - isSupervisorAutonomousPlayableLaneDefenseSuite() || isSupervisorSwarmSuite() || isSteerRunnerKillSuite() ? countExactSecrets(Buffer.from(report), formalConfigPathVariants()) @@ -1259,11 +919,9 @@ if (selfTestRequested) { remainingProjectPathReportLeakCount > 0 || remainingResponseStreamReportLeakCount > 0 || remainingWebSearchReportLeakCount > 0 || - remainingUserInputReportLeakCount > 0 || remainingScopedAgentsReportLeakCount > 0 || remainingProjectSkillReportLeakCount > 0 || remainingParallelReadReportLeakCount > 0 || - remainingSupervisorAutonomousPlayableReportLeakCount > 0 || remainingSupervisorSwarmReportLeakCount > 0 || remainingFormalConfigPathReportLeakCount > 0 ) { @@ -1273,21 +931,17 @@ if (selfTestRequested) { ? 'disposable-project-path-report-redaction-required' : remainingResponseStreamReportLeakCount > 0 ? 'response-stream-report-redaction-required' - : remainingUserInputReportLeakCount > 0 - ? 'user-input-report-redaction-required' - : remainingScopedAgentsReportLeakCount > 0 - ? 'scoped-agents-report-redaction-required' - : remainingProjectSkillReportLeakCount > 0 - ? 'project-skill-report-redaction-required' - : remainingParallelReadReportLeakCount > 0 - ? 'parallel-read-report-redaction-required' - : remainingSupervisorAutonomousPlayableReportLeakCount > 0 - ? 'supervisor-autonomous-playable-report-redaction-required' - : remainingSupervisorSwarmReportLeakCount > 0 - ? 'supervisor-swarm-report-redaction-required' - : remainingFormalConfigPathReportLeakCount > 0 - ? 'formal-config-path-report-redaction-required' - : 'web-search-report-redaction-required', + : remainingScopedAgentsReportLeakCount > 0 + ? 'scoped-agents-report-redaction-required' + : remainingProjectSkillReportLeakCount > 0 + ? 'project-skill-report-redaction-required' + : remainingParallelReadReportLeakCount > 0 + ? 'parallel-read-report-redaction-required' + : remainingSupervisorSwarmReportLeakCount > 0 + ? 'supervisor-swarm-report-redaction-required' + : remainingFormalConfigPathReportLeakCount > 0 + ? 'formal-config-path-report-redaction-required' + : 'web-search-report-redaction-required', ); const safeSummary = { status: state.status, @@ -1302,19 +956,13 @@ if (selfTestRequested) { projectPathReportLeakCount: remainingProjectPathReportLeakCount, responseStreamReportLeakCount: remainingResponseStreamReportLeakCount, webSearchReportLeakCount: remainingWebSearchReportLeakCount, - userInputReportLeakCount: remainingUserInputReportLeakCount, scopedAgentsReportLeakCount: remainingScopedAgentsReportLeakCount, projectSkillReportLeakCount: remainingProjectSkillReportLeakCount, parallelReadReportLeakCount: remainingParallelReadReportLeakCount, - supervisorAutonomousPlayableReportLeakCount: - remainingSupervisorAutonomousPlayableReportLeakCount, supervisorSwarmReportLeakCount: remainingSupervisorSwarmReportLeakCount, formalConfigPathReportLeakCount: remainingFormalConfigPathReportLeakCount, - ...(isSupervisorSwarmSuite() - ? supervisorSwarmChatSessionFailureEvidence() - : {}), }, errorCount: state.errors.length, errorHashes: state.errors.map(summarizeRecordedError), diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/harness/app-data.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/harness/app-data.mjs index 3d8e052c6..c5ed62fa2 100644 --- a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/harness/app-data.mjs +++ b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/harness/app-data.mjs @@ -37,26 +37,16 @@ import { state, steerRunnerKillAppDataSentinelFileName, steerRunnerKillAppDataSentinelSchema, - supervisorAutonomousPlayableAppDataSentinelFileName, - supervisorAutonomousPlayableAppDataSentinelSchema, supervisorSwarmAppDataSentinelFileName, supervisorSwarmAppDataSentinelSchema, - supervisorSwarmAutonomousChatAppDataSentinelFileName, - supervisorSwarmAutonomousChatAppDataSentinelSchema, - supervisorSwarmCollaborationPolicyAppDataSentinelFileName, - supervisorSwarmCollaborationPolicyAppDataSentinelSchema, supervisorSwarmFinalReplyTransientRetryAppDataSentinelFileName, supervisorSwarmFinalReplyTransientRetryAppDataSentinelSchema, supervisorSwarmRequiredAgentIds, - supervisorSwarmStaticIsolatedAutonomousChatAppDataSentinelFileName, - supervisorSwarmStaticIsolatedAutonomousChatAppDataSentinelSchema, supervisorSwarmToolPlanHandoffRunnerKillAppDataSentinelFileName, supervisorSwarmToolPlanHandoffRunnerKillAppDataSentinelSchema, supervisorSwarmTransientRetryAppDataSentinelFileName, supervisorSwarmTransientRetryAppDataSentinelSchema, supervisorSwarmTransientRetryTargetAgentId, - userInputAppDataSentinelFileName, - userInputAppDataSentinelSchema, webSearchAppDataSentinelFileName, webSearchAppDataSentinelSchema, windowsProcessHandleHelperSource, @@ -68,21 +58,14 @@ import { isProjectSkillSuite } from '../suites/project-skill.mjs'; import { isResponseStreamSuite } from '../suites/response-stream.mjs'; import { isScopedAgentsSuite } from '../suites/scoped-agents.mjs'; import { isSteerRunnerKillSuite } from '../suites/steer-runner-kill.mjs'; -import { isSupervisorAutonomousPlayableLaneDefenseSuite } from '../suites/supervisor-autonomous-playable.mjs'; import { - isSupervisorSwarmAutonomousChatSuite, - isSupervisorSwarmCollaborationPolicyMixedRecoverySuite, isSupervisorSwarmFinalReplyTransientRetrySuite, isSupervisorSwarmInitialTransientRetrySuite, - isSupervisorSwarmInteractiveChatSuite, - isSupervisorSwarmMixedHarnessSuite, - isSupervisorSwarmStaticIsolatedAutonomousChatSuite, isSupervisorSwarmSuite, isSupervisorSwarmToolPlanHandoffRunnerKillSuite, isSupervisorSwarmTransientRetrySuite, rebuildSupervisorSwarmTranscriptScanner, } from '../suites/supervisor-swarm.mjs'; -import { isUserInputRuntimeSuite } from '../suites/user-input.mjs'; import { isWebSearchSuite, sameEffectiveAgentLlmWithoutWebSearch, @@ -100,19 +83,10 @@ import { appendBounded, runProcess } from './process.mjs'; import { decodeUtf8Fatal, isIsolatedRunnerSuite } from './reporting.mjs'; import { killRunnerOnce, readRunnerStatus, runnerBootId } from './runtime.mjs'; -const platformSessionFixtureEnv = 'GENARRATIVE_AGC_PLATFORM_SESSION_FIXTURE'; -const platformSessionFixtureMaxBytes = 16 * 1024; -const isolatedPlatformSessionFixtureName = - '.deterministic-platform-session.json'; -const platformSessionFixtureSchema = - 'genarrative-agc-platform-session-fixture.v1'; - export function isolatedSuiteProtectsSourceAppData() { return ( isSupervisorSwarmTransientRetrySuite() || - isSupervisorSwarmToolPlanHandoffRunnerKillSuite() || - isSupervisorAutonomousPlayableLaneDefenseSuite() || - isSupervisorSwarmInteractiveChatSuite() + isSupervisorSwarmToolPlanHandoffRunnerKillSuite() ); } @@ -120,27 +94,6 @@ export function isolatedSuiteUsesSiblingAppData() { return isWebSearchSuite() || isolatedSuiteProtectsSourceAppData(); } -export function sameSupervisorPlayableProviderBinding(left, right) { - const scalarFields = [ - 'providerAgentMode', - 'providerModel', - 'providerApiKind', - 'providerReasoningEffort', - 'providerBaseUrlSha256', - ]; - return ( - left != null && - right != null && - scalarFields.every((field) => left[field] === right[field]) && - Array.isArray(left.boundAgentIds) && - Array.isArray(right.boundAgentIds) && - left.boundAgentIds.length === right.boundAgentIds.length && - left.boundAgentIds.every( - (agentId, index) => agentId === right.boundAgentIds[index], - ) - ); -} - export function isolatedSuiteAppDataProfile() { if (isSteerRunnerKillSuite()) { return { @@ -174,14 +127,6 @@ export function isolatedSuiteAppDataProfile() { codePrefix: 'context-compaction-appdata', }; } - if (isUserInputRuntimeSuite()) { - return { - prefix: '.agent-runtime-real-e2e-user-input-', - sentinelName: userInputAppDataSentinelFileName, - sentinelSchema: userInputAppDataSentinelSchema, - codePrefix: 'user-input-appdata', - }; - } if (isScopedAgentsSuite()) { return { prefix: '.agent-runtime-real-e2e-scoped-agents-', @@ -236,41 +181,6 @@ export function isolatedSuiteAppDataProfile() { codePrefix: 'supervisor-swarm-transient-retry-appdata', }; } - if (isSupervisorAutonomousPlayableLaneDefenseSuite()) { - return { - prefix: '.agent-runtime-real-e2e-supervisor-autonomous-playable-', - sentinelName: supervisorAutonomousPlayableAppDataSentinelFileName, - sentinelSchema: supervisorAutonomousPlayableAppDataSentinelSchema, - codePrefix: 'supervisor-autonomous-playable-appdata', - }; - } - if (isSupervisorSwarmAutonomousChatSuite()) { - return { - prefix: '.agent-runtime-real-e2e-supervisor-swarm-autonomous-chat-', - sentinelName: supervisorSwarmAutonomousChatAppDataSentinelFileName, - sentinelSchema: supervisorSwarmAutonomousChatAppDataSentinelSchema, - codePrefix: 'supervisor-swarm-autonomous-chat-appdata', - }; - } - if (isSupervisorSwarmStaticIsolatedAutonomousChatSuite()) { - return { - prefix: - '.agent-runtime-real-e2e-supervisor-swarm-static-isolated-autonomous-chat-', - sentinelName: - supervisorSwarmStaticIsolatedAutonomousChatAppDataSentinelFileName, - sentinelSchema: - supervisorSwarmStaticIsolatedAutonomousChatAppDataSentinelSchema, - codePrefix: 'supervisor-swarm-static-isolated-autonomous-chat-appdata', - }; - } - if (isSupervisorSwarmCollaborationPolicyMixedRecoverySuite()) { - return { - prefix: '.agent-runtime-real-e2e-supervisor-swarm-collaboration-policy-', - sentinelName: supervisorSwarmCollaborationPolicyAppDataSentinelFileName, - sentinelSchema: supervisorSwarmCollaborationPolicyAppDataSentinelSchema, - codePrefix: 'supervisor-swarm-collaboration-policy-appdata', - }; - } if (isSupervisorSwarmSuite()) { return { prefix: '.agent-runtime-real-e2e-supervisor-swarm-', @@ -505,120 +415,6 @@ export async function verifySourceAppDataDirectoryUntouched() { state.isolatedRunner.sourceAppDataDirectoryUntouched = true; } -async function readPlatformSessionFixtureForIsolatedSuite(sourceConfigDir) { - const rawPath = process.env[platformSessionFixtureEnv]; - assert( - isNonEmptyString(rawPath) && path.isAbsolute(rawPath), - 'supervisor-autonomous-playable-platform-session-fixture-missing', - ); - const sourceRealPath = await fs.realpath(sourceConfigDir); - const requestedPath = path.resolve(rawPath); - const requestedMetadata = await fs.lstat(requestedPath).catch((error) => { - if (error?.code === 'ENOENT') return null; - throw error; - }); - assert( - requestedMetadata?.isFile() && !requestedMetadata.isSymbolicLink(), - 'supervisor-autonomous-playable-platform-session-fixture-not-regular', - ); - assert( - requestedMetadata.size <= platformSessionFixtureMaxBytes, - 'supervisor-autonomous-playable-platform-session-fixture-too-large', - ); - const realPath = await fs.realpath(requestedPath); - assert( - isPathInside(sourceRealPath, realPath), - 'supervisor-autonomous-playable-platform-session-fixture-outside-config', - ); - const bytes = await fs.readFile(realPath); - assert( - bytes.length <= platformSessionFixtureMaxBytes, - 'supervisor-autonomous-playable-platform-session-fixture-too-large', - ); - let fixture; - try { - fixture = JSON.parse( - decodeUtf8Fatal(bytes, 'platform-session-fixture-invalid-utf8'), - ); - } catch (error) { - throw codedError( - 'supervisor-autonomous-playable-platform-session-fixture-invalid', - error, - ); - } - const expectedKeys = [ - 'schemaVersion', - 'userId', - 'accessToken', - 'apiBaseUrl', - 'generation', - ]; - assert( - isPlainObject(fixture) && - JSON.stringify(Object.keys(fixture).sort()) === - JSON.stringify([...expectedKeys].sort()) && - fixture.schemaVersion === platformSessionFixtureSchema && - isNonEmptyString(fixture.userId) && - isNonEmptyString(fixture.accessToken) && - isNonEmptyString(fixture.apiBaseUrl) && - Number.isSafeInteger(fixture.generation) && - fixture.generation > 0, - 'supervisor-autonomous-playable-platform-session-fixture-invalid', - ); - return { - sourcePath: realPath, - bytes, - fixture, - sha256: createHash('sha256').update(bytes).digest('hex'), - }; -} - -async function installPlatformSessionFixtureIntoIsolatedAppData( - sourceConfigDir, - appDataDir, -) { - if (!isSupervisorAutonomousPlayableLaneDefenseSuite()) return; - const source = - await readPlatformSessionFixtureForIsolatedSuite(sourceConfigDir); - const isolatedPath = path.join( - appDataDir, - isolatedPlatformSessionFixtureName, - ); - await fs.copyFile( - source.sourcePath, - isolatedPath, - fsConstants.COPYFILE_EXCL | fsConstants.COPYFILE_FICLONE, - ); - await fs.chmod(isolatedPath, 0o600).catch(() => {}); - const isolatedMetadata = await fs.lstat(isolatedPath); - assert( - isolatedMetadata.isFile() && - !isolatedMetadata.isSymbolicLink() && - isolatedMetadata.size === source.bytes.length, - 'supervisor-autonomous-playable-platform-session-fixture-copy-invalid', - ); - const isolatedBytes = await fs.readFile(isolatedPath); - assert( - createHash('sha256').update(isolatedBytes).digest('hex') === source.sha256, - 'supervisor-autonomous-playable-platform-session-fixture-copy-mismatch', - ); - state.isolatedRunner.platformSessionFixtureSourcePath = source.sourcePath; - state.isolatedRunner.platformSessionFixturePath = isolatedPath; - state.isolatedRunner.platformSessionFixtureSha256 = source.sha256; - state.isolatedRunner.platformSessionFixturePreviousEnv = - Object.prototype.hasOwnProperty.call(process.env, platformSessionFixtureEnv) - ? process.env[platformSessionFixtureEnv] - : undefined; - process.env[platformSessionFixtureEnv] = isolatedPath; - state.formalConfigPathTranscriptScanner?.addSecrets( - absolutePathVariants(source.sourcePath, isolatedPath), - ); - const previousLeakCount = state.transcriptScanner?.count ?? 0; - state.secrets = [...new Set([...state.secrets, source.fixture.accessToken])]; - rebuildSupervisorSwarmTranscriptScanner(); - state.transcriptScanner.count = previousLeakCount; -} - export async function prepareIsolatedSuiteAppData({ streamAgentId = null, webSearchAgentId = null, @@ -773,8 +569,7 @@ export async function prepareIsolatedSuiteAppData({ isScopedAgentsSuite() || isProjectSkillSuite() || isParallelReadSuite() || - isSupervisorSwarmSuite() || - isSupervisorAutonomousPlayableLaneDefenseSuite() + isSupervisorSwarmSuite() ? 'private-copy' : 'hardlink'; try { @@ -891,10 +686,6 @@ export async function prepareIsolatedSuiteAppData({ state.secrets = [...suiteSecrets]; rebuildSupervisorSwarmTranscriptScanner(); state.transcriptScanner.count = previousLeakCount; - await installPlatformSessionFixtureIntoIsolatedAppData( - sourceConfigDir, - appDataDir, - ); const unexpectedEndpoint = await fs .lstat(path.join(appDataDir, runnerEndpointFileName)) .catch((error) => { @@ -903,46 +694,6 @@ export async function prepareIsolatedSuiteAppData({ }); assert(!unexpectedEndpoint, `${profile.codePrefix}-endpoint-preexisted`); state.runtimeConfigDir = appDataDir; - if (isSupervisorAutonomousPlayableLaneDefenseSuite()) { - const isolatedConfig = await loadConfig(appDataDir); - const expectedBinding = state.config.providerBinding; - const agentIds = [projectSupervisorAgentId]; - const bindings = agentIds.map((agentId) => { - const effective = effectiveAgentLlmConfig(isolatedConfig.config, agentId); - assert( - ['apiKey', 'baseUrl', 'model', 'apiKind', 'reasoningEffort'].every( - (key) => isNonEmptyString(effective[key]), - ), - 'supervisor-autonomous-playable-effective-provider-incomplete', - ); - return { - providerModel: effective.model.trim(), - providerApiKind: effective.apiKind.trim(), - providerReasoningEffort: effective.reasoningEffort.trim(), - providerBaseUrlSha256: hashValue(effective.baseUrl.trim()), - }; - }); - const effectiveBinding = { - ...bindings[0], - boundAgentIds: [...agentIds].sort(), - }; - assert( - expectedBinding && - bindings.every( - (binding) => JSON.stringify(binding) === JSON.stringify(bindings[0]), - ) && - sameSupervisorPlayableProviderBinding( - effectiveBinding, - expectedBinding, - ), - 'supervisor-autonomous-playable-effective-provider-binding-mismatch', - ); - state.supervisorAutonomousPlayable.expectedProviderBinding = { - ...expectedBinding, - }; - state.supervisorAutonomousPlayable.effectiveProviderBinding = - effectiveBinding; - } if (streamAgentId) { const isolatedConfig = await loadConfig(appDataDir); const isolatedEffective = effectiveAgentLlmConfig( @@ -981,22 +732,6 @@ export async function prepareIsolatedSuiteAppData({ ); state.webSearch.effectiveEnabled = true; } - if (isUserInputRuntimeSuite()) { - const isolatedConfig = await loadConfig(appDataDir); - const isolatedEffective = effectiveAgentLlmConfig( - isolatedConfig.config, - projectSupervisorAgentId, - ); - assert( - isolatedEffective.model === 'gpt-5.5' && - ['apiKey', 'baseUrl', 'model'].every( - (key) => - typeof isolatedEffective[key] === 'string' && - isolatedEffective[key].trim().length > 0, - ), - 'user-input-effective-gpt-5-5-config-invalid', - ); - } if (isScopedAgentsSuite()) { const isolatedConfig = await loadConfig(appDataDir); const isolatedEffective = effectiveAgentLlmConfig( @@ -1073,9 +808,8 @@ export async function prepareIsolatedSuiteAppData({ effective.requestTimeoutMs > 0 && Number.isSafeInteger(effective.maxRetries) && effective.maxRetries >= - (isSupervisorSwarmInteractiveChatSuite() || (isSupervisorSwarmTransientRetrySuite() && - agentId !== supervisorSwarmTransientRetryTargetAgentId) + agentId !== supervisorSwarmTransientRetryTargetAgentId ? 0 : 1) && effective.maxRetries <= 3 && @@ -1095,24 +829,6 @@ export async function prepareIsolatedSuiteAppData({ { llm: isolatedConfig.config.llm }, '', ); - if (isSupervisorSwarmMixedHarnessSuite()) { - assert( - globalEffective.model === 'gpt-5.5' && - globalEffective.apiKind === 'openai_chat' && - isNonEmptyString(globalEffective.reasoningEffort) && - Number.isSafeInteger(globalEffective.requestTimeoutMs) && - globalEffective.requestTimeoutMs > 0 && - Number.isSafeInteger(globalEffective.maxRetries) && - globalEffective.maxRetries >= 0 && - globalEffective.maxRetries <= 3 && - Number.isSafeInteger(globalEffective.retryBackoffMs) && - globalEffective.retryBackoffMs > 0 && - ['apiKey', 'baseUrl', 'model'].every((key) => - isNonEmptyString(globalEffective[key]), - ), - 'supervisor-swarm-mixed-default-provider-policy-invalid', - ); - } const configuredAgentIds = Object.keys( isPlainObject(isolatedConfig.config.agentLlm) ? isolatedConfig.config.agentLlm @@ -2000,31 +1716,6 @@ export async function verifyIsolatedSuiteConfigLinksUnchanged() { 'isolated-source-config-changed-during-suite', ); } - const fixture = state.isolatedRunner; - if ( - fixture.platformSessionFixtureSourcePath && - fixture.platformSessionFixturePath && - fixture.platformSessionFixtureSha256 - ) { - const [sourceMetadata, isolatedMetadata, sourceBytes, isolatedBytes] = - await Promise.all([ - fs.lstat(fixture.platformSessionFixtureSourcePath), - fs.lstat(fixture.platformSessionFixturePath), - fs.readFile(fixture.platformSessionFixtureSourcePath), - fs.readFile(fixture.platformSessionFixturePath), - ]); - assert( - sourceMetadata.isFile() && - !sourceMetadata.isSymbolicLink() && - isolatedMetadata.isFile() && - !isolatedMetadata.isSymbolicLink() && - createHash('sha256').update(sourceBytes).digest('hex') === - fixture.platformSessionFixtureSha256 && - createHash('sha256').update(isolatedBytes).digest('hex') === - fixture.platformSessionFixtureSha256, - 'supervisor-autonomous-playable-platform-session-fixture-changed', - ); - } } export async function verifySourceConfigLinkCountsRestored() { @@ -2064,22 +1755,7 @@ export async function removeIsolatedSuiteAppData() { } catch (error) { ownershipError = error; } - try { - await fs.rm(appDataDir, { recursive: true, force: false }); - } finally { - if (state.isolatedRunner.platformSessionFixtureSourcePath) { - const previous = state.isolatedRunner.platformSessionFixturePreviousEnv; - if (previous === undefined) { - delete process.env[platformSessionFixtureEnv]; - } else { - process.env[platformSessionFixtureEnv] = previous; - } - } - state.isolatedRunner.platformSessionFixturePath = null; - state.isolatedRunner.platformSessionFixtureSourcePath = null; - state.isolatedRunner.platformSessionFixtureSha256 = null; - state.isolatedRunner.platformSessionFixturePreviousEnv = undefined; - } + await fs.rm(appDataDir, { recursive: true, force: false }); state.runtimeConfigDir = state.options.configDir; try { await verifySourceConfigLinkCountsRestored(); diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/harness/config.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/harness/config.mjs index 54efb5c75..43768d3b3 100644 --- a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/harness/config.mjs +++ b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/harness/config.mjs @@ -13,15 +13,10 @@ import { responseStreamSuite, scopedAgentsSuite, steerRunnerKillSuite, - supervisorAutonomousPlayableLaneDefenseSuite, - supervisorSwarmAutonomousChatSuite, - supervisorSwarmCollaborationPolicyMixedRecoverySuite, supervisorSwarmFinalReplyTransientRetrySuite, - supervisorSwarmStaticIsolatedAutonomousChatSuite, supervisorSwarmSuite, supervisorSwarmToolPlanHandoffRunnerKillSuite, supervisorSwarmTransientRetrySuite, - userInputRuntimeSuite, webSearchSuite, } from '../runtime-state.mjs'; import { collectApiKeys, isPathInside } from './io.mjs'; @@ -58,7 +53,6 @@ export function parseArguments(args) { suite === responseStreamSuite || suite === webSearchSuite || suite === contextCompactionSuite || - suite === userInputRuntimeSuite || suite === scopedAgentsSuite || suite === projectSkillSuite || suite === parallelReadSuite || @@ -66,10 +60,6 @@ export function parseArguments(args) { suite === supervisorSwarmTransientRetrySuite || suite === supervisorSwarmFinalReplyTransientRetrySuite || suite === supervisorSwarmToolPlanHandoffRunnerKillSuite || - suite === supervisorSwarmAutonomousChatSuite || - suite === supervisorAutonomousPlayableLaneDefenseSuite || - suite === supervisorSwarmStaticIsolatedAutonomousChatSuite || - suite === supervisorSwarmCollaborationPolicyMixedRecoverySuite || suite === steerRunnerKillSuite || processSessionSuites.has(suite), 'unsupported-suite', diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/harness/process.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/harness/process.mjs index 950b9646a..427b42e25 100644 --- a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/harness/process.mjs +++ b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/harness/process.mjs @@ -2,7 +2,6 @@ import { assert, codedError, hashValue, - sleep, throwIfShutdownRequested, } from '../assertions/core.mjs'; import { fs, path, spawn, withLoopbackNoProxy } from '../dependencies.mjs'; @@ -13,14 +12,9 @@ import { manifestPath, state, } from '../runtime-state.mjs'; -import { - isSupervisorSwarmTransientRetrySuite, - recordSupervisorSwarmChatSessionFailureDiagnostic, -} from '../suites/supervisor-swarm.mjs'; +import { isSupervisorSwarmTransientRetrySuite } from '../suites/supervisor-swarm.mjs'; import { isIsolatedRunnerSuite } from './reporting.mjs'; -export const activeInteractiveCliSessions = new Set(); - export async function prepareCliBinary() { const cargo = process.platform === 'win32' ? 'cargo.exe' : 'cargo'; await runProcess( @@ -144,197 +138,6 @@ export function codedProcessError(code, details) { return error; } -export function startInteractiveCli(args) { - assert(Boolean(state.cliBinary), 'interactive-cli-binary-not-ready'); - assert(Boolean(state.runtimeConfigDir), 'interactive-config-dir-not-ready'); - if ( - isIsolatedRunnerSuite() && - state.options?.configDir && - path.resolve(state.runtimeConfigDir) === - path.resolve(state.options.configDir) - ) { - state.isolatedRunner.sourceConfigCliCallCount += 1; - } - const child = spawn( - state.cliBinary, - [...args, '--config-dir', state.runtimeConfigDir], - { - cwd: appRoot, - env: buildCliChildEnvironment(), - stdio: ['pipe', 'pipe', 'pipe'], - }, - ); - return createInteractiveCliSession(child); -} - -export function createInteractiveCliSession(child) { - activeCommandChildren.add(child); - const session = { - child, - stdout: Buffer.alloc(0), - stderr: Buffer.alloc(0), - exited: false, - exitInfo: null, - exitPromise: null, - closed: false, - closeInfo: null, - closePromise: null, - stdioClosed: false, - stdioCloseInfo: null, - spawnError: null, - stdinError: null, - }; - activeInteractiveCliSessions.add(session); - session.exitPromise = new Promise((resolve) => { - const settle = (result) => { - if (session.exited) return; - activeCommandChildren.delete(child); - session.exited = true; - session.closed = true; - session.exitInfo = result; - session.closeInfo = result; - resolve(result); - }; - child.once('error', (error) => { - session.spawnError = error; - settle({ code: null, signal: null, error }); - }); - child.once('exit', (code, signal) => { - settle({ code, signal, error: null }); - }); - }); - session.closePromise = new Promise((resolve) => { - child.once('close', (code, signal) => { - activeCommandChildren.delete(child); - activeInteractiveCliSessions.delete(session); - session.stdioClosed = true; - session.stdioCloseInfo = { - code, - signal, - error: session.spawnError, - }; - resolve(session.stdioCloseInfo); - }); - }); - child.stdin?.on('error', (error) => { - session.stdinError ??= error; - }); - child.stdout.on('data', (chunk) => { - state.transcriptScanner?.scan('interactive-stdout', chunk); - state.projectPathTranscriptScanner?.scan('interactive-stdout', chunk); - state.formalConfigPathTranscriptScanner?.scan('interactive-stdout', chunk); - session.stdout = appendBounded(session.stdout, chunk, commandOutputLimit); - }); - child.stderr.on('data', (chunk) => { - state.transcriptScanner?.scan('interactive-stderr', chunk); - state.projectPathTranscriptScanner?.scan('interactive-stderr', chunk); - state.formalConfigPathTranscriptScanner?.scan('interactive-stderr', chunk); - session.stderr = appendBounded(session.stderr, chunk, commandOutputLimit); - }); - return session; -} - -export function interactiveCliOutput(session) { - return `${session.stdout.toString('utf8')}\n${session.stderr.toString('utf8')}`; -} - -export function writeInteractiveCliLine(session, line) { - assert(!session.closed, 'interactive-cli-already-closed'); - assert(session.child.stdin.writable, 'interactive-cli-stdin-not-writable'); - session.child.stdin.write(`${line}\n`); -} - -export async function waitForInteractiveCliOutput( - session, - predicate, - code, - timeoutMs, - { allowAfterProcessExit = false } = {}, -) { - const deadline = Date.now() + timeoutMs; - while (Date.now() < deadline) { - const output = interactiveCliOutput(session); - if (predicate(output)) return output; - if (session.exited && !allowAfterProcessExit) { - if (session === state.supervisorSwarmCliSession) { - recordSupervisorSwarmChatSessionFailureDiagnostic(session); - } - throw codedError(`${code}-cli-exited`); - } - if (session.stdioClosed) { - if (session === state.supervisorSwarmCliSession) { - recordSupervisorSwarmChatSessionFailureDiagnostic(session); - } - throw codedError(`${code}-cli-stdio-closed`); - } - await sleep(50); - } - throw codedError(code); -} - -export async function waitForInteractiveCliExit(session, timeoutMs) { - const result = await Promise.race([ - session.exitPromise, - sleep(timeoutMs).then(() => null), - ]); - if (!result) throw codedError('interactive-cli-exit-timeout'); - if (result.error) throw codedError('interactive-cli-process-error'); - assert( - result.code === 0 && result.signal === null, - 'interactive-cli-exit-invalid', - ); - return result; -} - -export async function closeInteractiveCli(session) { - if (!session) return null; - if (session.exited) return session.exitInfo; - if ( - session.child.stdin.writable && - !session.child.stdin.writableEnded && - !session.child.stdin.destroyed - ) { - session.child.stdin.write('/quit\n'); - } - let result = await Promise.race([ - session.exitPromise, - sleep(3_000).then(() => null), - ]); - if (!result && !session.exited) { - session.child.kill('SIGTERM'); - result = await Promise.race([ - session.exitPromise, - sleep(2_000).then(() => null), - ]); - } - if (!result && !session.exited) { - session.child.kill('SIGKILL'); - result = await Promise.race([ - session.exitPromise, - sleep(5_000).then(() => null), - ]); - } - assert(Boolean(result), 'interactive-cli-cleanup-timeout'); - return result; -} - -export async function waitForInteractiveCliStdioClose(session, timeoutMs) { - if (!session || session.stdioClosed) return session?.stdioCloseInfo ?? null; - const result = await Promise.race([ - session.closePromise, - sleep(timeoutMs).then(() => null), - ]); - if (!result) throw codedError('interactive-cli-stdio-close-timeout'); - return result; -} - -export function destroyInteractiveCliOutputStreams(session) { - if (!session) return; - for (const stream of [session.child.stdout, session.child.stderr]) { - if (stream && !stream.destroyed) stream.destroy(); - } -} - export async function runProcess( program, args, @@ -406,214 +209,6 @@ export async function runProcess( }); } -export async function listSystemProcessIdentities() { - if (process.platform === 'win32') { - const systemRoot = process.env.SystemRoot ?? process.env.SYSTEMROOT; - assert( - typeof systemRoot === 'string' && path.isAbsolute(systemRoot), - 'owned-process-snapshot-system-root-invalid', - ); - const powershell = path.join( - systemRoot, - 'System32/WindowsPowerShell/v1.0/powershell.exe', - ); - const metadata = await fs.lstat(powershell); - assert( - metadata.isFile() && !metadata.isSymbolicLink(), - 'owned-process-snapshot-powershell-invalid', - ); - const result = await runProcess( - powershell, - [ - '-NoProfile', - '-NonInteractive', - '-Command', - '$processes = @(Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,CreationDate,Name); $processes | ConvertTo-Json -Compress', - ], - { - cwd: appRoot, - timeoutMs: 30_000, - env: { ...process.env, NO_COLOR: '1', RUST_BACKTRACE: '0' }, - }, - ); - const parsed = JSON.parse(result.stdout); - return (Array.isArray(parsed) ? parsed : [parsed]) - .map((record) => ({ - pid: Number(record?.ProcessId), - parentPid: Number(record?.ParentProcessId), - startedAt: String(record?.CreationDate ?? ''), - name: String(record?.Name ?? ''), - })) - .filter(validSystemProcessIdentity); - } - assert( - process.platform === 'linux' || process.platform === 'darwin', - 'owned-process-snapshot-platform-unsupported', - ); - const result = await runProcess( - 'ps', - ['-A', '-o', 'pid=', '-o', 'ppid=', '-o', 'lstart=', '-o', 'comm='], - { cwd: appRoot, timeoutMs: 30_000 }, - ); - return result.stdout - .split(/\r?\n/u) - .map((line) => line.trim()) - .filter(Boolean) - .map((line) => { - const fields = line.split(/\s+/u); - return { - pid: Number(fields[0]), - parentPid: Number(fields[1]), - startedAt: fields.slice(2, 7).join(' '), - name: fields.slice(7).join(' '), - }; - }) - .filter(validSystemProcessIdentity); -} - -function validSystemProcessIdentity(record) { - return ( - Number.isSafeInteger(record?.pid) && - record.pid > 0 && - Number.isSafeInteger(record.parentPid) && - record.parentPid >= 0 && - typeof record.startedAt === 'string' && - record.startedAt.length > 0 && - typeof record.name === 'string' && - record.name.length > 0 - ); -} - -export function buildOwnedProcessCleanupSnapshot( - processRecords, - { rootPids = [], runnerPid = null, helperPids = [] } = {}, -) { - assert( - Array.isArray(processRecords) && - Array.isArray(rootPids) && - Array.isArray(helperPids), - 'owned-process-snapshot-input-invalid', - ); - const records = processRecords.filter(validSystemProcessIdentity); - const byPid = new Map(records.map((record) => [record.pid, record])); - const childrenByParent = new Map(); - for (const record of records) { - const children = childrenByParent.get(record.parentPid) ?? []; - children.push(record.pid); - childrenByParent.set(record.parentPid, children); - } - const normalizedRunnerPid = Number.isSafeInteger(runnerPid) - ? runnerPid - : null; - const helperPidSet = new Set( - helperPids.filter((pid) => Number.isSafeInteger(pid) && pid > 0), - ); - const roots = [ - ...new Set( - [...rootPids, normalizedRunnerPid, ...helperPidSet].filter( - (pid) => Number.isSafeInteger(pid) && pid > 0, - ), - ), - ]; - assert(roots.length > 0, 'owned-process-snapshot-root-missing'); - const ownedPids = new Set(); - const queue = [...roots]; - while (queue.length > 0) { - const pid = queue.shift(); - if (ownedPids.has(pid)) continue; - ownedPids.add(pid); - queue.push(...(childrenByParent.get(pid) ?? [])); - } - const identities = [...ownedPids] - .map((pid) => byPid.get(pid)) - .filter(Boolean) - .map((record) => ({ - pid: record.pid, - startedAt: record.startedAt, - name: record.name, - kind: ownedProcessKind(record, normalizedRunnerPid, helperPidSet), - })) - .sort((left, right) => left.pid - right.pid); - return { - identities, - observedCounts: countOwnedProcessKinds(identities), - }; -} - -function ownedProcessKind(record, runnerPid, helperPids) { - if (record.pid === runnerPid) return 'runner'; - if (helperPids.has(record.pid)) return 'helper'; - const name = path.basename(record.name).toLowerCase(); - if (/^node(?:\.exe)?$/u.test(name)) return 'node'; - if (/^(?:chrome|chromium|msedge|google-chrome)(?:\.exe)?$/u.test(name)) { - return 'browser'; - } - return 'command'; -} - -function countOwnedProcessKinds(identities) { - const counts = { - runner: 0, - helper: 0, - node: 0, - browser: 0, - command: 0, - total: identities.length, - }; - for (const identity of identities) counts[identity.kind] += 1; - return counts; -} - -export function inspectOwnedProcessCleanupResiduals( - snapshot, - processRecords, - { activeCommandChildCount = 0, activeInteractiveCliSessionCount = 0 } = {}, -) { - assert( - Array.isArray(snapshot?.identities) && Array.isArray(processRecords), - 'owned-process-residual-input-invalid', - ); - const currentByPid = new Map( - processRecords - .filter(validSystemProcessIdentity) - .map((record) => [record.pid, record]), - ); - const residualIdentities = snapshot.identities.filter((identity) => { - const current = currentByPid.get(identity.pid); - return ( - current?.startedAt === identity.startedAt && - current?.name === identity.name - ); - }); - return { - residualCounts: countOwnedProcessKinds(residualIdentities), - activeCommandChildCount, - activeInteractiveCliSessionCount, - clean: - residualIdentities.length === 0 && - activeCommandChildCount === 0 && - activeInteractiveCliSessionCount === 0, - }; -} - -export async function captureOwnedProcessCleanupSnapshot(options) { - return buildOwnedProcessCleanupSnapshot( - await listSystemProcessIdentities(), - options, - ); -} - -export async function verifyOwnedProcessCleanupSnapshot(snapshot) { - return inspectOwnedProcessCleanupResiduals( - snapshot, - await listSystemProcessIdentities(), - { - activeCommandChildCount: activeCommandChildren.size, - activeInteractiveCliSessionCount: activeInteractiveCliSessions.size, - }, - ); -} - export function appendBounded(current, chunk, limit) { const combined = Buffer.concat([current, chunk]); return combined.length <= limit diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/harness/project.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/harness/project.mjs index 9fa9ea193..9f787680f 100644 --- a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/harness/project.mjs +++ b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/harness/project.mjs @@ -28,12 +28,10 @@ import { isGoalRuntimeSuite, } from '../suites/goal.mjs'; import { isResponseStreamSuite } from '../suites/response-stream.mjs'; -import { isSupervisorAutonomousPlayableLaneDefenseSuite } from '../suites/supervisor-autonomous-playable.mjs'; import { isSupervisorSwarmSuite, supervisorSwarmVerificationFixtureSource, } from '../suites/supervisor-swarm.mjs'; -import { isUserInputRuntimeSuite } from '../suites/user-input.mjs'; import { isWebSearchSuite } from '../suites/web-search.mjs'; import { createSentinelOwnedTempDirectory } from './app-data.mjs'; import { effectiveAgentLlmConfig } from './config.mjs'; @@ -41,19 +39,15 @@ import { runProcess } from './process.mjs'; import { isIsolatedRunnerSuite } from './reporting.mjs'; export function requiredAgentIdsForSuite() { - return isUserInputRuntimeSuite() - ? [projectSupervisorAgentId] - : isSupervisorAutonomousPlayableLaneDefenseSuite() - ? [projectSupervisorAgentId] - : isSupervisorSwarmSuite() - ? [ - projectSupervisorAgentId, - supervisorSwarmDesignAgentId, - supervisorSwarmQualityAgentId, - ] - : isIsolatedRunnerSuite() - ? [mainAgentId] - : [mainAgentId, 'quality-review']; + return isSupervisorSwarmSuite() + ? [ + projectSupervisorAgentId, + supervisorSwarmDesignAgentId, + supervisorSwarmQualityAgentId, + ] + : isIsolatedRunnerSuite() + ? [mainAgentId] + : [mainAgentId, 'quality-review']; } export function expectedProviderBindingForSuite(config) { @@ -120,11 +114,9 @@ export async function checkPrerequisites(config) { return { llmConfigured, providerBinding, - chromeAvailable: - !isIsolatedRunnerSuite() || - isSupervisorAutonomousPlayableLaneDefenseSuite() - ? Boolean(await findSupportedBrowser()) - : false, + chromeAvailable: !isIsolatedRunnerSuite() + ? Boolean(await findSupportedBrowser()) + : false, editorApiConfigured, }; } @@ -267,8 +259,7 @@ export async function seedDisposableProject({ ? supervisorSwarmVerificationFixtureSource() : isGoalRuntimeSuite() || isResponseStreamSuite() || - isWebSearchSuite() || - isSupervisorAutonomousPlayableLaneDefenseSuite() + isWebSearchSuite() ? goalRevisionOneVerificationFixtureSource() : goalRevisionTwoVerificationFixtureSource(), ), diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/harness/reporting.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/harness/reporting.mjs index a176cf335..c59503ec2 100644 --- a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/harness/reporting.mjs +++ b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/harness/reporting.mjs @@ -20,9 +20,7 @@ import { isProjectSkillSuite } from '../suites/project-skill.mjs'; import { isResponseStreamSuite } from '../suites/response-stream.mjs'; import { isScopedAgentsSuite } from '../suites/scoped-agents.mjs'; import { isSteerRunnerKillSuite } from '../suites/steer-runner-kill.mjs'; -import { isSupervisorAutonomousPlayableLaneDefenseSuite } from '../suites/supervisor-autonomous-playable.mjs'; import { isSupervisorSwarmSuite } from '../suites/supervisor-swarm.mjs'; -import { isUserInputRuntimeSuite } from '../suites/user-input.mjs'; import { isWebSearchSuite } from '../suites/web-search.mjs'; import { isPathInside, readJson } from './io.mjs'; @@ -88,17 +86,12 @@ export function buildSummary() { config: state.config, blocked: state.blocked, run: { - agentId: - isUserInputRuntimeSuite() || - isSupervisorAutonomousPlayableLaneDefenseSuite() || - isSupervisorSwarmSuite() - ? projectSupervisorAgentId - : mainAgentId, + agentId: isSupervisorSwarmSuite() + ? projectSupervisorAgentId + : mainAgentId, runIdHash: hashValue(state.initialRunId), sessionIdHash: hashValue(state.initialSessionId), - runnerKilled: isSupervisorAutonomousPlayableLaneDefenseSuite() - ? false - : state.runnerKilled, + runnerKilled: state.runnerKilled, resumed: state.resumed, identityStable: state.identityStable, }, @@ -110,11 +103,9 @@ export function buildSummary() { projectPathReportLeakCount: state.projectPathReportLeakCount, ...(isWebSearchSuite() || isContextCompactionSuite() || - isUserInputRuntimeSuite() || isScopedAgentsSuite() || isProjectSkillSuite() || isParallelReadSuite() || - isSupervisorAutonomousPlayableLaneDefenseSuite() || isSupervisorSwarmSuite() || isSteerRunnerKillSuite() ? { @@ -408,11 +399,9 @@ export function isIsolatedRunnerSuite() { isResponseStreamSuite() || isWebSearchSuite() || isContextCompactionSuite() || - isUserInputRuntimeSuite() || isScopedAgentsSuite() || isProjectSkillSuite() || isParallelReadSuite() || - isSupervisorAutonomousPlayableLaneDefenseSuite() || isSupervisorSwarmSuite() ); } diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/harness/runtime.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/harness/runtime.mjs index f6f6ae0a4..d2f2cd567 100644 --- a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/harness/runtime.mjs +++ b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/harness/runtime.mjs @@ -87,11 +87,7 @@ import { import { isProjectSkillSuite } from '../suites/project-skill.mjs'; import { isScopedAgentsSuite } from '../suites/scoped-agents.mjs'; import { isSteerRunnerKillSuite } from '../suites/steer-runner-kill.mjs'; -import { - confirmSupervisorSwarmPendingActionsInChat, - isSupervisorSwarmInteractiveChatSuite, - isSupervisorSwarmSuite, -} from '../suites/supervisor-swarm.mjs'; +import { isSupervisorSwarmSuite } from '../suites/supervisor-swarm.mjs'; import { killRunnerPidOnce, verifyOwnedRunnerForKill } from './app-data.mjs'; import { isPlainObject } from './config.mjs'; import { @@ -1038,13 +1034,6 @@ export async function confirmPendingActions( allowedTools = null, shouldConfirm = () => true, ) { - if (isSupervisorSwarmInteractiveChatSuite()) { - await confirmSupervisorSwarmPendingActionsInChat( - allowedTools, - shouldConfirm, - ); - return; - } for (const pending of await findPendingActions()) { if (state.confirmedActionIds.has(pending.actionId)) continue; if (!shouldConfirm(pending)) continue; diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/runtime-state.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/runtime-state.mjs index c27fa5aea..a542f9e8a 100644 --- a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/runtime-state.mjs +++ b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/runtime-state.mjs @@ -43,12 +43,6 @@ export const contextCompactionAppDataSentinelFileName = export const contextCompactionAppDataSentinelSchema = 'genarrative-agent-runtime-real-e2e-context-compaction-appdata.v1'; -export const userInputAppDataSentinelFileName = - '.agent-runtime-real-e2e-user-input-appdata.json'; - -export const userInputAppDataSentinelSchema = - 'genarrative-agent-runtime-real-e2e-user-input-appdata.v1'; - export const scopedAgentsAppDataSentinelFileName = '.agent-runtime-real-e2e-scoped-agents-appdata.json'; @@ -109,30 +103,6 @@ export const supervisorSwarmToolPlanHandoffCheckpointReachedFileName = export const supervisorSwarmToolPlanHandoffCheckpointReachedSchema = 'game-creator-tool-plan-handoff-runner-kill-checkpoint-reached.v1'; -export const supervisorSwarmAutonomousChatAppDataSentinelFileName = - '.agent-runtime-real-e2e-supervisor-swarm-autonomous-chat-appdata.json'; - -export const supervisorSwarmAutonomousChatAppDataSentinelSchema = - 'genarrative-agent-runtime-real-e2e-supervisor-swarm-autonomous-chat-appdata.v1'; - -export const supervisorAutonomousPlayableAppDataSentinelFileName = - '.agent-runtime-real-e2e-supervisor-autonomous-playable-appdata.json'; - -export const supervisorAutonomousPlayableAppDataSentinelSchema = - 'genarrative-agent-runtime-real-e2e-supervisor-autonomous-playable-appdata.v1'; - -export const supervisorSwarmStaticIsolatedAutonomousChatAppDataSentinelFileName = - '.agent-runtime-real-e2e-supervisor-swarm-static-isolated-autonomous-chat-appdata.json'; - -export const supervisorSwarmStaticIsolatedAutonomousChatAppDataSentinelSchema = - 'genarrative-agent-runtime-real-e2e-supervisor-swarm-static-isolated-autonomous-chat-appdata.v1'; - -export const supervisorSwarmCollaborationPolicyAppDataSentinelFileName = - '.agent-runtime-real-e2e-supervisor-swarm-collaboration-policy-appdata.json'; - -export const supervisorSwarmCollaborationPolicyAppDataSentinelSchema = - 'genarrative-agent-runtime-real-e2e-supervisor-swarm-collaboration-policy-appdata.v1'; - export const mainAgentId = 'code-prototype'; export const projectSupervisorAgentId = 'project-supervisor'; @@ -179,8 +149,6 @@ export const webSearchSuite = 'web-search'; export const contextCompactionSuite = 'context-compaction'; -export const userInputRuntimeSuite = 'user-input-runtime'; - export const scopedAgentsSuite = 'scoped-agents'; export const projectSkillSuite = 'project-skill'; @@ -213,18 +181,6 @@ export const supervisorSwarmTransientRetryBackoffMs = 30_000; export const supervisorSwarmTransientRetryPreDueGuardMs = 1_500; -export const supervisorSwarmAutonomousChatSuite = - 'supervisor-swarm-autonomous-chat'; - -export const supervisorAutonomousPlayableLaneDefenseSuite = - 'supervisor-autonomous-playable-lane-defense'; - -export const supervisorSwarmStaticIsolatedAutonomousChatSuite = - 'supervisor-swarm-static-isolated-autonomous-chat'; - -export const supervisorSwarmCollaborationPolicyMixedRecoverySuite = - 'supervisor-swarm-collaboration-policy-mixed-recovery'; - export const runtimeContextBundleSchemaVersion = 'game-creator-runtime-context-bundle.v5'; @@ -287,12 +243,6 @@ export const contextCompactionTriggerTurns = new Set([4, 8]); export const contextCompactionConstraintCanary = `GENARRATIVE_CONTEXT_CONSTRAINT_${randomUUID().replaceAll('-', '').slice(0, 20)}`; -export const userInputAnswerCanary = `GENARRATIVE_USER_CHOICE_${randomUUID() - .replaceAll('-', '') - .slice(0, 20)}`; - -export const userInputAnswerText = `选择轻量像素风,优先保证移动端轮廓和动作可读性;确认标记 ${userInputAnswerCanary}`; - export const scopedAgentsRootCanary = `GENARRATIVE_SCOPE_ROOT_${randomUUID() .replaceAll('-', '') .slice(0, 20)}`; @@ -461,143 +411,6 @@ export const supervisorSwarmConfirmedTools = [ 'project.verify', ]; -export const supervisorSwarmAutonomousTask = - '请把这个试玩项目推进到可以交给首批玩家体验的状态。以仓库现有的正式交付要求、临时检查要求和实际验证结果为准;按阶段生效的临时检查必须在各自生效后全部完成,不能把后续检查并入前置检查或漏掉。完成后简短说明交付内容、验证结论和仍需关注的问题。'; - -export const supervisorAutonomousPlayableLaneDefenseTask = - '做一个植物大战僵尸式的塔防游戏,要能选择植物、阻挡敌人、正常闯关,并且生成后可以直接试玩。'; - -export const supervisorAutonomousPlayableSourceFieldMaxChars = 8_000; - -export const supervisorAutonomousPlayableSourceTotalMaxChars = 10_000; - -export const supervisorAutonomousPlayableSafeStatuses = new Set([ - 'cancelling', - 'budget-exhausted', - 'cancelled', - 'completed', - 'failed', - 'idle', - 'needs-reconciliation', - 'paused', - 'pausing', - 'pending', - 'running', - 'waiting-for-confirmation', - 'waiting-for-user-input', -]); - -export const supervisorAutonomousPlayableSafePhases = new Set([ - 'action', - 'budget-exhausted', - 'cancelled', - 'cancelling', - 'completion-contract-failed', - 'completed', - 'conversation-write-failed', - 'executing', - 'failed', - 'finalizing', - 'idle', - 'needs-reconciliation', - 'observation', - 'parent-terminal', - 'paused', - 'pausing', - 'planning', - 'provider-action-batch', - 'queued', - 'response', - 'running', - 'waiting-for-agent', - 'waiting-for-confirmation', - 'waiting-for-delegate-receipts', - 'waiting-for-isolated-join', - 'waiting-for-process-session', - 'waiting-for-provider-retry', - 'waiting-for-user-input', -]); - -export const supervisorAutonomousPlayableSafeDeliveryStatuses = new Set([ - 'claimed-by-parent', - 'dispatched', - 'ready', - 'suppressed', -]); - -export const supervisorAutonomousPlayableSafeTerminalStatuses = new Set([ - 'budget-exhausted', - 'cancelled', - 'completed', - 'failed', -]); - -export const supervisorAutonomousPlayableSafeTurnOutcomes = new Set([ - 'failed', - 'incomplete', - 'needs-reconciliation', - 'settled', -]); - -export const laneDefensePlaytestRequiredAssertions = [ - 'state-surface-valid', - 'level-positive', - 'start-control-clicked', - 'start-sequence-advanced', - 'start-phase-playing', - 'defender-option-control-clicked', - 'defender-selection-sequence-advanced', - 'defender-selection-recorded', - 'lane-cell-control-clicked', - 'defender-placement-sequence-advanced', - 'defender-count-increased', - 'enemies-present-after-placement', - 'speed-up-control-clicked', - 'battle-sequence-advanced', - 'battle-sequence-monotonic', - 'enemy-position-changed', - 'enemy-health-decreased', - 'phase-won', - 'next-level-control-clicked', - 'next-level-sequence-advanced', - 'level-increased', - 'restart-control-clicked', - 'restart-sequence-advanced', - 'restart-phase-ready-or-playing', -]; - -export const supervisorSwarmAutonomousRoutingTerms = [ - supervisorSwarmDesignAgentId, - supervisorSwarmQualityAgentId, - 'agent.delegate', - 'agent.run_status', - '同一个 planning 轮次', - '同轮', - '并行', - '返工', - 'repair', - '一次', - '两个 Agent', - '两个专业', - 'Runner', - 'pidfd', - 'actionId', - 'delegationId', -]; - -export const supervisorSwarmCollaborationPolicyControlTerms = [ - '.agent/collaboration-policy.json', - supervisorCollaborationPolicySchemaVersion, - 'requiredInitialWave', - 'minStaticDelegates', - 'requiredStaticAgentIds', - 'minIsolatedChildren', - 'minIsolatedGroupsBeforeClaim', - 'orchestratorOnlyAfterDelegation', - 'policyFingerprint', - 'contractFingerprint', -]; - export const supervisorSwarmIsolatedReviews = [ { path: 'e2e/isolated-a/evidence.txt', @@ -625,17 +438,6 @@ export const supervisorSwarmIsolatedReviews = [ }, ]; -export const supervisorSwarmIsolatedReviewGroups = [ - supervisorSwarmIsolatedReviews.slice(0, 2), - supervisorSwarmIsolatedReviews.slice(2), -]; - -export const supervisorSwarmInitialIsolatedReviews = - supervisorSwarmIsolatedReviewGroups[0]; - -export const supervisorSwarmFollowupIsolatedReviews = - supervisorSwarmIsolatedReviewGroups[1]; - export const webSearchBaselineApiUrl = 'https://api.github.com/repos/nodejs/node/releases/latest'; @@ -703,8 +505,6 @@ export const pollIntervalMs = 750; export const runTimeoutMs = 30 * 60 * 1000; -export const supervisorAutonomousPlayableRunTimeoutMs = 60 * 60 * 1000; - export const supervisorSwarmTerminalSidecarCleanupTimeoutMs = 10_000; export const processRunnerKillStartTimeoutMs = 5 * 60 * 1000; @@ -938,10 +738,6 @@ export class BlockedError extends Error { export const isolatedRunnerState = { appDataDir: null, - platformSessionFixturePath: null, - platformSessionFixtureSourcePath: null, - platformSessionFixtureSha256: null, - platformSessionFixturePreviousEnv: undefined, ownerToken: null, createdAt: 0, current: null, @@ -968,9 +764,6 @@ export const state = { linuxPidfdPythonPath: null, windowsProcessHandlePowerShellPath: null, cleanupInProgress: false, - userInputCliSession: null, - supervisorSwarmCliSession: null, - supervisorAutonomousPlayableCliSession: null, status: 'FAIL', suite: null, options: null, @@ -1082,23 +875,6 @@ export const state = { finalReplyFingerprint: null, reportLeakCount: 0, }, - userInput: { - requestId: null, - responseId: null, - actionId: null, - questionMessageId: null, - answerMessageId: null, - questionCount: 0, - optionCount: 0, - providerStartedBeforeKill: 0, - providerStartedAfterRestart: 0, - conversationCountBeforeKill: 0, - conversationCountAfterRestart: 0, - oldRunnerBootId: null, - newRunnerBootId: null, - privateValues: [], - reportLeakCount: 0, - }, scopedAgents: { effectiveModel: null, effectiveApiKind: null, @@ -1120,20 +896,6 @@ export const state = { privateValues: [], reportLeakCount: 0, }, - supervisorAutonomousPlayable: { - freshInitBaselineUsed: false, - initialGameIndexSha256: null, - expectedProviderBinding: null, - effectiveProviderBinding: null, - stdinWriteCount: 0, - stdinEnded: false, - stdinBytes: 0, - turnReport: null, - cliOutput: '', - privateValues: [], - reportLeakCount: 0, - ownedProcessCleanupSnapshot: null, - }, supervisorSwarm: { effectiveModel: null, effectiveApiKind: null, @@ -1215,47 +977,7 @@ export const state = { toolPlanHandoffLifecycleClosedExactlyOnce: false, toolPlanHandoffAuditIdempotent: false, toolPlanHandoffRecoveredPlanFingerprintMatched: false, - autonomousTaskRecipeFree: false, - autonomousRepositoryRecipeFree: false, - interactiveCliUsed: false, - chatSessionUnexpectedlyClosed: false, - chatSessionFailureKind: 'none', - chatSessionExitCode: 'none', - chatSessionCloseSignal: 'none', - chatSessionProcessErrorCode: 'none', - chatSessionStderrChars: 0, - chatSessionStderrSha256: 'none', - turnReport: null, - mixedSpawnActionId: null, - mixedSpawnRequestHash: null, - mixedFollowupSpawnActionId: null, - mixedFollowupSpawnRequestHash: null, - staticIsolatedProviderRequestIds: [], - staticIsolatedProviderOverlapObserved: false, - preKillMixedIdentity: null, collaborationPolicyWritten: false, - collaborationPolicySnapshotInitialRecord: null, - collaborationPolicySnapshotInitialBytes: null, - collaborationPolicySnapshotInitialBytesSha256: null, - collaborationPolicySnapshotInitialIdentityHash: null, - collaborationPolicySnapshotBindingInitialRecord: null, - collaborationPolicySnapshotBindingInitialBytes: null, - collaborationPolicySnapshotBindingInitialBytesSha256: null, - chatSessionFailureDiagnostic: null, - collaborationPolicyDriftFixtureWritten: false, - collaborationPolicyDriftFixturePolicyFingerprint: null, - collaborationPolicyDriftedMinIsolatedGroupsBeforeClaim: null, - initialBatchRecoveryBoundaryObserved: false, - initialBatchRecoveryOldRunnerBootId: null, - initialBatchRecoveryNewRunnerBootId: null, - initialBatchRecoveryPreKillIdentity: null, - initialBatchRecoveryPostRecoveryIdentity: null, - initialBatchRecoveryPreKillSideEffects: null, - initialBatchRecoveryPostRecoverySideEffects: null, - initialBatchRecoveryIdentityStable: false, - initialBatchRecoveryConfirmationRestoredCount: 0, - initialBatchRecoveryPidfdClaimCount: 0, - initialBatchRecoveryPidfdSignalCount: 0, }, confirmedActionIds: new Set(), cleanupPerformed: false, diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/self-test.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/self-test.mjs index b3f83ea49..f427b7b71 100644 --- a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/self-test.mjs +++ b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/self-test.mjs @@ -8,10 +8,8 @@ import { collectToolPlanRepairAuditEvidence, countExactSecrets, emptyToolPlanRepairCountsByProtocolErrorKind, - finalMessageId, hasSafeToolPlanAuditPayload, isNonEmptyString, - runtimePublicStatusMessageId, sumObjectValues, toolPlanRepairEvidenceHasNoFatalLocalRepair, } from '../assertions/runtime.mjs'; @@ -22,68 +20,37 @@ import { ensureOwnedRunnerStableKillSupport, isolatedSuiteAppDataProfile, isolatedSuiteProtectsSourceAppData, - isolatedSuiteUsesSiblingAppData, isProcessAlive, openOwnedRunnerKillHandle, - sameSupervisorPlayableProviderBinding, signalOwnedRunnerKillHandle, sourceAppDataDirectoryEventIsViolation, waitForChildClose, } from '../harness/app-data.mjs'; import { parseArguments } from '../harness/config.mjs'; -import { - buildOwnedProcessCleanupSnapshot, - closeInteractiveCli, - createInteractiveCliSession, - destroyInteractiveCliOutputStreams, - inspectOwnedProcessCleanupResiduals, - listSystemProcessIdentities, - safeProcessFailureDiagnostic, - waitForInteractiveCliExit, - waitForInteractiveCliOutput, - waitForInteractiveCliStdioClose, -} from '../harness/process.mjs'; -import { - expectedProviderBindingForSuite, - seededGameHtml, -} from '../harness/project.mjs'; +import { seededGameHtml } from '../harness/project.mjs'; import { buildSummary, isIsolatedRunnerSuite, providerUsedFromEvidence, } from '../harness/reporting.mjs'; -import { - agentConversationPath, - isEmptyExecutionOwnerLock, -} from '../harness/runtime.mjs'; +import { isEmptyExecutionOwnerLock } from '../harness/runtime.mjs'; import { activeCommandChildren, appRoot, commandFailureMarker, commandPassedMarker, - isolatedAgentJoinClaimSchemaVersion, projectSupervisorAgentId, providerActionBatchSchemaVersion, repoRoot, runnerEndpointFileName, state, StreamingSecretScanner, - supervisorAutonomousPlayableAppDataSentinelFileName, - supervisorAutonomousPlayableAppDataSentinelSchema, - supervisorAutonomousPlayableLaneDefenseSuite, - supervisorAutonomousPlayableLaneDefenseTask, supervisorCollaborationContractSchemaVersion, supervisorCollaborationPolicySchemaVersion, supervisorCollaborationPolicySnapshotInitialBatchBinding, supervisorCollaborationPolicySnapshotSchemaVersion, - supervisorSwarmAutonomousChatSuite, - supervisorSwarmCollaborationPolicyMixedRecoverySuite, - supervisorSwarmFollowupIsolatedReviews, - supervisorSwarmInitialIsolatedReviews, - supervisorSwarmIsolatedReviewGroups, supervisorSwarmIsolatedReviews, supervisorSwarmSessionId, - supervisorSwarmStaticIsolatedAutonomousChatSuite, supervisorSwarmToolPlanHandoffRunnerKillAppDataSentinelFileName, supervisorSwarmToolPlanHandoffRunnerKillAppDataSentinelSchema, supervisorSwarmToolPlanHandoffRunnerKillSuite, @@ -92,19 +59,7 @@ import { toolPlanRepairAuditSafeFields, } from '../runtime-state.mjs'; import { - buildSupervisorAutonomousPlayablePartialEvidence, - buildSupervisorAutonomousPlayableStdin, - collectPartialSupervisorAutonomousPlayableEvidence, - inspectSupervisorAutonomousPlayableConversationBoundary, - isSupervisorAutonomousPlayableAcceptedPublicStatus, - isSupervisorAutonomousPlayableLaneDefenseSuite, - supervisorAutonomousPlayableStaticSmokeMatchesFinalIndex, - waitForSupervisorAutonomousPlayableCliExit, -} from './supervisor-autonomous-playable.mjs'; -import { - buildSupervisorSwarmEvidence, collectSupervisorSwarmDynamicPrivateValues, - driftedSupervisorSwarmCollaborationPolicy, duplicateSupervisorSwarmCollaborationPolicySnapshotBindingCount, duplicateSupervisorSwarmCollaborationPolicySnapshotCount, expectedSupervisorSwarmCollaborationPolicy, @@ -120,18 +75,11 @@ import { supervisorSwarmCollaborationPolicyDriftObservationCount, supervisorSwarmCollaborationPolicySnapshotBinding, supervisorSwarmEvidenceFieldTemplate, - supervisorSwarmExpectedIsolatedReviewGroups, supervisorSwarmFinalReplyFaultInjectionAllowed, supervisorSwarmFinalReplyFaultPrerequisites, - supervisorSwarmFollowupBeforeClaimOrderValid, supervisorSwarmHostVerificationPassed, supervisorSwarmInitialBatchBindsPolicySnapshot, - supervisorSwarmIsolatedReviewGroupIndex, - supervisorSwarmIsolatedWriteScopeRoots, - supervisorSwarmMixedIsolatedGroupEntries, - supervisorSwarmObservedJoinClaimForGroups, supervisorSwarmProfessionalSessions, - supervisorSwarmReadyIsolatedGroupIds, supervisorSwarmResidualSidecarsEmpty, supervisorSwarmSeedPrivateValues, supervisorSwarmToolPlanHandoffProxyNeedsCleanup, @@ -141,33 +89,12 @@ import { } from './supervisor-swarm.mjs'; export async function runAgentRuntimeRealE2eSelfTests() { - const supervisorAcceptedPublicStatusLifecycle = - validateSupervisorAcceptedPublicStatusSelfTest(); - const interactiveCliLifecycle = - await validateInteractiveCliInheritedStdioLifecycle(); - const providerBindingLifecycle = - validateSupervisorPlayableProviderBindingSelfTest(); const providerUsedReportingLifecycle = validateProviderUsedReportingSelfTest(); - const ownedProcessCleanupLifecycle = - validateOwnedProcessCleanupIdentitySelfTest(); - const liveProcessIdentities = await listSystemProcessIdentities(); - assert( - liveProcessIdentities.some( - (identity) => - identity.pid === process.pid && - Number.isSafeInteger(identity.parentPid) && - isNonEmptyString(identity.startedAt) && - isNonEmptyString(identity.name), - ), - 'agent-runtime-real-e2e-self-test-live-process-identity-missing', - ); const professionalSessionLifecycle = validateSupervisorSwarmProfessionalSessionsSelfTest(); const executionOwnerLockScanLifecycle = validateExecutionOwnerLockScanSelfTest(); - const staticSmokeBindingLifecycle = - validateStaticSmokeFinalIndexBindingSelfTest(); const seededGameHtmlLifecycle = validateSeededGameHtmlContractSelfTest(); const lateRegisteredScanner = new StreamingSecretScanner([ 'initial-scanner-value', @@ -302,345 +229,6 @@ export async function runAgentRuntimeRealE2eSelfTests() { const shellPackage = JSON.parse( readFileSync(path.join(appRoot, 'package.json'), 'utf8'), ); - const previousSuiteForAutonomousPlayable = state.suite; - state.suite = supervisorAutonomousPlayableLaneDefenseSuite; - const autonomousPlayableProfile = isolatedSuiteAppDataProfile(); - const autonomousPlayableParsedArguments = parseArguments([ - '--config-dir', - path.resolve('synthetic-autonomous-playable-config'), - '--suite', - supervisorAutonomousPlayableLaneDefenseSuite, - ]); - const autonomousPlayableStdin = buildSupervisorAutonomousPlayableStdin(); - const autonomousPlayablePackageCommandsRegistered = - shellPackage.scripts?.[ - 'agent-runtime:supervisor-autonomous-playable-lane-defense-real-e2e' - ] === - 'node scripts/agent-runtime-real-e2e.mjs --suite supervisor-autonomous-playable-lane-defense' && - rootPackage.scripts?.[ - 'ai-game-creator-shell:agent-runtime:supervisor-autonomous-playable-lane-defense-real-e2e' - ] === - 'npm --prefix apps/ai-game-creator-shell run agent-runtime:supervisor-autonomous-playable-lane-defense-real-e2e --'; - const autonomousPlayableSuiteRegistered = - autonomousPlayableParsedArguments.suite === - supervisorAutonomousPlayableLaneDefenseSuite && - isSupervisorAutonomousPlayableLaneDefenseSuite() && - !isSupervisorSwarmSuite() && - isIsolatedRunnerSuite() && - isolatedSuiteProtectsSourceAppData() && - isolatedSuiteUsesSiblingAppData() && - autonomousPlayableProfile.sentinelName === - supervisorAutonomousPlayableAppDataSentinelFileName && - autonomousPlayableProfile.sentinelSchema === - supervisorAutonomousPlayableAppDataSentinelSchema && - autonomousPlayableStdin.equals( - Buffer.from(`${supervisorAutonomousPlayableLaneDefenseTask}\n`, 'utf8'), - ) && - autonomousPlayablePackageCommandsRegistered; - const previousInitialRunIdForAutonomousPlayable = state.initialRunId; - const previousProjectRootForAutonomousPlayable = state.projectRoot; - const previousInitialTaskForAutonomousPlayable = state.initialTask; - const previousAutonomousPlayableTurnReport = - state.supervisorAutonomousPlayable.turnReport; - const previousAutonomousPlayablePrivateValues = [ - ...state.supervisorAutonomousPlayable.privateValues, - ]; - const previousSupervisorSwarmPrivateValues = [ - ...state.supervisorSwarm.privateValues, - ]; - const previousSupervisorSwarmInitialProviderBatch = - state.supervisorSwarm.initialProviderBatch; - const autonomousPlayablePartialCanary = 'private-autonomous-partial-canary'; - const syntheticAutonomousPlayableRunId = - 'synthetic-autonomous-playable-partial-root'; - state.initialRunId = syntheticAutonomousPlayableRunId; - state.supervisorAutonomousPlayable.privateValues = [ - ...new Set([ - ...state.supervisorAutonomousPlayable.privateValues, - autonomousPlayablePartialCanary, - ]), - ]; - const syntheticAutonomousPlayablePartialEvidence = - buildSupervisorAutonomousPlayablePartialEvidence( - { - taskSnapshot: { - latest: [ - { - agentId: projectSupervisorAgentId, - runId: syntheticAutonomousPlayableRunId, - status: 'running', - phase: 'waiting-for-agent', - task: autonomousPlayablePartialCanary, - }, - { - agentId: 'code-prototype', - sessionId: 'synthetic-original-session', - runId: 'synthetic-original-run', - parentAgentId: projectSupervisorAgentId, - parentRunId: syntheticAutonomousPlayableRunId, - delegationId: 'synthetic-original-delivery', - status: 'failed', - phase: 'failed', - }, - { - agentId: 'code-prototype', - sessionId: 'synthetic-repair-session', - runId: 'synthetic-repair-run', - parentAgentId: projectSupervisorAgentId, - parentRunId: syntheticAutonomousPlayableRunId, - delegationId: 'synthetic-repair-delivery', - status: 'running', - phase: 'planning', - }, - ], - }, - runtimeStates: [ - { - agentId: projectSupervisorAgentId, - runId: syntheticAutonomousPlayableRunId, - status: 'running', - phase: 'waiting-for-agent', - currentTask: autonomousPlayablePartialCanary, - }, - { - agentId: 'code-prototype', - runId: 'synthetic-original-run', - parentAgentId: projectSupervisorAgentId, - parentRunId: syntheticAutonomousPlayableRunId, - status: 'failed', - phase: 'failed', - }, - { - agentId: 'code-prototype', - runId: 'synthetic-repair-run', - parentAgentId: projectSupervisorAgentId, - parentRunId: syntheticAutonomousPlayableRunId, - status: 'running', - phase: 'waiting-for-provider-retry', - }, - ], - deliveries: [ - { - parentAgentId: projectSupervisorAgentId, - parentRunId: syntheticAutonomousPlayableRunId, - delegationId: 'synthetic-original-delivery', - repairOfDelegationId: null, - targetAgentId: 'code-prototype', - targetSessionId: 'synthetic-original-session', - targetRunId: 'synthetic-original-run', - status: 'ready', - terminalStatus: 'failed', - structuredResult: { contractStatus: 'needs-repair' }, - }, - { - parentAgentId: projectSupervisorAgentId, - parentRunId: syntheticAutonomousPlayableRunId, - delegationId: 'synthetic-repair-delivery', - repairOfDelegationId: 'synthetic-original-delivery', - targetAgentId: 'code-prototype', - targetSessionId: 'synthetic-repair-session', - targetRunId: 'synthetic-repair-run', - status: 'dispatched', - terminalStatus: null, - }, - ], - agentDb: [ - { - recordType: 'agent.runtime.provider_request.lifecycle', - agentId: 'code-prototype', - runId: 'synthetic-original-run', - requestId: 'synthetic-provider-request', - status: 'started', - }, - { - recordType: 'agent.runtime.provider_request.lifecycle', - agentId: 'code-prototype', - runId: 'synthetic-original-run', - requestId: 'synthetic-provider-request', - status: 'failed', - }, - { - recordType: 'agent.runtime.provider_request.retry', - agentId: 'code-prototype', - runId: 'synthetic-original-run', - errorKind: 'transport', - }, - ], - supervisorConversation: [ - { - role: 'user', - content: autonomousPlayablePartialCanary, - }, - ], - professionalConversations: [], - failureEvidenceErrors: {}, - }, - { - residualSidecars: { providerRetries: 1 }, - pendingActionCount: 0, - turnReport: { - outcome: 'incomplete', - parentAgentId: projectSupervisorAgentId, - sessionId: supervisorSwarmSessionId, - parentRunId: syntheticAutonomousPlayableRunId, - runtimeCount: 3, - busyRuntimeCount: 2, - pendingTaskCount: 0, - runningTaskCount: 2, - waitingForConfirmationCount: 0, - waitingForUserInputCount: 0, - reconciliationAgentCount: 0, - }, - }, - ); - const autonomousPlayablePartialEvidenceValidated = - syntheticAutonomousPlayablePartialEvidence.evidenceCompleteness === - 'partial' && - syntheticAutonomousPlayablePartialEvidence.partialEvidenceCollected === - true && - syntheticAutonomousPlayablePartialEvidence.partialPrivacyScanComplete === - false && - syntheticAutonomousPlayablePartialEvidence.rootRunObserved === true && - syntheticAutonomousPlayablePartialEvidence.parentRuntimePhase === - 'waiting-for-agent' && - syntheticAutonomousPlayablePartialEvidence.childTaskStatusCounts.failed === - 1 && - syntheticAutonomousPlayablePartialEvidence.childTaskStatusCounts.running === - 1 && - syntheticAutonomousPlayablePartialEvidence.failedOriginalChildCount === 1 && - syntheticAutonomousPlayablePartialEvidence.failedRepairChildCount === 0 && - syntheticAutonomousPlayablePartialEvidence.awaitingRepairCount === 1 && - syntheticAutonomousPlayablePartialEvidence.repairDeliveryStatusCounts - .dispatched === 1 && - syntheticAutonomousPlayablePartialEvidence.providerLifecycleFailedCount === - 1 && - syntheticAutonomousPlayablePartialEvidence.providerRetryAuditCount === 1 && - syntheticAutonomousPlayablePartialEvidence.providerTransportRetryAuditCount === - 1 && - syntheticAutonomousPlayablePartialEvidence.providerRetrySidecarCount === - 1 && - syntheticAutonomousPlayablePartialEvidence.turnReportOutcome === - 'incomplete' && - syntheticAutonomousPlayablePartialEvidence.turnReportCaptured === true && - syntheticAutonomousPlayablePartialEvidence.turnReportParentIdentityStable === - true && - syntheticAutonomousPlayablePartialEvidence.turnReportPrivateLeakCount === - 0 && - !JSON.stringify(syntheticAutonomousPlayablePartialEvidence).includes( - autonomousPlayablePartialCanary, - ); - const syntheticAutonomousPlayableCollectorRoot = await fs.mkdtemp( - path.join(os.tmpdir(), 'genarrative-autonomous-partial-self-test-'), - ); - let syntheticAutonomousPlayableCollectorEvidence; - try { - state.projectRoot = syntheticAutonomousPlayableCollectorRoot; - state.initialTask = { - bytes: Buffer.byteLength(autonomousPlayablePartialCanary, 'utf8'), - sha256: hashValue(autonomousPlayablePartialCanary), - }; - state.supervisorSwarm.privateValues = [autonomousPlayablePartialCanary]; - state.supervisorSwarm.initialProviderBatch = null; - state.supervisorAutonomousPlayable.turnReport = { - outcome: 'needs-reconciliation', - parentAgentId: projectSupervisorAgentId, - sessionId: supervisorSwarmSessionId, - parentRunId: syntheticAutonomousPlayableRunId, - runtimeCount: 1, - busyRuntimeCount: 0, - pendingTaskCount: 0, - runningTaskCount: 0, - waitingForConfirmationCount: 0, - waitingForUserInputCount: 0, - reconciliationAgentCount: 1, - privateDiagnostic: autonomousPlayablePartialCanary, - }; - const taskPath = path.join( - state.projectRoot, - '.agent/runtime/tasks/project-supervisor.jsonl', - ); - const runtimePath = path.join( - state.projectRoot, - '.agent/runtime/agents/project-supervisor.json', - ); - const conversationPath = agentConversationPath( - projectSupervisorAgentId, - supervisorSwarmSessionId, - ); - await fs.mkdir(path.dirname(taskPath), { recursive: true }); - await fs.mkdir(path.dirname(runtimePath), { recursive: true }); - await fs.mkdir(path.dirname(conversationPath), { recursive: true }); - await fs.writeFile( - taskPath, - `${JSON.stringify({ - agentId: projectSupervisorAgentId, - runId: syntheticAutonomousPlayableRunId, - status: 'budget-exhausted', - phase: 'budget-exhausted', - task: autonomousPlayablePartialCanary, - })}\n`, - ); - await fs.writeFile( - runtimePath, - JSON.stringify({ - agentId: projectSupervisorAgentId, - runId: syntheticAutonomousPlayableRunId, - status: 'needs-reconciliation', - phase: 'needs-reconciliation', - currentTask: autonomousPlayablePartialCanary, - }), - ); - await fs.writeFile( - conversationPath, - `${JSON.stringify({ - role: 'user', - content: autonomousPlayablePartialCanary, - })}\n`, - ); - syntheticAutonomousPlayableCollectorEvidence = - await collectPartialSupervisorAutonomousPlayableEvidence(); - } finally { - state.projectRoot = previousProjectRootForAutonomousPlayable; - state.initialTask = previousInitialTaskForAutonomousPlayable; - state.supervisorAutonomousPlayable.turnReport = - previousAutonomousPlayableTurnReport; - state.supervisorAutonomousPlayable.privateValues = - previousAutonomousPlayablePrivateValues; - state.supervisorSwarm.privateValues = previousSupervisorSwarmPrivateValues; - state.supervisorSwarm.initialProviderBatch = - previousSupervisorSwarmInitialProviderBatch; - await fs.rm(syntheticAutonomousPlayableCollectorRoot, { - recursive: true, - force: true, - }); - } - const autonomousPlayablePartialCollectorPrivacyValidated = - syntheticAutonomousPlayableCollectorEvidence.evidenceCompleteness === - 'partial' && - syntheticAutonomousPlayableCollectorEvidence.rootRunObserved === true && - syntheticAutonomousPlayableCollectorEvidence.parentTaskStatus === - 'budget-exhausted' && - syntheticAutonomousPlayableCollectorEvidence.parentTaskPhase === - 'budget-exhausted' && - syntheticAutonomousPlayableCollectorEvidence.parentRuntimeStatus === - 'needs-reconciliation' && - syntheticAutonomousPlayableCollectorEvidence.parentRuntimePhase === - 'needs-reconciliation' && - syntheticAutonomousPlayableCollectorEvidence.supervisorUserMessageCount === - 1 && - syntheticAutonomousPlayableCollectorEvidence.turnReportPrivateLeakCount === - 1 && - !JSON.stringify(syntheticAutonomousPlayableCollectorEvidence).includes( - autonomousPlayablePartialCanary, - ); - state.initialRunId = previousInitialRunIdForAutonomousPlayable; - state.suite = previousSuiteForAutonomousPlayable; - assert( - autonomousPlayableSuiteRegistered && - autonomousPlayablePartialEvidenceValidated && - autonomousPlayablePartialCollectorPrivacyValidated, - 'agent-runtime-real-e2e-self-test-autonomous-playable-suite-invalid', - ); const rootToolPlanHandoffCommand = 'npm --prefix apps/ai-game-creator-shell run agent-runtime:supervisor-swarm-tool-plan-handoff-runner-kill-real-e2e --'; const shellToolPlanHandoffCommand = @@ -1210,94 +798,15 @@ export async function runAgentRuntimeRealE2eSelfTests() { ), 'agent-runtime-real-e2e-self-test-generic-boundary-term-private', ); - const syntheticMixedGroups = [ - { - delegationGroupId: 'synthetic-followup-group', - request: { - children: supervisorSwarmFollowupIsolatedReviews.map((review) => ({ - expectedArtifacts: [review.path], - })), - }, - }, - { - delegationGroupId: 'synthetic-initial-group', - request: { - children: supervisorSwarmInitialIsolatedReviews.map((review) => ({ - expectedArtifacts: [review.path], - })), - }, - }, - ]; - const syntheticMixedEntries = supervisorSwarmMixedIsolatedGroupEntries( - syntheticMixedGroups, - supervisorSwarmIsolatedReviewGroups, - ); - const syntheticSingleGroupEntries = supervisorSwarmMixedIsolatedGroupEntries( - [ - { - delegationGroupId: 'synthetic-single-group', - request: { - children: supervisorSwarmIsolatedReviews.map((review) => ({ - expectedArtifacts: [review.path], - })), - }, - }, - ], - [supervisorSwarmIsolatedReviews], - ); - const syntheticReadyGroupIds = supervisorSwarmReadyIsolatedGroupIds( - `readyIsolatedJoins: ${JSON.stringify({ - ready: true, - joins: syntheticMixedEntries.map(({ group }) => ({ - delegationGroupId: group.delegationGroupId, - })), - })}\n\nagentId: ${projectSupervisorAgentId}`, - ); - const syntheticObservedJoinClaim = supervisorSwarmObservedJoinClaimForGroups( - [ - { - schemaVersion: isolatedAgentJoinClaimSchemaVersion, - parentAgentId: projectSupervisorAgentId, - parentRunId: 'synthetic-parent-run', - actionId: 'synthetic-claim-action', - status: 'observed', - joins: syntheticMixedEntries.map(({ group }) => ({ - delegationGroupId: group.delegationGroupId, - })), - }, - ], - syntheticReadyGroupIds, - ); - const syntheticWriteScopeRoots = supervisorSwarmIsolatedWriteScopeRoots( - supervisorSwarmIsolatedReviewGroups.flatMap((reviews) => - reviews.map((review) => ({ writeScopes: [review.scope] })), - ), - ); - let syntheticOverlappingWriteScopesRejected = false; - try { - supervisorSwarmIsolatedWriteScopeRoots([ - { writeScopes: ['e2e/**'] }, - { writeScopes: ['e2e/isolated-a/**'] }, - ]); - } catch (error) { - syntheticOverlappingWriteScopesRejected = - error?.code === 'supervisor-swarm-mixed-child-write-scopes-overlap'; - } - const syntheticFollowupOrderValidated = - supervisorSwarmFollowupBeforeClaimOrderValid(3, 5, [8, 9]) && - !supervisorSwarmFollowupBeforeClaimOrderValid(3, 8, [8, 9]); - const originalSuite = state.suite; const originalInitialRunId = state.initialRunId; - state.suite = supervisorSwarmStaticIsolatedAutonomousChatSuite; - const multiGroupPolicy = expectedSupervisorSwarmCollaborationPolicy(); - const multiGroupCount = supervisorSwarmExpectedIsolatedReviewGroups().length; + const collaborationPolicy = expectedSupervisorSwarmCollaborationPolicy(); const syntheticSnapshotExpected = { projectId: 'synthetic-project-id', parentAgentId: projectSupervisorAgentId, parentRunId: 'synthetic-parent-run', boundFrom: supervisorCollaborationPolicySnapshotInitialBatchBinding, - policy: multiGroupPolicy, - policyFingerprint: hashValue(JSON.stringify(multiGroupPolicy)), + policy: collaborationPolicy, + policyFingerprint: hashValue(JSON.stringify(collaborationPolicy)), }; const syntheticSnapshotBindingBatch = { schemaVersion: providerActionBatchSchemaVersion, @@ -1311,7 +820,7 @@ export async function runAgentRuntimeRealE2eSelfTests() { collaborationContract: { schemaVersion: supervisorCollaborationContractSchemaVersion, policySchemaVersion: supervisorCollaborationPolicySchemaVersion, - policySnapshot: multiGroupPolicy, + policySnapshot: collaborationPolicy, policyFingerprint: syntheticSnapshotExpected.policyFingerprint, contractFingerprint: hashValue('synthetic-collaboration-contract'), }, @@ -1359,7 +868,6 @@ export async function runAgentRuntimeRealE2eSelfTests() { const syntheticSnapshotBindingBytes = JSON.stringify( syntheticSnapshotBinding, ); - const syntheticDriftedPolicy = driftedSupervisorSwarmCollaborationPolicy(); state.initialRunId = syntheticSnapshotExpected.parentRunId; const syntheticDriftObservationCount = supervisorSwarmCollaborationPolicyDriftObservationCount([ @@ -1412,38 +920,17 @@ export async function runAgentRuntimeRealE2eSelfTests() { duplicateSupervisorSwarmCollaborationPolicySnapshotBindingCount([ syntheticSnapshotBinding, ]); - const syntheticEnospcDiagnostic = safeProcessFailureDiagnostic({ - code: 1, - stderr: 'No space left on device (os error 28)', - }); - const syntheticChatSessionFailureEvidence = buildSupervisorSwarmEvidence({ - chatSessionUnexpectedlyClosed: true, - chatSessionFailureKind: syntheticEnospcDiagnostic.failureKind, - chatSessionExitCode: syntheticEnospcDiagnostic.exitCode, - chatSessionCloseSignal: syntheticEnospcDiagnostic.signal, - chatSessionProcessErrorCode: syntheticEnospcDiagnostic.processErrorCode, - chatSessionStderrChars: syntheticEnospcDiagnostic.stderrChars, - chatSessionStderrSha256: syntheticEnospcDiagnostic.stderrSha256, - }); - state.suite = supervisorSwarmCollaborationPolicyMixedRecoverySuite; - const recoveryPolicy = expectedSupervisorSwarmCollaborationPolicy(); - const recoveryGroupCount = - supervisorSwarmExpectedIsolatedReviewGroups().length; - state.suite = supervisorSwarmAutonomousChatSuite; const syntheticSnapshotBindingBatchSuiteIndependent = supervisorSwarmInitialBatchBindsPolicySnapshot( syntheticSnapshotBindingBatch, syntheticSnapshotExpected.parentRunId, ); - state.suite = originalSuite; assert( syntheticSnapshotInspection.identityHashMatched && syntheticSnapshotBindingInspection.snapshotMatched && syntheticSnapshotBytes === JSON.stringify(syntheticSnapshot) && syntheticSnapshotBindingBytes === JSON.stringify(syntheticSnapshotBinding) && - multiGroupPolicy.minIsolatedGroupsBeforeClaim === 2 && - syntheticDriftedPolicy.minIsolatedGroupsBeforeClaim === 1 && syntheticDriftObservationCount === 1 && syntheticSnapshotTamperRejected && syntheticSnapshotBindingTamperRejected && @@ -1454,47 +941,9 @@ export async function runAgentRuntimeRealE2eSelfTests() { syntheticSnapshotBindingBatchAccepted && syntheticSnapshotBindingBatchRejections && syntheticSnapshotBindingBatchSuiteIndependent && - syntheticEnospcDiagnostic.failureKind === 'enospc' && - syntheticEnospcDiagnostic.stderrChars > 0 && - /^[0-9a-f]{64}$/u.test(syntheticEnospcDiagnostic.stderrSha256) && - syntheticChatSessionFailureEvidence.chatSessionUnexpectedlyClosed === - true && - syntheticChatSessionFailureEvidence.chatSessionFailureKind === 'enospc', - 'agent-runtime-real-e2e-self-test-collaboration-policy-snapshot-binding-invalid', - ); - assert( - JSON.stringify( - syntheticMixedEntries.map(({ groupIndex }) => groupIndex), - ) === JSON.stringify([0, 1]) && - JSON.stringify( - syntheticSingleGroupEntries.map(({ groupIndex }) => groupIndex), - ) === JSON.stringify([0]) && - supervisorSwarmIsolatedReviewGroupIndex( - supervisorSwarmIsolatedReviews.map((review) => ({ - expectedArtifacts: [review.path], - })), - ) === -1 && - JSON.stringify(syntheticReadyGroupIds) === - JSON.stringify( - syntheticMixedGroups.map((group) => group.delegationGroupId).sort(), - ) && - syntheticObservedJoinClaim.actionId === 'synthetic-claim-action' && - syntheticWriteScopeRoots.length === - supervisorSwarmIsolatedReviews.length && - new Set(syntheticWriteScopeRoots).size === - supervisorSwarmIsolatedReviews.length && - syntheticOverlappingWriteScopesRejected && - syntheticFollowupOrderValidated && - multiGroupPolicy.minIsolatedChildren === 2 && - Object.hasOwn(multiGroupPolicy, 'minIsolatedGroupsBeforeClaim') && - multiGroupPolicy.minIsolatedGroupsBeforeClaim === 2 && - multiGroupCount === 2 && - recoveryPolicy.minIsolatedChildren === 3 && - !Object.hasOwn(recoveryPolicy, 'minIsolatedGroupsBeforeClaim') && - (recoveryPolicy.minIsolatedGroupsBeforeClaim ?? 0) === 0 && - recoveryGroupCount === 1, - 'agent-runtime-real-e2e-self-test-mixed-isolated-groups-invalid', + 'agent-runtime-real-e2e-self-test-collaboration-policy-snapshot-binding-invalid', ); + const syntheticIdentity = { agentId: 'private-agent-id', taskId: 'private-task-id', @@ -1732,29 +1181,21 @@ export async function runAgentRuntimeRealE2eSelfTests() { dynamicPrivateBodyCount: expectedPrivateValues.length, evidenceMetadataExcluded: true, genericBoundaryTermsExcluded: true, - mixedIsolatedGroupTopologyValidated: true, - mixedReadyJoinObservationValidated: true, - mixedObservedJoinClaimValidated: true, - mixedCrossGroupWriteScopesValidated: true, - mixedFollowupBeforeClaimOrderValidated: true, - legacySingleIsolatedGroupTopologyValidated: true, - mixedSuitePolicyIsolationValidated: true, + isolatedReviewGroupTopologyValidated: true, + isolatedReadyJoinObservationValidated: true, + isolatedObservedJoinClaimValidated: true, + isolatedWriteScopesValidated: true, + isolatedFollowupBeforeClaimOrderValidated: true, collaborationPolicySnapshotCaptured: true, collaborationPolicySnapshotStable: true, collaborationPolicySnapshotBindingCaptured: true, collaborationPolicySnapshotBindingStable: true, durableSnapshotEligibilityAndContractBindingValidated: true, sourceEndpointAbsentLifecycleGuardValidated, - ...interactiveCliLifecycle, - ...providerBindingLifecycle, ...providerUsedReportingLifecycle, - ...ownedProcessCleanupLifecycle, - liveProcessIdentityEnumerationValidated: true, ...professionalSessionLifecycle, ...executionOwnerLockScanLifecycle, - ...staticSmokeBindingLifecycle, ...seededGameHtmlLifecycle, - ...supervisorAcceptedPublicStatusLifecycle, isolatedAppDataLatePathScannerValidated: true, ...childCloseTimeoutLifecycle, stableKillHandlePlatformValidated, @@ -1762,11 +1203,6 @@ export async function runAgentRuntimeRealE2eSelfTests() { stableKillHandleSignalValidated, stableKillHandleCommandFailureCleanupValidated, windowsOwnedTempPathValidated, - autonomousPlayableSuiteRegistered, - autonomousPlayablePackageCommandsRegistered, - autonomousPlayableDedicatedPathValidated: true, - autonomousPlayableExactStdinValidated: true, - autonomousPlayablePartialCollectorPrivacyValidated, toolPlanHandoffSuiteRegistered, toolPlanHandoffPackageCommandsRegistered, toolPlanHandoffSourceGuardRegistered, @@ -1776,7 +1212,6 @@ export async function runAgentRuntimeRealE2eSelfTests() { toolPlanHandoffEvidenceFieldsRegistered, toolPlanHandoffUnknownResultBoundaryPreserved: true, finalReplyFaultPrerequisiteValidated: true, - collaborationPolicyDriftFixtureValidated: true, collaborationPolicyDriftStatusObserved: true, duplicateCollaborationPolicySnapshotCount: syntheticSnapshotDuplicateCount, duplicateCollaborationPolicySnapshotBindingCount: @@ -1787,7 +1222,6 @@ export async function runAgentRuntimeRealE2eSelfTests() { collaborationPolicyPartialMissingEvidenceSafe: syntheticMissingSnapshotSafe && syntheticMissingBindingSafe, enospcFailureDiagnosticClassified: true, - chatSessionFailureEvidenceSchemaValidated: true, toolPlanRepairCount: fullRepairEvidence.toolPlanRepairCount, toolPlanRepairedLoopCount: fullRepairEvidence.toolPlanRepairedLoopCount, toolPlanSecondRepairCount: fullRepairEvidence.toolPlanSecondRepairCount, @@ -1927,139 +1361,6 @@ function validateExecutionOwnerLockScanSelfTest() { }; } -function validateSupervisorAcceptedPublicStatusSelfTest() { - const runId = 'synthetic-supervisor-accepted-status-run'; - const correlationId = finalMessageId( - projectSupervisorAgentId, - supervisorSwarmSessionId, - runId, - ).slice('agent-finalization-'.length); - const expectedMessageId = `runtime-public-status-${correlationId}-070c160a6299c543`; - const canonicalAcceptedStatus = { - role: 'assistant', - agentId: null, - sessionId: null, - messageId: expectedMessageId, - content: '任务已接收,项目总控 Agent 正在启动处理。', - }; - const rejectedVariants = [ - { ...canonicalAcceptedStatus, role: 'user' }, - { ...canonicalAcceptedStatus, agentId: projectSupervisorAgentId }, - { ...canonicalAcceptedStatus, sessionId: supervisorSwarmSessionId }, - { ...canonicalAcceptedStatus, messageId: `${expectedMessageId}-legacy` }, - { ...canonicalAcceptedStatus, content: '任务已接收。' }, - ]; - const canonicalBoundary = - inspectSupervisorAutonomousPlayableConversationBoundary( - [canonicalAcceptedStatus], - [canonicalAcceptedStatus], - runId, - ); - const rejectedBoundaries = [ - ...rejectedVariants.map((message) => [canonicalAcceptedStatus, message]), - [canonicalAcceptedStatus, { role: 'user', content: 'legacy-user' }], - ].map((legacyConversation) => - inspectSupervisorAutonomousPlayableConversationBoundary( - legacyConversation, - legacyConversation, - runId, - ), - ); - assert( - runtimePublicStatusMessageId( - projectSupervisorAgentId, - supervisorSwarmSessionId, - runId, - 'accepted', - ) === expectedMessageId && - isSupervisorAutonomousPlayableAcceptedPublicStatus( - canonicalAcceptedStatus, - runId, - ) && - rejectedVariants.every( - (message) => - !isSupervisorAutonomousPlayableAcceptedPublicStatus(message, runId), - ) && - canonicalBoundary.legacyUserFacingMessageCount === 1 && - canonicalBoundary.acceptedPublicStatusCount === 1 && - canonicalBoundary.wrongAgentFinalAssistantCount === 0 && - rejectedBoundaries.every( - (boundary) => - boundary.legacyUserFacingMessageCount !== 1 || - boundary.acceptedPublicStatusCount !== 1 || - boundary.wrongAgentFinalAssistantCount !== 0, - ), - 'agent-runtime-real-e2e-self-test-supervisor-accepted-public-status-invalid', - ); - return { - supervisorAcceptedPublicStatusIdValidated: true, - supervisorAcceptedPublicStatusShapeValidated: true, - supervisorAcceptedPublicStatusLegacyVariantsRejected: true, - }; -} - -function validateSupervisorPlayableProviderBindingSelfTest() { - const previousSuite = state.suite; - state.suite = supervisorAutonomousPlayableLaneDefenseSuite; - try { - const config = { - agentMode: 'provider', - llm: { - apiKey: 'synthetic-provider-key-not-for-network', - baseUrl: 'https://synthetic-provider.invalid/gpt/v1', - model: 'gpt-5.6-sol', - apiKind: 'openai_responses', - reasoningEffort: 'max', - }, - }; - const binding = expectedProviderBindingForSuite(config); - assert( - binding?.providerModel === 'gpt-5.6-sol' && - binding.providerApiKind === 'openai_responses' && - binding.providerReasoningEffort === 'max' && - /^[0-9a-f]{64}$/u.test(binding.providerBaseUrlSha256) && - binding.boundAgentIds.length === 1 && - !JSON.stringify(binding).includes(config.llm.apiKey) && - !JSON.stringify(binding).includes(config.llm.baseUrl), - 'agent-runtime-real-e2e-self-test-provider-binding-invalid', - ); - const reorderedBinding = { - boundAgentIds: [...binding.boundAgentIds], - providerBaseUrlSha256: binding.providerBaseUrlSha256, - providerReasoningEffort: binding.providerReasoningEffort, - providerApiKind: binding.providerApiKind, - providerModel: binding.providerModel, - }; - assert( - sameSupervisorPlayableProviderBinding(binding, reorderedBinding) && - !sameSupervisorPlayableProviderBinding(binding, { - ...reorderedBinding, - providerReasoningEffort: 'high', - }), - 'agent-runtime-real-e2e-self-test-provider-binding-comparison-invalid', - ); - const drifted = expectedProviderBindingForSuite({ - ...config, - agentLlm: { 'art-director': { reasoningEffort: 'high' } }, - }); - assert( - drifted === null, - 'agent-runtime-real-e2e-self-test-provider-binding-drift-accepted', - ); - return { - providerAgentModeBindingValidated: true, - providerModelBindingValidated: true, - providerApiKindBindingValidated: true, - providerReasoningEffortBindingValidated: true, - providerBaseUrlHashBindingValidated: true, - providerBindingPropertyOrderInsensitiveValidated: true, - providerBindingDriftRejected: true, - }; - } finally { - state.suite = previousSuite; - } -} - function validateProviderUsedReportingSelfTest() { const completeProviderEvidence = { evidenceCompleteness: 'complete', @@ -2121,110 +1422,6 @@ function validateProviderUsedReportingSelfTest() { providerUsedBindingMismatchRejected: true, }; } - -function validateOwnedProcessCleanupIdentitySelfTest() { - const processRecords = [ - { - pid: 41001, - parentPid: 1, - startedAt: 'runner-start', - name: 'genarrative-ai-game-creator-shell.exe', - }, - { - pid: 41002, - parentPid: 41001, - startedAt: 'helper-start', - name: 'powershell.exe', - }, - { - pid: 41003, - parentPid: 41001, - startedAt: 'node-start', - name: 'node.exe', - }, - { - pid: 41004, - parentPid: 41003, - startedAt: 'browser-start', - name: 'chrome.exe', - }, - { - pid: 41005, - parentPid: 41001, - startedAt: 'command-start', - name: 'cmd.exe', - }, - { - pid: 42000, - parentPid: 1, - startedAt: 'unrelated-start', - name: 'node.exe', - }, - ]; - const snapshot = buildOwnedProcessCleanupSnapshot(processRecords, { - runnerPid: 41001, - helperPids: [41002], - rootPids: [41001, 41002], - }); - const live = inspectOwnedProcessCleanupResiduals(snapshot, processRecords, { - activeCommandChildCount: 1, - activeInteractiveCliSessionCount: 1, - }); - const reusedPids = processRecords.map((record) => - record.pid >= 41001 && record.pid <= 41005 - ? { ...record, startedAt: `${record.startedAt}-reused` } - : record, - ); - const cleaned = inspectOwnedProcessCleanupResiduals(snapshot, reusedPids, { - activeCommandChildCount: 0, - activeInteractiveCliSessionCount: 0, - }); - assert( - snapshot.observedCounts.runner === 1 && - snapshot.observedCounts.helper === 1 && - snapshot.observedCounts.node === 1 && - snapshot.observedCounts.browser === 1 && - snapshot.observedCounts.command === 1 && - live.clean === false && - live.residualCounts.total === 5 && - cleaned.clean === true && - cleaned.residualCounts.total === 0, - 'agent-runtime-real-e2e-self-test-owned-process-cleanup-invalid', - ); - return { - ownedRunnerResidualIdentityValidated: true, - ownedHelperResidualIdentityValidated: true, - ownedNodeResidualIdentityValidated: true, - ownedBrowserResidualIdentityValidated: true, - ownedCommandResidualIdentityValidated: true, - ownedProcessPidReuseRejected: true, - activeChildRegistryCleanupValidated: true, - }; -} - -function validateStaticSmokeFinalIndexBindingSelfTest() { - const sha256 = 'a'.repeat(64); - const gate = { - lastVerificationStatus: 'passed', - verifiedRevision: 7, - staticSmokeVerifiedRevision: 7, - staticSmokeVerifiedGameIndexSha256: sha256, - }; - assert( - supervisorAutonomousPlayableStaticSmokeMatchesFinalIndex(gate, 7, sha256) && - !supervisorAutonomousPlayableStaticSmokeMatchesFinalIndex( - gate, - 7, - 'b'.repeat(64), - ), - 'agent-runtime-real-e2e-self-test-static-smoke-final-index-binding-invalid', - ); - return { - staticSmokeFinalIndexSha256BindingValidated: true, - staticSmokeStaleIndexSha256Rejected: true, - }; -} - function validateSeededGameHtmlContractSelfTest() { const html = seededGameHtml(); const lowerHtml = html.toLowerCase(); @@ -2365,133 +1562,3 @@ async function validateStableKillHandleCommandFailureCleanup() { await stopSelfTestChild(victim); } } - -async function validateInteractiveCliInheritedStdioLifecycle() { - const expectedReport = { - schemaVersion: 'game-creator-swarm-turn-report.v1', - outcome: 'settled', - parentAgentId: 'project-supervisor', - sessionId: 'interactive-cli-fd3-self-test-session', - parentRunId: 'interactive-cli-fd3-self-test-run', - runtimeCount: 1, - busyRuntimeCount: 0, - pendingTaskCount: 0, - runningTaskCount: 0, - waitingForConfirmationCount: 0, - waitingForUserInputCount: 0, - newAssistantMessageCount: 1, - finalReplyChars: 1, - reconciliationAgentCount: 0, - }; - const stderrMarker = 'interactive-cli-fd3-holder-stderr-marker'; - const previousProjectPathScanner = state.projectPathTranscriptScanner; - const projectPathScanner = new StreamingSecretScanner([stderrMarker]); - state.projectPathTranscriptScanner = projectPathScanner; - const reportLine = `[turn.report] ${JSON.stringify(expectedReport)}\n`; - const splitIndex = Math.max(1, Math.floor(reportLine.length / 2)); - const holderSource = ` -process.stdin.setEncoding('utf8'); -let input = ''; -const safetyTimer = setTimeout(() => process.exit(74), 15_000); -process.stdin.on('data', (chunk) => { - input += chunk; - if (!input.includes('CLOSE\\n')) return; - clearTimeout(safetyTimer); - process.exit(0); -}); -process.stdin.on('end', () => process.exit(75)); -process.stdin.resume(); -`; - const wrapperSource = ` -const { spawn } = require('node:child_process'); -const holder = spawn( - process.execPath, - ['--input-type=commonjs', '-e', ${JSON.stringify(holderSource)}], - { stdio: [3, 'inherit', 'inherit'], windowsHide: true }, -); -holder.once('error', () => { - process.exitCode = 73; -}); -holder.unref(); -const reportLine = ${JSON.stringify(reportLine)}; -process.stdout.write(reportLine.slice(0, ${splitIndex}), () => { - setImmediate(() => { - process.stdout.write(reportLine.slice(${splitIndex}), () => { - process.stderr.write(${JSON.stringify(`${stderrMarker}\n`)}, () => { - process.exit(process.exitCode ?? 0); - }); - }); - }); -}); -`; - const wrapper = spawn( - process.execPath, - ['--input-type=commonjs', '-e', wrapperSource], - { - stdio: ['ignore', 'pipe', 'pipe', 'pipe'], - windowsHide: true, - }, - ); - const session = createInteractiveCliSession(wrapper); - const originalKill = wrapper.kill.bind(wrapper); - let killCallCount = 0; - wrapper.kill = (...args) => { - killCallCount += 1; - return originalKill(...args); - }; - let controlStreamError = null; - wrapper.stdio[3].on('error', (error) => { - controlStreamError ??= error; - }); - try { - await waitForInteractiveCliExit(session, 10_000); - assert( - session.exited && !session.stdioClosed, - 'agent-runtime-real-e2e-self-test-interactive-cli-two-phase-not-observed', - ); - const parsedReport = - await waitForSupervisorAutonomousPlayableCliExit(session); - await waitForInteractiveCliOutput( - session, - () => session.stderr.toString('utf8').includes(`${stderrMarker}\n`), - 'agent-runtime-real-e2e-self-test-interactive-cli-output-timeout', - 10_000, - { allowAfterProcessExit: true }, - ); - assert( - JSON.stringify(canonicalJsonValue(parsedReport)) === - JSON.stringify(canonicalJsonValue(expectedReport)), - 'agent-runtime-real-e2e-self-test-interactive-cli-report-mismatch', - ); - await closeInteractiveCli(session); - assert( - killCallCount === 0 && !session.stdioClosed, - 'agent-runtime-real-e2e-self-test-interactive-cli-exit-cleanup-killed', - ); - wrapper.stdio[3].end('CLOSE\n'); - await waitForInteractiveCliStdioClose(session, 10_000); - assert( - controlStreamError === null && - session.stdioClosed && - projectPathScanner.count === 1, - 'agent-runtime-real-e2e-self-test-interactive-cli-stdio-close-invalid', - ); - return { - interactiveCliExitBeforeStdioCloseValidated: true, - interactiveCliPostExitReportValidated: true, - interactiveCliExitCleanupDidNotKillValidated: true, - interactiveCliStdioCloseAfterHolderReleaseValidated: true, - interactiveCliProjectPathScannerValidated: true, - }; - } finally { - if (!wrapper.stdio[3].destroyed && !wrapper.stdio[3].writableEnded) { - wrapper.stdio[3].end('CLOSE\n'); - } - if (!session.stdioClosed) { - await waitForInteractiveCliStdioClose(session, 10_000).catch(() => { - destroyInteractiveCliOutputStreams(session); - }); - } - state.projectPathTranscriptScanner = previousProjectPathScanner; - } -} diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-autonomous-playable.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-autonomous-playable.mjs deleted file mode 100644 index 6d988a64a..000000000 --- a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-autonomous-playable.mjs +++ /dev/null @@ -1,1890 +0,0 @@ -import { - assert, - codedError, - hashJsonValue, - hashValue, - isFailedTask, - parseSingleSwarmTurnReport, - recordError, - sleep, -} from '../assertions/core.mjs'; -import { - absolutePathVariants, - actionAuditIdentity, - countExactSecrets, - duplicateCount, - finalMessageId, - isNonEmptyString, - runtimePublicStatusMessageId, -} from '../assertions/runtime.mjs'; -import { fs, path } from '../dependencies.mjs'; -import { - claimOwnedRunner, - ensureOwnedRunnerStableKillSupport, - prepareIsolatedSuiteAppData, -} from '../harness/app-data.mjs'; -import { isPlainObject } from '../harness/config.mjs'; -import { isLiveTask, isPathInside, listFiles } from '../harness/io.mjs'; -import { - codedProcessError, - interactiveCliOutput, - prepareCliBinary, - startInteractiveCli, - waitForInteractiveCliOutput, -} from '../harness/process.mjs'; -import { - productionDefaultGameIndexHtml, - seedDisposableProject, -} from '../harness/project.mjs'; -import { decodeUtf8Fatal } from '../harness/reporting.mjs'; -import { - countLureLeaks, - findPendingActions, - readRuntime, -} from '../harness/runtime.mjs'; -import { - autonomousCompletionContractSchemaVersion, - autonomousGameBuildRunProfile, - autonomousPlaytestReceiptSchemaVersion, - laneDefensePlaytestRequiredAssertions, - projectSupervisorAgentId, - providerRequestLifecycleSchemaVersion, - runProfileBindingSchemaVersion, - state, - supervisorAutonomousPlayableLaneDefenseSuite, - supervisorAutonomousPlayableLaneDefenseTask, - supervisorAutonomousPlayableRunTimeoutMs, - supervisorAutonomousPlayableSafeDeliveryStatuses, - supervisorAutonomousPlayableSafePhases, - supervisorAutonomousPlayableSafeStatuses, - supervisorAutonomousPlayableSafeTerminalStatuses, - supervisorAutonomousPlayableSafeTurnOutcomes, - supervisorAutonomousPlayableSourceFieldMaxChars, - supervisorAutonomousPlayableSourceTotalMaxChars, - supervisorSwarmSessionId, - supervisorSwarmTerminalSidecarCleanupTimeoutMs, -} from '../runtime-state.mjs'; -import { - collectSupervisorSwarmPublicLeakEvidence, - countSupervisorSwarmSteerRecords, - readSupervisorSwarmJsonDirectory, - readSupervisorSwarmPersistence, - readSupervisorSwarmProjectRevision, - supervisorSwarmParentDeliveries, - validateSupervisorSwarmFinalization, -} from './supervisor-swarm.mjs'; - -const projectSupervisorCliSource = 'project-supervisor-cli'; -const projectSupervisorAcceptedPublicStatus = 'accepted'; -const projectSupervisorAcceptedPublicStatusContent = - '任务已接收,项目总控 Agent 正在启动处理。'; - -export function isSupervisorAutonomousPlayableAcceptedPublicStatus( - message, - runId, -) { - return ( - isPlainObject(message) && - message.role === 'assistant' && - message.agentId == null && - message.sessionId == null && - message.messageId === - runtimePublicStatusMessageId( - projectSupervisorAgentId, - supervisorSwarmSessionId, - runId, - projectSupervisorAcceptedPublicStatus, - ) && - message.content === projectSupervisorAcceptedPublicStatusContent - ); -} - -export function inspectSupervisorAutonomousPlayableConversationBoundary( - legacyConversation, - userFacing, - runId, -) { - const legacyUserFacingMessages = legacyConversation.filter((message) => - ['user', 'assistant'].includes(message.role), - ); - const acceptedPublicStatuses = legacyUserFacingMessages.filter((message) => - isSupervisorAutonomousPlayableAcceptedPublicStatus(message, runId), - ); - const wrongAgentFinalAssistants = userFacing.filter( - (message) => - message.role === 'assistant' && - message.agentId !== projectSupervisorAgentId && - !isSupervisorAutonomousPlayableAcceptedPublicStatus(message, runId), - ); - return { - legacyUserFacingMessageCount: legacyUserFacingMessages.length, - acceptedPublicStatusCount: acceptedPublicStatuses.length, - wrongAgentFinalAssistantCount: wrongAgentFinalAssistants.length, - }; -} - -export function supervisorAutonomousPlayableMode() { - return { - rootSource: projectSupervisorCliSource, - evidenceAgentId: projectSupervisorAgentId, - scenario: 'project-supervisor-autonomous-playable-lane-defense', - cliFlags: [], - }; -} - -export function buildSupervisorAutonomousPlayableStdin() { - const task = supervisorAutonomousPlayableLaneDefenseTask; - assert( - task === - '做一个植物大战僵尸式的塔防游戏,要能选择植物、阻挡敌人、正常闯关,并且生成后可以直接试玩。' && - !/[\r\n]/u.test(task), - 'supervisor-autonomous-playable-task-not-exact', - ); - for (const forbidden of [ - projectSupervisorAgentId, - autonomousGameBuildRunProfile, - 'Agent', - 'Runtime', - 'approve', - 'answer', - 'steer', - 'runner', - 'tool', - 'actionId', - 'runId', - ]) { - assert( - !task.includes(forbidden), - 'supervisor-autonomous-playable-task-recipe-leak', - ); - } - return Buffer.from(`${task}\n`, 'utf8'); -} - -export async function waitForSupervisorAutonomousPlayableParentRuntime(task) { - const deadline = Date.now() + 120_000; - while (Date.now() < deadline) { - const runtime = await readRuntime(projectSupervisorAgentId).catch( - () => null, - ); - if ( - runtime?.agentId === projectSupervisorAgentId && - runtime.sessionId === supervisorSwarmSessionId && - isNonEmptyString(runtime.runId) && - runtime.currentTask === task && - runtime.runProfile === autonomousGameBuildRunProfile && - /^[0-9a-f]{64}$/u.test(runtime.runProfileBindingFingerprint ?? '') - ) { - return runtime; - } - const session = state.supervisorAutonomousPlayableCliSession; - if (session?.closed && session.closeInfo?.code !== 0) { - throw codedProcessError('supervisor-autonomous-playable-cli-failed', { - ...session.closeInfo, - stdout: session.stdout, - stderr: session.stderr, - }); - } - await sleep(50); - } - throw codedError('supervisor-autonomous-playable-parent-runtime-timeout'); -} - -export async function waitForSupervisorAutonomousPlayableCliExit(session) { - let timeoutHandle; - const timeout = new Promise((resolve) => { - timeoutHandle = setTimeout( - () => resolve(null), - supervisorAutonomousPlayableRunTimeoutMs, - ); - }); - let result; - try { - result = await Promise.race([session.exitPromise, timeout]); - } finally { - clearTimeout(timeoutHandle); - } - if (!result) { - throw codedError('supervisor-autonomous-playable-cli-timeout'); - } - if (result.error || result.code !== 0 || result.signal !== null) { - state.supervisorAutonomousPlayable.cliOutput = - interactiveCliOutput(session); - throw codedProcessError('supervisor-autonomous-playable-cli-failed', { - ...result, - stdout: session.stdout, - stderr: session.stderr, - }); - } - const output = await waitForInteractiveCliOutput( - session, - () => - /(?:^|\r?\n)\[turn\.report\] [^\r\n]+\r?\n/u.test( - session.stdout.toString('utf8'), - ), - 'supervisor-autonomous-playable-turn-report-timeout', - 10_000, - { allowAfterProcessExit: true }, - ); - state.supervisorAutonomousPlayable.cliOutput = output; - const report = parseSingleSwarmTurnReport( - output, - 'supervisor-autonomous-playable', - ); - state.supervisorAutonomousPlayable.turnReport = report; - return report; -} - -export async function readSupervisorAutonomousPlayableResidualSidecarCounts() { - const roots = { - confirmations: '.agent/runtime/confirmations', - finalizations: '.agent/runtime/finalizations', - parallelReadBatches: '.agent/runtime/parallel-read-batches', - pendingActions: '.agent/runtime/pending-actions', - providerActionBatches: '.agent/runtime/provider-action-batches', - providerHandoffs: '.agent/runtime/provider-handoffs', - providerRetries: '.agent/runtime/provider-retries', - toolPlanHandoffs: '.agent/runtime/tool-plan-handoffs', - userInput: '.agent/runtime/user-input', - }; - const counts = {}; - for (const [name, relativePath] of Object.entries(roots)) { - counts[name] = ( - await listFiles(path.join(state.projectRoot, relativePath)) - ).length; - } - return counts; -} - -export function supervisorAutonomousPlayableSafeLifecycleLabel(value, allowed) { - if (value == null || value === '') return 'absent'; - return typeof value === 'string' && allowed.has(value) ? value : 'unknown'; -} - -export function countSupervisorAutonomousPlayableLifecycleLabels( - records, - field, - allowed, -) { - const counts = {}; - for (const record of records) { - const label = supervisorAutonomousPlayableSafeLifecycleLabel( - record?.[field], - allowed, - ); - counts[label] = (counts[label] ?? 0) + 1; - } - return counts; -} - -export function supervisorAutonomousPlayableSafeTurnCount(report, field) { - const value = report?.[field]; - return Number.isSafeInteger(value) && value >= 0 ? value : null; -} - -export function summarizeSupervisorAutonomousPlayableSourcePayloadAudits( - records, -) { - const protocols = records.filter( - (record) => record.recordType === 'agent.runtime.tool_plan.protocol', - ); - let sourceMutationActionMax = 0; - let sourcePayloadMaxFieldChars = 0; - let sourcePayloadMaxTotalChars = 0; - let sourcePayloadPolicyViolationCount = 0; - for (const record of protocols) { - const mutationCount = record.autonomousSourceMutationActionCount; - const maxFieldChars = record.autonomousSourceMaxFieldChars; - const totalChars = record.autonomousSourceTotalChars; - const valid = - record.autonomousSourcePayloadValidated === true && - Number.isSafeInteger(mutationCount) && - mutationCount >= 0 && - mutationCount <= 1 && - Number.isSafeInteger(maxFieldChars) && - maxFieldChars >= 0 && - maxFieldChars <= supervisorAutonomousPlayableSourceFieldMaxChars && - Number.isSafeInteger(totalChars) && - totalChars >= maxFieldChars && - totalChars <= supervisorAutonomousPlayableSourceTotalMaxChars; - if (!valid) sourcePayloadPolicyViolationCount += 1; - if (Number.isSafeInteger(mutationCount)) { - sourceMutationActionMax = Math.max( - sourceMutationActionMax, - mutationCount, - ); - } - if (Number.isSafeInteger(maxFieldChars)) { - sourcePayloadMaxFieldChars = Math.max( - sourcePayloadMaxFieldChars, - maxFieldChars, - ); - } - if (Number.isSafeInteger(totalChars)) { - sourcePayloadMaxTotalChars = Math.max( - sourcePayloadMaxTotalChars, - totalChars, - ); - } - } - return { - acceptedAutonomousToolPlanCount: protocols.length, - sourceMutationActionMax, - sourcePayloadMaxFieldChars, - sourcePayloadMaxTotalChars, - sourcePayloadPolicyViolationCount, - }; -} - -export function buildSupervisorAutonomousPlayablePartialEvidence( - persistence, - { - residualSidecars = {}, - pendingActionCount = 0, - turnReport = null, - partialEvidenceReadErrorCount = 0, - } = {}, -) { - const productionDefaultGameIndexSha256 = hashValue( - Buffer.from(productionDefaultGameIndexHtml(), 'utf8'), - ); - const parentTask = persistence.taskSnapshot.latest.find( - (task) => - task.agentId === projectSupervisorAgentId && - task.runId === state.initialRunId, - ); - const parentRuntime = persistence.runtimeStates.find( - (runtime) => - runtime.agentId === projectSupervisorAgentId && - runtime.runId === state.initialRunId, - ); - const childTasks = persistence.taskSnapshot.latest.filter( - (task) => - task.parentAgentId === projectSupervisorAgentId && - task.parentRunId === state.initialRunId, - ); - const childRuntimes = persistence.runtimeStates.filter( - (runtime) => - runtime.parentAgentId === projectSupervisorAgentId && - runtime.parentRunId === state.initialRunId, - ); - const deliveries = (persistence.deliveries ?? []).filter( - (delivery) => - delivery.parentAgentId === projectSupervisorAgentId && - delivery.parentRunId === state.initialRunId, - ); - const initialDeliveries = deliveries.filter( - (delivery) => delivery.repairOfDelegationId == null, - ); - const repairDeliveries = deliveries.filter( - (delivery) => delivery.repairOfDelegationId != null, - ); - const repairDeliveryIds = new Set( - repairDeliveries.map((delivery) => delivery.delegationId), - ); - const failedRepairChildCount = childTasks.filter( - (task) => isFailedTask(task) && repairDeliveryIds.has(task.delegationId), - ).length; - const failedOriginalTasks = childTasks.filter( - (task) => isFailedTask(task) && !repairDeliveryIds.has(task.delegationId), - ); - let recoveredOriginalFailureCount = 0; - for (const task of failedOriginalTasks) { - const original = initialDeliveries.find( - (delivery) => - delivery.delegationId === task.delegationId && - delivery.targetAgentId === task.agentId && - delivery.targetSessionId === task.sessionId && - delivery.targetRunId === task.runId, - ); - if ( - original && - repairDeliveries.some( - (repair) => - repair.repairOfDelegationId === original.delegationId && - repair.terminalStatus === 'completed' && - repair.structuredResult?.contractStatus === 'evidence-ready', - ) - ) { - recoveredOriginalFailureCount += 1; - } - } - - const relevantRunKeys = new Set(); - for (const record of [ - parentTask, - parentRuntime, - ...childTasks, - ...childRuntimes, - ]) { - if (isNonEmptyString(record?.agentId) && isNonEmptyString(record?.runId)) { - relevantRunKeys.add(`${record.agentId}\0${record.runId}`); - } - } - const lifecycle = persistence.agentDb.filter( - (record) => - record.recordType === 'agent.runtime.provider_request.lifecycle' && - relevantRunKeys.has(`${record.agentId}\0${record.runId}`), - ); - const retryAudits = persistence.agentDb.filter( - (record) => - record.recordType === 'agent.runtime.provider_request.retry' && - relevantRunKeys.has(`${record.agentId}\0${record.runId}`), - ); - const lifecycleByRequest = new Map(); - let duplicateProviderLifecycleCount = 0; - for (const record of lifecycle) { - if (!isNonEmptyString(record.requestId)) continue; - const statuses = lifecycleByRequest.get(record.requestId) ?? new Set(); - if (statuses.has(record.status)) duplicateProviderLifecycleCount += 1; - statuses.add(record.status); - lifecycleByRequest.set(record.requestId, statuses); - } - const terminalTaskCount = persistence.taskSnapshot.latest.filter((task) => - ['budget-exhausted', 'cancelled', 'completed', 'failed'].includes( - task.status, - ), - ).length; - const supervisorMessages = persistence.supervisorConversation ?? []; - const turnReportCaptured = isPlainObject(turnReport); - const turnReportOutcome = turnReportCaptured - ? supervisorAutonomousPlayableSafeTurnOutcomes.has(turnReport.outcome) - ? turnReport.outcome - : 'unknown' - : null; - const turnReportParentIdentityStable = turnReportCaptured - ? turnReport.parentAgentId === projectSupervisorAgentId && - turnReport.sessionId === supervisorSwarmSessionId && - turnReport.parentRunId === state.initialRunId - : false; - const turnReportPrivateLeakCount = turnReportCaptured - ? countExactSecrets( - Buffer.from(JSON.stringify(turnReport)), - state.supervisorAutonomousPlayable.privateValues, - ) - : null; - const sourcePayload = - summarizeSupervisorAutonomousPlayableSourcePayloadAudits( - persistence.agentDb, - ); - const providerBinding = - state.supervisorAutonomousPlayable.effectiveProviderBinding ?? - state.supervisorAutonomousPlayable.expectedProviderBinding; - - return { - evidenceCompleteness: 'partial', - partialEvidenceCollected: true, - partialEvidenceReadErrorCount, - partialPrivacyScanComplete: false, - rootRunObserved: Boolean(parentTask || parentRuntime), - parentTaskStatus: supervisorAutonomousPlayableSafeLifecycleLabel( - parentTask?.status, - supervisorAutonomousPlayableSafeStatuses, - ), - parentTaskPhase: supervisorAutonomousPlayableSafeLifecycleLabel( - parentTask?.phase, - supervisorAutonomousPlayableSafePhases, - ), - parentRuntimeStatus: supervisorAutonomousPlayableSafeLifecycleLabel( - parentRuntime?.status, - supervisorAutonomousPlayableSafeStatuses, - ), - parentRuntimePhase: supervisorAutonomousPlayableSafeLifecycleLabel( - parentRuntime?.phase, - supervisorAutonomousPlayableSafePhases, - ), - childTaskStatusCounts: countSupervisorAutonomousPlayableLifecycleLabels( - childTasks, - 'status', - supervisorAutonomousPlayableSafeStatuses, - ), - childTaskPhaseCounts: countSupervisorAutonomousPlayableLifecycleLabels( - childTasks, - 'phase', - supervisorAutonomousPlayableSafePhases, - ), - childRuntimeStatusCounts: countSupervisorAutonomousPlayableLifecycleLabels( - childRuntimes, - 'status', - supervisorAutonomousPlayableSafeStatuses, - ), - childRuntimePhaseCounts: countSupervisorAutonomousPlayableLifecycleLabels( - childRuntimes, - 'phase', - supervisorAutonomousPlayableSafePhases, - ), - initialDeliveryStatusCounts: - countSupervisorAutonomousPlayableLifecycleLabels( - initialDeliveries, - 'status', - supervisorAutonomousPlayableSafeDeliveryStatuses, - ), - repairDeliveryStatusCounts: - countSupervisorAutonomousPlayableLifecycleLabels( - repairDeliveries, - 'status', - supervisorAutonomousPlayableSafeDeliveryStatuses, - ), - repairTerminalStatusCounts: - countSupervisorAutonomousPlayableLifecycleLabels( - repairDeliveries, - 'terminalStatus', - supervisorAutonomousPlayableSafeTerminalStatuses, - ), - failedOriginalChildCount: failedOriginalTasks.length, - failedRepairChildCount, - recoveredSpecialistFailureCount: recoveredOriginalFailureCount, - recoveredOriginalFailureCount, - awaitingRepairCount: - failedOriginalTasks.length - recoveredOriginalFailureCount, - stdinTaskCount: state.supervisorAutonomousPlayable.stdinWriteCount, - stdinEndedAfterTask: state.supervisorAutonomousPlayable.stdinEnded, - stdinBytes: state.supervisorAutonomousPlayable.stdinBytes, - taskSha256: state.initialTask?.sha256 ?? null, - providerAgentMode: providerBinding?.providerAgentMode ?? null, - providerModel: providerBinding?.providerModel ?? null, - providerApiKind: providerBinding?.providerApiKind ?? null, - providerReasoningEffort: providerBinding?.providerReasoningEffort ?? null, - providerBaseUrlSha256: providerBinding?.providerBaseUrlSha256 ?? null, - providerBoundAgentCount: providerBinding?.boundAgentIds?.length ?? 0, - providerBindingMatched: - state.supervisorAutonomousPlayable.effectiveProviderBinding != null && - JSON.stringify( - state.supervisorAutonomousPlayable.effectiveProviderBinding, - ) === - JSON.stringify( - state.supervisorAutonomousPlayable.expectedProviderBinding, - ), - turnReportCaptured, - turnReportParentIdentityStable, - turnReportNewAssistantMessageCount: - supervisorAutonomousPlayableSafeTurnCount( - turnReport, - 'newAssistantMessageCount', - ), - turnReportFinalReplyChars: supervisorAutonomousPlayableSafeTurnCount( - turnReport, - 'finalReplyChars', - ), - turnReportPrivateLeakCount, - turnReportOutcome, - turnReportRuntimeCount: supervisorAutonomousPlayableSafeTurnCount( - turnReport, - 'runtimeCount', - ), - turnReportBusyRuntimeCount: supervisorAutonomousPlayableSafeTurnCount( - turnReport, - 'busyRuntimeCount', - ), - turnReportPendingTaskCount: supervisorAutonomousPlayableSafeTurnCount( - turnReport, - 'pendingTaskCount', - ), - turnReportRunningTaskCount: supervisorAutonomousPlayableSafeTurnCount( - turnReport, - 'runningTaskCount', - ), - turnReportWaitingForConfirmationCount: - supervisorAutonomousPlayableSafeTurnCount( - turnReport, - 'waitingForConfirmationCount', - ), - turnReportWaitingForUserInputCount: - supervisorAutonomousPlayableSafeTurnCount( - turnReport, - 'waitingForUserInputCount', - ), - turnReportReconciliationAgentCount: - supervisorAutonomousPlayableSafeTurnCount( - turnReport, - 'reconciliationAgentCount', - ), - taskCount: persistence.taskSnapshot.latest.length, - terminalTaskCount, - childTaskCount: childTasks.length, - runtimeCount: persistence.runtimeStates.length, - childRuntimeCount: childRuntimes.length, - finalSupervisorAssistantCount: supervisorMessages.filter( - (message) => message.role === 'assistant', - ).length, - supervisorUserMessageCount: supervisorMessages.filter( - (message) => message.role === 'user', - ).length, - professionalAssistantCount: ( - persistence.professionalConversations ?? [] - ).reduce( - (count, conversation) => - count + - conversation.messages.filter((message) => message.role === 'assistant') - .length, - 0, - ), - providerRequestIdentityCount: lifecycleByRequest.size, - providerLifecycleStartedCount: lifecycle.filter( - (record) => record.status === 'started', - ).length, - providerLifecycleTerminalCount: lifecycle.filter((record) => - ['completed', 'failed'].includes(record.status), - ).length, - providerLifecycleCompletedCount: lifecycle.filter( - (record) => record.status === 'completed', - ).length, - providerLifecycleFailedCount: lifecycle.filter( - (record) => record.status === 'failed', - ).length, - providerRetryAuditCount: retryAudits.length, - providerConnectivityRetryAuditCount: retryAudits.filter( - (record) => record.errorKind === 'connectivity', - ).length, - providerTransportRetryAuditCount: retryAudits.filter( - (record) => record.errorKind === 'transport', - ).length, - providerTimeoutRetryAuditCount: retryAudits.filter( - (record) => record.errorKind === 'timeout', - ).length, - ...sourcePayload, - openProviderLifecycleCount: [...lifecycleByRequest.values()].filter( - (statuses) => - statuses.has('started') && - !statuses.has('completed') && - !statuses.has('failed'), - ).length, - duplicateProviderLifecycleCount, - pendingActionCount, - confirmationSidecarCount: residualSidecars.confirmations ?? 0, - userInputSidecarCount: residualSidecars.userInput ?? 0, - providerActionBatchSidecarCount: - residualSidecars.providerActionBatches ?? 0, - providerRetrySidecarCount: residualSidecars.providerRetries ?? 0, - providerHandoffSidecarCount: residualSidecars.providerHandoffs ?? 0, - toolPlanHandoffSidecarCount: residualSidecars.toolPlanHandoffs ?? 0, - finalizationJournalCount: residualSidecars.finalizations ?? 0, - reconciliationResidueCount: - supervisorAutonomousPlayableReconciliationResidueCount(persistence), - freshInitBaselineUsed: - state.supervisorAutonomousPlayable.freshInitBaselineUsed, - initialGameIndexMatchesProductionDefault: - state.supervisorAutonomousPlayable.initialGameIndexSha256 === - productionDefaultGameIndexSha256, - initialGameIndexSha256: - state.supervisorAutonomousPlayable.initialGameIndexSha256, - }; -} - -export async function collectPartialSupervisorAutonomousPlayableEvidence() { - const persistence = await readSupervisorSwarmPersistence({ - tolerateErrors: true, - }); - let partialEvidenceReadErrorCount = Object.keys( - persistence.failureEvidenceErrors, - ).length; - let residualSidecars = {}; - try { - residualSidecars = - await readSupervisorAutonomousPlayableResidualSidecarCounts(); - } catch { - partialEvidenceReadErrorCount += 1; - recordError('supervisor-autonomous-playable-partial-sidecar-read-failed'); - } - let pendingActionCount = 0; - try { - pendingActionCount = (await findPendingActions()).length; - } catch { - partialEvidenceReadErrorCount += 1; - recordError('supervisor-autonomous-playable-partial-pending-read-failed'); - } - state.supervisorAutonomousPlayable.privateValues = [ - ...new Set([ - ...state.supervisorAutonomousPlayable.privateValues, - ...state.supervisorSwarm.privateValues, - ]), - ]; - return buildSupervisorAutonomousPlayablePartialEvidence(persistence, { - residualSidecars, - pendingActionCount, - turnReport: state.supervisorAutonomousPlayable.turnReport, - partialEvidenceReadErrorCount, - }); -} - -export function supervisorAutonomousPlayableReconciliationResidueCount( - persistence, -) { - const taskCount = persistence.taskSnapshot.latest.filter( - (task) => task.phase === 'needs-reconciliation', - ).length; - const runtimeCount = persistence.runtimeStates.filter( - (runtime) => runtime.phase === 'needs-reconciliation', - ).length; - const auditCount = persistence.agentDb.filter( - (record) => - record.status === 'needs-reconciliation' || - record.phase === 'needs-reconciliation' || - String(record.recordType ?? '').includes('needs_reconciliation'), - ).length; - return taskCount + runtimeCount + auditCount; -} - -export function supervisorAutonomousPlayableFailureDisposition( - persistence, - { requireRecovered = false } = {}, -) { - const deliveries = supervisorSwarmParentDeliveries( - persistence.deliveries ?? [], - ); - const parentTask = persistence.taskSnapshot.latest.find( - (task) => - task.agentId === projectSupervisorAgentId && - task.runId === state.initialRunId, - ); - const parentRuntime = persistence.runtimeStates.find( - (runtime) => - runtime.agentId === projectSupervisorAgentId && - runtime.runId === state.initialRunId, - ); - assert( - !isFailedTask(parentTask ?? {}) && !isFailedTask(parentRuntime ?? {}), - 'supervisor-autonomous-playable-root-failed', - ); - const parentActive = - (parentTask && isLiveTask(parentTask)) || - [ - 'pending', - 'running', - 'waiting-for-confirmation', - 'waiting-for-user-input', - ].includes(parentRuntime?.status) || - ['queued', 'running', 'executing', 'finalizing'].includes( - parentRuntime?.phase, - ); - let recoveredOriginalFailureCount = 0; - let awaitingRepairCount = 0; - for (const task of persistence.taskSnapshot.latest.filter( - (candidate) => - candidate.parentAgentId === projectSupervisorAgentId && - candidate.parentRunId === state.initialRunId && - isFailedTask(candidate), - )) { - const delivery = deliveries.find( - (candidate) => - candidate.delegationId === task.delegationId && - candidate.targetAgentId === task.agentId && - candidate.targetSessionId === task.sessionId && - candidate.targetRunId === task.runId, - ); - assert( - delivery && delivery.repairOfDelegationId == null, - delivery?.repairOfDelegationId != null - ? 'supervisor-autonomous-playable-repair-child-failed' - : 'supervisor-autonomous-playable-unrecoverable-child-failed', - ); - const repairs = deliveries.filter( - (candidate) => candidate.repairOfDelegationId === delivery.delegationId, - ); - assert( - repairs.length <= 1, - 'supervisor-autonomous-playable-duplicate-repair', - ); - const repairable = - delivery.status !== 'suppressed' && - ((delivery.acceptanceCriteria?.length ?? 0) > 0 || - (delivery.expectedArtifacts?.length ?? 0) > 0) && - (delivery.structuredResult == null || - delivery.structuredResult.contractStatus === 'needs-repair'); - const recovered = repairs.some( - (repair) => - repair.terminalStatus === 'completed' && - repair.structuredResult?.contractStatus === 'evidence-ready', - ); - assert( - repairable && (recovered || parentActive || !requireRecovered), - 'supervisor-autonomous-playable-specialist-failure-unrecovered', - ); - if (recovered) recoveredOriginalFailureCount += 1; - else awaitingRepairCount += 1; - } - if (requireRecovered) { - assert( - awaitingRepairCount === 0, - 'supervisor-autonomous-playable-terminal-repair-incomplete', - ); - } - return { recoveredOriginalFailureCount, awaitingRepairCount }; -} - -export function assertSupervisorAutonomousPlayableRuntimeHealthy(persistence) { - for (const task of persistence.taskSnapshot.latest) { - if (task.phase === 'needs-reconciliation') { - throw codedError( - 'supervisor-autonomous-playable-runtime-needs-reconciliation', - ); - } - } - for (const runtime of persistence.runtimeStates) { - if (runtime.phase === 'needs-reconciliation') { - throw codedError( - 'supervisor-autonomous-playable-runtime-needs-reconciliation', - ); - } - } - return supervisorAutonomousPlayableFailureDisposition(persistence); -} - -export async function waitForSupervisorAutonomousPlayableDurableQuiescence() { - const deadline = Date.now() + supervisorSwarmTerminalSidecarCleanupTimeoutMs; - let quietPolls = 0; - while (Date.now() < deadline) { - const persistence = await readSupervisorSwarmPersistence(); - assertSupervisorAutonomousPlayableRuntimeHealthy(persistence); - const pending = await findPendingActions(); - const residualSidecars = - await readSupervisorAutonomousPlayableResidualSidecarCounts(); - const noLiveTasks = persistence.taskSnapshot.latest.every( - (task) => - !isLiveTask(task) && - !['waiting-for-user-input', 'waiting-for-confirmation'].includes( - task.status, - ), - ); - const noLiveRuntimes = persistence.runtimeStates.every( - (runtime) => - !isNonEmptyString(runtime.runId) || - ['completed', 'cancelled'].includes(runtime.phase), - ); - const settled = - noLiveTasks && - noLiveRuntimes && - pending.length === 0 && - Object.values(residualSidecars).every((count) => count === 0) && - supervisorAutonomousPlayableReconciliationResidueCount(persistence) === 0; - if (settled) { - quietPolls += 1; - if (quietPolls >= 3) { - const failureDisposition = - supervisorAutonomousPlayableFailureDisposition(persistence, { - requireRecovered: true, - }); - return { persistence, residualSidecars, failureDisposition }; - } - } else { - quietPolls = 0; - } - await sleep(50); - } - throw codedError('supervisor-autonomous-playable-residue-timeout'); -} - -export function supervisorAutonomousPlayableStaticSmokeMatchesFinalIndex( - gate, - revision, - finalGameIndexSha256, -) { - return ( - gate?.lastVerificationStatus === 'passed' && - gate.verifiedRevision === revision && - gate.staticSmokeVerifiedRevision === revision && - gate.staticSmokeVerifiedGameIndexSha256 === finalGameIndexSha256 - ); -} - -export async function validateSupervisorAutonomousPlayableEvidence( - persistence, - residualSidecars, -) { - const task = supervisorAutonomousPlayableLaneDefenseTask; - const mode = supervisorAutonomousPlayableMode(); - const report = state.supervisorAutonomousPlayable.turnReport; - assert( - report?.schemaVersion === 'game-creator-swarm-turn-report.v1' && - report.outcome === 'settled' && - report.parentAgentId === projectSupervisorAgentId && - report.sessionId === supervisorSwarmSessionId && - report.parentRunId === state.initialRunId && - report.runtimeCount >= 1 && - report.busyRuntimeCount === 0 && - report.pendingTaskCount === 0 && - report.runningTaskCount === 0 && - report.waitingForConfirmationCount === 0 && - report.waitingForUserInputCount === 0 && - report.newAssistantMessageCount === 1 && - report.finalReplyChars > 0 && - report.reconciliationAgentCount === 0, - 'supervisor-autonomous-playable-turn-report-invalid', - ); - - const failureDisposition = supervisorAutonomousPlayableFailureDisposition( - persistence, - { - requireRecovered: true, - }, - ); - const parentTask = persistence.taskSnapshot.latest.find( - (candidate) => - candidate.agentId === projectSupervisorAgentId && - candidate.sessionId === supervisorSwarmSessionId && - candidate.runId === state.initialRunId, - ); - const parentRuntime = persistence.runtimeStates.find( - (candidate) => - candidate.agentId === projectSupervisorAgentId && - candidate.runId === state.initialRunId, - ); - assert( - parentTask?.task === task && - parentTask.source === mode.rootSource && - parentTask.status === 'completed' && - parentTask.phase === 'completed' && - parentTask.runProfile === autonomousGameBuildRunProfile && - parentRuntime?.sessionId === supervisorSwarmSessionId && - parentRuntime.phase === 'completed' && - parentRuntime.pendingToolAction == null && - parentRuntime.pendingAction == null && - parentRuntime.queuedSteerCount === 0 && - parentRuntime.runProfile === autonomousGameBuildRunProfile && - persistence.taskSnapshot.latest.every( - (candidate) => - !isLiveTask(candidate) && - !['waiting-for-user-input', 'waiting-for-confirmation'].includes( - candidate.status, - ), - ) && - persistence.runtimeStates.every( - (candidate) => - candidate.phase !== 'needs-reconciliation' && - ![ - 'pending', - 'running', - 'waiting-for-user-input', - 'waiting-for-confirmation', - ].includes(candidate.status), - ), - 'supervisor-autonomous-playable-runtime-not-terminal', - ); - - const supervisorUsers = persistence.supervisorConversation.filter( - (message) => message.role === 'user', - ); - const supervisorAssistants = persistence.supervisorConversation.filter( - (message) => message.role === 'assistant', - ); - const assistant = supervisorAssistants[0]; - const professionalMessages = persistence.professionalConversations.flatMap( - (entry) => entry.messages, - ); - const isolatedMessages = persistence.isolatedConversations.flatMap( - (entry) => entry.messages, - ); - const userFacing = [ - ...persistence.supervisorConversation, - ...persistence.legacyConversation, - ]; - const conversationBoundary = - inspectSupervisorAutonomousPlayableConversationBoundary( - persistence.legacyConversation, - userFacing, - state.initialRunId, - ); - const allMessages = [ - ...userFacing, - ...professionalMessages, - ...isolatedMessages, - ]; - const duplicateMessageCount = duplicateCount( - allMessages.map((message) => message.messageId).filter(Boolean), - ); - const parentAssistantAudits = persistence.agentDb.filter( - (record) => - record.recordType === 'conversation.message' && - record.role === 'assistant' && - record.agentId === projectSupervisorAgentId && - record.sessionId === supervisorSwarmSessionId && - record.messageId === assistant?.messageId, - ); - const parentCompletedAudits = persistence.agentDb.filter( - (record) => - record.recordType === 'agent.runtime.completed' && - record.agentId === projectSupervisorAgentId && - record.runId === state.initialRunId, - ); - assert( - supervisorUsers.length === 1 && - supervisorUsers[0].content === task && - supervisorAssistants.length === 1 && - assistant.agentId === projectSupervisorAgentId && - assistant.messageId === - finalMessageId( - projectSupervisorAgentId, - supervisorSwarmSessionId, - state.initialRunId, - ) && - conversationBoundary.legacyUserFacingMessageCount === 1 && - conversationBoundary.acceptedPublicStatusCount === 1 && - conversationBoundary.wrongAgentFinalAssistantCount === 0 && - parentAssistantAudits.length === 1 && - parentCompletedAudits.length === 1 && - duplicateMessageCount === 0, - 'supervisor-autonomous-playable-final-assistant-invalid', - ); - const finalization = validateSupervisorSwarmFinalization( - persistence.agentDb, - { - agentId: projectSupervisorAgentId, - taskId: parentTask.taskId, - sessionId: supervisorSwarmSessionId, - runId: state.initialRunId, - }, - assistant, - ); - - const actionRecords = persistence.agentDb.filter( - (record) => - isNonEmptyString(record.actionId) && - [ - 'agent.runtime.tool_action.executing', - 'agent.runtime.tool_action.observed', - 'agent.runtime.tool_observation', - 'agent.runtime.action_receipt', - ].includes(record.recordType), - ); - const actionReceipts = actionRecords.filter( - (record) => record.recordType === 'agent.runtime.action_receipt', - ); - const duplicateActionLifecycleCount = duplicateCount( - actionRecords.map(actionAuditIdentity), - ); - const duplicateReceiptCount = duplicateCount( - actionReceipts.map( - (record) => `${record.agentId}\0${record.runId}\0${record.actionId}`, - ), - ); - assert( - duplicateActionLifecycleCount === 0 && duplicateReceiptCount === 0, - 'supervisor-autonomous-playable-duplicate-action-or-receipt', - ); - - const runProfileBindings = await readSupervisorSwarmJsonDirectory( - '.agent/runtime/run-profile-bindings', - ); - const autonomousRunKeys = new Set( - runProfileBindings - .filter( - (candidate) => - candidate.profile === autonomousGameBuildRunProfile && - candidate.rootAgentId === projectSupervisorAgentId && - candidate.rootRunId === state.initialRunId, - ) - .map((candidate) => `${candidate.agentId}\0${candidate.runId}`), - ); - const sourcePayload = - summarizeSupervisorAutonomousPlayableSourcePayloadAudits( - persistence.agentDb.filter((record) => - autonomousRunKeys.has(`${record.agentId}\0${record.runId}`), - ), - ); - assert( - sourcePayload.acceptedAutonomousToolPlanCount > 0 && - sourcePayload.sourcePayloadPolicyViolationCount === 0, - 'supervisor-autonomous-playable-source-payload-policy-invalid', - ); - const matchingBindings = runProfileBindings.filter( - (binding) => - binding.agentId === projectSupervisorAgentId && - binding.runId === state.initialRunId, - ); - const binding = matchingBindings[0]; - const bindingIdentity = binding && { - schemaVersion: binding.schemaVersion, - projectId: binding.projectId, - agentId: binding.agentId, - runId: binding.runId, - rootAgentId: binding.rootAgentId, - rootRunId: binding.rootRunId, - parentAgentId: binding.parentAgentId, - parentRunId: binding.parentRunId, - source: binding.source, - profile: binding.profile, - profileFingerprint: binding.profileFingerprint, - parentBindingFingerprint: binding.parentBindingFingerprint, - boundAt: binding.boundAt, - }; - assert( - matchingBindings.length === 1 && - binding.schemaVersion === runProfileBindingSchemaVersion && - binding.rootAgentId === projectSupervisorAgentId && - binding.rootRunId === state.initialRunId && - binding.parentAgentId == null && - binding.parentRunId == null && - binding.source === mode.rootSource && - binding.profile === autonomousGameBuildRunProfile && - binding.profileFingerprint === - hashJsonValue({ - schemaVersion: runProfileBindingSchemaVersion, - profile: autonomousGameBuildRunProfile, - }) && - binding.bindingFingerprint === hashJsonValue(bindingIdentity), - 'supervisor-autonomous-playable-run-profile-binding-invalid', - ); - - const contracts = await readSupervisorSwarmJsonDirectory( - '.agent/runtime/autonomous-completion-contracts', - ); - const receipts = await readSupervisorSwarmJsonDirectory( - '.agent/runtime/autonomous-playtest-receipts', - ); - assert( - contracts.length === 1 && receipts.length === 1, - 'supervisor-autonomous-playable-autonomous-artifact-count-invalid', - ); - const contract = contracts[0]; - const receipt = receipts[0]; - const evidenceBinding = binding; - const evidenceRunId = state.initialRunId; - const contractIdentity = { - schemaVersion: contract.schemaVersion, - projectId: contract.projectId, - agentId: contract.agentId, - runId: contract.runId, - runProfileBindingFingerprint: contract.runProfileBindingFingerprint, - taskSha256: contract.taskSha256, - baselineRevision: contract.baselineRevision, - baselineIndexSha256: contract.baselineIndexSha256, - baselineArtifacts: contract.baselineArtifacts, - playtestScenario: contract.playtestScenario, - createdAt: contract.createdAt, - }; - assert( - contract.schemaVersion === autonomousCompletionContractSchemaVersion && - contract.projectId === binding.projectId && - contract.agentId === projectSupervisorAgentId && - contract.runId === state.initialRunId && - contract.runProfileBindingFingerprint === binding.bindingFingerprint && - contract.taskSha256 === hashValue(task) && - contract.baselineIndexSha256 === - state.supervisorAutonomousPlayable.initialGameIndexSha256 && - Array.isArray(contract.baselineArtifacts) && - contract.baselineArtifacts.length === 1 && - contract.baselineArtifacts[0]?.path === 'game/index.html' && - contract.baselineArtifacts[0]?.sha256 === - state.supervisorAutonomousPlayable.initialGameIndexSha256 && - Number.isInteger(contract.baselineArtifacts[0]?.sizeBytes) && - contract.baselineArtifacts[0].sizeBytes > 0 && - contract.playtestScenario === 'lane-defense-v1' && - contract.contractFingerprint === hashJsonValue(contractIdentity), - 'supervisor-autonomous-playable-completion-contract-invalid', - ); - - const revision = await readSupervisorSwarmProjectRevision(); - const finalGameIndex = await fs.readFile( - path.join(state.projectRoot, 'game/index.html'), - ); - const finalGameIndexSha256 = hashValue(finalGameIndex); - assert( - revision.schemaVersion === 'game-creator-project-revision.v1' && - revision.projectId === contract.projectId && - revision.revision > contract.baselineRevision && - finalGameIndexSha256 !== - state.supervisorAutonomousPlayable.initialGameIndexSha256 && - finalGameIndexSha256 !== contract.baselineIndexSha256, - 'supervisor-autonomous-playable-project-output-invalid', - ); - - const gates = ( - await readSupervisorSwarmJsonDirectory('.agent/runtime/verification') - ).filter( - (gate) => - gate.agentId === mode.evidenceAgentId && gate.runId === evidenceRunId, - ); - const gate = gates[0]; - const staticSmokeAudits = persistence.agentDb.filter( - (record) => - record.recordType === 'agent.runtime.command.run_limited' && - record.agentId === mode.evidenceAgentId && - record.runId === evidenceRunId && - record.commandId === 'game.static_smoke' && - record.status === 'completed', - ); - assert( - gates.length === 1 && - supervisorAutonomousPlayableStaticSmokeMatchesFinalIndex( - gate, - revision.revision, - finalGameIndexSha256, - ) && - staticSmokeAudits.length >= 1, - 'supervisor-autonomous-playable-static-smoke-invalid', - ); - - const receiptIdentity = { - schemaVersion: receipt.schemaVersion, - projectId: receipt.projectId, - agentId: receipt.agentId, - runId: receipt.runId, - runProfileBindingFingerprint: receipt.runProfileBindingFingerprint, - executorAgentId: receipt.executorAgentId, - executorRunId: receipt.executorRunId, - executorSource: receipt.executorSource, - executorRunProfileBindingFingerprint: - receipt.executorRunProfileBindingFingerprint, - actionId: receipt.actionId, - actionFingerprint: receipt.actionFingerprint, - revision: receipt.revision, - gameIndex: receipt.gameIndex, - playtestScenario: receipt.playtestScenario, - scenarioFingerprint: receipt.scenarioFingerprint, - report: receipt.report, - screenshots: receipt.screenshots, - createdAt: receipt.createdAt, - }; - assert( - receipt.schemaVersion === autonomousPlaytestReceiptSchemaVersion && - receipt.projectId === contract.projectId && - receipt.agentId === mode.evidenceAgentId && - receipt.runId === evidenceRunId && - receipt.runProfileBindingFingerprint === - evidenceBinding.bindingFingerprint && - receipt.revision === revision.revision && - receipt.gameIndex.path === 'game/index.html' && - receipt.gameIndex.sha256 === finalGameIndexSha256 && - receipt.gameIndex.sizeBytes === finalGameIndex.length && - receipt.playtestScenario === 'lane-defense-v1' && - receipt.receiptFingerprint === hashJsonValue(receiptIdentity) && - actionReceipts.filter( - (record) => - record.agentId === mode.evidenceAgentId && - record.runId === evidenceRunId && - record.actionId === receipt.actionId && - record.actionFingerprint === receipt.actionFingerprint && - record.tool === 'preview.validate' && - record.status === 'ok', - ).length === 1, - 'supervisor-autonomous-playable-playtest-receipt-invalid', - ); - - const readDigest = async (digest, expectedSuffix) => { - assert( - isNonEmptyString(digest?.path) && - !path.isAbsolute(digest.path) && - digest.path.endsWith(expectedSuffix), - 'supervisor-autonomous-playable-evidence-path-invalid', - ); - const file = path.resolve(state.projectRoot, digest.path); - assert( - isPathInside(state.projectRoot, file), - 'supervisor-autonomous-playable-evidence-path-escape', - ); - const metadata = await fs.lstat(file); - assert( - metadata.isFile() && !metadata.isSymbolicLink(), - 'supervisor-autonomous-playable-evidence-file-invalid', - ); - const bytes = await fs.readFile(file); - assert( - hashValue(bytes) === digest.sha256 && bytes.length === digest.sizeBytes, - 'supervisor-autonomous-playable-evidence-digest-invalid', - ); - return bytes; - }; - const reportBytes = await readDigest(receipt.report, '/validation.json'); - const browserReport = JSON.parse( - decodeUtf8Fatal( - reportBytes, - 'supervisor-autonomous-playable-browser-report-invalid-utf8', - ), - ); - const screenshotEntries = await Promise.all( - receipt.screenshots.map(async (digest) => ({ - digest, - bytes: await readDigest(digest, '.png'), - })), - ); - assert( - screenshotEntries.length === 2 && - screenshotEntries.every(({ bytes }) => - bytes.subarray(0, 8).equals(Buffer.from('\x89PNG\r\n\x1a\n', 'binary')), - ), - 'supervisor-autonomous-playable-screenshot-invalid', - ); - const desktop = screenshotEntries.find(({ digest }) => - digest.path.endsWith('/desktop.png'), - ); - const mobile = screenshotEntries.find(({ digest }) => - digest.path.endsWith('/mobile.png'), - ); - const desktopViewport = browserReport.viewportResults?.find( - (entry) => entry.viewport === 'desktop', - ); - const mobileViewport = browserReport.viewportResults?.find( - (entry) => entry.viewport === 'mobile', - ); - const playtest = browserReport.playtest; - const assertionNames = new Set( - (playtest?.assertions ?? []).map((entry) => entry.name), - ); - assert( - desktop && - mobile && - browserReport.schemaVersion === 'browser-validation.v1' && - browserReport.passed === true && - Array.isArray(browserReport.viewportResults) && - browserReport.viewportResults.length === 2 && - JSON.stringify( - browserReport.viewportResults.map((entry) => entry.viewport).sort(), - ) === JSON.stringify(['desktop', 'mobile']) && - desktopViewport?.passed === true && - mobileViewport?.passed === true && - playtest?.scenario === 'lane-defense-v1' && - playtest.passed === true && - playtest.scenarioFingerprint === receipt.scenarioFingerprint && - playtest.assertions.every((entry) => entry.passed === true) && - laneDefensePlaytestRequiredAssertions.every((name) => - assertionNames.has(name), - ), - 'supervisor-autonomous-playable-lane-defense-playtest-invalid', - ); - const lifecycle = persistence.agentDb.filter( - (record) => - record.recordType === 'agent.runtime.provider_request.lifecycle', - ); - const retryAudits = persistence.agentDb.filter( - (record) => record.recordType === 'agent.runtime.provider_request.retry', - ); - const lifecycleByRequest = new Map(); - for (const record of lifecycle) { - assert( - record.auditSchemaVersion === providerRequestLifecycleSchemaVersion && - isNonEmptyString(record.requestId), - 'supervisor-autonomous-playable-provider-lifecycle-invalid', - ); - const records = lifecycleByRequest.get(record.requestId) ?? []; - records.push(record); - lifecycleByRequest.set(record.requestId, records); - } - for (const records of lifecycleByRequest.values()) { - assert( - records.length === 2 && - records[0].status === 'started' && - ['completed', 'failed'].includes(records[1].status) && - [ - 'agentId', - 'taskId', - 'sessionId', - 'runId', - 'source', - 'requestKind', - 'requestSlot', - ].every((field) => records[0][field] === records[1][field]), - 'supervisor-autonomous-playable-provider-lifecycle-open-or-duplicate', - ); - } - assert( - lifecycleByRequest.size > 0 && - retryAudits.every( - (retry) => - lifecycleByRequest.get(retry.requestId)?.[1]?.status === 'failed' && - lifecycle.some( - (record) => - record.status === 'started' && - record.agentId === retry.agentId && - record.runId === retry.runId && - record.requestSlot === retry.nextRequestSlot, - ), - ), - 'supervisor-autonomous-playable-provider-retry-invalid', - ); - - const pendingActions = await findPendingActions(); - const reconciliationResidueCount = - supervisorAutonomousPlayableReconciliationResidueCount(persistence); - const steerRecordCount = - (await countSupervisorSwarmSteerRecords()) + - persistence.agentDb.filter((record) => - String(record.recordType ?? '').startsWith('agent.runtime.steer'), - ).length; - const approvalAuditCount = persistence.agentDb.filter( - (record) => - record.recordType === 'agent.runtime.tool_confirmation.approved', - ).length; - const rejectionAuditCount = persistence.agentDb.filter( - (record) => - record.recordType === 'agent.runtime.tool_confirmation.rejected', - ).length; - const confirmationAuditCount = persistence.agentDb.filter((record) => - String(record.recordType ?? '').includes('confirmation'), - ).length; - const userInputAuditCount = persistence.agentDb.filter((record) => - String(record.recordType ?? '').startsWith('agent.runtime.user_input.'), - ).length; - assert( - pendingActions.length === 0 && - Object.values(residualSidecars).every((count) => count === 0) && - reconciliationResidueCount === 0 && - approvalAuditCount === 0 && - rejectionAuditCount === 0 && - confirmationAuditCount === 0 && - userInputAuditCount === 0 && - steerRecordCount === 0 && - !/\[待确认\]|\[Needs input\]|输入 approve 或 reject|请选择 1-/u.test( - state.supervisorAutonomousPlayable.cliOutput, - ), - 'supervisor-autonomous-playable-intervention-or-residue-detected', - ); - - const publicLeaks = collectSupervisorSwarmPublicLeakEvidence(persistence, { - requireZero: true, - }); - const projectLureLeakCount = await countLureLeaks(); - const cliLureLeakCount = countExactSecrets( - Buffer.from(state.supervisorAutonomousPlayable.cliOutput), - state.lures, - ); - state.lureLeakCount = projectLureLeakCount + cliLureLeakCount; - assert(state.lureLeakCount === 0, 'sensitive-lure-leak-detected'); - state.supervisorAutonomousPlayable.privateValues = [ - ...new Set(state.supervisorSwarm.privateValues), - ]; - const absolutePaths = absolutePathVariants( - state.projectRoot, - state.options?.configDir, - state.isolatedRunner.appDataDir, - ); - let logApiKeyLeakCount = 0; - let logPrivateBodyLeakCount = 0; - let logAbsolutePathLeakCount = 0; - for (const file of await listFiles( - path.join(state.projectRoot, '.agent/logs'), - )) { - const metadata = await fs.lstat(file); - if (!metadata.isFile() || metadata.isSymbolicLink()) continue; - const bytes = await fs.readFile(file); - logApiKeyLeakCount += countExactSecrets(bytes, state.secrets); - logPrivateBodyLeakCount += countExactSecrets( - bytes, - state.supervisorAutonomousPlayable.privateValues, - ); - logAbsolutePathLeakCount += countExactSecrets(bytes, absolutePaths); - } - const browserReportApiKeyLeakCount = countExactSecrets( - reportBytes, - state.secrets, - ); - const browserReportPrivateBodyLeakCount = countExactSecrets( - reportBytes, - state.supervisorAutonomousPlayable.privateValues, - ); - const browserReportAbsolutePathLeakCount = countExactSecrets( - reportBytes, - absolutePaths, - ); - assert( - logApiKeyLeakCount === 0 && - logPrivateBodyLeakCount === 0 && - logAbsolutePathLeakCount === 0 && - browserReportApiKeyLeakCount === 0 && - browserReportPrivateBodyLeakCount === 0 && - browserReportAbsolutePathLeakCount === 0, - 'supervisor-autonomous-playable-log-or-report-leak-detected', - ); - const lifecycleProjection = buildSupervisorAutonomousPlayablePartialEvidence( - persistence, - { - residualSidecars, - pendingActionCount: pendingActions.length, - turnReport: report, - }, - ); - const providerBinding = - state.supervisorAutonomousPlayable.effectiveProviderBinding; - assert( - providerBinding && - JSON.stringify(providerBinding) === - JSON.stringify( - state.supervisorAutonomousPlayable.expectedProviderBinding, - ) && - isNonEmptyString(providerBinding.providerModel) && - isNonEmptyString(providerBinding.providerApiKind) && - isNonEmptyString(providerBinding.providerReasoningEffort) && - /^[0-9a-f]{64}$/u.test(providerBinding.providerBaseUrlSha256), - 'supervisor-autonomous-playable-provider-binding-invalid', - ); - - return { - ...emptySupervisorAutonomousPlayableEvidence(), - ...lifecycleProjection, - evidenceCompleteness: 'complete', - partialEvidenceCollected: false, - partialEvidenceReadErrorCount: 0, - partialPrivacyScanComplete: true, - stdinTaskCount: state.supervisorAutonomousPlayable.stdinWriteCount, - stdinEndedAfterTask: state.supervisorAutonomousPlayable.stdinEnded, - stdinBytes: state.supervisorAutonomousPlayable.stdinBytes, - taskSha256: hashValue(task), - providerAgentMode: providerBinding.providerAgentMode, - providerModel: providerBinding.providerModel, - providerApiKind: providerBinding.providerApiKind, - providerReasoningEffort: providerBinding.providerReasoningEffort, - providerBaseUrlSha256: providerBinding.providerBaseUrlSha256, - providerBoundAgentCount: providerBinding.boundAgentIds.length, - providerBindingMatched: true, - turnReportCaptured: true, - turnReportParentIdentityStable: true, - turnReportNewAssistantMessageCount: report.newAssistantMessageCount, - turnReportFinalReplyChars: report.finalReplyChars, - turnReportPrivateLeakCount: 0, - turnReportOutcome: report.outcome, - turnReportRuntimeCount: report.runtimeCount, - turnReportBusyRuntimeCount: report.busyRuntimeCount, - turnReportPendingTaskCount: report.pendingTaskCount, - turnReportRunningTaskCount: report.runningTaskCount, - turnReportWaitingForConfirmationCount: report.waitingForConfirmationCount, - turnReportWaitingForUserInputCount: report.waitingForUserInputCount, - turnReportReconciliationAgentCount: report.reconciliationAgentCount, - taskCount: persistence.taskSnapshot.latest.length, - terminalTaskCount: persistence.taskSnapshot.latest.length, - childTaskCount: persistence.taskSnapshot.latest.filter( - (candidate) => candidate.parentRunId === state.initialRunId, - ).length, - runtimeCount: persistence.runtimeStates.length, - childRuntimeCount: persistence.runtimeStates.filter( - (candidate) => candidate.parentRunId === state.initialRunId, - ).length, - recoveredSpecialistFailureCount: - failureDisposition.recoveredOriginalFailureCount, - recoveredOriginalFailureCount: - failureDisposition.recoveredOriginalFailureCount, - finalSupervisorAssistantCount: supervisorAssistants.length, - supervisorUserMessageCount: supervisorUsers.length, - professionalAssistantCount: professionalMessages.filter( - (message) => message.role === 'assistant', - ).length, - isolatedAssistantCount: isolatedMessages.filter( - (message) => message.role === 'assistant', - ).length, - professionalUserFacingAssistantCount: 0, - completedAuditCount: parentCompletedAudits.length, - finalizationStageCount: finalization.stageCount, - providerRequestIdentityCount: lifecycleByRequest.size, - providerLifecycleStartedCount: lifecycle.filter( - (record) => record.status === 'started', - ).length, - providerLifecycleTerminalCount: lifecycle.filter((record) => - ['completed', 'failed'].includes(record.status), - ).length, - providerLifecycleCompletedCount: lifecycle.filter( - (record) => record.status === 'completed', - ).length, - providerLifecycleFailedCount: lifecycle.filter( - (record) => record.status === 'failed', - ).length, - providerRetryAuditCount: retryAudits.length, - providerConnectivityRetryAuditCount: retryAudits.filter( - (record) => record.errorKind === 'connectivity', - ).length, - providerTransportRetryAuditCount: retryAudits.filter( - (record) => record.errorKind === 'transport', - ).length, - providerTimeoutRetryAuditCount: retryAudits.filter( - (record) => record.errorKind === 'timeout', - ).length, - ...sourcePayload, - openProviderLifecycleCount: 0, - duplicateProviderLifecycleCount: 0, - duplicateMessageCount, - duplicateActionLifecycleCount, - duplicateReceiptCount, - runProfileBindingCount: matchingBindings.length, - baselineRevision: contract.baselineRevision, - projectRevision: revision.revision, - projectRevisionDelta: revision.revision - contract.baselineRevision, - initialGameIndexSha256: - state.supervisorAutonomousPlayable.initialGameIndexSha256, - finalGameIndexSha256, - gameIndexChanged: true, - gameIndexBytes: finalGameIndex.length, - staticSmokePassed: true, - staticSmokeAuditCount: staticSmokeAudits.length, - autonomousCompletionContractCount: contracts.length, - autonomousPlaytestReceiptCount: receipts.length, - playtestScenario: playtest.scenario, - laneDefensePlaytestPassed: true, - laneDefenseAssertionCount: playtest.assertions.length, - laneDefensePassedAssertionCount: playtest.assertions.filter( - (entry) => entry.passed, - ).length, - browserValidationPassed: true, - desktopScreenshotSha256: desktop.digest.sha256, - desktopScreenshotBytes: desktop.bytes.length, - mobileScreenshotSha256: mobile.digest.sha256, - mobileScreenshotBytes: mobile.bytes.length, - browserReportSha256: receipt.report.sha256, - browserReportBytes: reportBytes.length, - pendingActionCount: pendingActions.length, - confirmationSidecarCount: residualSidecars.confirmations, - userInputSidecarCount: residualSidecars.userInput, - providerActionBatchSidecarCount: residualSidecars.providerActionBatches, - providerRetrySidecarCount: residualSidecars.providerRetries, - providerHandoffSidecarCount: residualSidecars.providerHandoffs, - toolPlanHandoffSidecarCount: residualSidecars.toolPlanHandoffs, - finalizationJournalCount: residualSidecars.finalizations, - reconciliationResidueCount, - approvalAuditCount, - rejectionAuditCount, - userInputAuditCount, - steerRecordCount, - providerPayloadPublicLeakCount: publicLeaks.providerPayloadPublicLeakCount, - privateBodyPublicLeakCount: publicLeaks.privateBodyPublicLeakCount, - apiKeyPublicLeakCount: publicLeaks.apiKeyPublicLeakCount, - projectPathPublicLeakCount: publicLeaks.projectPathPublicLeakCount, - formalConfigPathPublicLeakCount: - publicLeaks.formalConfigPathPublicLeakCount, - logApiKeyLeakCount, - logPrivateBodyLeakCount, - logAbsolutePathLeakCount, - browserReportApiKeyLeakCount, - browserReportPrivateBodyLeakCount, - browserReportAbsolutePathLeakCount, - paths: [ - receipt.gameIndex.path, - receipt.report.path, - desktop.digest.path, - mobile.digest.path, - ], - }; -} - -export async function runSupervisorAutonomousPlayableLaneDefenseE2e() { - await ensureOwnedRunnerStableKillSupport(); - const mode = supervisorAutonomousPlayableMode(); - state.supervisorAutonomousPlayable.freshInitBaselineUsed = false; - await seedDisposableProject({ preserveProductionInitBaseline: false }); - const initialGameIndex = await fs.readFile( - path.join(state.projectRoot, 'game/index.html'), - ); - state.supervisorAutonomousPlayable.initialGameIndexSha256 = - hashValue(initialGameIndex); - state.cliBinary = await prepareCliBinary(); - await prepareIsolatedSuiteAppData(); - state.isolatedRunner.launchAttempted = true; - - const stdin = buildSupervisorAutonomousPlayableStdin(); - const task = supervisorAutonomousPlayableLaneDefenseTask; - state.initialTask = { - chars: [...task].length, - sha256: hashValue(task), - }; - state.supervisorAutonomousPlayable.privateValues = [task]; - state.supervisorSwarm.userTask = task; - state.supervisorSwarm.privateValues = [task]; - state.supervisorAutonomousPlayableCliSession = startInteractiveCli([ - '--swarm-chat', - '--init', - '--autonomous-game-build', - ...mode.cliFlags, - state.projectRoot, - ]); - state.supervisorAutonomousPlayable.stdinWriteCount = 1; - state.supervisorAutonomousPlayable.stdinBytes = stdin.length; - state.supervisorAutonomousPlayable.stdinEnded = true; - state.supervisorAutonomousPlayableCliSession.child.stdin.end(stdin); - - const started = await waitForSupervisorAutonomousPlayableParentRuntime(task); - state.initialRunId = started.runId; - state.initialSessionId = started.sessionId; - await claimOwnedRunner(); - const report = await waitForSupervisorAutonomousPlayableCliExit( - state.supervisorAutonomousPlayableCliSession, - ); - assert( - report.outcome === 'settled', - 'supervisor-autonomous-playable-turn-not-settled', - ); - - const { persistence, residualSidecars } = - await waitForSupervisorAutonomousPlayableDurableQuiescence(); - state.identityStable = true; - state.evidence = await validateSupervisorAutonomousPlayableEvidence( - persistence, - residualSidecars, - ); - assert(state.evidence.secretLeakCount === 0, 'loaded-key-leak-detected'); -} - -export function emptySupervisorAutonomousPlayableEvidence() { - const mode = supervisorAutonomousPlayableMode(); - return { - scenario: mode.scenario, - targetAgentId: projectSupervisorAgentId, - runProfile: autonomousGameBuildRunProfile, - dedicatedZeroInterventionPath: true, - faultInjectionUsed: false, - activeRunnerKillCount: 0, - approveInputCount: 0, - answerInputCount: 0, - steerInputCount: 0, - stdinTaskCount: 0, - stdinEndedAfterTask: false, - stdinBytes: 0, - taskSha256: null, - providerAgentMode: null, - providerModel: null, - providerApiKind: null, - providerReasoningEffort: null, - providerBaseUrlSha256: null, - providerBoundAgentCount: 0, - providerBindingMatched: false, - evidenceCompleteness: 'none', - partialEvidenceCollected: false, - partialEvidenceReadErrorCount: 0, - partialPrivacyScanComplete: false, - rootRunObserved: false, - parentTaskStatus: null, - parentTaskPhase: null, - parentRuntimeStatus: null, - parentRuntimePhase: null, - childTaskStatusCounts: {}, - childTaskPhaseCounts: {}, - childRuntimeStatusCounts: {}, - childRuntimePhaseCounts: {}, - initialDeliveryStatusCounts: {}, - repairDeliveryStatusCounts: {}, - repairTerminalStatusCounts: {}, - failedOriginalChildCount: 0, - failedRepairChildCount: 0, - recoveredSpecialistFailureCount: 0, - recoveredOriginalFailureCount: 0, - awaitingRepairCount: 0, - isolatedAppDataUsed: false, - formalConfigCliCallCount: 0, - sourceRunnerEndpointUnchanged: false, - sourceAppDataDirectoryUntouched: false, - sourceConfigReplicaCount: 0, - sourceConfigReplicasVerified: false, - turnReportOutcome: null, - turnReportCaptured: false, - turnReportParentIdentityStable: false, - turnReportNewAssistantMessageCount: null, - turnReportFinalReplyChars: null, - turnReportPrivateLeakCount: null, - turnReportRuntimeCount: 0, - turnReportBusyRuntimeCount: 0, - turnReportPendingTaskCount: 0, - turnReportRunningTaskCount: 0, - turnReportWaitingForConfirmationCount: 0, - turnReportWaitingForUserInputCount: 0, - turnReportReconciliationAgentCount: 0, - taskCount: 0, - terminalTaskCount: 0, - childTaskCount: 0, - runtimeCount: 0, - childRuntimeCount: 0, - uniqueRootRunCount: 0, - recursiveDescendantCount: 0, - completedDescendantCount: 0, - artChildCount: 0, - artDeliveryClaimCount: 0, - assetListBeforeArtDelegation: false, - finalSupervisorAssistantCount: 0, - supervisorUserMessageCount: 0, - professionalAssistantCount: 0, - isolatedAssistantCount: 0, - professionalUserFacingAssistantCount: 0, - completedAuditCount: 0, - finalizationStageCount: 0, - providerRequestIdentityCount: 0, - providerLifecycleStartedCount: 0, - providerLifecycleTerminalCount: 0, - providerLifecycleCompletedCount: 0, - providerLifecycleFailedCount: 0, - providerRetryAuditCount: 0, - providerConnectivityRetryAuditCount: 0, - providerTransportRetryAuditCount: 0, - providerTimeoutRetryAuditCount: 0, - acceptedAutonomousToolPlanCount: 0, - sourceMutationActionMax: 0, - sourcePayloadMaxFieldChars: 0, - sourcePayloadMaxTotalChars: 0, - sourcePayloadPolicyViolationCount: 0, - openProviderLifecycleCount: 0, - duplicateProviderLifecycleCount: 0, - duplicateMessageCount: 0, - duplicateActionLifecycleCount: 0, - duplicateReceiptCount: 0, - runProfileBindingCount: 0, - baselineRevision: 0, - projectRevision: 0, - projectRevisionDelta: 0, - freshInitBaselineUsed: false, - initialGameIndexMatchesProductionDefault: false, - initialGameIndexSha256: null, - finalGameIndexSha256: null, - gameIndexChanged: false, - finalGameIndexDiffersFromBaseline: false, - uniqueFixedChildCodePrototype: false, - gameIndexBytes: 0, - staticSmokePassed: false, - staticSmokeGameIndexSha256Matched: false, - staticSmokeAuditCount: 0, - autonomousCompletionContractCount: 0, - autonomousPlaytestReceiptCount: 0, - playtestScenario: null, - laneDefensePlaytestPassed: false, - laneDefenseAssertionCount: 0, - laneDefensePassedAssertionCount: 0, - browserValidationPassed: false, - desktopPlaytestPassed: false, - mobilePlaytestPassed: false, - desktopScreenshotSha256: null, - desktopScreenshotBytes: 0, - mobileScreenshotSha256: null, - mobileScreenshotBytes: 0, - browserReportSha256: null, - browserReportBytes: 0, - pendingActionCount: 0, - confirmationSidecarCount: 0, - userInputSidecarCount: 0, - providerActionBatchSidecarCount: 0, - providerRetrySidecarCount: 0, - providerHandoffSidecarCount: 0, - toolPlanHandoffSidecarCount: 0, - finalizationJournalCount: 0, - reconciliationResidueCount: 0, - approvalAuditCount: 0, - rejectionAuditCount: 0, - userInputAuditCount: 0, - steerRecordCount: 0, - providerPayloadPublicLeakCount: 0, - privateBodyPublicLeakCount: 0, - apiKeyPublicLeakCount: 0, - projectPathPublicLeakCount: 0, - formalConfigPathPublicLeakCount: 0, - logApiKeyLeakCount: 0, - logPrivateBodyLeakCount: 0, - logAbsolutePathLeakCount: 0, - browserReportApiKeyLeakCount: 0, - browserReportPrivateBodyLeakCount: 0, - browserReportAbsolutePathLeakCount: 0, - supervisorAutonomousPlayableReportLeakCount: 0, - supervisorAutonomousPlayableRunnerStopped: false, - supervisorAutonomousPlayableAppDataCleanupPerformed: false, - ownedProcessIdentityCaptured: false, - ownedRunnerObservedCount: 0, - ownedHelperObservedCount: 0, - ownedNodeDescendantObservedCount: 0, - ownedBrowserDescendantObservedCount: 0, - ownedCommandDescendantObservedCount: 0, - ownedRunnerResidualCount: 0, - ownedHelperResidualCount: 0, - ownedNodeDescendantResidualCount: 0, - ownedBrowserDescendantResidualCount: 0, - ownedCommandDescendantResidualCount: 0, - activeCommandChildrenAfterCleanup: 0, - activeInteractiveCliSessionsAfterCleanup: 0, - ownedProcessCleanupPassed: false, - secretLeakCount: 0, - lureLeakCount: 0, - paths: [], - }; -} - -export function isSupervisorAutonomousPlayableLaneDefenseSuite() { - return state.suite === supervisorAutonomousPlayableLaneDefenseSuite; -} diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm.mjs index 65f2668b1..80f725314 100644 --- a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm.mjs +++ b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm.mjs @@ -1,4 +1,3 @@ -export * from './supervisor-swarm/chat-session.mjs'; export * from './supervisor-swarm/collaboration-assertions.mjs'; export * from './supervisor-swarm/collaboration-policy.mjs'; export * from './supervisor-swarm/evidence-schema.mjs'; diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/chat-session.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/chat-session.mjs deleted file mode 100644 index b968340a5..000000000 --- a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/chat-session.mjs +++ /dev/null @@ -1,108 +0,0 @@ -import { - assert, - codedError, - commandPassedMarker, - findPendingActions, - readRuntime, - safeProcessFailureDiagnostic, - state, - supervisorSwarmConfirmedTools, - supervisorSwarmDesignContent, - supervisorSwarmDesignPath, - supervisorSwarmQualityContent, - supervisorSwarmQualityPath, - supervisorSwarmWeakQualityContent, - visibleText, - waitForInteractiveCliOutput, - writeInteractiveCliLine, -} from './shared.mjs'; - -export function supervisorSwarmVerificationFixtureSource() { - return `import fs from 'node:fs';\nconst html = fs.readFileSync('game/index.html', 'utf8');\nconst agents = fs.readFileSync('AGENTS.md', 'utf8');\nconst optionalExact = (file, expected) => !fs.existsSync(file) || fs.readFileSync(file, 'utf8') === expected;\nconst passed = html.includes(${JSON.stringify(visibleText)}) && html.includes(' { - if (state.confirmedActionIds.has(pending.actionId)) return false; - if (!shouldConfirm(pending)) return false; - assert( - allowed.has(pending.tool), - `pending-tool-not-allowed-in-scenario:${pending.tool}`, - ); - return true; - }); - if (candidates.length === 0) return; - const output = await waitForInteractiveCliOutput( - session, - (value) => - candidates.some((pending) => - value.includes( - `[待确认] agent=${pending.agentId} run=${pending.runId} action=${pending.actionId} tool=${pending.tool}`, - ), - ), - 'supervisor-swarm-chat-confirmation-prompt-timeout', - 120_000, - ); - const pending = candidates.find((candidate) => - output.includes( - `[待确认] agent=${candidate.agentId} run=${candidate.runId} action=${candidate.actionId} tool=${candidate.tool}`, - ), - ); - assert(pending, 'supervisor-swarm-chat-prompted-action-missing'); - const runtime = await readRuntime(pending.agentId); - assert(runtime.runId === pending.runId, 'pending-run-mismatch'); - writeInteractiveCliLine(session, 'approve'); - await waitForInteractiveCliOutput( - session, - (output) => output.includes(`[已批准] ${pending.actionId}`), - 'supervisor-swarm-chat-confirmation-result-timeout', - 120_000, - ); - state.confirmedActionIds.add(pending.actionId); -} diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/collaboration-assertions.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/collaboration-assertions.mjs index 321937131..fae07caa3 100644 --- a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/collaboration-assertions.mjs +++ b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/collaboration-assertions.mjs @@ -1,35 +1,15 @@ -import { - assertSupervisorSwarmIsolatedChildBusinessContract, - supervisorSwarmIsolatedReviewGroupIndex, - supervisorSwarmIsolatedReviewsForGroupIndex, - supervisorSwarmIsolatedWriteScopeRoots, - validateSupervisorSwarmIsolatedSpawnInput, -} from './collaboration-policy.mjs'; import { supervisorSwarmDeliveryIdentity, supervisorSwarmEffectiveAgentPolicy, - supervisorSwarmIsolatedGroupIdentity, - supervisorSwarmIsolatedInstanceIdentity, - supervisorSwarmIsolatedJoinClaimIdentity, - supervisorSwarmIsolatedJoinIdentity, - supervisorSwarmIsolatedResultIdentity, supervisorSwarmParentIsolatedRecords, - supervisorSwarmTaskIdentity, } from './persistence.mjs'; import { assert, codedError, - hashJsonValue, hashValue, isFailedTask, isLiveTask, isNonEmptyString, - isolatedAgentGroupSchemaVersion, - isolatedAgentInstanceSchemaVersion, - isolatedAgentJoinClaimSchemaVersion, - isolatedAgentJoinDeliverySchemaVersion, - isolatedAgentResultSchemaVersion, - isolatedJoinDeliveryTarget, isPlainObject, projectSupervisorAgentId, state, @@ -37,18 +17,12 @@ import { staticDelegateDeliverySchemaVersion, supervisorSwarmDesignAgentId, supervisorSwarmDesignPath, - supervisorSwarmIsolatedReviews, supervisorSwarmProviderRetryAuditGraceSeconds, supervisorSwarmQualityAgentId, supervisorSwarmQualityPath, supervisorSwarmSessionId, supervisorSwarmWeakQualityContent, } from './shared.mjs'; -import { - isSupervisorSwarmMixedHarnessSuite, - isSupervisorSwarmMultiIsolatedHarnessSuite, - supervisorSwarmExpectedIsolatedReviewGroups, -} from './suite-selection.mjs'; export function supervisorSwarmProviderIntervals(agentDb, agentId, runId) { const records = agentDb @@ -113,46 +87,6 @@ export function observeSupervisorSwarmInitialProviderOverlap( return false; } -export function observeSupervisorSwarmStaticIsolatedProviderOverlap( - agentDb, - deliveries, - instances, -) { - if (!isSupervisorSwarmMixedHarnessSuite()) return false; - for (const delivery of deliveries) { - const staticIntervals = supervisorSwarmProviderIntervals( - agentDb, - delivery.targetAgentId, - delivery.targetRunId, - ); - for (const instance of instances) { - const isolatedIntervals = supervisorSwarmProviderIntervals( - agentDb, - instance.instanceId, - instance.runId, - ); - for (const staticInterval of staticIntervals) { - for (const isolatedInterval of isolatedIntervals) { - if ( - supervisorSwarmProviderIntervalsOverlap( - staticInterval, - isolatedInterval, - ) - ) { - state.supervisorSwarm.staticIsolatedProviderRequestIds = [ - staticInterval.started.record.requestId, - isolatedInterval.started.record.requestId, - ]; - state.supervisorSwarm.staticIsolatedProviderOverlapObserved = true; - return true; - } - } - } - } - } - return false; -} - export function supervisorSwarmParentDeliveries(deliveries) { return deliveries.filter( (delivery) => @@ -526,546 +460,6 @@ export function supervisorSwarmArtifactHash(delivery, expectedPath) { return artifact?.sha256 ?? null; } -export function supervisorSwarmMixedIsolatedGroupEntries( - groups, - expectedReviewGroups = supervisorSwarmExpectedIsolatedReviewGroups(), -) { - assert( - Array.isArray(groups) && groups.length === expectedReviewGroups.length, - 'supervisor-swarm-mixed-group-count-invalid', - ); - const entries = groups.map((group) => ({ - group, - groupIndex: supervisorSwarmIsolatedReviewGroupIndex( - group?.request?.children, - expectedReviewGroups, - ), - })); - assert( - entries.every(({ groupIndex }) => groupIndex >= 0) && - new Set(entries.map(({ groupIndex }) => groupIndex)).size === - expectedReviewGroups.length, - 'supervisor-swarm-mixed-group-review-partition-invalid', - ); - return entries.sort((left, right) => left.groupIndex - right.groupIndex); -} - -export function supervisorSwarmMixedSpawnRequestHashesStable(groups) { - const expectedReviewGroups = supervisorSwarmExpectedIsolatedReviewGroups(); - if (!Array.isArray(groups) || groups.length !== expectedReviewGroups.length) { - return false; - } - const entries = groups - .map((group) => ({ - group, - groupIndex: supervisorSwarmIsolatedReviewGroupIndex( - group?.request?.children, - expectedReviewGroups, - ), - })) - .sort((left, right) => left.groupIndex - right.groupIndex); - return ( - entries.every(({ groupIndex }) => groupIndex >= 0) && - new Set(entries.map(({ groupIndex }) => groupIndex)).size === - expectedReviewGroups.length && - hashJsonValue(entries[0].group.request) === - state.supervisorSwarm.mixedSpawnRequestHash && - (!isSupervisorSwarmMultiIsolatedHarnessSuite() || - hashJsonValue(entries[1].group.request) === - state.supervisorSwarm.mixedFollowupSpawnRequestHash) - ); -} - -export function supervisorSwarmReadyIsolatedGroupIds(detail) { - assert( - isNonEmptyString(detail), - 'supervisor-swarm-mixed-ready-join-detail-missing', - ); - const payloadBlocks = detail - .split('\n\n') - .filter((block) => block.startsWith('readyIsolatedJoins: ')); - assert( - payloadBlocks.length === 1, - 'supervisor-swarm-mixed-ready-join-prefix-count-invalid', - ); - let payload; - try { - payload = JSON.parse(payloadBlocks[0].slice('readyIsolatedJoins: '.length)); - } catch (error) { - throw codedError('supervisor-swarm-mixed-ready-join-json-invalid', error); - } - const groupIds = payload?.joins?.map((join) => join?.delegationGroupId); - assert( - payload?.ready === true && - Array.isArray(groupIds) && - groupIds.length > 0 && - groupIds.every(isNonEmptyString) && - new Set(groupIds).size === groupIds.length, - 'supervisor-swarm-mixed-ready-join-payload-invalid', - ); - return [...groupIds].sort(); -} - -export function supervisorSwarmObservedJoinClaimForGroups( - claims, - expectedGroupIds, -) { - assert( - Array.isArray(claims) && - claims.length === 1 && - Array.isArray(expectedGroupIds) && - expectedGroupIds.length > 0, - 'supervisor-swarm-mixed-observed-join-claim-count-invalid', - ); - const claim = claims[0]; - const claimedGroupIds = (claim?.joins ?? []) - .map((join) => join?.delegationGroupId) - .filter(isNonEmptyString) - .sort(); - assert( - claim?.schemaVersion === isolatedAgentJoinClaimSchemaVersion && - claim.status === 'observed' && - isNonEmptyString(claim.actionId) && - Array.isArray(claim.joins) && - claimedGroupIds.length === claim.joins.length && - new Set(claimedGroupIds).size === claimedGroupIds.length && - JSON.stringify(claimedGroupIds) === JSON.stringify(expectedGroupIds), - 'supervisor-swarm-mixed-observed-join-claim-invalid', - ); - return claim; -} - -export function supervisorSwarmFollowupBeforeClaimOrderValid( - initialParentWakeIndex, - followupSpawnIndex, - claimAuditIndexes, -) { - return ( - Number.isSafeInteger(initialParentWakeIndex) && - Number.isSafeInteger(followupSpawnIndex) && - Array.isArray(claimAuditIndexes) && - claimAuditIndexes.length > 0 && - claimAuditIndexes.every(Number.isSafeInteger) && - initialParentWakeIndex >= 0 && - initialParentWakeIndex < followupSpawnIndex && - followupSpawnIndex < Math.min(...claimAuditIndexes) - ); -} - -export function supervisorSwarmMixedIsolatedClaimsReady(persistence) { - const records = supervisorSwarmParentIsolatedRecords(persistence); - const expectedGroupCount = - supervisorSwarmExpectedIsolatedReviewGroups().length; - return ( - records.groups.length === expectedGroupCount && - records.instances.length === supervisorSwarmIsolatedReviews.length && - records.results.length === supervisorSwarmIsolatedReviews.length && - records.results.every((record) => record.result?.status === 'completed') && - records.joinDeliveries.length === expectedGroupCount && - records.joinDeliveries.every( - (delivery) => delivery.status === 'claimed-by-parent', - ) - ); -} - -export function validateSupervisorSwarmMixedIsolatedPersistence(persistence) { - assert( - isSupervisorSwarmMixedHarnessSuite(), - 'supervisor-swarm-mixed-validation-outside-suite', - ); - const { groups, instances, results, joinDeliveries } = - supervisorSwarmParentIsolatedRecords(persistence); - const groupEntries = supervisorSwarmMixedIsolatedGroupEntries(groups); - assert( - persistence.isolatedGroups.length === groupEntries.length && - isNonEmptyString(state.supervisorSwarm.mixedSpawnActionId) && - (!isSupervisorSwarmMultiIsolatedHarnessSuite() || - (isNonEmptyString(state.supervisorSwarm.mixedFollowupSpawnActionId) && - state.supervisorSwarm.mixedSpawnActionId !== - state.supervisorSwarm.mixedFollowupSpawnActionId)), - 'supervisor-swarm-mixed-group-invalid', - ); - for (const { group, groupIndex } of groupEntries) { - const expectedReviewGroups = supervisorSwarmExpectedIsolatedReviewGroups(); - const reviews = supervisorSwarmIsolatedReviewsForGroupIndex( - groupIndex, - expectedReviewGroups, - ); - const expectedActionId = - groupIndex === 0 - ? state.supervisorSwarm.mixedSpawnActionId - : state.supervisorSwarm.mixedFollowupSpawnActionId; - const expectedRequestHash = - groupIndex === 0 - ? state.supervisorSwarm.mixedSpawnRequestHash - : state.supervisorSwarm.mixedFollowupSpawnRequestHash; - assert( - group.schemaVersion === isolatedAgentGroupSchemaVersion && - group.parentAgentId === projectSupervisorAgentId && - group.parentSessionId === supervisorSwarmSessionId && - group.parentRunId === state.initialRunId && - group.parentActionId === expectedActionId && - group.joinMode === 'all' && - group.depth === 1 && - Array.isArray(group.instanceIds) && - group.instanceIds.length === reviews.length && - hashJsonValue(group.request) === expectedRequestHash, - 'supervisor-swarm-mixed-group-contract-invalid', - ); - validateSupervisorSwarmIsolatedSpawnInput( - group.request, - groupIndex, - 'supervisor-swarm-mixed-persisted', - expectedReviewGroups, - ); - } - supervisorSwarmIsolatedWriteScopeRoots( - groupEntries.flatMap(({ group }) => group.request.children), - ); - const groupById = new Map( - groupEntries.map((entry) => [entry.group.delegationGroupId, entry]), - ); - const expectedByPath = new Map( - supervisorSwarmIsolatedReviews.map((review) => [review.path, review]), - ); - assert( - instances.length === supervisorSwarmIsolatedReviews.length && - persistence.isolatedInstances.length === instances.length && - new Set(instances.map((instance) => instance.instanceId)).size === - instances.length && - new Set(instances.map((instance) => instance.delegationId)).size === - instances.length && - new Set(instances.map((instance) => instance.sessionId)).size === - instances.length && - new Set(instances.map((instance) => instance.runId)).size === - instances.length, - 'supervisor-swarm-mixed-instance-cardinality-invalid', - ); - for (const { group, groupIndex } of groupEntries) { - const groupInstances = instances.filter( - (instance) => instance.delegationGroupId === group.delegationGroupId, - ); - const expectedIndexes = supervisorSwarmIsolatedReviewsForGroupIndex( - groupIndex, - supervisorSwarmExpectedIsolatedReviewGroups(), - ).map((_, index) => index); - assert( - groupInstances.length === expectedIndexes.length && - JSON.stringify( - groupInstances - .map((instance) => instance.childIndex) - .sort((left, right) => left - right), - ) === JSON.stringify(expectedIndexes) && - JSON.stringify( - groupInstances.map((instance) => instance.instanceId).sort(), - ) === JSON.stringify([...group.instanceIds].sort()), - 'supervisor-swarm-mixed-group-instance-set-invalid', - ); - } - for (const instance of instances) { - const groupEntry = groupById.get(instance.delegationGroupId); - const group = groupEntry?.group; - const expectedPath = instance.expectedArtifacts?.[0]; - const review = expectedByPath.get(expectedPath); - const requestChild = group?.request?.children?.[instance.childIndex]; - assert( - groupEntry && - review && - instance.schemaVersion === isolatedAgentInstanceSchemaVersion && - instance.parentAgentId === projectSupervisorAgentId && - instance.parentSessionId === supervisorSwarmSessionId && - instance.parentRunId === state.initialRunId && - instance.parentActionId === group.parentActionId && - instance.delegationGroupId === group.delegationGroupId && - group.instanceIds.includes(instance.instanceId) && - isNonEmptyString(instance.templateAgentId) && - isNonEmptyString(instance.task) && - instance.depth === 1 && - JSON.stringify(instance.acceptanceCriteria) === - JSON.stringify(requestChild?.acceptanceCriteria) && - JSON.stringify(instance.expectedArtifacts) === - JSON.stringify([review.path]) && - JSON.stringify(requestChild?.expectedArtifacts) === - JSON.stringify(instance.expectedArtifacts) && - JSON.stringify(requestChild?.writeScopes) === - JSON.stringify(instance.writeScopes) && - requestChild?.templateAgentId === instance.templateAgentId && - requestChild?.task === instance.task, - 'supervisor-swarm-mixed-instance-contract-invalid', - ); - assertSupervisorSwarmIsolatedChildBusinessContract(requestChild, review); - } - assert( - results.length === instances.length && - persistence.isolatedResults.length === results.length && - new Set(results.map((record) => record.result?.instanceId)).size === - results.length, - 'supervisor-swarm-mixed-result-cardinality-invalid', - ); - for (const record of results) { - const instance = instances.find( - (candidate) => candidate.instanceId === record.result?.instanceId, - ); - const groupEntry = groupById.get(record.delegationGroupId); - const review = expectedByPath.get(instance?.expectedArtifacts?.[0]); - const artifact = record.result?.artifacts?.find( - (candidate) => candidate.path === review?.path, - ); - assert( - instance && - groupEntry && - review && - record.schemaVersion === isolatedAgentResultSchemaVersion && - record.delegationGroupId === instance.delegationGroupId && - record.childIndex === instance.childIndex && - record.result.delegationId === instance.delegationId && - record.result.templateAgentId === instance.templateAgentId && - record.result.runId === instance.runId && - record.result.status === 'completed' && - isNonEmptyString(record.result.summary) && - Array.isArray(record.result.artifacts) && - JSON.stringify(record.result.artifacts) === - JSON.stringify([ - { path: review.path, sha256: hashValue(review.content) }, - ]) && - artifact?.sha256 === hashValue(review.content) && - Array.isArray(record.result.evidence) && - (record.result.verifiedRevision == null || - (Number.isSafeInteger(record.result.verifiedRevision) && - record.result.verifiedRevision >= 0)) && - record.result.error == null, - 'supervisor-swarm-mixed-result-invalid', - ); - } - assert( - joinDeliveries.length === groupEntries.length && - persistence.isolatedJoinDeliveries.length === groupEntries.length, - 'supervisor-swarm-mixed-join-delivery-invalid', - ); - const joinDeliveryByGroupId = new Map( - joinDeliveries.map((delivery) => [delivery.delegationGroupId, delivery]), - ); - for (const { group } of groupEntries) { - const delivery = joinDeliveryByGroupId.get(group.delegationGroupId); - assert( - delivery?.schemaVersion === isolatedAgentJoinDeliverySchemaVersion && - delivery.parentAgentId === projectSupervisorAgentId && - delivery.parentRunId === state.initialRunId && - delivery.joinRunId === group.joinRunId && - delivery.status === 'claimed-by-parent' && - isolatedJoinDeliveryTarget(delivery) === 'parent-wake' && - delivery.queuedRunId == null && - isNonEmptyString(delivery.claimedByActionId), - 'supervisor-swarm-mixed-group-join-delivery-invalid', - ); - } - const isolatedTasks = persistence.taskSnapshot.latest.filter((task) => - instances.some( - (instance) => - task.agentId === instance.instanceId && - task.sessionId === instance.sessionId && - task.runId === instance.runId && - task.delegationId === instance.delegationId, - ), - ); - const joinRunIds = new Set(groupEntries.map(({ group }) => group.joinRunId)); - const continuationTasks = persistence.taskSnapshot.all.filter( - (task) => - task.source === 'agent-isolated-join' && joinRunIds.has(task.runId), - ); - assert( - isolatedTasks.length === instances.length && - isolatedTasks.every( - (task) => - task.source === 'agent-isolated-child' && - task.parentAgentId === projectSupervisorAgentId && - task.parentRunId === state.initialRunId && - task.status === 'completed' && - task.phase === 'completed', - ) && - continuationTasks.length === 0, - 'supervisor-swarm-mixed-child-or-continuation-task-invalid', - ); - const spawnAudits = persistence.agentDb.filter( - (record) => - record.recordType === 'agent.runtime.agent.spawn_isolated' && - record.agentId === projectSupervisorAgentId && - record.sessionId === supervisorSwarmSessionId && - record.runId === state.initialRunId && - groupById.has(record.delegationGroupId), - ); - const parentWakeAudits = persistence.agentDb.filter( - (record) => - record.recordType === - 'agent.runtime.agent.isolated_join.parent_wake.dispatched' && - record.agentId === projectSupervisorAgentId && - record.sessionId === supervisorSwarmSessionId && - record.parentRunId === state.initialRunId && - groupById.has(record.delegationGroupId), - ); - const continuationAudits = persistence.agentDb.filter( - (record) => - record.recordType === 'agent.runtime.agent.isolated_join.dispatched' && - record.parentRunId === state.initialRunId && - groupById.has(record.delegationGroupId), - ); - const claimAudits = persistence.agentDb - .map((record, index) => ({ record, index })) - .filter( - ({ record }) => - record.recordType === - 'agent.runtime.agent.isolated_join.claimed_by_parent' && - record.agentId === projectSupervisorAgentId && - record.runId === state.initialRunId && - groupById.has(record.delegationGroupId), - ); - const claimObservations = persistence.agentDb - .map((record, index) => ({ record, index })) - .filter( - ({ record }) => - record.recordType === 'agent.runtime.tool_observation' && - record.agentId === projectSupervisorAgentId && - record.runId === state.initialRunId && - record.tool === 'agent.run_status' && - record.status === 'ok' && - String(record.summary ?? '').includes('ready all-join'), - ); - const expectedGroupIds = [...groupById.keys()].sort(); - const joinClaims = (persistence.isolatedJoinClaims ?? []).filter( - (claim) => - claim.parentAgentId === projectSupervisorAgentId && - claim.parentRunId === state.initialRunId, - ); - const joinClaim = supervisorSwarmObservedJoinClaimForGroups( - joinClaims, - expectedGroupIds, - ); - assert( - spawnAudits.length === groupEntries.length && - parentWakeAudits.length === groupEntries.length && - continuationAudits.length === 0 && - claimAudits.length === groupEntries.length && - claimObservations.length === 1 && - persistence.isolatedJoinClaims.length === 1 && - joinClaim.actionId === claimObservations[0].record.actionId && - joinClaim.joins.length === groupEntries.length && - String(claimObservations[0].record.summary ?? '').includes( - `${groupEntries.length} 个 ready all-join`, - ) && - JSON.stringify( - spawnAudits.map((record) => record.delegationGroupId).sort(), - ) === JSON.stringify(expectedGroupIds) && - JSON.stringify( - parentWakeAudits.map((record) => record.delegationGroupId).sort(), - ) === JSON.stringify(expectedGroupIds) && - JSON.stringify( - claimAudits.map(({ record }) => record.delegationGroupId).sort(), - ) === JSON.stringify(expectedGroupIds) && - claimAudits.every( - ({ record, index }) => - record.actionId === claimObservations[0].record.actionId && - joinDeliveryByGroupId.get(record.delegationGroupId) - ?.claimedByActionId === record.actionId && - index < claimObservations[0].index, - ), - 'supervisor-swarm-mixed-spawn-or-claim-audit-invalid', - ); - if (isSupervisorSwarmMultiIsolatedHarnessSuite()) { - const initialGroupId = groupEntries[0].group.delegationGroupId; - const followupGroupId = groupEntries[1].group.delegationGroupId; - const initialParentWakeIndex = persistence.agentDb.findIndex( - (record) => - record.recordType === - 'agent.runtime.agent.isolated_join.parent_wake.dispatched' && - record.delegationGroupId === initialGroupId, - ); - const followupSpawnIndex = persistence.agentDb.findIndex( - (record) => - record.recordType === 'agent.runtime.agent.spawn_isolated' && - record.delegationGroupId === followupGroupId, - ); - assert( - supervisorSwarmFollowupBeforeClaimOrderValid( - initialParentWakeIndex, - followupSpawnIndex, - claimAudits.map(({ index }) => index), - ), - 'supervisor-swarm-mixed-followup-before-claim-order-invalid', - ); - } - for (const { group } of groupEntries) { - assert( - spawnAudits.filter( - (record) => - record.actionId === group.parentActionId && - record.delegationGroupId === group.delegationGroupId && - record.joinRunId === group.joinRunId && - Array.isArray(record.children) && - record.children.length === group.instanceIds.length, - ).length === 1 && - parentWakeAudits.filter( - (record) => - record.parentActionId === group.parentActionId && - record.delegationGroupId === group.delegationGroupId && - record.joinRunId === group.joinRunId, - ).length === 1 && - claimAudits.filter( - ({ record }) => - record.parentActionId === group.parentActionId && - record.delegationGroupId === group.delegationGroupId && - record.joinRunId === group.joinRunId, - ).length === 1 && - joinClaim.joins.filter( - (join) => - join.parentActionId === group.parentActionId && - join.delegationGroupId === group.delegationGroupId && - join.joinRunId === group.joinRunId && - join.parentAgentId === projectSupervisorAgentId && - join.parentSessionId === supervisorSwarmSessionId && - join.parentRunId === state.initialRunId && - join.source === 'agent-isolated-join', - ).length === 1, - 'supervisor-swarm-mixed-group-audit-identity-invalid', - ); - } - return { - groups: groupEntries.map(({ group }) => group), - instances, - results, - joinDeliveries: groupEntries.map(({ group }) => - joinDeliveryByGroupId.get(group.delegationGroupId), - ), - joinClaims, - isolatedTasks, - continuationTasks, - continuationAudits, - claimAuditIndex: Math.max(...claimAudits.map(({ index }) => index)), - claimObservationIndex: claimObservations[0].index, - identity: { - groups: groupEntries - .map(({ group }) => supervisorSwarmIsolatedGroupIdentity(group)) - .sort((left, right) => - left.delegationGroupId.localeCompare(right.delegationGroupId), - ), - instances: instances - .map(supervisorSwarmIsolatedInstanceIdentity) - .sort((left, right) => left.instanceId.localeCompare(right.instanceId)), - results: results - .map(supervisorSwarmIsolatedResultIdentity) - .sort((left, right) => left.instanceId.localeCompare(right.instanceId)), - joinDeliveries: joinDeliveries - .map(supervisorSwarmIsolatedJoinIdentity) - .sort((left, right) => - left.delegationGroupId.localeCompare(right.delegationGroupId), - ), - joinClaims: joinClaims.map(supervisorSwarmIsolatedJoinClaimIdentity), - tasks: isolatedTasks - .map(supervisorSwarmTaskIdentity) - .sort((left, right) => left.agentId.localeCompare(right.agentId)), - }, - }; -} - export function assertSupervisorSwarmWeakQualityDelivery(delivery) { assert( delivery.status === 'claimed-by-parent' && diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/collaboration-policy.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/collaboration-policy.mjs index 4a39435e1..fd13dbbdd 100644 --- a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/collaboration-policy.mjs +++ b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/collaboration-policy.mjs @@ -1,45 +1,23 @@ import { assertSupervisorSwarmProviderFailureRecoverable } from './collaboration-assertions.mjs'; -import { - readSupervisorSwarmPersistence, - supervisorSwarmParentIsolatedRecords, -} from './persistence.mjs'; -import { - confirmSupervisorSwarmPendingActions, - readSupervisorSwarmExecutionOwner, - supervisorSwarmProviderStartedIdentities, -} from './repair-recovery.mjs'; -import { readSupervisorSwarmProjectRevision } from './runtime-setup.mjs'; -import { - expectedSupervisorSwarmCollaborationPolicy, - writeSupervisorSwarmCollaborationPolicyDriftFixture, -} from './setup.mjs'; +import { expectedSupervisorSwarmCollaborationPolicy } from './setup.mjs'; import { assert, canonicalJsonValue, - claimOwnedRunner, codedError, - configFileName, duplicateCount, fs, - gitSensitivePath, hasExactKeys, hashJsonValue, hashValue, isFailedTask, isNonEmptyString, isPlainObject, - killRunnerOnce, - listFiles, - localConfigFileName, path, projectSupervisorAgentId, providerActionBatchSchemaVersion, readJson, readOptionalJsonl, readTaskSnapshot, - runCli, - runProcess, - sentinelFileName, sleep, state, supervisorCollaborationContractSchemaVersion, @@ -49,23 +27,11 @@ import { supervisorCollaborationPolicySnapshotSchemaVersion, supervisorSwarmDesignAgentId, supervisorSwarmDesignPath, - supervisorSwarmIsolatedReviewGroups, - supervisorSwarmProjectMutationTools, + supervisorSwarmIsolatedReviews, supervisorSwarmQualityAgentId, supervisorSwarmQualityPath, supervisorSwarmSessionId, - verifyOwnedRunnerForKill, - waitForRunnerBootChange, } from './shared.mjs'; -import { - isSupervisorSwarmAutonomousChatSuite, - isSupervisorSwarmCollaborationPolicyMixedRecoverySuite, - isSupervisorSwarmInteractiveChatSuite, - isSupervisorSwarmMixedHarnessSuite, - isSupervisorSwarmMultiIsolatedHarnessSuite, - supervisorSwarmExpectedIsolatedReviewGroups, - supervisorSwarmInitialIsolatedReviewsForSuite, -} from './suite-selection.mjs'; export function assertSupervisorSwarmContractShape( input, @@ -145,7 +111,7 @@ export function supervisorSwarmIsolatedWriteScopeRoots(children) { export function supervisorSwarmIsolatedReviewGroupIndex( children, - reviewGroups = supervisorSwarmIsolatedReviewGroups, + reviewGroups = [supervisorSwarmIsolatedReviews], ) { if (!Array.isArray(children) || children.length === 0) return -1; const actualPaths = children @@ -162,7 +128,7 @@ export function supervisorSwarmIsolatedReviewGroupIndex( export function supervisorSwarmIsolatedReviewsForGroupIndex( groupIndex, - reviewGroups = supervisorSwarmIsolatedReviewGroups, + reviewGroups = [supervisorSwarmIsolatedReviews], ) { const reviews = reviewGroups[groupIndex]; assert( @@ -192,7 +158,7 @@ export function validateSupervisorSwarmIsolatedSpawnInput( input, groupIndex, codePrefix, - reviewGroups = supervisorSwarmIsolatedReviewGroups, + reviewGroups = [supervisorSwarmIsolatedReviews], ) { const reviews = supervisorSwarmIsolatedReviewsForGroupIndex( groupIndex, @@ -467,7 +433,7 @@ export function supervisorSwarmCollaborationPolicyDriftObservationCount( ).length; } -export function validateSupervisorSwarmCollaborationContract(contract, mixed) { +export function validateSupervisorSwarmCollaborationContract(contract) { const expectedPolicy = expectedSupervisorSwarmCollaborationPolicy(); const expectedStaticAgentIds = [ supervisorSwarmDesignAgentId, @@ -494,9 +460,8 @@ export function validateSupervisorSwarmCollaborationContract(contract, mixed) { JSON.stringify(contract.initialStaticAgentIds) === JSON.stringify(expectedStaticAgentIds) && contract.repairDelegateCount === 0 && - contract.isolatedSpawnCount === (mixed ? 1 : 0) && - contract.isolatedChildCount === - (mixed ? supervisorSwarmInitialIsolatedReviewsForSuite().length : 0) && + contract.isolatedSpawnCount === 0 && + contract.isolatedChildCount === 0 && /^[0-9a-f]{64}$/u.test(contract.policyFingerprint ?? '') && /^[0-9a-f]{64}$/u.test(contract.contractFingerprint ?? ''), 'supervisor-swarm-initial-collaboration-contract-invalid', @@ -563,8 +528,7 @@ export function supervisorSwarmInitialProviderBatchRecoveryIdentity(batch) { } export function validateSupervisorSwarmInitialProviderBatch(batch) { - const mixed = isSupervisorSwarmMixedHarnessSuite(); - const expectedActionCount = mixed ? 3 : 2; + const expectedActionCount = 2; assert( batch?.schemaVersion === providerActionBatchSchemaVersion && isNonEmptyString(batch.batchId) && @@ -587,22 +551,12 @@ export function validateSupervisorSwarmInitialProviderBatch(batch) { ); const collaborationContract = validateSupervisorSwarmCollaborationContract( batch.collaborationContract, - mixed, ); - if (mixed) { - assert( - batch.status === 'waiting-confirmation' && batch.nextActionIndex === 0, - 'supervisor-swarm-mixed-initial-confirmation-gate-missing', - ); - } const agentIds = new Set(); const actionIds = new Set(); const delegateActionIds = new Set(); - let spawnActionId = null; for (const [index, pending] of batch.actions.entries()) { const action = pending.action; - const confirmationExpected = - mixed && action?.tool === 'agent.spawn_isolated'; assert( pending.agentId === projectSupervisorAgentId && pending.sessionId === supervisorSwarmSessionId && @@ -611,74 +565,39 @@ export function validateSupervisorSwarmInitialProviderBatch(batch) { isNonEmptyString(pending.actionId) && /^[0-9a-f]{64}$/u.test(pending.actionFingerprint ?? '') && !actionIds.has(pending.actionId) && - pending.executionMode === - (confirmationExpected ? 'confirmation' : 'auto') && - (confirmationExpected - ? [ - 'pending-confirmation', - 'approved', - 'executing', - 'observed-approved', - ].includes(pending.status) - : ['approved', 'executing', 'observed-approved'].includes( - pending.status, - )) && + pending.executionMode === 'auto' && + ['approved', 'executing', 'observed-approved'].includes( + pending.status, + ) && + action?.tool === 'agent.delegate' && action?.tool === batch.plan.actions[index]?.tool && JSON.stringify(action.input) === JSON.stringify(batch.plan.actions[index].input), 'supervisor-swarm-initial-provider-batch-action-invalid', ); - if (action.tool === 'agent.delegate') { - const expectedPath = - action.input?.agentId === supervisorSwarmDesignAgentId - ? supervisorSwarmDesignPath - : supervisorSwarmQualityPath; - assertSupervisorSwarmContractShape( - action.input, - expectedPath, - 'supervisor-swarm-initial-provider-batch', - ); - assert( - [supervisorSwarmDesignAgentId, supervisorSwarmQualityAgentId].includes( - action.input.agentId, - ), - 'supervisor-swarm-initial-provider-batch-target-invalid', - ); - agentIds.add(action.input.agentId); - delegateActionIds.add(pending.actionId); - } else { - assert( - mixed && - action.tool === 'agent.spawn_isolated' && - spawnActionId == null, - 'supervisor-swarm-mixed-spawn-action-invalid', - ); - spawnActionId = pending.actionId; - state.supervisorSwarm.mixedSpawnRequestHash = - validateSupervisorSwarmIsolatedSpawnInput( - action.input, - 0, - 'supervisor-swarm-mixed-initial', - supervisorSwarmExpectedIsolatedReviewGroups(), - ); - } + const expectedPath = + action.input?.agentId === supervisorSwarmDesignAgentId + ? supervisorSwarmDesignPath + : supervisorSwarmQualityPath; + assertSupervisorSwarmContractShape( + action.input, + expectedPath, + 'supervisor-swarm-initial-provider-batch', + ); + assert( + [supervisorSwarmDesignAgentId, supervisorSwarmQualityAgentId].includes( + action.input.agentId, + ), + 'supervisor-swarm-initial-provider-batch-target-invalid', + ); + agentIds.add(action.input.agentId); + delegateActionIds.add(pending.actionId); actionIds.add(pending.actionId); } assert( - agentIds.size === 2 && (!mixed || isNonEmptyString(spawnActionId)), + agentIds.size === 2, 'supervisor-swarm-initial-provider-batch-target-count-invalid', ); - assert( - !mixed || - batch.actions.filter( - (pending) => - pending.actionId === spawnActionId && - pending.executionMode === 'confirmation' && - pending.status === 'pending-confirmation', - ).length === 1, - 'supervisor-swarm-initial-provider-batch-waiting-state-invalid', - ); - state.supervisorSwarm.mixedSpawnActionId = spawnActionId; state.supervisorSwarm.initialProviderBatch = { batchId: batch.batchId, projectId: batch.projectId, @@ -829,88 +748,6 @@ export async function readSupervisorSwarmCollaborationPolicySnapshotBindingFile( return { bindingPath, bytes, binding }; } -export async function captureSupervisorSwarmCollaborationPolicySnapshotAndDrift() { - if (!isSupervisorSwarmMultiIsolatedHarnessSuite()) return; - const expectedSnapshotPath = supervisorSwarmCollaborationPolicySnapshotPath(); - const expectedBindingPath = - supervisorSwarmCollaborationPolicySnapshotBindingPath(); - const deadline = Date.now() + 30_000; - while (Date.now() < deadline) { - const [snapshotFiles, bindingFiles] = await Promise.all([ - listFiles(supervisorSwarmCollaborationPolicySnapshotDirectory()), - listFiles(supervisorSwarmCollaborationPolicySnapshotBindingDirectory()), - ]); - if ( - !snapshotFiles.includes(expectedSnapshotPath) || - !bindingFiles.includes(expectedBindingPath) - ) { - await sleep(50); - continue; - } - assert( - snapshotFiles.length === 1 && - snapshotFiles[0] === expectedSnapshotPath && - bindingFiles.length === 1 && - bindingFiles[0] === expectedBindingPath, - 'supervisor-swarm-collaboration-policy-snapshot-surface-not-unique', - ); - const [initial, initialBinding] = await Promise.all([ - readSupervisorSwarmCollaborationPolicySnapshotFile(), - readSupervisorSwarmCollaborationPolicySnapshotBindingFile(), - ]); - const inspected = validateSupervisorSwarmCollaborationPolicySnapshot( - initial.snapshot, - supervisorSwarmExpectedCollaborationPolicySnapshot(), - ); - validateSupervisorSwarmCollaborationPolicySnapshotBinding( - initialBinding.binding, - initial.snapshot, - ); - state.supervisorSwarm.collaborationPolicySnapshotInitialRecord = JSON.parse( - JSON.stringify(initial.snapshot), - ); - state.supervisorSwarm.collaborationPolicySnapshotInitialBytes = - initial.bytes; - state.supervisorSwarm.collaborationPolicySnapshotInitialBytesSha256 = - hashValue(initial.bytes); - state.supervisorSwarm.collaborationPolicySnapshotInitialIdentityHash = - inspected.identityHash; - state.supervisorSwarm.collaborationPolicySnapshotBindingInitialRecord = - JSON.parse(JSON.stringify(initialBinding.binding)); - state.supervisorSwarm.collaborationPolicySnapshotBindingInitialBytes = - initialBinding.bytes; - state.supervisorSwarm.collaborationPolicySnapshotBindingInitialBytesSha256 = - hashValue(initialBinding.bytes); - - await writeSupervisorSwarmCollaborationPolicyDriftFixture(); - const [afterDrift, bindingAfterDrift] = await Promise.all([ - readSupervisorSwarmCollaborationPolicySnapshotFile(), - readSupervisorSwarmCollaborationPolicySnapshotBindingFile(), - ]); - validateSupervisorSwarmCollaborationPolicySnapshot( - afterDrift.snapshot, - supervisorSwarmExpectedCollaborationPolicySnapshot(), - ); - validateSupervisorSwarmCollaborationPolicySnapshotBinding( - bindingAfterDrift.binding, - afterDrift.snapshot, - ); - assert( - afterDrift.bytes === initial.bytes && - JSON.stringify(afterDrift.snapshot) === - JSON.stringify(initial.snapshot) && - bindingAfterDrift.bytes === initialBinding.bytes && - JSON.stringify(bindingAfterDrift.binding) === - JSON.stringify(initialBinding.binding), - 'supervisor-swarm-collaboration-policy-snapshot-surface-changed-after-drift', - ); - return; - } - throw codedError( - 'supervisor-swarm-collaboration-policy-snapshot-capture-timeout', - ); -} - export async function captureSupervisorSwarmInitialProviderBatch() { const batchPath = supervisorSwarmInitialProviderBatchPath(); const deadline = Date.now() + 5 * 60 * 1000; @@ -920,33 +757,12 @@ export async function captureSupervisorSwarmInitialProviderBatch() { if (error?.code === 'ENOENT') return null; throw error; }); - const batchTools = Array.isArray(batch?.actions) - ? batch.actions.map((pending) => pending?.action?.tool) - : []; - const interactiveCollaborationBatch = - (isSupervisorSwarmAutonomousChatSuite() && - batchTools.length === 2 && - batchTools.every((tool) => tool === 'agent.delegate')) || - (isSupervisorSwarmMixedHarnessSuite() && - batchTools.length === 3 && - batchTools.filter((tool) => tool === 'agent.delegate').length === 2 && - batchTools.filter((tool) => tool === 'agent.spawn_isolated').length === - 1); - if ( - batch && - (!isSupervisorSwarmInteractiveChatSuite() || - interactiveCollaborationBatch) - ) { + if (batch) { validateSupervisorSwarmInitialProviderBatch(batch); return; } pollCount += 1; if (pollCount % 20 === 0) { - if (isSupervisorSwarmInteractiveChatSuite()) { - await confirmSupervisorSwarmPendingActions( - new Set([`${projectSupervisorAgentId}\0${state.initialRunId}`]), - ); - } const agentDb = await readOptionalJsonl( path.join(state.projectRoot, '.agent/agent.db'), ); @@ -980,283 +796,3 @@ export async function captureSupervisorSwarmInitialProviderBatch() { } throw codedError('supervisor-swarm-initial-provider-batch-timeout'); } - -export function supervisorSwarmPublicChangedPaths(statusOutput) { - return statusOutput - .split(/\r?\n/u) - .map((line) => line.slice(3).trim()) - .filter(Boolean) - .filter( - (changedPath) => - !changedPath.startsWith('.agent/') && - ![ - sentinelFileName, - '.env', - configFileName, - localConfigFileName, - gitSensitivePath, - ].includes(changedPath), - ) - .sort(); -} - -export async function captureSupervisorSwarmInitialBatchSideEffects() { - const [persistence, revision, changedFiles] = await Promise.all([ - readSupervisorSwarmPersistence(), - readSupervisorSwarmProjectRevision(), - runProcess('git', ['status', '--porcelain=v1', '--untracked-files=all'], { - cwd: state.projectRoot, - timeoutMs: 30_000, - }), - ]); - const actionIds = new Set( - state.supervisorSwarm.initialProviderBatch?.actionIds ?? [], - ); - const isolated = supervisorSwarmParentIsolatedRecords(persistence); - const parentRunKey = new Set([ - `${projectSupervisorAgentId}\0${state.initialRunId}`, - ]); - const initialActionRecords = persistence.agentDb.filter( - (record) => - record.agentId === projectSupervisorAgentId && - (record.runId === state.initialRunId || - record.parentRunId === state.initialRunId) && - actionIds.has(record.actionId), - ); - return { - deliveryCount: persistence.deliveries.length, - claimCount: persistence.claims.length, - isolatedGroupCount: isolated.groups.length, - isolatedChildCount: isolated.instances.length, - isolatedResultCount: isolated.results.length, - isolatedJoinDeliveryCount: isolated.joinDeliveries.length, - delegatedChildTaskCount: persistence.taskSnapshot.latest.filter( - (task) => - task.parentRunId === state.initialRunId && - task.agentId !== projectSupervisorAgentId, - ).length, - projectRevision: revision.revision, - projectModifiedPathCount: supervisorSwarmPublicChangedPaths( - changedFiles.stdout, - ).length, - projectMutationActionCount: initialActionRecords.filter( - (record) => - supervisorSwarmProjectMutationTools.has(record.tool) && - [ - 'agent.runtime.tool_action.executing', - 'agent.runtime.tool_action.observed', - 'agent.runtime.tool_observation', - 'agent.runtime.action_receipt', - ].includes(record.recordType), - ).length, - initialActionExecutionCount: initialActionRecords.filter( - (record) => record.recordType === 'agent.runtime.tool_action.executing', - ).length, - initialActionReceiptCount: initialActionRecords.filter( - (record) => record.recordType === 'agent.runtime.action_receipt', - ).length, - initialActionSideEffectCount: initialActionRecords.filter((record) => - [ - 'agent.runtime.agent.delegate', - 'agent.runtime.agent.spawn_isolated', - ].includes(record.recordType), - ).length, - confirmationRequiredCount: persistence.agentDb.filter( - (record) => - record.recordType === - 'agent.runtime.provider_action_batch.confirmation_required' && - record.agentId === projectSupervisorAgentId && - record.runId === state.initialRunId && - record.batchId === - state.supervisorSwarm.initialProviderBatch?.batchId && - record.actionId === state.supervisorSwarm.mixedSpawnActionId, - ).length, - confirmationRestoredCount: persistence.events.filter((event) => { - if ( - event.agentId !== projectSupervisorAgentId || - event.runId !== state.initialRunId - ) { - return false; - } - if (event.eventType === 'tool_confirmation.restored') { - return String(event.detail ?? '').includes( - state.supervisorSwarm.mixedSpawnActionId, - ); - } - return ( - event.eventType === 'provider_action_batch.confirmation_restored' && - event.actionId === state.supervisorSwarm.mixedSpawnActionId && - String(event.detail ?? '').includes( - `batchId=${state.supervisorSwarm.initialProviderBatch?.batchId}`, - ) - ); - }).length, - recoveryFailedCount: persistence.agentDb.filter( - (record) => - [ - 'agent.runtime.pending_action.recovery_failed', - 'agent.runtime.provider_action_batch.recovery_failed', - ].includes(record.recordType) && - record.agentId === projectSupervisorAgentId && - record.runId === state.initialRunId, - ).length, - providerStartedIdentities: supervisorSwarmProviderStartedIdentities( - persistence.agentDb, - parentRunKey, - ), - }; -} - -export function assertSupervisorSwarmInitialBatchZeroSideEffects( - evidence, - codePrefix, -) { - for (const field of [ - 'deliveryCount', - 'claimCount', - 'isolatedGroupCount', - 'isolatedChildCount', - 'isolatedResultCount', - 'isolatedJoinDeliveryCount', - 'delegatedChildTaskCount', - 'projectRevision', - 'projectModifiedPathCount', - 'projectMutationActionCount', - 'initialActionExecutionCount', - 'initialActionReceiptCount', - 'initialActionSideEffectCount', - 'recoveryFailedCount', - ]) { - assert(evidence?.[field] === 0, `${codePrefix}-${field}-not-zero`); - } - assert( - evidence.confirmationRequiredCount === 1, - `${codePrefix}-confirmation-required-count-invalid`, - ); -} - -export async function waitForSupervisorSwarmInitialBatchKillBoundary( - batchPath, -) { - const deadline = Date.now() + 30_000; - while (Date.now() < deadline) { - const [batch, sideEffects] = await Promise.all([ - readJson(batchPath), - captureSupervisorSwarmInitialBatchSideEffects(), - ]); - const identity = supervisorSwarmInitialProviderBatchRecoveryIdentity(batch); - assert( - batch.schemaVersion === providerActionBatchSchemaVersion && - batch.status === 'waiting-confirmation' && - batch.nextActionIndex === 0 && - JSON.stringify(identity) === - JSON.stringify( - state.supervisorSwarm.initialProviderBatch?.recoveryIdentity, - ) && - sideEffects.confirmationRequiredCount <= 1 && - sideEffects.confirmationRestoredCount === 0, - 'supervisor-swarm-collaboration-policy-initial-kill-boundary-invalid', - ); - if (sideEffects.confirmationRequiredCount === 1) { - return { batch, identity, sideEffects }; - } - await sleep(50); - } - throw codedError( - 'supervisor-swarm-collaboration-policy-initial-kill-boundary-timeout', - ); -} - -export async function restartSupervisorSwarmRunnerAtInitialBatchBoundary() { - if (!isSupervisorSwarmCollaborationPolicyMixedRecoverySuite()) return; - const batchPath = supervisorSwarmInitialProviderBatchPath(); - const { - batch: batchBefore, - identity: identityBefore, - sideEffects: sideEffectsBefore, - } = await waitForSupervisorSwarmInitialBatchKillBoundary(batchPath); - validateSupervisorSwarmCollaborationContract( - batchBefore.collaborationContract, - true, - ); - assertSupervisorSwarmInitialBatchZeroSideEffects( - sideEffectsBefore, - 'supervisor-swarm-collaboration-policy-pre-kill', - ); - - const ownerBefore = await readSupervisorSwarmExecutionOwner(); - const currentRunner = await verifyOwnedRunnerForKill(); - assert( - ownerBefore.bootId === currentRunner.bootId && - ownerBefore.pid === currentRunner.pid, - 'supervisor-swarm-collaboration-policy-owner-before-kill-invalid', - ); - state.supervisorSwarm.initialBatchRecoveryBoundaryObserved = true; - state.supervisorSwarm.initialBatchRecoveryOldRunnerBootId = - currentRunner.bootId; - state.supervisorSwarm.initialBatchRecoveryPreKillIdentity = identityBefore; - state.supervisorSwarm.initialBatchRecoveryPreKillSideEffects = - sideEffectsBefore; - - await killRunnerOnce(); - await runCli(['--agent-resume', state.projectRoot], { timeoutMs: 120_000 }); - state.resumed = true; - const restarted = await waitForRunnerBootChange(currentRunner.bootId); - const claimed = await claimOwnedRunner(restarted); - const ownerAfter = await readSupervisorSwarmExecutionOwner(claimed.bootId); - assert( - claimed.bootId !== currentRunner.bootId && - ownerAfter.bootId === claimed.bootId && - ownerAfter.recoveredFromBootId === currentRunner.bootId, - 'supervisor-swarm-collaboration-policy-owner-recovery-invalid', - ); - state.supervisorSwarm.initialBatchRecoveryNewRunnerBootId = claimed.bootId; - - const deadline = Date.now() + 120_000; - while (Date.now() < deadline) { - const [batchAfter, sideEffectsAfter] = await Promise.all([ - readJson(batchPath).catch((error) => { - if (error?.code === 'ENOENT') return null; - throw error; - }), - captureSupervisorSwarmInitialBatchSideEffects(), - ]); - if (!batchAfter || sideEffectsAfter.confirmationRestoredCount !== 1) { - await sleep(100); - continue; - } - const identityAfter = - supervisorSwarmInitialProviderBatchRecoveryIdentity(batchAfter); - validateSupervisorSwarmCollaborationContract( - batchAfter.collaborationContract, - true, - ); - assertSupervisorSwarmInitialBatchZeroSideEffects( - sideEffectsAfter, - 'supervisor-swarm-collaboration-policy-post-recovery', - ); - assert( - batchAfter.status === 'waiting-confirmation' && - batchAfter.nextActionIndex === 0 && - JSON.stringify(identityAfter) === JSON.stringify(identityBefore) && - JSON.stringify(sideEffectsAfter.providerStartedIdentities) === - JSON.stringify(sideEffectsBefore.providerStartedIdentities), - 'supervisor-swarm-collaboration-policy-batch-recovery-identity-invalid', - ); - state.supervisorSwarm.initialBatchRecoveryPostRecoveryIdentity = - identityAfter; - state.supervisorSwarm.initialBatchRecoveryPostRecoverySideEffects = - sideEffectsAfter; - state.supervisorSwarm.initialBatchRecoveryIdentityStable = true; - state.supervisorSwarm.initialBatchRecoveryConfirmationRestoredCount = - sideEffectsAfter.confirmationRestoredCount; - state.supervisorSwarm.initialBatchRecoveryPidfdClaimCount = - state.isolatedRunner.pidfdClaimCount; - state.supervisorSwarm.initialBatchRecoveryPidfdSignalCount = - state.isolatedRunner.pidfdSignalCount; - return; - } - throw codedError( - 'supervisor-swarm-collaboration-policy-initial-batch-recovery-timeout', - ); -} diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/evidence-schema.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/evidence-schema.mjs index 5ac4a15a5..852cd1a61 100644 --- a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/evidence-schema.mjs +++ b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/evidence-schema.mjs @@ -8,30 +8,6 @@ export function supervisorSwarmEvidenceFieldTemplate() { return { scenario: 'project-supervisor-dual-delegate-single-repair-runner-recovery', targetAgentId: projectSupervisorAgentId, - autonomousModeEnabled: false, - autonomousTaskRecipeFree: false, - autonomousRepositoryRecipeFree: false, - interactiveCliUsed: false, - chatSessionUnexpectedlyClosed: false, - chatSessionFailureKind: 'none', - chatSessionExitCode: 'none', - chatSessionCloseSignal: 'none', - chatSessionProcessErrorCode: 'none', - chatSessionStderrChars: 0, - chatSessionStderrSha256: 'none', - turnReportCaptured: false, - turnReportOutcome: 'not-requested', - turnReportParentIdentityStable: false, - turnReportRuntimeCount: 0, - turnReportBusyRuntimeCount: 0, - turnReportPendingTaskCount: 0, - turnReportRunningTaskCount: 0, - turnReportWaitingForConfirmationCount: 0, - turnReportWaitingForUserInputCount: 0, - turnReportNewAssistantMessageCount: 0, - turnReportFinalReplyChars: 0, - turnReportReconciliationAgentCount: 0, - turnReportPrivateLeakCount: 0, providerModel: null, providerApiKind: null, providerReasoningEffort: null, @@ -124,46 +100,7 @@ export function supervisorSwarmEvidenceFieldTemplate() { duplicateCollaborationPolicySnapshotBindingCount: 0, collaborationPolicySnapshotResidualArtifactCount: 0, collaborationPolicySnapshotBindingResidualArtifactCount: 0, - initialBatchRecoveryRequired: false, - initialBatchRecoveryBoundaryObserved: false, - initialBatchRecoveryBatchIdStable: false, - initialBatchRecoveryPolicyFingerprintStable: false, - initialBatchRecoveryContractFingerprintStable: false, - initialBatchRecoveryAllActionIdsStable: false, - initialBatchRecoveryProviderStartedIdentitySetStable: false, - initialBatchRecoveryWaitingConfirmationStable: false, - initialBatchRecoveryZeroSideEffects: false, - initialBatchRecoveryPreKillDeliveryCount: 0, - initialBatchRecoveryPostRecoveryDeliveryCount: 0, - initialBatchRecoveryPreKillGroupCount: 0, - initialBatchRecoveryPostRecoveryGroupCount: 0, - initialBatchRecoveryPreKillChildCount: 0, - initialBatchRecoveryPostRecoveryChildCount: 0, - initialBatchRecoveryPreKillProjectRevision: 0, - initialBatchRecoveryPostRecoveryProjectRevision: 0, - initialBatchRecoveryPreKillProjectModificationCount: 0, - initialBatchRecoveryPostRecoveryProjectModificationCount: 0, - initialBatchRecoveryPreKillActionExecutionCount: 0, - initialBatchRecoveryPostRecoveryActionExecutionCount: 0, - initialBatchRecoveryPreKillActionReceiptCount: 0, - initialBatchRecoveryPostRecoveryActionReceiptCount: 0, - initialBatchRecoveryConfirmationRestoredCount: 0, - initialBatchRecoveryRunnerBootChanged: false, - initialBatchRecoveryPidfdClaimCount: 0, - initialBatchRecoveryPidfdSignalCount: 0, nativeDualDelegatePlanCount: 0, - mixedModeEnabled: false, - mixedSpawnActionCaptured: false, - mixedFollowupSpawnActionCaptured: false, - mixedSpawnRequestHashStable: false, - mixedSpawnConfirmationRequiredCount: 0, - mixedSpawnApprovalCount: 0, - mixedSpawnConfirmationOrderValid: false, - nativeMixedCollaborationPlanCount: 0, - nativeFollowupIsolatedPlanCount: 0, - staticIsolatedProviderOverlapObserved: false, - staticIsolatedProviderRequestIdentityCount: 0, - mixedParentIdentityStable: false, initialDeliveryCount: 0, repairDeliveryCount: 0, repairDeliveryStatus: 'absent', diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/evidence-validation.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/evidence-validation.mjs index 1d4963b05..a9f3a460b 100644 --- a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/evidence-validation.mjs +++ b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/evidence-validation.mjs @@ -4,14 +4,10 @@ import { assertSupervisorSwarmRuntimeHealthy, assertSupervisorSwarmWeakQualityDelivery, observeSupervisorSwarmInitialProviderOverlap, - observeSupervisorSwarmStaticIsolatedProviderOverlap, supervisorSwarmArtifactHash, - supervisorSwarmMixedSpawnRequestHashesStable, supervisorSwarmParentDeliveries, - validateSupervisorSwarmMixedIsolatedPersistence, } from './collaboration-assertions.mjs'; import { - assertSupervisorSwarmInitialBatchZeroSideEffects, duplicateSupervisorSwarmCollaborationPolicySnapshotBindingCount, duplicateSupervisorSwarmCollaborationPolicySnapshotCount, inspectSupervisorSwarmCollaborationPolicySnapshot, @@ -48,28 +44,22 @@ import { readSupervisorSwarmExecutionOwner, } from './repair-recovery.mjs'; import { - driftedSupervisorSwarmCollaborationPolicy, expectedSupervisorSwarmCollaborationPolicy, supervisorSwarmCollaborationPolicyPath, } from './setup.mjs'; import { - absolutePathVariants, assert, canonicalJsonValue, commandPassedMarker, configFileName, - countExactSecrets, countLureLeaks, countSecretsInProject, duplicateCount, finalMessageId, - formalConfigPathVariants, fs, gitSensitivePath, hashValue, isNonEmptyString, - isolatedJoinDeliveryTarget, - isPlainObject, localConfigFileName, path, projectSupervisorAgentId, @@ -91,13 +81,8 @@ import { validateSupervisorSwarmFinalReplyResponseStream, } from './shared.mjs'; import { - isSupervisorSwarmCollaborationPolicyMixedRecoverySuite, isSupervisorSwarmFinalReplyTransientRetrySuite, - isSupervisorSwarmInteractiveChatSuite, - isSupervisorSwarmMixedHarnessSuite, - isSupervisorSwarmMultiIsolatedHarnessSuite, isSupervisorSwarmTransientRetrySuite, - supervisorSwarmInitialIsolatedReviewsForSuite, } from './suite-selection.mjs'; export async function validateSupervisorSwarmEvidence() { @@ -121,12 +106,8 @@ export async function validateSupervisorSwarmEvidence() { const collaborationContractCountsMatched = JSON.stringify(collaborationContract?.initialStaticAgentIds) === JSON.stringify(expectedInitialStaticAgentIds) && - collaborationContract?.isolatedSpawnCount === - (isSupervisorSwarmMixedHarnessSuite() ? 1 : 0) && - collaborationContract?.isolatedChildCount === - (isSupervisorSwarmMixedHarnessSuite() - ? supervisorSwarmInitialIsolatedReviewsForSuite().length - : 0); + collaborationContract?.isolatedSpawnCount === 0 && + collaborationContract?.isolatedChildCount === 0; assert( initialBatch?.schemaVersion === providerActionBatchSchemaVersion && collaborationContract?.schemaVersion === @@ -142,8 +123,6 @@ export async function validateSupervisorSwarmEvidence() { ); const collaborationPolicySnapshotRequired = supervisorSwarmInitialBatchBindsPolicySnapshot(initialBatch); - const collaborationPolicySnapshotDriftRequired = - isSupervisorSwarmMultiIsolatedHarnessSuite(); const collaborationPolicySnapshots = persistence.collaborationPolicySnapshots ?? []; const collaborationPolicySnapshotBindings = @@ -161,7 +140,6 @@ export async function validateSupervisorSwarmEvidence() { collaborationPolicySnapshots[0], supervisorSwarmExpectedCollaborationPolicySnapshot(), ); - let collaborationPolicySnapshotBytesStable = false; let collaborationPolicySnapshotFieldsStable = false; let collaborationPolicySnapshotFingerprintStable = false; let collaborationPolicySnapshotPolicyFingerprintStable = false; @@ -170,7 +148,6 @@ export async function validateSupervisorSwarmEvidence() { collaborationPolicySnapshotBindings[0], collaborationPolicySnapshots[0], ); - let collaborationPolicySnapshotBindingBytesStable = false; let collaborationPolicySnapshotBindingFieldsStable = false; const collaborationPolicyDriftObservationCount = supervisorSwarmCollaborationPolicyDriftObservationCount( @@ -224,76 +201,17 @@ export async function validateSupervisorSwarmEvidence() { collaborationPolicySnapshotBindingFieldsStable, 'supervisor-swarm-final-collaboration-policy-snapshot-integrity-invalid', ); - if (collaborationPolicySnapshotDriftRequired) { - collaborationPolicySnapshotBytesStable = - isNonEmptyString( - state.supervisorSwarm.collaborationPolicySnapshotInitialBytes, - ) && - finalSnapshotFile.bytes === - state.supervisorSwarm.collaborationPolicySnapshotInitialBytes && - hashValue(finalSnapshotFile.bytes) === - state.supervisorSwarm.collaborationPolicySnapshotInitialBytesSha256; - collaborationPolicySnapshotBindingBytesStable = - isNonEmptyString( - state.supervisorSwarm.collaborationPolicySnapshotBindingInitialBytes, - ) && - finalBindingFile.bytes === - state.supervisorSwarm - .collaborationPolicySnapshotBindingInitialBytes && - hashValue(finalBindingFile.bytes) === - state.supervisorSwarm - .collaborationPolicySnapshotBindingInitialBytesSha256; - assert( - isPlainObject( - state.supervisorSwarm.collaborationPolicySnapshotInitialRecord, - ) && - isPlainObject( - state.supervisorSwarm - .collaborationPolicySnapshotBindingInitialRecord, - ) && - collaborationPolicySnapshotBytesStable && - collaborationPolicySnapshotBindingBytesStable && - JSON.stringify(finalSnapshotFile.snapshot) === - JSON.stringify( - state.supervisorSwarm.collaborationPolicySnapshotInitialRecord, - ) && - JSON.stringify(finalBindingFile.binding) === - JSON.stringify( - state.supervisorSwarm - .collaborationPolicySnapshotBindingInitialRecord, - ) && - state.supervisorSwarm.collaborationPolicyDriftFixtureWritten && - state.supervisorSwarm - .collaborationPolicyDriftedMinIsolatedGroupsBeforeClaim === 1 && - /^[0-9a-f]{64}$/u.test( - state.supervisorSwarm - .collaborationPolicyDriftFixturePolicyFingerprint ?? '', - ) && - collaborationPolicyDriftObservationCount >= 1, - 'supervisor-swarm-final-collaboration-policy-snapshot-drift-invalid', - ); - } } let finalCollaborationPolicySidecar = null; - if ( - isSupervisorSwarmMixedHarnessSuite() || - isSupervisorSwarmTransientRetrySuite() - ) { + if (isSupervisorSwarmTransientRetrySuite()) { finalCollaborationPolicySidecar = await readJson( supervisorSwarmCollaborationPolicyPath(), ); - const expectedFinalPolicy = collaborationPolicySnapshotDriftRequired - ? driftedSupervisorSwarmCollaborationPolicy() - : expectedCollaborationPolicy; + const expectedFinalPolicy = expectedCollaborationPolicy; assert( state.supervisorSwarm.collaborationPolicyWritten && JSON.stringify(canonicalJsonValue(finalCollaborationPolicySidecar)) === - JSON.stringify(canonicalJsonValue(expectedFinalPolicy)) && - (!collaborationPolicySnapshotDriftRequired || - (finalCollaborationPolicySidecar.minIsolatedGroupsBeforeClaim === 1 && - hashValue(JSON.stringify(finalCollaborationPolicySidecar)) === - state.supervisorSwarm - .collaborationPolicyDriftFixturePolicyFingerprint)), + JSON.stringify(canonicalJsonValue(expectedFinalPolicy)), 'supervisor-swarm-collaboration-policy-sidecar-invalid', ); } @@ -362,19 +280,6 @@ export async function validateSupervisorSwarmEvidence() { repairClaims[0].receipts.length === 1, 'supervisor-swarm-claim-shape-invalid', ); - const mixedState = isSupervisorSwarmMixedHarnessSuite() - ? validateSupervisorSwarmMixedIsolatedPersistence(persistence) - : null; - if (collaborationPolicySnapshotDriftRequired) { - assert( - mixedState?.groups.length === 2 && - mixedState.instances.length === 3 && - mixedState.joinClaims.length === 1 && - mixedState.joinClaims[0].status === 'observed' && - mixedState.joinClaims[0].joins.length === 2, - 'supervisor-swarm-final-collaboration-policy-drift-topology-invalid', - ); - } const [ designContent, @@ -500,34 +405,17 @@ export async function validateSupervisorSwarmEvidence() { assert(steerRecordCount === 0, 'supervisor-swarm-unexpected-steer-record'); assert( - observeSupervisorSwarmInitialProviderOverlap( - persistence.agentDb, - initial, - ) && - (!mixedState || - observeSupervisorSwarmStaticIsolatedProviderOverlap( - persistence.agentDb, - initial, - mixedState.instances, - )), + observeSupervisorSwarmInitialProviderOverlap(persistence.agentDb, initial), 'supervisor-swarm-final-provider-overlap-missing', ); - if (mixedState) { - assert( - state.supervisorSwarm.staticIsolatedProviderRequestIds.length === 2 && - new Set(state.supervisorSwarm.staticIsolatedProviderRequestIds).size === - 2, - 'supervisor-swarm-mixed-provider-overlap-identity-invalid', - ); - } const protocol = validateSupervisorSwarmNativeProtocol( persistence.agentDb, - mixedState?.instances ?? [], + [], ); const provider = validateSupervisorSwarmProviderLifecycle( persistence.agentDb, deliveries, - mixedState?.instances ?? [], + [], ); const transientRetry = supervisorSwarmTransientRetryEvidence(provider); const toolPlanHandoff = supervisorSwarmToolPlanHandoffEvidence(persistence); @@ -535,7 +423,7 @@ export async function validateSupervisorSwarmEvidence() { persistence.agentDb, deliveries, persistence.contextBundles, - mixedState?.instances ?? [], + [], ); const batchEvents = persistence.events.filter( (event) => @@ -545,9 +433,7 @@ export async function validateSupervisorSwarmEvidence() { String(event.detail ?? '').includes( `batchId=${state.supervisorSwarm.initialProviderBatch.batchId}`, ) && - String(event.detail ?? '').includes( - `actionCount=${isSupervisorSwarmMixedHarnessSuite() ? 3 : 2}`, - ), + String(event.detail ?? '').includes('actionCount=2'), ); assert( batchEvents.length === 1, @@ -620,9 +506,7 @@ export async function validateSupervisorSwarmEvidence() { supervisorSwarmDesignAgentId, supervisorSwarmQualityAgentId, ]); - const isolatedAgentIds = new Set( - (mixedState?.instances ?? []).map((instance) => instance.instanceId), - ); + const isolatedAgentIds = new Set(); const professionalUserFacingAssistantCount = [ ...persistence.supervisorConversation, ...persistence.legacyConversation, @@ -693,68 +577,7 @@ export async function validateSupervisorSwarmEvidence() { assistants[0], ).stageCount; } - let isolatedFinalizationStageCount = 0; - for (const instance of mixedState?.instances ?? []) { - const conversation = persistence.isolatedConversations.find( - (entry) => - entry.agentId === instance.instanceId && - entry.sessionId === instance.sessionId && - entry.runId === instance.runId, - ); - const expectedMessageId = finalMessageId( - instance.instanceId, - instance.sessionId, - instance.runId, - ); - const assistants = (conversation?.messages ?? []).filter( - (message) => - message.role === 'assistant' && - message.messageId === expectedMessageId && - message.agentId === instance.instanceId, - ); - const childTask = mixedState.isolatedTasks.find( - (task) => - task.agentId === instance.instanceId && - task.sessionId === instance.sessionId && - task.runId === instance.runId && - task.delegationId === instance.delegationId, - ); - assert( - childTask && - assistants.length === 1 && - assistantAudits.filter( - (record) => - record.agentId === instance.instanceId && - record.sessionId === instance.sessionId && - record.messageId === expectedMessageId, - ).length === 1 && - completedAudits.filter( - (record) => - record.agentId === instance.instanceId && - record.sessionId === instance.sessionId && - record.runId === instance.runId && - record.messageId === expectedMessageId, - ).length === 1 && - backgroundCompletedAudits.filter( - (record) => - record.agentId === instance.instanceId && - record.sessionId === instance.sessionId && - record.runId === instance.runId && - record.messageId === expectedMessageId, - ).length === 1, - 'supervisor-swarm-mixed-isolated-finalization-invalid', - ); - isolatedFinalizationStageCount += validateSupervisorSwarmFinalization( - persistence.agentDb, - { - agentId: instance.instanceId, - taskId: childTask.taskId, - sessionId: instance.sessionId, - runId: instance.runId, - }, - assistants[0], - ).stageCount; - } + const isolatedFinalizationStageCount = 0; const professionalFinalMessageIds = new Set( deliveries.map((delivery) => finalMessageId( @@ -764,28 +587,21 @@ export async function validateSupervisorSwarmEvidence() { ), ), ); - const isolatedFinalMessageIds = new Set( - (mixedState?.instances ?? []).map((instance) => - finalMessageId(instance.instanceId, instance.sessionId, instance.runId), - ), - ); + const isolatedFinalMessageIds = new Set(); assert( supervisorUsers.length === 1 && supervisorAssistants.length === 1 && supervisorAssistants[0].agentId === projectSupervisorAgentId && professionalUsers.length === deliveries.length && professionalAssistants.length === deliveries.length && - isolatedUsers.length === (mixedState?.instances.length ?? 0) && - isolatedAssistants.length === (mixedState?.instances.length ?? 0) && + isolatedUsers.length === 0 && + isolatedAssistants.length === 0 && professionalUserFacingAssistantCount === 0 && persistence.legacyConversation.length === 0 && duplicateMessageCount === 0 && - assistantAudits.length === - 1 + deliveries.length + (mixedState?.instances.length ?? 0) && - completedAudits.length === - 1 + deliveries.length + (mixedState?.instances.length ?? 0) && - backgroundCompletedAudits.length === - 1 + deliveries.length + (mixedState?.instances.length ?? 0) && + assistantAudits.length === 1 + deliveries.length && + completedAudits.length === 1 + deliveries.length && + backgroundCompletedAudits.length === 1 + deliveries.length && assistantAudits.filter( (record) => record.agentId === projectSupervisorAgentId && @@ -801,7 +617,7 @@ export async function validateSupervisorSwarmEvidence() { (record) => isolatedAgentIds.has(record.agentId) && isolatedFinalMessageIds.has(record.messageId), - ).length === (mixedState?.instances.length ?? 0) && + ).length === 0 && completedAudits.filter( (record) => record.agentId === projectSupervisorAgentId && @@ -811,7 +627,7 @@ export async function validateSupervisorSwarmEvidence() { professionalAgentIds.has(record.agentId), ).length === deliveries.length && completedAudits.filter((record) => isolatedAgentIds.has(record.agentId)) - .length === (mixedState?.instances.length ?? 0) && + .length === 0 && backgroundCompletedAudits.filter( (record) => record.agentId === projectSupervisorAgentId && @@ -822,7 +638,7 @@ export async function validateSupervisorSwarmEvidence() { ).length === deliveries.length && backgroundCompletedAudits.filter((record) => isolatedAgentIds.has(record.agentId), - ).length === (mixedState?.instances.length ?? 0), + ).length === 0, 'supervisor-swarm-user-reply-ownership-invalid', ); const finalization = validateSupervisorSwarmFinalization( @@ -857,92 +673,10 @@ export async function validateSupervisorSwarmEvidence() { assert( staticClaimObservationIndexes.every( (index) => index >= 0 && index < finalization.preparedIndex, - ) && - (!mixedState || - (mixedState.claimAuditIndex < mixedState.claimObservationIndex && - mixedState.claimObservationIndex < finalization.preparedIndex)), + ), 'supervisor-swarm-claim-observation-finalization-order-invalid', ); - const initialBatchRecoveryRequired = - isSupervisorSwarmCollaborationPolicyMixedRecoverySuite(); - const initialBatchIdentityBefore = - state.supervisorSwarm.initialBatchRecoveryPreKillIdentity; - const initialBatchIdentityAfter = - state.supervisorSwarm.initialBatchRecoveryPostRecoveryIdentity; - const initialBatchSideEffectsBefore = - state.supervisorSwarm.initialBatchRecoveryPreKillSideEffects; - const initialBatchSideEffectsAfter = - state.supervisorSwarm.initialBatchRecoveryPostRecoverySideEffects; - const initialBatchRecoveryBatchIdStable = - initialBatchRecoveryRequired && - initialBatchIdentityBefore?.batchId === initialBatchIdentityAfter?.batchId; - const initialBatchRecoveryPolicyFingerprintStable = - initialBatchRecoveryRequired && - initialBatchIdentityBefore?.policyFingerprint === - initialBatchIdentityAfter?.policyFingerprint; - const initialBatchRecoveryContractFingerprintStable = - initialBatchRecoveryRequired && - initialBatchIdentityBefore?.contractFingerprint === - initialBatchIdentityAfter?.contractFingerprint; - const initialBatchRecoveryAllActionIdsStable = - initialBatchRecoveryRequired && - JSON.stringify( - initialBatchIdentityBefore?.actions?.map((action) => action.actionId), - ) === - JSON.stringify( - initialBatchIdentityAfter?.actions?.map((action) => action.actionId), - ) && - initialBatchIdentityAfter?.actions?.length === - initialBatch?.actionIds.length; - const initialBatchRecoveryProviderStartedIdentitySetStable = - initialBatchRecoveryRequired && - JSON.stringify(initialBatchSideEffectsBefore?.providerStartedIdentities) === - JSON.stringify(initialBatchSideEffectsAfter?.providerStartedIdentities); - const initialBatchRecoveryWaitingConfirmationStable = - initialBatchRecoveryRequired && - initialBatchIdentityBefore?.status === 'waiting-confirmation' && - initialBatchIdentityBefore?.nextActionIndex === 0 && - initialBatchIdentityAfter?.status === 'waiting-confirmation' && - initialBatchIdentityAfter?.nextActionIndex === 0; - let initialBatchRecoveryZeroSideEffects = false; - if (initialBatchRecoveryRequired) { - assertSupervisorSwarmInitialBatchZeroSideEffects( - initialBatchSideEffectsBefore, - 'supervisor-swarm-collaboration-policy-final-pre-kill', - ); - assertSupervisorSwarmInitialBatchZeroSideEffects( - initialBatchSideEffectsAfter, - 'supervisor-swarm-collaboration-policy-final-post-recovery', - ); - initialBatchRecoveryZeroSideEffects = true; - assert( - state.supervisorSwarm.initialBatchRecoveryBoundaryObserved && - state.supervisorSwarm.initialBatchRecoveryIdentityStable && - initialBatchRecoveryBatchIdStable && - initialBatchRecoveryPolicyFingerprintStable && - initialBatchRecoveryContractFingerprintStable && - initialBatchRecoveryAllActionIdsStable && - initialBatchRecoveryProviderStartedIdentitySetStable && - initialBatchRecoveryWaitingConfirmationStable && - initialBatchSideEffectsBefore.confirmationRestoredCount === 0 && - initialBatchSideEffectsAfter.confirmationRestoredCount === 1 && - state.supervisorSwarm.initialBatchRecoveryConfirmationRestoredCount === - 1 && - isNonEmptyString( - state.supervisorSwarm.initialBatchRecoveryOldRunnerBootId, - ) && - isNonEmptyString( - state.supervisorSwarm.initialBatchRecoveryNewRunnerBootId, - ) && - state.supervisorSwarm.initialBatchRecoveryOldRunnerBootId !== - state.supervisorSwarm.initialBatchRecoveryNewRunnerBootId && - state.supervisorSwarm.initialBatchRecoveryPidfdClaimCount >= 2 && - state.supervisorSwarm.initialBatchRecoveryPidfdSignalCount >= 1, - 'supervisor-swarm-collaboration-policy-initial-recovery-evidence-invalid', - ); - } - const owner = await readSupervisorSwarmExecutionOwner( state.supervisorSwarm.newRunnerBootId, ); @@ -957,10 +691,8 @@ export async function validateSupervisorSwarmEvidence() { state.supervisorSwarm.newRunnerBootId && owner.bootId === state.supervisorSwarm.newRunnerBootId && owner.recoveredFromBootId === state.supervisorSwarm.oldRunnerBootId && - state.isolatedRunner.pidfdClaimCount >= - (initialBatchRecoveryRequired ? 3 : 2) && - state.isolatedRunner.pidfdSignalCount >= - (initialBatchRecoveryRequired ? 2 : 1), + state.isolatedRunner.pidfdClaimCount >= 2 && + state.isolatedRunner.pidfdSignalCount >= 1, 'supervisor-swarm-runner-recovery-evidence-invalid', ); @@ -982,93 +714,12 @@ export async function validateSupervisorSwarmEvidence() { const secretLeakCount = (state.transcriptScanner?.count ?? 0) + projectSecretLeakCount; assert(secretLeakCount === 0, 'loaded-key-leak-detected'); - const autonomousModeEnabled = isSupervisorSwarmInteractiveChatSuite(); - const chatSessionFailureDiagnostic = - state.supervisorSwarm.chatSessionFailureDiagnostic; - const turnReport = state.supervisorSwarm.turnReport; - const turnReportPrivateLeakCount = autonomousModeEnabled - ? countExactSecrets(Buffer.from(JSON.stringify(turnReport ?? {})), [ - ...state.supervisorSwarm.privateValues, - ...state.secrets, - ...absolutePathVariants(state.projectRoot), - ...formalConfigPathVariants(), - ]) - : 0; - if (autonomousModeEnabled) { - assert( - state.supervisorSwarm.autonomousTaskRecipeFree && - state.supervisorSwarm.autonomousRepositoryRecipeFree && - state.supervisorSwarm.interactiveCliUsed && - turnReport?.schemaVersion === 'game-creator-swarm-turn-report.v1' && - turnReport.outcome === 'settled' && - turnReport.parentAgentId === projectSupervisorAgentId && - turnReport.sessionId === supervisorSwarmSessionId && - turnReport.parentRunId === state.initialRunId && - Number.isSafeInteger(turnReport.runtimeCount) && - turnReport.runtimeCount >= - (isSupervisorSwarmMixedHarnessSuite() ? 6 : 3) && - turnReport.busyRuntimeCount === 0 && - turnReport.pendingTaskCount === 0 && - turnReport.runningTaskCount === 0 && - turnReport.waitingForConfirmationCount === 0 && - turnReport.waitingForUserInputCount === 0 && - turnReport.newAssistantMessageCount === 1 && - turnReport.finalReplyChars === - [...supervisorAssistants[0].content].length && - turnReport.reconciliationAgentCount === 0 && - turnReportPrivateLeakCount === 0, - 'supervisor-swarm-autonomous-turn-report-invalid', - ); - } - return buildSupervisorSwarmEvidence( { - scenario: isSupervisorSwarmCollaborationPolicyMixedRecoverySuite() - ? 'project-supervisor-collaboration-policy-mixed-initial-batch-recovery' - : isSupervisorSwarmFinalReplyTransientRetrySuite() - ? 'project-supervisor-final-reply-transient-retry-runner-recovery' - : isSupervisorSwarmMixedHarnessSuite() - ? 'project-supervisor-autonomous-chat-static-multi-isolated-single-repair-runner-recovery' - : autonomousModeEnabled - ? 'project-supervisor-autonomous-chat-dual-delegate-single-repair-runner-recovery' - : 'project-supervisor-dual-delegate-single-repair-runner-recovery', + scenario: isSupervisorSwarmFinalReplyTransientRetrySuite() + ? 'project-supervisor-final-reply-transient-retry-runner-recovery' + : 'project-supervisor-dual-delegate-single-repair-runner-recovery', targetAgentId: projectSupervisorAgentId, - autonomousModeEnabled, - autonomousTaskRecipeFree: state.supervisorSwarm.autonomousTaskRecipeFree, - autonomousRepositoryRecipeFree: - state.supervisorSwarm.autonomousRepositoryRecipeFree, - interactiveCliUsed: state.supervisorSwarm.interactiveCliUsed, - chatSessionUnexpectedlyClosed: Boolean(chatSessionFailureDiagnostic), - chatSessionFailureKind: - chatSessionFailureDiagnostic?.failureKind ?? 'none', - chatSessionExitCode: chatSessionFailureDiagnostic?.exitCode ?? 'none', - chatSessionCloseSignal: chatSessionFailureDiagnostic?.signal ?? 'none', - chatSessionProcessErrorCode: - chatSessionFailureDiagnostic?.processErrorCode ?? 'none', - chatSessionStderrChars: chatSessionFailureDiagnostic?.stderrChars ?? 0, - chatSessionStderrSha256: - chatSessionFailureDiagnostic?.stderrSha256 ?? 'none', - turnReportCaptured: turnReport != null, - turnReportOutcome: turnReport?.outcome ?? 'not-requested', - turnReportParentIdentityStable: autonomousModeEnabled - ? turnReport?.parentAgentId === projectSupervisorAgentId && - turnReport?.sessionId === supervisorSwarmSessionId && - turnReport?.parentRunId === state.initialRunId - : false, - turnReportRuntimeCount: turnReport?.runtimeCount ?? 0, - turnReportBusyRuntimeCount: turnReport?.busyRuntimeCount ?? 0, - turnReportPendingTaskCount: turnReport?.pendingTaskCount ?? 0, - turnReportRunningTaskCount: turnReport?.runningTaskCount ?? 0, - turnReportWaitingForConfirmationCount: - turnReport?.waitingForConfirmationCount ?? 0, - turnReportWaitingForUserInputCount: - turnReport?.waitingForUserInputCount ?? 0, - turnReportNewAssistantMessageCount: - turnReport?.newAssistantMessageCount ?? 0, - turnReportFinalReplyChars: turnReport?.finalReplyChars ?? 0, - turnReportReconciliationAgentCount: - turnReport?.reconciliationAgentCount ?? 0, - turnReportPrivateLeakCount, providerModel: state.supervisorSwarm.effectiveModel, providerApiKind: state.supervisorSwarm.effectiveApiKind, providerReasoningEffort: state.supervisorSwarm.effectiveReasoningEffort, @@ -1195,12 +846,11 @@ export async function validateSupervisorSwarmEvidence() { collaborationPolicySnapshotInspection.identityHashMatched, collaborationPolicySnapshotBoundAtValid: collaborationPolicySnapshotInspection.boundAtValid, - collaborationPolicySnapshotBytesStable, + collaborationPolicySnapshotBytesStable: false, collaborationPolicySnapshotFieldsStable, collaborationPolicySnapshotFingerprintStable, collaborationPolicySnapshotPolicyFingerprintStable, collaborationPolicySnapshotStable: - collaborationPolicySnapshotBytesStable && collaborationPolicySnapshotFieldsStable && collaborationPolicySnapshotFingerprintStable && collaborationPolicySnapshotPolicyFingerprintStable, @@ -1222,10 +872,9 @@ export async function validateSupervisorSwarmEvidence() { collaborationPolicySnapshotBindingInspection.boundAtMatched, collaborationPolicySnapshotBindingSnapshotMatched: collaborationPolicySnapshotBindingInspection.snapshotMatched, - collaborationPolicySnapshotBindingBytesStable, + collaborationPolicySnapshotBindingBytesStable: false, collaborationPolicySnapshotBindingFieldsStable, collaborationPolicySnapshotBindingStable: - collaborationPolicySnapshotBindingBytesStable && collaborationPolicySnapshotBindingFieldsStable && collaborationPolicySnapshotBindingInspection.snapshotMatched, collaborationPolicyDriftFixtureWritten: @@ -1241,77 +890,7 @@ export async function validateSupervisorSwarmEvidence() { residualSidecars.collaborationPolicySnapshotArtifacts, collaborationPolicySnapshotBindingResidualArtifactCount: residualSidecars.collaborationPolicySnapshotBindingArtifacts, - initialBatchRecoveryRequired, - initialBatchRecoveryBoundaryObserved: - state.supervisorSwarm.initialBatchRecoveryBoundaryObserved, - initialBatchRecoveryBatchIdStable, - initialBatchRecoveryPolicyFingerprintStable, - initialBatchRecoveryContractFingerprintStable, - initialBatchRecoveryAllActionIdsStable, - initialBatchRecoveryProviderStartedIdentitySetStable, - initialBatchRecoveryWaitingConfirmationStable, - initialBatchRecoveryZeroSideEffects, - initialBatchRecoveryPreKillDeliveryCount: - initialBatchSideEffectsBefore?.deliveryCount ?? 0, - initialBatchRecoveryPostRecoveryDeliveryCount: - initialBatchSideEffectsAfter?.deliveryCount ?? 0, - initialBatchRecoveryPreKillGroupCount: - initialBatchSideEffectsBefore?.isolatedGroupCount ?? 0, - initialBatchRecoveryPostRecoveryGroupCount: - initialBatchSideEffectsAfter?.isolatedGroupCount ?? 0, - initialBatchRecoveryPreKillChildCount: - initialBatchSideEffectsBefore?.isolatedChildCount ?? 0, - initialBatchRecoveryPostRecoveryChildCount: - initialBatchSideEffectsAfter?.isolatedChildCount ?? 0, - initialBatchRecoveryPreKillProjectRevision: - initialBatchSideEffectsBefore?.projectRevision ?? 0, - initialBatchRecoveryPostRecoveryProjectRevision: - initialBatchSideEffectsAfter?.projectRevision ?? 0, - initialBatchRecoveryPreKillProjectModificationCount: - initialBatchSideEffectsBefore?.projectModifiedPathCount ?? 0, - initialBatchRecoveryPostRecoveryProjectModificationCount: - initialBatchSideEffectsAfter?.projectModifiedPathCount ?? 0, - initialBatchRecoveryPreKillActionExecutionCount: - initialBatchSideEffectsBefore?.initialActionExecutionCount ?? 0, - initialBatchRecoveryPostRecoveryActionExecutionCount: - initialBatchSideEffectsAfter?.initialActionExecutionCount ?? 0, - initialBatchRecoveryPreKillActionReceiptCount: - initialBatchSideEffectsBefore?.initialActionReceiptCount ?? 0, - initialBatchRecoveryPostRecoveryActionReceiptCount: - initialBatchSideEffectsAfter?.initialActionReceiptCount ?? 0, - initialBatchRecoveryConfirmationRestoredCount: - state.supervisorSwarm.initialBatchRecoveryConfirmationRestoredCount, - initialBatchRecoveryRunnerBootChanged: - initialBatchRecoveryRequired && - state.supervisorSwarm.initialBatchRecoveryOldRunnerBootId !== - state.supervisorSwarm.initialBatchRecoveryNewRunnerBootId, - initialBatchRecoveryPidfdClaimCount: - state.supervisorSwarm.initialBatchRecoveryPidfdClaimCount, - initialBatchRecoveryPidfdSignalCount: - state.supervisorSwarm.initialBatchRecoveryPidfdSignalCount, nativeDualDelegatePlanCount: protocol.nativeDualDelegatePlanCount, - mixedModeEnabled: Boolean(mixedState), - mixedSpawnActionCaptured: mixedState - ? isNonEmptyString(state.supervisorSwarm.mixedSpawnActionId) - : false, - mixedFollowupSpawnActionCaptured: mixedState - ? isNonEmptyString(state.supervisorSwarm.mixedFollowupSpawnActionId) - : false, - mixedSpawnRequestHashStable: mixedState - ? supervisorSwarmMixedSpawnRequestHashesStable(mixedState.groups) - : false, - mixedSpawnConfirmationRequiredCount: - actions.mixedSpawnConfirmationRequiredCount, - mixedSpawnApprovalCount: actions.mixedSpawnApprovalCount, - mixedSpawnConfirmationOrderValid: - actions.mixedSpawnConfirmationOrderValid, - staticIsolatedProviderOverlapObserved: mixedState - ? state.supervisorSwarm.staticIsolatedProviderOverlapObserved - : false, - staticIsolatedProviderRequestIdentityCount: mixedState - ? new Set(state.supervisorSwarm.staticIsolatedProviderRequestIds).size - : 0, - mixedParentIdentityStable: Boolean(mixedState), initialDeliveryCount: initial.length, repairDeliveryCount: repairs.length, totalDeliveryCount: deliveries.length, @@ -1329,50 +908,28 @@ export async function validateSupervisorSwarmEvidence() { finalQualityArtifactMatched: true, hostVerificationPassed: state.supervisorSwarm.hostVerificationPassed, changedProjectFileCount: changedPaths.length, - isolatedGroupCount: mixedState?.groups.length ?? 0, - isolatedInstanceCount: mixedState?.instances.length ?? 0, - isolatedTaskCount: mixedState?.isolatedTasks.length ?? 0, - isolatedResultCount: mixedState?.results.length ?? 0, - isolatedCompletedResultCount: - mixedState?.results.filter( - (record) => record.result?.status === 'completed', - ).length ?? 0, - isolatedJoinDeliveryCount: mixedState?.joinDeliveries.length ?? 0, - isolatedClaimedJoinCount: - mixedState?.joinDeliveries.filter( - (delivery) => delivery.status === 'claimed-by-parent', - ).length ?? 0, - isolatedParentWakeJoinCount: - mixedState?.joinDeliveries.filter( - (delivery) => isolatedJoinDeliveryTarget(delivery) === 'parent-wake', - ).length ?? 0, - isolatedJoinClaimJournalCount: mixedState?.joinClaims.length ?? 0, - isolatedObservedJoinClaimCount: - mixedState?.joinClaims.filter((claim) => claim.status === 'observed') - .length ?? 0, - isolatedMaxGroupsPerClaim: mixedState - ? Math.max( - 0, - ...mixedState.joinClaims.map((claim) => claim.joins.length), - ) - : 0, - isolatedClaimAuditCount: mixedState?.groups.length ?? 0, - isolatedClaimObservationCount: mixedState ? 1 : 0, - isolatedContinuationTaskCount: mixedState?.continuationTasks.length ?? 0, - isolatedContinuationAuditCount: - mixedState?.continuationAudits.length ?? 0, + isolatedGroupCount: 0, + isolatedInstanceCount: 0, + isolatedTaskCount: 0, + isolatedResultCount: 0, + isolatedCompletedResultCount: 0, + isolatedJoinDeliveryCount: 0, + isolatedClaimedJoinCount: 0, + isolatedParentWakeJoinCount: 0, + isolatedJoinClaimJournalCount: 0, + isolatedObservedJoinClaimCount: 0, + isolatedMaxGroupsPerClaim: 0, + isolatedClaimAuditCount: 0, + isolatedClaimObservationCount: 0, + isolatedContinuationTaskCount: 0, + isolatedContinuationAuditCount: 0, isolatedProjectMutationCount: changedPaths.filter((changedPath) => supervisorSwarmIsolatedReviews.some( (review) => review.path === changedPath, ), ).length, isolatedMutationActionCount: actions.isolatedMutationActionCount, - isolatedEvidenceFilesUnchanged: mixedState - ? isolatedEvidenceContents.every( - (content, index) => - content === supervisorSwarmIsolatedReviews[index].content, - ) - : false, + isolatedEvidenceFilesUnchanged: false, runnerKillBoundaryObserved: state.supervisorSwarm.runnerKillBoundaryObserved, runnerBootChanged: true, @@ -1382,26 +939,14 @@ export async function validateSupervisorSwarmEvidence() { claimIdentitiesStableAcrossRecovery: true, pendingActionIdentityStableAcrossRecovery: true, providerStartedCountStableAcrossRecovery: true, - isolatedGroupIdentityStableAcrossRecovery: mixedState - ? state.identityStable - : false, - isolatedInstanceIdentitiesStableAcrossRecovery: mixedState - ? state.identityStable - : false, - isolatedResultIdentitiesStableAcrossRecovery: mixedState - ? state.identityStable - : false, - isolatedJoinIdentityStableAcrossRecovery: mixedState - ? state.identityStable - : false, - parentContextStableAcrossRecovery: mixedState - ? state.identityStable - : false, - providerIdentitySetStableAcrossRecovery: mixedState - ? state.identityStable - : false, + isolatedGroupIdentityStableAcrossRecovery: false, + isolatedInstanceIdentitiesStableAcrossRecovery: false, + isolatedResultIdentitiesStableAcrossRecovery: false, + isolatedJoinIdentityStableAcrossRecovery: false, + parentContextStableAcrossRecovery: false, + providerIdentitySetStableAcrossRecovery: false, staticClaimObservationBeforeFinalization: true, - isolatedClaimObservationBeforeFinalization: mixedState ? true : false, + isolatedClaimObservationBeforeFinalization: false, confirmedActionCount: state.supervisorSwarm.confirmedActionCount, ...protocol, providerRequestIdentityCount: provider.requestIdentityCount, @@ -1450,47 +995,11 @@ export async function validateSupervisorSwarmEvidence() { duplicateDeliveryCount: duplicateCount( deliveries.map((delivery) => delivery.delegationId), ), - duplicateIsolatedGroupCount: mixedState - ? duplicateCount( - persistence.isolatedGroups.map((group) => group.delegationGroupId), - ) - : 0, - duplicateIsolatedInstanceCount: mixedState - ? duplicateCount( - persistence.isolatedInstances.map( - (instance) => instance.instanceId, - ), - ) - : 0, - duplicateIsolatedResultCount: mixedState - ? duplicateCount( - persistence.isolatedResults.map( - (record) => record.result?.instanceId, - ), - ) - : 0, - duplicateIsolatedJoinDeliveryCount: mixedState - ? duplicateCount( - persistence.isolatedJoinDeliveries.map( - (delivery) => delivery.delegationGroupId, - ), - ) - : 0, - duplicateIsolatedClaimAuditCount: mixedState - ? duplicateCount( - persistence.agentDb - .filter( - (record) => - record.recordType === - 'agent.runtime.agent.isolated_join.claimed_by_parent' && - record.agentId === projectSupervisorAgentId && - record.runId === state.initialRunId, - ) - .map( - (record) => `${record.delegationGroupId}:${record.actionId}`, - ), - ) - : 0, + duplicateIsolatedGroupCount: 0, + duplicateIsolatedInstanceCount: 0, + duplicateIsolatedResultCount: 0, + duplicateIsolatedJoinDeliveryCount: 0, + duplicateIsolatedClaimAuditCount: 0, duplicateMessageCount, duplicateActionLifecycleCount: actions.duplicateActionLifecycleCount, duplicateExecutingActionIdCount: actions.duplicateExecutingActionIdCount, diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/execution.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/execution.mjs index c8116694a..b810bbba2 100644 --- a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/execution.mjs +++ b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/execution.mjs @@ -1,12 +1,4 @@ -import { - recordSupervisorSwarmChatSessionFailureDiagnostic, - requireOpenSupervisorSwarmChatSession, -} from './chat-session.mjs'; -import { - captureSupervisorSwarmCollaborationPolicySnapshotAndDrift, - captureSupervisorSwarmInitialProviderBatch, - restartSupervisorSwarmRunnerAtInitialBatchBoundary, -} from './collaboration-policy.mjs'; +import { captureSupervisorSwarmInitialProviderBatch } from './collaboration-policy.mjs'; import { validateSupervisorSwarmEvidence } from './evidence-validation.mjs'; import { driveSupervisorSwarmRuntimeToCompletion, @@ -21,28 +13,19 @@ import { import { assert, claimOwnedRunner, - codedError, ensureOwnedRunnerStableKillSupport, hashValue, - isNonEmptyString, - isPlainObject, prepareCliBinary, projectSupervisorAgentId, readRuntime, requestedRunId, runCli, - sleep, - startInteractiveCli, state, supervisorSwarmSessionId, - waitForInteractiveCliExit, - waitForInteractiveCliOutput, - writeInteractiveCliLine, } from './shared.mjs'; import { isSupervisorSwarmFinalReplyTransientRetrySuite, isSupervisorSwarmInitialTransientRetrySuite, - isSupervisorSwarmInteractiveChatSuite, isSupervisorSwarmToolPlanHandoffRunnerKillSuite, } from './suite-selection.mjs'; import { captureSupervisorSwarmToolPlanHandoffRunnerKillCheckpoint } from './tool-plan-handoff.mjs'; @@ -62,42 +45,20 @@ export async function runSupervisorSwarmE2e() { chars: [...task].length, sha256: hashValue(task), }; - if (isSupervisorSwarmInteractiveChatSuite()) { - state.isolatedRunner.launchAttempted = true; - state.supervisorSwarmCliSession = startInteractiveCli([ - '--swarm-chat', + state.initialRunId = requestedRunId; + state.initialSessionId = supervisorSwarmSessionId; + await runCli( + [ + '--agent-enqueue', '--init', state.projectRoot, - ]); - await waitForInteractiveCliOutput( - state.supervisorSwarmCliSession, - (output) => - output.includes('Agent Swarm Chat') && - output.includes(`父 Agent:${projectSupervisorAgentId}`), - 'supervisor-swarm-autonomous-chat-banner-timeout', - 30_000, - ); - state.supervisorSwarm.interactiveCliUsed = true; - writeInteractiveCliLine(state.supervisorSwarmCliSession, task); - const started = await waitForSupervisorSwarmAutonomousParentRuntime(task); - state.initialRunId = started.runId; - state.initialSessionId = started.sessionId; - } else { - state.initialRunId = requestedRunId; - state.initialSessionId = supervisorSwarmSessionId; - await runCli( - [ - '--agent-enqueue', - '--init', - state.projectRoot, - projectSupervisorAgentId, - state.initialRunId, - task, - ], - { timeoutMs: 120_000 }, - ); - state.isolatedRunner.launchAttempted = true; - } + projectSupervisorAgentId, + state.initialRunId, + task, + ], + { timeoutMs: 120_000 }, + ); + state.isolatedRunner.launchAttempted = true; await claimOwnedRunner(); const runtime = await readRuntime(projectSupervisorAgentId); assert( @@ -114,86 +75,11 @@ export async function runSupervisorSwarmE2e() { await captureSupervisorSwarmToolPlanHandoffRunnerKillCheckpoint(); } await captureSupervisorSwarmInitialProviderBatch(); - await captureSupervisorSwarmCollaborationPolicySnapshotAndDrift(); - await restartSupervisorSwarmRunnerAtInitialBatchBoundary(); await driveSupervisorSwarmToRepairKillBoundary(); if (isSupervisorSwarmFinalReplyTransientRetrySuite()) { await captureSupervisorSwarmTransientRetryCheckpoint(); } await driveSupervisorSwarmRuntimeToCompletion(); - if (isSupervisorSwarmInteractiveChatSuite()) { - await captureSupervisorSwarmAutonomousTurnReport(); - writeInteractiveCliLine(state.supervisorSwarmCliSession, '/quit'); - await waitForInteractiveCliExit(state.supervisorSwarmCliSession, 30_000); - state.supervisorSwarmCliSession = null; - } state.evidence = await validateSupervisorSwarmEvidence(); assert(state.evidence.secretLeakCount === 0, 'loaded-key-leak-detected'); } - -export async function waitForSupervisorSwarmAutonomousParentRuntime(task) { - const deadline = Date.now() + 120_000; - while (Date.now() < deadline) { - const runtime = await readRuntime(projectSupervisorAgentId).catch( - () => null, - ); - if ( - runtime?.agentId === projectSupervisorAgentId && - runtime.sessionId === supervisorSwarmSessionId && - isNonEmptyString(runtime.runId) && - runtime.currentTask === task - ) { - return runtime; - } - if (state.supervisorSwarmCliSession?.closed) { - recordSupervisorSwarmChatSessionFailureDiagnostic( - state.supervisorSwarmCliSession, - ); - throw codedError('supervisor-swarm-autonomous-chat-closed-before-run'); - } - await sleep(50); - } - throw codedError('supervisor-swarm-autonomous-parent-runtime-timeout'); -} - -export async function captureSupervisorSwarmAutonomousTurnReport() { - const session = requireOpenSupervisorSwarmChatSession(); - const output = await waitForInteractiveCliOutput( - session, - (value) => value.includes('[turn.report] '), - 'supervisor-swarm-autonomous-turn-report-timeout', - 60_000, - ); - const reportLines = output - .split(/\r?\n/u) - .filter((line) => line.startsWith('[turn.report] ')); - assert( - reportLines.length === 1, - 'supervisor-swarm-autonomous-turn-report-count-invalid', - ); - const report = JSON.parse(reportLines[0].slice('[turn.report] '.length)); - assert( - isPlainObject(report) && - JSON.stringify(Object.keys(report).sort()) === - JSON.stringify( - [ - 'schemaVersion', - 'outcome', - 'parentAgentId', - 'sessionId', - 'parentRunId', - 'runtimeCount', - 'busyRuntimeCount', - 'pendingTaskCount', - 'runningTaskCount', - 'waitingForConfirmationCount', - 'waitingForUserInputCount', - 'newAssistantMessageCount', - 'finalReplyChars', - 'reconciliationAgentCount', - ].sort(), - ), - 'supervisor-swarm-autonomous-turn-report-shape-invalid', - ); - state.supervisorSwarm.turnReport = report; -} diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/partial-evidence.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/partial-evidence.mjs index d076cf64d..c459c46a7 100644 --- a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/partial-evidence.mjs +++ b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/partial-evidence.mjs @@ -1,5 +1,4 @@ import { - supervisorSwarmMixedSpawnRequestHashesStable, supervisorSwarmParentDeliveries, supervisorSwarmRelevantRunKeys, } from './collaboration-assertions.mjs'; @@ -8,7 +7,6 @@ import { duplicateSupervisorSwarmCollaborationPolicySnapshotCount, inspectSupervisorSwarmCollaborationPolicySnapshot, inspectSupervisorSwarmCollaborationPolicySnapshotBinding, - readSupervisorSwarmCollaborationPolicySnapshotBindingFile, readSupervisorSwarmCollaborationPolicySnapshotFile, supervisorSwarmCollaborationPolicyDriftObservationCount, supervisorSwarmExpectedCollaborationPolicySnapshot, @@ -59,17 +57,9 @@ import { supervisorSwarmQualityAgentId, supervisorSwarmQualityContent, supervisorSwarmQualityPath, - supervisorSwarmSessionId, supervisorSwarmWeakQualityContent, } from './shared.mjs'; -import { - isSupervisorSwarmCollaborationPolicyMixedRecoverySuite, - isSupervisorSwarmFinalReplyTransientRetrySuite, - isSupervisorSwarmMixedHarnessSuite, - isSupervisorSwarmMultiIsolatedHarnessSuite, - supervisorSwarmExpectedIsolatedReviewGroups, - supervisorSwarmInitialIsolatedReviewsForSuite, -} from './suite-selection.mjs'; +import { isSupervisorSwarmFinalReplyTransientRetrySuite } from './suite-selection.mjs'; export async function collectPartialSupervisorSwarmEvidence(baseEvidence) { const persistence = await readSupervisorSwarmPersistence({ @@ -125,127 +115,6 @@ export async function collectPartialSupervisorSwarmEvidence(baseEvidence) { record.recordType === 'agent.runtime.provider_request.lifecycle' && relevantRuns.has(`${record.agentId}\0${record.runId}`), ); - const indexedAgentDb = persistence.agentDb.map((record, index) => ({ - record, - index, - })); - const initialBatchActions = - state.supervisorSwarm.initialProviderBatch?.actions ?? []; - const mixedSpawnAction = initialBatchActions.find( - (action) => action.actionId === state.supervisorSwarm.mixedSpawnActionId, - ); - const matchesMixedInitialAction = (record, action) => - action != null && - record.agentId === projectSupervisorAgentId && - record.runId === state.initialRunId && - record.actionId === action.actionId && - record.actionFingerprint === action.actionFingerprint && - record.tool === action.tool; - const mixedSpawnConfirmationRequirements = indexedAgentDb.filter( - ({ record }) => - record.recordType === - 'agent.runtime.provider_action_batch.confirmation_required' && - record.sessionId === supervisorSwarmSessionId && - record.batchId === state.supervisorSwarm.initialProviderBatch?.batchId && - matchesMixedInitialAction(record, mixedSpawnAction), - ); - const mixedSpawnApprovals = indexedAgentDb.filter( - ({ record }) => - record.recordType === 'agent.runtime.tool_confirmation.approved' && - record.sessionId === supervisorSwarmSessionId && - record.confirmedRunId === state.initialRunId && - matchesMixedInitialAction(record, mixedSpawnAction), - ); - const mixedDelegateExecutionIndexes = initialBatchActions - .filter((action) => action.tool === 'agent.delegate') - .flatMap((action) => - indexedAgentDb - .filter( - ({ record }) => - record.recordType === 'agent.runtime.tool_action.executing' && - matchesMixedInitialAction(record, action), - ) - .map(({ index }) => index), - ); - const mixedSpawnReceiptIndexes = indexedAgentDb - .filter( - ({ record }) => - record.recordType === 'agent.runtime.action_receipt' && - record.sessionId === supervisorSwarmSessionId && - record.status === 'ok' && - matchesMixedInitialAction(record, mixedSpawnAction), - ) - .map(({ index }) => index); - const mixedInitialSpawnConfirmationOrderValid = - mixedSpawnConfirmationRequirements.length === 1 && - mixedSpawnApprovals.length === 1 && - mixedDelegateExecutionIndexes.length === 2 && - mixedSpawnReceiptIndexes.length === 1 && - mixedSpawnConfirmationRequirements[0].index < - mixedSpawnApprovals[0].index && - mixedDelegateExecutionIndexes.every( - (index) => mixedSpawnApprovals[0].index < index, - ) && - mixedSpawnApprovals[0].index < mixedSpawnReceiptIndexes[0]; - const matchesMixedFollowupIdentity = (record) => - isNonEmptyString(state.supervisorSwarm.mixedFollowupSpawnActionId) && - record.agentId === projectSupervisorAgentId && - (record.runId === state.initialRunId || - record.confirmedRunId === state.initialRunId) && - record.actionId === state.supervisorSwarm.mixedFollowupSpawnActionId; - const matchesMixedFollowupAction = (record) => - matchesMixedFollowupIdentity(record) && - (record.tool === 'agent.spawn_isolated' || - record.commandId === 'agent.spawn_isolated'); - const mixedFollowupConfirmationRequirements = indexedAgentDb.filter( - ({ record }) => - record.recordType === - 'agent.runtime.provider_action_batch.confirmation_required' && - record.sessionId === supervisorSwarmSessionId && - matchesMixedFollowupAction(record), - ); - const mixedFollowupApprovals = indexedAgentDb.filter( - ({ record }) => - record.recordType === 'agent.runtime.tool_confirmation.approved' && - record.sessionId === supervisorSwarmSessionId && - matchesMixedFollowupAction(record), - ); - const mixedFollowupSideEffects = indexedAgentDb.filter( - ({ record }) => - record.recordType === 'agent.runtime.agent.spawn_isolated' && - matchesMixedFollowupIdentity(record), - ); - const mixedFollowupReceiptIndexes = indexedAgentDb - .filter( - ({ record }) => - record.recordType === 'agent.runtime.action_receipt' && - record.status === 'ok' && - matchesMixedFollowupAction(record), - ) - .map(({ index }) => index); - const mixedFollowupObservationIndexes = indexedAgentDb - .filter( - ({ record }) => - record.recordType === 'agent.runtime.tool_observation' && - record.status === 'ok' && - matchesMixedFollowupAction(record), - ) - .map(({ index }) => index); - const mixedFollowupSpawnConfirmationOrderValid = - mixedFollowupConfirmationRequirements.length === 1 && - mixedFollowupApprovals.length === 1 && - mixedFollowupSideEffects.length === 1 && - mixedFollowupReceiptIndexes.length === 1 && - mixedFollowupObservationIndexes.length === 1 && - mixedFollowupConfirmationRequirements[0].index < - mixedFollowupApprovals[0].index && - mixedFollowupApprovals[0].index < mixedFollowupSideEffects[0].index && - mixedFollowupSideEffects[0].index < mixedFollowupReceiptIndexes[0] && - mixedFollowupReceiptIndexes[0] < mixedFollowupObservationIndexes[0]; - const mixedSpawnConfirmationOrderValid = - mixedInitialSpawnConfirmationOrderValid && - (!isSupervisorSwarmMultiIsolatedHarnessSuite() || - mixedFollowupSpawnConfirmationOrderValid); const toleratePartialRead = async ( surface, reader, @@ -278,7 +147,6 @@ export async function collectPartialSupervisorSwarmEvidence(baseEvidence) { isolatedEvidenceContents, changedFiles, collaborationPolicySnapshotFile, - collaborationPolicySnapshotBindingFile, collaborationPolicySidecar, finalReplyResponseStream, ] = await Promise.all([ @@ -359,14 +227,6 @@ export async function collectPartialSupervisorSwarmEvidence(baseEvidence) { : null, null, ), - toleratePartialRead( - 'collaboration-policy-snapshot-binding', - () => - supervisorSwarmInitialBatchBindsPolicySnapshot() - ? readSupervisorSwarmCollaborationPolicySnapshotBindingFile() - : null, - null, - ), toleratePartialRead( 'collaboration-policy-sidecar', () => readJson(supervisorSwarmCollaborationPolicyPath()), @@ -469,63 +329,12 @@ export async function collectPartialSupervisorSwarmEvidence(baseEvidence) { partialCollaborationPolicySnapshots[0], supervisorSwarmExpectedCollaborationPolicySnapshot(), ); - const partialCollaborationPolicySnapshotBytesStable = - isNonEmptyString( - state.supervisorSwarm.collaborationPolicySnapshotInitialBytes, - ) && - collaborationPolicySnapshotFile?.bytes === - state.supervisorSwarm.collaborationPolicySnapshotInitialBytes && - hashValue(collaborationPolicySnapshotFile.bytes) === - state.supervisorSwarm.collaborationPolicySnapshotInitialBytesSha256; - const partialCollaborationPolicySnapshotFieldsStable = - isPlainObject( - state.supervisorSwarm.collaborationPolicySnapshotInitialRecord, - ) && - isPlainObject(collaborationPolicySnapshotFile?.snapshot) && - JSON.stringify(collaborationPolicySnapshotFile.snapshot) === - JSON.stringify( - state.supervisorSwarm.collaborationPolicySnapshotInitialRecord, - ) && - JSON.stringify(collaborationPolicySnapshotFile.snapshot) === - JSON.stringify(partialCollaborationPolicySnapshots[0]); - const partialCollaborationPolicySnapshotFingerprintStable = - partialCollaborationPolicySnapshotFieldsStable && - collaborationPolicySnapshotFile.snapshot.snapshotFingerprint === - state.supervisorSwarm.collaborationPolicySnapshotInitialRecord - .snapshotFingerprint && - partialCollaborationPolicySnapshotInspection.identityHash === - state.supervisorSwarm.collaborationPolicySnapshotInitialIdentityHash; - const partialCollaborationPolicySnapshotPolicyFingerprintStable = - partialCollaborationPolicySnapshotFieldsStable && - collaborationPolicySnapshotFile.snapshot.policyFingerprint === - state.supervisorSwarm.collaborationPolicySnapshotInitialRecord - .policyFingerprint; const partialCollaborationPolicySnapshotBindingInspection = inspectSupervisorSwarmCollaborationPolicySnapshotBinding( partialCollaborationPolicySnapshotBindings[0], collaborationPolicySnapshotFile?.snapshot ?? partialCollaborationPolicySnapshots[0], ); - const partialCollaborationPolicySnapshotBindingBytesStable = - isNonEmptyString( - state.supervisorSwarm.collaborationPolicySnapshotBindingInitialBytes, - ) && - collaborationPolicySnapshotBindingFile?.bytes === - state.supervisorSwarm.collaborationPolicySnapshotBindingInitialBytes && - hashValue(collaborationPolicySnapshotBindingFile.bytes) === - state.supervisorSwarm - .collaborationPolicySnapshotBindingInitialBytesSha256; - const partialCollaborationPolicySnapshotBindingFieldsStable = - isPlainObject( - state.supervisorSwarm.collaborationPolicySnapshotBindingInitialRecord, - ) && - isPlainObject(collaborationPolicySnapshotBindingFile?.binding) && - JSON.stringify(collaborationPolicySnapshotBindingFile.binding) === - JSON.stringify( - state.supervisorSwarm.collaborationPolicySnapshotBindingInitialRecord, - ) && - JSON.stringify(collaborationPolicySnapshotBindingFile.binding) === - JSON.stringify(partialCollaborationPolicySnapshotBindings[0]); const partialCollaborationPolicyDriftObservationCount = supervisorSwarmCollaborationPolicyDriftObservationCount( persistence.agentDb, @@ -538,62 +347,9 @@ export async function collectPartialSupervisorSwarmEvidence(baseEvidence) { duplicateSupervisorSwarmCollaborationPolicySnapshotBindingCount( partialCollaborationPolicySnapshotBindings, ); - const partialChatSessionFailureDiagnostic = - state.supervisorSwarm.chatSessionFailureDiagnostic; - const partialIdentityBefore = - state.supervisorSwarm.initialBatchRecoveryPreKillIdentity; - const partialIdentityAfter = - state.supervisorSwarm.initialBatchRecoveryPostRecoveryIdentity; - const partialSideEffectsBefore = - state.supervisorSwarm.initialBatchRecoveryPreKillSideEffects; - const partialSideEffectsAfter = - state.supervisorSwarm.initialBatchRecoveryPostRecoverySideEffects; - const partialRecoveryRequired = - isSupervisorSwarmCollaborationPolicyMixedRecoverySuite(); - const zeroSideEffectFields = [ - 'deliveryCount', - 'claimCount', - 'isolatedGroupCount', - 'isolatedChildCount', - 'isolatedResultCount', - 'isolatedJoinDeliveryCount', - 'delegatedChildTaskCount', - 'projectRevision', - 'projectModifiedPathCount', - 'projectMutationActionCount', - 'initialActionExecutionCount', - 'initialActionReceiptCount', - 'initialActionSideEffectCount', - 'recoveryFailedCount', - ]; - const partialRecoveryZeroSideEffects = - partialRecoveryRequired && - partialSideEffectsBefore != null && - partialSideEffectsAfter != null && - zeroSideEffectFields.every( - (field) => - partialSideEffectsBefore[field] === 0 && - partialSideEffectsAfter[field] === 0, - ); return buildSupervisorSwarmEvidence({ ...baseEvidence, - scenario: partialRecoveryRequired - ? 'project-supervisor-collaboration-policy-mixed-initial-batch-recovery' - : baseEvidence.scenario, - interactiveCliUsed: state.supervisorSwarm.interactiveCliUsed, - chatSessionUnexpectedlyClosed: Boolean(partialChatSessionFailureDiagnostic), - chatSessionFailureKind: - partialChatSessionFailureDiagnostic?.failureKind ?? 'none', - chatSessionExitCode: - partialChatSessionFailureDiagnostic?.exitCode ?? 'none', - chatSessionCloseSignal: - partialChatSessionFailureDiagnostic?.signal ?? 'none', - chatSessionProcessErrorCode: - partialChatSessionFailureDiagnostic?.processErrorCode ?? 'none', - chatSessionStderrChars: - partialChatSessionFailureDiagnostic?.stderrChars ?? 0, - chatSessionStderrSha256: - partialChatSessionFailureDiagnostic?.stderrSha256 ?? 'none', + scenario: baseEvidence.scenario, providerModel: state.supervisorSwarm.effectiveModel, providerApiKind: state.supervisorSwarm.effectiveApiKind, providerReasoningEffort: state.supervisorSwarm.effectiveReasoningEffort, @@ -701,12 +457,8 @@ export async function collectPartialSupervisorSwarmEvidence(baseEvidence) { ?.orchestratorOnlyAfterDelegation ?? false, initialCollaborationContractCountsMatched: partialCollaborationContract?.initialStaticAgentIds?.length === 2 && - partialCollaborationContract?.isolatedSpawnCount === - (isSupervisorSwarmMixedHarnessSuite() ? 1 : 0) && - partialCollaborationContract?.isolatedChildCount === - (isSupervisorSwarmMixedHarnessSuite() - ? supervisorSwarmInitialIsolatedReviewsForSuite().length - : 0), + partialCollaborationContract?.isolatedSpawnCount === 0 && + partialCollaborationContract?.isolatedChildCount === 0, initialCollaborationPolicyFingerprint: partialCollaborationContract?.policyFingerprint ?? null, initialCollaborationContractFingerprint: @@ -750,19 +502,6 @@ export async function collectPartialSupervisorSwarmEvidence(baseEvidence) { partialCollaborationPolicySnapshotInspection.identityHashMatched, collaborationPolicySnapshotBoundAtValid: partialCollaborationPolicySnapshotInspection.boundAtValid, - collaborationPolicySnapshotBytesStable: - partialCollaborationPolicySnapshotBytesStable, - collaborationPolicySnapshotFieldsStable: - partialCollaborationPolicySnapshotFieldsStable, - collaborationPolicySnapshotFingerprintStable: - partialCollaborationPolicySnapshotFingerprintStable, - collaborationPolicySnapshotPolicyFingerprintStable: - partialCollaborationPolicySnapshotPolicyFingerprintStable, - collaborationPolicySnapshotStable: - partialCollaborationPolicySnapshotBytesStable && - partialCollaborationPolicySnapshotFieldsStable && - partialCollaborationPolicySnapshotFingerprintStable && - partialCollaborationPolicySnapshotPolicyFingerprintStable, collaborationPolicySnapshotBindingCaptured: partialCollaborationPolicySnapshotRequired && partialCollaborationPolicySnapshotBindingInspection.exactShape, @@ -781,16 +520,6 @@ export async function collectPartialSupervisorSwarmEvidence(baseEvidence) { partialCollaborationPolicySnapshotBindingInspection.boundAtMatched, collaborationPolicySnapshotBindingSnapshotMatched: partialCollaborationPolicySnapshotBindingInspection.snapshotMatched, - collaborationPolicySnapshotBindingBytesStable: - partialCollaborationPolicySnapshotBindingBytesStable, - collaborationPolicySnapshotBindingFieldsStable: - partialCollaborationPolicySnapshotBindingFieldsStable, - collaborationPolicySnapshotBindingStable: - partialCollaborationPolicySnapshotBindingBytesStable && - partialCollaborationPolicySnapshotBindingFieldsStable && - partialCollaborationPolicySnapshotBindingInspection.snapshotMatched, - collaborationPolicyDriftFixtureWritten: - state.supervisorSwarm.collaborationPolicyDriftFixtureWritten, collaborationPolicySidecarMinIsolatedGroupsBeforeClaim: collaborationPolicySidecar?.minIsolatedGroupsBeforeClaim ?? 0, collaborationPolicyDriftStatusObserved: @@ -805,142 +534,6 @@ export async function collectPartialSupervisorSwarmEvidence(baseEvidence) { residualSidecars.collaborationPolicySnapshotArtifacts, collaborationPolicySnapshotBindingResidualArtifactCount: residualSidecars.collaborationPolicySnapshotBindingArtifacts, - initialBatchRecoveryRequired: partialRecoveryRequired, - initialBatchRecoveryBoundaryObserved: - state.supervisorSwarm.initialBatchRecoveryBoundaryObserved, - initialBatchRecoveryBatchIdStable: - partialRecoveryRequired && - partialIdentityBefore?.batchId === partialIdentityAfter?.batchId, - initialBatchRecoveryPolicyFingerprintStable: - partialRecoveryRequired && - partialIdentityBefore?.policyFingerprint === - partialIdentityAfter?.policyFingerprint, - initialBatchRecoveryContractFingerprintStable: - partialRecoveryRequired && - partialIdentityBefore?.contractFingerprint === - partialIdentityAfter?.contractFingerprint, - initialBatchRecoveryAllActionIdsStable: - partialRecoveryRequired && - JSON.stringify( - partialIdentityBefore?.actions?.map((action) => action.actionId), - ) === - JSON.stringify( - partialIdentityAfter?.actions?.map((action) => action.actionId), - ), - initialBatchRecoveryProviderStartedIdentitySetStable: - partialRecoveryRequired && - JSON.stringify(partialSideEffectsBefore?.providerStartedIdentities) === - JSON.stringify(partialSideEffectsAfter?.providerStartedIdentities), - initialBatchRecoveryWaitingConfirmationStable: - partialRecoveryRequired && - partialIdentityBefore?.status === 'waiting-confirmation' && - partialIdentityBefore?.nextActionIndex === 0 && - partialIdentityAfter?.status === 'waiting-confirmation' && - partialIdentityAfter?.nextActionIndex === 0, - initialBatchRecoveryZeroSideEffects: partialRecoveryZeroSideEffects, - initialBatchRecoveryPreKillDeliveryCount: - partialSideEffectsBefore?.deliveryCount ?? 0, - initialBatchRecoveryPostRecoveryDeliveryCount: - partialSideEffectsAfter?.deliveryCount ?? 0, - initialBatchRecoveryPreKillGroupCount: - partialSideEffectsBefore?.isolatedGroupCount ?? 0, - initialBatchRecoveryPostRecoveryGroupCount: - partialSideEffectsAfter?.isolatedGroupCount ?? 0, - initialBatchRecoveryPreKillChildCount: - partialSideEffectsBefore?.isolatedChildCount ?? 0, - initialBatchRecoveryPostRecoveryChildCount: - partialSideEffectsAfter?.isolatedChildCount ?? 0, - initialBatchRecoveryPreKillProjectRevision: - partialSideEffectsBefore?.projectRevision ?? 0, - initialBatchRecoveryPostRecoveryProjectRevision: - partialSideEffectsAfter?.projectRevision ?? 0, - initialBatchRecoveryPreKillProjectModificationCount: - partialSideEffectsBefore?.projectModifiedPathCount ?? 0, - initialBatchRecoveryPostRecoveryProjectModificationCount: - partialSideEffectsAfter?.projectModifiedPathCount ?? 0, - initialBatchRecoveryPreKillActionExecutionCount: - partialSideEffectsBefore?.initialActionExecutionCount ?? 0, - initialBatchRecoveryPostRecoveryActionExecutionCount: - partialSideEffectsAfter?.initialActionExecutionCount ?? 0, - initialBatchRecoveryPreKillActionReceiptCount: - partialSideEffectsBefore?.initialActionReceiptCount ?? 0, - initialBatchRecoveryPostRecoveryActionReceiptCount: - partialSideEffectsAfter?.initialActionReceiptCount ?? 0, - initialBatchRecoveryConfirmationRestoredCount: - state.supervisorSwarm.initialBatchRecoveryConfirmationRestoredCount, - initialBatchRecoveryRunnerBootChanged: - partialRecoveryRequired && - isNonEmptyString( - state.supervisorSwarm.initialBatchRecoveryOldRunnerBootId, - ) && - isNonEmptyString( - state.supervisorSwarm.initialBatchRecoveryNewRunnerBootId, - ) && - state.supervisorSwarm.initialBatchRecoveryOldRunnerBootId !== - state.supervisorSwarm.initialBatchRecoveryNewRunnerBootId, - initialBatchRecoveryPidfdClaimCount: - state.supervisorSwarm.initialBatchRecoveryPidfdClaimCount, - initialBatchRecoveryPidfdSignalCount: - state.supervisorSwarm.initialBatchRecoveryPidfdSignalCount, - mixedModeEnabled: isSupervisorSwarmMixedHarnessSuite(), - mixedSpawnActionCaptured: isNonEmptyString( - state.supervisorSwarm.mixedSpawnActionId, - ), - mixedFollowupSpawnActionCaptured: isNonEmptyString( - state.supervisorSwarm.mixedFollowupSpawnActionId, - ), - mixedSpawnRequestHashStable: supervisorSwarmMixedSpawnRequestHashesStable( - isolatedRecords.groups, - ), - mixedSpawnConfirmationRequiredCount: - mixedSpawnConfirmationRequirements.length + - mixedFollowupConfirmationRequirements.length, - mixedSpawnApprovalCount: - mixedSpawnApprovals.length + mixedFollowupApprovals.length, - mixedSpawnConfirmationOrderValid, - nativeMixedCollaborationPlanCount: persistence.agentDb.filter( - (record) => - record.recordType === 'agent.runtime.tool_plan.protocol' && - record.agentId === projectSupervisorAgentId && - record.runId === state.initialRunId && - Array.isArray(record.functionNames) && - record.functionNames.filter( - (name) => name === 'runtime_tool_agent_delegate', - ).length === 2 && - record.functionNames.filter( - (name) => name === 'runtime_tool_agent_spawn_isolated', - ).length === 1, - ).length, - nativeFollowupIsolatedPlanCount: - isSupervisorSwarmMultiIsolatedHarnessSuite() - ? persistence.agentDb.filter( - (record) => - record.recordType === 'agent.runtime.tool_plan.protocol' && - record.agentId === projectSupervisorAgentId && - record.runId === state.initialRunId && - Array.isArray(record.functionNames) && - record.functionNames.filter( - (name) => name === 'runtime_tool_agent_delegate', - ).length === 0 && - record.functionNames.filter( - (name) => name === 'runtime_tool_agent_spawn_isolated', - ).length === 1, - ).length - : 0, - staticIsolatedProviderOverlapObserved: - state.supervisorSwarm.staticIsolatedProviderOverlapObserved, - staticIsolatedProviderRequestIdentityCount: new Set( - state.supervisorSwarm.staticIsolatedProviderRequestIds, - ).size, - mixedParentIdentityStable: - isolatedRecords.groups.length === - supervisorSwarmExpectedIsolatedReviewGroups().length && - isolatedRecords.groups.every( - (group) => - group.parentAgentId === projectSupervisorAgentId && - group.parentSessionId === supervisorSwarmSessionId && - group.parentRunId === state.initialRunId, - ), initialDeliveryCount: initial.length, repairDeliveryCount: repairs.length, repairDeliveryStatus: observedRepairStatus, diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/protocol-validation.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/protocol-validation.mjs index dbe6a7882..43c080801 100644 --- a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/protocol-validation.mjs +++ b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/protocol-validation.mjs @@ -22,8 +22,6 @@ import { } from './shared.mjs'; import { isSupervisorSwarmFinalReplyTransientRetrySuite, - isSupervisorSwarmMixedHarnessSuite, - isSupervisorSwarmMultiIsolatedHarnessSuite, isSupervisorSwarmTransientRetrySuite, } from './suite-selection.mjs'; @@ -403,12 +401,10 @@ export function validateSupervisorSwarmNativeProtocol( toolPlanRepairEvidenceHasNoFatalLocalRepair(repairEvidence), 'supervisor-swarm-native-tool-protocol-required', ); - const mixed = isSupervisorSwarmMixedHarnessSuite(); - const expectedActionFunctionCount = mixed ? 3 : 2; + const expectedActionFunctionCount = 2; const allowedInitialFunctionNames = new Set([ 'update_agent_plan', 'runtime_tool_agent_delegate', - ...(mixed ? ['runtime_tool_agent_spawn_isolated'] : []), ]); const initialMultiCall = protocols.filter((record) => { if (!Array.isArray(record.functionNames)) return false; @@ -428,7 +424,7 @@ export function validateSupervisorSwarmNativeProtocol( ).length === 2 && actionFunctionNames.filter( (name) => name === 'runtime_tool_agent_spawn_isolated', - ).length === (mixed ? 1 : 0) && + ).length === 0 && record.functionNames.every((name) => allowedInitialFunctionNames.has(name), ) && @@ -445,40 +441,6 @@ export function validateSupervisorSwarmNativeProtocol( initialMultiCall.length === 1, 'supervisor-swarm-native-collaboration-plan-count-invalid', ); - const followupIsolatedPlans = isSupervisorSwarmMultiIsolatedHarnessSuite() - ? protocols.filter((record) => { - if (!Array.isArray(record.functionNames)) return false; - const actionFunctionNames = record.functionNames.filter((name) => - name.startsWith('runtime_tool_'), - ); - return ( - record.agentId === projectSupervisorAgentId && - record.sessionId === supervisorSwarmSessionId && - record.runId === state.initialRunId && - record.loopIteration > - state.supervisorSwarm.initialProviderBatch.loopIteration && - actionFunctionNames.length === 1 && - actionFunctionNames[0] === 'runtime_tool_agent_spawn_isolated' && - record.functionNames.every((name) => - ['update_agent_plan', 'runtime_tool_agent_spawn_isolated'].includes( - name, - ), - ) && - record.functionNames.filter((name) => name === 'update_agent_plan') - .length <= 1 && - hasValidToolPlanProtocolCallProjection(record) && - record.functionCallCount === record.functionNames.length && - Array.isArray(record.callIdSha256s) && - record.callIdSha256s.length === record.functionCallCount && - new Set(record.callIdSha256s).size === record.callIdSha256s.length - ); - }) - : []; - assert( - !isSupervisorSwarmMultiIsolatedHarnessSuite() || - followupIsolatedPlans.length === 1, - 'supervisor-swarm-native-followup-isolated-plan-count-invalid', - ); return { toolPlanProtocolCount: relevant.filter( (record) => record.recordType === 'agent.runtime.tool_plan.protocol', @@ -496,10 +458,6 @@ export function validateSupervisorSwarmNativeProtocol( (record) => record.protocol === 'text_json', ).length, nativeDualDelegatePlanCount: initialMultiCall.length, - nativeMixedCollaborationPlanCount: isSupervisorSwarmMixedHarnessSuite() - ? initialMultiCall.length - : 0, - nativeFollowupIsolatedPlanCount: followupIsolatedPlans.length, }; } @@ -637,9 +595,7 @@ export function validateSupervisorSwarmActionPersistence( } const initialBatchActions = state.supervisorSwarm.initialProviderBatch?.actions ?? []; - const expectedInitialActionCount = isSupervisorSwarmMixedHarnessSuite() - ? 3 - : 2; + const expectedInitialActionCount = 2; assert( initialBatchActions.length === expectedInitialActionCount, 'supervisor-swarm-initial-batch-action-count-invalid', @@ -651,284 +607,20 @@ export function validateSupervisorSwarmActionPersistence( record.actionId === action.actionId && record.actionFingerprint === action.actionFingerprint && record.tool === action.tool; - let mixedSpawnConfirmationRequiredCount = 0; - let mixedSpawnApprovalCount = 0; - let mixedSpawnConfirmationOrderValid = false; - let mixedInitialSpawnTerminalObservationIndex = -1; - if (isSupervisorSwarmMixedHarnessSuite()) { - const delegateActions = initialBatchActions.filter( - (action) => action.tool === 'agent.delegate', - ); - const spawnActions = initialBatchActions.filter( + assert( + initialBatchActions.every( (action) => - action.tool === 'agent.spawn_isolated' && - action.actionId === state.supervisorSwarm.mixedSpawnActionId, - ); - const spawnAction = spawnActions[0]; - const indexedAgentDb = agentDb.map((record, index) => ({ record, index })); - const initialActionTimeline = (action) => { - const auto = action.tool === 'agent.delegate'; - const executions = indexedAgentDb.filter( - ({ record }) => - record.recordType === 'agent.runtime.tool_action.executing' && - record.executionMode === 'auto' && - matchesInitialBatchAction(record, action), - ); - const autoObserved = indexedAgentDb.filter( - ({ record }) => - record.recordType === 'agent.runtime.tool_action.observed' && - record.executionMode === 'auto' && - record.observationStatus === 'ok' && - matchesInitialBatchAction(record, action), - ); - const receipts = indexedAgentDb.filter( - ({ record }) => - record.recordType === 'agent.runtime.action_receipt' && - record.sessionId === supervisorSwarmSessionId && - record.executionMode === (auto ? 'auto' : 'confirmation') && - record.status === 'ok' && - matchesInitialBatchAction(record, action), - ); - const terminalObservations = indexedAgentDb.filter( - ({ record }) => - record.recordType === 'agent.runtime.tool_observation' && - record.status === 'ok' && - record.decision === (auto ? 'auto' : 'approved') && - matchesInitialBatchAction(record, action), - ); - let sideEffects = []; - if (auto) { - const actionDeliveries = deliveries.filter( - (delivery) => - delivery.parentAgentId === projectSupervisorAgentId && - delivery.parentSessionId === supervisorSwarmSessionId && - delivery.parentRunId === state.initialRunId && - delivery.parentActionId === action.actionId && - delivery.repairOfDelegationId == null, - ); - sideEffects = - actionDeliveries.length === 1 - ? indexedAgentDb.filter( - ({ record }) => - record.recordType === 'agent.runtime.agent.delegate' && - record.parentRunId === state.initialRunId && - record.delegationId === actionDeliveries[0].delegationId && - record.targetAgentId === actionDeliveries[0].targetAgentId && - record.runId === actionDeliveries[0].targetRunId, - ) - : []; - } else { - sideEffects = indexedAgentDb.filter( - ({ record }) => - record.recordType === 'agent.runtime.agent.spawn_isolated' && - record.agentId === projectSupervisorAgentId && + executing.filter((record) => matchesInitialBatchAction(record, action)) + .length === 1 && + receipts.filter( + (record) => record.sessionId === supervisorSwarmSessionId && - record.runId === state.initialRunId && - record.actionId === action.actionId, - ); - } - return { - action, - auto, - executions, - autoObserved, - receipts, - terminalObservations, - sideEffects, - }; - }; - const actionTimelines = initialBatchActions - .map(initialActionTimeline) - .sort( - (left, right) => left.action.actionIndex - right.action.actionIndex, - ); - const delegateTimelines = actionTimelines.filter(({ auto }) => auto); - const spawnTimeline = actionTimelines.find(({ auto }) => !auto); - const confirmationRequired = spawnAction - ? indexedAgentDb.filter( - ({ record }) => - record.recordType === - 'agent.runtime.provider_action_batch.confirmation_required' && - record.agentId === projectSupervisorAgentId && - record.sessionId === supervisorSwarmSessionId && - record.runId === state.initialRunId && - record.batchId === - state.supervisorSwarm.initialProviderBatch.batchId && - record.actionCount === expectedInitialActionCount && - record.actionIndex === spawnAction.actionIndex && - matchesInitialBatchAction(record, spawnAction), - ) - : []; - const approvals = spawnAction - ? indexedAgentDb.filter( - ({ record }) => - record.recordType === 'agent.runtime.tool_confirmation.approved' && - record.sessionId === supervisorSwarmSessionId && - record.confirmedRunId === state.initialRunId && - record.commandId === 'agent.spawn_isolated' && - matchesInitialBatchAction(record, spawnAction), - ) - : []; - const actionLifecycleOrderValid = actionTimelines.every((timeline) => { - if ( - timeline.sideEffects.length !== 1 || - timeline.receipts.length !== 1 || - timeline.terminalObservations.length !== 1 - ) { - return false; - } - if (timeline.auto) { - return ( - timeline.executions.length === 1 && - timeline.autoObserved.length === 1 && - timeline.executions[0].index < timeline.sideEffects[0].index && - timeline.sideEffects[0].index < timeline.autoObserved[0].index && - timeline.autoObserved[0].index < timeline.receipts[0].index && - timeline.receipts[0].index < timeline.terminalObservations[0].index - ); - } - return ( - timeline.executions.length === 0 && - timeline.autoObserved.length === 0 && - approvals.length === 1 && - approvals[0].index < timeline.sideEffects[0].index && - timeline.sideEffects[0].index < timeline.receipts[0].index && - timeline.receipts[0].index < timeline.terminalObservations[0].index - ); - }); - const batchActionOrderValid = actionTimelines.every((timeline, index) => { - if (timeline.action.actionIndex !== index) return false; - if (index === 0) return true; - const previousEnd = - actionTimelines[index - 1].terminalObservations[0]?.index; - const currentStart = timeline.auto - ? timeline.executions[0]?.index - : timeline.sideEffects[0]?.index; - return ( - Number.isSafeInteger(previousEnd) && - Number.isSafeInteger(currentStart) && - previousEnd < currentStart - ); - }); - mixedSpawnConfirmationRequiredCount = confirmationRequired.length; - mixedSpawnApprovalCount = approvals.length; - mixedInitialSpawnTerminalObservationIndex = - spawnTimeline?.terminalObservations[0]?.index ?? -1; - mixedSpawnConfirmationOrderValid = - confirmationRequired.length === 1 && - approvals.length === 1 && - confirmationRequired[0].index < approvals[0].index && - actionTimelines.every( - (timeline) => - approvals[0].index < - (timeline.auto - ? timeline.executions[0]?.index - : timeline.sideEffects[0]?.index), - ) && - actionLifecycleOrderValid && - batchActionOrderValid; - assert( - delegateActions.length === 2 && - new Set(delegateActions.map((action) => action.actionId)).size === 2 && - spawnActions.length === 1 && - spawnAction.executionMode === 'confirmation' && - delegateActions.every((action) => action.executionMode === 'auto') && - delegateTimelines.length === 2 && - spawnTimeline != null && - mixedSpawnConfirmationRequiredCount === 1 && - mixedSpawnApprovalCount === 1 && - mixedSpawnConfirmationOrderValid, - 'supervisor-swarm-mixed-initial-batch-action-persistence-invalid', - ); - } else { - assert( - initialBatchActions.every( - (action) => - executing.filter((record) => + record.status === 'ok' && matchesInitialBatchAction(record, action), - ).length === 1 && - receipts.filter( - (record) => - record.sessionId === supervisorSwarmSessionId && - record.status === 'ok' && - matchesInitialBatchAction(record, action), - ).length === 1, - ), - 'supervisor-swarm-initial-batch-action-persistence-invalid', - ); - } - if (isSupervisorSwarmMultiIsolatedHarnessSuite()) { - const followupActionId = state.supervisorSwarm.mixedFollowupSpawnActionId; - assert( - isNonEmptyString(followupActionId) && - followupActionId !== state.supervisorSwarm.mixedSpawnActionId, - 'supervisor-swarm-followup-spawn-action-missing', - ); - const indexedAgentDb = agentDb.map((record, index) => ({ record, index })); - const matchesFollowupIdentity = (record) => - record.agentId === projectSupervisorAgentId && - (record.runId === state.initialRunId || - record.confirmedRunId === state.initialRunId) && - record.actionId === followupActionId; - const matchesFollowupToolRecord = (record) => - matchesFollowupIdentity(record) && - (record.tool === 'agent.spawn_isolated' || - record.commandId === 'agent.spawn_isolated'); - const confirmationRequired = indexedAgentDb.filter( - ({ record }) => - record.recordType === - 'agent.runtime.provider_action_batch.confirmation_required' && - record.sessionId === supervisorSwarmSessionId && - matchesFollowupToolRecord(record), - ); - const approvals = indexedAgentDb.filter( - ({ record }) => - record.recordType === 'agent.runtime.tool_confirmation.approved' && - record.sessionId === supervisorSwarmSessionId && - matchesFollowupToolRecord(record), - ); - const sideEffects = indexedAgentDb.filter( - ({ record }) => - record.recordType === 'agent.runtime.agent.spawn_isolated' && - record.sessionId === supervisorSwarmSessionId && - matchesFollowupIdentity(record), - ); - const followupReceipts = indexedAgentDb.filter( - ({ record }) => - record.recordType === 'agent.runtime.action_receipt' && - record.sessionId === supervisorSwarmSessionId && - record.executionMode === 'confirmation' && - record.status === 'ok' && - matchesFollowupToolRecord(record), - ); - const terminalObservations = indexedAgentDb.filter( - ({ record }) => - record.recordType === 'agent.runtime.tool_observation' && - record.status === 'ok' && - record.decision === 'approved' && - matchesFollowupToolRecord(record), - ); - const followupOrderValid = - confirmationRequired.length === 1 && - approvals.length === 1 && - sideEffects.length === 1 && - followupReceipts.length === 1 && - terminalObservations.length === 1 && - mixedInitialSpawnTerminalObservationIndex >= 0 && - mixedInitialSpawnTerminalObservationIndex < - confirmationRequired[0].index && - confirmationRequired[0].index < approvals[0].index && - approvals[0].index < sideEffects[0].index && - sideEffects[0].index < followupReceipts[0].index && - followupReceipts[0].index < terminalObservations[0].index; - assert( - followupOrderValid, - 'supervisor-swarm-followup-spawn-action-persistence-invalid', - ); - mixedSpawnConfirmationRequiredCount += confirmationRequired.length; - mixedSpawnApprovalCount += approvals.length; - mixedSpawnConfirmationOrderValid &&= followupOrderValid; - } + ).length === 1, + ), + 'supervisor-swarm-initial-batch-action-persistence-invalid', + ); const matchesRecoveredRepairPending = (record) => record.agentId === state.supervisorSwarm.repairTargetAgentId && record.runId === state.supervisorSwarm.repairTargetRunId && @@ -957,9 +649,6 @@ export function validateSupervisorSwarmActionPersistence( duplicateReceiptCount, duplicateExecutingActionIdCount, isolatedMutationActionCount: isolatedMutationActions.length, - mixedSpawnConfirmationRequiredCount, - mixedSpawnApprovalCount, - mixedSpawnConfirmationOrderValid, recoveredRepairPendingActionCount: recoveredRepairConfirmationLifecycle.receipts.length, failedRepairActionCount: repairAttempts.failedRepairActions.length, diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/repair-recovery.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/repair-recovery.mjs index 79274d129..e2b644de0 100644 --- a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/repair-recovery.mjs +++ b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/repair-recovery.mjs @@ -6,14 +6,10 @@ import { assertSupervisorSwarmRuntimeHealthy, assertSupervisorSwarmWeakQualityDelivery, observeSupervisorSwarmInitialProviderOverlap, - observeSupervisorSwarmStaticIsolatedProviderOverlap, - supervisorSwarmMixedIsolatedClaimsReady, supervisorSwarmParentDeliveries, supervisorSwarmRelevantRunKeys, supervisorSwarmRepairAttemptEvidence, - validateSupervisorSwarmMixedIsolatedPersistence, } from './collaboration-assertions.mjs'; -import { validateSupervisorSwarmIsolatedSpawnInput } from './collaboration-policy.mjs'; import { readSupervisorSwarmResidualSidecarCounts, supervisorSwarmResidualSidecarsEmpty, @@ -33,8 +29,6 @@ import { codedError, confirmPendingActions, findPendingActions, - hashValue, - isNonEmptyString, isPlainObject, killRunnerOnce, path, @@ -45,7 +39,6 @@ import { sleep, state, supervisorSwarmConfirmedTools, - supervisorSwarmIsolatedReviews, supervisorSwarmQualityAgentId, supervisorSwarmQualityPath, supervisorSwarmSessionId, @@ -55,11 +48,7 @@ import { } from './shared.mjs'; import { isSupervisorSwarmInitialTransientRetrySuite, - isSupervisorSwarmInteractiveChatSuite, - isSupervisorSwarmMixedHarnessSuite, - isSupervisorSwarmMultiIsolatedHarnessSuite, isSupervisorSwarmTransientRetrySuite, - supervisorSwarmExpectedIsolatedReviewGroups, } from './suite-selection.mjs'; export async function confirmSupervisorSwarmPendingActions( @@ -68,50 +57,9 @@ export async function confirmSupervisorSwarmPendingActions( ) { const before = state.confirmedActionIds.size; const allowedTools = new Set(supervisorSwarmConfirmedTools); - if (isSupervisorSwarmMixedHarnessSuite()) { - allowedTools.add('agent.spawn_isolated'); - } await confirmPendingActions(allowedTools, (pending) => { const runKey = `${pending.agentId}\0${pending.runId}`; const deferred = deferredRunKeys.has(runKey); - if (pending.tool === 'agent.spawn_isolated') { - const initialSpawn = - pending.actionId === state.supervisorSwarm.mixedSpawnActionId; - if (!initialSpawn) { - assert( - isSupervisorSwarmMultiIsolatedHarnessSuite(), - 'supervisor-swarm-followup-spawn-outside-multi-group-suite', - ); - const requestHash = validateSupervisorSwarmIsolatedSpawnInput( - pending.action?.input, - 1, - 'supervisor-swarm-mixed-followup', - supervisorSwarmExpectedIsolatedReviewGroups(), - ); - assert( - isNonEmptyString(state.supervisorSwarm.mixedSpawnActionId) && - pending.actionId !== state.supervisorSwarm.mixedSpawnActionId && - (state.supervisorSwarm.mixedFollowupSpawnActionId == null || - state.supervisorSwarm.mixedFollowupSpawnActionId === - pending.actionId) && - (state.supervisorSwarm.mixedFollowupSpawnRequestHash == null || - state.supervisorSwarm.mixedFollowupSpawnRequestHash === - requestHash), - 'supervisor-swarm-followup-spawn-identity-invalid', - ); - state.supervisorSwarm.mixedFollowupSpawnActionId = pending.actionId; - state.supervisorSwarm.mixedFollowupSpawnRequestHash = requestHash; - } - assert( - isSupervisorSwarmMixedHarnessSuite() && - pending.agentId === projectSupervisorAgentId && - pending.runId === state.initialRunId && - (initialSpawn || - pending.actionId === - state.supervisorSwarm.mixedFollowupSpawnActionId), - 'supervisor-swarm-unexpected-spawn-confirmation', - ); - } assert( confirmRunKeys.has(runKey) || deferred, 'supervisor-swarm-unexpected-pending-run', @@ -121,14 +69,7 @@ export async function confirmSupervisorSwarmPendingActions( pending.agentId === projectSupervisorAgentId && pending.runId === state.initialRunId; assert( - !isParentRun || - pending.tool === 'project.verify' || - (isSupervisorSwarmMixedHarnessSuite() && - pending.tool === 'agent.spawn_isolated' && - [ - state.supervisorSwarm.mixedSpawnActionId, - state.supervisorSwarm.mixedFollowupSpawnActionId, - ].includes(pending.actionId)), + !isParentRun || pending.tool === 'project.verify', 'supervisor-swarm-parent-pending-tool-invalid', ); return true; @@ -255,9 +196,6 @@ export async function restartSupervisorSwarmRunnerAtRepairBoundary( repair, repairPending, ) { - const mixedState = isSupervisorSwarmMixedHarnessSuite() - ? validateSupervisorSwarmMixedIsolatedPersistence(persistence) - : null; const initial = supervisorSwarmParentDeliveries( persistence.deliveries, ).filter((delivery) => delivery.repairOfDelegationId == null); @@ -290,11 +228,6 @@ export async function restartSupervisorSwarmRunnerAtRepairBoundary( (delivery) => task.agentId === delivery.targetAgentId && task.runId === delivery.targetRunId, - ) || - (mixedState?.instances ?? []).some( - (instance) => - task.agentId === instance.instanceId && - task.runId === instance.runId, ), ) .map(supervisorSwarmTaskIdentity) @@ -312,14 +245,10 @@ export async function restartSupervisorSwarmRunnerAtRepairBoundary( .map(supervisorSwarmClaimIdentity) .sort((left, right) => left.actionId.localeCompare(right.actionId)); assert( - preKillTaskIdentities.length === (mixedState ? 7 : 4) && - preKillClaimIdentities.length === 1, + preKillTaskIdentities.length === 4 && preKillClaimIdentities.length === 1, 'supervisor-swarm-pre-kill-work-or-claim-identity-invalid', ); - const relevantRuns = supervisorSwarmRelevantRunKeys( - parentDeliveries, - mixedState?.instances ?? [], - ); + const relevantRuns = supervisorSwarmRelevantRunKeys(parentDeliveries); const preKillProviderStartedIdentities = supervisorSwarmProviderStartedIdentities(persistence.agentDb, relevantRuns); const preKillParentContext = persistence.contextBundles.find( @@ -353,12 +282,6 @@ export async function restartSupervisorSwarmRunnerAtRepairBoundary( .sort((left, right) => left.delegationId.localeCompare(right.delegationId)); state.supervisorSwarm.preKillClaimIdentities = preKillClaimIdentities; state.supervisorSwarm.preKillTaskIdentities = preKillTaskIdentities; - state.supervisorSwarm.preKillMixedIdentity = mixedState - ? { - isolated: mixedState.identity, - parentContextSha256: hashValue(JSON.stringify(preKillParentContext)), - } - : null; state.supervisorSwarm.runnerKillBoundaryObserved = true; await killRunnerOnce(); @@ -437,21 +360,11 @@ export async function restartSupervisorSwarmRunnerAtRepairBoundary( candidate.tool === repairPending.tool, ); if (recoveredPending) { - const currentMixedState = isSupervisorSwarmMixedHarnessSuite() - ? validateSupervisorSwarmMixedIsolatedPersistence(after) - : null; - const currentParentContext = after.contextBundles.find( - (bundle) => - bundle.agentId === projectSupervisorAgentId && - bundle.sessionId === supervisorSwarmSessionId && - bundle.runId === state.initialRunId, - ); const currentProviderStartedIdentities = supervisorSwarmProviderStartedIdentities( after.agentDb, supervisorSwarmRelevantRunKeys( supervisorSwarmParentDeliveries(after.deliveries), - currentMixedState?.instances ?? [], ), ); assert( @@ -468,15 +381,7 @@ export async function restartSupervisorSwarmRunnerAtRepairBoundary( JSON.stringify(currentProviderStartedIdentities) === JSON.stringify( state.supervisorSwarm.preKillProviderStartedIdentities, - ) && - (!currentMixedState || - (JSON.stringify(currentMixedState.identity) === - JSON.stringify( - state.supervisorSwarm.preKillMixedIdentity?.isolated, - ) && - hashValue(JSON.stringify(currentParentContext)) === - state.supervisorSwarm.preKillMixedIdentity - ?.parentContextSha256)), + ), 'supervisor-swarm-recovery-identity-or-provider-replay-invalid', ); state.identityStable = true; @@ -500,84 +405,18 @@ export async function driveSupervisorSwarmToRepairKillBoundary() { (delivery) => delivery.repairOfDelegationId == null, ); assert(initial.length <= 2, 'supervisor-swarm-extra-initial-delivery'); - if ( - isSupervisorSwarmMixedHarnessSuite() && - isNonEmptyString(state.supervisorSwarm.mixedSpawnActionId) && - !state.confirmedActionIds.has(state.supervisorSwarm.mixedSpawnActionId) - ) { - const pending = await findPendingActions(); - if ( - pending.some( - (candidate) => - candidate.agentId === projectSupervisorAgentId && - candidate.runId === state.initialRunId && - candidate.actionId === state.supervisorSwarm.mixedSpawnActionId && - candidate.tool === 'agent.spawn_isolated', - ) - ) { - await confirmSupervisorSwarmPendingActions( - new Set([`${projectSupervisorAgentId}\0${state.initialRunId}`]), - new Set( - initial.map( - (delivery) => - `${delivery.targetAgentId}\0${delivery.targetRunId}`, - ), - ), - ); - } - } - if (isSupervisorSwarmMultiIsolatedHarnessSuite()) { - const pending = await findPendingActions(); - const followupSpawnPending = pending.find( - (candidate) => - candidate.agentId === projectSupervisorAgentId && - candidate.runId === state.initialRunId && - candidate.tool === 'agent.spawn_isolated' && - candidate.actionId !== state.supervisorSwarm.mixedSpawnActionId, - ); - if ( - followupSpawnPending && - !state.confirmedActionIds.has(followupSpawnPending.actionId) - ) { - await confirmSupervisorSwarmPendingActions( - new Set([`${projectSupervisorAgentId}\0${state.initialRunId}`]), - new Set( - parentDeliveries.map( - (delivery) => - `${delivery.targetAgentId}\0${delivery.targetRunId}`, - ), - ), - ); - } - } if (initial.length === 2) { const { quality } = assertSupervisorSwarmInitialDeliveries(initial); observeSupervisorSwarmInitialProviderOverlap( persistence.agentDb, initial, ); - const isolatedInstances = - supervisorSwarmParentIsolatedRecords(persistence).instances; - observeSupervisorSwarmStaticIsolatedProviderOverlap( - persistence.agentDb, - initial, - isolatedInstances, - ); - if ( - state.supervisorSwarm.initialProviderOverlapObserved && - (!isSupervisorSwarmMixedHarnessSuite() || - state.supervisorSwarm.staticIsolatedProviderOverlapObserved) - ) { + if (state.supervisorSwarm.initialProviderOverlapObserved) { const confirmRunKeys = new Set( initial.map( (delivery) => `${delivery.targetAgentId}\0${delivery.targetRunId}`, ), ); - if (isSupervisorSwarmInteractiveChatSuite()) { - confirmRunKeys.add( - `${projectSupervisorAgentId}\0${state.initialRunId}`, - ); - } await confirmSupervisorSwarmPendingActions( confirmRunKeys, new Set( @@ -685,13 +524,6 @@ export async function driveSupervisorSwarmToRepairKillBoundary() { } return; } - if (isSupervisorSwarmMixedHarnessSuite()) { - if (!supervisorSwarmMixedIsolatedClaimsReady(persistence)) { - await sleep(50); - continue; - } - validateSupervisorSwarmMixedIsolatedPersistence(persistence); - } await restartSupervisorSwarmRunnerAtRepairBoundary( persistence, repair, @@ -790,29 +622,6 @@ export async function driveSupervisorSwarmRuntimeToCompletion() { delivery.delegationId === task.delegationId, ), ); - const isolatedRecords = supervisorSwarmParentIsolatedRecords(persistence); - observeSupervisorSwarmStaticIsolatedProviderOverlap( - persistence.agentDb, - deliveries.filter((delivery) => delivery.repairOfDelegationId == null), - isolatedRecords.instances, - ); - const mixedReady = - !isSupervisorSwarmMixedHarnessSuite() || - (isolatedRecords.groups.length === - supervisorSwarmExpectedIsolatedReviewGroups().length && - isolatedRecords.instances.length === - supervisorSwarmIsolatedReviews.length && - isolatedRecords.results.length === - supervisorSwarmIsolatedReviews.length && - isolatedRecords.results.every( - (record) => record.result?.status === 'completed', - ) && - isolatedRecords.joinDeliveries.length === - supervisorSwarmExpectedIsolatedReviewGroups().length && - isolatedRecords.joinDeliveries.every( - (delivery) => delivery.status === 'claimed-by-parent', - ) && - validateSupervisorSwarmMixedIsolatedPersistence(persistence)); const terminalStateObserved = deliveries.length === 3 && deliveries.every((delivery) => delivery.status === 'claimed-by-parent') && @@ -820,7 +629,6 @@ export async function driveSupervisorSwarmRuntimeToCompletion() { childTasks.every( (task) => task.status === 'completed' && task.phase === 'completed', ) && - Boolean(mixedReady) && parentRuntime?.runId === state.initialRunId && parentRuntime?.sessionId === supervisorSwarmSessionId && parentRuntime?.status === 'idle' && diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/setup.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/setup.mjs index 3d4c3d4f6..e39cdb29e 100644 --- a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/setup.mjs +++ b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/setup.mjs @@ -5,8 +5,6 @@ import { commandPassedMarker, configFileName, fs, - hashValue, - mainAgentId, path, randomUUID, readJson, @@ -14,9 +12,6 @@ import { seedDisposableProject, state, supervisorCollaborationPolicySchemaVersion, - supervisorSwarmAutonomousRoutingTerms, - supervisorSwarmAutonomousTask, - supervisorSwarmCollaborationPolicyControlTerms, supervisorSwarmDeniedMutationTools, supervisorSwarmDesignAgentId, supervisorSwarmDesignContent, @@ -29,61 +24,15 @@ import { supervisorSwarmQualityMarker, supervisorSwarmQualityPath, supervisorSwarmWeakQualityContent, + visibleText, } from './shared.mjs'; -import { - isSupervisorSwarmAutonomousChatSuite, - isSupervisorSwarmInteractiveChatSuite, - isSupervisorSwarmMixedHarnessSuite, - isSupervisorSwarmMultiIsolatedHarnessSuite, - isSupervisorSwarmTransientRetrySuite, - supervisorSwarmInitialIsolatedReviewsForSuite, -} from './suite-selection.mjs'; + +export function supervisorSwarmVerificationFixtureSource() { + return `import fs from 'node:fs';\nconst html = fs.readFileSync('game/index.html', 'utf8');\nconst agents = fs.readFileSync('AGENTS.md', 'utf8');\nconst optionalExact = (file, expected) => !fs.existsSync(file) || fs.readFileSync(file, 'utf8') === expected;\nconst passed = html.includes(${JSON.stringify(visibleText)}) && html.includes(' output.includes('Agent Swarm Chat'), - 'user-input-cli-banner-timeout', - 30_000, - ); - writeInteractiveCliLine(state.userInputCliSession, task); - - const pending = await waitForPendingUserInputRequest(); - await waitForInteractiveCliOutput( - state.userInputCliSession, - (output) => - output.includes(`[Needs input] agent=${projectSupervisorAgentId}`) && - output.includes(`request=${pending.requestId}`), - 'user-input-cli-question-timeout', - 120_000, - ); - await claimOwnedRunner(); - const beforeKillRunner = await readRunnerStatus(); - state.userInput.oldRunnerBootId = runnerBootId(beforeKillRunner); - assert( - isNonEmptyString(state.userInput.oldRunnerBootId), - 'user-input-runner-boot-before-kill-missing', - ); - await captureUserInputWaitingBoundary('before-kill'); - await killRunnerOnce(); - await runCli(['--agent-resume', state.projectRoot], { timeoutMs: 120_000 }); - state.resumed = true; - const restarted = await waitForRunnerBootChange( - state.userInput.oldRunnerBootId, - ); - state.userInput.newRunnerBootId = runnerBootId(restarted); - await claimOwnedRunner(restarted); - await captureUserInputWaitingBoundary('after-restart'); - await sleep(750); - await captureUserInputWaitingBoundary('after-stable-window'); - - writeInteractiveCliLine(state.userInputCliSession, userInputAnswerText); - await answerRemainingInteractiveQuestions(state.userInputCliSession); - await waitForInteractiveCliOutput( - state.userInputCliSession, - (output) => output.includes(`[\u5df2\u56de\u7b54] ${pending.requestId}`), - 'user-input-cli-answer-timeout', - 60_000, - ); - await waitForUserInputRuntimeCompletion(); - await waitForInteractiveCliOutput( - state.userInputCliSession, - (output) => - output.includes('\nAgent> ') || - output.includes( - '[\u672c\u8f6e\u7ed3\u675f] 父 Agent 回复已完整流式输出。', - ), - 'user-input-cli-final-reply-timeout', - 120_000, - ); - writeInteractiveCliLine(state.userInputCliSession, '/quit'); - await waitForInteractiveCliExit(state.userInputCliSession, 30_000); - state.userInputCliSession = null; - - state.identityStable = true; - state.evidence = await validateUserInputRuntimeEvidence(); - assert(state.evidence.secretLeakCount === 0, 'loaded-key-leak-detected'); -} - -export async function readUserInputSidecars() { - const files = ( - await listFiles(path.join(state.projectRoot, '.agent/runtime/user-input')) - ) - .filter((file) => file.endsWith('.json')) - .sort(); - return Promise.all( - files.map(async (file) => ({ file, record: await readJson(file) })), - ); -} - -export async function readUserInputPersistence() { - const runtimeStatePath = path.join( - state.projectRoot, - '.agent/runtime/agents', - `${projectSupervisorAgentId}.json`, - ); - const [ - taskSnapshot, - events, - agentDb, - activity, - output, - runtimeState, - sidecars, - ] = await Promise.all([ - readTaskSnapshot(), - readAllRuntimeEvents(), - readOptionalJsonl(path.join(state.projectRoot, '.agent/agent.db')), - readOptionalJsonl(path.join(state.projectRoot, '.agent/activity.jsonl')), - readOptionalJsonl(path.join(state.projectRoot, '.agent/output.jsonl')), - readJson(runtimeStatePath).catch(() => null), - readUserInputSidecars(), - ]); - const sessionId = runtimeState?.sessionId ?? state.initialSessionId; - const conversations = isNonEmptyString(sessionId) - ? await readOptionalJsonl( - agentConversationPath(projectSupervisorAgentId, sessionId), - ) - : []; - return { - taskSnapshot, - events, - agentDb, - activity, - output, - runtimeState, - sidecars, - conversations, - }; -} - -export function userInputProviderLifecycleStarted(agentDb) { - return agentDb.filter( - (record) => - record.recordType === 'agent.runtime.provider_request.lifecycle' && - record.agentId === projectSupervisorAgentId && - record.runId === state.initialRunId && - record.status === 'started', - ); -} - -export async function waitForPendingUserInputRequest() { - const deadline = Date.now() + 8 * 60 * 1000; - while (Date.now() < deadline) { - const persistence = await readUserInputPersistence(); - const { runtimeState, sidecars, conversations, taskSnapshot, agentDb } = - persistence; - const latest = taskSnapshot.latest.find( - (task) => - task.agentId === projectSupervisorAgentId && - task.runId === runtimeState?.runId, - ); - if (latest && isFailedTask(latest)) { - throw codedError('user-input-runtime-failed-before-question'); - } - if ( - runtimeState?.agentId === projectSupervisorAgentId && - runtimeState.status === 'waiting-for-user-input' && - runtimeState.phase === 'waiting-for-user-input' && - sidecars.length === 1 - ) { - const record = sidecars[0].record; - assert( - record.schemaVersion === 'game-creator-runtime-user-input.v1' && - record.agentId === projectSupervisorAgentId && - record.runId === runtimeState.runId && - record.sessionId === runtimeState.sessionId && - record.status === 'pending' && - Array.isArray(record.questions) && - record.questions.length === 1 && - record.responseId == null && - Object.keys(record.answers ?? {}).length === 0, - 'user-input-pending-sidecar-invalid', - ); - const questionMessages = conversations.filter( - (message) => - message.role === 'assistant' && - message.messageId === record.questionMessageId, - ); - assert( - questionMessages.length === 1 && - questionMessages[0].content.includes(record.questions[0].question), - 'user-input-question-conversation-invalid', - ); - state.initialRunId = runtimeState.runId; - state.initialSessionId = runtimeState.sessionId; - state.userInput.requestId = record.requestId; - state.userInput.actionId = record.actionId; - state.userInput.questionMessageId = record.questionMessageId; - state.userInput.questionCount = record.questions.length; - state.userInput.optionCount = record.questions.reduce( - (count, question) => count + question.options.length, - 0, - ); - state.userInput.privateValues = [ - ...record.questions.map((question) => question.question), - ...record.questions.flatMap((question) => - question.options.map((option) => option.description), - ), - ].filter(isNonEmptyString); - state.userInput.providerStartedBeforeKill = - userInputProviderLifecycleStarted(agentDb).length; - state.userInput.conversationCountBeforeKill = conversations.length; - assert( - state.userInput.providerStartedBeforeKill > 0, - 'user-input-provider-planning-lifecycle-missing', - ); - return record; - } - if (runtimeState?.phase === 'needs-reconciliation') { - throw codedError('user-input-runtime-needs-reconciliation'); - } - await sleep(250); - } - throw codedError('user-input-question-timeout'); -} - -export async function captureUserInputWaitingBoundary(stage) { - const persistence = await readUserInputPersistence(); - const { runtimeState, sidecars, conversations, agentDb } = persistence; - assert( - runtimeState?.agentId === projectSupervisorAgentId && - runtimeState.runId === state.initialRunId && - runtimeState.sessionId === state.initialSessionId && - runtimeState.status === 'waiting-for-user-input' && - runtimeState.phase === 'waiting-for-user-input' && - sidecars.length === 1 && - sidecars[0].record.requestId === state.userInput.requestId && - sidecars[0].record.actionId === state.userInput.actionId && - sidecars[0].record.status === 'pending' && - sidecars[0].record.responseId == null && - conversations.length === state.userInput.conversationCountBeforeKill, - `user-input-${stage}-waiting-boundary-invalid`, - ); - const providerStarted = userInputProviderLifecycleStarted(agentDb).length; - assert( - providerStarted === state.userInput.providerStartedBeforeKill, - `user-input-${stage}-provider-called-while-waiting`, - ); - if (stage !== 'before-kill') { - state.userInput.providerStartedAfterRestart = providerStarted; - state.userInput.conversationCountAfterRestart = conversations.length; - } -} - -export async function waitForUserInputRuntimeCompletion() { - const deadline = Date.now() + 8 * 60 * 1000; - while (Date.now() < deadline) { - const persistence = await readUserInputPersistence(); - const { runtimeState, sidecars, taskSnapshot, conversations } = persistence; - if (sidecars.length > 1) { - throw codedError('user-input-unexpected-second-request'); - } - const latest = taskSnapshot.latest.find( - (task) => - task.agentId === projectSupervisorAgentId && - task.runId === state.initialRunId, - ); - if (latest && isFailedTask(latest)) { - throw codedError('user-input-runtime-failed-after-answer'); - } - if ( - runtimeState?.runId === state.initialRunId && - runtimeState.sessionId === state.initialSessionId && - runtimeState.status === 'idle' && - runtimeState.phase === 'completed' && - latest?.status === 'completed' && - latest.phase === 'completed' && - sidecars.length === 1 && - sidecars[0].record.status === 'answered' - ) { - const record = sidecars[0].record; - state.userInput.responseId = record.responseId; - state.userInput.answerMessageId = record.answerMessageId; - const finalAssistants = conversations.filter( - (message) => - message.role === 'assistant' && - message.messageId !== record.questionMessageId, - ); - if (finalAssistants.length === 1) return persistence; - } - if (runtimeState?.phase === 'needs-reconciliation') { - throw codedError('user-input-runtime-needs-reconciliation-after-answer'); - } - await sleep(250); - } - throw codedError('user-input-completion-timeout'); -} - -export function validateUserInputProviderLifecycle(agentDb) { - const lifecycle = agentDb.filter( - (record) => - record.recordType === 'agent.runtime.provider_request.lifecycle' && - record.agentId === projectSupervisorAgentId && - record.runId === state.initialRunId, - ); - const byRequest = new Map(); - for (const record of lifecycle) { - assert( - isNonEmptyString(record.requestId) && - isNonEmptyString(record.requestKind) && - isNonEmptyString(record.requestSlot), - 'user-input-provider-lifecycle-identity-invalid', - ); - const records = byRequest.get(record.requestId) ?? []; - records.push(record); - byRequest.set(record.requestId, records); - } - for (const records of byRequest.values()) { - assert( - records.length === 2 && - records[0].status === 'started' && - ['completed', 'failed', 'interrupted'].includes(records[1].status) && - records[0].requestKind === records[1].requestKind && - records[0].requestSlot === records[1].requestSlot && - records[0].runId === records[1].runId, - 'user-input-provider-lifecycle-sequence-invalid', - ); - } - const started = lifecycle.filter((record) => record.status === 'started'); - assert( - started.length >= 2 && - started.length === byRequest.size && - state.userInput.providerStartedBeforeKill === - state.userInput.providerStartedAfterRestart, - 'user-input-provider-lifecycle-count-invalid', - ); - return { - requestIdentityCount: byRequest.size, - startedCount: started.length, - terminalCount: lifecycle.length - started.length, - }; -} - -export async function validateUserInputRuntimeEvidence() { - const persistence = await readUserInputPersistence(); - const { - taskSnapshot, - events, - agentDb, - activity, - output, - runtimeState, - sidecars, - conversations, - } = persistence; - assert(sidecars.length === 1, 'user-input-sidecar-count-invalid'); - const sidecar = sidecars[0].record; - const latest = taskSnapshot.latest.find( - (task) => - task.agentId === projectSupervisorAgentId && - task.runId === state.initialRunId, - ); - assert( - runtimeState?.agentId === projectSupervisorAgentId && - runtimeState.runId === state.initialRunId && - runtimeState.sessionId === state.initialSessionId && - runtimeState.status === 'idle' && - runtimeState.phase === 'completed' && - latest?.status === 'completed' && - latest.phase === 'completed', - 'user-input-final-runtime-identity-invalid', - ); - assert( - sidecar.schemaVersion === 'game-creator-runtime-user-input.v1' && - sidecar.agentId === projectSupervisorAgentId && - sidecar.runId === state.initialRunId && - sidecar.sessionId === state.initialSessionId && - sidecar.requestId === state.userInput.requestId && - sidecar.actionId === state.userInput.actionId && - sidecar.status === 'answered' && - sidecar.responseId === state.userInput.responseId && - sidecar.questionMessageId === state.userInput.questionMessageId && - sidecar.answerMessageId === state.userInput.answerMessageId && - sidecar.questions.length === 1 && - Object.keys(sidecar.answers).length === 1 && - Object.values(sidecar.answers)[0] === userInputAnswerText, - 'user-input-final-sidecar-invalid', - ); - const questionMessages = conversations.filter( - (message) => - message.role === 'assistant' && - message.messageId === sidecar.questionMessageId, - ); - const answerMessages = conversations.filter( - (message) => - message.role === 'user' && message.messageId === sidecar.answerMessageId, - ); - const finalAssistants = conversations.filter( - (message) => - message.role === 'assistant' && - message.messageId !== sidecar.questionMessageId, - ); - assert( - questionMessages.length === 1 && - answerMessages.length === 1 && - answerMessages[0].content.includes(userInputAnswerCanary) && - finalAssistants.length === 1, - 'user-input-conversation-cardinality-invalid', - ); - const duplicateMessageCount = duplicateCount( - conversations.map((message) => message.messageId).filter(Boolean), - ); - assert(duplicateMessageCount === 0, 'user-input-duplicate-message-identity'); - const observations = agentDb.filter( - (record) => - record.recordType === 'agent.runtime.user_input.answered' && - record.agentId === projectSupervisorAgentId && - record.runId === state.initialRunId && - record.actionId === sidecar.actionId && - record.requestId === sidecar.requestId, - ); - assert( - observations.length === 1 && - JSON.stringify(observations[0]).includes('answerCount=1'), - 'user-input-public-observation-count-invalid', - ); - const lifecycle = validateUserInputProviderLifecycle(agentDb); - const completedAudits = agentDb.filter( - (record) => - record.recordType === 'agent.runtime.completed' && - record.agentId === projectSupervisorAgentId && - record.runId === state.initialRunId, - ); - assert( - completedAudits.length === 1, - 'user-input-completed-audit-count-invalid', - ); - const finalizationFiles = ( - await listFiles( - path.join(state.projectRoot, '.agent/runtime/finalizations'), - ) - ).filter((file) => file.endsWith('.json')); - assert( - finalizationFiles.length === 0, - 'user-input-finalization-journal-present', - ); - assert( - countExactSecrets( - Buffer.from(finalAssistants.map((message) => message.content).join('\n')), - disposableProjectPathVariants(), - ) === 0, - 'user-input-final-assistant-project-path-leak', - ); - - const publicSurfaces = { - event: events, - agentDb, - activity, - output, - runtimeState, - }; - const privateValues = [ - userInputAnswerCanary, - userInputAnswerText, - ...state.userInput.privateValues, - ].filter(isNonEmptyString); - const taskPrivateValues = privateValues.filter( - (value) => !buildUserInputTaskPrompt().includes(value), - ); - const privateBodyPublicCounts = countSensitiveValuesBySurface( - publicSurfaces, - privateValues, - 'user-input-private-body-public', - ); - const taskPrivateBodyPublicCounts = countSensitiveValuesBySurface( - { task: taskSnapshot.all }, - taskPrivateValues, - 'user-input-private-body-public', - ); - const apiKeyPublicCounts = countSensitiveValuesBySurface( - { task: taskSnapshot.all, ...publicSurfaces }, - state.secrets, - 'user-input-api-key-public', - ); - const projectPathPublicCounts = countSensitiveValuesBySurface( - { task: taskSnapshot.all, ...publicSurfaces }, - disposableProjectPathVariants(), - 'user-input-project-path-public', - ); - const sidecarSecretLeakCount = countExactSecrets( - Buffer.from(JSON.stringify(sidecar)), - state.secrets, - ); - assert( - sidecarSecretLeakCount === 0, - 'user-input-sidecar-secret-leak-detected', - ); - const secretLeakCount = await countSecretsInProject( - state.projectRoot, - state.secrets, - ); - assert(secretLeakCount === 0, 'user-input-project-secret-leak-detected'); - - return { - scenario: 'project-supervisor-needs-input-runner-restart', - targetAgentId: projectSupervisorAgentId, - providerModel: 'gpt-5.5', - isolatedAppDataUsed: true, - formalConfigCliCallCount: state.isolatedRunner.sourceConfigCliCallCount, - sourceRunnerEndpointUnchanged: false, - sourceConfigHardlinkCount: state.isolatedRunner.configLinks.length, - sourceConfigLinksVerified: false, - taskCount: taskSnapshot.all.length, - eventCount: events.length, - agentDbRecordCount: agentDb.length, - conversationMessageCount: conversations.length, - targetRunCount: new Set( - taskSnapshot.all - .filter((task) => task.agentId === projectSupervisorAgentId) - .map((task) => task.runId), - ).size, - stableSessionCount: new Set( - taskSnapshot.all - .filter((task) => task.agentId === projectSupervisorAgentId) - .map((task) => task.sessionId), - ).size, - userInputSidecarCount: sidecars.length, - userInputQuestionCount: sidecar.questions.length, - userInputOptionCount: state.userInput.optionCount, - userInputAnswerCount: Object.keys(sidecar.answers).length, - userInputQuestionMessageCount: questionMessages.length, - userInputAnswerMessageCount: answerMessages.length, - finalAssistantCount: finalAssistants.length, - completedAuditCount: completedAudits.length, - toolObservationCount: observations.length, - providerRequestIdentityCount: lifecycle.requestIdentityCount, - providerLifecycleStartedCount: lifecycle.startedCount, - providerLifecycleTerminalCount: lifecycle.terminalCount, - providerStartedBeforeRunnerKill: state.userInput.providerStartedBeforeKill, - providerStartedAfterRunnerRestart: - state.userInput.providerStartedAfterRestart, - providerCalledWhileWaiting: false, - conversationCountBeforeRunnerKill: - state.userInput.conversationCountBeforeKill, - conversationCountAfterRunnerRestart: - state.userInput.conversationCountAfterRestart, - runnerBootChanged: - state.userInput.oldRunnerBootId !== state.userInput.newRunnerBootId, - duplicateMessageCount, - finalizationJournalCount: finalizationFiles.length, - privateBodyPublicLeakCount: - sumObjectValues(privateBodyPublicCounts) + - sumObjectValues(taskPrivateBodyPublicCounts), - apiKeyPublicLeakCount: sumObjectValues(apiKeyPublicCounts), - projectPathPublicLeakCount: sumObjectValues(projectPathPublicCounts), - projectPathPublicSurfaceCount: Object.keys(projectPathPublicCounts).length, - userInputSidecarSecretLeakCount: sidecarSecretLeakCount, - userInputReportLeakCount: state.userInput.reportLeakCount, - userInputRunnerKillMethod: null, - userInputRunnerPidfdClaimCount: state.isolatedRunner.pidfdClaimCount, - userInputRunnerPidfdSignalCount: state.isolatedRunner.pidfdSignalCount, - userInputRunnerStopped: false, - userInputAppDataCleanupPerformed: false, - secretLeakCount, - lureLeakCount: state.lureLeakCount, - paths: [ - '.agent/runtime/user-input', - '.agent/runtime/tasks', - '.agent/runtime/events', - '.agent/agent.db', - '.agent/conversations', - ], - }; -} - -export async function collectPartialUserInputEvidence() { - const persistence = await readUserInputPersistence(); - return { - taskCount: persistence.taskSnapshot.all.length, - eventCount: persistence.events.length, - agentDbRecordCount: persistence.agentDb.length, - conversationMessageCount: persistence.conversations.length, - userInputSidecarCount: persistence.sidecars.length, - userInputQuestionCount: - persistence.sidecars[0]?.record?.questions?.length ?? 0, - userInputAnswerCount: Object.keys( - persistence.sidecars[0]?.record?.answers ?? {}, - ).length, - finalAssistantCount: persistence.conversations.filter( - (message) => - message.role === 'assistant' && - message.messageId !== - persistence.sidecars[0]?.record?.questionMessageId, - ).length, - }; -} - -export function emptyUserInputEvidence() { - return { - scenario: 'project-supervisor-needs-input-runner-restart', - targetAgentId: projectSupervisorAgentId, - providerModel: 'gpt-5.5', - isolatedAppDataUsed: false, - formalConfigCliCallCount: 0, - sourceRunnerEndpointUnchanged: false, - sourceConfigHardlinkCount: 0, - sourceConfigLinksVerified: false, - taskCount: 0, - eventCount: 0, - agentDbRecordCount: 0, - conversationMessageCount: 0, - targetRunCount: 0, - stableSessionCount: 0, - userInputSidecarCount: 0, - userInputQuestionCount: 0, - userInputOptionCount: 0, - userInputAnswerCount: 0, - userInputQuestionMessageCount: 0, - userInputAnswerMessageCount: 0, - finalAssistantCount: 0, - completedAuditCount: 0, - toolObservationCount: 0, - providerRequestIdentityCount: 0, - providerLifecycleStartedCount: 0, - providerLifecycleTerminalCount: 0, - providerStartedBeforeRunnerKill: 0, - providerStartedAfterRunnerRestart: 0, - providerCalledWhileWaiting: false, - conversationCountBeforeRunnerKill: 0, - conversationCountAfterRunnerRestart: 0, - runnerBootChanged: false, - duplicateMessageCount: 0, - finalizationJournalCount: 0, - privateBodyPublicLeakCount: 0, - apiKeyPublicLeakCount: 0, - projectPathPublicLeakCount: 0, - projectPathPublicSurfaceCount: 0, - userInputSidecarSecretLeakCount: 0, - userInputReportLeakCount: 0, - userInputRunnerKillMethod: null, - userInputRunnerPidfdClaimCount: 0, - userInputRunnerPidfdSignalCount: 0, - userInputRunnerStopped: false, - userInputAppDataCleanupPerformed: false, - secretLeakCount: 0, - lureLeakCount: 0, - paths: [], - }; -} - -export function isUserInputRuntimeSuite() { - return state.suite === userInputRuntimeSuite; -} diff --git a/apps/ai-game-creator-shell/scripts/agent-swarm-test-chat.mjs b/apps/ai-game-creator-shell/scripts/agent-swarm-test-chat.mjs deleted file mode 100644 index 45656fc67..000000000 --- a/apps/ai-game-creator-shell/scripts/agent-swarm-test-chat.mjs +++ /dev/null @@ -1,2149 +0,0 @@ -import { spawn } from 'node:child_process'; -import { randomUUID } from 'node:crypto'; -import { constants as fsConstants } from 'node:fs'; -import { - chmod, - copyFile, - lstat, - mkdir, - mkdtemp, - open, - readdir, - readFile, - realpath, - rm, - writeFile, -} from 'node:fs/promises'; -import os from 'node:os'; -import path from 'node:path'; -import { createInterface } from 'node:readline/promises'; -import { fileURLToPath, pathToFileURL } from 'node:url'; -import { inflateSync } from 'node:zlib'; - -import { AGC_APP_IDENTIFIER } from './channel-identity.mjs'; - -// 联调工具驱动的始终是默认渠道客户端:安装身份取渠道基线,不跟随发布渠道。 -export const appIdentifier = AGC_APP_IDENTIFIER; -export const configFileName = 'game-creator.config.json'; -export const localConfigFileName = 'game-creator.config.local.json'; -export const runnerEndpointFileName = 'agent-runner.endpoint.json'; -export const testProjectPrefix = 'genarrative-agc-swarm-test-'; -export const testProjectSentinelName = '.agc-swarm-test.json'; -export const testProjectSentinelSchema = - 'genarrative-agc-swarm-test-project.v1'; -export const testRuntimeConfigPrefix = 'genarrative-agc-swarm-config-'; -export const testRuntimeConfigSentinelName = '.agc-swarm-config.json'; -export const testRuntimeConfigSentinelSchema = - 'genarrative-agc-swarm-test-config.v1'; -export const ungeneratedGameEntryMarker = - '还没有生成游戏。回到聊天输入创意并确认生成后'; -export const defaultRealSwarmTestTask = - '制作一个可直接试玩的原创植物塔防小游戏:玩家选择并放置原创守卫阻挡敌人,完成波次后可以进入下一关并重新开始。主题、单位名称与视觉语言必须原创,不使用任何现有游戏角色、单位名、Logo 或受保护视觉语言。请自主完成正式产物、静态检查和双视口试玩验证。'; -export const swarmTurnReportPrefix = '[turn.report] '; -export const swarmTurnReportSchema = 'game-creator-swarm-turn-report.v1'; - -const swarmTurnReportKeys = [ - 'schemaVersion', - 'outcome', - 'parentAgentId', - 'sessionId', - 'parentRunId', - 'runtimeCount', - 'busyRuntimeCount', - 'pendingTaskCount', - 'runningTaskCount', - 'waitingForConfirmationCount', - 'waitingForUserInputCount', - 'newAssistantMessageCount', - 'finalReplyChars', - 'reconciliationAgentCount', -].sort(); -const settledZeroCountFields = [ - 'busyRuntimeCount', - 'pendingTaskCount', - 'runningTaskCount', - 'waitingForConfirmationCount', - 'waitingForUserInputCount', - 'reconciliationAgentCount', -]; - -const requiredFormalArtifactSpecs = [ - { path: 'memory/project.md', kind: 'file' }, - { path: 'game/game_design.md', kind: 'file' }, - { path: 'game/balance.json', kind: 'json' }, - { path: 'assets/manifest.art.json', kind: 'json' }, - { path: 'assets/manifest.audio.json', kind: 'json' }, - { path: 'game/index.html', kind: 'file' }, - { path: 'exports/README.md', kind: 'file' }, -]; -const editorImageArtifactSpecs = [ - { path: 'assets/ui-prototype.png', kind: 'image', aspectRatio: 16 / 9 }, - { path: 'assets/art-spritesheet.png', kind: 'image', aspectRatio: 1 }, -]; -export const requiredSwarmManifestTaskIds = Object.freeze([ - 'design-director', - 'design-foundation', - 'balance-director', - 'balance-seed', - 'art-director', - 'art-asset-plan', - 'art-polish', - 'audio-director', - 'audio-asset-plan', - 'code-director', - 'code-prototype', - 'quality-review', - 'preview-readiness', - 'preview-playtest', - 'publish-strategy', - 'publish-package', -]); - -const appRoot = path.resolve(fileURLToPath(new URL('..', import.meta.url))); -const cargoManifestPath = path.join(appRoot, 'src-tauri', 'Cargo.toml'); -const configWizardPath = path.join( - appRoot, - 'scripts', - 'game-creator-config-wizard.mjs', -); -const cargoCommand = process.platform === 'win32' ? 'cargo.exe' : 'cargo'; -const childTerminationGraceMs = 10_000; -const childForceTerminationWaitMs = 5_000; -const runnerShutdownTimeoutMs = 20_000; -const cleanupDirectoryTimeoutMs = 10_000; -const maximumValidatedPngBytes = 64 * 1024 * 1024; -const maximumValidatedPngPixels = 100_000_000; -const maximumInflatedPngBytes = 256 * 1024 * 1024; -const minimumMarkdownBodyCharacters = 24; -const minimumHtmlCharacters = 120; -const incompleteArtifactTextPattern = - /\b(?:todo|tbd|placeholder|coming[\t ]+soon|lorem[\t ]+ipsum)\b|待补充|待完善|占位|尚未完成|稍后补充|待填写|待验证|待复核|待确认|待定/iu; -const uncheckedMarkdownChecklistPattern = - /^[\t ]*(?:>[\t ]*)*(?:[-+*]|\d+[.)])[\t ]+\[[\t ]\](?:[\t ]|$)/mu; - -export function hasIncompleteArtifactMarker(content) { - return ( - incompleteArtifactTextPattern.test(content) || - uncheckedMarkdownChecklistPattern.test(content) - ); -} - -export const usage = `用法: - npm run agc:test:chat - npm run agc:test:chat:manual -- [选项] - -自动读取客户端 AppData 配置并复制到隔离目录,创建一次性项目后进入 Project Supervisor 自主测试。 -带 --task 时完成正式产物验收后自动退出;手工聊天模式完成后启动持续预览。 - -选项: - --config-dir <绝对路径> 显式指定客户端 AppData 配置来源目录 - --project-dir <绝对路径> 使用已有项目或空目录,不自动删除 - --keep-project 保留自动创建的一次性项目 - --no-open 手工模式启动预览但不自动打开浏览器 - --task <需求> 通过 manual 入口非交互提交自定义需求 - --timeout-minutes <分钟> 设置本次执行期限;自动任务默认 50 分钟,手工模式默认不限时 - --dry-run 只检查目录发现和项目准备,不启动 LLM - -h, --help 显示帮助 -`; - -function readOptionValue(args, index, option) { - const value = args[index + 1]?.trim(); - if (!value || value.startsWith('--')) { - throw new Error(`${option} 缺少路径`); - } - return value; -} - -function readTimeoutMinutes(args, index, option) { - const value = readOptionValue(args, index, option); - if (!/^[1-9]\d*$/u.test(value)) { - throw new Error(`${option} 必须是 1-1440 的整数分钟`); - } - const minutes = Number(value); - if (!Number.isSafeInteger(minutes) || minutes > 1_440) { - throw new Error(`${option} 必须是 1-1440 的整数分钟`); - } - return minutes; -} - -export function parseSwarmTestArguments(args) { - const options = { - configDir: null, - projectDir: null, - keepProject: false, - openBrowser: true, - task: null, - timeoutMinutes: null, - dryRun: false, - help: false, - }; - for (let index = 0; index < args.length; index += 1) { - const argument = args[index]; - if (argument === '--config-dir') { - if (options.configDir) throw new Error('--config-dir 只能指定一次'); - options.configDir = readOptionValue(args, index, argument); - index += 1; - } else if (argument === '--project-dir') { - if (options.projectDir) throw new Error('--project-dir 只能指定一次'); - options.projectDir = readOptionValue(args, index, argument); - index += 1; - } else if (argument === '--keep-project') { - options.keepProject = true; - } else if (argument === '--no-open') { - options.openBrowser = false; - } else if (argument === '--task') { - if (options.task) throw new Error('--task 只能指定一次'); - const task = readOptionValue(args, index, argument); - if (task.length > 4_000) throw new Error('--task 不能超过 4000 字符'); - options.task = task; - index += 1; - } else if (argument === '--timeout-minutes') { - if (options.timeoutMinutes !== null) { - throw new Error('--timeout-minutes 只能指定一次'); - } - options.timeoutMinutes = readTimeoutMinutes(args, index, argument); - index += 1; - } else if (argument === '--dry-run') { - options.dryRun = true; - } else if (argument === '--help' || argument === '-h') { - options.help = true; - } else { - throw new Error(`未知选项:${argument}`); - } - } - return options; -} - -export function shouldStartPersistentPreview(options) { - return !options.task; -} - -export function resolveSwarmTestTimeoutMs(options) { - const minutes = options.timeoutMinutes ?? (options.task ? 50 : null); - return minutes === null ? null : minutes * 60_000; -} - -function pushUnique(values, value) { - if (value && !values.includes(value)) values.push(value); -} - -export function defaultRuntimeConfigDirCandidates({ - platform = process.platform, - environment = process.env, - homeDirectory = os.homedir(), -} = {}) { - const candidates = []; - if (platform === 'win32') { - pushUnique( - candidates, - environment.APPDATA - ? path.win32.join(environment.APPDATA, appIdentifier) - : path.win32.join(homeDirectory, 'AppData', 'Roaming', appIdentifier), - ); - if (environment.LOCALAPPDATA) { - pushUnique( - candidates, - path.win32.join(environment.LOCALAPPDATA, appIdentifier), - ); - } - } else if (platform === 'darwin') { - pushUnique( - candidates, - path.posix.join( - homeDirectory, - 'Library', - 'Application Support', - appIdentifier, - ), - ); - } else { - const configuredRoot = environment.XDG_CONFIG_HOME; - const posixAbsoluteConfiguredRoot = - configuredRoot && path.posix.isAbsolute(configuredRoot); - const hostAbsoluteConfiguredRoot = - configuredRoot && - !posixAbsoluteConfiguredRoot && - path.isAbsolute(configuredRoot); - const configRoot = - configuredRoot && - (posixAbsoluteConfiguredRoot || hostAbsoluteConfiguredRoot) - ? configuredRoot - : path.posix.join(homeDirectory, '.config'); - pushUnique( - candidates, - hostAbsoluteConfiguredRoot - ? path.join(configRoot, appIdentifier) - : path.posix.join(configRoot, appIdentifier), - ); - } - return candidates; -} - -async function isRegularFileWithoutSymlink(filePath) { - const metadata = await lstat(filePath).catch((error) => { - if (error?.code === 'ENOENT') return null; - throw error; - }); - return Boolean(metadata?.isFile() && !metadata.isSymbolicLink()); -} - -export async function discoverRuntimeConfigDir( - explicitConfigDir, - platformContext, -) { - if (explicitConfigDir && !path.isAbsolute(explicitConfigDir)) { - throw new Error('--config-dir 必须是绝对路径'); - } - const candidates = explicitConfigDir - ? [explicitConfigDir] - : defaultRuntimeConfigDirCandidates(platformContext); - for (const candidate of candidates) { - const resolved = path.resolve(candidate); - const metadata = await lstat(resolved).catch((error) => { - if (error?.code === 'ENOENT') return null; - throw error; - }); - if (!metadata?.isDirectory() || metadata.isSymbolicLink()) continue; - if ( - !(await isRegularFileWithoutSymlink(path.join(resolved, configFileName))) - ) { - continue; - } - return realpath(resolved); - } - if (explicitConfigDir) { - throw new Error( - `指定目录中没有可用的 ${configFileName}:${explicitConfigDir}`, - ); - } - throw new Error( - `未找到客户端 AppData 配置。请运行 npm run agc:config,或启动 npm run agc 后在“运行时配置”中保存 LLM Provider。`, - ); -} - -export function canPromptForMissingRuntimeConfig( - stdinIsTty = process.stdin.isTTY, - stdoutIsTty = process.stdout.isTTY, -) { - return Boolean(stdinIsTty && stdoutIsTty); -} - -async function askToConfigureMissingRuntime() { - if (!canPromptForMissingRuntimeConfig()) return false; - const readline = createInterface({ - input: process.stdin, - output: process.stdout, - }); - try { - const answer = ( - await readline.question( - '未找到客户端 AppData 配置,是否现在进入安全配置向导? [Y/n]: ', - ) - ) - .trim() - .toLowerCase(); - return !answer || ['y', 'yes', '是'].includes(answer); - } finally { - readline.close(); - } -} - -export function buildMissingConfigWizardArguments(explicitConfigDir) { - return [ - configWizardPath, - '--configure-only', - ...(explicitConfigDir ? ['--config-dir', explicitConfigDir] : []), - ]; -} - -async function runMissingConfigWizard(setActiveChild, explicitConfigDir) { - const child = spawnChild( - process.execPath, - buildMissingConfigWizardArguments(explicitConfigDir), - { - stdio: 'inherit', - }, - ); - setActiveChild(child); - const result = await childExit(child); - setActiveChild(null); - if (result.code !== 0 || result.signal) { - throw new Error( - `配置向导未正常完成:code=${result.code ?? ''} signal=${result.signal ?? ''}`, - ); - } -} - -async function secureWindowsPrivateRuntimePath( - targetPath, - options, - secureWindowsPath = null, -) { - if (secureWindowsPath) { - await secureWindowsPath(targetPath, options); - return; - } - const { secureWindowsGameCreatorPathForCurrentUser } = await import( - './game-creator-config-wizard.mjs' - ); - await secureWindowsGameCreatorPathForCurrentUser(targetPath, options); -} - -async function copyPrivateRuntimeConfigEntry( - sourceConfigDir, - runtimeConfigDir, - fileName, - required, - secureWindowsPath = null, -) { - const sourcePath = path.join(sourceConfigDir, fileName); - const sourceMetadata = await lstat(sourcePath).catch((error) => { - if (error?.code === 'ENOENT') return null; - throw error; - }); - if (!sourceMetadata) { - if (required) throw new Error(`配置来源缺少 ${fileName}`); - return false; - } - if (!sourceMetadata.isFile() || sourceMetadata.isSymbolicLink()) { - throw new Error(`配置来源必须是无符号链接普通文件:${fileName}`); - } - - const destinationPath = path.join(runtimeConfigDir, fileName); - if (process.platform === 'win32') { - const sourceBytes = await readFile(sourcePath); - const destinationFile = await open(destinationPath, 'wx', 0o600); - try { - await secureWindowsPrivateRuntimePath( - destinationPath, - { isDirectory: false }, - secureWindowsPath, - ); - await destinationFile.writeFile(sourceBytes); - await destinationFile.sync(); - } finally { - await destinationFile.close(); - } - } else { - await copyFile(sourcePath, destinationPath, fsConstants.COPYFILE_EXCL); - await chmod(destinationPath, 0o600); - } - const [sourceMetadataAfterCopy, destinationMetadata] = await Promise.all([ - lstat(sourcePath), - lstat(destinationPath), - ]); - if ( - !sourceMetadataAfterCopy.isFile() || - sourceMetadataAfterCopy.isSymbolicLink() || - sourceMetadataAfterCopy.dev !== sourceMetadata.dev || - sourceMetadataAfterCopy.ino !== sourceMetadata.ino || - sourceMetadataAfterCopy.size !== sourceMetadata.size || - sourceMetadataAfterCopy.mtimeMs !== sourceMetadata.mtimeMs || - !destinationMetadata.isFile() || - destinationMetadata.isSymbolicLink() || - (process.platform !== 'win32' && - ((destinationMetadata.mode & 0o077) !== 0 || - (destinationMetadata.dev === sourceMetadata.dev && - destinationMetadata.ino === sourceMetadata.ino))) - ) { - throw new Error(`隔离配置副本身份或权限无效:${fileName}`); - } - return true; -} - -export async function prepareSwarmTestRuntimeConfig( - sourceConfigDir, - tempRoot, - { secureWindowsPath = null } = {}, -) { - if (!path.isAbsolute(sourceConfigDir)) { - throw new Error('配置来源目录必须是绝对路径'); - } - const sourceMetadata = await lstat(sourceConfigDir).catch((error) => { - if (error?.code === 'ENOENT') return null; - throw error; - }); - if (!sourceMetadata?.isDirectory() || sourceMetadata.isSymbolicLink()) { - throw new Error(`配置来源必须是无符号链接普通目录:${sourceConfigDir}`); - } - const canonicalSourceConfigDir = await realpath(sourceConfigDir); - const canonicalTempRoot = await realpath( - path.resolve(tempRoot ?? os.tmpdir()), - ); - const runtimeConfigDir = await mkdtemp( - path.join(canonicalTempRoot, testRuntimeConfigPrefix), - ); - try { - if (process.platform === 'win32') { - await secureWindowsPrivateRuntimePath( - runtimeConfigDir, - { isDirectory: true }, - secureWindowsPath, - ); - } else { - await chmod(runtimeConfigDir, 0o700); - } - const sentinelToken = randomUUID(); - await writeFile( - path.join(runtimeConfigDir, testRuntimeConfigSentinelName), - `${JSON.stringify({ - schemaVersion: testRuntimeConfigSentinelSchema, - token: sentinelToken, - })}\n`, - { flag: 'wx', mode: 0o600 }, - ); - await copyPrivateRuntimeConfigEntry( - canonicalSourceConfigDir, - runtimeConfigDir, - configFileName, - true, - secureWindowsPath, - ); - await copyPrivateRuntimeConfigEntry( - canonicalSourceConfigDir, - runtimeConfigDir, - localConfigFileName, - false, - secureWindowsPath, - ); - return { - path: await realpath(runtimeConfigDir), - sourcePath: canonicalSourceConfigDir, - owned: true, - sentinelToken, - }; - } catch (error) { - await rm(runtimeConfigDir, { recursive: true, force: true }); - throw error; - } -} - -const cleanupDirectoryChildProgram = String.raw` -const { rm } = require('node:fs/promises'); -const target = process.argv[1]; -rm(target, { recursive: true, force: false }).catch((error) => { - process.stderr.write(String(error && error.message || error)); - process.exitCode = 1; -}); -`; - -export async function removeDirectoryWithTimeout( - directoryPath, - { - timeoutMs = cleanupDirectoryTimeoutMs, - childProgram = cleanupDirectoryChildProgram, - } = {}, -) { - const child = spawnChild( - process.execPath, - ['-e', childProgram, directoryPath], - { - stdio: ['ignore', 'ignore', 'ignore'], - }, - ); - const result = await childExitWithTimeout( - child, - timeoutMs, - `清理目录 ${path.basename(directoryPath)}`, - { graceMs: 1_000, forceWaitMs: 2_000 }, - ); - if (result.code !== 0 || result.signal) { - throw new Error( - `清理目录失败:${path.basename(directoryPath)} code=${result.code ?? ''} signal=${result.signal ?? ''}`, - ); - } -} - -export async function cleanupSwarmTestRuntimeConfig(runtimeConfig) { - if (!runtimeConfig?.owned) return false; - const sentinelPath = path.join( - runtimeConfig.path, - testRuntimeConfigSentinelName, - ); - if (!(await isRegularFileWithoutSymlink(sentinelPath))) { - throw new Error('拒绝清理:隔离配置哨兵缺失或类型无效'); - } - let sentinel; - try { - sentinel = JSON.parse(await readFile(sentinelPath, 'utf8')); - } catch (error) { - throw new Error(`拒绝清理:隔离配置哨兵无效:${error.message}`); - } - if ( - sentinel.schemaVersion !== testRuntimeConfigSentinelSchema || - sentinel.token !== runtimeConfig.sentinelToken - ) { - throw new Error('拒绝清理:隔离配置哨兵身份不匹配'); - } - const canonical = await realpath(runtimeConfig.path); - if ( - canonical !== runtimeConfig.path || - canonical === runtimeConfig.sourcePath || - !path.basename(canonical).startsWith(testRuntimeConfigPrefix) - ) { - throw new Error('拒绝清理:隔离配置目录身份不匹配'); - } - const endpointMetadata = await lstat( - path.join(canonical, runnerEndpointFileName), - ).catch((error) => { - if (error?.code === 'ENOENT') return null; - throw error; - }); - if (endpointMetadata) { - throw new Error('拒绝清理:隔离 Agent Runner 尚未退出'); - } - await removeDirectoryWithTimeout(canonical); - return true; -} - -async function ensureExplicitProject(projectDir) { - if (!path.isAbsolute(projectDir)) { - throw new Error('--project-dir 必须是绝对路径'); - } - const resolved = path.resolve(projectDir); - const metadata = await lstat(resolved).catch((error) => { - if (error?.code === 'ENOENT') return null; - throw error; - }); - if (!metadata) { - await mkdir(resolved, { recursive: true, mode: 0o700 }); - } else if (!metadata.isDirectory() || metadata.isSymbolicLink()) { - throw new Error(`测试项目路径必须是普通目录:${resolved}`); - } - const canonical = await realpath(resolved); - const entries = await readdir(canonical); - const initialized = await isRegularFileWithoutSymlink( - path.join(canonical, '.agent', 'manifest.json'), - ); - if (entries.length > 0 && !initialized) { - throw new Error(`--project-dir 只能指向空目录或已初始化项目:${canonical}`); - } - return { - path: canonical, - owned: false, - sentinelToken: null, - }; -} - -export async function prepareSwarmTestProject(explicitProjectDir, tempRoot) { - if (explicitProjectDir) return ensureExplicitProject(explicitProjectDir); - const root = path.resolve(tempRoot ?? os.tmpdir()); - const projectPath = await mkdtemp(path.join(root, testProjectPrefix)); - try { - if (process.platform !== 'win32') await chmod(projectPath, 0o700); - const sentinelToken = randomUUID(); - await writeFile( - path.join(projectPath, testProjectSentinelName), - `${JSON.stringify({ - schemaVersion: testProjectSentinelSchema, - token: sentinelToken, - })}\n`, - { flag: 'wx', mode: 0o600 }, - ); - return { - path: await realpath(projectPath), - owned: true, - sentinelToken, - }; - } catch (error) { - await rm(projectPath, { recursive: true, force: true }); - throw error; - } -} - -export async function cleanupSwarmTestProject(project) { - if (!project?.owned) return false; - const sentinelPath = path.join(project.path, testProjectSentinelName); - if (!(await isRegularFileWithoutSymlink(sentinelPath))) { - throw new Error('拒绝清理:一次性项目哨兵缺失或类型无效'); - } - let sentinel; - try { - sentinel = JSON.parse(await readFile(sentinelPath, 'utf8')); - } catch (error) { - throw new Error(`拒绝清理:一次性项目哨兵无效:${error.message}`); - } - if ( - sentinel.schemaVersion !== testProjectSentinelSchema || - sentinel.token !== project.sentinelToken - ) { - throw new Error('拒绝清理:一次性项目哨兵身份不匹配'); - } - const canonical = await realpath(project.path); - if ( - canonical !== project.path || - !path.basename(canonical).startsWith(testProjectPrefix) - ) { - throw new Error('拒绝清理:一次性项目目录身份不匹配'); - } - await removeDirectoryWithTimeout(canonical); - return true; -} - -export function buildCargoCliArguments(cliArguments) { - // `--quiet` only silences cargo's own build chatter; compiler errors and the - // CLI's stdout still come through. Without it the crate's several hundred - // dead-code warnings are reprinted on every spawn and bury the run output - // this script exists to show. - return [ - 'run', - '--quiet', - '--manifest-path', - cargoManifestPath, - '--', - ...cliArguments, - ]; -} - -function spawnChild(command, args, options = {}) { - return spawn(command, args, { - cwd: appRoot, - env: process.env, - detached: process.platform !== 'win32', - ...options, - }); -} - -const childExitPromises = new WeakMap(); -const closedChildren = new WeakSet(); - -function childExit(child) { - const existing = childExitPromises.get(child); - if (existing) return existing; - const exitPromise = new Promise((resolve, reject) => { - child.once('error', reject); - child.once('close', (code, signal) => { - closedChildren.add(child); - resolve({ code, signal }); - }); - }); - childExitPromises.set(child, exitPromise); - return exitPromise; -} - -async function childExitWithin(exitPromise, timeoutMs) { - let timeoutHandle; - const timeoutPromise = new Promise((resolve) => { - timeoutHandle = setTimeout(() => resolve(null), timeoutMs); - }); - try { - return await Promise.race([exitPromise, timeoutPromise]); - } finally { - clearTimeout(timeoutHandle); - } -} - -export async function terminateChildTree( - child, - signal = 'SIGTERM', - force = false, -) { - if (!child || closedChildren.has(child) || !Number.isInteger(child.pid)) { - return; - } - if (process.platform === 'win32') { - const taskkill = spawn( - 'taskkill.exe', - ['/PID', String(child.pid), '/T', ...(force ? ['/F'] : [])], - { - stdio: 'ignore', - windowsHide: true, - }, - ); - const taskkillExit = childExit(taskkill).catch(() => null); - if (!(await childExitWithin(taskkillExit, childForceTerminationWaitMs))) { - try { - taskkill.kill('SIGKILL'); - } catch { - // The taskkill helper may have exited at the timeout boundary. - } - await childExitWithin(taskkillExit, childForceTerminationWaitMs); - } - return; - } - try { - process.kill(-child.pid, force ? 'SIGKILL' : signal); - } catch (error) { - try { - child.kill(force ? 'SIGKILL' : signal); - } catch { - if (error?.code !== 'ESRCH') throw error; - } - } -} - -async function terminateChildTreeAndWait( - child, - exitPromise, - signal, - label, - { - graceMs = childTerminationGraceMs, - forceWaitMs = childForceTerminationWaitMs, - } = {}, -) { - await terminateChildTree(child, signal, false); - const gracefulResult = await childExitWithin(exitPromise, graceMs); - if (gracefulResult) return gracefulResult; - - await terminateChildTree(child, 'SIGKILL', true); - const forcedResult = await childExitWithin(exitPromise, forceWaitMs); - if (forcedResult) return forcedResult; - throw new Error(`${label} 无法在强制终止进程树后关闭 stdio`); -} - -export async function childExitWithTimeout( - child, - timeoutMs, - label, - terminationOptions, -) { - const exitPromise = childExit(child); - if (timeoutMs === null) return exitPromise; - let timeoutHandle; - const timeoutPromise = new Promise((resolve) => { - timeoutHandle = setTimeout(() => resolve({ kind: 'timeout' }), timeoutMs); - }); - try { - const first = await Promise.race([ - exitPromise.then((result) => ({ kind: 'exit', result })), - timeoutPromise, - ]); - if (first.kind === 'exit') return first.result; - try { - await terminateChildTreeAndWait( - child, - exitPromise, - 'SIGTERM', - label, - terminationOptions, - ); - } catch (error) { - error.code = 'AGC_CHILD_TIMEOUT'; - throw error; - } - throw Object.assign( - new Error(`${label} 超过 ${Math.ceil(timeoutMs / 1000)} 秒期限`), - { code: 'AGC_CHILD_TIMEOUT' }, - ); - } finally { - clearTimeout(timeoutHandle); - } -} - -function isPlainObject(value) { - return ( - value !== null && - typeof value === 'object' && - !Array.isArray(value) && - Object.getPrototypeOf(value) === Object.prototype - ); -} - -function isNonNegativeSafeInteger(value) { - return Number.isSafeInteger(value) && value >= 0; -} - -export function parseSettledSwarmTurnReport(output) { - const reportLines = String(output) - .split(/\r?\n/u) - .filter((line) => line.startsWith(swarmTurnReportPrefix)); - if (reportLines.length !== 1) { - throw new Error( - `Agent Swarm 终态报告数量无效:expected=1 actual=${reportLines.length}`, - ); - } - - let report; - try { - report = JSON.parse(reportLines[0].slice(swarmTurnReportPrefix.length)); - } catch { - throw new Error('Agent Swarm 终态报告不是有效 JSON'); - } - if ( - !isPlainObject(report) || - JSON.stringify(Object.keys(report).sort()) !== - JSON.stringify(swarmTurnReportKeys) - ) { - throw new Error('Agent Swarm 终态报告结构无效'); - } - if (report.schemaVersion !== swarmTurnReportSchema) { - throw new Error('Agent Swarm 终态报告 schema 无效'); - } - if ( - typeof report.parentAgentId !== 'string' || - !report.parentAgentId.trim() || - typeof report.sessionId !== 'string' || - !report.sessionId.trim() || - typeof report.parentRunId !== 'string' || - !report.parentRunId.trim() - ) { - throw new Error('Agent Swarm 终态报告运行身份无效'); - } - const countFields = [ - 'runtimeCount', - ...settledZeroCountFields, - 'newAssistantMessageCount', - 'finalReplyChars', - ]; - if (countFields.some((field) => !isNonNegativeSafeInteger(report[field]))) { - throw new Error('Agent Swarm 终态报告计数无效'); - } - if ( - !['settled', 'failed', 'incomplete', 'needs-reconciliation'].includes( - report.outcome, - ) - ) { - throw new Error('Agent Swarm 终态报告 outcome 无效'); - } - if (report.outcome !== 'settled') { - throw new Error(`Agent Swarm 本轮未收束:outcome=${report.outcome}`); - } - const unsettledField = settledZeroCountFields.find( - (field) => report[field] !== 0, - ); - if (unsettledField) { - throw new Error( - `Agent Swarm 本轮仍有未收束工作:${unsettledField}=${report[unsettledField]}`, - ); - } - if (report.runtimeCount < 1) { - throw new Error('Agent Swarm 终态报告没有 Runtime'); - } - if ( - report.newAssistantMessageCount !== 1 || - report.finalReplyChars < 1 || - report.finalReplyChars > 1_000_000 - ) { - throw new Error('Agent Swarm 最终回复无效'); - } - return report; -} - -async function runCapturedCargo( - cliArguments, - setActiveChild, - { timeoutMs = null, label = 'Cargo 子命令', stdin = null } = {}, -) { - const child = spawnChild(cargoCommand, buildCargoCliArguments(cliArguments), { - stdio: [stdin === null ? 'ignore' : 'pipe', 'pipe', 'pipe'], - }); - setActiveChild(child); - if (stdin !== null) { - child.stdin.end(stdin); - } - let stdout = ''; - let stderr = ''; - child.stdout.setEncoding('utf8'); - child.stderr.setEncoding('utf8'); - child.stdout.on('data', (chunk) => { - stdout += chunk; - }); - child.stderr.on('data', (chunk) => { - stderr += chunk; - }); - try { - const result = await childExitWithTimeout(child, timeoutMs, label); - return { ...result, stdout, stderr }; - } finally { - setActiveChild(null); - } -} - -async function runInteractiveCargo(cliArguments, setActiveChild) { - const child = spawnChild(cargoCommand, buildCargoCliArguments(cliArguments), { - stdio: 'inherit', - }); - setActiveChild(child); - const result = await childExit(child); - setActiveChild(null); - return result; -} - -const swarmConfirmationPromptPattern = /输入 approve 或 reject:$/u; -const swarmUserInputPromptPattern = /请选择 1-\d+,或直接输入其他答案:$/u; - -export function nextSwarmAutoPilotReply(output) { - if (swarmConfirmationPromptPattern.test(output)) return 'approve'; - if (swarmUserInputPromptPattern.test(output)) return '1'; - return null; -} - -// CLI 的 REPL 是「先打印提示符再读行」,所以第一个「你>」出现时本轮还没开始跑: -// 它就是用来读我们这条任务的。收 stdin 必须等到投递之后的下一个提示符——那才是 -// 本轮结束、CLI 回到待输入状态。绝大多数情况下此前已经打印过 turn 回执,但总控也 -// 可能判定直接回复而不起持久 Run,那条路径没有回执,只等回执会一直干等到超时。 -const swarmChatPromptPattern = /(^|\n)你> $/u; - -export function swarmAutoPilotSitsAtPrompt(output) { - return swarmChatPromptPattern.test(output); -} - -export function swarmAutoPilotShouldCloseInput(output, promptsAfterSubmit) { - return swarmAutoPilotSitsAtPrompt(output) && promptsAfterSubmit >= 1; -} - -async function runTaskCargo( - cliArguments, - task, - setActiveChild, - timeoutMs, - autoPilot = false, -) { - const child = spawnChild(cargoCommand, buildCargoCliArguments(cliArguments), { - stdio: ['pipe', 'pipe', 'inherit'], - }); - setActiveChild(child); - const reportLines = []; - let pendingLine = ''; - let settled = false; - let taskSubmitted = false; - let promptsSeen = 0; - let sittingAtPrompt = false; - child.stdout.setEncoding('utf8'); - child.stdout.on('data', (chunk) => { - process.stdout.write(chunk); - pendingLine += chunk; - const lines = pendingLine.split('\n'); - pendingLine = lines.pop() ?? ''; - for (const line of lines) { - const normalizedLine = line.endsWith('\r') ? line.slice(0, -1) : line; - if (normalizedLine.startsWith(swarmTurnReportPrefix)) { - reportLines.push(normalizedLine); - settled = true; - } - } - if (!autoPilot || child.stdin.writableEnded) return; - const atPrompt = swarmAutoPilotSitsAtPrompt(pendingLine); - if (atPrompt && !sittingAtPrompt) promptsSeen += 1; - sittingAtPrompt = atPrompt; - // turn 已给出回执、或 CLI 回到了投递之后的下一个提示符,都说明本轮结束。 - if ( - settled || - (taskSubmitted && - swarmAutoPilotShouldCloseInput(pendingLine, promptsSeen - 1)) - ) { - child.stdin.end(); - return; - } - const reply = nextSwarmAutoPilotReply(pendingLine); - if (reply === null) return; - console.log(`[自动应答] ${reply}`); - pendingLine = ''; - child.stdin.write(`${reply}\n`); - }); - if (autoPilot) { - child.stdin.write(`${task}\n`); - taskSubmitted = true; - } else { - child.stdin.end(`${task}\n`); - } - try { - const result = await childExitWithTimeout( - child, - timeoutMs, - 'Agent Swarm 自动任务', - ); - const normalizedPendingLine = pendingLine.endsWith('\r') - ? pendingLine.slice(0, -1) - : pendingLine; - if (normalizedPendingLine.startsWith(swarmTurnReportPrefix)) { - reportLines.push(normalizedPendingLine); - } - return { - ...result, - turnReportOutput: reportLines.join('\n'), - }; - } finally { - setActiveChild(null); - } -} - -export function parseRunnerShutdownOutput(output) { - const match = output.match(/^runner\.stopped=(true|false)$/m); - if (!match) throw new Error('Runner 收束命令缺少 stopped 状态'); - return match[1] === 'true'; -} - -async function shutdownSwarmTestRunner(runtimeConfig, setActiveChild) { - const result = await runCapturedCargo( - ['--config-dir', runtimeConfig.path, '--runner-shutdown-if-idle'], - setActiveChild, - { - timeoutMs: runnerShutdownTimeoutMs, - label: '隔离 Agent Runner 收束命令', - }, - ); - if (result.code !== 0 || result.signal) { - throw new Error( - `隔离 Agent Runner 收束失败:${result.stderr.trim() || result.stdout.trim() || `code=${result.code ?? ''} signal=${result.signal ?? ''}`}`, - ); - } - return parseRunnerShutdownOutput(result.stdout); -} - -export function validatePreviewUrl(value) { - const url = new URL(value); - if ( - url.protocol !== 'http:' || - url.hostname !== '127.0.0.1' || - !url.port || - url.username || - url.password || - url.pathname !== '/' || - url.search || - url.hash - ) { - throw new Error('预览命令返回了非 loopback URL'); - } - return url.toString(); -} - -export async function hasGeneratedGameEntry(projectPath) { - const gameEntryPath = path.join(projectPath, 'game', 'index.html'); - if (!(await isRegularFileWithoutSymlink(gameEntryPath))) return false; - const html = await readFile(gameEntryPath, 'utf8'); - return html.trim().length > 0 && !html.includes(ungeneratedGameEntryMarker); -} - -const pngSignature = Buffer.from([ - 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, -]); -const pngCrcTable = Uint32Array.from({ length: 256 }, (_unused, index) => { - let value = index; - for (let bit = 0; bit < 8; bit += 1) { - value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1; - } - return value >>> 0; -}); - -function pngCrc32(bytes) { - let value = 0xffffffff; - for (const byte of bytes) { - value = pngCrcTable[(value ^ byte) & 0xff] ^ (value >>> 8); - } - return (value ^ 0xffffffff) >>> 0; -} - -export function validatePngBytes(bytes) { - if ( - !Buffer.isBuffer(bytes) || - bytes.length < 57 || - bytes.length > maximumValidatedPngBytes || - !bytes.subarray(0, pngSignature.length).equals(pngSignature) - ) { - throw new Error('PNG 签名或文件大小无效'); - } - - let offset = pngSignature.length; - let ihdr = null; - let ihdrCount = 0; - let idatSeen = false; - let idatEnded = false; - let iendSeen = false; - let plteCount = 0; - let plteEntries = 0; - const idatChunks = []; - while (offset < bytes.length) { - if (bytes.length - offset < 12) throw new Error('PNG chunk 被截断'); - const length = bytes.readUInt32BE(offset); - const typeOffset = offset + 4; - const dataOffset = typeOffset + 4; - const dataEnd = dataOffset + length; - const chunkEnd = dataEnd + 4; - if (dataEnd > bytes.length - 4 || chunkEnd > bytes.length) { - throw new Error('PNG chunk 长度越界'); - } - const type = bytes.subarray(typeOffset, dataOffset).toString('ascii'); - if (!/^[A-Za-z]{4}$/u.test(type)) throw new Error('PNG chunk 类型无效'); - const expectedCrc = bytes.readUInt32BE(dataEnd); - const actualCrc = pngCrc32(bytes.subarray(typeOffset, dataEnd)); - if (expectedCrc !== actualCrc) throw new Error(`${type} CRC 无效`); - - const data = bytes.subarray(dataOffset, dataEnd); - if (offset === pngSignature.length && type !== 'IHDR') { - throw new Error('IHDR 必须是第一个 chunk'); - } - if (type === 'IHDR') { - ihdrCount += 1; - if (ihdrCount !== 1 || length !== 13) { - throw new Error('IHDR 数量或长度无效'); - } - ihdr = { - width: data.readUInt32BE(0), - height: data.readUInt32BE(4), - bitDepth: data[8], - colorType: data[9], - compression: data[10], - filter: data[11], - interlace: data[12], - }; - } else if (type === 'PLTE') { - plteCount += 1; - if ( - !ihdr || - idatSeen || - iendSeen || - plteCount !== 1 || - length < 3 || - length > 768 || - length % 3 !== 0 - ) { - throw new Error('PLTE 数量、长度或顺序无效'); - } - plteEntries = length / 3; - } else if (type === 'IDAT') { - if (!ihdr || idatEnded || iendSeen) { - throw new Error('IDAT 顺序无效'); - } - idatSeen = true; - idatChunks.push(data); - } else if (type === 'IEND') { - if (!ihdr || !idatSeen || iendSeen || length !== 0) { - throw new Error('IEND 数量、长度或顺序无效'); - } - iendSeen = true; - if (chunkEnd !== bytes.length) throw new Error('IEND 后存在额外数据'); - } else { - if ((type.charCodeAt(0) & 0x20) === 0) { - throw new Error(`不支持的 PNG critical chunk:${type}`); - } - if (idatSeen) idatEnded = true; - } - offset = chunkEnd; - } - - if (!ihdr || ihdrCount !== 1 || !idatSeen || !iendSeen) { - throw new Error('PNG 缺少唯一 IHDR、IDAT 或 IEND'); - } - const validBitDepths = { - 0: [1, 2, 4, 8, 16], - 2: [8, 16], - 3: [1, 2, 4, 8], - 4: [8, 16], - 6: [8, 16], - }; - if ( - ihdr.width < 1 || - ihdr.height < 1 || - ihdr.width * ihdr.height > maximumValidatedPngPixels || - !validBitDepths[ihdr.colorType]?.includes(ihdr.bitDepth) || - ihdr.compression !== 0 || - ihdr.filter !== 0 || - ihdr.interlace !== 0 - ) { - throw new Error('IHDR 参数无效或不支持交错 PNG'); - } - if ( - (ihdr.colorType === 3 && - (plteCount !== 1 || plteEntries > 2 ** ihdr.bitDepth)) || - ([0, 4].includes(ihdr.colorType) && plteCount !== 0) - ) { - throw new Error('PLTE 与 PNG color type 或 bit depth 不匹配'); - } - - const channels = { 0: 1, 2: 3, 3: 1, 4: 2, 6: 4 }[ihdr.colorType]; - const rowBytes = Math.ceil((ihdr.width * channels * ihdr.bitDepth) / 8); - const expectedInflatedBytes = ihdr.height * (rowBytes + 1); - if ( - !Number.isSafeInteger(expectedInflatedBytes) || - expectedInflatedBytes > maximumInflatedPngBytes - ) { - throw new Error('PNG scanline 大小无效'); - } - let inflated; - try { - const compressed = Buffer.concat(idatChunks); - const result = inflateSync(compressed, { - maxOutputLength: expectedInflatedBytes + 1, - info: true, - }); - if (result.engine.bytesWritten !== compressed.length) { - throw new Error('trailing compressed bytes'); - } - inflated = result.buffer; - } catch { - throw new Error('IDAT zlib 数据无法完整解压'); - } - if (inflated.length !== expectedInflatedBytes) { - throw new Error('非交错 PNG scanline 长度无效'); - } - for (let row = 0; row < ihdr.height; row += 1) { - if (inflated[row * (rowBytes + 1)] > 4) { - throw new Error(`第 ${row + 1} 行 filter byte 无效`); - } - } - return { width: ihdr.width, height: ihdr.height }; -} - -async function validatePngFile(filePath) { - const metadata = await lstat(filePath); - if ( - !metadata.isFile() || - metadata.isSymbolicLink() || - metadata.size < 1_024 || - metadata.size > maximumValidatedPngBytes - ) { - throw new Error('PNG 文件缺失、类型无效或大小超限'); - } - const noFollowFlag = - process.platform === 'win32' ? 0 : (fsConstants.O_NOFOLLOW ?? 0); - const file = await open(filePath, fsConstants.O_RDONLY | noFollowFlag); - try { - return validatePngBytes(await file.readFile()); - } finally { - await file.close(); - } -} - -async function inspectFormalArtifact(projectPath, spec) { - const artifactPath = path.join(projectPath, ...spec.path.split('/')); - let metadata; - try { - metadata = await lstat(artifactPath); - } catch (error) { - return { - path: spec.path, - reason: - error?.code === 'ENOENT' || error?.code === 'ENOTDIR' - ? '缺失' - : '无法安全读取', - }; - } - if (!metadata.isFile() || metadata.isSymbolicLink()) { - return { path: spec.path, reason: '不是无符号链接普通文件' }; - } - if (metadata.size === 0) return { path: spec.path, reason: '空文件' }; - - const noFollowFlag = - process.platform === 'win32' ? 0 : (fsConstants.O_NOFOLLOW ?? 0); - let file; - try { - file = await open(artifactPath, fsConstants.O_RDONLY | noFollowFlag); - } catch { - return { path: spec.path, reason: '无法安全读取' }; - } - try { - const openedMetadata = await file.stat(); - if (!openedMetadata.isFile()) { - return { path: spec.path, reason: '不是无符号链接普通文件' }; - } - if (openedMetadata.size === 0) { - return { path: spec.path, reason: '空文件' }; - } - - if (spec.kind === 'json') { - let parsed; - try { - parsed = JSON.parse(await file.readFile('utf8')); - } catch { - return { path: spec.path, reason: 'JSON 无法解析' }; - } - if (!isPlainObject(parsed) || Object.keys(parsed).length === 0) { - return { path: spec.path, reason: 'JSON 必须是非空对象' }; - } - } else if (spec.kind === 'file' || spec.kind === 'game-entry') { - const content = await file.readFile('utf8'); - if (content.trim().length === 0) { - return { path: spec.path, reason: '空文件' }; - } - if (hasIncompleteArtifactMarker(content)) { - return { path: spec.path, reason: '仍包含占位标记' }; - } - const compactContent = content.replace(/\s/gu, ''); - if ( - spec.kind === 'file' && - compactContent.length < minimumMarkdownBodyCharacters - ) { - return { path: spec.path, reason: 'Markdown 正文过短' }; - } - if ( - spec.kind === 'game-entry' && - content.includes(ungeneratedGameEntryMarker) - ) { - return { path: spec.path, reason: '仍是初始化占位页' }; - } - if ( - spec.kind === 'game-entry' && - (compactContent.length < minimumHtmlCharacters || - !/]*>[\s\S]*<\/html>/iu.test(content) || - !/ maximumValidatedPngBytes) { - return { path: spec.path, reason: '图片文件大小超限' }; - } - let dimensions; - try { - dimensions = validatePngBytes(await file.readFile()); - } catch (error) { - return { - path: spec.path, - reason: `PNG 文件无效:${error.message}`, - }; - } - const ratio = dimensions.width / dimensions.height; - if (Math.abs(ratio - spec.aspectRatio) > 0.03) { - return { - path: spec.path, - reason: `图片比例无效:${dimensions.width}x${dimensions.height}`, - }; - } - } - } catch { - return { path: spec.path, reason: '无法安全读取' }; - } finally { - await file.close(); - } - return null; -} - -function resolveProjectEvidencePath(projectPath, relativePath) { - if ( - typeof relativePath !== 'string' || - !relativePath || - path.isAbsolute(relativePath) || - relativePath.includes('\\') - ) { - throw new Error('证据路径无效'); - } - const root = path.resolve(projectPath); - const targetPath = path.resolve(root, ...relativePath.split('/')); - if (!targetPath.startsWith(`${root}${path.sep}`)) { - throw new Error('证据路径越界'); - } - return targetPath; -} - -async function readSafeJson(projectPath, relativePath, maximumBytes) { - const targetPath = resolveProjectEvidencePath(projectPath, relativePath); - const metadata = await lstat(targetPath).catch(() => null); - if ( - !metadata?.isFile() || - metadata.isSymbolicLink() || - metadata.size < 2 || - metadata.size > maximumBytes - ) { - throw new Error('文件缺失、类型无效或大小超限'); - } - const noFollowFlag = - process.platform === 'win32' ? 0 : (fsConstants.O_NOFOLLOW ?? 0); - const file = await open(targetPath, fsConstants.O_RDONLY | noFollowFlag); - try { - return JSON.parse(await file.readFile('utf8')); - } finally { - await file.close(); - } -} - -async function readSafeJsonLines(projectPath, relativePath, maximumBytes) { - const targetPath = resolveProjectEvidencePath(projectPath, relativePath); - const metadata = await lstat(targetPath); - if ( - !metadata.isFile() || - metadata.isSymbolicLink() || - metadata.size < 2 || - metadata.size > maximumBytes - ) { - throw new Error('JSONL 文件缺失、类型无效或大小超限'); - } - const noFollowFlag = - process.platform === 'win32' ? 0 : (fsConstants.O_NOFOLLOW ?? 0); - const file = await open(targetPath, fsConstants.O_RDONLY | noFollowFlag); - try { - const content = await file.readFile('utf8'); - if (!content.endsWith('\n')) throw new Error('JSONL 尾记录不完整'); - return content - .split(/\r?\n/u) - .filter(Boolean) - .map((line) => JSON.parse(line)); - } finally { - await file.close(); - } -} - -async function inspectReadyTaskExactlyOnce( - projectPath, - parentRunId, - agentDbRecords, -) { - const issues = []; - for (const taskId of requiredSwarmManifestTaskIds) { - const journalPath = `.agent/runtime/tasks/${taskId}.jsonl`; - try { - const journal = await readSafeJsonLines( - projectPath, - journalPath, - 8 * 1024 * 1024, - ); - const currentRunRecords = journal.filter( - (record) => - record?.agentId === taskId && - record?.taskId === taskId && - record?.source === 'agent-ready-task-scheduler' && - record?.parentAgentId === 'project-supervisor' && - record?.parentRunId === parentRunId, - ); - const runIds = [ - ...new Set(currentRunRecords.map((record) => record?.runId)), - ].filter((runId) => typeof runId === 'string' && runId.length > 0); - if (runIds.length !== 1) throw new Error('logical-run-count'); - const runId = runIds[0]; - const latest = currentRunRecords - .filter((record) => record?.runId === runId) - .at(-1); - if ( - latest?.status !== 'completed' || - latest?.phase !== 'completed' || - latest?.runProfile !== 'autonomous-game-build' - ) { - throw new Error('logical-run-terminal'); - } - const count = (recordType) => - agentDbRecords.filter( - (record) => - record?.recordType === recordType && - record?.agentId === taskId && - record?.taskId === taskId && - record?.runId === runId && - record?.source === 'agent-ready-task-scheduler', - ).length; - const projections = agentDbRecords.filter( - (record) => - record?.recordType === - 'agent.runtime.autonomous_ready_task.manifest_projected' && - record?.agentId === taskId && - record?.taskId === taskId && - record?.runId === runId && - record?.source === 'agent-ready-task-scheduler' && - record?.parentAgentId === 'project-supervisor' && - record?.parentRunId === parentRunId && - record?.terminalPhase === 'completed' && - record?.manifestStatus === 'completed', - ); - if ( - count('agent.runtime.background_task') !== 1 || - count('agent.runtime.background_task.completed') !== 1 || - count('agent.runtime.background_task.failed') !== 0 || - count('agent.runtime.background_task.cancelled') !== 0 || - projections.length !== 1 - ) { - throw new Error('lifecycle-count'); - } - } catch { - issues.push({ - path: journalPath, - reason: `正式任务 ${taskId} 未在当前父 Run 中恰好启动并完成一次`, - }); - } - } - return issues; -} - -async function inspectSwarmRuntimeAcceptance(projectPath, parentRunId = null) { - const issues = []; - let manifest; - try { - manifest = await readSafeJson( - projectPath, - '.agent/manifest.json', - 2 * 1024 * 1024, - ); - } catch { - issues.push({ - path: '.agent/manifest.json', - reason: '无法读取正式任务图', - }); - } - if (manifest) { - const tasks = Array.isArray(manifest.tasks) ? manifest.tasks : []; - const taskIds = tasks.map((task) => task?.id); - const expectedIds = [...requiredSwarmManifestTaskIds].sort(); - const actualIds = [...taskIds].sort(); - if ( - tasks.length !== requiredSwarmManifestTaskIds.length || - new Set(taskIds).size !== taskIds.length || - JSON.stringify(actualIds) !== JSON.stringify(expectedIds) || - tasks.some((task) => task?.status !== 'completed') - ) { - issues.push({ - path: '.agent/manifest.json', - reason: '固定 16 个正式任务未全部且仅完成一次', - }); - } - } - - let revision; - try { - const revisionRecord = await readSafeJson( - projectPath, - '.agent/runtime/project-revision.json', - 64 * 1024, - ); - revision = revisionRecord?.revision; - if (!Number.isSafeInteger(revision) || revision < 1) throw new Error(); - } catch { - issues.push({ - path: '.agent/runtime/project-revision.json', - reason: '当前项目 revision 无效', - }); - } - - let records = []; - let recordsValid = true; - try { - const databasePath = path.join(projectPath, '.agent', 'agent.db'); - const metadata = await lstat(databasePath); - if ( - !metadata.isFile() || - metadata.isSymbolicLink() || - metadata.size < 2 || - metadata.size > 64 * 1024 * 1024 - ) { - throw new Error(); - } - records = (await readFile(databasePath, 'utf8')) - .split(/\r?\n/u) - .filter(Boolean) - .map((line) => JSON.parse(line)); - } catch { - recordsValid = false; - issues.push({ path: '.agent/agent.db', reason: 'Runtime 证据库无效' }); - } - if (!recordsValid) return issues; - if (parentRunId) { - issues.push( - ...(await inspectReadyTaskExactlyOnce(projectPath, parentRunId, records)), - ); - } - const staticSmoke = records.some( - (record) => - record?.recordType === 'agent.runtime.command.run_limited' && - record?.commandId === 'game.static_smoke' && - record?.status === 'completed' && - record?.revision === revision, - ); - if (!staticSmoke) { - issues.push({ - path: '.agent/agent.db', - reason: '缺少当前 revision 的静态检查通过凭证', - }); - } - const browserEvidence = records - .filter( - (record) => - record?.recordType === 'agent.runtime.preview.validation' && - record?.passed === true && - record?.playtestPassed === true && - record?.revision === revision, - ) - .sort((left, right) => (right.updatedAt ?? 0) - (left.updatedAt ?? 0))[0]; - if (!browserEvidence) { - issues.push({ - path: '.agent/agent.db', - reason: '缺少当前 revision 的桌面与移动试玩通过凭证', - }); - return issues; - } - const screenshots = Array.isArray(browserEvidence.screenshots) - ? browserEvidence.screenshots - : []; - for (const viewport of ['desktop', 'mobile']) { - const screenshot = screenshots.find((item) => - String(item).endsWith(`/${viewport}.png`), - ); - let screenshotValid = false; - try { - if (screenshot) { - await validatePngFile( - resolveProjectEvidencePath(projectPath, screenshot), - ); - screenshotValid = true; - } - } catch { - screenshotValid = false; - } - if (!screenshotValid) { - issues.push({ - path: '.agent/agent.db', - reason: `缺少 ${viewport} 试玩截图`, - }); - } - } - try { - const report = await readSafeJson( - projectPath, - browserEvidence.reportPath, - 2 * 1024 * 1024, - ); - const viewportResults = Array.isArray(report?.viewportResults) - ? report.viewportResults - : []; - if ( - report?.passed !== true || - report?.playtest?.passed !== true || - !['desktop', 'mobile'].every((viewport) => - viewportResults.some( - (result) => result?.viewport === viewport && result?.passed === true, - ), - ) - ) { - throw new Error(); - } - } catch { - issues.push({ - path: String( - browserEvidence.reportPath ?? '.agent/runtime/browser-validations', - ), - reason: '双视口试玩报告无效或已过期', - }); - } - return issues; -} - -export async function inspectSwarmProjectArtifacts( - projectPath, - { requireEditorImages = false, parentRunId = null } = {}, -) { - const specs = requireEditorImages - ? [...requiredFormalArtifactSpecs, ...editorImageArtifactSpecs] - : requiredFormalArtifactSpecs; - const issues = ( - await Promise.all( - specs.map((spec) => inspectFormalArtifact(projectPath, spec)), - ) - ).filter(Boolean); - issues.push( - ...(await inspectSwarmRuntimeAcceptance(projectPath, parentRunId)), - ); - return { - valid: issues.length === 0, - requireEditorImages, - invalidPaths: issues.map((issue) => issue.path), - issues, - }; -} - -export async function validateSwarmProjectArtifacts(projectPath, options) { - const inspection = await inspectSwarmProjectArtifacts(projectPath, options); - if (!inspection.valid) { - throw new Error( - `Agent Swarm 已退出,但最小正式产物检查失败:\n${inspection.issues - .map((issue) => `- ${issue.path}:${issue.reason}`) - .join('\n')}`, - ); - } - return inspection; -} - -export async function hasConfiguredEditorApiKey(configDir) { - let configured = false; - for (const fileName of [configFileName, localConfigFileName]) { - const configPath = path.join(configDir, fileName); - const metadata = await lstat(configPath).catch((error) => { - if (error?.code === 'ENOENT') return null; - throw error; - }); - if (!metadata) continue; - if (!metadata.isFile() || metadata.isSymbolicLink()) { - throw new Error(`运行配置必须是无符号链接普通文件:${fileName}`); - } - let config; - try { - config = JSON.parse(await readFile(configPath, 'utf8')); - } catch (error) { - throw new Error(`解析运行配置失败:${fileName}:${error.message}`); - } - if ( - config?.editorApi && - Object.prototype.hasOwnProperty.call(config.editorApi, 'apiKey') && - typeof config.editorApi.apiKey === 'string' - ) { - configured = config.editorApi.apiKey.trim().length > 0; - } - } - return configured; -} - -export async function openPreviewUrl(url, platform = process.platform) { - const validated = validatePreviewUrl(url); - const command = - platform === 'win32' - ? 'cmd.exe' - : platform === 'darwin' - ? 'open' - : 'xdg-open'; - const args = - platform === 'win32' - ? ['/d', '/s', '/c', 'start', '', validated] - : [validated]; - await new Promise((resolve, reject) => { - const child = spawn(command, args, { - detached: true, - stdio: 'ignore', - windowsHide: true, - }); - child.once('error', reject); - child.once('spawn', () => { - child.unref(); - resolve(); - }); - }); -} - -async function runPreview( - projectPath, - openBrowser, - setActiveChild, - stopRequested, -) { - const child = spawnChild( - cargoCommand, - buildCargoCliArguments(['--preview-serve', projectPath]), - { stdio: ['ignore', 'pipe', 'pipe'] }, - ); - setActiveChild(child); - child.stdout.setEncoding('utf8'); - child.stderr.setEncoding('utf8'); - child.stdout.pipe(process.stdout); - child.stderr.pipe(process.stderr); - - let pending = ''; - let previewUrl = null; - let resolvePreviewUrl; - let rejectPreviewUrl; - const previewUrlReady = new Promise((resolve, reject) => { - resolvePreviewUrl = resolve; - rejectPreviewUrl = reject; - }); - child.stdout.on('data', (chunk) => { - pending += chunk; - const lines = pending.split(/\r?\n/); - pending = lines.pop() ?? ''; - for (const line of lines) { - if (!line.startsWith('previewUrl=')) continue; - try { - previewUrl = validatePreviewUrl( - line.slice('previewUrl='.length).trim(), - ); - resolvePreviewUrl(previewUrl); - } catch (error) { - rejectPreviewUrl(error); - } - } - }); - - const exitPromise = childExit(child); - try { - exitPromise.then(({ code, signal }) => { - if (!previewUrl) { - rejectPreviewUrl( - new Error( - `预览进程提前退出:code=${code ?? ''} signal=${signal ?? ''}`, - ), - ); - } - }); - const url = await previewUrlReady; - console.log(`\n试玩地址:${url}`); - console.log('按 Ctrl+C 结束预览。'); - if (openBrowser) { - await openPreviewUrl(url).catch((error) => { - console.warn( - `无法自动打开浏览器,请手动访问上面的地址:${error.message}`, - ); - }); - } - const result = await exitPromise; - if (!stopRequested() && (result.code !== 0 || result.signal)) { - throw new Error( - `预览进程异常退出:code=${result.code ?? ''} signal=${result.signal ?? ''}`, - ); - } - } finally { - if (!closedChildren.has(child)) { - await terminateChildTreeAndWait( - child, - exitPromise, - 'SIGINT', - '持续预览进程', - ); - } - setActiveChild(null); - } -} - -export async function runSwarmTestChat(options) { - let sourceConfigDir = null; - let runtimeConfig = null; - let project = null; - let activeChild = null; - let receivedSignal = null; - let timedOut = false; - let phase = 'setup'; - let runnerMayHaveStarted = false; - let turnReport = null; - let primaryError = null; - let forceTerminationHandle = null; - const setActiveChild = (child) => { - activeChild = child; - }; - const concurrentChildren = new Set(); - const stopRequested = () => receivedSignal !== null; - const handleSignal = (signal) => { - const repeatedSignal = receivedSignal !== null; - receivedSignal ??= signal; - const targets = [activeChild, ...concurrentChildren].filter(Boolean); - if (targets.length === 0) return; - for (const target of targets) { - void terminateChildTree(target, signal, repeatedSignal).catch(() => {}); - } - if (repeatedSignal) return; - forceTerminationHandle = setTimeout(() => { - for (const target of [activeChild, ...concurrentChildren].filter( - Boolean, - )) { - void terminateChildTree(target, 'SIGKILL', true).catch(() => {}); - } - }, childTerminationGraceMs); - forceTerminationHandle.unref(); - }; - const clearForceTermination = () => { - if (forceTerminationHandle) { - clearTimeout(forceTerminationHandle); - forceTerminationHandle = null; - } - }; - const timeoutMs = resolveSwarmTestTimeoutMs(options); - const timeoutDeadline = timeoutMs === null ? null : Date.now() + timeoutMs; - const timeoutHandle = - timeoutMs === null - ? null - : setTimeout(() => { - timedOut = true; - console.error( - `agc:test:chat 已达到 ${options.timeoutMinutes ?? 50} 分钟执行期限,正在安全收束。`, - ); - handleSignal('SIGTERM'); - }, timeoutMs); - timeoutHandle?.unref(); - process.on('SIGINT', handleSignal); - process.on('SIGTERM', handleSignal); - - try { - session: { - try { - sourceConfigDir = await discoverRuntimeConfigDir(options.configDir); - } catch (error) { - if (!(await askToConfigureMissingRuntime())) throw error; - await runMissingConfigWizard(setActiveChild, options.configDir); - sourceConfigDir = await discoverRuntimeConfigDir(options.configDir); - } - runtimeConfig = await prepareSwarmTestRuntimeConfig(sourceConfigDir); - if (receivedSignal) break session; - project = await prepareSwarmTestProject(options.projectDir); - if (receivedSignal) break session; - - console.log(`配置来源:${path.join(sourceConfigDir, configFileName)}`); - console.log( - `隔离运行配置:${path.join(runtimeConfig.path, configFileName)}`, - ); - console.log(`测试项目:${project.path}`); - if (options.dryRun) { - console.log('测试环境检查通过;未启动 LLM。'); - break session; - } - - console.log('\n正在检查 LLM 配置...'); - const llmStatus = await runCapturedCargo( - ['--config-dir', runtimeConfig.path, '--llm-status'], - setActiveChild, - ); - if (receivedSignal) break session; - if (llmStatus.code !== 0 || llmStatus.signal) { - throw new Error( - `LLM 配置未就绪:${llmStatus.stderr.trim() || llmStatus.stdout.trim()}`, - ); - } - console.log('LLM 配置已就绪。'); - console.log( - options.task - ? '已提交一条非交互游戏需求,正在等待 Swarm 自主完成。\n' - : '输入一条游戏需求并回车;提交后按 Ctrl+D,让 Swarm 自主完成。\n', - ); - - phase = 'chat'; - runnerMayHaveStarted = true; - const chatArguments = [ - '--config-dir', - runtimeConfig.path, - '--swarm-chat', - '--init', - '--autonomous-game-build', - project.path, - ]; - let chat; - try { - chat = options.task - ? await runTaskCargo( - chatArguments, - options.task, - setActiveChild, - timeoutDeadline === null - ? null - : Math.max(1, timeoutDeadline - Date.now()), - ) - : await runInteractiveCargo(chatArguments, setActiveChild); - } catch (error) { - if (error?.code === 'AGC_CHILD_TIMEOUT') timedOut = true; - throw error; - } - if (receivedSignal) break session; - if (chat.code !== 0 || chat.signal) { - throw new Error( - `Agent Swarm 未正常收束:code=${chat.code ?? ''} signal=${chat.signal ?? ''}`, - ); - } - if (options.task) { - turnReport = parseSettledSwarmTurnReport(chat.turnReportOutput); - } - const requireEditorImages = await hasConfiguredEditorApiKey( - runtimeConfig.path, - ); - await validateSwarmProjectArtifacts(project.path, { - requireEditorImages, - parentRunId: turnReport?.parentRunId ?? null, - }); - - if (!shouldStartPersistentPreview(options)) { - phase = 'complete'; - console.log( - `\n真实 Swarm 测试通过:正式产物${ - requireEditorImages ? '及画布图片' : '' - }已验收,Runtime 已完成静态检查和双视口试玩。`, - ); - break session; - } - - phase = 'preview'; - console.log('\nAgent Swarm 已收束,正在启动试玩...'); - await runPreview( - project.path, - options.openBrowser, - setActiveChild, - stopRequested, - ); - phase = 'complete'; - } - } catch (error) { - primaryError = error; - } finally { - if (timeoutHandle) clearTimeout(timeoutHandle); - clearForceTermination(); - let cleanupError = null; - let runnerStopped = true; - if (runtimeConfig && runnerMayHaveStarted) { - try { - runnerStopped = await shutdownSwarmTestRunner( - runtimeConfig, - setActiveChild, - ); - if (!runnerStopped) { - cleanupError = new Error( - '隔离 Agent Runner 仍有任务,已保留测试项目和隔离运行配置', - ); - } - } catch (error) { - runnerStopped = false; - cleanupError = error; - } - } - const preserveFailedRun = - project?.owned && - (phase === 'chat' || - phase === 'artifact-validation' || - (phase === 'preview' && !receivedSignal)); - const preserveForRunner = project?.owned && !runnerStopped; - if ( - project?.owned && - (options.keepProject || preserveFailedRun || preserveForRunner) - ) { - if (preserveFailedRun && !options.keepProject) { - console.warn( - '测试尚未正常结束,为避免删除后台任务或失败证据,测试项目不会自动清理。', - ); - } - console.log(`已保留测试项目:${project.path}`); - } else if (project?.owned) { - try { - await cleanupSwarmTestProject(project); - console.log('已清理一次性测试项目。'); - } catch (error) { - cleanupError ??= error; - } - } - if (runtimeConfig) { - if (runnerStopped) { - try { - await cleanupSwarmTestRuntimeConfig(runtimeConfig); - console.log('已清理隔离运行配置。'); - } catch (error) { - cleanupError ??= error; - } - } else { - console.warn(`已保留隔离运行配置:${runtimeConfig.path}`); - } - } - if (cleanupError) { - if (primaryError) { - console.warn(`测试现场清理未完成:${cleanupError.message}`); - } else { - primaryError = cleanupError; - } - } - process.off('SIGINT', handleSignal); - process.off('SIGTERM', handleSignal); - } - if (timedOut) { - if (primaryError) { - console.warn(`超时收束附带错误:${primaryError.message}`); - } - primaryError = Object.assign(new Error('真实 Swarm 测试超过内部执行期限'), { - exitCode: 124, - }); - } else if (receivedSignal) { - if (primaryError) { - console.warn(`信号收束附带错误:${primaryError.message}`); - } - primaryError = Object.assign( - new Error(`真实 Swarm 测试收到 ${receivedSignal}`), - { exitCode: receivedSignal === 'SIGINT' ? 130 : 143 }, - ); - } - if (primaryError) throw primaryError; -} - -async function main() { - const options = parseSwarmTestArguments(process.argv.slice(2)); - if (options.help) { - console.log(usage); - return; - } - await runSwarmTestChat(options); -} - -const entryPath = process.argv[1] - ? pathToFileURL(path.resolve(process.argv[1])).href - : ''; -if (entryPath === import.meta.url) { - main().catch((error) => { - console.error(`agc:test:chat 失败:${error.message}`); - process.exitCode = Number.isInteger(error.exitCode) ? error.exitCode : 1; - }); -} diff --git a/apps/ai-game-creator-shell/scripts/check-config.mjs b/apps/ai-game-creator-shell/scripts/check-config.mjs index eaba4e582..159e50782 100644 --- a/apps/ai-game-creator-shell/scripts/check-config.mjs +++ b/apps/ai-game-creator-shell/scripts/check-config.mjs @@ -23,10 +23,6 @@ execFileSync( { stdio: 'inherit' }, ); -import { - appIdentifier, - defaultRealSwarmTestTask, -} from './agent-swarm-test-chat.mjs'; import { AGC_APP_IDENTIFIER, AGC_PRODUCT_NAME, @@ -83,10 +79,6 @@ const defaultAppConfig = JSON.parse( const rootPackageConfig = JSON.parse( fs.readFileSync(new URL('../../../package.json', import.meta.url), 'utf8'), ); -const swarmTestChatSource = fs.readFileSync( - new URL('../scripts/agent-swarm-test-chat.mjs', import.meta.url), - 'utf8', -); const viteConfigSource = fs.readFileSync( new URL('../vite.config.ts', import.meta.url), 'utf8', @@ -132,8 +124,8 @@ const rustSharedContractSource = fs.readFileSync( const allowedUncalledTauriCommands = [ 'append_direct_project_conversation_message', // Supervisor 调试窗口、开发者面板、专业 Agent 对话与旧命令聊天的前端调用方已随 - // Supervisor 前端链路整体删除;命令本身仍注册在 Rust 侧并由 native Runtime、CLI - // swarm 与 Rust 测试使用,保留 present,仅不再出现在 App 前端源码里。 + // Supervisor 前端链路整体删除,终端 swarm CLI 也已退役;命令本身仍注册在 Rust 侧 + // 供 native Runtime 与 Rust 测试使用,保留 present,仅不出现在 App 前端源码里。 'answer_game_creator_agent_runtime_user_input', 'cancel_game_creator_agent_runtime_task', 'chat_with_game_creator_role_agent', @@ -141,12 +133,11 @@ const allowedUncalledTauriCommands = [ 'check_game_creator_llm_config', 'confirm_game_creator_agent_runtime_task', 'diff_local_project_checkpoint', - 'get_game_creation_agent_capabilities', - 'get_limited_local_commands', 'list_local_project_export_packages', 'read_game_creator_agent_runtime', 'read_local_agent_memory', 'read_local_game_memory', + 'read_local_project_file', 'reject_game_creator_agent_runtime_task', 'retry_game_creator_agent_runtime_task', 'schedule_game_creator_agent_ready_tasks', @@ -155,7 +146,7 @@ const allowedUncalledTauriCommands = [ 'write_local_agent_memory', 'write_local_game_memory', 'write_local_project_file', - // Agent 运行时会话 / 目标 / 协作命令由 native 侧与 CLI swarm 驱动,前端没有调用方。 + // Agent 运行时会话 / 目标 / 协作命令由 native 侧驱动,前端没有调用方。 'archive_game_creator_agent_session', 'clear_game_creator_agent_goal', 'compact_game_creator_agent_runtime_context', @@ -795,14 +786,14 @@ async function runConfigWizardRegressionChecks() { assertSafeGameCreatorConfigDestination(outsideConfigDir, { requireDedicatedLeaf: true, }), - new RegExp(appIdentifier.replaceAll('.', '\\.')), + new RegExp(AGC_APP_IDENTIFIER.replaceAll('.', '\\.')), ); - const dedicatedConfigDir = path.join(testRoot, appIdentifier); + const dedicatedConfigDir = path.join(testRoot, AGC_APP_IDENTIFIER); assert.equal( await assertSafeGameCreatorConfigDestination(dedicatedConfigDir, { requireDedicatedLeaf: true, }), - path.join(canonicalTestRoot, appIdentifier), + path.join(canonicalTestRoot, AGC_APP_IDENTIFIER), ); const injectedNonGitConfigDir = path.join(testRoot, 'injected-non-git'); @@ -1297,15 +1288,6 @@ if ( ); } -if ( - packageConfig.scripts?.swarm !== - 'node scripts/run-cli-with-config.mjs --swarm-chat' -) { - throw new Error( - 'AI game creator shell swarm must use client config before starting the interactive Agent runtime', - ); -} - if ( packageConfig.scripts?.config !== 'node scripts/game-creator-config-wizard.mjs' @@ -1315,31 +1297,6 @@ if ( ); } -if ( - packageConfig.scripts?.['test:chat'] !== - `node scripts/agent-swarm-test-chat.mjs --task ${JSON.stringify(defaultRealSwarmTestTask)} --no-open` -) { - throw new Error( - 'AI game creator shell test:chat must use the one-click Swarm test entry', - ); -} - -if ( - packageConfig.scripts?.['test:chat:manual'] !== - 'node scripts/agent-swarm-test-chat.mjs' -) { - throw new Error( - 'AI game creator shell test:chat:manual must keep the interactive Swarm test entry', - ); -} - -if ( - rootPackageConfig.scripts?.['agc:test'] !== - 'npm --prefix apps/ai-game-creator-shell run agent-runtime:supervisor-autonomous-playable-lane-defense-deterministic-e2e --' -) { - throw new Error('agc:test must delegate to the deterministic playable E2E'); -} - if ( rootPackageConfig.scripts?.['agc:config'] !== 'npm --prefix apps/ai-game-creator-shell run config --' @@ -1349,37 +1306,6 @@ if ( ); } -if ( - rootPackageConfig.scripts?.['agc:test:chat'] !== - 'npm --prefix apps/ai-game-creator-shell run test:chat --' -) { - throw new Error( - 'agc:test:chat must delegate to the one-click Swarm test entry', - ); -} - -if ( - rootPackageConfig.scripts?.['agc:test:chat:manual'] !== - 'npm --prefix apps/ai-game-creator-shell run test:chat:manual --' -) { - throw new Error( - 'agc:test:chat:manual must delegate to the interactive Swarm test entry', - ); -} - -for (const requiredSource of [ - "import { AGC_APP_IDENTIFIER } from './channel-identity.mjs'", - 'export const appIdentifier = AGC_APP_IDENTIFIER', - "'--swarm-chat'", - "'--autonomous-game-build'", - "'--preview-serve'", - 'cleanupSwarmTestProject(project)', -]) { - if (!swarmTestChatSource.includes(requiredSource)) { - throw new Error(`Swarm test entry contract drifted: ${requiredSource}`); - } -} - // 基线配置必须等于默认渠道的安装身份:默认渠道不能改身份,否则已发布客户端 // 的升级链路与既有安装目录都会断开。 const defaultChannelIdentity = resolveChannelInstallIdentity('dev'); @@ -1939,18 +1865,12 @@ for (const snippet of [ '陶泥儿智能创作(固定)', '官方账号服务(固定)', 'runtime_config.save', - "'/run:运行自检,启动本地 HTTP 预览并载入客户端运行视图'", 'async function executeRunLocal', 'function needsInitializedChatProject', - "'/remember [short|long|blackboard] 内容:追加短期、长期或黑板记忆'", - "'/memory-set [short|long|blackboard] 内容:覆盖保存对应记忆'", - 'function parseRememberInput', - "'/trace 或 /loop:查看最近一次 Agent loop trace'", "'permission.pending'", "'permission.confirm'", "'permission.cancel'", "'command.auto'", - 'function summarizeAgentRunTrace', ]) { if (!appSource.includes(snippet)) { throw new Error( @@ -1964,7 +1884,6 @@ for (const script of [ 'ai-game-creator-shell:dev-server', 'ai-game-creator-shell:build', 'ai-game-creator-shell:agent-task', - 'agc:swarm', 'ai-game-creator-shell:typecheck', 'ai-game-creator-shell:agent-run:smoke', 'ai-game-creator-shell:check', @@ -1974,13 +1893,6 @@ for (const script of [ } } -if ( - rootPackageConfig.scripts?.['agc:swarm'] !== - 'npm --prefix apps/ai-game-creator-shell run swarm --' -) { - throw new Error('root agc:swarm script must forward CLI args'); -} - if ( rootPackageConfig.scripts?.['ai-game-creator-shell:build'] !== 'npm --prefix apps/ai-game-creator-shell run build --' diff --git a/apps/ai-game-creator-shell/scripts/deterministic-lane-defense-provider.mjs b/apps/ai-game-creator-shell/scripts/deterministic-lane-defense-provider.mjs deleted file mode 100644 index 9c8318c57..000000000 --- a/apps/ai-game-creator-shell/scripts/deterministic-lane-defense-provider.mjs +++ /dev/null @@ -1,3040 +0,0 @@ -import http from 'node:http'; -import { deflateSync } from 'node:zlib'; - -const LOOPBACK_HOST = '127.0.0.1'; -const DEFAULT_FALLBACK_PORTS = Object.freeze( - Array.from({ length: 128 }, (_, index) => 62_128 + index), -); -const CHAT_COMPLETIONS_PATH = '/v1/chat/completions'; -const MAX_REQUEST_BYTES = 16 * 1024 * 1024; -const PNG_SIGNATURE = Buffer.from([ - 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, -]); -const PNG_CRC_TABLE = Object.freeze( - Array.from({ length: 256 }, (_, value) => { - let crc = value; - for (let bit = 0; bit < 8; bit += 1) { - crc = (crc & 1) !== 0 ? 0xedb88320 ^ (crc >>> 1) : crc >>> 1; - } - return crc >>> 0; - }), -); - -export const deterministicLaneDefenseModel = - 'deterministic-lane-defense-provider-v1'; -export const deterministicManifestReadyAgentIds = Object.freeze([ - 'design-director', - 'design-foundation', - 'balance-director', - 'balance-seed', - 'art-director', - 'art-asset-plan', - 'art-polish', - 'audio-director', - 'audio-asset-plan', - 'code-director', - 'code-prototype', - 'quality-review', - 'preview-readiness', - 'preview-playtest', - 'publish-strategy', - 'publish-package', -]); -const deterministicReadOnlyReadyAgentIds = new Set([ - 'design-director', - 'balance-director', - 'art-polish', - 'audio-director', - 'code-director', - 'quality-review', - 'preview-readiness', - 'preview-playtest', - 'publish-strategy', -]); -// These owner tasks are checked by Runtime's owner-artifact gate. Their -// request-scoped tool catalog deliberately removes command.run_limited, so a -// deterministic response must deliver after a successful fixed-path write -// instead of trying to emit a tool that the runtime did not advertise. -const deterministicOwnerArtifactValidationAgentIds = new Set([ - 'design-foundation', - 'balance-seed', - 'art-asset-plan', - 'audio-asset-plan', -]); -const deterministicProjectMutationTools = new Set([ - 'file.write', - 'file.patch', - 'file.delete', - 'project.patchset', - 'project.restore', - 'command.exec', - 'command.start', - 'canvas.asset_generate', -]); -export const hiddenCanvasCss = '#game{display:none;'; -export const visibleCanvasCss = '#game{display:block;'; - -function pngCrc32(bytes) { - let crc = 0xffffffff; - for (const byte of bytes) { - crc = PNG_CRC_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8); - } - return (crc ^ 0xffffffff) >>> 0; -} - -function pngChunk(type, data) { - const typeBytes = Buffer.from(type, 'ascii'); - const length = Buffer.alloc(4); - length.writeUInt32BE(data.length); - const crc = Buffer.alloc(4); - crc.writeUInt32BE(pngCrc32(Buffer.concat([typeBytes, data]))); - return Buffer.concat([length, typeBytes, data, crc]); -} - -function deterministicPng( - width, - height, - { variant = 0, transparent = false } = {}, -) { - const stride = width * 4 + 1; - const pixels = Buffer.alloc(stride * height); - for (let y = 0; y < height; y += 1) { - const row = y * stride; - pixels[row] = 0; - for (let x = 0; x < width; x += 1) { - const offset = row + 1 + x * 4; - const tile = (Math.floor(x / 48) + Math.floor(y / 48)) % 6; - pixels[offset] = (42 + x + tile * 29 + variant * 13) % 256; - pixels[offset + 1] = (86 + y * 2 + tile * 17 + variant * 19) % 256; - pixels[offset + 2] = (118 + x + y + tile * 31 + variant * 23) % 256; - pixels[offset + 3] = - transparent && (x + y + variant) % 11 === 0 ? 0 : 255; - } - } - const ihdr = Buffer.alloc(13); - ihdr.writeUInt32BE(width, 0); - ihdr.writeUInt32BE(height, 4); - ihdr[8] = 8; - ihdr[9] = 6; - return Buffer.concat([ - PNG_SIGNATURE, - pngChunk('IHDR', ihdr), - pngChunk('IDAT', deflateSync(pixels, { level: 6 })), - pngChunk('IEND', Buffer.alloc(0)), - ]); -} - -export function deterministicLaneDefenseInitialHtml() { - return ` - - - -灵露花园 - - -
Garden defenders -

灵露花园

GENARRATIVE_REAL_E2E_VISIBLE

Goal: defend the garden and win every wave.

-
-
-
Level 1 ready
- -
-`; -} - -const deterministicLaneDefenseBalance = Object.freeze({ - schemaVersion: 'lane-defense-balance.v1', - startingSun: 150, - defenderCosts: { 'nectar-bloom': 50, 'thorn-sentry': 100 }, - enemyHealth: 100, - enemySpeed: 20, - waveTimingsMs: [120, 300, 560], - scorePerEnemy: 100, - levelDifficultyMultiplier: 1.18, -}); - -export function deterministicLaneDefenseCanonicalHtml() { - const upstreamContract = JSON.stringify({ - schemaVersion: 'lane-defense-upstream-contract.v1', - design: 'game/game_design.md', - balance: 'game/balance.json', - art: 'assets/manifest.art.json', - audio: 'assets/manifest.audio.json', - }); - const balance = JSON.stringify(deterministicLaneDefenseBalance); - return deterministicLaneDefenseInitialHtml() - .replace(hiddenCanvasCss, visibleCanvasCss) - .replace( - '
', - '
', - ) - .replace( - '\n