vtcode_core/core/agent/
blocked_handoff.rs1use 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 ¤t_path,
49 &archive_path,
50 relevant_paths,
51 timestamp.to_rfc3339(),
52 );
53
54 fs::write(¤t_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}