1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
// ── INVARIANT (2026-04-16): DO NOT DELETE THIS FILE ──
// verify.rs was deleted once (01afc5b, "all dead_code") and had to be
// restored (4f704cb) after causing 22-49 turn sessions. The functions
// are called conditionally from mod.rs — #[allow(dead_code)] does NOT
// mean unused. If you think this file is dead, grep for `should_verify`
// and `inject_verify_prompt` in mod.rs before touching it.
// ──────────────────────────────────────────────────────────────────
use super::*;
impl AgentLoop {
/// Check if the model should verify its changes before finishing.
/// Returns true if: edits were made AND no bash/build command was run AFTER the last edit.
#[allow(dead_code)]
pub(crate) fn should_verify(&self) -> bool {
if self.files_edited_this_turn.is_empty() {
return false; // No edits, nothing to verify
}
if self.tool_call_count >= 20 {
return false; // Near step limit, don't waste steps
}
// Check the LAST tool call and its result.
// If it's a SUCCESSFUL bash → already verified. No need for another.
// If it's a FAILED bash (build error) → need to verify/fix.
// If it's edit/write/read → hasn't verified yet.
let mut last_tool_name = String::new();
let mut last_result_success = true;
for msg in self.conversation.messages.iter().rev() {
if let (Some(success), Some(output)) =
(msg.tool_result_success(), msg.tool_result_output())
{
if last_tool_name.is_empty() {
last_result_success = success;
// Also check output for build failure keywords
let out = output.to_lowercase();
if out.contains("build failed")
|| out.contains("error")
|| out.contains("failed")
{
last_result_success = false;
}
}
}
if let crate::conversation::message::MessageContent::AssistantWithToolCalls {
tool_calls,
..
} = &msg.content
{
if let Some(last_tc) = tool_calls.last() {
if last_tool_name.is_empty() {
last_tool_name = last_tc.name.clone();
}
// If last tool was bash AND it succeeded → no verify needed
// If last tool was bash AND it failed → verify/fix needed
return last_tool_name != "bash" || !last_result_success;
}
}
if matches!(msg.role, crate::conversation::message::Role::User) {
break;
}
}
false
}
// ── INVARIANT (2026-04-16): verify prompt must NOT mention dev server ──
// "check if the dev server shows errors" caused models to run `npm run dev`
// for verification → 140-168s blocking waits. Always guide toward build/check
// commands that exit immediately. Tech-stack neutral: no npm/cargo/mvn.
// History: "dev server" wording survived 16 commits unnoticed. Fixed today.
// ────────────────────────────────────────────────────────────────────────
/// Inject a verification prompt into the conversation as a user message,
/// forcing the model to check its work before declaring success.
#[allow(dead_code)]
pub(crate) fn inject_verify_prompt(&mut self) {
let files = self.files_edited_this_turn.join(", ");
let verify_msg = format!(
"[SYSTEM: You edited {}. Before finishing, verify your changes work. \
Run the project's build/check/compile command to catch errors. \
Do NOT start any long-running process that does not exit on its own. \
If you find errors, fix them now.]",
files
);
// Inject as assistant thought + will trigger another LLM call
self.conversation.push_delta(&verify_msg);
self.conversation.finalize_stream();
}
/// Legacy auto-compile verification — DISABLED since Phase 4.2.
/// Replaced by language-agnostic approach: discipline.rs now prompts
/// the model to verify its own changes (build/test/lint/run).
/// The model decides the appropriate verification for the project.
#[allow(dead_code)]
pub(crate) async fn auto_compile_verify(&mut self) {
// No-op. Verification is now model-driven via discipline prompt.
// See discipline.rs apply_post_turn_discipline() for the verification nudge.
}
/// Tree-sitter syntax check on recently edited files.
/// Language-agnostic: works on any file tree-sitter can parse.
/// Catches bracket mismatches, missing closings, duplicate declarations
/// that build tools may miss (e.g., Vite doesn't catch Vue SFC syntax errors).
/// Called for non-compiled projects as an auto-compile equivalent.
#[allow(dead_code)]
pub(crate) async fn syntax_check_edited_files(&mut self) {
let wd = self
.turn_runner
.context
.working_dir
.try_read()
.map(|g| g.clone())
.unwrap_or_default();
let mut warnings: Vec<String> = Vec::new();
let mut searcher = self.turn_runner.context.semantic.lock().await;
for file in &self.files_edited_this_turn {
// Resolve to full path
let path = if std::path::Path::new(file).is_absolute() {
std::path::PathBuf::from(file)
} else {
wd.join(file)
};
if let Ok(content) = std::fs::read_to_string(&path) {
let (errors, lines) = searcher.count_syntax_errors(&content, &path);
if errors > 0 {
let lines_str = lines
.iter()
.map(|l| format!("L{}", l))
.collect::<Vec<_>>()
.join(", ");
warnings.push(format!(
"{}: {} syntax error(s) at {}",
file, errors, lines_str
));
}
}
}
drop(searcher);
if !warnings.is_empty() {
let msg = format!(
"[SYNTAX CHECK: {}. Fix these before continuing — the file structure may be broken.]",
warnings.join("; ")
);
self.conversation.add_user_message(&msg);
}
}
/// Snapshot dev server log sizes before an edit, so we can diff after.
#[allow(dead_code)]
pub(crate) fn snapshot_devserver_log_sizes(&self) -> std::collections::HashMap<String, u64> {
let wd = self
.turn_runner
.context
.working_dir
.try_read()
.map(|g| g.clone())
.unwrap_or_default();
let candidates = [
"frontend.log",
"backend.log",
"server.log",
"frontend/frontend.log",
"backend/backend.log",
];
let mut sizes = std::collections::HashMap::new();
for name in &candidates {
let path = wd.join(name);
if let Ok(meta) = std::fs::metadata(&path) {
sizes.insert(name.to_string(), meta.len());
}
}
sizes
}
/// Check dev server logs for NEW errors after editing frontend/backend files.
/// Only reads lines appended AFTER `pre_sizes` snapshot, ignoring stale errors.
#[allow(dead_code)]
pub(crate) async fn check_devserver_logs(
&mut self,
pre_sizes: &std::collections::HashMap<String, u64>,
) {
let wd = self
.turn_runner
.context
.working_dir
.try_read()
.map(|g| g.clone())
.unwrap_or_default();
// Small delay to let HMR process the file change
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
for (log_name, &old_size) in pre_sizes {
let log_path = wd.join(log_name);
let new_size = match tokio::fs::metadata(&log_path).await {
Ok(m) => m.len(),
Err(_) => continue,
};
// No new content since edit → skip
if new_size <= old_size {
continue;
}
// Read only the NEW bytes
let content = match tokio::fs::read_to_string(&log_path).await {
Ok(c) => c,
Err(_) => continue,
};
let new_content = if old_size == 0 {
&content
} else {
// Approximate: skip old_size bytes (may split a UTF-8 char, but log lines are mostly ASCII)
let skip = old_size as usize;
if skip < content.len() {
&content[skip..]
} else {
continue;
}
};
// Look for error patterns in the new content only
let error_lines: Vec<&str> = new_content
.lines()
.filter(|l| {
let lower = l.to_lowercase();
(lower.contains("error")
|| lower.contains("failed")
|| lower.contains("syntaxerror"))
&& !lower.contains("0 error")
&& !lower.contains("error overlay")
})
.collect();
if !error_lines.is_empty() {
let errors: String = error_lines
.iter()
.take(5)
.map(|l| l.to_string())
.collect::<Vec<_>>()
.join("\n");
let msg = format!(
"[DEV SERVER ERROR in {}:]\n{}\n\nFix these errors before continuing.",
log_name, errors
);
self.conversation.add_user_message(&msg);
break; // One log file's errors is enough
}
}
}
/// No-op: Vue partial edit detection removed. Multi-edit is disabled;
/// serial edit_file calls with old_string/new_string are the standard approach.
#[allow(dead_code)]
pub(crate) async fn check_vue_partial_edit(&mut self) {
// Intentionally empty. Kept as stub to avoid changing call sites.
}
}