1use anyhow::Result;
10use serde::{Deserialize, Serialize};
11use std::fs;
12use std::path::PathBuf;
13use std::process::Command;
14
15use crate::backpressure::ValidationResult;
16
17pub fn get_current_commit() -> Option<String> {
19 Command::new("git")
20 .args(["rev-parse", "HEAD"])
21 .output()
22 .ok()
23 .and_then(|output| {
24 if output.status.success() {
25 Some(String::from_utf8_lossy(&output.stdout).trim().to_string())
26 } else {
27 None
28 }
29 })
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct WaveSummary {
36 pub wave_number: usize,
38 pub tasks_completed: Vec<String>,
40 pub files_changed: Vec<String>,
42}
43
44impl WaveSummary {
45 pub fn to_text(&self) -> String {
47 let mut lines = Vec::new();
48
49 lines.push(format!(
50 "Wave {} completed {} task(s):",
51 self.wave_number,
52 self.tasks_completed.len()
53 ));
54
55 for task_id in &self.tasks_completed {
56 lines.push(format!(" - {}", task_id));
57 }
58
59 if !self.files_changed.is_empty() {
60 let file_summary = if self.files_changed.len() <= 5 {
61 self.files_changed.join(", ")
62 } else {
63 format!(
64 "{} and {} more",
65 self.files_changed[..5].join(", "),
66 self.files_changed.len() - 5
67 )
68 };
69 lines.push(format!("Files changed: {}", file_summary));
70 }
71
72 lines.join("\n")
73 }
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct RoundState {
79 pub round_number: usize,
81 pub task_ids: Vec<String>,
83 pub tags: Vec<String>,
85 pub failures: Vec<String>,
87 pub started_at: String,
89 pub completed_at: Option<String>,
91}
92
93impl RoundState {
94 pub fn new(round_number: usize) -> Self {
95 Self {
96 round_number,
97 task_ids: Vec::new(),
98 tags: Vec::new(),
99 failures: Vec::new(),
100 started_at: chrono::Utc::now().to_rfc3339(),
101 completed_at: None,
102 }
103 }
104
105 pub fn mark_complete(&mut self) {
106 self.completed_at = Some(chrono::Utc::now().to_rfc3339());
107 }
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct ReviewState {
113 pub reviewed_tasks: Vec<String>,
115 pub all_passed: bool,
117 pub tasks_needing_improvement: Vec<String>,
119 pub completed_at: String,
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct RepairAttempt {
126 pub attempt_number: usize,
128 pub attributed_tasks: Vec<String>,
130 pub cleared_tasks: Vec<String>,
132 pub attribution_confidence: String,
134 pub validation_passed: bool,
136 pub completed_at: String,
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct WaveState {
143 pub wave_number: usize,
145 pub rounds: Vec<RoundState>,
147 pub validation: Option<ValidationResult>,
149 pub summary: Option<WaveSummary>,
151 #[serde(default)]
153 pub start_commit: Option<String>,
154 #[serde(default)]
156 pub review: Option<ReviewState>,
157 #[serde(default)]
159 pub repairs: Vec<RepairAttempt>,
160 pub started_at: String,
162 pub completed_at: Option<String>,
164}
165
166impl WaveState {
167 pub fn new(wave_number: usize) -> Self {
168 Self {
169 wave_number,
170 rounds: Vec::new(),
171 validation: None,
172 summary: None,
173 start_commit: get_current_commit(),
174 review: None,
175 repairs: Vec::new(),
176 started_at: chrono::Utc::now().to_rfc3339(),
177 completed_at: None,
178 }
179 }
180
181 pub fn mark_complete(&mut self) {
182 self.completed_at = Some(chrono::Utc::now().to_rfc3339());
183 }
184
185 pub fn all_task_ids(&self) -> Vec<String> {
187 self.rounds
188 .iter()
189 .flat_map(|r| r.task_ids.clone())
190 .collect()
191 }
192
193 pub fn task_tags(&self) -> Vec<(String, String)> {
195 self.rounds
196 .iter()
197 .flat_map(|r| {
198 r.task_ids
199 .iter()
200 .zip(r.tags.iter())
201 .map(|(id, tag)| (id.clone(), tag.clone()))
202 })
203 .collect()
204 }
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize)]
209pub struct SwarmSession {
210 pub session_name: String,
212 pub tag: String,
214 pub terminal: String,
216 pub working_dir: String,
218 pub round_size: usize,
220 pub waves: Vec<WaveState>,
222 pub started_at: String,
224 pub completed_at: Option<String>,
226}
227
228impl SwarmSession {
229 pub fn new(
230 session_name: &str,
231 tag: &str,
232 terminal: &str,
233 working_dir: &str,
234 round_size: usize,
235 ) -> Self {
236 Self {
237 session_name: session_name.to_string(),
238 tag: tag.to_string(),
239 terminal: terminal.to_string(),
240 working_dir: working_dir.to_string(),
241 round_size,
242 waves: Vec::new(),
243 started_at: chrono::Utc::now().to_rfc3339(),
244 completed_at: None,
245 }
246 }
247
248 pub fn mark_complete(&mut self) {
249 self.completed_at = Some(chrono::Utc::now().to_rfc3339());
250 }
251
252 pub fn total_tasks(&self) -> usize {
254 self.waves
255 .iter()
256 .flat_map(|w| &w.rounds)
257 .map(|r| r.task_ids.len())
258 .sum()
259 }
260
261 pub fn total_failures(&self) -> usize {
263 self.waves
264 .iter()
265 .flat_map(|w| &w.rounds)
266 .map(|r| r.failures.len())
267 .sum()
268 }
269
270 pub fn get_previous_summary(&self) -> Option<String> {
273 self.waves
274 .last()
275 .and_then(|w| w.summary.as_ref().map(|s| s.to_text()))
276 }
277}
278
279pub fn swarm_dir(project_root: Option<&PathBuf>) -> PathBuf {
281 let root = project_root
282 .cloned()
283 .unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
284 root.join(".scud").join("swarm")
285}
286
287pub fn lock_file_path(project_root: Option<&PathBuf>, tag: &str) -> PathBuf {
291 let root = project_root
292 .cloned()
293 .unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
294 let worktree_id = get_worktree_id(&root);
295 let lock_name = match worktree_id {
296 Some(wt_id) => format!("{}-{}.lock", tag, wt_id),
297 None => format!("{}.lock", tag),
298 };
299 swarm_dir(project_root).join(lock_name)
300}
301
302fn get_worktree_id(project_root: &std::path::Path) -> Option<String> {
305 let git_path = project_root.join(".git");
306 if git_path.is_file() {
307 project_root
309 .file_name()
310 .and_then(|n| n.to_str())
311 .map(|s| s.to_string())
312 } else {
313 None
314 }
315}
316
317pub struct SessionLock {
320 _file: fs::File,
321 path: PathBuf,
322}
323
324impl SessionLock {
325 pub fn path(&self) -> &PathBuf {
327 &self.path
328 }
329}
330
331impl Drop for SessionLock {
332 fn drop(&mut self) {
333 let _ = fs::remove_file(&self.path);
336 }
337}
338
339pub fn acquire_session_lock(project_root: Option<&PathBuf>, tag: &str) -> Result<SessionLock> {
343 use fs2::FileExt;
344
345 let dir = swarm_dir(project_root);
346 fs::create_dir_all(&dir)?;
347
348 let lock_path = lock_file_path(project_root, tag);
349 let file = fs::OpenOptions::new()
350 .write(true)
351 .create(true)
352 .truncate(true)
353 .open(&lock_path)?;
354
355 file.try_lock_exclusive().map_err(|_| {
357 anyhow::anyhow!(
358 "Another swarm session is already running for tag '{}'. \
359 If this is incorrect, remove the lock file: {}",
360 tag,
361 lock_path.display()
362 )
363 })?;
364
365 use std::io::Write;
367 let mut file = file;
368 writeln!(
369 file,
370 "pid={}\nstarted={}",
371 std::process::id(),
372 chrono::Utc::now().to_rfc3339()
373 )?;
374
375 Ok(SessionLock {
376 _file: file,
377 path: lock_path,
378 })
379}
380
381pub fn session_file(project_root: Option<&PathBuf>, session_name: &str) -> PathBuf {
383 swarm_dir(project_root).join(format!("{}.json", session_name))
384}
385
386pub fn save_session(project_root: Option<&PathBuf>, session: &SwarmSession) -> Result<()> {
388 let dir = swarm_dir(project_root);
389 fs::create_dir_all(&dir)?;
390
391 let file = session_file(project_root, &session.session_name);
392 let json = serde_json::to_string_pretty(session)?;
393 fs::write(file, json)?;
394
395 Ok(())
396}
397
398pub fn load_session(project_root: Option<&PathBuf>, session_name: &str) -> Result<SwarmSession> {
400 let file = session_file(project_root, session_name);
401 let json = fs::read_to_string(&file)?;
402 let session: SwarmSession = serde_json::from_str(&json)?;
403 Ok(session)
404}
405
406pub fn list_sessions(project_root: Option<&PathBuf>) -> Result<Vec<String>> {
408 let dir = swarm_dir(project_root);
409 if !dir.exists() {
410 return Ok(Vec::new());
411 }
412
413 let mut sessions = Vec::new();
414 for entry in fs::read_dir(dir)? {
415 let entry = entry?;
416 let path = entry.path();
417 if path.extension().map(|e| e == "json").unwrap_or(false) {
418 if let Some(stem) = path.file_stem() {
419 sessions.push(stem.to_string_lossy().to_string());
420 }
421 }
422 }
423
424 Ok(sessions)
425}
426
427use std::path::Path;
432use tokio::sync::mpsc;
433
434use crate::commands::spawn::agent::generate_prompt;
435use crate::commands::spawn::terminal::Harness;
436use crate::extensions::runner::{load_agent_config, AgentEvent, AgentRunner, SpawnConfig};
437use crate::models::task::Task;
438
439#[derive(Debug, Clone)]
441pub struct WaveAgent {
442 pub task: Task,
444 pub tag: String,
446}
447
448impl WaveAgent {
449 pub fn new(task: Task, tag: impl Into<String>) -> Self {
451 Self {
452 task,
453 tag: tag.into(),
454 }
455 }
456
457 pub fn from_task_pairs<I>(pairs: I) -> Vec<Self>
462 where
463 I: IntoIterator<Item = (Task, String)>,
464 {
465 pairs
466 .into_iter()
467 .map(|(task, tag)| Self::new(task, tag))
468 .collect()
469 }
470
471 pub fn task_id(&self) -> &str {
473 &self.task.id
474 }
475}
476
477#[derive(Debug, Clone)]
479pub struct WaveExecutionResult {
480 pub round_state: RoundState,
482 pub agent_results: Vec<crate::extensions::runner::AgentResult>,
484}
485
486impl WaveExecutionResult {
487 pub fn all_succeeded(&self) -> bool {
489 self.agent_results.iter().all(|r| r.success)
490 }
491
492 pub fn successful_task_ids(&self) -> Vec<String> {
494 self.agent_results
495 .iter()
496 .filter(|r| r.success)
497 .map(|r| r.task_id.clone())
498 .collect()
499 }
500
501 pub fn failed_task_ids(&self) -> Vec<String> {
503 self.agent_results
504 .iter()
505 .filter(|r| !r.success)
506 .map(|r| r.task_id.clone())
507 .collect()
508 }
509
510 pub fn total_duration_ms(&self) -> u64 {
512 self.agent_results
513 .iter()
514 .map(|r| r.duration_ms)
515 .max()
516 .unwrap_or(0)
517 }
518}
519
520impl WaveState {
521 pub fn apply_execution_result(&mut self, result: WaveExecutionResult) {
526 self.rounds.push(result.round_state);
527 }
528
529 pub fn from_execution_result(wave_number: usize, result: WaveExecutionResult) -> Self {
531 let mut state = Self::new(wave_number);
532 state.apply_execution_result(result);
533 state
534 }
535}
536
537pub async fn execute_wave_async(
552 agents: &[WaveAgent],
553 working_dir: &Path,
554 round_number: usize,
555 default_harness: Harness,
556) -> Result<WaveExecutionResult> {
557 let mut round_state = RoundState::new(round_number);
558 let mut runner = AgentRunner::new(agents.len() * 10);
559
560 for agent in agents {
562 let (harness, model) = load_agent_config(
564 agent.task.agent_type.as_deref(),
565 default_harness,
566 None,
567 working_dir,
568 );
569
570 let prompt = generate_prompt(&agent.task, &agent.tag);
572
573 let config = SpawnConfig {
574 task_id: agent.task.id.clone(),
575 prompt,
576 working_dir: working_dir.to_path_buf(),
577 harness,
578 model,
579 };
580
581 match runner.spawn(config).await {
582 Ok(()) => {
583 round_state.task_ids.push(agent.task.id.clone());
584 round_state.tags.push(agent.tag.clone());
585 }
586 Err(e) => {
587 round_state.failures.push(agent.task.id.clone());
588 eprintln!("Failed to spawn agent for {}: {}", agent.task.id, e);
589 }
590 }
591 }
592
593 let agent_results = runner.wait_all().await;
595
596 round_state.mark_complete();
597
598 Ok(WaveExecutionResult {
599 round_state,
600 agent_results,
601 })
602}
603
604pub async fn execute_wave_with_events(
618 agents: &[WaveAgent],
619 working_dir: &Path,
620 round_number: usize,
621 default_harness: Harness,
622 event_tx: mpsc::Sender<AgentEvent>,
623) -> Result<WaveExecutionResult> {
624 use crate::extensions::runner::spawn_agent;
625
626 let mut round_state = RoundState::new(round_number);
627 let mut handles = Vec::new();
628
629 for agent in agents {
631 let (harness, model) = load_agent_config(
633 agent.task.agent_type.as_deref(),
634 default_harness,
635 None,
636 working_dir,
637 );
638
639 let prompt = generate_prompt(&agent.task, &agent.tag);
641
642 let config = SpawnConfig {
643 task_id: agent.task.id.clone(),
644 prompt,
645 working_dir: working_dir.to_path_buf(),
646 harness,
647 model,
648 };
649
650 match spawn_agent(config, event_tx.clone()).await {
651 Ok(handle) => {
652 handles.push(handle);
653 round_state.task_ids.push(agent.task.id.clone());
654 round_state.tags.push(agent.tag.clone());
655 }
656 Err(e) => {
657 round_state.failures.push(agent.task.id.clone());
658 let _ = event_tx
659 .send(AgentEvent::SpawnFailed {
660 task_id: agent.task.id.clone(),
661 error: e.to_string(),
662 })
663 .await;
664 }
665 }
666 }
667
668 let mut agent_results = Vec::new();
670 for handle in handles {
671 if let Ok(result) = handle.await {
672 agent_results.push(result);
673 }
674 }
675
676 round_state.mark_complete();
677
678 Ok(WaveExecutionResult {
679 round_state,
680 agent_results,
681 })
682}
683
684pub async fn execute_wave_with_tracking<I>(
701 wave_number: usize,
702 task_pairs: I,
703 working_dir: &Path,
704 default_harness: Harness,
705) -> Result<(WaveState, Vec<crate::extensions::runner::AgentResult>)>
706where
707 I: IntoIterator<Item = (Task, String)>,
708{
709 let agents = WaveAgent::from_task_pairs(task_pairs);
710 let result = execute_wave_async(&agents, working_dir, 0, default_harness).await?;
711
712 let wave_state = WaveState::from_execution_result(wave_number, result.clone());
713
714 Ok((wave_state, result.agent_results))
715}
716
717pub async fn execute_wave_in_rounds<I>(
732 wave_number: usize,
733 task_pairs: I,
734 working_dir: &Path,
735 round_size: usize,
736 default_harness: Harness,
737) -> Result<WaveState>
738where
739 I: IntoIterator<Item = (Task, String)>,
740{
741 let agents: Vec<WaveAgent> = WaveAgent::from_task_pairs(task_pairs);
742 let mut wave_state = WaveState::new(wave_number);
743
744 for (round_idx, chunk) in agents.chunks(round_size).enumerate() {
745 let result = execute_wave_async(chunk, working_dir, round_idx, default_harness).await?;
746 wave_state.apply_execution_result(result);
747 }
748
749 wave_state.mark_complete();
750 Ok(wave_state)
751}
752
753pub async fn spawn_subagent(
757 task: &Task,
758 tag: &str,
759 working_dir: &Path,
760 default_harness: Harness,
761) -> Result<crate::extensions::runner::AgentResult> {
762 let agents = vec![WaveAgent {
763 task: task.clone(),
764 tag: tag.to_string(),
765 }];
766
767 let result = execute_wave_async(&agents, working_dir, 0, default_harness).await?;
768
769 result
770 .agent_results
771 .into_iter()
772 .next()
773 .ok_or_else(|| anyhow::anyhow!("No result from agent"))
774}
775
776#[cfg(test)]
777mod tests {
778 use super::*;
779
780 #[test]
781 fn test_round_state_new() {
782 let round = RoundState::new(0);
783 assert_eq!(round.round_number, 0);
784 assert!(round.task_ids.is_empty());
785 assert!(round.completed_at.is_none());
786 }
787
788 #[test]
789 fn test_wave_state_all_task_ids() {
790 let mut wave = WaveState::new(1);
791
792 let mut round1 = RoundState::new(0);
793 round1.task_ids = vec!["task:1".to_string(), "task:2".to_string()];
794
795 let mut round2 = RoundState::new(1);
796 round2.task_ids = vec!["task:3".to_string()];
797
798 wave.rounds.push(round1);
799 wave.rounds.push(round2);
800
801 let all_ids = wave.all_task_ids();
802 assert_eq!(all_ids.len(), 3);
803 assert!(all_ids.contains(&"task:1".to_string()));
804 assert!(all_ids.contains(&"task:2".to_string()));
805 assert!(all_ids.contains(&"task:3".to_string()));
806 }
807
808 #[test]
809 fn test_swarm_session_total_tasks() {
810 let mut session = SwarmSession::new("test-session", "test-tag", "tmux", "/test/path", 5);
811
812 let mut wave = WaveState::new(1);
813 let mut round = RoundState::new(0);
814 round.task_ids = vec!["task:1".to_string(), "task:2".to_string()];
815 wave.rounds.push(round);
816 session.waves.push(wave);
817
818 assert_eq!(session.total_tasks(), 2);
819 }
820
821 #[test]
822 fn test_wave_summary_to_text() {
823 let summary = WaveSummary {
824 wave_number: 1,
825 tasks_completed: vec!["task:1".to_string(), "task:2".to_string()],
826 files_changed: vec!["src/main.rs".to_string()],
827 };
828
829 let text = summary.to_text();
830 assert!(text.contains("Wave 1"));
831 assert!(text.contains("task:1"));
832 assert!(text.contains("src/main.rs"));
833 }
834
835 #[test]
836 fn test_get_previous_summary() {
837 let mut session = SwarmSession::new("test", "tag", "tmux", "/path", 5);
838
839 assert!(session.get_previous_summary().is_none());
841
842 let mut wave = WaveState::new(1);
844 wave.summary = Some(WaveSummary {
845 wave_number: 1,
846 tasks_completed: vec!["task:1".to_string()],
847 files_changed: vec![],
848 });
849 session.waves.push(wave);
850
851 let summary = session.get_previous_summary();
852 assert!(summary.is_some());
853 assert!(summary.unwrap().contains("task:1"));
854 }
855
856 #[test]
857 fn test_session_lock_contention() {
858 use tempfile::TempDir;
859
860 let temp_dir = TempDir::new().unwrap();
862 let project_root = temp_dir.path().to_path_buf();
863
864 let _lock1 = acquire_session_lock(Some(&project_root), "test-tag")
866 .expect("First lock should succeed");
867
868 let result = acquire_session_lock(Some(&project_root), "test-tag");
870
871 match result {
873 Ok(_) => panic!("Second lock should fail"),
874 Err(e) => {
875 let error_msg = e.to_string();
876 assert!(
877 error_msg.contains("already running"),
878 "Error message should mention 'already running', got: {}",
879 error_msg
880 );
881 }
882 }
883 }
884
885 #[test]
886 fn test_get_current_commit() {
887 let result = get_current_commit();
888
889 assert!(result.is_some(), "Expected Some(sha) in a git repository");
891
892 let sha = result.unwrap();
893
894 assert_eq!(
896 sha.len(),
897 40,
898 "Expected SHA to be 40 characters long, got {}",
899 sha.len()
900 );
901
902 assert!(
904 sha.chars().all(|c| c.is_ascii_hexdigit()),
905 "Expected SHA to contain only hex characters, got: {}",
906 sha
907 );
908 }
909
910 #[test]
911 fn test_wave_agent_new() {
912 let task = Task::new(
913 "task:1".to_string(),
914 "Test task".to_string(),
915 "Description".to_string(),
916 );
917 let agent = WaveAgent::new(task.clone(), "test-tag");
918
919 assert_eq!(agent.task_id(), "task:1");
920 assert_eq!(agent.tag, "test-tag");
921 }
922
923 #[test]
924 fn test_wave_agent_from_task_pairs() {
925 let task1 = Task::new(
926 "task:1".to_string(),
927 "Task 1".to_string(),
928 "Description".to_string(),
929 );
930 let task2 = Task::new(
931 "task:2".to_string(),
932 "Task 2".to_string(),
933 "Description".to_string(),
934 );
935
936 let pairs = vec![(task1, "tag-a".to_string()), (task2, "tag-b".to_string())];
937
938 let agents = WaveAgent::from_task_pairs(pairs);
939
940 assert_eq!(agents.len(), 2);
941 assert_eq!(agents[0].task_id(), "task:1");
942 assert_eq!(agents[0].tag, "tag-a");
943 assert_eq!(agents[1].task_id(), "task:2");
944 assert_eq!(agents[1].tag, "tag-b");
945 }
946
947 #[test]
948 fn test_wave_execution_result_helpers() {
949 use crate::extensions::runner::AgentResult;
950
951 let result = WaveExecutionResult {
952 round_state: RoundState::new(0),
953 agent_results: vec![
954 AgentResult {
955 task_id: "task:1".to_string(),
956 success: true,
957 exit_code: Some(0),
958 output: String::new(),
959 duration_ms: 1000,
960 },
961 AgentResult {
962 task_id: "task:2".to_string(),
963 success: false,
964 exit_code: Some(1),
965 output: String::new(),
966 duration_ms: 2000,
967 },
968 ],
969 };
970
971 assert!(!result.all_succeeded());
972 assert_eq!(result.successful_task_ids(), vec!["task:1"]);
973 assert_eq!(result.failed_task_ids(), vec!["task:2"]);
974 assert_eq!(result.total_duration_ms(), 2000);
975 }
976
977 #[test]
978 fn test_wave_state_from_execution_result() {
979 use crate::extensions::runner::AgentResult;
980
981 let mut round_state = RoundState::new(0);
982 round_state.task_ids = vec!["task:1".to_string()];
983
984 let result = WaveExecutionResult {
985 round_state,
986 agent_results: vec![AgentResult {
987 task_id: "task:1".to_string(),
988 success: true,
989 exit_code: Some(0),
990 output: String::new(),
991 duration_ms: 1000,
992 }],
993 };
994
995 let wave_state = WaveState::from_execution_result(1, result);
996
997 assert_eq!(wave_state.wave_number, 1);
998 assert_eq!(wave_state.rounds.len(), 1);
999 assert_eq!(wave_state.rounds[0].task_ids, vec!["task:1"]);
1000 }
1001
1002 #[test]
1003 fn test_wave_state_apply_execution_result() {
1004 use crate::extensions::runner::AgentResult;
1005
1006 let mut wave_state = WaveState::new(1);
1007 assert!(wave_state.rounds.is_empty());
1008
1009 let mut round_state = RoundState::new(0);
1010 round_state.task_ids = vec!["task:1".to_string()];
1011
1012 let result = WaveExecutionResult {
1013 round_state,
1014 agent_results: vec![AgentResult {
1015 task_id: "task:1".to_string(),
1016 success: true,
1017 exit_code: Some(0),
1018 output: String::new(),
1019 duration_ms: 1000,
1020 }],
1021 };
1022
1023 wave_state.apply_execution_result(result);
1024
1025 assert_eq!(wave_state.rounds.len(), 1);
1026 assert_eq!(wave_state.all_task_ids(), vec!["task:1"]);
1027 }
1028}