use super::ToolAdapter;
use crate::core::state::ForgeState;
use crate::core::task::{AgentType, Task};
use std::path::Path;
use std::process::Command;
pub struct CodexAdapter;
impl CodexAdapter {
pub(crate) fn task_prompt(task: &Task, task_type: &str) -> String {
let criteria = if task.acceptance_criteria.is_empty() {
String::new()
} else {
format!(
"Acceptance criteria:\n{}\n\n",
task.acceptance_criteria
.iter()
.map(|c| format!("- {c}"))
.collect::<Vec<_>>()
.join("\n")
)
};
match task_type {
"verify" => format!(
"You are working on task {id}: {title}\n\n\
This is a VERIFICATION task. You must ACTIVELY TEST the code, not just read it.\n\n\
Description: {desc}\n\n\
{criteria}\
Required verification steps:\n\
1. Run the project's test suite (e.g. `npm test`, `pytest`, `cargo test`)\n\
2. Run the type checker (e.g. `tsc --noEmit`, `mypy`, `cargo clippy`)\n\
3. Check that all new/modified files have corresponding test files\n\
4. Verify test mocks match production code shapes (interceptors, constructors, etc.)\n\
5. Run the linter (e.g. `eslint`, `ruff`, `cargo fmt --check`)\n\
6. If this task touches auth/routing/middleware, also run E2E tests\n\n\
Report your findings: what passed, what failed, what's missing.\n\
If tests fail or coverage is missing, report the specific failures.",
id = task.id,
title = task.title,
desc = task.description,
),
"review" => format!(
"You are working on task {id}: {title}\n\n\
This is a REVIEW task. Analyze the code without making changes.\n\n\
Description: {desc}\n\n\
{criteria}\
Do NOT modify files — this is a read-only review.\n\
When done, summarize your findings.",
id = task.id,
title = task.title,
desc = task.description,
),
_ => format!(
"Complete task {id}: {title}\n\nDescription: {desc}\n\n\
{criteria}\
IMPORTANT: Before marking this task complete:\n\
- Write unit tests for any new files you create\n\
- Run the existing test suite to verify nothing breaks\n\
- Run the type checker if the project has one\n\
- If you modify a file that has a corresponding .test file, update the test mocks\n\n\
When done, summarize what you did.",
id = task.id,
title = task.title,
desc = task.description,
),
}
}
}
impl ToolAdapter for CodexAdapter {
fn name(&self) -> &str {
"codex"
}
fn parse_ctx_pct(&self, statusline: &str) -> Option<u8> {
for line in statusline.lines() {
let lower = line.to_lowercase();
let has_model = lower.contains("gpt-");
let has_separator = line.contains('\u{00b7}'); let has_context = lower.contains("context") && lower.contains("% left");
let has_weekly = lower.contains("weekly");
if has_model
&& has_separator
&& has_context
&& has_weekly
&& let Some(left) = super::pct_after_word_percent(&lower, "context")
{
return Some(100 - left);
}
}
None
}
fn render_config(
&self,
state: &ForgeState,
tasks: &[Task],
project_root: &Path,
) -> anyhow::Result<()> {
let assigned: Vec<_> = tasks
.iter()
.filter(|t| {
t.assigned_to
.as_ref()
.is_some_and(|a| *a == AgentType::Codex)
})
.collect();
let mut content = String::new();
content.push_str("# AGENTS.md (Auto-generated by Forge Orchestrator)\n\n");
content.push_str(&format!("**Project:** {}\n", state.project_name));
content.push_str(&format!(
"**Last updated:** {}\n\n",
state.updated_at.format("%Y-%m-%d %H:%M UTC")
));
content.push_str("---\n\n");
if assigned.is_empty() {
content.push_str("## Status\n\nNo tasks currently assigned to Codex.\n\n");
} else {
content.push_str("## Assigned Tasks\n\n");
for task in &assigned {
content.push_str(&format!(
"### {}: {}\n\n{}\n\n",
task.id, task.title, task.description
));
if !task.acceptance_criteria.is_empty() {
content.push_str("**Acceptance Criteria:**\n");
for c in &task.acceptance_criteria {
content.push_str(&format!("- [ ] {c}\n"));
}
content.push('\n');
}
if !task.locked_files.is_empty() {
content.push_str("**Files you may modify:**\n");
for file in &task.locked_files {
content.push_str(&format!("- `{file}`\n"));
}
content.push('\n');
}
}
}
content.push_str("## Rules\n\n");
content.push_str("- Complete tasks one at a time\n");
content.push_str("- Do NOT modify files outside your assigned task scope\n");
content.push_str("- Run tests before marking a task complete\n");
content.push_str("- Check `.forge/state.json` for current orchestration state\n");
std::fs::write(project_root.join("AGENTS.md"), content)?;
Ok(())
}
fn build_command(
&self,
task: &Task,
project_root: &Path,
auth_mode: &str,
permissions: &str,
) -> Command {
let task_type = task.task_type.as_deref().unwrap_or("");
let prompt = Self::task_prompt(task, task_type);
let mut cmd = Command::new("codex");
if permissions == "yolo" {
cmd.args(["exec", "--full-auto", "--skip-git-repo-check", &prompt]);
} else {
cmd.args(["exec", "--skip-git-repo-check", &prompt]);
}
cmd.current_dir(project_root);
if auth_mode == "subscription" {
cmd.env_remove("OPENAI_API_KEY");
}
cmd
}
fn build_command_interactive(
&self,
task: &Task,
project_root: &Path,
auth_mode: &str,
_permissions: &str,
) -> Command {
let task_type = task.task_type.as_deref().unwrap_or("");
let prompt = Self::task_prompt(task, task_type);
let mut cmd = Command::new("codex");
cmd.args(["exec", "--full-auto", "--skip-git-repo-check", &prompt]);
cmd.current_dir(project_root);
if auth_mode == "subscription" {
cmd.env_remove("OPENAI_API_KEY");
}
cmd
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_task(task_type: &str) -> Task {
let mut task = Task::new("T-001", "Add login form", "Implement the login UI");
task.task_type = Some(task_type.to_string());
task.acceptance_criteria = vec!["Form validates email".to_string()];
task
}
#[test]
fn test_codex_verify_prompt() {
let task = make_task("verify");
let prompt = CodexAdapter::task_prompt(&task, "verify");
assert!(prompt.contains("VERIFICATION task"));
assert!(prompt.contains("ACTIVELY TEST"));
assert!(prompt.contains("Run the project's test suite"));
}
#[test]
fn test_codex_implement_prompt_has_test_requirement() {
let task = make_task("implement");
let prompt = CodexAdapter::task_prompt(&task, "implement");
assert!(prompt.contains("Write unit tests"));
assert!(prompt.contains("Run the existing test suite"));
}
#[test]
fn test_codex_review_prompt_is_readonly() {
let task = make_task("review");
let prompt = CodexAdapter::task_prompt(&task, "review");
assert!(prompt.contains("REVIEW task"));
assert!(prompt.contains("Do NOT modify files"));
}
#[test]
fn test_codex_default_prompt() {
let task = make_task("something");
let prompt = CodexAdapter::task_prompt(&task, "something");
assert!(prompt.contains("T-001"));
assert!(prompt.contains("Write unit tests"));
}
}