hematite/agent/
instructions.rs1use std::collections::HashSet;
2use std::fs;
3use std::path::{Path, PathBuf};
4
5#[derive(Debug, Clone)]
6pub struct InstructionFile {
7 pub _path: PathBuf,
8 pub content: String,
9}
10
11pub fn discover_instruction_files(cwd: &Path) -> Vec<InstructionFile> {
13 let mut directories = Vec::new();
14 let mut cursor = Some(cwd);
15 while let Some(dir) = cursor {
16 directories.push(dir.to_path_buf());
17 cursor = dir.parent();
18 }
19 directories.reverse();
20
21 let mut files = Vec::new();
22 let mut seen_hashes = HashSet::new();
23
24 for dir in directories {
25 for candidate_name in [
26 "HEMATITE.md",
27 "HEMATITE.local.md",
28 ".hematite/rules.md",
29 ".hematite/instructions.md",
30 ] {
31 let candidate_path = if candidate_name.contains('/') {
32 let parts: Vec<&str> = candidate_name.split('/').collect();
33 dir.join(parts[0]).join(parts[1])
34 } else {
35 dir.join(candidate_name)
36 };
37
38 if let Ok(content) = fs::read_to_string(&candidate_path) {
39 let trimmed = content.trim();
40 if !trimmed.is_empty() {
41 let hash = stable_hash(trimmed);
43 if seen_hashes.contains(&hash) {
44 continue;
45 }
46 seen_hashes.insert(hash);
47 files.push(InstructionFile {
48 _path: candidate_path,
49 content: trimmed.to_string(),
50 });
51 }
52 }
53 }
54 }
55 files
56}
57
58fn stable_hash(s: &str) -> u64 {
59 use std::collections::hash_map::DefaultHasher;
60 use std::hash::{Hash, Hasher};
61 let mut hasher = DefaultHasher::new();
62 s.hash(&mut hasher);
63 hasher.finish()
64}
65
66pub fn render_instructions(files: &[InstructionFile], max_chars: usize) -> Option<String> {
68 if files.is_empty() {
69 return None;
70 }
71
72 let mut output = Vec::new();
73 output.push("# Project Instructions".to_string());
74 output.push(
75 "These rules were discovered in the directory tree for the current repository:".to_string(),
76 );
77
78 let mut remaining = max_chars;
79 for file in files {
80 if remaining < 100 {
81 output.push("\n... [further instructions omitted due to context limit]".to_string());
82 break;
83 }
84
85 let content = if file.content.len() > remaining {
86 format!("{}\n... [truncated]", &file.content[..remaining - 20])
87 } else {
88 file.content.clone()
89 };
90
91 remaining = remaining.saturating_sub(content.len());
92 output.push(format!("\n## Source: HEMATITE FILE\n{}", content));
93 }
94
95 Some(output.join("\n"))
96}