1use serde::{Deserialize, Serialize};
13
14#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
16#[serde(rename_all = "snake_case")]
17pub enum BoundaryStatus {
18 Done,
20 InProgress,
22 NotStarted,
24 Blocked,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct BoundaryItem {
33 pub feature: String,
35 pub status: BoundaryStatus,
37 #[serde(default)]
39 pub notes: Option<String>,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct HandoffRequest {
50 pub target_agent: String,
52
53 pub state_summary: String,
55
56 #[serde(default)]
58 pub modified_files: Vec<String>,
59
60 #[serde(default)]
62 pub open_decisions: Vec<String>,
63
64 pub task_context: String,
66
67 #[serde(default)]
69 pub context_files: Vec<String>,
70
71 #[serde(default)]
74 pub test_results: Option<String>,
75
76 #[serde(default)]
79 pub boundary_status: Vec<BoundaryItem>,
80
81 #[serde(default)]
83 pub known_issues: Vec<String>,
84
85 #[serde(default)]
87 pub next_actions: Vec<String>,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct HandoffReceipt {
93 pub accepted: bool,
95
96 #[serde(default)]
98 pub continuation_message: String,
99
100 #[serde(default)]
102 pub revised_plan: Option<String>,
103}
104
105impl HandoffRequest {
106 pub fn to_handoff_prompt(&self) -> String {
113 let mut parts = vec![
114 "## HANDOFF FROM PREVIOUS AGENT".to_string(),
115 String::new(),
116 "Previous agent completed work and is handing off to you.".to_string(),
117 String::new(),
118 format!("### Current State\n{}", self.state_summary),
119 ];
120
121 if !self.boundary_status.is_empty() {
122 let items: Vec<String> = self
123 .boundary_status
124 .iter()
125 .map(|item| {
126 let status_str = match item.status {
127 BoundaryStatus::Done => "DONE",
128 BoundaryStatus::InProgress => "IN PROGRESS",
129 BoundaryStatus::NotStarted => "NOT STARTED",
130 BoundaryStatus::Blocked => "BLOCKED",
131 };
132 match &item.notes {
133 Some(notes) => format!("- [{}] {} -- {}", status_str, item.feature, notes),
134 None => format!("- [{}] {}", status_str, item.feature),
135 }
136 })
137 .collect();
138 parts.push(format!("### Boundary Status\n{}", items.join("\n")));
139 }
140
141 if !self.modified_files.is_empty() {
142 parts.push(format!("### Modified Files\n{}", self.modified_files.join("\n")));
143 }
144
145 if let Some(test_results) = &self.test_results {
146 parts.push(format!("### Test Results\n{test_results}"));
147 }
148
149 if !self.open_decisions.is_empty() {
150 parts.push(format!("### Open Decisions\n{}", self.open_decisions.join("\n")));
151 }
152
153 if !self.known_issues.is_empty() {
154 parts.push(format!(
155 "### Known Issues\n{}",
156 self.known_issues
157 .iter()
158 .map(|i| format!("- {i}"))
159 .collect::<Vec<_>>()
160 .join("\n")
161 ));
162 }
163
164 if !self.next_actions.is_empty() {
165 parts.push(format!(
166 "### Recommended Next Actions\n{}",
167 self.next_actions
168 .iter()
169 .map(|a| format!("1. {a}"))
170 .collect::<Vec<_>>()
171 .join("\n")
172 ));
173 }
174
175 parts.push(format!(
176 "### Task Context\n{}\n\n\
177 Continue from where the previous agent left off. \
178 Read the modified files to understand the current state. \
179 Check boundary status to know what's done vs. what needs work.",
180 self.task_context
181 ));
182
183 parts.join("\n\n")
184 }
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190
191 fn minimal_request() -> HandoffRequest {
192 HandoffRequest {
193 target_agent: "coder".into(),
194 state_summary: "Implemented the login feature".into(),
195 modified_files: vec!["src/auth.rs".into()],
196 open_decisions: vec!["Should we use JWT?".into()],
197 task_context: "Build authentication system".into(),
198 context_files: vec![],
199 test_results: None,
200 boundary_status: vec![],
201 known_issues: vec![],
202 next_actions: vec![],
203 }
204 }
205
206 #[test]
207 fn handoff_request_round_trip() {
208 let request = minimal_request();
209 let json = serde_json::to_string(&request).expect("serialize");
210 let deserialized: HandoffRequest = serde_json::from_str(&json).expect("deserialize");
211
212 assert_eq!(deserialized.target_agent, "coder");
213 assert_eq!(deserialized.state_summary, "Implemented the login feature");
214 }
215
216 #[test]
217 fn handoff_request_round_trip_with_new_fields() {
218 let request = HandoffRequest {
219 target_agent: "verifier".into(),
220 state_summary: "All features implemented".into(),
221 modified_files: vec!["src/main.rs".into()],
222 open_decisions: vec![],
223 task_context: "Review the implementation".into(),
224 context_files: vec![],
225 test_results: Some("12 passed, 0 failed".into()),
226 boundary_status: vec![
227 BoundaryItem {
228 feature: "login".into(),
229 status: BoundaryStatus::Done,
230 notes: Some("JWT auth working".into()),
231 },
232 BoundaryItem {
233 feature: "signup".into(),
234 status: BoundaryStatus::InProgress,
235 notes: None,
236 },
237 ],
238 known_issues: vec!["Rate limiting not implemented".into()],
239 next_actions: vec!["Add rate limiting".into(), "Write integration tests".into()],
240 };
241
242 let json = serde_json::to_string(&request).expect("serialize");
243 let deserialized: HandoffRequest = serde_json::from_str(&json).expect("deserialize");
244
245 assert_eq!(deserialized.test_results.as_deref(), Some("12 passed, 0 failed"));
246 assert_eq!(deserialized.boundary_status.len(), 2);
247 assert_eq!(deserialized.boundary_status[0].status, BoundaryStatus::Done);
248 assert_eq!(deserialized.boundary_status[1].status, BoundaryStatus::InProgress);
249 assert_eq!(deserialized.known_issues.len(), 1);
250 assert_eq!(deserialized.next_actions.len(), 2);
251 }
252
253 #[test]
254 fn handoff_prompt_includes_state() {
255 let request = minimal_request();
256 let prompt = request.to_handoff_prompt();
257 assert!(!prompt.contains("Code is ready for review")); assert!(prompt.contains("src/auth.rs"));
259 assert!(prompt.contains("Build authentication system"));
260 assert!(prompt.contains("HANDOFF FROM PREVIOUS AGENT"));
261 }
262
263 #[test]
264 fn handoff_prompt_includes_boundary_status() {
265 let request = HandoffRequest {
266 target_agent: "verifier".into(),
267 state_summary: "Code ready".into(),
268 modified_files: vec![],
269 open_decisions: vec![],
270 task_context: "Review".into(),
271 context_files: vec![],
272 test_results: None,
273 boundary_status: vec![
274 BoundaryItem {
275 feature: "auth".into(),
276 status: BoundaryStatus::Done,
277 notes: None,
278 },
279 BoundaryItem {
280 feature: "api".into(),
281 status: BoundaryStatus::InProgress,
282 notes: Some("endpoints working, tests pending".into()),
283 },
284 ],
285 known_issues: vec![],
286 next_actions: vec![],
287 };
288
289 let prompt = request.to_handoff_prompt();
290 assert!(prompt.contains("[DONE] auth"));
291 assert!(prompt.contains("[IN PROGRESS] api -- endpoints working, tests pending"));
292 }
293
294 #[test]
295 fn handoff_prompt_includes_test_results() {
296 let request = HandoffRequest {
297 target_agent: "verifier".into(),
298 state_summary: "Done".into(),
299 modified_files: vec![],
300 open_decisions: vec![],
301 task_context: "Review".into(),
302 context_files: vec![],
303 test_results: Some(
304 "15 passed, 2 failed\n FAIL: test_login_rate_limit\n FAIL: test_signup_validation".into(),
305 ),
306 boundary_status: vec![],
307 known_issues: vec!["Rate limiting missing".into()],
308 next_actions: vec!["Fix failing tests".into()],
309 };
310
311 let prompt = request.to_handoff_prompt();
312 assert!(prompt.contains("### Test Results"));
313 assert!(prompt.contains("15 passed, 2 failed"));
314 assert!(prompt.contains("### Known Issues"));
315 assert!(prompt.contains("- Rate limiting missing"));
316 assert!(prompt.contains("### Recommended Next Actions"));
317 assert!(prompt.contains("1. Fix failing tests"));
318 }
319
320 #[test]
321 fn handoff_prompt_omits_empty_sections() {
322 let request = minimal_request();
323 let prompt = request.to_handoff_prompt();
324 assert!(!prompt.contains("### Boundary Status"));
326 assert!(!prompt.contains("### Test Results"));
327 assert!(!prompt.contains("### Known Issues"));
328 assert!(!prompt.contains("### Recommended Next Actions"));
329 assert!(prompt.contains("### Current State"));
331 assert!(prompt.contains("### Modified Files"));
332 assert!(prompt.contains("### Open Decisions"));
333 assert!(prompt.contains("### Task Context"));
334 }
335
336 #[test]
337 fn handoff_receipt_round_trip() {
338 let receipt = HandoffReceipt {
339 accepted: true,
340 continuation_message: "Will review the code".into(),
341 revised_plan: Some("1. Check auth".into()),
342 };
343
344 let json = serde_json::to_string(&receipt).expect("serialize");
345 let deserialized: HandoffReceipt = serde_json::from_str(&json).expect("deserialize");
346
347 assert!(deserialized.accepted);
348 assert_eq!(deserialized.continuation_message, "Will review the code");
349 }
350
351 #[test]
352 fn boundary_status_default_deserialization() {
353 let json = r#"{
355 "target_agent": "coder",
356 "state_summary": "done",
357 "task_context": "build",
358 "modified_files": [],
359 "open_decisions": [],
360 "context_files": []
361 }"#;
362 let request: HandoffRequest = serde_json::from_str(json).expect("deserialize");
363 assert!(request.test_results.is_none());
364 assert!(request.boundary_status.is_empty());
365 assert!(request.known_issues.is_empty());
366 assert!(request.next_actions.is_empty());
367 }
368}