新增立项策划链路的无头入口
`--swarm-chat` 加 `--plan`,把新根 Run 绑定到 project-supervisor-plan standard 档,和 GUI「做方案」按钮走同一条门禁;与 --autonomous-game-build / --game-chat-smoke 互斥,非总控父 Agent 拒绝。plan 与做游戏同为 standard 档, 只按 profile 匹配会串链,因此 plan 入口按 source 精确认领自己的 Run。plan 根 Run 不接受 steer,无头入口把 source 交给后端才能触发既有否决,其余入口维持 不带 source 的旧行为。 harness 侧 --plan 跳过游戏产物验收和试玩,改为报告策划产物落盘清单。立项策划 跑 standard 档,agent.delegate 这类动作按项目权限策略必须逐个确认,而确认只从 CLI stdin 读;自主构建档没有这一步,所以只给 --plan 加一个 stdin 应答器,确认 本身仍然走后端确认命令。做游戏链路不受影响。 真实无头跑已验证:plan 根 Run 正常起来,source=project-supervisor-plan、 runProfile=standard,两个 agent.delegate 确认卡自动批准并进入委派。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -706,7 +706,7 @@ pub(crate) fn parse_cli_command(args: &[String]) -> Result<Option<CliCommand>, S
|
||||
}));
|
||||
}
|
||||
if args.first().map(String::as_str) == Some("--swarm-chat") {
|
||||
const USAGE: &str = "用法:--swarm-chat [--init] [--autonomous-game-build] [--game-chat-smoke] <本地项目绝对路径> [parentAgentId]";
|
||||
const USAGE: &str = "用法:--swarm-chat [--init] [--autonomous-game-build] [--game-chat-smoke] [--plan] <本地项目绝对路径> [parentAgentId]";
|
||||
let mut rest = args[1..].to_vec();
|
||||
let initialize = if let Some(index) = rest.iter().position(|arg| arg == "--init") {
|
||||
rest.remove(index);
|
||||
@@ -746,6 +746,18 @@ pub(crate) fn parse_cli_command(args: &[String]) -> Result<Option<CliCommand>, S
|
||||
}
|
||||
_ => return Err(USAGE.to_string()),
|
||||
};
|
||||
let plan = match rest.iter().filter(|arg| arg.as_str() == "--plan").count() {
|
||||
0 => false,
|
||||
1 => {
|
||||
let index = rest
|
||||
.iter()
|
||||
.position(|arg| arg == "--plan")
|
||||
.expect("counted plan 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());
|
||||
}
|
||||
@@ -760,6 +772,20 @@ pub(crate) fn parse_cli_command(args: &[String]) -> Result<Option<CliCommand>, S
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
// 立项策划根 Run 只跑 standard 档(后端 reject_supervisor_plan_autonomous_profile
|
||||
// 同样否决),且必须挂在总控上;这里先拦一道,免得建完项目才失败。
|
||||
if plan
|
||||
&& (autonomous_game_build
|
||||
|| game_chat_smoke
|
||||
|| rest.get(1).is_some_and(|parent| {
|
||||
parent.trim() != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|
||||
}))
|
||||
{
|
||||
return Err(
|
||||
"--plan 仅允许 project-supervisor 的 standard 档,不能搭配 --autonomous-game-build / --game-chat-smoke"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
return Ok(Some(CliCommand::SwarmChat {
|
||||
project_path: PathBuf::from(&rest[0]),
|
||||
parent_agent_id: rest
|
||||
@@ -774,6 +800,8 @@ pub(crate) fn parse_cli_command(args: &[String]) -> Result<Option<CliCommand>, S
|
||||
},
|
||||
supervisor_source: if game_chat_smoke {
|
||||
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE
|
||||
} else if plan {
|
||||
AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE
|
||||
} else {
|
||||
AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE
|
||||
},
|
||||
@@ -2110,6 +2138,58 @@ mod tests {
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swarm_chat_plan_flag_selects_plan_source_on_standard_profile() {
|
||||
let project_path = std::env::current_dir().expect("current directory");
|
||||
let command = parse_cli_command(&[
|
||||
"--swarm-chat".to_string(),
|
||||
"--init".to_string(),
|
||||
"--plan".to_string(),
|
||||
project_path.display().to_string(),
|
||||
])
|
||||
.expect("parse plan swarm chat")
|
||||
.expect("plan swarm chat command");
|
||||
assert_eq!(
|
||||
command,
|
||||
CliCommand::SwarmChat {
|
||||
project_path,
|
||||
parent_agent_id: GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string(),
|
||||
initialize: true,
|
||||
run_profile: AGENT_RUNTIME_RUN_PROFILE_STANDARD.to_string(),
|
||||
supervisor_source: AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE,
|
||||
}
|
||||
);
|
||||
assert!(parse_cli_command(&[
|
||||
"--swarm-chat".to_string(),
|
||||
"--plan".to_string(),
|
||||
"--autonomous-game-build".to_string(),
|
||||
"/tmp/game-project".to_string(),
|
||||
])
|
||||
.is_err());
|
||||
assert!(parse_cli_command(&[
|
||||
"--swarm-chat".to_string(),
|
||||
"--plan".to_string(),
|
||||
"--autonomous-game-build".to_string(),
|
||||
"--game-chat-smoke".to_string(),
|
||||
"/tmp/game-project".to_string(),
|
||||
])
|
||||
.is_err());
|
||||
assert!(parse_cli_command(&[
|
||||
"--swarm-chat".to_string(),
|
||||
"--plan".to_string(),
|
||||
"/tmp/game-project".to_string(),
|
||||
"code-prototype".to_string(),
|
||||
])
|
||||
.is_err());
|
||||
assert!(parse_cli_command(&[
|
||||
"--swarm-chat".to_string(),
|
||||
"--plan".to_string(),
|
||||
"--plan".to_string(),
|
||||
"/tmp/game-project".to_string(),
|
||||
])
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swarm_chat_rejects_missing_or_extra_arguments() {
|
||||
assert!(parse_cli_command(&["--swarm-chat".to_string()]).is_err());
|
||||
|
||||
@@ -47,8 +47,11 @@ pub(super) enum SwarmNewRunLaunch<'a> {
|
||||
impl SwarmNewRunLaunch<'_> {
|
||||
pub(super) fn expected_parent_source(self) -> Option<&'static str> {
|
||||
match self {
|
||||
// 受限入口按 source 精确认领自己的 Run:立项策划和做游戏同样是 standard 档,
|
||||
// 只按 profile 匹配会让 --plan 把上一条 CLI 链路的残留 Run 当成自己的。
|
||||
Self::ProjectSupervisor { source, .. }
|
||||
if source == AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE =>
|
||||
if source == AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE
|
||||
|| source == AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE =>
|
||||
{
|
||||
Some(source)
|
||||
}
|
||||
@@ -71,7 +74,9 @@ pub(super) fn resolve_swarm_new_run_launch<'a>(
|
||||
if parent_agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID {
|
||||
if !matches!(
|
||||
supervisor_source,
|
||||
AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE | AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE
|
||||
AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE
|
||||
| AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE
|
||||
| AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE
|
||||
) {
|
||||
return Err(format!(
|
||||
"不支持的 Project Supervisor source:{supervisor_source}"
|
||||
@@ -82,13 +87,18 @@ pub(super) fn resolve_swarm_new_run_launch<'a>(
|
||||
{
|
||||
return Err("game-chat smoke source 仅支持 autonomous-game-build".to_string());
|
||||
}
|
||||
// 与 GUI「做方案」入口同一条门禁;能力开关和 source 可信性由 task_start 统一判定,
|
||||
// 这里只把档位冲突提前到起 Run 之前。
|
||||
reject_supervisor_plan_autonomous_profile(supervisor_source, run_profile)?;
|
||||
return Ok(SwarmNewRunLaunch::ProjectSupervisor {
|
||||
source: supervisor_source,
|
||||
run_profile,
|
||||
});
|
||||
}
|
||||
if supervisor_source != AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE {
|
||||
return Err("game-chat smoke source 仅支持 project-supervisor 总控入口".to_string());
|
||||
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());
|
||||
|
||||
@@ -138,6 +138,43 @@ fn restricted_game_chat_smoke_launch_selects_trusted_single_main_source() {
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plan_launch_claims_its_own_source_and_rejects_autonomous_profile() {
|
||||
assert_eq!(
|
||||
resolve_swarm_new_run_launch(
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
AGENT_RUNTIME_RUN_PROFILE_STANDARD,
|
||||
AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE,
|
||||
)
|
||||
.expect("resolve plan launch"),
|
||||
SwarmNewRunLaunch::ProjectSupervisor {
|
||||
source: AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE,
|
||||
run_profile: AGENT_RUNTIME_RUN_PROFILE_STANDARD,
|
||||
}
|
||||
);
|
||||
// 立项策划 Run 和做游戏 Run 都是 standard 档,只有按 source 认领才不会串链。
|
||||
assert_eq!(
|
||||
SwarmNewRunLaunch::ProjectSupervisor {
|
||||
source: AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE,
|
||||
run_profile: AGENT_RUNTIME_RUN_PROFILE_STANDARD,
|
||||
}
|
||||
.expected_parent_source(),
|
||||
Some(AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE)
|
||||
);
|
||||
assert!(resolve_swarm_new_run_launch(
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD,
|
||||
AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE,
|
||||
)
|
||||
.is_err());
|
||||
assert!(resolve_swarm_new_run_launch(
|
||||
"code-prototype",
|
||||
AGENT_RUNTIME_RUN_PROFILE_STANDARD,
|
||||
AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE,
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_run_steer_preserves_bound_autonomous_profile() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
|
||||
@@ -377,6 +377,12 @@ fn steer_and_wait_for_swarm_turn<W: Write>(
|
||||
) -> Result<SwarmChatFlow, String> {
|
||||
require_external_agent_runner_for_cli_runtime_write(root)?;
|
||||
let steer_id = format!("swarm-steer-{}", unix_millis());
|
||||
// 立项策划根 Run 不接受 steer(换根会作废旧委派链剩余的问询轮次),GUI 侧不发起、
|
||||
// 后端另有否决。无头入口只有把 plan source 交给后端,这道否决才会生效;其余入口
|
||||
// 继续沿用不带 source 的旧行为,免得给 game-chat / CLI 链路加新的一致性判据。
|
||||
let steer_source = expected_parent_source
|
||||
.filter(|source| agent_runtime_supervisor_source_is_plan(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(),
|
||||
@@ -385,7 +391,7 @@ fn steer_and_wait_for_swarm_turn<W: Write>(
|
||||
steer_id.clone(),
|
||||
message.to_string(),
|
||||
Some(run_profile.to_string()),
|
||||
None,
|
||||
steer_source,
|
||||
))?;
|
||||
writeln!(
|
||||
output,
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
cleanupSwarmTestProject,
|
||||
cleanupSwarmTestRuntimeConfig,
|
||||
configFileName,
|
||||
defaultRealSwarmPlanTask,
|
||||
defaultRealSwarmTestTask,
|
||||
defaultRuntimeConfigDirCandidates,
|
||||
discoverRuntimeConfigDir,
|
||||
@@ -34,6 +35,7 @@ import {
|
||||
hasIncompleteArtifactMarker,
|
||||
inspectSwarmProjectArtifacts,
|
||||
localConfigFileName,
|
||||
nextSwarmAutoPilotReply,
|
||||
parseRunnerShutdownOutput,
|
||||
parseSettledSwarmTurnReport,
|
||||
parseSwarmTestArguments,
|
||||
@@ -653,6 +655,7 @@ describe('Swarm test argument parsing', () => {
|
||||
keepProject: false,
|
||||
openBrowser: true,
|
||||
task: null,
|
||||
plan: false,
|
||||
timeoutMinutes: null,
|
||||
dryRun: false,
|
||||
help: false,
|
||||
@@ -673,6 +676,7 @@ describe('Swarm test argument parsing', () => {
|
||||
'--no-open',
|
||||
'--task',
|
||||
'生成一款可试玩的塔防游戏',
|
||||
'--plan',
|
||||
'--timeout-minutes',
|
||||
'75',
|
||||
'--dry-run',
|
||||
@@ -684,6 +688,7 @@ describe('Swarm test argument parsing', () => {
|
||||
keepProject: true,
|
||||
openBrowser: false,
|
||||
task: '生成一款可试玩的塔防游戏',
|
||||
plan: true,
|
||||
timeoutMinutes: 75,
|
||||
dryRun: true,
|
||||
help: true,
|
||||
@@ -692,6 +697,24 @@ describe('Swarm test argument parsing', () => {
|
||||
expect(() => parseSwarmTestArguments(['--task', ''])).toThrowError();
|
||||
});
|
||||
|
||||
it('auto-answers only the prompts the plan lane actually blocks on', () => {
|
||||
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('keeps persistent preview only for manual chat mode', () => {
|
||||
expect(shouldStartPersistentPreview(parseSwarmTestArguments([]))).toBe(
|
||||
true,
|
||||
@@ -701,6 +724,10 @@ describe('Swarm test argument parsing', () => {
|
||||
parseSwarmTestArguments(['--task', '生成一款可试玩的塔防游戏']),
|
||||
),
|
||||
).toBe(false);
|
||||
// 做方案链路不出可试玩产物,交互模式同样不该起预览。
|
||||
expect(
|
||||
shouldStartPersistentPreview(parseSwarmTestArguments(['--plan'])),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('applies a bounded default only to non-interactive tasks', () => {
|
||||
@@ -1804,6 +1831,12 @@ describe('package script registration', () => {
|
||||
expect(rootPackage.scripts?.['agc:test:chat:manual']).toBe(
|
||||
'npm --prefix apps/ai-game-creator-shell run test:chat:manual --',
|
||||
);
|
||||
expect(rootPackage.scripts?.['agc:test:plan']).toBe(
|
||||
'npm --prefix apps/ai-game-creator-shell run test:plan --',
|
||||
);
|
||||
expect(rootPackage.scripts?.['agc:test:plan:manual']).toBe(
|
||||
'npm --prefix apps/ai-game-creator-shell run test:plan:manual --',
|
||||
);
|
||||
expect(appPackage.scripts?.['test:chat']).toBe(
|
||||
`node scripts/agent-swarm-test-chat.mjs --task "${defaultRealSwarmTestTask}" --no-open`,
|
||||
);
|
||||
@@ -1813,6 +1846,12 @@ describe('package script registration', () => {
|
||||
expect(appPackage.scripts?.['test:chat:manual']).toBe(
|
||||
'node scripts/agent-swarm-test-chat.mjs',
|
||||
);
|
||||
expect(appPackage.scripts?.['test:plan']).toBe(
|
||||
`node scripts/agent-swarm-test-chat.mjs --plan --task "${defaultRealSwarmPlanTask}"`,
|
||||
);
|
||||
expect(appPackage.scripts?.['test:plan:manual']).toBe(
|
||||
'node scripts/agent-swarm-test-chat.mjs --plan',
|
||||
);
|
||||
expect(checkConfigSource).toMatch(
|
||||
/packageConfig\.scripts\?\.config\s*!==\s*'node scripts\/game-creator-config-wizard\.mjs'/u,
|
||||
);
|
||||
|
||||
@@ -155,6 +155,8 @@
|
||||
"agc:test": "npm --prefix apps/ai-game-creator-shell run agent-runtime:supervisor-autonomous-playable-lane-defense-deterministic-e2e --",
|
||||
"agc:test:chat": "npm --prefix apps/ai-game-creator-shell run test:chat --",
|
||||
"agc:test:chat:manual": "npm --prefix apps/ai-game-creator-shell run test:chat:manual --",
|
||||
"agc:test:plan": "npm --prefix apps/ai-game-creator-shell run test:plan --",
|
||||
"agc:test:plan:manual": "npm --prefix apps/ai-game-creator-shell run test:plan:manual --",
|
||||
"ai-game-creator-shell:dev": "npm --prefix apps/ai-game-creator-shell run dev",
|
||||
"ai-game-creator-shell:dev-server": "npm --prefix apps/ai-game-creator-shell run dev-server",
|
||||
"ai-game-creator-shell:build": "npm --prefix apps/ai-game-creator-shell run build --",
|
||||
|
||||
Reference in New Issue
Block a user