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