ralph_workflow/files/io/
agent_files.rs1use std::fs;
7use std::io::{self, BufRead};
8use std::path::Path;
9
10use crate::workspace::Workspace;
11
12use super::{
13 context::overwrite_one_liner, context::VAGUE_ISSUES_LINE, context::VAGUE_NOTES_LINE,
14 context::VAGUE_STATUS_LINE, integrity, recovery,
15};
16
17const PLAN_XSD_SCHEMA: &str = include_str!("../llm_output_extraction/plan.xsd");
20const DEVELOPMENT_RESULT_XSD_SCHEMA: &str =
21 include_str!("../llm_output_extraction/development_result.xsd");
22const ISSUES_XSD_SCHEMA: &str = include_str!("../llm_output_extraction/issues.xsd");
23const FIX_RESULT_XSD_SCHEMA: &str = include_str!("../llm_output_extraction/fix_result.xsd");
24const COMMIT_MESSAGE_XSD_SCHEMA: &str = include_str!("../llm_output_extraction/commit_message.xsd");
25
26pub const GENERATED_FILES: &[&str] = &[
28 ".no_agent_commit",
29 ".agent/PLAN.md",
30 ".agent/commit-message.txt",
31 ".agent/checkpoint.json.tmp",
32];
33
34pub fn file_contains_marker(file_path: &Path, marker: &str) -> io::Result<bool> {
48 if !file_path.exists() {
49 return Ok(false);
50 }
51
52 let file = fs::File::open(file_path)?;
53 let reader = io::BufReader::new(file);
54
55 for line in reader.lines().map_while(Result::ok) {
56 if line.contains(marker) {
57 return Ok(true);
58 }
59 }
60
61 Ok(false)
62}
63
64pub fn ensure_files(isolation_mode: bool) -> io::Result<()> {
75 ensure_files_at(Path::new("."), isolation_mode)
76}
77
78pub fn ensure_files_at(repo_root: &Path, isolation_mode: bool) -> io::Result<()> {
91 let agent_dir = repo_root.join(".agent");
92
93 if let recovery::RecoveryStatus::Unrecoverable(msg) = recovery::auto_repair(&agent_dir)? {
96 return Err(io::Error::other(format!(
97 "Failed to repair .agent state: {msg}"
98 )));
99 }
100
101 integrity::check_filesystem_ready(&agent_dir)?;
102 fs::create_dir_all(agent_dir.join("logs"))?;
103 fs::create_dir_all(agent_dir.join("tmp"))?;
104
105 let tmp_dir = agent_dir.join("tmp");
108 let _ = integrity::cleanup_stale_xml_files(&tmp_dir, false);
109 setup_xsd_schemas_at(repo_root)?;
113
114 if !isolation_mode {
116 overwrite_one_liner(&agent_dir.join("STATUS.md"), VAGUE_STATUS_LINE)?;
119 overwrite_one_liner(&agent_dir.join("NOTES.md"), VAGUE_NOTES_LINE)?;
120 overwrite_one_liner(&agent_dir.join("ISSUES.md"), VAGUE_ISSUES_LINE)?;
121 }
122
123 Ok(())
124}
125
126pub fn setup_xsd_schemas() -> io::Result<()> {
143 setup_xsd_schemas_at(Path::new("."))
144}
145
146pub fn setup_xsd_schemas_at(repo_root: &Path) -> io::Result<()> {
152 let tmp_dir = repo_root.join(".agent/tmp");
153 fs::create_dir_all(&tmp_dir)?;
154
155 fs::write(tmp_dir.join("plan.xsd"), PLAN_XSD_SCHEMA)?;
156 fs::write(
157 tmp_dir.join("development_result.xsd"),
158 DEVELOPMENT_RESULT_XSD_SCHEMA,
159 )?;
160 fs::write(tmp_dir.join("issues.xsd"), ISSUES_XSD_SCHEMA)?;
161 fs::write(tmp_dir.join("fix_result.xsd"), FIX_RESULT_XSD_SCHEMA)?;
162 fs::write(
163 tmp_dir.join("commit_message.xsd"),
164 COMMIT_MESSAGE_XSD_SCHEMA,
165 )?;
166
167 Ok(())
168}
169
170pub fn ensure_files_with_workspace(
179 workspace: &dyn Workspace,
180 isolation_mode: bool,
181) -> io::Result<()> {
182 let agent_dir = Path::new(".agent");
183
184 if let recovery::RecoveryStatus::Unrecoverable(msg) =
187 recovery::auto_repair_with_workspace(workspace, agent_dir)?
188 {
189 return Err(io::Error::other(format!(
190 "Failed to repair .agent state: {msg}"
191 )));
192 }
193
194 integrity::check_filesystem_ready_with_workspace(workspace, agent_dir)?;
195 workspace.create_dir_all(&agent_dir.join("logs"))?;
196 workspace.create_dir_all(&agent_dir.join("tmp"))?;
197
198 let tmp_dir = agent_dir.join("tmp");
200 let _ = integrity::cleanup_stale_xml_files_with_workspace(workspace, &tmp_dir, false);
201 setup_xsd_schemas_with_workspace(workspace)?;
205
206 if !isolation_mode {
208 workspace.write_atomic(
211 &agent_dir.join("STATUS.md"),
212 &format!("{VAGUE_STATUS_LINE}\n"),
213 )?;
214 workspace.write_atomic(
215 &agent_dir.join("NOTES.md"),
216 &format!("{VAGUE_NOTES_LINE}\n"),
217 )?;
218 workspace.write_atomic(
219 &agent_dir.join("ISSUES.md"),
220 &format!("{VAGUE_ISSUES_LINE}\n"),
221 )?;
222 }
223
224 Ok(())
225}
226
227pub fn delete_plan_file() -> io::Result<()> {
235 delete_plan_file_at(Path::new("."))
236}
237
238pub fn delete_plan_file_at(repo_root: &Path) -> io::Result<()> {
244 let plan_path = repo_root.join(".agent/PLAN.md");
245 if plan_path.exists() {
246 fs::remove_file(plan_path)?;
247 }
248 Ok(())
249}
250
251pub fn delete_commit_message_file() -> io::Result<()> {
259 delete_commit_message_file_at(Path::new("."))
260}
261
262pub fn delete_commit_message_file_at(repo_root: &Path) -> io::Result<()> {
268 let msg_path = repo_root.join(".agent/commit-message.txt");
269 if msg_path.exists() {
270 fs::remove_file(msg_path)?;
271 }
272 Ok(())
273}
274
275pub fn read_commit_message_file() -> io::Result<String> {
284 read_commit_message_file_at(Path::new("."))
285}
286
287pub fn read_commit_message_file_at(repo_root: &Path) -> io::Result<String> {
297 let msg_path = repo_root.join(".agent/commit-message.txt");
298 if msg_path.exists() && !integrity::verify_file_not_corrupted(&msg_path)? {
299 return Err(io::Error::new(
300 io::ErrorKind::InvalidData,
301 ".agent/commit-message.txt appears corrupted",
302 ));
303 }
304 let content = fs::read_to_string(&msg_path).map_err(|e| {
305 io::Error::new(
306 e.kind(),
307 format!("Failed to read .agent/commit-message.txt: {e}"),
308 )
309 })?;
310 let trimmed = content.trim();
311 if trimmed.is_empty() {
312 return Err(io::Error::new(
313 io::ErrorKind::InvalidData,
314 ".agent/commit-message.txt is empty",
315 ));
316 }
317 Ok(trimmed.to_string())
318}
319
320pub fn write_commit_message_file(message: &str) -> io::Result<()> {
336 write_commit_message_file_at(Path::new("."), message)
337}
338
339pub fn write_commit_message_file_at(repo_root: &Path, message: &str) -> io::Result<()> {
353 let msg_path = repo_root.join(".agent/commit-message.txt");
354 if let Some(parent) = msg_path.parent() {
355 fs::create_dir_all(parent)?;
356 }
357 integrity::write_file_atomic(&msg_path, message)?;
358 Ok(())
359}
360
361pub fn cleanup_generated_files() {
373 cleanup_generated_files_at(Path::new("."))
374}
375
376pub fn cleanup_generated_files_at(repo_root: &Path) {
389 for file in GENERATED_FILES {
390 let _ = fs::remove_file(repo_root.join(file));
391 }
392}
393
394pub fn file_contains_marker_with_workspace(
410 workspace: &dyn Workspace,
411 path: &Path,
412 marker: &str,
413) -> io::Result<bool> {
414 if !workspace.exists(path) {
415 return Ok(false);
416 }
417
418 let content = workspace.read(path)?;
419 for line in content.lines() {
420 if line.contains(marker) {
421 return Ok(true);
422 }
423 }
424
425 Ok(false)
426}
427
428pub fn delete_plan_file_with_workspace(workspace: &dyn Workspace) -> io::Result<()> {
432 let plan_path = Path::new(".agent/PLAN.md");
433 if workspace.exists(plan_path) {
434 workspace.remove(plan_path)?;
435 }
436 Ok(())
437}
438
439pub fn delete_commit_message_file_with_workspace(workspace: &dyn Workspace) -> io::Result<()> {
443 let msg_path = Path::new(".agent/commit-message.txt");
444 if workspace.exists(msg_path) {
445 workspace.remove(msg_path)?;
446 }
447 Ok(())
448}
449
450pub fn read_commit_message_file_with_workspace(workspace: &dyn Workspace) -> io::Result<String> {
458 let msg_path = Path::new(".agent/commit-message.txt");
459
460 if workspace.exists(msg_path) {
461 if !super::integrity::verify_file_not_corrupted_with_workspace(workspace, msg_path)? {
463 return Err(io::Error::new(
464 io::ErrorKind::InvalidData,
465 ".agent/commit-message.txt appears corrupted",
466 ));
467 }
468 }
469
470 let content = workspace.read(msg_path).map_err(|e| {
471 io::Error::new(
472 e.kind(),
473 format!("Failed to read .agent/commit-message.txt: {e}"),
474 )
475 })?;
476
477 let trimmed = content.trim();
478 if trimmed.is_empty() {
479 return Err(io::Error::new(
480 io::ErrorKind::InvalidData,
481 ".agent/commit-message.txt is empty",
482 ));
483 }
484 Ok(trimmed.to_string())
485}
486
487pub fn write_commit_message_file_with_workspace(
494 workspace: &dyn Workspace,
495 message: &str,
496) -> io::Result<()> {
497 let msg_path = Path::new(".agent/commit-message.txt");
498 workspace.write_atomic(msg_path, message)
499}
500
501pub fn cleanup_generated_files_with_workspace(workspace: &dyn Workspace) {
505 for file in GENERATED_FILES {
506 let _ = workspace.remove(Path::new(file));
507 }
508}
509
510pub fn setup_xsd_schemas_with_workspace(workspace: &dyn Workspace) -> io::Result<()> {
514 let tmp_dir = Path::new(".agent/tmp");
515 workspace.create_dir_all(tmp_dir)?;
516
517 workspace.write(&tmp_dir.join("plan.xsd"), PLAN_XSD_SCHEMA)?;
518 workspace.write(
519 &tmp_dir.join("development_result.xsd"),
520 DEVELOPMENT_RESULT_XSD_SCHEMA,
521 )?;
522 workspace.write(&tmp_dir.join("issues.xsd"), ISSUES_XSD_SCHEMA)?;
523 workspace.write(&tmp_dir.join("fix_result.xsd"), FIX_RESULT_XSD_SCHEMA)?;
524 workspace.write(
525 &tmp_dir.join("commit_message.xsd"),
526 COMMIT_MESSAGE_XSD_SCHEMA,
527 )?;
528
529 Ok(())
530}
531
532#[cfg(test)]
533mod tests {
534 #[cfg(feature = "test-utils")]
539 mod workspace_tests {
540 use super::super::*;
541 use crate::workspace::MemoryWorkspace;
542
543 #[test]
544 fn test_file_contains_marker_with_workspace() {
545 let workspace =
546 MemoryWorkspace::new_test().with_file("test.txt", "line1\nMARKER_TEST\nline3");
547
548 assert!(file_contains_marker_with_workspace(
549 &workspace,
550 Path::new("test.txt"),
551 "MARKER_TEST"
552 )
553 .unwrap());
554 assert!(!file_contains_marker_with_workspace(
555 &workspace,
556 Path::new("test.txt"),
557 "NONEXISTENT"
558 )
559 .unwrap());
560 }
561
562 #[test]
563 fn test_file_contains_marker_with_workspace_missing() {
564 let workspace = MemoryWorkspace::new_test();
565
566 let result = file_contains_marker_with_workspace(
567 &workspace,
568 Path::new("nonexistent.txt"),
569 "MARKER",
570 );
571 assert!(!result.unwrap());
572 }
573
574 #[test]
575 fn test_delete_plan_file_with_workspace() {
576 let workspace = MemoryWorkspace::new_test().with_file(".agent/PLAN.md", "test plan");
577
578 assert!(workspace.exists(Path::new(".agent/PLAN.md")));
579
580 delete_plan_file_with_workspace(&workspace).unwrap();
581
582 assert!(!workspace.exists(Path::new(".agent/PLAN.md")));
583 }
584
585 #[test]
586 fn test_delete_plan_file_with_workspace_nonexistent() {
587 let workspace = MemoryWorkspace::new_test();
588
589 delete_plan_file_with_workspace(&workspace).unwrap();
591 }
592
593 #[test]
594 fn test_read_commit_message_file_with_workspace() {
595 let workspace = MemoryWorkspace::new_test()
596 .with_file(".agent/commit-message.txt", "feat: test commit\n");
597
598 let msg = read_commit_message_file_with_workspace(&workspace).unwrap();
599 assert_eq!(msg, "feat: test commit");
600 }
601
602 #[test]
603 fn test_read_commit_message_file_with_workspace_empty() {
604 let workspace =
605 MemoryWorkspace::new_test().with_file(".agent/commit-message.txt", " \n");
606
607 assert!(read_commit_message_file_with_workspace(&workspace).is_err());
608 }
609
610 #[test]
611 fn test_write_commit_message_file_with_workspace() {
612 let workspace = MemoryWorkspace::new_test();
613
614 write_commit_message_file_with_workspace(&workspace, "feat: new feature").unwrap();
615
616 assert!(workspace.exists(Path::new(".agent/commit-message.txt")));
617 let content = workspace
618 .read(Path::new(".agent/commit-message.txt"))
619 .unwrap();
620 assert_eq!(content, "feat: new feature");
621 }
622
623 #[test]
624 fn test_delete_commit_message_file_with_workspace() {
625 let workspace =
626 MemoryWorkspace::new_test().with_file(".agent/commit-message.txt", "test message");
627
628 assert!(workspace.exists(Path::new(".agent/commit-message.txt")));
629
630 delete_commit_message_file_with_workspace(&workspace).unwrap();
631
632 assert!(!workspace.exists(Path::new(".agent/commit-message.txt")));
633 }
634
635 #[test]
636 fn test_cleanup_generated_files_with_workspace() {
637 let workspace = MemoryWorkspace::new_test()
638 .with_file(".no_agent_commit", "")
639 .with_file(".agent/PLAN.md", "plan")
640 .with_file(".agent/commit-message.txt", "msg");
641
642 cleanup_generated_files_with_workspace(&workspace);
643
644 assert!(!workspace.exists(Path::new(".no_agent_commit")));
645 assert!(!workspace.exists(Path::new(".agent/PLAN.md")));
646 assert!(!workspace.exists(Path::new(".agent/commit-message.txt")));
647 }
648
649 #[test]
650 fn test_setup_xsd_schemas_with_workspace() {
651 let workspace = MemoryWorkspace::new_test();
652
653 setup_xsd_schemas_with_workspace(&workspace).unwrap();
654
655 assert!(workspace.exists(Path::new(".agent/tmp/plan.xsd")));
657 assert!(workspace.exists(Path::new(".agent/tmp/development_result.xsd")));
658 assert!(workspace.exists(Path::new(".agent/tmp/issues.xsd")));
659 assert!(workspace.exists(Path::new(".agent/tmp/fix_result.xsd")));
660 assert!(workspace.exists(Path::new(".agent/tmp/commit_message.xsd")));
661
662 let plan_xsd = workspace.read(Path::new(".agent/tmp/plan.xsd")).unwrap();
664 assert!(plan_xsd.contains("xs:schema"));
665 assert!(plan_xsd.contains("ralph-plan"));
666 }
667 }
668}