1pub mod autonomy;
2pub mod communication;
3pub mod style;
4pub mod tools;
5
6use std::collections::HashSet;
7use std::fs;
8use std::io;
9use std::path::{Path, PathBuf};
10use std::sync::Arc;
11
12use crate::agents::defaults;
13use crate::module::ContextComponent;
14use crate::module::Module;
15use crate::module::PromptComponent;
16use crate::settings::config::CommunicationTone;
17use crate::settings::SettingsManager;
18use crate::tools::r#trait::ToolExecutor;
19
20#[derive(Copy, Clone, Debug)]
21pub enum Builtin {
22 UnderstandingTools,
23 StyleMandates,
24 CommunicationGuidelines,
25}
26
27pub struct SteeringModule {
35 documents: Arc<SteeringDocuments>,
36 settings: SettingsManager,
37}
38
39impl SteeringModule {
40 pub fn new(documents: Arc<SteeringDocuments>, settings: SettingsManager) -> Self {
41 Self {
42 documents,
43 settings,
44 }
45 }
46}
47
48impl Module for SteeringModule {
49 fn prompt_components(&self) -> Vec<Arc<dyn PromptComponent>> {
50 let autonomy_level = self.settings.settings().autonomy_level;
51
52 vec![
53 Arc::new(style::StyleMandatesComponent::new(self.documents.clone())),
54 Arc::new(tools::ToolInstructionsComponent::new(
55 self.documents.clone(),
56 )),
57 Arc::new(communication::CommunicationComponent::new(
58 self.documents.clone(),
59 )),
60 Arc::new(autonomy::AutonomyComponent::new(autonomy_level)),
61 ]
62 }
63
64 fn context_components(&self) -> Vec<Arc<dyn ContextComponent>> {
65 vec![]
66 }
67
68 fn tools(&self) -> Vec<Arc<dyn ToolExecutor>> {
69 vec![]
70 }
71}
72
73impl Builtin {
74 pub fn all() -> &'static [Builtin] {
75 &[
76 Builtin::UnderstandingTools,
77 Builtin::StyleMandates,
78 Builtin::CommunicationGuidelines,
79 ]
80 }
81
82 fn as_str(&self) -> &'static str {
83 match self {
84 Builtin::UnderstandingTools => "understanding_tools",
85 Builtin::StyleMandates => "style_mandates",
86 Builtin::CommunicationGuidelines => "communication_guidelines",
87 }
88 }
89}
90
91#[derive(Clone)]
92pub struct SteeringDocuments {
93 workspace_roots: Vec<PathBuf>,
94 home_dir: PathBuf,
95 communication_tone: CommunicationTone,
96}
97
98impl SteeringDocuments {
99 pub fn new(
100 workspace_roots: Vec<PathBuf>,
101 home_dir: PathBuf,
102 communication_tone: CommunicationTone,
103 ) -> Self {
104 Self {
105 workspace_roots,
106 home_dir,
107 communication_tone,
108 }
109 }
110
111 pub fn get_builtin(&self, builtin: Builtin) -> String {
112 let name = builtin.as_str();
113 if let Some(content) = self.load_from_workspace(name) {
114 return content;
115 }
116
117 if let Some(content) = self.load_from_home(name) {
118 return content;
119 }
120
121 self.get_default(name)
122 }
123
124 pub fn get_custom_documents(&self) -> Vec<String> {
125 let mut documents = Vec::new();
126 let mut seen_paths = HashSet::new();
127
128 for workspace in &self.workspace_roots {
129 let tycode_dir = workspace.join(".tycode");
130 self.collect_custom_from_dir(&tycode_dir, &mut documents, &mut seen_paths);
131 }
132
133 let home_tycode = self.home_dir.join(".tycode");
134 self.collect_custom_from_dir(&home_tycode, &mut documents, &mut seen_paths);
135
136 documents
137 }
138
139 pub fn get_external_documents(&self) -> Vec<String> {
140 let mut documents = Vec::new();
141
142 for workspace in &self.workspace_roots {
143 self.collect_cursor_docs(workspace, &mut documents);
144 self.collect_cline_docs(workspace, &mut documents);
145 self.collect_roo_docs(workspace, &mut documents);
146 self.collect_kiro_docs(workspace, &mut documents);
147 }
148
149 documents
150 }
151
152 pub fn build_steering_content(&self) -> String {
153 let mut sections = Vec::new();
154
155 for builtin in Builtin::all() {
156 sections.push(self.get_builtin(*builtin));
157 }
158
159 for doc in self.get_custom_documents() {
160 sections.push(doc);
161 }
162
163 for doc in self.get_external_documents() {
164 sections.push(doc);
165 }
166
167 sections.join("\n\n")
168 }
169
170 pub fn build_system_prompt(&self, core_prompt: &str, include_custom: bool) -> String {
171 let mut prompt = core_prompt.to_string();
172
173 if include_custom {
174 for doc in self.get_custom_documents() {
175 prompt.push_str("\n\n");
176 prompt.push_str(&doc);
177 }
178
179 for doc in self.get_external_documents() {
180 prompt.push_str("\n\n");
181 prompt.push_str(&doc);
182 }
183 }
184
185 prompt
186 }
187
188 fn load_from_workspace(&self, name: &str) -> Option<String> {
189 let filename = format!("{}.md", name);
190
191 for workspace in &self.workspace_roots {
192 let path = workspace.join(".tycode").join(&filename);
193 if let Some(content) = self.read_file(&path) {
194 tracing::debug!(
195 "Loaded steering document override from workspace: {}",
196 path.display()
197 );
198 return Some(content);
199 }
200 }
201
202 None
203 }
204
205 fn load_from_home(&self, name: &str) -> Option<String> {
206 let filename = format!("{}.md", name);
207 let path = self.home_dir.join(".tycode").join(&filename);
208
209 if let Some(content) = self.read_file(&path) {
210 tracing::debug!(
211 "Loaded steering document override from home: {}",
212 path.display()
213 );
214 return Some(content);
215 }
216
217 None
218 }
219
220 fn get_default(&self, name: &str) -> String {
221 match name {
222 "style_mandates" => defaults::STYLE_MANDATES.to_string(),
223 "communication_guidelines" => {
224 defaults::get_communication_guidelines(self.communication_tone).to_string()
225 }
226 "understanding_tools" => defaults::UNDERSTANDING_TOOLS.to_string(),
227 _ => String::new(),
228 }
229 }
230
231 fn collect_custom_from_dir(
232 &self,
233 dir: &Path,
234 documents: &mut Vec<String>,
235 seen_paths: &mut HashSet<PathBuf>,
236 ) {
237 let entries = match fs::read_dir(dir) {
238 Ok(entries) => entries,
239 Err(e) if e.kind() == io::ErrorKind::NotFound => return,
240 Err(e) => {
241 tracing::warn!("Failed to read directory {}: {:?}", dir.display(), e);
242 return;
243 }
244 };
245
246 for entry in entries {
247 let entry = match entry {
248 Ok(e) => e,
249 Err(e) => {
250 tracing::warn!(
251 "Error reading directory entry in {}: {:?}",
252 dir.display(),
253 e
254 );
255 continue;
256 }
257 };
258 let path = entry.path();
259
260 if !path.extension().map_or(false, |ext| ext == "md") {
261 continue;
262 }
263
264 if seen_paths.contains(&path) {
265 continue;
266 }
267
268 let stem = match path.file_stem().and_then(|s| s.to_str()) {
269 Some(s) => s,
270 None => continue,
271 };
272
273 if Builtin::all().iter().any(|b| b.as_str() == stem) {
274 continue;
275 }
276
277 if let Some(content) = self.read_file(&path) {
278 tracing::debug!("Loaded custom steering document: {}", path.display());
279 seen_paths.insert(path);
280 documents.push(content);
281 }
282 }
283 }
284
285 fn collect_cursor_docs(&self, workspace: &Path, documents: &mut Vec<String>) {
286 let rules_dir = workspace.join(".cursor").join("rules");
287 self.collect_md_files_from_dir(&rules_dir, documents);
288
289 let cursorrules = workspace.join(".cursorrules");
290 if let Some(content) = self.read_file(&cursorrules) {
291 tracing::debug!("Loaded Cursor rules: {}", cursorrules.display());
292 documents.push(content);
293 }
294 }
295
296 fn collect_cline_docs(&self, workspace: &Path, documents: &mut Vec<String>) {
297 let cline_dir = workspace.join(".cline");
298 self.collect_md_files_from_dir(&cline_dir, documents);
299
300 let clinerules = workspace.join(".clinerules");
301 if let Some(content) = self.read_file(&clinerules) {
302 tracing::debug!("Loaded Cline rules: {}", clinerules.display());
303 documents.push(content);
304 }
305 }
306
307 fn collect_roo_docs(&self, workspace: &Path, documents: &mut Vec<String>) {
308 let rules_dir = workspace.join(".roo").join("rules");
309 self.collect_md_files_from_dir(&rules_dir, documents);
310
311 let roorules = workspace.join(".roorules");
312 if let Some(content) = self.read_file(&roorules) {
313 tracing::debug!("Loaded Roo rules: {}", roorules.display());
314 documents.push(content);
315 }
316 }
317
318 fn collect_kiro_docs(&self, workspace: &Path, documents: &mut Vec<String>) {
319 let steering_dir = workspace.join(".kiro").join("steering-docs");
320 self.collect_md_files_from_dir(&steering_dir, documents);
321 }
322
323 fn collect_md_files_from_dir(&self, dir: &Path, documents: &mut Vec<String>) {
324 let entries = match fs::read_dir(dir) {
325 Ok(entries) => entries,
326 Err(e) if e.kind() == io::ErrorKind::NotFound => return,
327 Err(e) => {
328 tracing::warn!("Failed to read directory {}: {:?}", dir.display(), e);
329 return;
330 }
331 };
332
333 for entry in entries {
334 let entry = match entry {
335 Ok(e) => e,
336 Err(e) => {
337 tracing::warn!(
338 "Error reading directory entry in {}: {:?}",
339 dir.display(),
340 e
341 );
342 continue;
343 }
344 };
345 let path = entry.path();
346
347 if !path.extension().map_or(false, |ext| ext == "md") {
348 continue;
349 }
350
351 if let Some(content) = self.read_file(&path) {
352 tracing::debug!("Loaded external steering document: {}", path.display());
353 documents.push(content);
354 }
355 }
356 }
357
358 fn read_file(&self, path: &Path) -> Option<String> {
359 match fs::read_to_string(path) {
360 Ok(content) => Some(content),
361 Err(e) if e.kind() == io::ErrorKind::NotFound => None,
362 Err(e) => {
363 tracing::warn!(
364 "Failed to read steering document {}: {:?}",
365 path.display(),
366 e
367 );
368 None
369 }
370 }
371 }
372}