use anyhow::{Context, Result};
use std::fs;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
const TASKS_DIR: &str = ".vtcode/tasks";
const CURRENT_TASK_FILE: &str = "current_task.md";
const CURRENT_SPEC_FILE: &str = "current_spec.md";
const CURRENT_CONTRACT_FILE: &str = "current_contract.md";
const CURRENT_EVALUATION_FILE: &str = "current_evaluation.md";
const CURRENT_SPRINT_CONTRACT_FILE: &str = "current_sprint_contract.md";
const CURRENT_OUTCOME_VERIFICATION_FILE: &str = "current_outcome_verification.md";
const CURRENT_FEATURE_LIST_FILE: &str = "current_feature_list.md";
const SUMMARY_PREVIEW_CHARS: usize = 280;
pub fn current_task_path(workspace_root: &Path) -> PathBuf {
workspace_root.join(TASKS_DIR).join(CURRENT_TASK_FILE)
}
pub fn current_context_reset_path(workspace_root: &Path) -> PathBuf {
workspace_root
.join(TASKS_DIR)
.join(crate::core::agent::context_reset::CONTEXT_RESET_FILE)
}
pub fn current_spec_path(workspace_root: &Path) -> PathBuf {
workspace_root.join(TASKS_DIR).join(CURRENT_SPEC_FILE)
}
pub fn current_contract_path(workspace_root: &Path) -> PathBuf {
workspace_root.join(TASKS_DIR).join(CURRENT_CONTRACT_FILE)
}
pub fn current_evaluation_path(workspace_root: &Path) -> PathBuf {
workspace_root.join(TASKS_DIR).join(CURRENT_EVALUATION_FILE)
}
pub fn current_sprint_contract_path(workspace_root: &Path) -> PathBuf {
workspace_root.join(TASKS_DIR).join(CURRENT_SPRINT_CONTRACT_FILE)
}
pub fn current_outcome_verification_path(workspace_root: &Path) -> PathBuf {
workspace_root.join(TASKS_DIR).join(CURRENT_OUTCOME_VERIFICATION_FILE)
}
pub fn existing_harness_artifact_paths(workspace_root: &Path) -> Vec<PathBuf> {
[
current_spec_path(workspace_root),
current_contract_path(workspace_root),
current_evaluation_path(workspace_root),
current_sprint_contract_path(workspace_root),
current_outcome_verification_path(workspace_root),
current_feature_list_path(workspace_root),
]
.into_iter()
.filter(|path| path.exists())
.collect()
}
pub fn read_spec_summary(workspace_root: &Path) -> Option<String> {
read_markdown_summary(¤t_spec_path(workspace_root), "Spec")
}
pub fn read_spec_summary_fresh(workspace_root: &Path, not_before: Option<SystemTime>) -> Option<String> {
read_markdown_summary_fresh(¤t_spec_path(workspace_root), "Spec", not_before)
}
pub fn read_contract_summary(workspace_root: &Path) -> Option<String> {
read_markdown_summary(¤t_contract_path(workspace_root), "Contract")
}
pub fn read_evaluation_summary(workspace_root: &Path) -> Option<String> {
read_markdown_summary(¤t_evaluation_path(workspace_root), "Evaluation")
}
pub fn read_evaluation_summary_fresh(workspace_root: &Path, not_before: Option<SystemTime>) -> Option<String> {
read_markdown_summary_fresh(¤t_evaluation_path(workspace_root), "Evaluation", not_before)
}
pub fn read_contract_summary_fresh(workspace_root: &Path, not_before: Option<SystemTime>) -> Option<String> {
read_markdown_summary_fresh(¤t_contract_path(workspace_root), "Contract", not_before)
}
pub fn read_feature_list_summary_fresh(workspace_root: &Path, not_before: Option<SystemTime>) -> Option<String> {
read_markdown_summary_fresh(¤t_feature_list_path(workspace_root), "FeatureList", not_before)
}
pub fn read_sprint_contract_summary_fresh(workspace_root: &Path, not_before: Option<SystemTime>) -> Option<String> {
read_markdown_summary_fresh(¤t_sprint_contract_path(workspace_root), "SprintContract", not_before)
}
pub fn read_outcome_verification_summary_fresh(
workspace_root: &Path,
not_before: Option<SystemTime>,
) -> Option<String> {
read_markdown_summary_fresh(¤t_outcome_verification_path(workspace_root), "OutcomeVerification", not_before)
}
pub fn session_artifact_cutoff(workspace_root: &Path, session_id: &str) -> Option<SystemTime> {
let candidates = [
vtcode_memory::session_directory(workspace_root, session_id),
workspace_root.join(".vtcode").join("sessions").join(session_id),
];
for dir in candidates {
let Ok(metadata) = fs::metadata(&dir) else {
continue;
};
if let Ok(created) = metadata.created() {
return Some(created);
}
}
None
}
pub fn archive_completed_current_task(workspace_root: &Path, session_id: &str) -> Result<Option<PathBuf>> {
let task_path = current_task_path(workspace_root);
let Ok(content) = fs::read_to_string(&task_path) else {
return Ok(None);
};
let checklist: Vec<&str> = content
.lines()
.map(str::trim_start)
.filter(|line| line.starts_with("- ["))
.collect();
let is_checked = |line: &str| line.starts_with("- [x]") || line.starts_with("- [X]");
if checklist.is_empty() || !checklist.iter().all(|line| is_checked(line)) {
return Ok(None);
}
let archive_dir = workspace_root.join(TASKS_DIR).join("archive");
fs::create_dir_all(&archive_dir).with_context(|| format!("create task archive dir {}", archive_dir.display()))?;
let archive_path = archive_dir.join(format!("current_task-{}.md", filename_safe_id(session_id, 64)));
fs::rename(&task_path, &archive_path)
.with_context(|| format!("archive completed task tracker to {}", archive_path.display()))?;
Ok(Some(archive_path))
}
fn filename_safe_id(id: &str, max_chars: usize) -> String {
id.chars()
.map(|c| {
if c.is_alphanumeric() || c == '_' || c == '-' {
c
} else {
'_'
}
})
.take(max_chars)
.collect()
}
pub async fn write_spec(workspace_root: &Path, content: &str) -> Result<PathBuf> {
let path = current_spec_path(workspace_root);
write_artifact(path.as_path(), content, "current spec").await?;
Ok(path)
}
pub async fn write_evaluation(workspace_root: &Path, content: &str) -> Result<PathBuf> {
let path = current_evaluation_path(workspace_root);
write_artifact(path.as_path(), content, "current evaluation").await?;
Ok(path)
}
pub async fn write_contract(workspace_root: &Path, content: &str) -> Result<PathBuf> {
let path = current_contract_path(workspace_root);
write_artifact(path.as_path(), content, "current contract").await?;
Ok(path)
}
pub fn read_sprint_contract_summary(workspace_root: &Path) -> Option<String> {
read_markdown_summary(¤t_sprint_contract_path(workspace_root), "SprintContract")
}
pub async fn write_sprint_contract(workspace_root: &Path, content: &str) -> Result<PathBuf> {
let path = current_sprint_contract_path(workspace_root);
write_artifact(path.as_path(), content, "sprint contract").await?;
Ok(path)
}
pub fn read_outcome_verification_summary(workspace_root: &Path) -> Option<String> {
read_markdown_summary(¤t_outcome_verification_path(workspace_root), "OutcomeVerification")
}
pub fn current_feature_list_path(workspace_root: &Path) -> PathBuf {
workspace_root.join(TASKS_DIR).join(CURRENT_FEATURE_LIST_FILE)
}
pub fn read_feature_list_summary(workspace_root: &Path) -> Option<String> {
read_markdown_summary(¤t_feature_list_path(workspace_root), "FeatureList")
}
pub async fn write_feature_list(workspace_root: &Path, content: &str) -> Result<PathBuf> {
let path = current_feature_list_path(workspace_root);
write_artifact(path.as_path(), content, "feature list").await?;
Ok(path)
}
pub async fn write_outcome_verification(workspace_root: &Path, content: &str) -> Result<PathBuf> {
let path = current_outcome_verification_path(workspace_root);
write_artifact(path.as_path(), content, "outcome verification").await?;
Ok(path)
}
async fn write_artifact(path: &Path, content: &str, label: &str) -> Result<()> {
if let Some(parent) = path.parent() {
tokio::fs::create_dir_all(parent)
.await
.with_context(|| format!("create {} directory {}", label, parent.display()))?;
}
tokio::fs::write(path, content)
.await
.with_context(|| format!("write {} {}", label, path.display()))?;
Ok(())
}
fn read_markdown_summary(path: &Path, label: &str) -> Option<String> {
let content = fs::read_to_string(path).ok()?;
let lines = content
.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
.filter(|line| !line.starts_with('#'))
.take(4)
.collect::<Vec<_>>();
if lines.is_empty() {
return None;
}
let joined = lines.join(" | ");
Some(format!("{label}: {}", truncate_summary(&joined)))
}
const ARTIFACT_FRESHNESS_GRACE: std::time::Duration = std::time::Duration::from_secs(24 * 60 * 60);
fn read_markdown_summary_fresh(path: &Path, label: &str, not_before: Option<SystemTime>) -> Option<String> {
if let Some(not_before) = not_before {
let modified = fs::metadata(path).ok()?.modified().ok()?;
let stale_before = not_before
.checked_sub(ARTIFACT_FRESHNESS_GRACE)
.unwrap_or(SystemTime::UNIX_EPOCH);
if modified < stale_before {
return None;
}
}
read_markdown_summary(path, label)
}
fn truncate_summary(text: &str) -> String {
vtcode_commons::formatting::truncate_within(text, SUMMARY_PREVIEW_CHARS, "...")
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[tokio::test]
async fn writes_and_summarizes_spec_and_evaluation_artifacts() {
let temp = tempdir().expect("tempdir");
write_spec(temp.path(), "# Spec\n\nBuild a stronger exec harness.\n\nKeep it resumable.\n")
.await
.expect("write spec");
write_contract(temp.path(), "# Contract\n\n- Deliver the requested change.\n- Verify with cargo check.\n")
.await
.expect("write contract");
write_evaluation(temp.path(), "# Evaluation\n\nVerdict: fail\n\nNeed another revision round.\n")
.await
.expect("write evaluation");
let paths = existing_harness_artifact_paths(temp.path());
assert_eq!(paths.len(), 3);
assert_eq!(
read_spec_summary(temp.path()),
Some("Spec: Build a stronger exec harness. | Keep it resumable.".to_string())
);
assert_eq!(
read_contract_summary(temp.path()),
Some("Contract: - Deliver the requested change. | - Verify with cargo check.".to_string())
);
assert_eq!(
read_evaluation_summary(temp.path()),
Some("Evaluation: Verdict: fail | Need another revision round.".to_string())
);
}
#[tokio::test]
async fn writes_and_summarizes_sprint_contract() {
let temp = tempdir().expect("tempdir");
write_sprint_contract(
temp.path(),
"# Sprint Contract\n\nScope: implement login endpoint.\nAcceptance: POST /login returns JWT.\n",
)
.await
.expect("write sprint contract");
let paths = existing_harness_artifact_paths(temp.path());
assert_eq!(paths.len(), 1);
assert_eq!(
read_sprint_contract_summary(temp.path()),
Some("SprintContract: Scope: implement login endpoint. | Acceptance: POST /login returns JWT.".to_string())
);
}
#[tokio::test]
async fn writes_and_summarizes_outcome_verification() {
let temp = tempdir().expect("tempdir");
write_outcome_verification(
temp.path(),
"# Outcome Verification\n\nCommand: cargo nextest run\nResult: 12 passed, 0 failed\nBuild: cargo check PASSED\n",
)
.await
.expect("write outcome verification");
let paths = existing_harness_artifact_paths(temp.path());
assert_eq!(paths.len(), 1);
assert_eq!(
read_outcome_verification_summary(temp.path()),
Some(
"OutcomeVerification: Command: cargo nextest run | Result: 12 passed, 0 failed | Build: cargo check PASSED"
.to_string()
)
);
}
#[tokio::test]
async fn writes_and_summarizes_feature_list() {
let temp = tempdir().expect("tempdir");
write_feature_list(
temp.path(),
"# Feature List\n\n- [ ] Auth: login endpoint returns JWT\n- [x] API: health check endpoint\n",
)
.await
.expect("write feature list");
let paths = existing_harness_artifact_paths(temp.path());
assert_eq!(paths.len(), 1);
assert_eq!(
read_feature_list_summary(temp.path()),
Some("FeatureList: - [ ] Auth: login endpoint returns JWT | - [x] API: health check endpoint".to_string())
);
}
#[tokio::test]
async fn all_artifacts_counted_in_existing_paths() {
let temp = tempdir().expect("tempdir");
write_spec(temp.path(), "# Spec\ncontent\n").await.unwrap();
write_contract(temp.path(), "# Contract\ncontent\n").await.unwrap();
write_evaluation(temp.path(), "# Evaluation\ncontent\n").await.unwrap();
write_sprint_contract(temp.path(), "# Sprint\ncontent\n").await.unwrap();
write_outcome_verification(temp.path(), "# Outcome\ncontent\n").await.unwrap();
write_feature_list(temp.path(), "# Features\ncontent\n").await.unwrap();
let paths = existing_harness_artifact_paths(temp.path());
assert_eq!(paths.len(), 6);
}
#[test]
fn stale_spec_summary_is_dropped_for_later_sessions() {
let temp = tempdir().expect("tempdir");
let spec_path = current_spec_path(temp.path());
fs::create_dir_all(spec_path.parent().expect("parent")).expect("tasks dir");
fs::write(&spec_path, "# Execution Spec\nExplore the codebase and summarize.\n").expect("write spec");
let old = SystemTime::now() - std::time::Duration::from_secs(48 * 60 * 60);
let file = fs::File::options().write(true).open(&spec_path).expect("open");
file.set_modified(old).expect("set mtime");
assert!(read_spec_summary(temp.path()).is_some(), "unfiltered read still sees the file");
assert!(
read_spec_summary_fresh(temp.path(), Some(SystemTime::now())).is_none(),
"a leftover fixture must not describe a later session"
);
assert!(
read_spec_summary_fresh(temp.path(), Some(old + std::time::Duration::from_secs(10))).is_some(),
"fresh reads still accept artifacts written during the session"
);
}
#[test]
fn handoff_artifact_written_just_before_session_start_stays_live() {
let temp = tempdir().expect("tempdir");
let spec_path = current_spec_path(temp.path());
fs::create_dir_all(spec_path.parent().expect("parent")).expect("tasks dir");
fs::write(&spec_path, "# Spec\n\nShip the residual hygiene fix.\n").expect("write spec");
let session_start = SystemTime::now();
assert!(
read_spec_summary_fresh(temp.path(), Some(session_start)).is_some(),
"a just-written handoff artifact must survive the freshness cutoff"
);
}
#[test]
fn archive_completed_current_task_moves_only_fully_checked_trackers() {
let temp = tempdir().expect("tempdir");
let task_path = current_task_path(temp.path());
fs::create_dir_all(task_path.parent().expect("parent")).expect("tasks dir");
fs::write(&task_path, "# Work\n\n- [ ] open item\n- [x] done item\n").expect("write partial");
assert!(
archive_completed_current_task(temp.path(), "session-a")
.expect("archive")
.is_none(),
"incomplete checklists stay live"
);
assert!(task_path.exists());
fs::write(&task_path, "# Work\n\n- [x] done one\n- [x] done two\n").expect("write complete");
let archived = archive_completed_current_task(temp.path(), "session-a")
.expect("archive")
.expect("fully-checked tracker is archived");
assert!(!task_path.exists(), "live path must be clear for the next plan");
assert!(archived.ends_with("current_task-session-a.md"));
assert!(archived.exists());
}
}