hematite/agent/
diagnose.rs1use std::fmt::Write as _;
3
4pub fn triage_follow_up_topics(health_output: &str) -> Vec<&'static str> {
6 let lower = health_output.to_ascii_lowercase();
7 let mut topics: Vec<&'static str> = Vec::with_capacity(12);
8
9 let action_required = lower.contains("action required");
10 let worth_a_look = lower.contains("worth a look");
11 if !action_required && !worth_a_look {
12 return topics; }
14
15 if lower.contains("[!]") && (lower.contains("disk") || lower.contains("drive")) {
16 topics.push("storage");
17 topics.push("disk_health");
18 } else if lower.contains("[-]") && (lower.contains("disk") || lower.contains("drive")) {
19 topics.push("storage");
20 }
21
22 if (lower.contains("[!]") || lower.contains("[-]")) && lower.contains("ram") {
23 topics.push("resource_load");
24 topics.push("processes");
25 }
26
27 if (lower.contains("critical") || lower.contains("error event")) && lower.contains("event") {
28 topics.push("log_check");
29 }
30
31 if (lower.contains("[!]") || lower.contains("[-]")) && lower.contains("service") {
32 topics.push("services");
33 }
34
35 if (lower.contains("[!]") || lower.contains("[-]"))
36 && (lower.contains("defender") || lower.contains("firewall") || lower.contains("security"))
37 {
38 topics.push("security");
39 }
40
41 if lower.contains("[!]") && lower.contains("internet connectivity") {
42 topics.push("connectivity");
43 topics.push("network");
44 } else if lower.contains("[-]") && lower.contains("internet connectivity") {
45 topics.push("connectivity");
46 }
47
48 if lower.contains("pending reboot") {
49 topics.push("pending_reboot");
50 }
51
52 if (lower.contains("[!]") || lower.contains("[-]"))
53 && (lower.contains("thermal") || lower.contains("°c"))
54 {
55 topics.push("thermal");
56 topics.push("overclocker");
57 }
58
59 let mut seen = std::collections::HashSet::new();
60 topics.retain(|t| seen.insert(*t));
61
62 topics
63}
64
65pub fn fix_follow_up_topics(
66 combined_output: &str,
67 already_ran: &[&str],
68) -> Vec<(&'static str, &'static str)> {
69 let lower = combined_output.to_ascii_lowercase();
70 let ran: std::collections::HashSet<&str> = already_ran.iter().copied().collect();
71 let mut candidates: Vec<(&'static str, &'static str)> = Vec::with_capacity(16);
72 let mut seen = std::collections::HashSet::new();
73
74 macro_rules! add {
75 ($topic:expr, $label:expr, $cond:expr) => {
76 if $cond && !ran.contains($topic) && seen.insert($topic) {
77 candidates.push(($topic, $label));
78 }
79 };
80 }
81
82 add!(
83 "processes",
84 "Top Processes",
85 lower.contains("very high") && (lower.contains("cpu") || lower.contains("processor"))
86 );
87
88 add!(
89 "cpu_power",
90 "CPU Power",
91 lower.contains("throttling") || (lower.contains("very high") && lower.contains("°c"))
92 );
93
94 add!(
95 "app_crashes",
96 "Application Crashes",
97 lower.contains("very high") && lower.contains("memory")
98 );
99
100 add!(
101 "shadow_copies",
102 "Shadow Copies",
103 (lower.contains("unhealthy") || lower.contains("predictive failure"))
104 && lower.contains("disk")
105 );
106
107 add!(
108 "log_check",
109 "Event Log",
110 lower.contains("unexpected shutdown")
111 || lower.contains("kernel: critical")
112 || lower.contains("stop error")
113 );
114
115 add!(
116 "services",
117 "Services",
118 lower.contains("critical/error event")
119 || lower.contains("error events in windows event log")
120 );
121
122 add!(
123 "wifi",
124 "Wi-Fi",
125 lower.contains("unreachable") && !lower.contains("reachable: yes")
126 );
127
128 add!(
129 "connectivity",
130 "Connectivity",
131 lower.contains("dns resolution: failed") || lower.contains("dns: failed")
132 );
133
134 add!(
135 "defender_quarantine",
136 "Defender Quarantine",
137 lower.contains("real-time protection: disabled") || lower.contains("threat detected")
138 );
139
140 add!(
141 "identity_auth",
142 "Identity & Auth",
143 (lower.contains("teams") || lower.contains("outlook"))
144 && (lower.contains("sign-in fail")
145 || lower.contains("auth fail")
146 || lower.contains("token broker"))
147 );
148
149 add!(
150 "credentials",
151 "Credentials",
152 lower.contains("token broker: not running")
153 || lower.contains("wam: not running")
154 || lower.contains("aad broker plugin: not found")
155 );
156
157 candidates.truncate(3);
158 candidates
159}
160
161pub fn build_diagnose_instruction(health_output: &str, follow_up_topics: &[&str]) -> String {
163 if follow_up_topics.is_empty() {
164 return format!(
165 "DIAGNOSE MODE — triage complete.\n\n\
166 Health report:\n{}\n\n\
167 The machine is in good health. Summarize the key findings for the user \
168 in 2-3 sentences and confirm no action is needed.",
169 health_output
170 );
171 }
172
173 let topic_list = {
174 let mut s = String::with_capacity(follow_up_topics.len() * 40);
175 for (i, t) in follow_up_topics.iter().enumerate() {
176 if i > 0 {
177 s.push('\n');
178 }
179 let _ = write!(s, "{}. inspect_host(topic=\"{}\")", i + 1, t);
180 }
181 s
182 };
183
184 format!(
185 "DIAGNOSE MODE — harness triage identified {} area(s) to investigate.\n\n\
186 Health report (already run by harness):\n{}\n\n\
187 PROTOCOL — follow this exactly:\n\
188 Call each topic below in order:\n{}\n\n\
189 After all calls complete:\n\
190 - Write a numbered fix plan grounded in the tool output\n\
191 - Lead with the most critical issue first\n\
192 - Every step must reference specific data from the results (exact path, count, service name, etc.)\n\
193 - No generic advice — only steps that address what the tools actually found\n\
194 - If a finding needs a restart or elevated privileges, say so explicitly",
195 follow_up_topics.len(),
196 health_output,
197 topic_list
198 )
199}