Files
Genarrative/apps/ai-game-creator-shell/src-tauri/src/cli.rs
T
suzmii 44e1e03f2e
Project CI / Repository checks (pull_request) Failing after 13s
Project CI / Backend tests (pull_request) Failing after 11s
Project CI / Frontend tests (pull_request) Failing after 2m13s
Project CI / Native shell tests (pull_request) Failing after 4m39s
格式化 AGC Rust 模块
按当前 rustfmt 规则整理 Agent Runtime、配置、预览、会话与相关测试的代码布局。

本提交不改变运行逻辑,保持 AGC Rust 工作区格式门禁可通过。
2026-09-02 14:57:18 +08:00

2417 lines
90 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use super::*;
const PREVIEW_SERVE_USAGE: &str = "用法:--preview-serve <本地项目绝对路径>";
#[derive(Debug, Eq, PartialEq)]
pub(crate) enum CliCommand {
LlmStatus,
PreviewServe {
project_path: PathBuf,
},
AgentChat {
project_path: PathBuf,
agent_id: String,
prompt: String,
},
DirectCodexChat {
project_path: PathBuf,
prompt: String,
},
AgentTask {
project_path: PathBuf,
agent_id: String,
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,
run_id: String,
task: String,
initialize: bool,
},
AgentRuntimeStatus {
project_path: PathBuf,
agent_id: String,
},
AgentContextCompact {
project_path: PathBuf,
agent_id: String,
session_id: Option<String>,
},
AgentGoalStatus {
project_path: PathBuf,
agent_id: String,
session_id: String,
},
AgentGoalStart {
project_path: PathBuf,
agent_id: String,
session_id: String,
run_id: String,
initialize: bool,
},
AgentGoalEdit {
project_path: PathBuf,
agent_id: String,
session_id: String,
goal_id: String,
expected_revision: u64,
},
AgentGoalPause {
project_path: PathBuf,
agent_id: String,
session_id: String,
goal_id: String,
expected_revision: u64,
},
AgentGoalResume {
project_path: PathBuf,
agent_id: String,
session_id: String,
goal_id: String,
expected_revision: u64,
},
AgentGoalClear {
project_path: PathBuf,
agent_id: String,
session_id: String,
goal_id: String,
expected_revision: u64,
},
AgentConfirm {
project_path: PathBuf,
agent_id: String,
run_id: String,
action_id: String,
},
AgentCancel {
project_path: PathBuf,
agent_id: String,
run_id: String,
},
AgentRetry {
project_path: PathBuf,
agent_id: String,
run_id: String,
next_run_id: String,
},
AgentSteer {
project_path: PathBuf,
agent_id: String,
session_id: String,
run_id: String,
steer_id: String,
},
AgentResume {
project_path: PathBuf,
},
PlanGddStatus {
project_path: PathBuf,
},
PlanGddDecide {
project_path: PathBuf,
action: String,
response_id: Option<String>,
read_comment_from_stdin: bool,
},
RunnerStatus,
RunnerShutdownIfIdle,
AgentRun {
project_path: PathBuf,
prompt: String,
wait_for_enter: bool,
},
}
#[derive(Debug, Deserialize, Eq, PartialEq)]
#[serde(deny_unknown_fields)]
struct CliAgentGoalPayload {
outcome: String,
constraints: Vec<String>,
verification: Vec<String>,
}
impl CliCommand {
pub(crate) fn requires_external_agent_runner(&self) -> bool {
matches!(
self,
Self::AgentTask { .. }
| Self::SwarmChat { .. }
| Self::AgentEnqueue { .. }
| Self::AgentContextCompact { .. }
| Self::AgentConfirm { .. }
| Self::AgentCancel { .. }
| Self::AgentRetry { .. }
| Self::AgentSteer { .. }
| Self::AgentGoalStart { .. }
| Self::AgentGoalEdit { .. }
| Self::AgentGoalPause { .. }
| Self::AgentGoalResume { .. }
| Self::AgentGoalClear { .. }
| Self::AgentResume { .. }
| Self::PlanGddDecide { .. }
| Self::RunnerShutdownIfIdle
)
}
pub(crate) fn is_read_only_status(&self) -> bool {
matches!(
self,
Self::AgentRuntimeStatus { .. }
| Self::AgentGoalStatus { .. }
| Self::PlanGddStatus { .. }
| Self::RunnerStatus
)
}
pub(crate) fn requires_started_external_agent_runner(&self) -> bool {
self.requires_external_agent_runner()
&& !matches!(
self,
Self::AgentCancel { .. } | Self::AgentResume { .. } | Self::RunnerShutdownIfIdle
)
}
fn project_path_mut(&mut self) -> Option<(&mut PathBuf, bool)> {
match self {
Self::AgentTask {
project_path,
initialize,
..
}
| Self::SwarmChat {
project_path,
initialize,
..
}
| Self::AgentEnqueue {
project_path,
initialize,
..
}
| Self::AgentGoalStart {
project_path,
initialize,
..
} => Some((project_path, *initialize)),
Self::AgentChat { project_path, .. }
| Self::DirectCodexChat { project_path, .. }
| Self::AgentRuntimeStatus { project_path, .. }
| Self::AgentContextCompact { project_path, .. }
| Self::AgentGoalStatus { project_path, .. }
| Self::AgentGoalEdit { project_path, .. }
| Self::AgentGoalPause { project_path, .. }
| Self::AgentGoalResume { project_path, .. }
| Self::AgentGoalClear { project_path, .. }
| Self::AgentConfirm { project_path, .. }
| Self::AgentCancel { project_path, .. }
| Self::AgentRetry { project_path, .. }
| Self::AgentSteer { project_path, .. }
| Self::AgentResume { project_path }
| Self::PlanGddStatus { project_path }
| Self::PlanGddDecide { project_path, .. }
| Self::PreviewServe { project_path }
| Self::AgentRun { project_path, .. } => Some((project_path, false)),
Self::LlmStatus | Self::RunnerStatus | Self::RunnerShutdownIfIdle => None,
}
}
}
fn canonicalize_cli_path(
path: &Path,
label: &str,
allow_missing_leaf: bool,
) -> Result<PathBuf, String> {
if !path.is_absolute() {
return Err(format!("{label} 必须是绝对路径"));
}
match fs::canonicalize(path) {
Ok(path) => Ok(path),
Err(error) if allow_missing_leaf && error.kind() == std::io::ErrorKind::NotFound => {
let parent = path
.parent()
.ok_or_else(|| format!("{label} 缺少可解析的父目录"))?;
let file_name = path
.file_name()
.ok_or_else(|| format!("{label} 缺少目录名"))?;
let parent = fs::canonicalize(parent).map_err(|parent_error| {
format!(
"解析 {label} 父目录失败:{}: {parent_error}",
parent.display()
)
})?;
Ok(parent.join(file_name))
}
Err(error) => Err(format!("解析 {label} 失败:{}: {error}", path.display())),
}
}
pub(crate) fn prepare_cli_command_paths(
command: &mut CliCommand,
runtime_config_dir: Option<&Path>,
) -> Result<Option<PathBuf>, String> {
let project_path = if let Some((path, allow_missing_leaf)) = command.project_path_mut() {
let canonical = canonicalize_cli_path(path, "本地项目路径", allow_missing_leaf)?;
*path = canonical.clone();
Some(canonical)
} else {
None
};
let config_dir = runtime_config_dir
.map(|path| canonicalize_cli_path(path, "--config-dir", true))
.transpose()?;
if command.requires_external_agent_runner() && config_dir.is_none() {
return Err(
"Agent Runtime 写命令必须显式传入 --config-dir <项目外 AppData 绝对路径>".to_string(),
);
}
if let (Some(config_dir), Some(project_path)) = (&config_dir, &project_path) {
validate_game_creator_runtime_config_dir_outside_project(config_dir, project_path)?;
}
Ok(config_dir)
}
pub(crate) fn take_cli_runtime_config_dir(
args: &mut Vec<String>,
) -> Result<Option<PathBuf>, String> {
let positions = args
.iter()
.enumerate()
.filter_map(|(index, arg)| (arg == "--config-dir").then_some(index))
.collect::<Vec<_>>();
if positions.len() > 1 {
return Err("--config-dir 只能指定一次".to_string());
}
let Some(index) = positions.first().copied() else {
return Ok(None);
};
let value = args
.get(index + 1)
.map(String::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or_else(|| "--config-dir 缺少目录路径".to_string())?;
let path = PathBuf::from(value);
if !path.is_absolute() {
return Err("--config-dir 必须是绝对路径".to_string());
}
args.drain(index..=index + 1);
Ok(Some(path))
}
pub(crate) fn game_creator_llm_status_lines(status: &GameCreatorLlmConfigStatus) -> Vec<String> {
let mut lines = vec![
format!("agent.mode={}", status.agent_mode),
format!("llm.configured={}", status.configured),
format!(
"llm.accountCredentialState={}",
status.account_credential_state
),
format!("llm.officialRouteLocked={}", status.official_route_locked),
format!("llm.reasoningEffort={}", status.reasoning_effort),
format!("llm.stream={}", status.stream),
format!("llm.contextWindowTokens={}", status.context_window_tokens),
format!(
"llm.autoCompactTokenLimit={}",
status.auto_compact_token_limit
),
format!(
"llm.toolOutputTokenLimit={}",
status.tool_output_token_limit
),
format!("llm.requestTimeoutMs={}", status.request_timeout_ms),
format!("llm.maxRetries={}", status.max_retries),
format!("llm.retryBackoffMs={}", status.retry_backoff_ms),
];
if status.agent_mode == GAME_CREATOR_AGENT_MODE_CODEX_APP_SERVER {
lines.push(format!(
"llm.controlledWebSearchEnabled={}",
status.web_search_enabled
));
lines.push("llm.codexNativeWebSearch=disabled".to_string());
} else {
lines.push(format!(
"llm.webSearchEnabled={}",
status.web_search_enabled
));
}
for agent in &status.agents {
lines.push(format!(
"llm.agent.{}.configured={}",
agent.agent_id, agent.configured
));
lines.push(format!(
"llm.agent.{}.accountCredentialState={}",
agent.agent_id, agent.account_credential_state
));
lines.push(format!(
"llm.agent.{}.officialRouteLocked={}",
agent.agent_id, agent.official_route_locked
));
lines.push(format!(
"llm.agent.{}.reasoningEffort={}",
agent.agent_id, agent.reasoning_effort
));
lines.push(format!(
"llm.agent.{}.stream={}",
agent.agent_id, agent.stream
));
if agent.agent_mode == GAME_CREATOR_AGENT_MODE_CODEX_APP_SERVER {
lines.push(format!(
"llm.agent.{}.controlledWebSearchEnabled={}",
agent.agent_id, agent.web_search_enabled
));
lines.push(format!(
"llm.agent.{}.codexNativeWebSearch=disabled",
agent.agent_id
));
} else {
lines.push(format!(
"llm.agent.{}.webSearchEnabled={}",
agent.agent_id, agent.web_search_enabled
));
}
lines.push(format!(
"llm.agent.{}.contextWindowTokens={}",
agent.agent_id, agent.context_window_tokens
));
lines.push(format!(
"llm.agent.{}.autoCompactTokenLimit={}",
agent.agent_id, agent.auto_compact_token_limit
));
lines.push(format!(
"llm.agent.{}.toolOutputTokenLimit={}",
agent.agent_id, agent.tool_output_token_limit
));
lines.push(format!(
"llm.agent.{}.requestTimeoutMs={}",
agent.agent_id, agent.request_timeout_ms
));
lines.push(format!(
"llm.agent.{}.maxRetries={}",
agent.agent_id, agent.max_retries
));
lines.push(format!(
"llm.agent.{}.retryBackoffMs={}",
agent.agent_id, agent.retry_backoff_ms
));
if let Some(error) = agent.error.as_deref() {
lines.push(format!("llm.agent.{}.error={error}", agent.agent_id));
}
}
if let Some(error) = status.error.as_deref() {
lines.push(format!("llm.error={error}"));
}
lines
}
fn read_cli_agent_steer_instruction(reader: &mut impl Read) -> Result<String, String> {
const MAX_INSTRUCTION_BYTES: usize = 4 * 1024;
const MAX_STDIN_BYTES: u64 = 8 * 1024;
let mut bytes = Vec::new();
reader
.take(MAX_STDIN_BYTES + 1)
.read_to_end(&mut bytes)
.map_err(|error| format!("从 stdin 读取 Agent 追加指令失败:{error}"))?;
if bytes.len() as u64 > MAX_STDIN_BYTES {
return Err(format!(
"Agent 追加指令 stdin 超过 {MAX_STDIN_BYTES} 字节上限"
));
}
let instruction = String::from_utf8(bytes)
.map_err(|_| "Agent 追加指令 stdin 必须是 UTF-8 文本".to_string())?;
let instruction = instruction.trim();
if instruction.is_empty() {
return Err("Agent 追加指令 stdin 不能为空".to_string());
}
if instruction.len() > MAX_INSTRUCTION_BYTES {
return Err(format!(
"Agent 追加指令 stdin 超过 {MAX_INSTRUCTION_BYTES} 字节上限"
));
}
Ok(instruction.to_string())
}
fn read_cli_plan_gdd_approval_comment(reader: &mut impl Read) -> Result<String, String> {
const MAX_STDIN_BYTES: u64 = 8 * 1024;
let mut bytes = Vec::new();
reader
.take(MAX_STDIN_BYTES + 1)
.read_to_end(&mut bytes)
.map_err(|error| format!("从 stdin 读取 GDD 审批意见失败:{error}"))?;
if bytes.len() as u64 > MAX_STDIN_BYTES {
return Err(format!(
"GDD 审批意见 stdin 超过 {MAX_STDIN_BYTES} 字节上限"
));
}
let comment =
String::from_utf8(bytes).map_err(|_| "GDD 审批意见 stdin 必须是 UTF-8 文本".to_string())?;
let comment = comment.trim();
if comment.is_empty() {
return Err("GDD 审批意见 stdin 不能为空".to_string());
}
// 长度上界交给 normalize_plan_gdd_approval_comment:审批意见的 11000 scalar
// 约束是写入侧权威,CLI 再抄一份就会有两个会漂移的判据。
Ok(comment.to_string())
}
fn take_cli_named_flag_value(
args: &mut Vec<String>,
flag: &str,
usage: &str,
) -> Result<Option<String>, String> {
let positions = args
.iter()
.enumerate()
.filter_map(|(index, arg)| (arg == flag).then_some(index))
.collect::<Vec<_>>();
if positions.len() > 1 {
return Err(usage.to_string());
}
let Some(index) = positions.first().copied() else {
return Ok(None);
};
let value = args
.get(index + 1)
.map(String::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or_else(|| usage.to_string())?
.to_string();
args.drain(index..=index + 1);
Ok(Some(value))
}
fn read_cli_agent_goal_payload(reader: &mut impl Read) -> Result<CliAgentGoalPayload, String> {
const MAX_STDIN_BYTES: u64 = 64 * 1024;
let mut bytes = Vec::new();
reader
.take(MAX_STDIN_BYTES + 1)
.read_to_end(&mut bytes)
.map_err(|error| format!("从 stdin 读取 Agent Goal JSON 失败:{error}"))?;
if bytes.len() as u64 > MAX_STDIN_BYTES {
return Err(format!("Agent Goal stdin 超过 {MAX_STDIN_BYTES} 字节上限"));
}
if bytes.iter().all(u8::is_ascii_whitespace) {
return Err("Agent Goal stdin 不能为空".to_string());
}
let mut payload = serde_json::from_slice::<CliAgentGoalPayload>(&bytes)
.map_err(|error| format!("Agent Goal stdin 必须是结构化 JSON{error}"))?;
payload.outcome = payload.outcome.trim().to_string();
if payload.outcome.is_empty() {
return Err("Agent Goal outcome 不能为空".to_string());
}
Ok(payload)
}
fn parse_cli_agent_goal_revision(value: &str, usage: &str) -> Result<u64, String> {
let revision = value.parse::<u64>().map_err(|_| usage.to_string())?;
if revision == 0 {
return Err(usage.to_string());
}
Ok(revision)
}
pub(crate) fn parse_cli_command(args: &[String]) -> Result<Option<CliCommand>, String> {
if args.first().map(String::as_str) == Some("--preview-serve") {
if args.len() != 2 || args[1].trim().is_empty() {
return Err(PREVIEW_SERVE_USAGE.to_string());
}
return Ok(Some(CliCommand::PreviewServe {
project_path: PathBuf::from(&args[1]),
}));
}
if args.first().map(String::as_str) == Some("--llm-status") {
return Ok(Some(CliCommand::LlmStatus));
}
if args.first().map(String::as_str) == Some("--runner-status") {
if args.len() != 1 {
return Err("用法:--runner-status".to_string());
}
return Ok(Some(CliCommand::RunnerStatus));
}
if args.first().map(String::as_str) == Some("--runner-shutdown-if-idle") {
if args.len() != 1 {
return Err("用法:--runner-shutdown-if-idle".to_string());
}
return Ok(Some(CliCommand::RunnerShutdownIfIdle));
}
if args.first().map(String::as_str) == Some("--agent-runtime-status") {
if args.len() != 3 {
return Err("用法:--agent-runtime-status <本地项目绝对路径> <agentId>".to_string());
}
return Ok(Some(CliCommand::AgentRuntimeStatus {
project_path: PathBuf::from(&args[1]),
agent_id: args[2].trim().to_string(),
}));
}
if args.first().map(String::as_str) == Some("--agent-context-compact") {
const USAGE: &str =
"用法:--agent-context-compact <本地项目绝对路径> <agentId> [sessionId]";
if !(args.len() == 3 || args.len() == 4)
|| args[1..].iter().any(|value| value.trim().is_empty())
{
return Err(USAGE.to_string());
}
return Ok(Some(CliCommand::AgentContextCompact {
project_path: PathBuf::from(&args[1]),
agent_id: args[2].trim().to_string(),
session_id: args.get(3).map(|value| value.trim().to_string()),
}));
}
if args.first().map(String::as_str) == Some("--agent-goal-status") {
const USAGE: &str = "用法:--agent-goal-status <本地项目绝对路径> <agentId> <sessionId>";
if args.len() != 4 || args[1..].iter().any(|value| value.trim().is_empty()) {
return Err(USAGE.to_string());
}
return Ok(Some(CliCommand::AgentGoalStatus {
project_path: PathBuf::from(&args[1]),
agent_id: args[2].trim().to_string(),
session_id: args[3].trim().to_string(),
}));
}
if args.first().map(String::as_str) == Some("--agent-goal-start") {
const USAGE: &str = "用法:--agent-goal-start [--init] <本地项目绝对路径> <agentId> <sessionId> <runId> --stdin";
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
};
if rest.len() != 5
|| rest.last().map(String::as_str) != Some("--stdin")
|| rest[..4].iter().any(|value| value.trim().is_empty())
{
return Err(USAGE.to_string());
}
return Ok(Some(CliCommand::AgentGoalStart {
project_path: PathBuf::from(&rest[0]),
agent_id: rest[1].trim().to_string(),
session_id: rest[2].trim().to_string(),
run_id: rest[3].trim().to_string(),
initialize,
}));
}
if args.first().map(String::as_str) == Some("--agent-goal-edit") {
const USAGE: &str = "用法:--agent-goal-edit <本地项目绝对路径> <agentId> <sessionId> <goalId> <revision> --stdin";
if args.len() != 7
|| args.last().map(String::as_str) != Some("--stdin")
|| args[1..5].iter().any(|value| value.trim().is_empty())
{
return Err(USAGE.to_string());
}
return Ok(Some(CliCommand::AgentGoalEdit {
project_path: PathBuf::from(&args[1]),
agent_id: args[2].trim().to_string(),
session_id: args[3].trim().to_string(),
goal_id: args[4].trim().to_string(),
expected_revision: parse_cli_agent_goal_revision(&args[5], USAGE)?,
}));
}
if args.first().map(String::as_str) == Some("--agent-goal-pause") {
const USAGE: &str =
"用法:--agent-goal-pause <本地项目绝对路径> <agentId> <sessionId> <goalId> <revision>";
if args.len() != 6 || args[1..5].iter().any(|value| value.trim().is_empty()) {
return Err(USAGE.to_string());
}
return Ok(Some(CliCommand::AgentGoalPause {
project_path: PathBuf::from(&args[1]),
agent_id: args[2].trim().to_string(),
session_id: args[3].trim().to_string(),
goal_id: args[4].trim().to_string(),
expected_revision: parse_cli_agent_goal_revision(&args[5], USAGE)?,
}));
}
if args.first().map(String::as_str) == Some("--agent-goal-resume") {
const USAGE: &str = "用法:--agent-goal-resume <本地项目绝对路径> <agentId> <sessionId> <goalId> <revision>";
if args.len() != 6 || args[1..5].iter().any(|value| value.trim().is_empty()) {
return Err(USAGE.to_string());
}
return Ok(Some(CliCommand::AgentGoalResume {
project_path: PathBuf::from(&args[1]),
agent_id: args[2].trim().to_string(),
session_id: args[3].trim().to_string(),
goal_id: args[4].trim().to_string(),
expected_revision: parse_cli_agent_goal_revision(&args[5], USAGE)?,
}));
}
if args.first().map(String::as_str) == Some("--agent-goal-clear") {
const USAGE: &str =
"用法:--agent-goal-clear <本地项目绝对路径> <agentId> <sessionId> <goalId> <revision>";
if args.len() != 6 || args[1..5].iter().any(|value| value.trim().is_empty()) {
return Err(USAGE.to_string());
}
return Ok(Some(CliCommand::AgentGoalClear {
project_path: PathBuf::from(&args[1]),
agent_id: args[2].trim().to_string(),
session_id: args[3].trim().to_string(),
goal_id: args[4].trim().to_string(),
expected_revision: parse_cli_agent_goal_revision(&args[5], USAGE)?,
}));
}
if args.first().map(String::as_str) == Some("--agent-confirm") {
if args.len() != 5 {
return Err(
"用法:--agent-confirm <本地项目绝对路径> <agentId> <runId> <actionId>".to_string(),
);
}
return Ok(Some(CliCommand::AgentConfirm {
project_path: PathBuf::from(&args[1]),
agent_id: args[2].trim().to_string(),
run_id: args[3].trim().to_string(),
action_id: args[4].trim().to_string(),
}));
}
if args.first().map(String::as_str) == Some("--agent-cancel") {
const USAGE: &str = "用法:--agent-cancel <本地项目绝对路径> <agentId> <runId>";
if args.len() != 4 || args[1..].iter().any(|value| value.trim().is_empty()) {
return Err(USAGE.to_string());
}
return Ok(Some(CliCommand::AgentCancel {
project_path: PathBuf::from(&args[1]),
agent_id: args[2].trim().to_string(),
run_id: args[3].trim().to_string(),
}));
}
if args.first().map(String::as_str) == Some("--agent-retry") {
const USAGE: &str = "用法:--agent-retry <本地项目绝对路径> <agentId> <runId> <nextRunId>";
if args.len() != 5 || args[1..].iter().any(|value| value.trim().is_empty()) {
return Err(USAGE.to_string());
}
return Ok(Some(CliCommand::AgentRetry {
project_path: PathBuf::from(&args[1]),
agent_id: args[2].trim().to_string(),
run_id: args[3].trim().to_string(),
next_run_id: args[4].trim().to_string(),
}));
}
if args.first().map(String::as_str) == Some("--agent-steer") {
const USAGE: &str = "用法:--agent-steer <本地项目绝对路径> <agentId> <sessionId> <runId> <steerId> --stdin";
if args.len() != 7 || args.last().map(String::as_str) != Some("--stdin") {
return Err(USAGE.to_string());
}
let values = &args[1..6];
if values.iter().any(|value| value.trim().is_empty()) {
return Err(USAGE.to_string());
}
return Ok(Some(CliCommand::AgentSteer {
project_path: PathBuf::from(&values[0]),
agent_id: values[1].trim().to_string(),
session_id: values[2].trim().to_string(),
run_id: values[3].trim().to_string(),
steer_id: values[4].trim().to_string(),
}));
}
if args.first().map(String::as_str) == Some("--agent-resume") {
if args.len() != 2 {
return Err("用法:--agent-resume <本地项目绝对路径>".to_string());
}
return Ok(Some(CliCommand::AgentResume {
project_path: PathBuf::from(&args[1]),
}));
}
if args.first().map(String::as_str) == Some("--plan-gdd-status") {
const USAGE: &str = "用法:--plan-gdd-status <本地项目绝对路径>";
if args.len() != 2 || args[1].trim().is_empty() {
return Err(USAGE.to_string());
}
return Ok(Some(CliCommand::PlanGddStatus {
project_path: PathBuf::from(&args[1]),
}));
}
if args.first().map(String::as_str) == Some("--plan-gdd-decide") {
const USAGE: &str = "用法:--plan-gdd-decide <本地项目绝对路径> <approve|revise|reject> [--response-id <gdd-response-…>] [--stdin]";
let mut rest = args[1..].to_vec();
let read_comment_from_stdin =
match rest.iter().filter(|arg| arg.as_str() == "--stdin").count() {
0 => false,
1 => {
let index = rest
.iter()
.position(|arg| arg == "--stdin")
.ok_or_else(|| USAGE.to_string())?;
rest.remove(index);
true
}
_ => return Err(USAGE.to_string()),
};
let response_id = take_cli_named_flag_value(&mut rest, "--response-id", USAGE)?;
if rest.len() != 2 || rest.iter().any(|value| value.trim().is_empty()) {
return Err(USAGE.to_string());
}
let action = rest[1].trim().to_string();
if !matches!(action.as_str(), "approve" | "revise" | "reject") {
return Err(USAGE.to_string());
}
// revise/reject 的 comment 是写入侧硬约束,缺了必然在落盘前失败。在解析期就拒绝,
// 错误才指得回命令行本身,而不是变成一条读起来像后端故障的存储错误。
if action != "approve" && !read_comment_from_stdin {
return Err(
"revise/reject 审批必须通过 --stdin 提供 11000 scalar 的修改意见".to_string(),
);
}
return Ok(Some(CliCommand::PlanGddDecide {
project_path: PathBuf::from(&rest[0]),
action,
response_id,
read_comment_from_stdin,
}));
}
if args.first().map(String::as_str) == Some("--agent-enqueue") {
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
};
if rest.len() < 4 {
return Err(
"用法:--agent-enqueue [--init] <本地项目绝对路径> <agentId> <runId> <任务>"
.to_string(),
);
}
let task = rest[3..].join(" ");
if task.trim().is_empty() {
return Err("Agent 任务不能为空".to_string());
}
return Ok(Some(CliCommand::AgentEnqueue {
project_path: PathBuf::from(&rest[0]),
agent_id: rest[1].trim().to_string(),
run_id: rest[2].trim().to_string(),
task: task.trim().to_string(),
initialize,
}));
}
if args.first().map(String::as_str) == Some("--agent-chat") {
let project_path = args.get(1).map(String::as_str).ok_or_else(|| {
"用法:--agent-chat <本地项目绝对路径> <agentId> <聊天内容>".to_string()
})?;
let agent_id = args.get(2).map(String::as_str).ok_or_else(|| {
"用法:--agent-chat <本地项目绝对路径> <agentId> <聊天内容>".to_string()
})?;
if args.len() < 4 {
return Err("用法:--agent-chat <本地项目绝对路径> <agentId> <聊天内容>".to_string());
}
let prompt = args[3..].join(" ");
let prompt = prompt.trim();
if prompt.is_empty() {
return Err("聊天内容不能为空".to_string());
}
return Ok(Some(CliCommand::AgentChat {
project_path: PathBuf::from(project_path),
agent_id: agent_id.trim().to_string(),
prompt: prompt.to_string(),
}));
}
if args.first().map(String::as_str) == Some("--direct-codex-chat") {
if args.len() < 3 {
return Err("用法:--direct-codex-chat <本地项目绝对路径> <聊天内容>".to_string());
}
let prompt = args[2..].join(" ");
if prompt.trim().is_empty() {
return Err("聊天内容不能为空".to_string());
}
return Ok(Some(CliCommand::DirectCodexChat {
project_path: PathBuf::from(&args[1]),
prompt: prompt.trim().to_string(),
}));
}
if args.first().map(String::as_str) == Some("--swarm-chat") {
const USAGE: &str = "用法:--swarm-chat [--init] [--autonomous-game-build] [--plan] <本地项目绝对路径> [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()),
};
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());
}
// 立项策划根 Run 只跑 standard 档(后端 reject_supervisor_plan_autonomous_profile
// 同样否决),且必须挂在总控上;这里先拦一道,免得建完项目才失败。
if plan
&& (autonomous_game_build
|| rest.get(1).is_some_and(|parent| {
parent.trim() != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
}))
{
return Err(
"--plan 仅允许 project-supervisor 的 standard 档,不能搭配 --autonomous-game-build"
.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: if plan {
AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE
} else {
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") {
rest.remove(index);
true
} else {
false
};
let project_path = rest.first().map(String::as_str).ok_or_else(|| {
"用法:--agent-task [--init] <本地项目绝对路径> <agentId> <任务>".to_string()
})?;
let agent_id = rest.get(1).map(String::as_str).ok_or_else(|| {
"用法:--agent-task [--init] <本地项目绝对路径> <agentId> <任务>".to_string()
})?;
if rest.len() < 3 {
return Err(
"用法:--agent-task [--init] <本地项目绝对路径> <agentId> <任务>".to_string(),
);
}
let task = rest[2..].join(" ");
let task = task.trim();
if task.is_empty() {
return Err("Agent 任务不能为空".to_string());
}
return Ok(Some(CliCommand::AgentTask {
project_path: PathBuf::from(project_path),
agent_id: agent_id.trim().to_string(),
task: task.to_string(),
initialize,
}));
}
if args.first().map(String::as_str) != Some("--agent-run") {
return Ok(None);
}
let mut rest = args[1..].to_vec();
let wait_for_enter = if let Some(index) = rest.iter().position(|arg| arg == "--no-wait") {
rest.remove(index);
false
} else {
true
};
let project_path = rest
.first()
.map(String::as_str)
.ok_or_else(|| "用法:--agent-run [--no-wait] <本地项目绝对路径> <创作需求>".to_string())?;
if rest.len() < 2 {
return Err("用法:--agent-run [--no-wait] <本地项目绝对路径> <创作需求>".to_string());
}
let prompt = rest[1..].join(" ");
let prompt = prompt.trim();
if prompt.is_empty() {
return Err("创作需求不能为空".to_string());
}
Ok(Some(CliCommand::AgentRun {
project_path: PathBuf::from(project_path),
prompt: prompt.to_string(),
wait_for_enter,
}))
}
fn strip_agent_runtime_cli_private_paths(value: &mut serde_json::Value) {
match value {
serde_json::Value::Array(values) => {
for value in values {
strip_agent_runtime_cli_private_paths(value);
}
}
serde_json::Value::Object(object) => {
object.remove("sessionPath");
object.remove("eventPath");
object.remove("taskPath");
for value in object.values_mut() {
strip_agent_runtime_cli_private_paths(value);
}
}
_ => {}
}
}
pub(crate) fn start_cli_agent_task_at(
project_path: &Path,
agent_id: &str,
task: &str,
run_id: &str,
) -> Result<AgentRuntimeResult, String> {
if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID {
start_game_creator_supervisor_background_task_for_session_at(
project_path,
None,
task,
run_id,
AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE,
AGENT_RUNTIME_RUN_PROFILE_STANDARD,
)
} else {
start_game_creator_agent_background_task_at(project_path, agent_id, task, run_id)
}
}
fn serialize_agent_runtime_cli_payload<T: serde::Serialize>(payload: &T) -> Result<String, String> {
let mut value = serde_json::to_value(payload)
.map_err(|error| format!("序列化 Agent Runtime 状态失败:{error}"))?;
strip_agent_runtime_cli_private_paths(&mut value);
serde_json::to_string(&value).map_err(|error| format!("序列化 Agent Runtime 状态失败:{error}"))
}
pub(crate) fn run_cli_command(command: CliCommand) -> Result<(), String> {
match command {
CliCommand::LlmStatus => {
let status = check_game_creator_llm_config_from_config();
for line in game_creator_llm_status_lines(&status) {
println!("{line}");
}
if status.configured {
Ok(())
} else {
Err("LLM 配置未就绪".to_string())
}
}
CliCommand::PreviewServe { project_path } => {
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.map_err(|error| format!("创建 CLI runtime 失败:{error}"))?;
let registry = game_creator_preview_registry();
let preview = start_local_game_preview_at(&project_path, &registry)?;
println!("preview.running");
println!("projectPath={}", project_path.display());
println!("previewUrl={}", preview.url);
let wait_result = std::io::stdout()
.flush()
.map_err(|error| format!("刷新本地预览 CLI 输出失败:{error}"))
.and_then(|()| {
runtime
.block_on(tokio::signal::ctrl_c())
.map_err(|error| format!("等待 Ctrl+C 失败:{error}"))
});
let stop_result = stop_local_game_preview_for_root(Some(&project_path), &registry)
.map(|_| ())
.map_err(|error| format!("停止本地预览失败:{error}"));
match (wait_result, stop_result) {
(Ok(()), Ok(())) => Ok(()),
(Err(error), Ok(())) | (Ok(()), Err(error)) => Err(error),
(Err(wait_error), Err(stop_error)) => {
Err(format!("{wait_error};同时{stop_error}"))
}
}
}
CliCommand::AgentChat {
project_path,
agent_id,
prompt,
} => {
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.map_err(|error| format!("创建 CLI runtime 失败:{error}"))?;
let reply = runtime.block_on(chat_with_game_creator_role_agent_at(
&project_path,
&agent_id,
&prompt,
))?;
println!("agent.chat.completed");
println!("projectPath={}", project_path.display());
println!("agentId={agent_id}");
println!("replyText={}", reply.reply_text);
Ok(())
}
CliCommand::DirectCodexChat {
project_path,
prompt,
} => {
let project_path = canonicalize_cli_path(&project_path, "本地项目路径", true)?;
if !project_path.join(".agent/manifest.json").is_file() {
let project_name = project_path
.file_name()
.and_then(|value| value.to_str())
.unwrap_or("Codex 直连项目");
init_local_game_project_at(
&project_path,
&format!("direct-codex-{}", unix_millis()),
project_name,
)?;
}
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.map_err(|error| format!("创建 CLI runtime 失败:{error}"))?;
let reply_result = runtime
.block_on(async { run_direct_game_creator_turn_at(&project_path, &prompt).await });
let shutdown_result = shutdown_game_creator_codex_app_servers();
let reply = reply_result?;
shutdown_result?;
println!("direct-codex.chat.completed");
println!("projectPath={}", project_path.display());
println!("replyText={reply}");
Ok(())
}
CliCommand::AgentTask {
project_path,
agent_id,
task,
initialize,
} => {
let project_path = canonicalize_cli_path(&project_path, "本地项目路径", initialize)?;
require_external_agent_runner_for_cli_runtime_write(&project_path)?;
if initialize && !project_path.join(".agent/manifest.json").is_file() {
let project_name = project_path
.file_name()
.and_then(|value| value.to_str())
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or("CLI Agent 项目");
init_local_game_project_at(
&project_path,
&format!("cli-agent-{}", unix_millis()),
project_name,
)?;
}
if !project_path.join(".agent/manifest.json").is_file() {
return Err("项目尚未初始化;请先在 App 中创建项目,或显式传入 --init".to_string());
}
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.map_err(|error| format!("创建 CLI runtime 失败:{error}"))?;
let run_id = format!("cli-{agent_id}-{}", unix_millis());
let terminal = runtime.block_on(async {
let started = start_cli_agent_task_at(&project_path, &agent_id, &task, &run_id)?;
let canonical_run_id = started.state.run_id.clone();
let deadline = std::time::Instant::now() + Duration::from_secs(600);
loop {
let current = read_game_creator_agent_runtime_at(&project_path, &agent_id)?;
if current.state.run_id == canonical_run_id
&& (current.state.status == "idle"
|| current.state.status == "failed"
|| current.state.status == "waiting-for-confirmation"
|| current.state.status == "waiting-for-user-input")
{
break Ok::<AgentRuntimeState, String>(current.state);
}
if std::time::Instant::now() >= deadline {
break Err(format!(
"等待单 Agent 任务超时:agentId={agent_id} runId={canonical_run_id}"
));
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
})?;
println!("agent.task.terminal");
println!("projectPath={}", project_path.display());
println!("agentId={agent_id}");
println!("runId={}", terminal.run_id);
println!("status={}", terminal.status);
println!("phase={}", terminal.phase);
if let Some(reply) = terminal.last_response.as_deref() {
println!("replyText={reply}");
}
if let Some(pending) = terminal.pending_tool_action.as_ref() {
println!("pendingActionId={}", pending.action_id);
println!("pendingTool={}", pending.tool);
if let Some(summary) = pending.input_summary.as_deref() {
println!("pendingInput={summary}");
}
}
if let Some(error) = terminal.error.as_deref() {
println!("error={error}");
}
if terminal.status == "idle" && terminal.phase == "completed" {
Ok(())
} 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())
} else {
Err(format!(
"单 Agent 任务未完成:{} / {}",
terminal.status, terminal.phase
))
}
}
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,
run_id,
task,
initialize,
} => {
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)?;
let runtime = start_game_creator_agent_background_task_at(
&project_path,
&agent_id,
&task,
&run_id,
)?;
println!("agent.enqueue.accepted");
println!("agentId={agent_id}");
println!("requestedRunId={run_id}");
println!(
"runtimeJson={}",
serialize_agent_runtime_cli_payload(&runtime)?
);
Ok(())
}
CliCommand::AgentRuntimeStatus {
project_path,
agent_id,
} => {
let runtime = read_game_creator_agent_runtime_at(&project_path, &agent_id)?;
println!(
"runtimeJson={}",
serialize_agent_runtime_cli_payload(&runtime)?
);
Ok(())
}
CliCommand::AgentContextCompact {
project_path,
agent_id,
session_id,
} => {
let project_path = canonicalize_cli_path(&project_path, "本地项目路径", false)?;
require_external_agent_runner_for_cli_runtime_write(&project_path)?;
let result = compact_external_agent_runner_context(
&project_path,
&agent_id,
session_id.as_deref(),
)?;
println!(
"contextCompactionJson={}",
serde_json::to_string(&result)
.map_err(|error| format!("序列化上下文压缩结果失败:{error}"))?
);
Ok(())
}
CliCommand::AgentGoalStatus {
project_path,
agent_id,
session_id,
} => {
let project_path = canonicalize_cli_path(&project_path, "本地项目路径", false)?;
let goal = read_game_creator_agent_goal(
project_path.display().to_string(),
agent_id,
session_id,
)?;
println!("agent.goal.status");
println!("goalJson={}", serialize_agent_runtime_cli_payload(&goal)?);
Ok(())
}
CliCommand::AgentGoalStart {
project_path,
agent_id,
session_id,
run_id,
initialize,
} => {
let project_path = canonicalize_cli_path(&project_path, "本地项目路径", initialize)?;
require_external_agent_runner_for_cli_runtime_write(&project_path)?;
if initialize && !project_path.join(".agent/manifest.json").is_file() {
let project_name = project_path
.file_name()
.and_then(|value| value.to_str())
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or("CLI Goal 项目");
init_local_game_project_at(
&project_path,
&format!("cli-goal-{}", unix_millis()),
project_name,
)?;
}
if !project_path.join(".agent/manifest.json").is_file() {
return Err("项目尚未初始化;请先在 App 中创建项目,或显式传入 --init".to_string());
}
let payload = read_cli_agent_goal_payload(&mut std::io::stdin().lock())?;
let result = start_game_creator_agent_goal(
project_path.display().to_string(),
agent_id,
Some(session_id),
payload.outcome,
payload.constraints,
payload.verification,
run_id,
)?;
println!("agent.goal.started");
println!(
"goalMutationJson={}",
serialize_agent_runtime_cli_payload(&result)?
);
Ok(())
}
CliCommand::AgentGoalEdit {
project_path,
agent_id,
session_id,
goal_id,
expected_revision,
} => {
let project_path = canonicalize_cli_path(&project_path, "本地项目路径", false)?;
require_external_agent_runner_for_cli_runtime_write(&project_path)?;
let payload = read_cli_agent_goal_payload(&mut std::io::stdin().lock())?;
let result = edit_game_creator_agent_goal(
project_path.display().to_string(),
agent_id,
session_id,
goal_id,
expected_revision,
payload.outcome,
payload.constraints,
payload.verification,
)?;
println!("agent.goal.edited");
println!(
"goalMutationJson={}",
serialize_agent_runtime_cli_payload(&result)?
);
Ok(())
}
CliCommand::AgentGoalPause {
project_path,
agent_id,
session_id,
goal_id,
expected_revision,
} => {
let project_path = canonicalize_cli_path(&project_path, "本地项目路径", false)?;
require_external_agent_runner_for_cli_runtime_write(&project_path)?;
let result = pause_game_creator_agent_goal(
project_path.display().to_string(),
agent_id,
session_id,
goal_id,
expected_revision,
)?;
println!("agent.goal.paused");
println!(
"goalMutationJson={}",
serialize_agent_runtime_cli_payload(&result)?
);
Ok(())
}
CliCommand::AgentGoalResume {
project_path,
agent_id,
session_id,
goal_id,
expected_revision,
} => {
let project_path = canonicalize_cli_path(&project_path, "本地项目路径", false)?;
require_external_agent_runner_for_cli_runtime_write(&project_path)?;
let result = resume_game_creator_agent_goal(
project_path.display().to_string(),
agent_id,
session_id,
goal_id,
expected_revision,
)?;
println!("agent.goal.resumed");
println!(
"goalMutationJson={}",
serialize_agent_runtime_cli_payload(&result)?
);
Ok(())
}
CliCommand::AgentGoalClear {
project_path,
agent_id,
session_id,
goal_id,
expected_revision,
} => {
let project_path = canonicalize_cli_path(&project_path, "本地项目路径", false)?;
require_external_agent_runner_for_cli_runtime_write(&project_path)?;
let result = clear_game_creator_agent_goal(
project_path.display().to_string(),
agent_id,
session_id,
goal_id,
expected_revision,
)?;
println!("agent.goal.cleared");
println!(
"goalMutationJson={}",
serialize_agent_runtime_cli_payload(&result)?
);
Ok(())
}
CliCommand::AgentConfirm {
project_path,
agent_id,
run_id,
action_id,
} => {
let project_path = canonicalize_cli_path(&project_path, "本地项目路径", false)?;
require_external_agent_runner_for_cli_runtime_write(&project_path)?;
let runtime = confirm_game_creator_agent_runtime_task_at(
&project_path,
&agent_id,
&run_id,
&action_id,
"真实 E2E CLI 精确确认",
)?;
println!("agent.confirm.accepted");
println!(
"runtimeJson={}",
serialize_agent_runtime_cli_payload(&runtime)?
);
Ok(())
}
CliCommand::AgentCancel {
project_path,
agent_id,
run_id,
} => {
let project_path = canonicalize_cli_path(&project_path, "本地项目路径", false)?;
// Cancellation must remain available when a busy older Runner blocks a build
// handover. It writes the durable cancel tombstone/state locally; retry still
// requires the current executable's Runner after the old run becomes idle.
require_external_agent_runner_configured_for_cli_runtime_write(&project_path)?;
let runtime =
cancel_game_creator_agent_runtime_task_at(&project_path, &agent_id, &run_id)?;
println!("agent.cancel.accepted");
println!(
"runtimeJson={}",
serialize_agent_runtime_cli_payload(&runtime)?
);
Ok(())
}
CliCommand::AgentRetry {
project_path,
agent_id,
run_id,
next_run_id,
} => {
let project_path = canonicalize_cli_path(&project_path, "本地项目路径", false)?;
require_external_agent_runner_for_cli_runtime_write(&project_path)?;
let runtime = retry_game_creator_agent_runtime_task_at(
&project_path,
&agent_id,
&run_id,
&next_run_id,
)?;
println!("agent.retry.accepted");
println!(
"runtimeJson={}",
serialize_agent_runtime_cli_payload(&runtime)?
);
Ok(())
}
CliCommand::AgentSteer {
project_path,
agent_id,
session_id,
run_id,
steer_id,
} => {
let project_path = canonicalize_cli_path(&project_path, "本地项目路径", false)?;
require_external_agent_runner_for_cli_runtime_write(&project_path)?;
let instruction = read_cli_agent_steer_instruction(&mut std::io::stdin().lock())?;
let mut result = steer_game_creator_agent_runtime_task_at(
&project_path,
&agent_id,
&session_id,
&run_id,
&steer_id,
&instruction,
"cli",
)?;
if !result.provider_interrupted {
result.provider_interrupted =
steer_external_agent_runner(&project_path, &agent_id, &run_id, &steer_id)?;
}
println!("agent.steer.accepted");
println!("agentId={agent_id}");
println!("sessionId={session_id}");
println!("runId={run_id}");
println!("steerId={steer_id}");
println!(
"steerJson={}",
serialize_agent_runtime_cli_payload(&result)?
);
Ok(())
}
CliCommand::AgentResume { project_path } => {
let project_path = canonicalize_cli_path(&project_path, "本地项目路径", false)?;
let completed_finalizations_cleaned =
cleanup_game_creator_agent_runtime_completed_finalizations_at(&project_path)?;
require_external_agent_runner_for_cli_runtime_write(&project_path)?;
let runtimes = resume_game_creator_agent_background_tasks_at(&project_path)?;
println!("agent.resume.accepted");
println!("completedFinalizationsCleaned={completed_finalizations_cleaned}");
println!(
"runtimesJson={}",
serialize_agent_runtime_cli_payload(&runtimes)?
);
Ok(())
}
CliCommand::PlanGddStatus { project_path } => {
let project_path = canonicalize_cli_path(&project_path, "本地项目路径", false)?;
let state =
hydrate_game_creator_plan_gdd_state_for_path(&project_path.display().to_string())?;
println!("plan.gdd.status");
println!(
"planGddStateJson={}",
serialize_agent_runtime_cli_payload(&state)?
);
Ok(())
}
CliCommand::PlanGddDecide {
project_path,
action,
response_id,
read_comment_from_stdin,
} => {
let project_path = canonicalize_cli_path(&project_path, "本地项目路径", false)?;
require_external_agent_runner_for_cli_runtime_write(&project_path)?;
let comment = if read_comment_from_stdin {
Some(read_cli_plan_gdd_approval_comment(
&mut std::io::stdin().lock(),
)?)
} else {
None
};
let project_path_value = project_path.display().to_string();
// 审批卡的 identity 只有投影这一个权威来源。CLI 不接受手工传 gddId/fingerprint
// 那样每个调用方都要自己拼一遍身份,拼错的后果是 PLAN_STALE_APPROVAL
// 而不是一条能读懂的用法错误。
let state = hydrate_game_creator_plan_gdd_state_for_path(&project_path_value)?;
let pending = state
.pending_approval
.ok_or_else(|| "当前没有待决定的 Fast GDD 审批".to_string())?;
// 每次调用换新 responseId 是安全方向:重复键的最坏后果是 replayed 降级成
// already-decided(两者都成功),而复用键改 action 会撞「同 responseId 的
// 审批意图不一致」硬错误。要复放同一次决定时才显式传 --response-id。
let response_id = response_id
.unwrap_or_else(|| format!("gdd-response-{}", uuid::Uuid::new_v4().hyphenated()));
let result = decide_game_creator_plan_gdd(
project_path_value,
pending.gdd_ref.gdd_id.clone(),
pending.gdd_ref.version,
pending.gdd_ref.fingerprint.clone(),
pending.pending_action_id.clone(),
pending.approval_request_id.clone(),
response_id,
action,
comment,
)?;
println!("plan.gdd.decided");
println!(
"planGddDecisionJson={}",
serialize_agent_runtime_cli_payload(&result)?
);
Ok(())
}
CliCommand::RunnerStatus => {
println!(
"runnerJson={}",
serde_json::to_string(&read_external_agent_runner_status())
.map_err(|error| format!("序列化 Agent Runner 状态失败:{error}"))?
);
Ok(())
}
CliCommand::RunnerShutdownIfIdle => {
let stopped = shutdown_external_agent_runner_if_idle()?;
println!("runner.stopped={stopped}");
Ok(())
}
CliCommand::AgentRun {
project_path,
prompt,
wait_for_enter,
} => {
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.map_err(|error| format!("创建 CLI runtime 失败:{error}"))?;
let result =
runtime.block_on(generate_local_game_draft_at(&project_path, &prompt, None))?;
let (preview, stop) = start_local_game_preview_for_project(&project_path)?;
record_preview_state(
&project_path,
GameCreationAppPreviewStatus::Running,
Some(preview.url.clone()),
Some(preview.port),
)?;
append_preview_log(&project_path, "running", Some(&preview.url))?;
append_preview_start_trace_step(&project_path, &preview)?;
println!("agent.run.completed");
println!("projectPath={}", result.project_path);
println!("gameIndexPath={}", result.game_index_path);
println!("designPath={}", result.design_path);
println!(
"tracePath={}",
project_path.join(".agent/run.latest.json").display()
);
println!("previewUrl={}", preview.url);
if wait_for_enter {
println!("按 Enter 停止本地预览。");
let mut line = String::new();
let _ = std::io::stdin().read_line(&mut line);
}
let _ = stop.send(());
let _ = record_preview_state(
&project_path,
GameCreationAppPreviewStatus::Stopped,
None,
None,
);
let _ = append_preview_log(&project_path, "stopped", None);
let _ = append_preview_stop_trace_step(&project_path);
Ok(())
}
}
}
fn initialize_cli_agent_project(project_path: &Path, initialize: bool) -> Result<(), String> {
if initialize && !project_path.join(".agent/manifest.json").is_file() {
let project_name = project_path
.file_name()
.and_then(|value| value.to_str())
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or("CLI Agent 项目");
init_local_game_project_at(
project_path,
&format!("cli-agent-{}", unix_millis()),
project_name,
)?;
}
if !project_path.join(".agent/manifest.json").is_file() {
return Err("项目尚未初始化;请先在 App 中创建项目,或显式传入 --init".to_string());
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
#[test]
fn runtime_cli_payload_omits_private_storage_paths_recursively() {
let payload = serde_json::json!({
"sessionPath": "/tmp/private/session.json",
"runtime": {
"eventPath": "/tmp/private/events.jsonl",
"state": {
"sessionId": "session-7",
"runId": "run-9"
}
},
"runtimes": [{
"taskPath": "/tmp/private/tasks.jsonl",
"state": { "status": "running" }
}]
});
let encoded = serialize_agent_runtime_cli_payload(&payload)
.expect("serialize redacted Runtime CLI payload");
assert!(!encoded.contains("/tmp/private"));
let parsed = serde_json::from_str::<serde_json::Value>(&encoded)
.expect("parse redacted Runtime CLI payload");
assert!(parsed.get("sessionPath").is_none());
assert!(parsed["runtime"].get("eventPath").is_none());
assert!(parsed["runtimes"][0].get("taskPath").is_none());
assert_eq!(parsed["runtime"]["state"]["sessionId"], "session-7");
assert_eq!(parsed["runtimes"][0]["state"]["status"], "running");
}
#[test]
fn parses_preview_serve_without_runner_or_config() {
let project_path = std::env::current_dir().expect("current directory");
let mut command = parse_cli_command(&[
"--preview-serve".to_string(),
project_path.display().to_string(),
])
.expect("parse preview serve")
.expect("preview serve command");
assert_eq!(
command,
CliCommand::PreviewServe {
project_path: project_path.clone(),
}
);
assert!(!command.requires_external_agent_runner());
assert!(!command.requires_started_external_agent_runner());
assert!(!command.is_read_only_status());
assert_eq!(
prepare_cli_command_paths(&mut command, None)
.expect("prepare preview serve without config dir"),
None
);
assert_eq!(
command,
CliCommand::PreviewServe {
project_path: fs::canonicalize(project_path).expect("canonical project path"),
}
);
}
#[test]
fn preview_serve_requires_exactly_one_non_empty_project_path() {
for args in [
vec!["--preview-serve"],
vec!["--preview-serve", " "],
vec!["--preview-serve", "/tmp/game-project", "/tmp/extra"],
] {
let args = args.into_iter().map(str::to_string).collect::<Vec<_>>();
assert_eq!(
parse_cli_command(&args),
Err(PREVIEW_SERVE_USAGE.to_string()),
"unexpected parse result for {args:?}"
);
}
}
#[test]
fn parses_agent_steer_with_stdin_only_contract() {
let command = parse_cli_command(&[
"--agent-steer".to_string(),
"/tmp/game-project".to_string(),
"code-prototype".to_string(),
"session-7".to_string(),
"run-9".to_string(),
"steer-11".to_string(),
"--stdin".to_string(),
])
.expect("parse agent steer")
.expect("agent steer command");
assert_eq!(
command,
CliCommand::AgentSteer {
project_path: PathBuf::from("/tmp/game-project"),
agent_id: "code-prototype".to_string(),
session_id: "session-7".to_string(),
run_id: "run-9".to_string(),
steer_id: "steer-11".to_string(),
}
);
assert!(command.requires_external_agent_runner());
}
#[test]
fn parses_agent_cancel_and_retry_with_explicit_run_identity() {
let project_path = PathBuf::from("/tmp/game-project");
let cancel = parse_cli_command(&[
"--agent-cancel".to_string(),
project_path.display().to_string(),
" project-supervisor ".to_string(),
" run-9 ".to_string(),
])
.expect("parse agent cancel")
.expect("agent cancel command");
assert_eq!(
cancel,
CliCommand::AgentCancel {
project_path: project_path.clone(),
agent_id: "project-supervisor".to_string(),
run_id: "run-9".to_string(),
}
);
assert!(cancel.requires_external_agent_runner());
assert!(!cancel.requires_started_external_agent_runner());
assert!(!cancel.is_read_only_status());
let retry = parse_cli_command(&[
"--agent-retry".to_string(),
project_path.display().to_string(),
" project-supervisor ".to_string(),
" run-9 ".to_string(),
" run-10 ".to_string(),
])
.expect("parse agent retry")
.expect("agent retry command");
assert_eq!(
retry,
CliCommand::AgentRetry {
project_path,
agent_id: "project-supervisor".to_string(),
run_id: "run-9".to_string(),
next_run_id: "run-10".to_string(),
}
);
assert!(retry.requires_external_agent_runner());
assert!(retry.requires_started_external_agent_runner());
assert!(!retry.is_read_only_status());
}
#[test]
fn agent_cancel_and_retry_reject_missing_or_blank_identity() {
for args in [
vec!["--agent-cancel"],
vec![
"--agent-cancel",
"/tmp/game-project",
"project-supervisor",
" ",
],
vec![
"--agent-retry",
"/tmp/game-project",
"project-supervisor",
"run-9",
],
vec![
"--agent-retry",
"/tmp/game-project",
"project-supervisor",
"run-9",
"\t",
],
] {
let args = args.into_iter().map(str::to_string).collect::<Vec<_>>();
assert!(
parse_cli_command(&args).is_err(),
"args should fail: {args:?}"
);
}
}
#[test]
fn agent_steer_rejects_missing_params_and_argv_instruction() {
assert!(parse_cli_command(&["--agent-steer".to_string()]).is_err());
assert!(parse_cli_command(&[
"--agent-steer".to_string(),
"/tmp/game-project".to_string(),
"code-prototype".to_string(),
"session-7".to_string(),
"run-9".to_string(),
"steer-11".to_string(),
])
.is_err());
assert!(parse_cli_command(&[
"--agent-steer".to_string(),
"/tmp/game-project".to_string(),
"code-prototype".to_string(),
"session-7".to_string(),
"run-9".to_string(),
"steer-11".to_string(),
"正文不能出现在 argv".to_string(),
])
.is_err());
}
#[test]
fn reads_agent_steer_instruction_only_from_stdin() {
let mut stdin = Cursor::new(" 先停下当前方案,改用键盘操作。\n");
assert_eq!(
read_cli_agent_steer_instruction(&mut stdin).as_deref(),
Ok("先停下当前方案,改用键盘操作。")
);
let mut empty = Cursor::new(" \r\n\t");
assert!(read_cli_agent_steer_instruction(&mut empty).is_err());
let mut exact_with_newline = Cursor::new(format!("{}\n", "x".repeat(4 * 1024)));
assert_eq!(
read_cli_agent_steer_instruction(&mut exact_with_newline)
.expect("read exact-size instruction")
.len(),
4 * 1024
);
let mut oversized = Cursor::new(vec![b'x'; 4 * 1024 + 1]);
assert!(read_cli_agent_steer_instruction(&mut oversized).is_err());
}
#[test]
fn agent_steer_requires_external_config_dir() {
let mut command = CliCommand::AgentSteer {
project_path: std::env::current_dir().expect("current directory"),
agent_id: "code-prototype".to_string(),
session_id: "session-7".to_string(),
run_id: "run-9".to_string(),
steer_id: "steer-11".to_string(),
};
let error = prepare_cli_command_paths(&mut command, None)
.expect_err("agent steer must require config dir");
assert!(error.contains("--config-dir"));
}
#[test]
fn parses_all_non_interactive_goal_commands_with_explicit_cas_identity() {
let project_path = PathBuf::from("/tmp/game-project");
let status = parse_cli_command(&[
"--agent-goal-status".to_string(),
project_path.display().to_string(),
"code-prototype".to_string(),
"session-7".to_string(),
])
.expect("parse goal status")
.expect("goal status command");
assert_eq!(
status,
CliCommand::AgentGoalStatus {
project_path: project_path.clone(),
agent_id: "code-prototype".to_string(),
session_id: "session-7".to_string(),
}
);
assert!(status.is_read_only_status());
assert!(!status.requires_external_agent_runner());
let start = parse_cli_command(&[
"--agent-goal-start".to_string(),
project_path.display().to_string(),
"code-prototype".to_string(),
"session-7".to_string(),
"goal-run-9".to_string(),
"--stdin".to_string(),
])
.expect("parse goal start")
.expect("goal start command");
assert_eq!(
start,
CliCommand::AgentGoalStart {
project_path: project_path.clone(),
agent_id: "code-prototype".to_string(),
session_id: "session-7".to_string(),
run_id: "goal-run-9".to_string(),
initialize: false,
}
);
let mut initialized_start = parse_cli_command(&[
"--agent-goal-start".to_string(),
"--init".to_string(),
project_path.display().to_string(),
"code-prototype".to_string(),
"session-8".to_string(),
"goal-run-10".to_string(),
"--stdin".to_string(),
])
.expect("parse initialized goal start")
.expect("initialized goal start command");
assert_eq!(
initialized_start,
CliCommand::AgentGoalStart {
project_path: project_path.clone(),
agent_id: "code-prototype".to_string(),
session_id: "session-8".to_string(),
run_id: "goal-run-10".to_string(),
initialize: true,
}
);
let (_, initialize) = initialized_start
.project_path_mut()
.expect("initialized Goal start project path");
assert!(initialize);
let edit = parse_cli_command(&[
"--agent-goal-edit".to_string(),
project_path.display().to_string(),
"code-prototype".to_string(),
"session-7".to_string(),
"goal-11".to_string(),
"3".to_string(),
"--stdin".to_string(),
])
.expect("parse goal edit")
.expect("goal edit command");
assert_eq!(
edit,
CliCommand::AgentGoalEdit {
project_path: project_path.clone(),
agent_id: "code-prototype".to_string(),
session_id: "session-7".to_string(),
goal_id: "goal-11".to_string(),
expected_revision: 3,
}
);
let cas_commands = [
("--agent-goal-pause", "pause"),
("--agent-goal-resume", "resume"),
("--agent-goal-clear", "clear"),
];
for (flag, expected) in cas_commands {
let command = parse_cli_command(&[
flag.to_string(),
project_path.display().to_string(),
"code-prototype".to_string(),
"session-7".to_string(),
"goal-11".to_string(),
"3".to_string(),
])
.expect("parse Goal CAS command")
.expect("Goal CAS command");
match (expected, &command) {
(
"pause",
CliCommand::AgentGoalPause {
expected_revision, ..
},
)
| (
"resume",
CliCommand::AgentGoalResume {
expected_revision, ..
},
)
| (
"clear",
CliCommand::AgentGoalClear {
expected_revision, ..
},
) => {
assert_eq!(*expected_revision, 3)
}
_ => panic!("unexpected Goal CAS command: {command:?}"),
}
assert!(command.requires_external_agent_runner());
assert!(!command.is_read_only_status());
}
assert!(start.requires_external_agent_runner());
assert!(edit.requires_external_agent_runner());
}
#[test]
fn goal_cli_rejects_argv_bodies_bad_revisions_and_keeps_global_resume() {
assert!(parse_cli_command(&[
"--agent-goal-start".to_string(),
"/tmp/game-project".to_string(),
"code-prototype".to_string(),
"session-7".to_string(),
"goal-run-9".to_string(),
"正文不能出现在 argv".to_string(),
])
.is_err());
assert!(parse_cli_command(&[
"--agent-goal-edit".to_string(),
"/tmp/game-project".to_string(),
"code-prototype".to_string(),
"session-7".to_string(),
"goal-11".to_string(),
"0".to_string(),
"--stdin".to_string(),
])
.is_err());
assert!(parse_cli_command(&[
"--agent-goal-pause".to_string(),
"/tmp/game-project".to_string(),
"code-prototype".to_string(),
"session-7".to_string(),
"goal-11".to_string(),
"not-a-revision".to_string(),
])
.is_err());
assert_eq!(
parse_cli_command(&[
"--agent-resume".to_string(),
"/tmp/game-project".to_string(),
])
.expect("parse global agent resume")
.expect("global agent resume command"),
CliCommand::AgentResume {
project_path: PathBuf::from("/tmp/game-project"),
}
);
}
#[test]
fn parses_plan_gdd_headless_approval_entries() {
assert_eq!(
parse_cli_command(&[
"--plan-gdd-status".to_string(),
"/tmp/game-project".to_string(),
])
.expect("parse plan gdd status")
.expect("plan gdd status command"),
CliCommand::PlanGddStatus {
project_path: PathBuf::from("/tmp/game-project"),
}
);
assert_eq!(
parse_cli_command(&[
"--plan-gdd-decide".to_string(),
"/tmp/game-project".to_string(),
" approve ".to_string(),
])
.expect("parse plan gdd approve")
.expect("plan gdd approve command"),
CliCommand::PlanGddDecide {
project_path: PathBuf::from("/tmp/game-project"),
action: "approve".to_string(),
response_id: None,
read_comment_from_stdin: false,
}
);
assert_eq!(
parse_cli_command(&[
"--plan-gdd-decide".to_string(),
"/tmp/game-project".to_string(),
"revise".to_string(),
"--response-id".to_string(),
" gdd-response-1b4e28ba-2fa1-11d2-883f-0016d3cca427 ".to_string(),
"--stdin".to_string(),
])
.expect("parse plan gdd revise")
.expect("plan gdd revise command"),
CliCommand::PlanGddDecide {
project_path: PathBuf::from("/tmp/game-project"),
action: "revise".to_string(),
response_id: Some("gdd-response-1b4e28ba-2fa1-11d2-883f-0016d3cca427".to_string()),
read_comment_from_stdin: true,
}
);
// revise/reject 没有 --stdin 时必须在解析期就失败,否则错误会伪装成写入侧故障。
for action in ["revise", "reject"] {
assert!(parse_cli_command(&[
"--plan-gdd-decide".to_string(),
"/tmp/game-project".to_string(),
action.to_string(),
])
.is_err());
}
for args in [
vec!["--plan-gdd-decide"],
vec!["--plan-gdd-decide", "/tmp/game-project"],
vec!["--plan-gdd-decide", "/tmp/game-project", "confirm"],
vec!["--plan-gdd-decide", "/tmp/game-project", "approve", "extra"],
vec![
"--plan-gdd-decide",
"/tmp/game-project",
"approve",
"--response-id",
],
vec!["--plan-gdd-status", "/tmp/game-project", "extra"],
] {
assert!(
parse_cli_command(&args.into_iter().map(str::to_string).collect::<Vec<_>>())
.is_err()
);
}
}
#[test]
fn plan_gdd_decision_requires_started_external_runner_but_status_does_not() {
let decide = CliCommand::PlanGddDecide {
project_path: PathBuf::from("/tmp/game-project"),
action: "approve".to_string(),
response_id: None,
read_comment_from_stdin: false,
};
assert!(decide.requires_external_agent_runner());
assert!(decide.requires_started_external_agent_runner());
assert!(!decide.is_read_only_status());
let status = CliCommand::PlanGddStatus {
project_path: PathBuf::from("/tmp/game-project"),
};
assert!(!status.requires_external_agent_runner());
assert!(status.is_read_only_status());
}
#[test]
fn reads_plan_gdd_approval_comment_from_stdin() {
assert_eq!(
read_cli_plan_gdd_approval_comment(&mut Cursor::new(" 把核心循环写具体 \n"))
.expect("read approval comment"),
"把核心循环写具体"
);
assert!(read_cli_plan_gdd_approval_comment(&mut Cursor::new(" \n")).is_err());
assert!(
read_cli_plan_gdd_approval_comment(&mut Cursor::new("a".repeat(9 * 1024))).is_err()
);
}
#[test]
fn reads_strict_structured_goal_payload_from_stdin() {
let mut stdin = Cursor::new(
r#"{
"outcome": " 完成首个可玩版本 ",
"constraints": ["不新增平行 Runtime"],
"verification": ["键盘与触屏均可完成一局"]
}"#,
);
assert_eq!(
read_cli_agent_goal_payload(&mut stdin).expect("read Goal JSON"),
CliAgentGoalPayload {
outcome: "完成首个可玩版本".to_string(),
constraints: vec!["不新增平行 Runtime".to_string()],
verification: vec!["键盘与触屏均可完成一局".to_string()],
}
);
for invalid in [
r#"{"outcome":"目标","constraints":[]}"#,
r#"{"outcome":"目标","constraints":[],"verification":[],"extra":true}"#,
r#"{"outcome":" ","constraints":[],"verification":[]}"#,
"not-json",
" ",
] {
let mut stdin = Cursor::new(invalid);
assert!(
read_cli_agent_goal_payload(&mut stdin).is_err(),
"invalid payload should fail: {invalid}"
);
}
let mut oversized = Cursor::new(vec![b'x'; 64 * 1024 + 1]);
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()])
.expect("parse idle Runner shutdown")
.expect("idle Runner shutdown command");
assert_eq!(command, CliCommand::RunnerShutdownIfIdle);
assert!(command.requires_external_agent_runner());
assert!(!command.requires_started_external_agent_runner());
let error = prepare_cli_command_paths(&mut command, None)
.expect_err("idle Runner shutdown must require config dir");
assert!(error.contains("--config-dir"));
assert!(parse_cli_command(
&["--runner-shutdown-if-idle".to_string(), "extra".to_string(),]
)
.is_err());
}
#[test]
fn parses_agent_context_compaction_with_optional_session() {
assert_eq!(
parse_cli_command(&[
"--agent-context-compact".to_string(),
"/tmp/game-project".to_string(),
"code-prototype".to_string(),
"agent-session-code-prototype".to_string(),
])
.expect("parse context compaction"),
Some(CliCommand::AgentContextCompact {
project_path: PathBuf::from("/tmp/game-project"),
agent_id: "code-prototype".to_string(),
session_id: Some("agent-session-code-prototype".to_string()),
})
);
assert_eq!(
parse_cli_command(&[
"--agent-context-compact".to_string(),
"/tmp/game-project".to_string(),
"code-prototype".to_string(),
])
.expect("parse active-session compaction"),
Some(CliCommand::AgentContextCompact {
project_path: PathBuf::from("/tmp/game-project"),
agent_id: "code-prototype".to_string(),
session_id: None,
})
);
assert!(parse_cli_command(&[
"--agent-context-compact".to_string(),
"/tmp/game-project".to_string(),
])
.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_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(),
"/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());
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());
}
}