1pub mod context;
2pub mod orchestrator;
3pub mod validation;
4
5use crate::db::{get_db, get_dial_dir};
6use crate::errors::{DialError, Result};
7use crate::failure::{find_trusted_solutions, record_failure};
8use crate::git::{git_commit, git_has_changes, git_is_repo, git_revert_to};
9use crate::output::{bold, dim, green, print_success, red, yellow};
10use crate::task::models::Task;
11use crate::MAX_FIX_ATTEMPTS;
12use chrono::Local;
13use rusqlite::Connection;
14use std::fs;
15
16pub use context::{gather_context, generate_subagent_prompt};
17pub use orchestrator::auto_run;
18pub use validation::run_validation;
19
20pub fn create_iteration(conn: &Connection, task_id: i64, attempt_number: i32) -> Result<i64> {
21 let now = Local::now().to_rfc3339();
22
23 conn.execute(
24 "INSERT INTO iterations (task_id, attempt_number, started_at)
25 VALUES (?1, ?2, ?3)",
26 rusqlite::params![task_id, attempt_number, now],
27 )?;
28
29 let iteration_id = conn.last_insert_rowid();
30
31 conn.execute(
33 "UPDATE tasks SET status = 'in_progress', started_at = ?1 WHERE id = ?2",
34 rusqlite::params![now, task_id],
35 )?;
36
37 Ok(iteration_id)
38}
39
40pub fn complete_iteration(
41 conn: &Connection,
42 iteration_id: i64,
43 status: &str,
44 commit_hash: Option<&str>,
45 notes: Option<&str>,
46) -> Result<()> {
47 let started_at: String = conn.query_row(
48 "SELECT started_at FROM iterations WHERE id = ?1",
49 [iteration_id],
50 |row| row.get(0),
51 )?;
52
53 let started = chrono::DateTime::parse_from_rfc3339(&started_at)
54 .map(|dt| dt.with_timezone(&chrono::Local))
55 .unwrap_or_else(|_| Local::now());
56
57 let duration = (Local::now() - started).num_milliseconds() as f64 / 1000.0;
58 let now = Local::now().to_rfc3339();
59
60 conn.execute(
61 "UPDATE iterations
62 SET status = ?1, ended_at = ?2, duration_seconds = ?3, commit_hash = ?4, notes = ?5
63 WHERE id = ?6",
64 rusqlite::params![status, now, duration, commit_hash, notes, iteration_id],
65 )?;
66
67 Ok(())
68}
69
70pub fn iterate_once() -> Result<(bool, String)> {
71 let conn = get_db(None)?;
72
73 let mut stmt = conn.prepare(
75 "SELECT id, description, status, priority, blocked_by, spec_section_id, created_at, started_at, completed_at
76 FROM tasks WHERE status = 'pending'
77 ORDER BY priority, id LIMIT 1",
78 )?;
79
80 let task = stmt.query_row([], |row| Task::from_row(row)).ok();
81
82 let task = match task {
83 Some(t) => t,
84 None => {
85 println!("{}", dim("No pending tasks. Task queue empty."));
86 return Ok((false, "empty_queue".to_string()));
87 }
88 };
89
90 println!("{}", bold(&"=".repeat(60)));
91 println!("{}", bold(&format!("Iteration: Task #{}", task.id)));
92 println!("Description: {}", task.description);
93 println!("{}", bold(&"=".repeat(60)));
94 println!();
95
96 let max_attempt: Option<i32> = conn
98 .query_row(
99 "SELECT MAX(attempt_number) FROM iterations WHERE task_id = ?1 AND status = 'failed'",
100 [task.id],
101 |row| row.get(0),
102 )
103 .ok()
104 .flatten();
105
106 let attempt_number = max_attempt.unwrap_or(0) + 1;
107
108 if attempt_number > MAX_FIX_ATTEMPTS as i32 {
109 println!(
110 "{}",
111 red(&format!(
112 "Task #{} has failed {} times. Skipping.",
113 task.id, MAX_FIX_ATTEMPTS
114 ))
115 );
116
117 conn.execute(
118 "UPDATE tasks SET status = 'blocked', blocked_by = ?1 WHERE id = ?2",
119 rusqlite::params![format!("Failed {} times", MAX_FIX_ATTEMPTS), task.id],
120 )?;
121
122 return Ok((true, "max_attempts".to_string()));
123 }
124
125 let _iteration_id = create_iteration(&conn, task.id, attempt_number)?;
127 println!("Attempt {} of {}", attempt_number, MAX_FIX_ATTEMPTS);
128
129 let context = gather_context(&conn, &task)?;
131 if !context.is_empty() {
132 println!("{}", dim("\nContext gathered. Relevant specs and solutions loaded."));
133 }
134
135 let context_file = get_dial_dir().join("current_context.md");
137 let context_content = format!("# Task: {}\n\n{}", task.description, context);
138 fs::write(&context_file, context_content)?;
139 println!("Context written to: {}", context_file.display());
140
141 println!("{}", yellow("\n>>> Agent should now implement the task <<<"));
142 println!("{}", yellow(">>> Run 'dial validate' when ready to validate <<<"));
143 println!("{}", yellow(">>> Or 'dial complete' to mark complete without validation <<<\n"));
144
145 Ok((true, "awaiting_work".to_string()))
146}
147
148pub fn validate_current() -> Result<bool> {
149 let conn = get_db(None)?;
150
151 let iteration: Option<(i64, i64, String)> = conn
153 .query_row(
154 "SELECT i.id, i.task_id, t.description
155 FROM iterations i
156 INNER JOIN tasks t ON i.task_id = t.id
157 WHERE i.status = 'in_progress'
158 ORDER BY i.id DESC LIMIT 1",
159 [],
160 |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
161 )
162 .ok();
163
164 let (iteration_id, task_id, task_description) = match iteration {
165 Some(i) => i,
166 None => {
167 return Err(DialError::NoIterationInProgress);
168 }
169 };
170
171 println!("Validating iteration #{} for task #{}", iteration_id, task_id);
172
173 let (success, error_output) = run_validation(&conn, iteration_id)?;
175
176 if success {
177 let commit_hash = if git_is_repo() && git_has_changes() {
179 let message = format!("DIAL: {}", task_description);
180 if let Some(hash) = git_commit(&message)? {
181 println!("{}", green(&format!("Committed: {}", &hash[..8])));
182 Some(hash)
183 } else {
184 None
185 }
186 } else {
187 None
188 };
189
190 complete_iteration(&conn, iteration_id, "completed", commit_hash.as_deref(), None)?;
192
193 let now = Local::now().to_rfc3339();
195 conn.execute(
196 "UPDATE tasks SET status = 'completed', completed_at = ?1 WHERE id = ?2",
197 rusqlite::params![now, task_id],
198 )?;
199
200 println!("{}", green(&format!("\nIteration #{} completed successfully!", iteration_id)));
201 println!("{}", green(&format!("Task #{} marked as completed.", task_id)));
202
203 println!();
205 println!("{}", bold("📝 Learning Capture"));
206 println!("{}", dim("Did you learn something during this task? Record it now:"));
207 println!("{}", yellow(" dial learn \"what you learned\" -c <category>"));
208 println!("{}", dim("Categories: build, test, setup, gotcha, pattern, tool, other"));
209 println!();
210
211 Ok(true)
212 } else {
213 let (failure_id, pattern_id) = record_failure(&conn, iteration_id, &error_output, None, None)?;
215 println!("{}", red(&format!("Recorded failure #{}", failure_id)));
216
217 let solutions = find_trusted_solutions(&conn, pattern_id)?;
219 if !solutions.is_empty() {
220 println!("{}", yellow("\nTrusted solutions available:"));
221 for sol in solutions {
222 println!(" - {}", sol.description);
223 }
224 }
225
226 let notes = if error_output.len() > 500 {
228 &error_output[..500]
229 } else {
230 &error_output
231 };
232 complete_iteration(&conn, iteration_id, "failed", None, Some(notes))?;
233
234 let fail_count: i64 = conn.query_row(
236 "SELECT COUNT(*) FROM iterations WHERE task_id = ?1 AND status = 'failed'",
237 [task_id],
238 |row| row.get(0),
239 )?;
240
241 if fail_count >= MAX_FIX_ATTEMPTS as i64 {
242 println!("{}", red(&format!("\nMax attempts ({}) reached.", MAX_FIX_ATTEMPTS)));
243
244 let last_good_commit: Option<String> = conn
246 .query_row(
247 "SELECT commit_hash FROM iterations
248 WHERE status = 'completed' AND commit_hash IS NOT NULL
249 ORDER BY id DESC LIMIT 1",
250 [],
251 |row| row.get(0),
252 )
253 .ok();
254
255 if let Some(hash) = last_good_commit {
256 if git_is_repo() {
257 println!("{}", yellow(&format!("Reverting to last good commit: {}", &hash[..8])));
258 git_revert_to(&hash)?;
259 }
260 }
261
262 conn.execute(
264 "UPDATE tasks SET status = 'blocked', blocked_by = ?1 WHERE id = ?2",
265 rusqlite::params![format!("Failed {} attempts", MAX_FIX_ATTEMPTS), task_id],
266 )?;
267 } else {
268 conn.execute(
270 "UPDATE tasks SET status = 'pending' WHERE id = ?1",
271 [task_id],
272 )?;
273 let remaining = MAX_FIX_ATTEMPTS as i64 - fail_count;
274 println!("{}", yellow(&format!("\nTask reset to pending. {} attempts remaining.", remaining)));
275 }
276
277 Ok(false)
278 }
279}
280
281pub fn run_loop(max_iterations: Option<u32>) -> Result<()> {
282 let dial_dir = get_dial_dir();
283 let stop_file = dial_dir.join("stop");
284
285 if stop_file.exists() {
287 fs::remove_file(&stop_file)?;
288 }
289
290 println!("{}", bold("Starting DIAL run loop..."));
291 println!("{}", dim("Create .dial/stop file to stop gracefully.\n"));
292
293 let mut iteration_count = 0u32;
294
295 loop {
296 if stop_file.exists() {
298 println!("{}", yellow("\nStop flag detected. Stopping gracefully."));
299 fs::remove_file(&stop_file)?;
300 break;
301 }
302
303 if let Some(max) = max_iterations {
305 if iteration_count >= max {
306 println!("{}", yellow(&format!("\nReached max iterations ({}). Stopping.", max)));
307 break;
308 }
309 }
310
311 let (_success, result) = iterate_once()?;
313
314 if result == "empty_queue" {
315 println!("\n{}", bold(&"=".repeat(60)));
316 println!("{}", bold("Task queue empty. DIAL run complete."));
317 show_run_summary()?;
318 break;
319 }
320
321 if result == "awaiting_work" {
322 println!("{}", dim("\nWaiting for work. Run 'dial validate' after implementing."));
323 break;
324 }
325
326 iteration_count += 1;
327 }
328
329 Ok(())
330}
331
332fn show_run_summary() -> Result<()> {
333 let conn = get_db(None)?;
334
335 let (_total, completed, failed): (i64, i64, i64) = conn.query_row(
336 "SELECT
337 COUNT(*),
338 SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END),
339 SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END)
340 FROM iterations",
341 [],
342 |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
343 )?;
344
345 println!("\nCompleted: {}", completed);
346 println!("Failed: {}", failed);
347
348 let solutions_count: i64 = conn.query_row(
349 "SELECT COUNT(*) FROM solutions WHERE confidence >= ?1",
350 [crate::TRUST_THRESHOLD],
351 |row| row.get(0),
352 )?;
353
354 println!("Solutions learned: {}", solutions_count);
355
356 Ok(())
357}
358
359pub fn revert_to_last_good() -> Result<bool> {
360 if !git_is_repo() {
361 return Err(DialError::NotGitRepo);
362 }
363
364 let conn = get_db(None)?;
365
366 let commit_hash: Option<String> = conn
367 .query_row(
368 "SELECT commit_hash FROM iterations
369 WHERE status = 'completed' AND commit_hash IS NOT NULL
370 ORDER BY id DESC LIMIT 1",
371 [],
372 |row| row.get(0),
373 )
374 .ok();
375
376 match commit_hash {
377 Some(hash) => {
378 println!("{}", yellow(&format!("Reverting to: {}", hash)));
379 if git_revert_to(&hash)? {
380 print_success("Reverted successfully.");
381 Ok(true)
382 } else {
383 println!("{}", red("Revert failed."));
384 Ok(false)
385 }
386 }
387 None => {
388 println!("{}", red("No successful commits found."));
389 Ok(false)
390 }
391 }
392}
393
394pub fn reset_current() -> Result<()> {
395 let conn = get_db(None)?;
396
397 let iteration: Option<(i64, i64)> = conn
398 .query_row(
399 "SELECT id, task_id FROM iterations WHERE status = 'in_progress' ORDER BY id DESC LIMIT 1",
400 [],
401 |row| Ok((row.get(0)?, row.get(1)?)),
402 )
403 .ok();
404
405 match iteration {
406 Some((iteration_id, task_id)) => {
407 let now = Local::now().to_rfc3339();
408
409 conn.execute(
411 "UPDATE iterations SET status = 'reverted', ended_at = ?1 WHERE id = ?2",
412 rusqlite::params![now, iteration_id],
413 )?;
414
415 conn.execute(
417 "UPDATE tasks SET status = 'pending' WHERE id = ?1",
418 [task_id],
419 )?;
420
421 print_success(&format!(
422 "Reset iteration #{}. Task returned to pending.",
423 iteration_id
424 ));
425 }
426 None => {
427 println!("{}", dim("No iteration in progress."));
428 }
429 }
430
431 Ok(())
432}
433
434pub fn stop_loop() -> Result<()> {
435 let stop_file = get_dial_dir().join("stop");
436 fs::write(&stop_file, "")?;
437 println!("{}", yellow("Stop flag created. DIAL will stop after current iteration."));
438 Ok(())
439}
440
441pub fn show_context() -> Result<()> {
443 let conn = get_db(None)?;
444
445 let task: Option<Task> = conn
447 .query_row(
448 "SELECT t.id, t.description, t.status, t.priority, t.blocked_by, t.spec_section_id, t.created_at, t.started_at, t.completed_at
449 FROM tasks t
450 INNER JOIN iterations i ON i.task_id = t.id
451 WHERE i.status = 'in_progress'
452 ORDER BY i.id DESC LIMIT 1",
453 [],
454 |row| Task::from_row(row),
455 )
456 .ok();
457
458 let task = match task {
460 Some(t) => t,
461 None => {
462 let mut stmt = conn.prepare(
463 "SELECT id, description, status, priority, blocked_by, spec_section_id, created_at, started_at, completed_at
464 FROM tasks WHERE status = 'pending'
465 ORDER BY priority, id LIMIT 1",
466 )?;
467
468 match stmt.query_row([], |row| Task::from_row(row)).ok() {
469 Some(t) => t,
470 None => {
471 println!("{}", dim("No pending tasks. Task queue empty."));
472 return Ok(());
473 }
474 }
475 }
476 };
477
478 println!("{}", bold(&"=".repeat(60)));
479 println!("{}", bold(&format!("Fresh Context: Task #{}", task.id)));
480 println!("{}", bold(&"=".repeat(60)));
481 println!();
482
483 let context = gather_context(&conn, &task)?;
484 let full_context = format!("# Task: {}\n\n{}", task.description, context);
485
486 println!("{}", full_context);
487
488 let context_file = get_dial_dir().join("current_context.md");
490 fs::write(&context_file, &full_context)?;
491 println!("\n{}", dim(&format!("Context written to: {}", context_file.display())));
492
493 Ok(())
494}
495
496pub fn orchestrate() -> Result<()> {
498 let conn = get_db(None)?;
499
500 let mut stmt = conn.prepare(
502 "SELECT id, description, status, priority, blocked_by, spec_section_id, created_at, started_at, completed_at
503 FROM tasks WHERE status = 'pending'
504 ORDER BY priority, id LIMIT 1",
505 )?;
506
507 let task = match stmt.query_row([], |row| Task::from_row(row)).ok() {
508 Some(t) => t,
509 None => {
510 println!("{}", green("All tasks completed! Nothing to orchestrate."));
511 return Ok(());
512 }
513 };
514
515 let prompt = generate_subagent_prompt(&conn, &task)?;
517
518 println!("{}", bold(&"=".repeat(70)));
519 println!("{}", bold("DIAL Orchestrator Mode"));
520 println!("{}", bold(&"=".repeat(70)));
521 println!();
522 println!("{}", dim("Copy the prompt below to spawn a fresh sub-agent for this task."));
523 println!("{}", dim("After the sub-agent completes, run `dial validate` to commit."));
524 println!();
525 println!("{}", bold("--- SUB-AGENT PROMPT START ---"));
526 println!();
527 println!("{}", prompt);
528 println!("{}", bold("--- SUB-AGENT PROMPT END ---"));
529 println!();
530
531 let prompt_file = get_dial_dir().join("subagent_prompt.md");
533 fs::write(&prompt_file, &prompt)?;
534 println!("{}", dim(&format!("Prompt also saved to: {}", prompt_file.display())));
535
536 println!();
538 println!("{}", bold("Platform Commands:"));
539 println!(" {}", dim("Claude Code: claude -p \"$(cat .dial/subagent_prompt.md)\""));
540 println!(" {}", dim("Codex CLI: codex --task \"$(cat .dial/subagent_prompt.md)\""));
541 println!(" {}", dim("Gemini: Copy prompt to new Gemini session"));
542 println!();
543
544 Ok(())
545}