diff --git a/apps/ai-game-creator-shell/package.json b/apps/ai-game-creator-shell/package.json
index 69bd74590..ba1fd4e0e 100644
--- a/apps/ai-game-creator-shell/package.json
+++ b/apps/ai-game-creator-shell/package.json
@@ -17,6 +17,8 @@
"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",
+ "test:plan": "node scripts/agent-swarm-test-chat.mjs --plan --task \"我想做一款原创横版像素解谜小游戏,主角是一个能操控自己影子的小机器人,影子可以变成平台和开关。请完成立项策划并给出 Fast GDD。主题、角色名与视觉语言必须原创,不使用任何现有游戏角色、名称、Logo 或受保护视觉语言。\"",
+ "test:plan:manual": "node scripts/agent-swarm-test-chat.mjs --plan",
"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",
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
index 756e17422..246cc297a 100644
--- a/apps/ai-game-creator-shell/scripts/agent-swarm-test-chat.mjs
+++ b/apps/ai-game-creator-shell/scripts/agent-swarm-test-chat.mjs
@@ -36,6 +36,8 @@ export const ungeneratedGameEntryMarker =
'还没有生成游戏。回到聊天输入创意并确认生成后';
export const defaultRealSwarmTestTask =
'制作一个可直接试玩的原创植物塔防小游戏:玩家选择并放置原创守卫阻挡敌人,完成波次后可以进入下一关并重新开始。主题、单位名称与视觉语言必须原创,不使用任何现有游戏角色、单位名、Logo 或受保护视觉语言。请自主完成正式产物、静态检查和双视口试玩验证。';
+export const defaultRealSwarmPlanTask =
+ '我想做一款原创横版像素解谜小游戏,主角是一个能操控自己影子的小机器人,影子可以变成平台和开关。请完成立项策划并给出 Fast GDD。主题、角色名与视觉语言必须原创,不使用任何现有游戏角色、名称、Logo 或受保护视觉语言。';
export const swarmTurnReportPrefix = '[turn.report] ';
export const swarmTurnReportSchema = 'game-creator-swarm-turn-report.v1';
@@ -138,6 +140,7 @@ export const usage = `用法:
--keep-project 保留自动创建的一次性项目
--no-open 手工模式启动预览但不自动打开浏览器
--task <需求> 通过 manual 入口非交互提交自定义需求
+ --plan 走「做方案」立项策划入口,不做游戏,不做产物验收和试玩
--timeout-minutes <分钟> 设置本次执行期限;自动任务默认 50 分钟,手工模式默认不限时
--dry-run 只检查目录发现和项目准备,不启动 LLM
-h, --help 显示帮助`;
@@ -169,6 +172,7 @@ export function parseSwarmTestArguments(args) {
keepProject: false,
openBrowser: true,
task: null,
+ plan: false,
timeoutMinutes: null,
dryRun: false,
help: false,
@@ -193,6 +197,8 @@ export function parseSwarmTestArguments(args) {
if (task.length > 4_000) throw new Error('--task 不能超过 4000 字符');
options.task = task;
index += 1;
+ } else if (argument === '--plan') {
+ options.plan = true;
} else if (argument === '--timeout-minutes') {
if (options.timeoutMinutes !== null) {
throw new Error('--timeout-minutes 只能指定一次');
@@ -211,7 +217,8 @@ export function parseSwarmTestArguments(args) {
}
export function shouldStartPersistentPreview(options) {
- return !options.task;
+ // 立项策划链路只出 GDD,没有可试玩产物,任何模式都不该起预览。
+ return !options.task && !options.plan;
}
export function resolveSwarmTestTimeoutMs(options) {
@@ -923,13 +930,32 @@ async function runInteractiveCargo(cliArguments, setActiveChild) {
return result;
}
-async function runTaskCargo(cliArguments, task, setActiveChild, timeoutMs) {
+// 立项策划跑 standard 档,`agent.delegate` 这类动作按项目权限策略必须逐个确认,
+// 而确认和问询都只从 CLI 的 stdin 读。自主构建档没有这一步,所以只有 --plan 需要
+// 一个把「人坐在终端前敲 approve」自动化掉的应答器;判据本身仍然走后端确认命令。
+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;
+}
+
+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;
child.stdout.setEncoding('utf8');
child.stdout.on('data', (chunk) => {
process.stdout.write(chunk);
@@ -940,10 +966,26 @@ async function runTaskCargo(cliArguments, task, setActiveChild, timeoutMs) {
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;
+ // turn 已给出回执后不再应答,直接收 stdin 让 CLI 正常退出。
+ if (settled) {
+ child.stdin.end();
+ return;
+ }
+ const reply = nextSwarmAutoPilotReply(pendingLine);
+ if (reply === null) return;
+ console.log(`[自动应答] ${reply}`);
+ pendingLine = '';
+ child.stdin.write(`${reply}\n`);
});
- child.stdin.end(`${task}\n`);
+ if (autoPilot) {
+ child.stdin.write(`${task}\n`);
+ } else {
+ child.stdin.end(`${task}\n`);
+ }
try {
const result = await childExitWithTimeout(
child,
@@ -1645,6 +1687,42 @@ export async function validateSwarmProjectArtifacts(projectPath, options) {
return inspection;
}
+export const planningOutputPaths = [
+ '.agent/planning/session.json',
+ '.agent/planning/index.json',
+ '.agent/planning/pending.json',
+ 'game/fast_gdd.md',
+];
+
+export async function inspectPlanningOutputs(projectPath) {
+ const outputs = [];
+ for (const relativePath of planningOutputPaths) {
+ const absolutePath = path.join(projectPath, ...relativePath.split('/'));
+ const metadata = await lstat(absolutePath).catch((error) => {
+ if (error?.code === 'ENOENT') return null;
+ throw error;
+ });
+ outputs.push({
+ path: relativePath,
+ exists: Boolean(metadata?.isFile()),
+ bytes: metadata?.isFile() ? metadata.size : 0,
+ });
+ }
+ return outputs;
+}
+
+async function reportPlanningOutputs(projectPath) {
+ const outputs = await inspectPlanningOutputs(projectPath);
+ console.log('\n立项策划产物:');
+ for (const output of outputs) {
+ console.log(
+ output.exists
+ ? ` [有] ${output.path}(${output.bytes} 字节)`
+ : ` [无] ${output.path}`,
+ );
+ }
+}
+
export async function hasConfiguredEditorApiKey(configDir) {
let configured = false;
for (const fileName of [configFileName, localConfigFileName]) {
@@ -1869,10 +1947,11 @@ export async function runSwarmTestChat(options) {
);
}
console.log('LLM 配置已就绪。');
+ const requirementNoun = options.plan ? '立项策划需求' : '游戏需求';
console.log(
options.task
- ? '已提交一条非交互游戏需求,正在等待 Swarm 自主完成。\n'
- : '输入一条游戏需求并回车;提交后按 Ctrl+D,让 Swarm 自主完成。\n',
+ ? `已提交一条非交互${requirementNoun},正在等待 Swarm 自主完成。\n`
+ : `输入一条${requirementNoun}并回车;提交后按 Ctrl+D,让 Swarm 自主完成。\n`,
);
phase = 'chat';
@@ -1882,7 +1961,8 @@ export async function runSwarmTestChat(options) {
runtimeConfig.path,
'--swarm-chat',
'--init',
- '--autonomous-game-build',
+ // 做方案链路只能跑 standard 档,后端对 plan + autonomous 是硬否决。
+ options.plan ? '--plan' : '--autonomous-game-build',
project.path,
];
let chat;
@@ -1895,6 +1975,7 @@ export async function runSwarmTestChat(options) {
timeoutDeadline === null
? null
: Math.max(1, timeoutDeadline - Date.now()),
+ options.plan,
)
: await runInteractiveCargo(chatArguments, setActiveChild);
} catch (error) {
@@ -1910,6 +1991,15 @@ export async function runSwarmTestChat(options) {
if (options.task) {
turnReport = parseSettledSwarmTurnReport(chat.turnReportOutput);
}
+ if (options.plan) {
+ // 立项策划不出游戏产物,正式验收在 GDD 审批卡上;这里只报告落盘情况,
+ // 是否收束已经由 CLI 的退出码判过了。
+ phase = 'plan-report';
+ await reportPlanningOutputs(project.path);
+ phase = 'complete';
+ console.log('\n立项策划链路已收束:Run 正常结束,策划产物见上方清单。');
+ break session;
+ }
phase = 'artifact-validation';
const requireEditorImages = await hasConfiguredEditorApiKey(
runtimeConfig.path,
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 d957fc284..803454cb9 100644
--- a/apps/ai-game-creator-shell/src-tauri/src/cli.rs
+++ b/apps/ai-game-creator-shell/src-tauri/src/cli.rs
@@ -706,7 +706,7 @@ pub(crate) fn parse_cli_command(args: &[String]) -> Result