Skip to main content

hematite/agent/
prompt.rs

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    // Strong coding signals — any of these present means it's a coding workspace
14    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    // No strong coding signal — check file extensions
37    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    /// Build the full system prompt with Rule Hierarchy and Gemma-4 Optimization.
82    /// Hierarchy: Global ($HOME) -> Project (Root) -> Local (Ignored).
83    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        // --- Intelligence Injection: Flat File Inventory ---
237        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}