Skip to main content

hematite/agent/
prompt.rs

1use std::fs;
2use std::path::PathBuf;
3
4use crate::agent::git;
5use crate::agent::instructions::{
6    guidance_section_title, resolve_guidance_path, PROJECT_GUIDANCE_FILES,
7};
8
9enum WorkspaceMode {
10    Coding,
11    Document,
12    General,
13}
14
15fn detect_workspace_mode(root: &PathBuf) -> WorkspaceMode {
16    // Strong coding signals — any of these present means it's a coding workspace
17    let coding_markers = [
18        "Cargo.toml",
19        "package.json",
20        "pyproject.toml",
21        "setup.py",
22        "go.mod",
23        "pom.xml",
24        "build.gradle",
25        "CMakeLists.txt",
26        "index.html",
27        "style.css",
28        "script.js",
29        ".git",
30        "src",
31        "lib",
32    ];
33    for marker in &coding_markers {
34        if root.join(marker).exists() {
35            return WorkspaceMode::Coding;
36        }
37    }
38
39    // No strong coding signal — check file extensions
40    let code_exts = [
41        "rs", "py", "ts", "js", "go", "cpp", "c", "java", "cs", "rb", "swift", "kt",
42    ];
43    let doc_exts = ["pdf", "md", "txt", "docx", "epub", "rst"];
44    let mut code_count = 0usize;
45    let mut doc_count = 0usize;
46
47    if let Ok(entries) = fs::read_dir(root) {
48        for entry in entries.flatten() {
49            let path = entry.path();
50            if path.is_file() {
51                if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
52                    let ext = ext.to_lowercase();
53                    if code_exts.contains(&ext.as_str()) {
54                        code_count += 1;
55                    }
56                    if doc_exts.contains(&ext.as_str()) {
57                        doc_count += 1;
58                    }
59                }
60            }
61        }
62    }
63
64    if code_count > 0 {
65        WorkspaceMode::Coding
66    } else if doc_count > 0 {
67        WorkspaceMode::Document
68    } else {
69        WorkspaceMode::General
70    }
71}
72
73pub struct SystemPromptBuilder {
74    pub workspace_root: PathBuf,
75}
76
77impl SystemPromptBuilder {
78    pub fn new(root: PathBuf) -> Self {
79        Self {
80            workspace_root: root,
81        }
82    }
83
84    /// Build the full system prompt with Rule Hierarchy and Gemma-4 Optimization.
85    /// Hierarchy: Global ($HOME) -> Project (Root) -> Local (Ignored).
86    pub fn build(
87        &self,
88        base_instructions: &str,
89        memory: Option<&str>,
90        summary: Option<&str>,
91        mcp_tools: &[crate::agent::mcp::McpTool],
92    ) -> String {
93        let config = crate::agent::config::load_config();
94        let mut static_sections = Vec::new();
95
96        let workspace_framing = match detect_workspace_mode(&self.workspace_root) {
97            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\
98                                       - **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\
99                                       - **Forbidden Regressions**: NEVER call raw shell commands like `nvidia-smi`, `wmic`, or `tasklist` for telemetry if a native `inspect_host` topic covers it.\n\
100                                       - **Session History Awareness**: Use the RAM-only Silicon Historian trends reported by `inspect_host` to identify anomalies since the start of the session.\n\
101                                       The current directory is a software project — lean into code editing, build verification, and repo-aware tooling.",
102            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\
103                                         - **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\
104                                         - **Forbidden Regressions**: NEVER call raw shell commands like `nvidia-smi`, `wmic`, or `tasklist` for telemetry if a native `inspect_host` topic covers it.\n\
105                                         - **Session History Awareness**: Use the RAM-only Silicon Historian trends reported by `inspect_host` to identify anomalies since the start of the session.\n\
106                                         The current directory contains documents and files — lean into reading, summarizing, and hardware/network diagnostics.",
107            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\
108                                       - **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\
109                                       - **Forbidden Regressions**: NEVER call raw shell commands like `nvidia-smi`, `wmic`, or `tasklist` for telemetry if a native `inspect_host` topic covers it.\n\
110                                       - **Session History Awareness**: Use the RAM-only Silicon Historian trends reported by `inspect_host` to identify anomalies since the start of the session.\n\
111                                       No specific project or document context is loaded — focus on general machine health, system diagnostics, and shell-based tasks.",
112        };
113
114        static_sections.push("# IDENTITY & TONE".to_string());
115        static_sections.push(format!("{} \
116                             Be direct, practical, technically precise, and ASCII-first in ordinary prose. \
117                             You provide 100% workstation visibility across 81+ read-only diagnostic topics (Hardware, Network, Security, OS). \
118                             For simple questions, answer briefly in plain language. \
119                             Do not expose internal tool names, hidden protocols, or planning jargon unless the user asks.", workspace_framing));
120        static_sections.push(format!(
121            "- Running Hematite build: {}",
122            crate::hematite_version_display()
123        ));
124        static_sections.push(format!(
125            "- Hematite author and maintainer: {}",
126            crate::HEMATITE_AUTHOR
127        ));
128        static_sections.push(format!(
129            "- Hematite repository: {}",
130            crate::HEMATITE_REPOSITORY_URL
131        ));
132
133        static_sections.push(format!("\n# BASE INSTRUCTIONS\n{base_instructions}"));
134
135        if let Some(home) = std::env::var_os("USERPROFILE") {
136            let global_path = PathBuf::from(home).join(".hematite").join("CLAUDE.md");
137            if global_path.exists() {
138                if let Ok(content) = fs::read_to_string(&global_path) {
139                    static_sections.push(format!("\n# GLOBAL USER PREFERENCES\n{content}"));
140                }
141            }
142        }
143
144        for name in PROJECT_GUIDANCE_FILES {
145            let path = resolve_guidance_path(&self.workspace_root, name);
146            if path.exists() {
147                if let Ok(content) = fs::read_to_string(&path) {
148                    let content = if content.len() > 6000 {
149                        format!("{}...[Guidance Truncated]", &content[..6000])
150                    } else {
151                        content
152                    };
153                    static_sections.push(format!(
154                        "\n# {} ({})\n{}",
155                        guidance_section_title(name),
156                        name,
157                        content
158                    ));
159                }
160            }
161        }
162
163        if let Some(skill_catalog) = crate::agent::instructions::render_skill_catalog(
164            &crate::agent::instructions::discover_agent_skills(&self.workspace_root, &config.trust),
165            6_000,
166        ) {
167            static_sections.push(format!("\n{}", skill_catalog));
168        }
169
170        let instructions_dir = crate::tools::file_ops::hematite_dir().join("instructions");
171        if instructions_dir.exists() && instructions_dir.is_dir() {
172            if let Ok(entries) = fs::read_dir(instructions_dir) {
173                for entry in entries.flatten() {
174                    let path = entry.path();
175                    if path.extension().map(|e| e == "md").unwrap_or(false) {
176                        let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
177                        let include = if let Some(mem) = memory {
178                            mem.to_lowercase().contains(&stem.to_lowercase())
179                        } else {
180                            false
181                        };
182
183                        if include {
184                            if let Ok(content) = fs::read_to_string(&path) {
185                                static_sections.push(format!(
186                                    "\n# DEEP CONTEXT RULES ({}.md)\n{}",
187                                    stem, content
188                                ));
189                            }
190                        }
191                    }
192                }
193            }
194        }
195
196        let mut prompt = static_sections.join("\n");
197        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.");
198        prompt.push_str(
199            "\n\n###############################################################################\n",
200        );
201        prompt.push_str(
202            "# DYNAMIC CONTEXT (Changes every turn)                                        #\n",
203        );
204        prompt.push_str(
205            "###############################################################################\n",
206        );
207
208        if let Some(s) = summary {
209            prompt.push_str(&format!(
210                "\n# COMPACTED HISTORY SUMMARY\n{}\nRecent messages are preserved below.",
211                s
212            ));
213        }
214
215        if let Some(mem) = memory {
216            prompt.push_str(&format!("\n# SESSION MEMORY\n{mem}"));
217        }
218
219        prompt.push_str("\n# ENVIRONMENT");
220        prompt.push_str(&format!(
221            "\n- Local Time: {}",
222            chrono::Local::now().format("%Y-%m-%d %H:%M:%S")
223        ));
224        prompt.push_str(&format!(
225            "\n- Hematite Build: {}",
226            crate::hematite_version_display()
227        ));
228        if let Ok(user) = std::env::var("USERPROFILE") {
229            prompt.push_str(&format!("\n- USERPROFILE (Authoritative): {user}"));
230        }
231        if let Ok(comp) = std::env::var("COMPUTERNAME") {
232            prompt.push_str(&format!("\n- COMPUTERNAME (Authoritative): {comp}"));
233        }
234        prompt.push_str("\n- Operating System: Windows (User workspace)");
235
236        if git::is_git_repo(&self.workspace_root) {
237            if let Ok(branch) = git::get_active_branch(&self.workspace_root) {
238                prompt.push_str(&format!("\n- Git Branch: {branch}"));
239            }
240        }
241
242        // --- Intelligence Injection: Flat File Inventory ---
243        if let Ok(entries) = fs::read_dir(&self.workspace_root) {
244            let mut list = Vec::new();
245            for entry in entries.flatten() {
246                let path = entry.path();
247                if path.is_file() {
248                    if let Some(name) = path.file_name().and_then(|s| s.to_str()) {
249                        if !name.starts_with('.') && name != "Cargo.lock" {
250                            list.push(name.to_string());
251                        }
252                    }
253                }
254            }
255            if !list.is_empty() {
256                list.sort();
257                prompt.push_str(&format!("\n- Workspace Files (Root): {}", list.join(", ")));
258            }
259        }
260
261        let hematite_dir = crate::tools::file_ops::hematite_dir();
262        for (name, path) in [
263            ("TASK", hematite_dir.join("TASK.md")),
264            ("PLAN", hematite_dir.join("PLAN.md")),
265        ] {
266            if path.exists() {
267                if let Ok(content) = fs::read_to_string(&path) {
268                    if !content.trim().is_empty() {
269                        let content = if content.len() > 3000 {
270                            format!("{}...[Truncated]", &content[..3000])
271                        } else {
272                            content
273                        };
274                        prompt.push_str(&format!(
275                            "\n\n# ACTIVE TASK {} (.hematite/)\n{}",
276                            name, content
277                        ));
278                    }
279                }
280            }
281        }
282
283        if !mcp_tools.is_empty() {
284            prompt.push_str("\n\n# ACTIVE MCP TOOLS");
285            for tool in mcp_tools {
286                let mut description = tool
287                    .description
288                    .clone()
289                    .unwrap_or_else(|| "No description provided.".to_string());
290                if description.len() > 180 {
291                    description.truncate(180);
292                    description.push_str("...");
293                }
294                prompt.push_str(&format!("\n- {}: {}", tool.name, description));
295            }
296        }
297
298        if let Some(hint) = &config.context_hint {
299            prompt.push_str(&format!("\n## PROJECT CONTEXT HINT\n{}\n", hint));
300        }
301
302        prompt.push_str("\n## HEMATITE OPERATIONAL PROTOCOL\n");
303        prompt.push_str("1. **Thinking Mode**: ALWAYS use the thought channel (`<|channel>thought ... <channel|>`) to plan your response.\n");
304        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");
305        prompt.push_str("3. **Tool Format**: Use structured XML tags for tool calling. No natural language inside tool arguments.\n");
306        prompt.push_str("4. **Identity**: You are a world-class Software Engineer. Answer from the codebase first.\n");
307        prompt.push_str("5. **Continuous Goal**: Continue your task until you have fulfilled the user's intent. Stay grounded in results.\n");
308        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");
309        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");
310        prompt.push_str("8. **Host Inspection**: Use `inspect_host` ONLY for legitimate system diagnostics. Topics: hardware, security, network, updates, health_report, storage, storage_spaces, defender_quarantine, data_audit.\n");
311        prompt.push_str("9. **Proof Before Action**: ALWAYS `grep_files` for symbols and `read_file` to verify content before any edit.\n");
312        prompt.push_str("10. **Proof Before Commit**: Run `verify_build` (or `workflow=build`) after all edits to confirm zero regressions.\n");
313        prompt.push_str("11. **Edit Precision**: Match indentation and whitespace exactly in search/replace targets.\n");
314        prompt.push_str("12. **Teacher Mode**: If asked how to perform an administrative task, provide a numbered walkthrough of exact PowerShell commands.\n");
315        prompt.push_str("13. **Search Priority**: Use regex searches for complex patterns. Never assume a file exists without listing the directory.\n");
316        prompt.push_str("14. **Communication**: Keep technical explanations concise. Focus on the 'what' and 'why' of the code change.\n");
317        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");
318        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");
319        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");
320        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");
321        prompt.push_str("19. **Scientific Mandate**: You are a Lead Computational Researcher. NEVER guess results for math, physics, or algorithmic complexity. Use `scientific_compute` for formal proofs, unit-safety, and empirical Big-O auditing. Use the `ledger` mode to persist cross-session math memory.\n");
322
323        prompt
324    }
325}