Skip to main content

vtcode_core/core/agent/
blocked_handoff.rs

1use std::fs;
2use std::path::{Path, PathBuf};
3
4use anyhow::{Context, Result};
5use chrono::Utc;
6
7use crate::utils::session_debug::sanitize_debug_component;
8
9const TASKS_DIR: &str = ".vtcode/tasks";
10const CURRENT_BLOCKED_FILE: &str = "current_blocked.md";
11const BLOCKERS_DIR: &str = "blockers";
12const CURRENT_TASK_FILE: &str = "current_task.md";
13
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct BlockedHandoffArtifacts {
16    pub current_path: PathBuf,
17    pub archive_path: PathBuf,
18}
19
20pub fn write_blocked_handoff(
21    workspace: &Path,
22    session_id: &str,
23    outcome_code: &str,
24    blocker_summary: &str,
25    relevant_paths: &[PathBuf],
26) -> Result<BlockedHandoffArtifacts> {
27    let tasks_dir = workspace.join(TASKS_DIR);
28    let blockers_dir = tasks_dir.join(BLOCKERS_DIR);
29    fs::create_dir_all(&blockers_dir)
30        .with_context(|| format!("failed to create blockers dir {}", blockers_dir.display()))?;
31
32    let tracker_path = tasks_dir.join(CURRENT_TASK_FILE);
33    let current_path = tasks_dir.join(CURRENT_BLOCKED_FILE);
34    let timestamp = Utc::now();
35    let archive_name = format!(
36        "{}-{}.md",
37        sanitize_debug_component(session_id, "session"),
38        timestamp.format("%Y%m%dT%H%M%SZ")
39    );
40    let archive_path = blockers_dir.join(archive_name);
41
42    let markdown = render_blocked_handoff(
43        workspace,
44        session_id,
45        outcome_code,
46        blocker_summary,
47        &tracker_path,
48        &current_path,
49        &archive_path,
50        relevant_paths,
51        timestamp.to_rfc3339(),
52    );
53
54    fs::write(&current_path, &markdown)
55        .with_context(|| format!("failed to write {}", current_path.display()))?;
56    fs::write(&archive_path, markdown)
57        .with_context(|| format!("failed to write {}", archive_path.display()))?;
58
59    Ok(BlockedHandoffArtifacts {
60        current_path,
61        archive_path,
62    })
63}
64
65fn render_blocked_handoff(
66    workspace: &Path,
67    session_id: &str,
68    outcome_code: &str,
69    blocker_summary: &str,
70    tracker_path: &Path,
71    current_path: &Path,
72    archive_path: &Path,
73    relevant_paths: &[PathBuf],
74    created_at: String,
75) -> String {
76    let tracker_snapshot = fs::read_to_string(tracker_path)
77        .ok()
78        .filter(|content| !content.trim().is_empty())
79        .unwrap_or_else(|| "_No current tracker snapshot found._".to_string());
80
81    let mut paths = vec![
82        workspace.to_path_buf(),
83        tracker_path.to_path_buf(),
84        current_path.to_path_buf(),
85        archive_path.to_path_buf(),
86    ];
87    for path in relevant_paths {
88        if !paths.iter().any(|existing| existing == path) {
89            paths.push(path.clone());
90        }
91    }
92
93    let relevant_paths_section = paths
94        .iter()
95        .map(|path| format!("- `{}`", path.display()))
96        .collect::<Vec<_>>()
97        .join("\n");
98
99    format!(
100        "---\nsession_id: {session_id}\noutcome: {outcome_code}\ncreated_at: {created_at}\nworkspace: {}\nresume_command: \"vtcode --resume {session_id}\"\n---\n\n# Blocker Summary\n\n{}\n\n# Current Tracker Snapshot\n\n{}\n\n# Relevant Paths\n\n{}\n\n# Resume Metadata\n\n- Session ID: `{session_id}`\n- Outcome: `{outcome_code}`\n- Resume command: `vtcode --resume {session_id}`\n",
101        workspace.display(),
102        blocker_summary.trim(),
103        tracker_snapshot,
104        relevant_paths_section,
105    )
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn writes_current_and_archived_blocked_handoffs() {
114        let temp = tempfile::tempdir().expect("temp dir");
115        let tasks_dir = temp.path().join(".vtcode/tasks");
116        fs::create_dir_all(&tasks_dir).expect("tasks dir");
117        fs::write(
118            tasks_dir.join("current_task.md"),
119            "# Current Task\n\n- [ ] investigate blocker\n",
120        )
121        .expect("tracker");
122
123        let artifacts = write_blocked_handoff(
124            temp.path(),
125            "session-123",
126            "loop_detected",
127            "Execution stalled on a loop.",
128            &[temp.path().join("src/lib.rs")],
129        )
130        .expect("write handoff");
131
132        let current = fs::read_to_string(&artifacts.current_path).expect("current handoff");
133        let archive = fs::read_to_string(&artifacts.archive_path).expect("archive handoff");
134
135        assert_eq!(current, archive);
136        assert!(current.contains("session_id: session-123"));
137        assert!(current.contains("# Blocker Summary"));
138        assert!(current.contains("Execution stalled on a loop."));
139        assert!(current.contains("# Current Task"));
140        assert!(current.contains("vtcode --resume session-123"));
141        assert!(current.contains("src/lib.rs"));
142    }
143}