use crate::tool::{Tool, ToolDefinition, ToolInvocation, ToolKind, ToolResult};
use crate::verifier::{VerificationStore, VerifierRunner, VerifierSpec};
use anyhow::{Context, Result};
use async_trait::async_trait;
use serde_json::{Value, json};
use std::path::PathBuf;
use std::sync::Arc;
use super::helpers;
struct VerifierToolState {
project_root: PathBuf,
store: VerificationStore,
}
pub(crate) struct VerifierTool {
inner: Arc<VerifierToolState>,
}
impl VerifierTool {
pub(crate) fn new(project_root: PathBuf) -> Self {
Self {
inner: Arc::new(VerifierToolState {
project_root,
store: VerificationStore::new(),
}),
}
}
}
#[async_trait]
impl Tool for VerifierTool {
fn definition(&self) -> ToolDefinition {
helpers::definition(
"verifier",
"Run verification commands (build, test, typecheck, lint) and capture structured results.\n\n\
Actions:\n\
- `run`: execute a verification command with optional timeout and cwd.\n\
- `status`: retrieve a stored verification result by key.\n\
- `list`: show all stored verification results.\n\n\
When a write tool has a verifier hint in its metadata, use `run` to verify after writing.",
ToolKind::Command,
json!({
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": ["run", "status", "list"],
"description": "What the verifier should do."
},
"verifier": {
"type": "string",
"enum": ["build", "test", "typecheck", "lint", "command"],
"description": "Category of verification (used for error classification). Required when action=run."
},
"command": {
"type": "string",
"description": "Shell command to run. Required when action=run."
},
"key": {
"type": "string",
"description": "Store/retrieval key for the verification result. Defaults to '<verifier>:<command>' when action=run. Required when action=status."
},
"cwd": {
"type": "string",
"description": "Working directory override (project-relative or absolute). Defaults to project root."
},
"timeout_ms": {
"type": "integer",
"description": "Timeout in milliseconds. Defaults to 120000 (2 minutes)."
},
"required": {
"type": "boolean",
"description": "Whether this verification is required. When false, a non-zero exit code does not block progress."
}
},
"required": ["action"],
"additionalProperties": false
}),
)
}
async fn invoke(&self, invocation: ToolInvocation) -> Result<ToolResult> {
let action = helpers::required_string(&invocation.input, "action")?.to_string();
match action.as_str() {
"run" => self.invoke_run(invocation).await,
"status" => self.invoke_status(invocation).await,
"list" => self.invoke_list(invocation).await,
other => Ok(ToolResult {
invocation_id: invocation.id,
ok: false,
output: json!({
"error_code": "invalid_action",
"error": format!("Unknown action `{other}`. Use run, status, or list."),
}),
}),
}
}
}
impl VerifierTool {
async fn invoke_run(&self, invocation: ToolInvocation) -> Result<ToolResult> {
let verifier_type = helpers::optional_string(&invocation.input, "verifier")
.unwrap_or_else(|| "command".to_string());
let command = helpers::required_string(&invocation.input, "command")?.to_string();
let key = helpers::optional_string(&invocation.input, "key")
.unwrap_or_else(|| format!("{verifier_type}:{command}"));
let cwd = helpers::optional_string(&invocation.input, "cwd");
let timeout_ms = helpers::optional_u64(&invocation.input, "timeout_ms");
let required = helpers::optional_bool(&invocation.input, "required").unwrap_or(true);
let spec = VerifierSpec {
verifier_type,
command,
cwd,
timeout_ms,
required,
};
let project_root = self.inner.project_root.clone();
let result = VerifierRunner::run(&spec, &project_root).await;
let store = self.inner.store.clone();
store.store(key.clone(), result.clone()).await;
let mut output =
serde_json::to_value(&result).context("failed to serialize verifier result")?;
if let Value::Object(ref mut map) = output {
map.insert("key".to_string(), json!(key));
map.insert("schema_version".to_string(), json!(1));
let summary = match result.status.as_str() {
"pass" => format!("{} passed ({} ms)", result.command, result.duration_ms),
"fail" => {
let ec = result.exit_code.unwrap_or(-1);
format!(
"{} failed (exit code {}) — {}",
result.command,
ec,
result.error_class.as_deref().unwrap_or("unknown error")
)
}
"error" => format!(
"{} error — {}",
result.command,
result.error_class.as_deref().unwrap_or("system error")
),
"skipped" => format!("{} skipped", result.command),
other => format!("{}: {}", other, result.command),
};
map.insert("summary".to_string(), json!(summary));
}
Ok(ToolResult {
invocation_id: invocation.id,
ok: matches!(result.status.as_str(), "pass" | "skipped"),
output,
})
}
async fn invoke_status(&self, invocation: ToolInvocation) -> Result<ToolResult> {
let key = helpers::required_string(&invocation.input, "key")?.to_string();
let store = self.inner.store.clone();
match store.get(&key).await {
Some(result) => {
let output =
serde_json::to_value(&result).context("failed to serialize verifier result")?;
Ok(helpers::ok(invocation.id, output))
}
None => Ok(ToolResult {
invocation_id: invocation.id,
ok: false,
output: json!({
"error_code": "not_found",
"error": format!("No verification result found for key `{key}`"),
}),
}),
}
}
async fn invoke_list(&self, invocation: ToolInvocation) -> Result<ToolResult> {
let store = self.inner.store.clone();
let items = store.list().await;
let results: Vec<Value> = items
.into_iter()
.map(|(key, result)| {
let mut v = serde_json::to_value(&result).unwrap_or(json!({}));
if let Value::Object(ref mut map) = v {
map.insert("key".to_string(), json!(key));
}
v
})
.collect();
Ok(helpers::ok(
invocation.id,
json!({
"results": results,
"total": results.len(),
}),
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn verifier_tool_definition_has_correct_name() {
let tool = VerifierTool::new(PathBuf::from("/tmp"));
let def = tool.definition();
assert_eq!(def.name, "verifier");
assert_eq!(def.kind, ToolKind::Command);
}
#[test]
fn verifier_tool_definition_has_required_fields() {
let tool = VerifierTool::new(PathBuf::from("/tmp"));
let def = tool.definition();
let required = def.input_schema["required"]
.as_array()
.expect("required array");
assert!(required.contains(&json!("action")));
}
#[test]
fn verifier_tool_definition_has_action_enum() {
let tool = VerifierTool::new(PathBuf::from("/tmp"));
let def = tool.definition();
let enum_vals = def.input_schema["properties"]["action"]["enum"]
.as_array()
.expect("enum array");
assert!(enum_vals.contains(&json!("run")));
assert!(enum_vals.contains(&json!("status")));
assert!(enum_vals.contains(&json!("list")));
}
#[test]
fn verifier_tool_definition_has_verifier_enum() {
let tool = VerifierTool::new(PathBuf::from("/tmp"));
let def = tool.definition();
let enum_vals = def.input_schema["properties"]["verifier"]["enum"]
.as_array()
.expect("enum array");
assert!(enum_vals.contains(&json!("build")));
assert!(enum_vals.contains(&json!("test")));
assert!(enum_vals.contains(&json!("typecheck")));
assert!(enum_vals.contains(&json!("lint")));
assert!(enum_vals.contains(&json!("command")));
}
#[tokio::test]
async fn verifier_tool_run_echo() {
let dir = tempfile::tempdir().unwrap();
let tool = VerifierTool::new(dir.path().to_path_buf());
let result = tool
.invoke(ToolInvocation {
id: "test".to_string(),
tool_name: "verifier".to_string(),
input: json!({
"action": "run",
"verifier": "command",
"command": "echo hello world",
}),
})
.await
.unwrap();
assert!(result.ok, "echo should pass: {:?}", result.output);
assert_eq!(result.output["status"], "pass");
assert_eq!(result.output["exit_code"], 0);
}
#[tokio::test]
async fn verifier_tool_list() {
let dir = tempfile::tempdir().unwrap();
let tool = VerifierTool::new(dir.path().to_path_buf());
tool.invoke(ToolInvocation {
id: "run1".to_string(),
tool_name: "verifier".to_string(),
input: json!({
"action": "run",
"verifier": "command",
"command": "true",
}),
})
.await
.unwrap();
let result = tool
.invoke(ToolInvocation {
id: "list".to_string(),
tool_name: "verifier".to_string(),
input: json!({ "action": "list" }),
})
.await
.unwrap();
assert!(result.ok);
let results = result.output["results"].as_array().unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0]["key"], "command:true");
}
#[tokio::test]
async fn verifier_tool_status() {
let dir = tempfile::tempdir().unwrap();
let tool = VerifierTool::new(dir.path().to_path_buf());
tool.invoke(ToolInvocation {
id: "run".to_string(),
tool_name: "verifier".to_string(),
input: json!({
"action": "run",
"verifier": "build",
"command": "echo built",
"key": "my-build",
}),
})
.await
.unwrap();
let result = tool
.invoke(ToolInvocation {
id: "status".to_string(),
tool_name: "verifier".to_string(),
input: json!({ "action": "status", "key": "my-build" }),
})
.await
.unwrap();
assert!(result.ok);
assert_eq!(result.output["status"], "pass");
}
#[tokio::test]
async fn verifier_tool_status_not_found() {
let dir = tempfile::tempdir().unwrap();
let tool = VerifierTool::new(dir.path().to_path_buf());
let result = tool
.invoke(ToolInvocation {
id: "status".to_string(),
tool_name: "verifier".to_string(),
input: json!({ "action": "status", "key": "nonexistent" }),
})
.await
.unwrap();
assert!(!result.ok);
assert_eq!(result.output["error_code"], "not_found");
}
#[tokio::test]
async fn verifier_tool_invalid_action() {
let dir = tempfile::tempdir().unwrap();
let tool = VerifierTool::new(dir.path().to_path_buf());
let result = tool
.invoke(ToolInvocation {
id: "bad".to_string(),
tool_name: "verifier".to_string(),
input: json!({ "action": "bake" }),
})
.await
.unwrap();
assert!(!result.ok);
assert_eq!(result.output["error_code"], "invalid_action");
}
}