use std::path::PathBuf;
use tracing::{info, warn};
use crate::error::Result;
use crate::models::checkpoint::{Checkpoint, CheckpointSession};
use super::collector::CheckpointCollector;
use super::storage::CheckpointStore;
pub struct AutoCheckpointTrigger {
repo_path: PathBuf,
teams_base: PathBuf,
tasks_base: PathBuf,
}
impl AutoCheckpointTrigger {
pub fn new(repo_path: PathBuf, teams_base: PathBuf, tasks_base: PathBuf) -> Self {
Self {
repo_path,
teams_base,
tasks_base,
}
}
pub fn from_defaults() -> Option<Self> {
let repo_path = std::env::current_dir().ok()?;
if !is_git_repo(&repo_path) {
return None;
}
let home = dirs::home_dir()?;
let claude_dir = home.join(".claude");
Some(Self {
repo_path,
teams_base: claude_dir.join("teams"),
tasks_base: claude_dir.join("tasks"),
})
}
pub fn on_task_completed(
&self,
team_name: Option<&str>,
task_subject: &str,
agent_name: &str,
) -> Option<Checkpoint> {
match self.try_create_checkpoint(team_name, task_subject, agent_name) {
Ok(checkpoint) => {
info!(
checkpoint_id = %checkpoint.id,
agent = agent_name,
task = task_subject,
"Auto-checkpoint created on task completion"
);
Some(checkpoint)
}
Err(e) => {
warn!(
agent = agent_name,
task = task_subject,
error = %e,
"Auto-checkpoint failed (non-fatal)"
);
None
}
}
}
fn try_create_checkpoint(
&self,
team_name: Option<&str>,
task_subject: &str,
agent_name: &str,
) -> Result<Checkpoint> {
let mut collector = CheckpointCollector::new(&self.repo_path)
.with_teams_base(&self.teams_base)
.with_tasks_base(&self.tasks_base);
if let Some(team) = team_name {
collector = collector.with_team(team);
}
let mut session = CheckpointSession::new(agent_name);
session.prompt_summary = Some(format!("Task completed: {task_subject}"));
let discovered = crate::util::session_discovery::discover_sessions(&self.repo_path);
let mut checkpoint = collector.collect(session)?;
if let Some(latest) = discovered.first() {
match crate::util::session_discovery::parse_token_usage(&latest.path) {
Ok(Some(usage)) => {
checkpoint.token_usage = Some(usage);
}
Ok(None) => {}
Err(e) => {
warn!(error = %e, "Failed to parse session for auto-checkpoint token data");
}
}
}
checkpoint.metadata.insert(
"autoTriggered".to_string(),
serde_json::json!(true),
);
let store = CheckpointStore::open(&self.repo_path)?;
store.save(&checkpoint)?;
Ok(checkpoint)
}
}
fn is_git_repo(path: &std::path::Path) -> bool {
git2::Repository::discover(path).is_ok()
}
#[cfg(test)]
mod tests {
use super::*;
use git2::Repository;
use std::path::Path;
use tempfile::TempDir;
fn setup_test_repo() -> (TempDir, Repository) {
let dir = TempDir::new().unwrap();
let repo = Repository::init(dir.path()).unwrap();
{
let sig = git2::Signature::now("Test", "test@test.com").unwrap();
let tree_id = {
let mut index = repo.index().unwrap();
std::fs::write(dir.path().join("file.txt"), "hello").unwrap();
index.add_path(Path::new("file.txt")).unwrap();
index.write().unwrap();
index.write_tree().unwrap()
};
let tree = repo.find_tree(tree_id).unwrap();
repo.commit(Some("HEAD"), &sig, &sig, "Initial commit", &tree, &[])
.unwrap();
}
(dir, repo)
}
#[test]
fn auto_checkpoint_on_task_completed() {
let (dir, _repo) = setup_test_repo();
let trigger = AutoCheckpointTrigger::new(
dir.path().to_path_buf(),
dir.path().join("teams"),
dir.path().join("tasks"),
);
let result = trigger.on_task_completed(
None,
"Fix authentication bug",
"coder-1",
);
assert!(result.is_some());
let ckpt = result.unwrap();
assert_eq!(ckpt.session.agent_name, "coder-1");
assert!(ckpt.metadata.contains_key("autoTriggered"));
let store = CheckpointStore::open(dir.path()).unwrap();
let loaded = store.load(&ckpt.commit_sha).unwrap();
assert_eq!(loaded.id, ckpt.id);
}
#[test]
fn auto_checkpoint_not_git_repo() {
let dir = TempDir::new().unwrap();
let trigger = AutoCheckpointTrigger::new(
dir.path().to_path_buf(),
dir.path().join("teams"),
dir.path().join("tasks"),
);
let result = trigger.on_task_completed(None, "Test", "agent");
assert!(result.is_none()); }
#[test]
fn is_git_repo_works() {
let dir = TempDir::new().unwrap();
assert!(!is_git_repo(dir.path()));
let _repo = Repository::init(dir.path()).unwrap();
assert!(is_git_repo(dir.path()));
}
}