1use std::fs;
2use std::path::PathBuf;
3
4use crate::agent::git;
5
6enum WorkspaceMode {
7 Coding,
8 Document,
9 General,
10}
11
12fn detect_workspace_mode(root: &PathBuf) -> WorkspaceMode {
13 let coding_markers = [
15 "Cargo.toml",
16 "package.json",
17 "pyproject.toml",
18 "setup.py",
19 "go.mod",
20 "pom.xml",
21 "build.gradle",
22 "CMakeLists.txt",
23 "index.html",
24 "style.css",
25 "script.js",
26 ".git",
27 "src",
28 "lib",
29 ];
30 for marker in &coding_markers {
31 if root.join(marker).exists() {
32 return WorkspaceMode::Coding;
33 }
34 }
35
36 let code_exts = [
38 "rs", "py", "ts", "js", "go", "cpp", "c", "java", "cs", "rb", "swift", "kt",
39 ];
40 let doc_exts = ["pdf", "md", "txt", "docx", "epub", "rst"];
41 let mut code_count = 0usize;
42 let mut doc_count = 0usize;
43
44 if let Ok(entries) = fs::read_dir(root) {
45 for entry in entries.flatten() {
46 let path = entry.path();
47 if path.is_file() {
48 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
49 let ext = ext.to_lowercase();
50 if code_exts.contains(&ext.as_str()) {
51 code_count += 1;
52 }
53 if doc_exts.contains(&ext.as_str()) {
54 doc_count += 1;
55 }
56 }
57 }
58 }
59 }
60
61 if code_count > 0 {
62 WorkspaceMode::Coding
63 } else if doc_count > 0 {
64 WorkspaceMode::Document
65 } else {
66 WorkspaceMode::General
67 }
68}
69
70pub struct SystemPromptBuilder {
71 pub workspace_root: PathBuf,
72}
73
74impl SystemPromptBuilder {
75 pub fn new(root: PathBuf) -> Self {
76 Self {
77 workspace_root: root,
78 }
79 }
80
81 pub fn build(
84 &self,
85 base_instructions: &str,
86 memory: Option<&str>,
87 summary: Option<&str>,
88 mcp_tools: &[crate::agent::mcp::McpTool],
89 ) -> String {
90 let config = crate::agent::config::load_config();
91 let mut static_sections = Vec::new();
92
93 let workspace_framing = match detect_workspace_mode(&self.workspace_root) {
94 WorkspaceMode::Coding => "- **Authoritative Identity**: You are a Senior SysAdmin, Network Admin, and Software Engineer. Deliver grounded, expert diagnostics without generic assistant boilerplate. You have 100% workstation visibility via native tools.\n\
95 - **Hardware Truth & Tool Discipline**: For any hardware, silicon, or performance query (GPU Vitals, CPU Thermals, Throttling), you MUST use `inspect_host` (topic=\"overclocker\", \"thermal\", \"hardware\").\n\
96 - **Forbidden Regressions**: NEVER call raw shell commands like `nvidia-smi`, `wmic`, or `tasklist` for telemetry if a native `inspect_host` topic covers it.\n\
97 - **Session History Awareness**: Use the RAM-only Silicon Historian trends reported by `inspect_host` to identify anomalies since the start of the session.\n\
98 The current directory is a software project — lean into code editing, build verification, and repo-aware tooling.",
99 WorkspaceMode::Document => "- **Authoritative Identity**: You are a Senior SysAdmin, Network Admin, and Software Engineer. Deliver grounded, expert diagnostics without generic assistant boilerplate. You have 100% workstation visibility via native tools.\n\
100 - **Hardware Truth & Tool Discipline**: For any hardware, silicon, or performance query (GPU Vitals, CPU Thermals, Throttling), you MUST use `inspect_host` (topic=\"overclocker\", \"thermal\", \"hardware\").\n\
101 - **Forbidden Regressions**: NEVER call raw shell commands like `nvidia-smi`, `wmic`, or `tasklist` for telemetry if a native `inspect_host` topic covers it.\n\
102 - **Session History Awareness**: Use the RAM-only Silicon Historian trends reported by `inspect_host` to identify anomalies since the start of the session.\n\
103 The current directory contains documents and files — lean into reading, summarizing, and hardware/network diagnostics.",
104 WorkspaceMode::General => "- **Authoritative Identity**: You are a Senior SysAdmin, Network Admin, and Software Engineer. Deliver grounded, expert diagnostics without generic assistant boilerplate. You have 100% workstation visibility via native tools.\n\
105 - **Hardware Truth & Tool Discipline**: For any hardware, silicon, or performance query (GPU Vitals, CPU Thermals, Throttling), you MUST use `inspect_host` (topic=\"overclocker\", \"thermal\", \"hardware\").\n\
106 - **Forbidden Regressions**: NEVER call raw shell commands like `nvidia-smi`, `wmic`, or `tasklist` for telemetry if a native `inspect_host` topic covers it.\n\
107 - **Session History Awareness**: Use the RAM-only Silicon Historian trends reported by `inspect_host` to identify anomalies since the start of the session.\n\
108 No specific project or document context is loaded — focus on general machine health, system diagnostics, and shell-based tasks.",
109 };
110
111 static_sections.push("# IDENTITY & TONE".to_string());
112 static_sections.push(format!("{} \
113 Be direct, practical, technically precise, and ASCII-first in ordinary prose. \
114 You provide 100% workstation visibility across 81+ read-only diagnostic topics (Hardware, Network, Security, OS). \
115 For simple questions, answer briefly in plain language. \
116 Do not expose internal tool names, hidden protocols, or planning jargon unless the user asks.", workspace_framing));
117 static_sections.push(format!(
118 "- Running Hematite build: {}",
119 crate::hematite_version_display()
120 ));
121 static_sections.push(format!(
122 "- Hematite author and maintainer: {}",
123 crate::HEMATITE_AUTHOR
124 ));
125 static_sections.push(format!(
126 "- Hematite repository: {}",
127 crate::HEMATITE_REPOSITORY_URL
128 ));
129
130 static_sections.push(format!("\n# BASE INSTRUCTIONS\n{base_instructions}"));
131
132 if let Some(home) = std::env::var_os("USERPROFILE") {
133 let global_path = PathBuf::from(home).join(".hematite").join("CLAUDE.md");
134 if global_path.exists() {
135 if let Ok(content) = fs::read_to_string(&global_path) {
136 static_sections.push(format!("\n# GLOBAL USER PREFERENCES\n{content}"));
137 }
138 }
139 }
140
141 let project_rule_files = [
142 "CLAUDE.md",
143 ".claude.md",
144 "CLAUDE.local.md",
145 "HEMATITE.md",
146 ".hematite/rules.md",
147 ".hematite/rules.local.md",
148 ];
149
150 for name in &project_rule_files {
151 let path = self.workspace_root.join(name);
152 if path.exists() {
153 if let Ok(content) = fs::read_to_string(&path) {
154 let content = if content.len() > 6000 {
155 format!("{}...[Rules Truncated]", &content[..6000])
156 } else {
157 content
158 };
159 static_sections.push(format!("\n# PROJECT RULES ({})\n{}", name, content));
160 }
161 }
162 }
163
164 let instructions_dir = crate::tools::file_ops::hematite_dir().join("instructions");
165 if instructions_dir.exists() && instructions_dir.is_dir() {
166 if let Ok(entries) = fs::read_dir(instructions_dir) {
167 for entry in entries.flatten() {
168 let path = entry.path();
169 if path.extension().map(|e| e == "md").unwrap_or(false) {
170 let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
171 let include = if let Some(mem) = memory {
172 mem.to_lowercase().contains(&stem.to_lowercase())
173 } else {
174 false
175 };
176
177 if include {
178 if let Ok(content) = fs::read_to_string(&path) {
179 static_sections.push(format!(
180 "\n# DEEP CONTEXT RULES ({}.md)\n{}",
181 stem, content
182 ));
183 }
184 }
185 }
186 }
187 }
188 }
189
190 let mut prompt = static_sections.join("\n");
191 prompt.push_str("\n\n- **RECOVERY MANDATE**: If a tool returns 'Read discipline' or 'HALLUCINATION BLOCKED', do NOT repeat the failing thought or call. Pivot immediately to a different grounded tool (like `inspect_host` or `inspect_lines` on a different window) to break the loop.");
192 prompt.push_str(
193 "\n\n###############################################################################\n",
194 );
195 prompt.push_str(
196 "# DYNAMIC CONTEXT (Changes every turn) #\n",
197 );
198 prompt.push_str(
199 "###############################################################################\n",
200 );
201
202 if let Some(s) = summary {
203 prompt.push_str(&format!(
204 "\n# COMPACTED HISTORY SUMMARY\n{}\nRecent messages are preserved below.",
205 s
206 ));
207 }
208
209 if let Some(mem) = memory {
210 prompt.push_str(&format!("\n# SESSION MEMORY\n{mem}"));
211 }
212
213 prompt.push_str("\n# ENVIRONMENT");
214 prompt.push_str(&format!(
215 "\n- Local Time: {}",
216 chrono::Local::now().format("%Y-%m-%d %H:%M:%S")
217 ));
218 prompt.push_str(&format!(
219 "\n- Hematite Build: {}",
220 crate::hematite_version_display()
221 ));
222 if let Ok(user) = std::env::var("USERPROFILE") {
223 prompt.push_str(&format!("\n- USERPROFILE (Authoritative): {user}"));
224 }
225 if let Ok(comp) = std::env::var("COMPUTERNAME") {
226 prompt.push_str(&format!("\n- COMPUTERNAME (Authoritative): {comp}"));
227 }
228 prompt.push_str("\n- Operating System: Windows (User workspace)");
229
230 if git::is_git_repo(&self.workspace_root) {
231 if let Ok(branch) = git::get_active_branch(&self.workspace_root) {
232 prompt.push_str(&format!("\n- Git Branch: {branch}"));
233 }
234 }
235
236 if let Ok(entries) = fs::read_dir(&self.workspace_root) {
238 let mut list = Vec::new();
239 for entry in entries.flatten() {
240 let path = entry.path();
241 if path.is_file() {
242 if let Some(name) = path.file_name().and_then(|s| s.to_str()) {
243 if !name.starts_with('.') && name != "Cargo.lock" {
244 list.push(name.to_string());
245 }
246 }
247 }
248 }
249 if !list.is_empty() {
250 list.sort();
251 prompt.push_str(&format!("\n- Workspace Files (Root): {}", list.join(", ")));
252 }
253 }
254
255 let hematite_dir = crate::tools::file_ops::hematite_dir();
256 for (name, path) in [
257 ("TASK", hematite_dir.join("TASK.md")),
258 ("PLAN", hematite_dir.join("PLAN.md")),
259 ] {
260 if path.exists() {
261 if let Ok(content) = fs::read_to_string(&path) {
262 if !content.trim().is_empty() {
263 let content = if content.len() > 3000 {
264 format!("{}...[Truncated]", &content[..3000])
265 } else {
266 content
267 };
268 prompt.push_str(&format!(
269 "\n\n# ACTIVE TASK {} (.hematite/)\n{}",
270 name, content
271 ));
272 }
273 }
274 }
275 }
276
277 if !mcp_tools.is_empty() {
278 prompt.push_str("\n\n# ACTIVE MCP TOOLS");
279 for tool in mcp_tools {
280 let mut description = tool
281 .description
282 .clone()
283 .unwrap_or_else(|| "No description provided.".to_string());
284 if description.len() > 180 {
285 description.truncate(180);
286 description.push_str("...");
287 }
288 prompt.push_str(&format!("\n- {}: {}", tool.name, description));
289 }
290 }
291
292 if let Some(hint) = &config.context_hint {
293 prompt.push_str(&format!("\n## PROJECT CONTEXT HINT\n{}\n", hint));
294 }
295
296 prompt.push_str("\n## HEMATITE OPERATIONAL PROTOCOL\n");
297 prompt.push_str("1. **Thinking Mode**: ALWAYS use the thought channel (`<|channel>thought ... <channel|>`) to plan your response.\n");
298 prompt.push_str("2. **Direct Answer**: Unless hardware is specifically named (CPU, GPU, RAM, Disk), assume all performance questions are about the ACTIVE CODE/UI logic. DO NOT use `inspect_host` for code-vitals.\n");
299 prompt.push_str("3. **Tool Format**: Use structured XML tags for tool calling. No natural language inside tool arguments.\n");
300 prompt.push_str("4. **Identity**: You are a world-class Software Engineer. Answer from the codebase first.\n");
301 prompt.push_str("5. **Continuous Goal**: Continue your task until you have fulfilled the user's intent. Stay grounded in results.\n");
302 prompt.push_str("6. **Tool Discipline**: Use surgical file tools (`write_file`, `edit_file`, `grep_files`) instead of shell. Overwriting code is blocked; use hunk-patching.\n");
303 prompt.push_str("7. **Workspace Efficiency**: Use `run_workspace_workflow` ONLY for project-level `build`, `test`, `lint`, or `fix`. Do NOT use it for general coding or autonomy.\n");
304 prompt.push_str("8. **Host Inspection**: Use `inspect_host` ONLY for legitimate system diagnostics. Topics: hardware, security, network, updates, health_report, storage.\n");
305 prompt.push_str("9. **Proof Before Action**: ALWAYS `grep_files` for symbols and `read_file` to verify content before any edit.\n");
306 prompt.push_str("10. **Proof Before Commit**: Run `verify_build` (or `workflow=build`) after all edits to confirm zero regressions.\n");
307 prompt.push_str("11. **Edit Precision**: Match indentation and whitespace exactly in search/replace targets.\n");
308 prompt.push_str("12. **Teacher Mode**: If asked how to perform an administrative task, provide a numbered walkthrough of exact PowerShell commands.\n");
309 prompt.push_str("13. **Search Priority**: Use regex searches for complex patterns. Never assume a file exists without listing the directory.\n");
310 prompt.push_str("14. **Communication**: Keep technical explanations concise. Focus on the 'what' and 'why' of the code change.\n");
311 prompt.push_str("15. **Sovereign Safety**: If at a drive root or major system directory, ask to move to a project folder for better context.\n");
312 prompt.push_str("16. **Proactive Research**: If you encounter a technical term, library version, or external API syntax you are not 100% certain about, do NOT guess. Use `research_web` to verify the latest authoritative facts. Double-check your own internal knowledge against current web reality when implementing modern tech stacks.\n");
313 prompt.push_str("17. **Tool Precedence**: NEVER use the `shell` tool (e.g., `curl`, `wget`, or raw `grep` on URLs) to perform web research or fetch content if native precision tools like `research_web` or `fetch_docs` are available. Prioritize native tools for privacy and cleaner output.\n");
314 prompt.push_str("18. **Entity Discovery**: For 'Who is', 'Who are', 'What is', or 'What was' queries about people, organizations, or concepts not explicitly defined in your local workspace context, ALWAYS use `research_web` to verify current facts. Do NOT guess or hallucinate identities from internal training data. If the user asks who you or your creator is, you may provide your identity from local context, but if they ask you to 'search' or 'google' that identity, you MUST use `research_web` as requested.\n");
315
316 prompt
317 }
318}