ralph_workflow/phases/commit_logging/file_logging.rs
1// File-based logging operations - writing to log files, file management.
2// This file is included via include!() macro from the parent commit_logging.rs module.
3
4/// Session tracker for commit generation logging.
5///
6/// Manages a unique run directory for a commit generation session,
7/// ensuring log files are organized and don't overwrite each other.
8#[derive(Debug)]
9pub struct CommitLogSession {
10 /// Base log directory
11 run_dir: PathBuf,
12 /// Current attempt counter
13 attempt_counter: usize,
14}
15
16impl CommitLogSession {
17 /// Create a new logging session using workspace abstraction.
18 ///
19 /// Creates a unique run directory under the base log path.
20 ///
21 /// # Arguments
22 ///
23 /// * `base_log_dir` - Base directory for commit logs (e.g., `.agent/logs/commit_generation`)
24 /// * `workspace` - The workspace to use for filesystem operations
25 ///
26 /// # Errors
27 ///
28 /// Returns error if the operation fails.
29 pub fn new(base_log_dir: &str, workspace: &dyn Workspace) -> std::io::Result<Self> {
30 let timestamp = Local::now().format("%Y%m%d_%H%M%S");
31 let run_dir = PathBuf::from(base_log_dir).join(format!("run_{timestamp}"));
32 workspace.create_dir_all(&run_dir)?;
33
34 Ok(Self {
35 run_dir,
36 attempt_counter: 0,
37 })
38 }
39
40 /// Create a no-op logging session that discards all writes.
41 ///
42 /// This is used as a fallback when all log directories fail to be created.
43 /// The session will still track attempt numbers and provide a dummy `run_dir`,
44 /// but writes will silently succeed without actually writing anything.
45 ///
46 /// # Returns
47 ///
48 /// A `CommitLogSession` that uses `/dev/null` equivalent as its run directory.
49 #[must_use]
50 pub fn noop() -> Self {
51 // Use a path that indicates this is a noop session
52 // The path won't be created or written to by noop session
53 Self {
54 run_dir: PathBuf::from("/dev/null/ralph-noop-session"),
55 attempt_counter: 0,
56 }
57 }
58
59 /// Check if this is a no-op session.
60 #[must_use]
61 pub fn is_noop(&self) -> bool {
62 self.run_dir.starts_with("/dev/null")
63 }
64
65 /// Get the path to the run directory.
66 #[must_use]
67 pub fn run_dir(&self) -> &Path {
68 &self.run_dir
69 }
70
71 /// Get the next attempt number and increment the counter.
72 pub const fn next_attempt_number(&mut self) -> usize {
73 self.attempt_counter += 1;
74 self.attempt_counter
75 }
76
77 /// Create a new attempt log for this session.
78 ///
79 /// # Arguments
80 ///
81 /// * `agent` - The agent being used
82 /// * `strategy` - The retry strategy being used
83 pub fn new_attempt(&mut self, agent: &str, strategy: &str) -> CommitAttemptLog {
84 let attempt_number = self.next_attempt_number();
85 CommitAttemptLog::new(attempt_number, agent, strategy)
86 }
87
88 /// Write summary file at end of session.
89 ///
90 /// For noop sessions, this silently succeeds without writing anything.
91 ///
92 /// # Arguments
93 ///
94 /// * `total_attempts` - Total number of attempts made
95 /// * `final_outcome` - Description of the final outcome
96 /// * `workspace` - The workspace to use for filesystem operations
97 ///
98 /// # Errors
99 ///
100 /// Returns error if the operation fails.
101 pub fn write_summary(
102 &self,
103 total_attempts: usize,
104 final_outcome: &str,
105 workspace: &dyn Workspace,
106 ) -> std::io::Result<()> {
107 use std::fmt::Write;
108
109 // Skip writing for noop sessions
110 if self.is_noop() {
111 return Ok(());
112 }
113
114 let summary_path = self.run_dir.join("SUMMARY.txt");
115 let mut content = String::new();
116
117 let _ = writeln!(content, "COMMIT GENERATION SESSION SUMMARY");
118 let _ = writeln!(content, "=================================");
119 let _ = writeln!(content);
120 let _ = writeln!(content, "Run directory: {}", self.run_dir.display());
121 let _ = writeln!(content, "Total attempts: {total_attempts}");
122 let _ = writeln!(content, "Final outcome: {final_outcome}");
123 let _ = writeln!(content);
124 let _ = writeln!(content, "Individual attempt logs are in this directory.");
125
126 workspace.write(&summary_path, &content)?;
127 Ok(())
128 }
129}