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 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 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 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 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}