CLI 与夹具退役:DirectProject 只剩入队一个命令入口

- cli.rs 删 CliCommand::DirectCodexChat 变体、project_path_mut 分支、--direct-codex-chat 解析与派发
- direct_runtime/mod.rs 删 run_direct_game_creator_turn_at 与 run_direct_game_creator_turn_at_with_creation_type
- 删 scripts/direct-execution-production-fixture.mjs(CLI 唯一消费者)
- user_input.rs 模块注释改成"唯一的命令入口",不再承诺 CLI 分工
- 顺带删掉只剩测试在用的 direct_turn_error_boundary_text,判据只剩 direct_turn_enqueue_failure 一处
- 三条命令边界测试改打 direct_turn_enqueue_failure(...).message,覆盖不变
- 文档:09-22 里程碑把 --direct-codex-chat 从"范围外(保留)"改成退役项,两份 Direct 技术方案的 CLI 承诺删掉,
  09-23 ADR 的"CLI 保持 await"标为口径反转,decision-log 追加口径修正,实施计划标注第 4 步落地
This commit is contained in:
2026-09-30 10:25:38 +08:00
parent 0534e9c678
commit 25f3e41623
10 changed files with 33 additions and 1097 deletions
File diff suppressed because it is too large Load Diff
@@ -2148,24 +2148,10 @@ pub(crate) fn record_direct_codex_failure(
public_text
}
/// 命令边界的错误文本:可留痕的调用级入队失败在这里补一份运行错误诊断(文案里不带诊断引用),
/// 其余只输出 [`DirectTurnError`] 的 `Display`。
///
/// 分层改成 typed 之前,这几条"宿主 / 环境事实"是在回合失败通道里被写进诊断的;分层之后它们不再
/// 进那条通道,留痕在这里补回来。GUI 命令与 CLI 边界共用这一份,禁止在各自边界再写一套判据;
/// 回合级失败已在上游写过诊断,这里直接放行。
pub(crate) fn direct_turn_error_boundary_text(
root: &Path,
client_turn_id: Option<&str>,
failure: DirectTurnError,
) -> String {
direct_turn_enqueue_failure(root, client_turn_id, failure).message
}
/// 入队失败边界:可留痕的调用级失败(宿主 / 环境事实)在这里补一份运行错误诊断,然后连同**结构化
/// 变体**一起交给前端;其余只输出 [`DirectTurnError`] 的 `Display`。
///
/// GUI 命令与 CLI 边界共用这一份判据(CLI 只要文本,走上面的 `..._text`),禁止在各自边界再写一套。
/// 命令边界只有这一份判据,禁止在各处再写一套。
pub(crate) fn direct_turn_enqueue_failure(
root: &Path,
client_turn_id: Option<&str>,
@@ -4432,39 +4418,6 @@ pub(crate) fn check_direct_turn_preconditions(
.map_err(|detail| DirectTurnError::InputRejected { detail })?;
Ok(())
}
pub(crate) async fn run_direct_game_creator_turn_at(
root: &Path,
prompt: &str,
) -> Result<String, DirectTurnError> {
// The CLI entry point does not receive the GUI's clientTurnId. Still arm
// one invocation identity so an otherwise optional AGC generation tool
// cannot fail merely because the request came through the CLI. This is
// local execution identity only; it does not create a Runtime task or DAG.
let invocation_id = format!("direct-cli-{}", unix_millis());
let _invocation = DirectTaonierActiveInvocationGuard::enter(root, &invocation_id)?;
run_direct_game_creator_turn_at_with_creation_type(root, prompt, None).await
}
pub(crate) async fn run_direct_game_creator_turn_at_with_creation_type(
root: &Path,
prompt: &str,
creation_type: Option<&str>,
) -> Result<String, DirectTurnError> {
// CLI 入口没有"入队 / 放行"这两步(它 await 整轮,要那段回复文本),前置条件在这里自己过一遍;
// GUI 命令在入队半里过同一份判据。
check_direct_turn_preconditions(root, prompt, creation_type)?;
run_direct_game_creator_turn_at_with_creation_type_and_emitter(
root,
prompt,
creation_type,
None,
None,
None,
None,
)
.await
}
pub(crate) async fn run_direct_game_creator_turn_at_with_creation_type_and_emitter(
root: &Path,
prompt: &str,
@@ -7739,13 +7692,14 @@ mod tests {
let root = parent.path().join("project");
init_local_game_project_at(&root, "direct-diagnostic", "直连诊断").expect("init project");
let text = direct_turn_error_boundary_text(
let text = direct_turn_enqueue_failure(
&root,
Some("direct-codex:turn-1:user"),
DirectTurnError::EnvironmentNotReady {
detail: "Codex app-server 启动失败:找不到可执行文件".into(),
},
);
)
.message;
assert!(text.contains("direct-codex-failure:v2"), "{text}");
assert!(!text.contains("详情:"), "{text}");
@@ -7765,7 +7719,7 @@ mod tests {
let root = parent.path().join("project");
init_local_game_project_at(&root, "direct-diagnostic", "直连诊断").expect("init project");
let text = direct_turn_error_boundary_text(&root, None, DirectTurnError::ContentEmpty);
let text = direct_turn_enqueue_failure(&root, None, DirectTurnError::ContentEmpty).message;
assert_eq!(text, "聊天内容不能为空");
assert!(!root.join(".agent/runtime/errors").exists());
@@ -7782,11 +7736,12 @@ mod tests {
DirectTurnError::turn_failed(DirectCodexFailureStage::CodeGeneration, "模型失败");
let recorded = record_direct_codex_failure(&root, &failure, None);
// 上游把返回串挂进 `TurnFailed.detail`,边界再见到它时只做 `Display`,不再写诊断。
let text = direct_turn_error_boundary_text(
let text = direct_turn_enqueue_failure(
&root,
None,
DirectTurnError::turn_failed(DirectCodexFailureStage::CodeGeneration, recorded.clone()),
);
)
.message;
assert_eq!(text, recorded);
let entries = std::fs::read_dir(root.join(".agent/runtime/errors"))
@@ -39,9 +39,8 @@ pub(crate) fn normalize_direct_client_turn_id(
/// 入队失败不写用户条目、不产生任何事件。可留痕的调用级失败(宿主 / 环境事实)仍在边界补一份运行
/// 错误诊断,返回串不带诊断引用。
///
/// 与 CLI 的分工:CLI 入口(`cli.rs` 的 `direct-codex.chat`)**保持 await**——它要把那段回复文本
/// 打到终端上,没有事件订阅可用;两个入口共用 [`direct_turn_error_boundary_text`] /
/// [`direct_turn_enqueue_failure`],不要再各写一套判据。
/// 它是 DirectProject 唯一的命令入口:终端入口 `--direct-codex-chat`(它要保持 await 才能把回复打到
/// 终端上)已经退役,不要再为"手工跑一轮"新增第二条直接起回合的路径。
///
/// 设计见 `docs/adr/【ADR】DirectProject命令入队化与待发消息队列归宿主-2026-09-24.md`。
#[tauri::command]
@@ -14,10 +14,6 @@ pub(crate) enum CliCommand {
agent_id: String,
prompt: String,
},
DirectCodexChat {
project_path: PathBuf,
prompt: String,
},
AgentTask {
project_path: PathBuf,
agent_id: String,
@@ -178,7 +174,6 @@ impl CliCommand {
..
} => Some((project_path, *initialize)),
Self::AgentChat { project_path, .. }
| Self::DirectCodexChat { project_path, .. }
| Self::AgentRuntimeStatus { project_path, .. }
| Self::AgentContextCompact { project_path, .. }
| Self::AgentGoalStatus { project_path, .. }
@@ -699,19 +694,6 @@ pub(crate) fn parse_cli_command(args: &[String]) -> Result<Option<CliCommand>, S
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("--agent-task") {
let mut rest = args[1..].to_vec();
let initialize = if let Some(index) = rest.iter().position(|arg| arg == "--init") {
@@ -896,44 +878,6 @@ pub(crate) fn run_cli_command(command: CliCommand) -> Result<(), String> {
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
// CLI 也是命令边界:typed 错误在这里序列化成一行给终端看的文本;可留痕的
// 调用级拒绝(宿主 / 环境事实)与 GUI 走同一份投影(同一份 `Display` 文案,
// 不另加 `详情:` 引用),差别只在 CLI 自己 await 整轮、拿到回复文本。
.map_err(|failure| {
direct_turn_error_boundary_text(&project_path, None, failure)
})
});
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,