use super::ToolAdapter;
use crate::core::state::ForgeState;
use crate::core::task::Task;
use regex::Regex;
use std::path::Path;
use std::process::Command;
use std::sync::LazyLock;
pub struct ClaudeAdapter;
static VERSIONED_MODEL: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^\[[A-Za-z][A-Za-z ]* [0-9]+(\.[0-9]+)?(:(low|medium|high|xhigh|max))?\]$")
.unwrap()
});
static SIBLING_GAUGE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(^|\s)(5h|7d):[0-9]{1,3}%(\(|\s|$)").unwrap());
static CTX_GAUGE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(^|\s)ctx:[0-9]{1,3}%").unwrap());
impl ToolAdapter for ClaudeAdapter {
fn name(&self) -> &str {
"claude"
}
fn parse_ctx_pct(&self, statusline: &str) -> Option<u8> {
for line in statusline.lines() {
let lower = line.to_lowercase();
if !CTX_GAUGE.is_match(&lower) || !SIBLING_GAUGE.is_match(&lower) {
continue;
}
let has_versioned_model = super::bracket_contents(line)
.iter()
.any(|c| VERSIONED_MODEL.is_match(&format!("[{c}]")));
if has_versioned_model {
return super::pct_after(&lower, "ctx:");
}
}
None
}
fn render_config(
&self,
state: &ForgeState,
tasks: &[Task],
project_root: &Path,
) -> anyhow::Result<()> {
let assigned_tasks: Vec<&Task> = tasks
.iter()
.filter(|t| {
t.assigned_to
.as_ref()
.is_some_and(|a| *a == crate::core::task::AgentType::Claude)
})
.collect();
let mut content = String::new();
content.push_str("# CLAUDE.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_tasks.is_empty() {
content.push_str("## Status\n\nNo tasks currently assigned to Claude.\n\n");
} else {
content.push_str("## Assigned Tasks\n\n");
for task in &assigned_tasks {
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 criterion in &task.acceptance_criteria {
content.push_str(&format!("- [ ] {criterion}\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("## Governance\n\n");
content.push_str("- Report task completion via MCP: `forge_complete_task()`\n");
content.push_str("- Do NOT modify files outside your locked file list\n");
content.push_str("- Run tests before marking task complete\n");
content.push_str("- Check `.forge/state.json` for current orchestration state\n\n");
let claude_md_path = project_root.join("CLAUDE.md");
std::fs::write(claude_md_path, 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 = build_prompt(task, task_type);
let mut cmd = Command::new("claude");
cmd.args(["-p", &prompt, "--output-format", "stream-json", "--verbose"])
.current_dir(project_root);
if permissions == "yolo" {
match task_type {
"implement" => {
cmd.args(["--allowedTools", "Write,Edit,Read,Glob,Grep,Bash"]);
}
"review" | "test" | "verify" => {
cmd.args(["--allowedTools", "Read,Glob,Grep,Bash"]);
}
"document" => {
cmd.args(["--allowedTools", "Write,Edit,Read,Glob,Grep"]);
}
_ => {
cmd.args(["--dangerously-skip-permissions"]);
}
}
}
match task_type {
"review" | "test" | "verify" => {
cmd.args(["--max-turns", "20"]);
}
"design" => {
cmd.args(["--max-turns", "30"]);
}
_ => {}
}
if auth_mode == "subscription" {
cmd.env_remove("ANTHROPIC_API_KEY");
}
cmd
}
fn build_command_interactive(
&self,
_task: &Task,
project_root: &Path,
auth_mode: &str,
_permissions: &str,
) -> Command {
let mut cmd = Command::new("claude");
cmd.arg("--dangerously-skip-permissions");
cmd.current_dir(project_root);
if auth_mode == "subscription" {
cmd.env_remove("ANTHROPIC_API_KEY");
}
cmd
}
fn initial_input(&self, task: &Task) -> Option<String> {
let task_type = task.task_type.as_deref().unwrap_or("");
let prompt = build_prompt(task, task_type).replace('\n', " ");
Some(format!("{}\r", prompt))
}
fn ready_pattern(&self) -> Option<&str> {
Some("to cycle")
}
}
pub(crate) fn build_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")
)
};
let files = if task.locked_files.is_empty() {
String::new()
} else {
format!(
"Files in scope:\n{}\n\n",
task.locked_files
.iter()
.map(|f| format!("- {f}"))
.collect::<Vec<_>>()
.join("\n")
)
};
match task_type {
"design" => format!(
"You are working on task {id}: {title}\n\n\
This is a DESIGN task. Focus on architecture and system design.\n\n\
Description: {desc}\n\n\
{criteria}{files}\
Think through the design carefully. Consider trade-offs, edge cases, \
and how this fits into the broader system. Produce clear, actionable \
design documents or code structures.\n\n\
When done, summarize your design decisions and rationale.",
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}{files}\
Read the relevant code, identify issues, and provide a clear \
assessment. Do NOT modify files — this is a read-only review.\n\n\
When done, summarize your findings.",
id = task.id,
title = task.title,
desc = task.description,
),
"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}{files}\
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\
7. If Playwright/E2E tests exist, run them and check for browser console errors:\n\
- Use `page.on('console', ...)` and `page.on('pageerror', ...)` to capture errors\n\
- Zero uncaught exceptions and pageerrors = pass\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,
),
"test" => format!(
"You are working on task {id}: {title}\n\n\
This is a TESTING task. Focus on test coverage and quality.\n\n\
Description: {desc}\n\n\
{criteria}{files}\
Write thorough tests with edge cases. Run existing tests to verify \
nothing breaks.\n\n\
When done, summarize what tests you added and their results.",
id = task.id,
title = task.title,
desc = task.description,
),
"document" => format!(
"You are working on task {id}: {title}\n\n\
This is a DOCUMENTATION task. Focus on clarity and completeness.\n\n\
Description: {desc}\n\n\
{criteria}{files}\
Write clear, accurate documentation. Include examples where helpful.\n\n\
When done, summarize what you documented.",
id = task.id,
title = task.title,
desc = task.description,
),
_ => format!(
"You are working on task {id}: {title}\n\n\
Description: {desc}\n\n\
{criteria}{files}\
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\
Complete this task. When done, summarize what you did.",
id = task.id,
title = task.title,
desc = task.description,
),
}
}
#[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.locked_files = vec!["src/login.rs".to_string()];
task
}
#[test]
fn test_verify_prompt_contains_verification_task() {
let task = make_task("verify");
let prompt = build_prompt(&task, "verify");
assert!(
prompt.contains("VERIFICATION task"),
"verify prompt must contain 'VERIFICATION task'"
);
}
#[test]
fn test_verify_prompt_contains_test_suite_instruction() {
let task = make_task("verify");
let prompt = build_prompt(&task, "verify");
assert!(
prompt.contains("Run the project's test suite"),
"verify prompt must instruct running the test suite"
);
}
#[test]
fn test_verify_prompt_contains_actively_test() {
let task = make_task("verify");
let prompt = build_prompt(&task, "verify");
assert!(
prompt.contains("ACTIVELY TEST"),
"verify prompt must say ACTIVELY TEST"
);
}
#[test]
fn test_implement_prompt_contains_write_unit_tests() {
let task = make_task("implement");
let prompt = build_prompt(&task, "implement");
assert!(
prompt.contains("Write unit tests"),
"implement/default prompt must contain 'Write unit tests'"
);
}
#[test]
fn test_implement_prompt_contains_run_test_suite() {
let task = make_task("implement");
let prompt = build_prompt(&task, "implement");
assert!(
prompt.contains("Run the existing test suite"),
"implement/default prompt must instruct running tests"
);
}
#[test]
fn test_default_prompt_works_for_unknown_type() {
let task = make_task("something_unknown");
let prompt = build_prompt(&task, "something_unknown");
assert!(prompt.contains("T-001"));
assert!(prompt.contains("Add login form"));
assert!(prompt.contains("Write unit tests"));
}
#[test]
fn test_review_prompt_unchanged() {
let task = make_task("review");
let prompt = build_prompt(&task, "review");
assert!(
prompt.contains("REVIEW task"),
"review prompt must still say REVIEW task"
);
assert!(
prompt.contains("Do NOT modify files"),
"review prompt must still be read-only"
);
assert!(
!prompt.contains("VERIFICATION"),
"review prompt must NOT contain VERIFICATION"
);
}
#[test]
fn test_verify_prompt_includes_criteria_and_files() {
let task = make_task("verify");
let prompt = build_prompt(&task, "verify");
assert!(
prompt.contains("Form validates email"),
"verify prompt must include acceptance criteria"
);
assert!(
prompt.contains("src/login.rs"),
"verify prompt must include locked files"
);
}
#[test]
fn test_design_prompt_unchanged() {
let task = make_task("design");
let prompt = build_prompt(&task, "design");
assert!(prompt.contains("DESIGN task"));
assert!(!prompt.contains("VERIFICATION"));
}
}