dirge-agent 0.19.15

Minimalistic coding agent written in Rust, optimized for memory footprint and performance
use std::path::PathBuf;

pub mod plan;
pub mod transcript;

pub struct LoopState {
    pub active: bool,
    pub prompt: String,
    pub plan_file: PathBuf,
    pub iteration: u32,
    pub max_iterations: Option<u32>,
    pub last_summary: Option<String>,
    pub run_cmd: Option<String>,
    pub last_run_output: Option<String>,
}

impl LoopState {
    pub fn new(
        prompt: String,
        plan_file: PathBuf,
        max_iterations: Option<u32>,
        run_cmd: Option<String>,
    ) -> Self {
        LoopState {
            active: true,
            prompt,
            plan_file,
            iteration: 0,
            max_iterations,
            last_summary: None,
            run_cmd,
            last_run_output: None,
        }
    }

    pub fn build_prompt(&self) -> String {
        let plan_contents = plan::read_plan(&self.plan_file).unwrap_or_default();

        let max_label = match self.max_iterations {
            Some(max) => max.to_string(),
            None => "∞".to_string(),
        };

        let summary = self.last_summary.as_deref().unwrap_or("starting fresh");
        let run_output = self.last_run_output.as_deref().unwrap_or("(none)");

        format!(
            "{}\n\n--- Loop Context (Iteration {}/{}) ---\n\nCurrent plan ({}):\n{}\n\nPrevious iteration summary:\n{}\n\nPrevious validation output:\n{}\n\n--- Instructions ---\n- Choose ONE task from the plan. Do not implement multiple things.\n- Before writing code, search the codebase with grep/find_files first.\n- After implementing: run the tests for the changed code.\n- Keep LOOP_PLAN.md up to date: mark completed items, add new findings.\n- If you discover bugs unrelated to your task, document them in LOOP_PLAN.md.\n- Commit working changes with descriptive messages.",
            self.prompt,
            self.iteration,
            max_label,
            self.plan_file.display(),
            plan_contents,
            summary,
            run_output,
        )
    }

    pub fn iteration_label(&self) -> String {
        let max_label = match self.max_iterations {
            Some(max) => max.to_string(),
            None => "∞".to_string(),
        };
        format!("LOOP {}/{}", self.iteration, max_label)
    }

    pub fn should_stop(&self) -> bool {
        match self.max_iterations {
            Some(max) => self.iteration >= max,
            None => false,
        }
    }

    /// dirge-vpma.15: advance to the next iteration, or report that the
    /// loop is done. Returns `false` WITHOUT incrementing once the max is
    /// reached, so `--loop-max N` runs exactly N iterations. The previous
    /// caller incremented before the `should_stop` check, so N ran N-1
    /// times (and N=1 ran zero). `None` max = infinite.
    pub fn next_iteration(&mut self) -> bool {
        if self.should_stop() {
            return false;
        }
        self.iteration += 1;
        true
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn state(max: Option<u32>) -> LoopState {
        LoopState::new(String::new(), PathBuf::from("plan.md"), max, None)
    }

    #[test]
    fn loop_max_one_runs_exactly_one_iteration() {
        let mut s = state(Some(1));
        assert!(s.next_iteration(), "first iteration runs");
        assert_eq!(s.iteration, 1);
        assert!(!s.next_iteration(), "second call stops");
        assert_eq!(s.iteration, 1, "iteration must not advance past max");
    }

    #[test]
    fn loop_max_three_runs_exactly_three() {
        let mut s = state(Some(3));
        assert!(s.next_iteration());
        assert!(s.next_iteration());
        assert!(s.next_iteration());
        assert!(!s.next_iteration());
        assert_eq!(s.iteration, 3);
    }

    #[test]
    fn loop_no_max_never_stops() {
        let mut s = state(None);
        for _ in 0..100 {
            assert!(s.next_iteration());
        }
        assert_eq!(s.iteration, 100);
    }
}