Skip to main content

autom8/
runner.rs

1use crate::claude::{
2    run_corrector, run_for_commit, run_for_spec_generation, run_reviewer, ClaudeOutcome,
3    ClaudeRunner, ClaudeStoryResult, CommitOutcome, CorrectorOutcome, ReviewOutcome,
4};
5use crate::config::get_effective_config;
6use crate::display::{BannerColor, StoryResult};
7use crate::error::{Autom8Error, Result};
8use crate::gh::{create_pull_request, PRResult};
9use crate::git;
10use crate::output::{
11    print_all_complete, print_breadcrumb_trail, print_claude_output, print_error_panel,
12    print_full_progress, print_generating_spec, print_header, print_info, print_interrupted,
13    print_issues_found, print_iteration_complete, print_iteration_start,
14    print_max_review_iterations, print_phase_banner, print_phase_footer, print_pr_already_exists,
15    print_pr_skipped, print_pr_success, print_pr_updated, print_proceeding_to_implementation,
16    print_project_info, print_resuming_interrupted, print_review_passed, print_reviewing,
17    print_run_completed, print_run_summary, print_skip_review, print_spec_generated,
18    print_spec_loaded, print_state_transition, print_story_complete, print_tasks_progress,
19    print_worktree_context, print_worktree_created, print_worktree_reused, BOLD, CYAN, GRAY, RESET,
20    YELLOW,
21};
22use crate::progress::{
23    AgentDisplay, Breadcrumb, BreadcrumbState, ClaudeSpinner, Outcome, VerboseTimer,
24};
25use crate::signal::SignalHandler;
26use crate::spec::{Spec, UserStory};
27use crate::state::{IterationStatus, LiveState, MachineState, RunState, RunStatus, StateManager};
28use crate::worktree::{
29    ensure_worktree, format_worktree_error, generate_session_id, generate_worktree_path,
30    is_in_worktree, remove_worktree, WorktreeResult,
31};
32use std::fs;
33use std::path::{Path, PathBuf};
34use std::time::{Duration, Instant};
35
36// ============================================================================
37// Constants
38// ============================================================================
39
40/// Maximum number of review/correct iterations before giving up.
41/// This prevents infinite loops when the corrector cannot resolve review issues.
42const MAX_REVIEW_ITERATIONS: u32 = 3;
43
44// ============================================================================
45// Progress Display Helper (US-006)
46// ============================================================================
47
48/// Flush thresholds for live output (US-003).
49/// Flush when either threshold is reached.
50const LIVE_FLUSH_INTERVAL_MS: u64 = 200;
51const LIVE_FLUSH_LINE_COUNT: usize = 10;
52
53/// Heartbeat interval for indicating the run is still active (US-002).
54/// The heartbeat is updated every 2-3 seconds.
55const HEARTBEAT_INTERVAL_MS: u64 = 2500;
56
57/// Helper struct that wraps a callback and periodically flushes output to live.json.
58/// Flushes every ~200ms or every ~10 lines, whichever comes first.
59/// Also updates heartbeat every ~2.5 seconds to indicate the run is still active.
60struct LiveOutputFlusher<'a> {
61    state_manager: &'a StateManager,
62    live_state: LiveState,
63    line_count_since_flush: usize,
64    last_flush: Instant,
65    last_heartbeat: Instant,
66}
67
68impl<'a> LiveOutputFlusher<'a> {
69    fn new(state_manager: &'a StateManager, machine_state: MachineState) -> Self {
70        let mut flusher = Self {
71            state_manager,
72            live_state: LiveState::new(machine_state),
73            line_count_since_flush: 0,
74            last_flush: Instant::now(),
75            last_heartbeat: Instant::now(),
76        };
77        // Immediately flush to ensure live.json exists with current state
78        flusher.flush();
79        flusher
80    }
81
82    /// Append a line to the buffer and flush if thresholds are met.
83    fn append(&mut self, line: &str) {
84        self.live_state.append_line(line.to_string());
85        self.line_count_since_flush += 1;
86
87        // Check if we should flush output
88        let time_elapsed =
89            self.last_flush.elapsed() >= Duration::from_millis(LIVE_FLUSH_INTERVAL_MS);
90        let lines_threshold = self.line_count_since_flush >= LIVE_FLUSH_LINE_COUNT;
91
92        if time_elapsed || lines_threshold {
93            self.flush();
94        }
95
96        // Check if we should update heartbeat (even if no flush needed)
97        self.maybe_update_heartbeat();
98    }
99
100    /// Update heartbeat if the interval has elapsed.
101    /// This ensures the heartbeat is updated even during periods of low output.
102    fn maybe_update_heartbeat(&mut self) {
103        if self.last_heartbeat.elapsed() >= Duration::from_millis(HEARTBEAT_INTERVAL_MS) {
104            self.live_state.update_heartbeat();
105            self.flush();
106            self.last_heartbeat = Instant::now();
107        }
108    }
109
110    /// Flush the current state to live.json.
111    fn flush(&mut self) {
112        // Update heartbeat on every flush to keep it fresh
113        self.live_state.update_heartbeat();
114        // Ignore errors - live output is best-effort for monitoring
115        let _ = self.state_manager.save_live(&self.live_state);
116        self.line_count_since_flush = 0;
117        self.last_flush = Instant::now();
118        self.last_heartbeat = Instant::now();
119    }
120
121    /// Final flush to ensure all remaining output is written.
122    fn final_flush(&mut self) {
123        if self.line_count_since_flush > 0 {
124            self.flush();
125        }
126    }
127}
128
129/// Flush live.json immediately with a state update.
130/// This is used outside of Claude operations (e.g., during state transitions)
131/// to ensure the GUI sees state changes immediately.
132fn flush_live_state(state_manager: &StateManager, machine_state: MachineState) {
133    let live_state = LiveState::new(machine_state);
134    let _ = state_manager.save_live(&live_state);
135}
136
137/// Runs an operation with either a verbose timer or spinner display,
138/// handling the display lifecycle (start, update, finish with outcome).
139///
140/// This eliminates the duplicate verbose/spinner branching pattern throughout
141/// the codebase by abstracting the display logic into a single helper.
142///
143/// # Arguments
144/// * `verbose` - Whether to use verbose mode (timer) or spinner mode
145/// * `create_timer` - Factory function to create a VerboseTimer
146/// * `create_spinner` - Factory function to create a ClaudeSpinner
147/// * `run_operation` - The operation to run, receiving a callback for progress updates
148/// * `map_outcome` - Maps the operation result to an Outcome for display
149///
150/// # Returns
151/// The result of the operation, after the display has been finished with the appropriate outcome.
152fn with_progress_display<T, F, M>(
153    verbose: bool,
154    create_timer: impl FnOnce() -> VerboseTimer,
155    create_spinner: impl FnOnce() -> ClaudeSpinner,
156    run_operation: F,
157    map_outcome: M,
158) -> Result<T>
159where
160    F: FnOnce(&mut dyn FnMut(&str)) -> Result<T>,
161    M: FnOnce(&Result<T>) -> Outcome,
162{
163    if verbose {
164        let mut timer = create_timer();
165        let result = run_operation(&mut |line| {
166            print_claude_output(line);
167        });
168        let outcome = map_outcome(&result);
169        timer.finish_with_outcome(outcome);
170        result
171    } else {
172        let mut spinner = create_spinner();
173        let result = run_operation(&mut |line| {
174            spinner.update(line);
175        });
176        let outcome = map_outcome(&result);
177        spinner.finish_with_outcome(outcome);
178        result
179    }
180}
181
182/// Runs an operation with progress display and live output streaming to live.json.
183///
184/// Similar to `with_progress_display`, but also writes streaming output to live.json
185/// for the monitor command to read. Flushes every ~200ms or ~10 lines.
186///
187/// # Arguments
188/// * `verbose` - Whether to use verbose mode (timer) or spinner mode
189/// * `state_manager` - StateManager for writing live output
190/// * `machine_state` - Current machine state to include in live.json
191/// * `create_timer` - Factory function to create a VerboseTimer
192/// * `create_spinner` - Factory function to create a ClaudeSpinner
193/// * `run_operation` - The operation to run, receiving a callback for progress updates
194/// * `map_outcome` - Maps the operation result to an Outcome for display
195///
196/// # Returns
197/// The result of the operation, after the display has been finished with the appropriate outcome.
198fn with_progress_display_and_live<T, F, M>(
199    verbose: bool,
200    state_manager: &StateManager,
201    machine_state: MachineState,
202    create_timer: impl FnOnce() -> VerboseTimer,
203    create_spinner: impl FnOnce() -> ClaudeSpinner,
204    run_operation: F,
205    map_outcome: M,
206) -> Result<T>
207where
208    F: FnOnce(&mut dyn FnMut(&str)) -> Result<T>,
209    M: FnOnce(&Result<T>) -> Outcome,
210{
211    let mut live_flusher = LiveOutputFlusher::new(state_manager, machine_state);
212
213    let result = if verbose {
214        let mut timer = create_timer();
215        let result = run_operation(&mut |line| {
216            print_claude_output(line);
217            live_flusher.append(line);
218        });
219        let outcome = map_outcome(&result);
220        timer.finish_with_outcome(outcome);
221        result
222    } else {
223        let mut spinner = create_spinner();
224        let result = run_operation(&mut |line| {
225            spinner.update(line);
226            live_flusher.append(line);
227        });
228        let outcome = map_outcome(&result);
229        spinner.finish_with_outcome(outcome);
230        result
231    };
232
233    // Ensure any remaining output is flushed
234    live_flusher.final_flush();
235
236    result
237}
238
239/// Control flow action returned from extracted helper methods
240/// to communicate back to the main implementation loop.
241enum LoopAction {
242    /// Continue to the next iteration of the main loop
243    Continue,
244    /// Break out of the main loop (run complete)
245    Break,
246}
247
248/// Context for worktree setup that tracks partial state for cleanup on interruption.
249///
250/// This struct is used to track the state of worktree setup so that if the process
251/// is interrupted mid-setup, we can properly clean up:
252/// - If worktree was created but not yet changed into, remove the worktree
253/// - If CWD was changed, restore the original CWD
254/// - If session metadata wasn't saved, the worktree may be orphaned
255#[derive(Debug, Clone)]
256pub struct WorktreeSetupContext {
257    /// The original working directory before any changes
258    pub original_cwd: PathBuf,
259    /// The worktree path if one was created (but may not have been entered yet)
260    pub worktree_path: Option<PathBuf>,
261    /// Whether the worktree was newly created (vs reused)
262    pub worktree_was_created: bool,
263    /// Whether we've changed into the worktree directory
264    pub cwd_changed: bool,
265    /// Whether session metadata has been saved
266    pub metadata_saved: bool,
267}
268
269impl WorktreeSetupContext {
270    /// Create a new setup context, capturing the current working directory.
271    pub fn new() -> Result<Self> {
272        let original_cwd = std::env::current_dir()?;
273        Ok(Self {
274            original_cwd,
275            worktree_path: None,
276            worktree_was_created: false,
277            cwd_changed: false,
278            metadata_saved: false,
279        })
280    }
281
282    /// Clean up partial worktree setup on interruption.
283    ///
284    /// This method:
285    /// 1. Restores the original CWD if it was changed
286    /// 2. Removes the worktree if it was newly created and metadata wasn't saved
287    pub fn cleanup_on_interruption(&self) {
288        // First, restore original CWD if it was changed
289        if self.cwd_changed {
290            if let Err(e) = std::env::set_current_dir(&self.original_cwd) {
291                eprintln!(
292                    "Warning: failed to restore original directory '{}': {}",
293                    self.original_cwd.display(),
294                    e
295                );
296            }
297        }
298
299        // Only remove the worktree if:
300        // - It was newly created (not reused)
301        // - Metadata wasn't saved (session isn't trackable)
302        if self.worktree_was_created && !self.metadata_saved {
303            if let Some(ref worktree_path) = self.worktree_path {
304                // Try to remove the worktree (force=true to handle incomplete state)
305                if let Err(e) = remove_worktree(worktree_path, true) {
306                    eprintln!(
307                        "Warning: failed to remove partial worktree '{}': {}",
308                        worktree_path.display(),
309                        e
310                    );
311                }
312            }
313        }
314    }
315}
316
317pub struct Runner {
318    state_manager: StateManager,
319    verbose: bool,
320    skip_review: bool,
321    /// Override for the worktree config setting.
322    /// None = use config value, Some(true/false) = override config.
323    worktree_override: Option<bool>,
324    /// Override for the commit config setting.
325    /// None = use config value, Some(true/false) = override config.
326    commit_override: Option<bool>,
327    /// Override for the pull_request config setting.
328    /// None = use config value, Some(true/false) = override config.
329    pull_request_override: Option<bool>,
330}
331
332impl Runner {
333    pub fn new() -> Result<Self> {
334        Ok(Self {
335            state_manager: StateManager::new()?,
336            verbose: false,
337            skip_review: false,
338            worktree_override: None,
339            commit_override: None,
340            pull_request_override: None,
341        })
342    }
343
344    pub fn with_verbose(mut self, verbose: bool) -> Self {
345        self.verbose = verbose;
346        self
347    }
348
349    pub fn with_skip_review(mut self, skip_review: bool) -> Self {
350        self.skip_review = skip_review;
351        self
352    }
353
354    /// Set the worktree mode override.
355    ///
356    /// When set, this overrides the `worktree` setting from the config file.
357    /// Use `true` to enable worktree mode (--worktree flag).
358    /// Use `false` to disable worktree mode (--no-worktree flag).
359    pub fn with_worktree(mut self, worktree: bool) -> Self {
360        self.worktree_override = Some(worktree);
361        self
362    }
363
364    /// Set the commit mode override.
365    ///
366    /// When set, this overrides the `commit` setting from the config file.
367    pub fn with_commit(mut self, commit: bool) -> Self {
368        self.commit_override = Some(commit);
369        self
370    }
371
372    /// Set the pull_request mode override.
373    ///
374    /// When set, this overrides the `pull_request` setting from the config file.
375    pub fn with_pull_request(mut self, pull_request: bool) -> Self {
376        self.pull_request_override = Some(pull_request);
377        self
378    }
379
380    /// Get the effective worktree mode, considering CLI override and config.
381    ///
382    /// Priority: CLI flag > config file > default (false).
383    #[allow(dead_code)]
384    pub fn effective_worktree(&self) -> Result<bool> {
385        if let Some(override_value) = self.worktree_override {
386            return Ok(override_value);
387        }
388        let config = get_effective_config()?;
389        Ok(config.worktree)
390    }
391
392    /// Load the effective config, applying any CLI overrides.
393    fn load_config_with_override(&self) -> Result<crate::config::Config> {
394        let mut config = get_effective_config()?;
395
396        // Apply commit override if set
397        if let Some(commit) = self.commit_override {
398            config.commit = commit;
399        }
400
401        // Apply pull_request override if set
402        if let Some(pull_request) = self.pull_request_override {
403            config.pull_request = pull_request;
404        }
405
406        Ok(config)
407    }
408
409    /// Check if worktree mode is effective (considering CLI override and config).
410    fn is_worktree_mode(&self, config: &crate::config::Config) -> bool {
411        if let Some(override_value) = self.worktree_override {
412            return override_value;
413        }
414        config.worktree
415    }
416
417    /// Flush live.json with the current state without saving state.
418    /// Used when we want to update live.json but state has already been saved.
419    fn flush_live(&self, machine_state: MachineState) {
420        flush_live_state(&self.state_manager, machine_state);
421    }
422
423    /// Setup worktree context for a run.
424    ///
425    /// When worktree mode is enabled, this will:
426    /// 1. Create or reuse a worktree for the specified branch
427    /// 2. Change the current working directory to the worktree
428    /// 3. Generate a new session ID for the worktree
429    ///
430    /// Returns a tuple of:
431    /// - The session ID and worktree path if a worktree was created/reused, or None if not in worktree mode
432    /// - The setup context for cleanup on interruption
433    fn setup_worktree_context(
434        &self,
435        config: &crate::config::Config,
436        branch_name: &str,
437    ) -> Result<(Option<(String, PathBuf)>, WorktreeSetupContext)> {
438        // Create setup context to track partial state for cleanup
439        let mut setup_ctx = WorktreeSetupContext::new()?;
440
441        // Check if worktree mode is enabled
442        if !self.is_worktree_mode(config) {
443            return Ok((None, setup_ctx));
444        }
445
446        // Check if we're in a git repo
447        if !git::is_git_repo() {
448            print_info(
449                "Worktree mode enabled but not in a git repository. Running in current directory.",
450            );
451            return Ok((None, setup_ctx));
452        }
453
454        // Create or reuse worktree
455        let pattern = &config.worktree_path_pattern;
456        let result = ensure_worktree(pattern, branch_name).map_err(|e| {
457            // Provide enhanced error message for worktree failures
458            if let Autom8Error::WorktreeError(msg) = &e {
459                let worktree_path = generate_worktree_path(pattern, branch_name)
460                    .unwrap_or_else(|_| PathBuf::from("<unknown>"));
461                let formatted = format_worktree_error(msg, branch_name, &worktree_path);
462                Autom8Error::WorktreeError(formatted)
463            } else {
464                e
465            }
466        })?;
467
468        // Get the worktree path and inform the user
469        let worktree_path = result.path().to_path_buf();
470        setup_ctx.worktree_path = Some(worktree_path.clone());
471        setup_ctx.worktree_was_created = result.was_created();
472
473        match result {
474            WorktreeResult::Created(_) => {
475                print_worktree_created(&worktree_path, branch_name);
476            }
477            WorktreeResult::Reused(_) => {
478                print_worktree_reused(&worktree_path, branch_name);
479            }
480        }
481
482        // Change to the worktree directory
483        std::env::set_current_dir(&worktree_path).map_err(|e| {
484            // If we can't change to the worktree, clean up if we created it
485            setup_ctx.cleanup_on_interruption();
486            Autom8Error::WorktreeError(format!(
487                "Failed to change to worktree directory '{}': {}",
488                worktree_path.display(),
489                e
490            ))
491        })?;
492        setup_ctx.cwd_changed = true;
493
494        // Print context info
495        print_worktree_context(&worktree_path);
496
497        // Generate session ID for the worktree
498        let session_id = generate_session_id(&worktree_path);
499
500        Ok((Some((session_id, worktree_path)), setup_ctx))
501    }
502
503    /// Handle a fatal error by transitioning to Failed state, saving, displaying error, and optionally printing summary.
504    /// This standardizes error handling across the runner to ensure Failed state is always persisted before returning errors.
505    #[allow(clippy::too_many_arguments)]
506    fn handle_fatal_error<F>(
507        &self,
508        state: &mut RunState,
509        error_panel_title: &str,
510        error_panel_msg: &str,
511        exit_code: Option<i32>,
512        stderr: Option<&str>,
513        print_summary: Option<F>,
514        error: Autom8Error,
515    ) -> Autom8Error
516    where
517        F: FnOnce() -> Result<()>,
518    {
519        // Always transition to Failed state first
520        state.transition_to(MachineState::Failed);
521
522        // Always persist the failed state before returning error
523        if let Err(e) = self.state_manager.save(state) {
524            // If we can't save state, log it but continue with the original error
525            eprintln!("Warning: failed to save state: {}", e);
526        }
527
528        // Display error panel (unless title is empty, for cases like max iterations)
529        if !error_panel_title.is_empty() {
530            print_error_panel(error_panel_title, error_panel_msg, exit_code, stderr);
531        }
532
533        // Print summary if provided
534        if let Some(summary_fn) = print_summary {
535            if let Err(e) = summary_fn() {
536                eprintln!("Warning: failed to print summary: {}", e);
537            }
538        }
539
540        error
541    }
542
543    /// Handle graceful shutdown on SIGINT.
544    ///
545    /// This method:
546    /// 1. Kills any running Claude subprocess
547    /// 2. Updates state status to `Interrupted` (preserving the current machine_state)
548    /// 3. Saves state and session metadata (`is_running: false`)
549    /// 4. Clears the live output file
550    /// 5. Restores original CWD if it was changed during worktree setup
551    /// 6. Displays interruption message to user
552    ///
553    /// Returns `Err(Autom8Error::Interrupted)` to signal the run was interrupted.
554    fn handle_interruption(
555        &self,
556        state: &mut RunState,
557        claude_runner: &ClaudeRunner,
558        worktree_setup_ctx: Option<&WorktreeSetupContext>,
559    ) -> Autom8Error {
560        // Kill any running Claude subprocess
561        if let Err(e) = claude_runner.kill() {
562            eprintln!("Warning: failed to kill Claude subprocess: {}", e);
563        }
564
565        // Update state to Interrupted (preserves machine_state)
566        state.status = RunStatus::Interrupted;
567        state.finished_at = Some(chrono::Utc::now());
568
569        // Save state and session metadata (is_running will be set to false
570        // because status is Interrupted, not Running)
571        if let Err(e) = self.state_manager.save(state) {
572            eprintln!("Warning: failed to save state: {}", e);
573        }
574
575        // Clear live output file
576        if let Err(e) = self.state_manager.clear_live() {
577            eprintln!("Warning: failed to clear live output: {}", e);
578        }
579
580        // Clean up worktree setup if needed (restores CWD, removes partial worktree)
581        if let Some(setup_ctx) = worktree_setup_ctx {
582            setup_ctx.cleanup_on_interruption();
583        }
584
585        // Display message to user
586        print_interrupted();
587
588        Autom8Error::Interrupted
589    }
590
591    /// Run the review/correct loop until review passes or max iterations reached.
592    /// Returns Ok(()) if review passes, Err if max iterations exceeded or error occurs.
593    fn run_review_correct_loop(
594        &self,
595        state: &mut RunState,
596        spec: &Spec,
597        breadcrumb: &mut Breadcrumb,
598        story_results: &[StoryResult],
599        print_summary_fn: &impl Fn(u32, &[StoryResult]) -> Result<()>,
600    ) -> Result<()> {
601        state.review_iteration = 1;
602
603        loop {
604            // Check if we've exceeded max review iterations
605            if state.review_iteration > MAX_REVIEW_ITERATIONS {
606                print_max_review_iterations();
607                let iteration = state.iteration;
608                let results = story_results;
609                return Err(self.handle_fatal_error(
610                    state,
611                    "", // No error panel for max iterations (has its own message)
612                    "",
613                    None,
614                    None,
615                    Some(|| print_summary_fn(iteration, results)),
616                    Autom8Error::MaxReviewIterationsReached,
617                ));
618            }
619
620            // Transition to Reviewing state
621            print_state_transition(state.machine_state, MachineState::Reviewing);
622            state.transition_to(MachineState::Reviewing);
623            self.state_manager.save(state)?;
624            self.flush_live(MachineState::Reviewing);
625
626            // Update breadcrumb to enter Review state
627            breadcrumb.enter_state(BreadcrumbState::Review);
628
629            print_phase_banner("REVIEWING", BannerColor::Cyan);
630            print_reviewing(state.review_iteration, MAX_REVIEW_ITERATIONS);
631
632            // Run reviewer with progress display and live output (for heartbeat updates)
633            let review_iter = state.review_iteration;
634            let review_result = with_progress_display_and_live(
635                self.verbose,
636                &self.state_manager,
637                MachineState::Reviewing,
638                || VerboseTimer::new_for_review(review_iter, MAX_REVIEW_ITERATIONS),
639                || ClaudeSpinner::new_for_review(review_iter, MAX_REVIEW_ITERATIONS),
640                |callback| run_reviewer(spec, review_iter, MAX_REVIEW_ITERATIONS, callback),
641                |res| match res {
642                    Ok(r) => {
643                        let tokens = r.usage.as_ref().map(|u| u.total_tokens());
644                        match &r.outcome {
645                            ReviewOutcome::Pass => {
646                                Outcome::success("No issues found").with_optional_tokens(tokens)
647                            }
648                            ReviewOutcome::IssuesFound => {
649                                Outcome::success("Issues found").with_optional_tokens(tokens)
650                            }
651                            ReviewOutcome::Error(e) => Outcome::failure(e.to_string()),
652                        }
653                    }
654                    Err(e) => Outcome::failure(e.to_string()),
655                },
656            )?;
657
658            // Capture usage from review into "Final Review" phase (US-005)
659            state.capture_usage("Final Review", review_result.usage.clone());
660
661            // Print bottom border to close the output frame
662            print_phase_footer(BannerColor::Cyan);
663
664            // Print breadcrumb trail after review phase completion
665            print_breadcrumb_trail(breadcrumb);
666
667            // Show progress bar after review task completion
668            print_full_progress(
669                spec.completed_count(),
670                spec.total_count(),
671                state.review_iteration,
672                MAX_REVIEW_ITERATIONS,
673            );
674            println!();
675
676            match review_result.outcome {
677                ReviewOutcome::Pass => {
678                    // Delete autom8_review.md if it exists
679                    let review_path = std::path::Path::new("autom8_review.md");
680                    if review_path.exists() {
681                        let _ = fs::remove_file(review_path);
682                    }
683                    // Save state with captured review usage before exiting (US-005)
684                    self.state_manager.save(state)?;
685                    print_review_passed();
686                    return Ok(()); // Exit review loop, proceed to commit
687                }
688                ReviewOutcome::IssuesFound => {
689                    // Transition to Correcting state
690                    print_state_transition(MachineState::Reviewing, MachineState::Correcting);
691                    state.transition_to(MachineState::Correcting);
692                    self.state_manager.save(state)?;
693                    self.flush_live(MachineState::Correcting);
694
695                    // Update breadcrumb to enter Correct state
696                    breadcrumb.enter_state(BreadcrumbState::Correct);
697
698                    print_phase_banner("CORRECTING", BannerColor::Yellow);
699                    print_issues_found(state.review_iteration, MAX_REVIEW_ITERATIONS);
700
701                    // Run corrector with progress display and live output (for heartbeat updates)
702                    let corrector_result = with_progress_display_and_live(
703                        self.verbose,
704                        &self.state_manager,
705                        MachineState::Correcting,
706                        || VerboseTimer::new_for_correct(review_iter, MAX_REVIEW_ITERATIONS),
707                        || ClaudeSpinner::new_for_correct(review_iter, MAX_REVIEW_ITERATIONS),
708                        |callback| run_corrector(spec, review_iter, callback),
709                        |res| match res {
710                            Ok(r) => {
711                                let tokens = r.usage.as_ref().map(|u| u.total_tokens());
712                                match &r.outcome {
713                                    CorrectorOutcome::Complete => {
714                                        Outcome::success("Issues addressed")
715                                            .with_optional_tokens(tokens)
716                                    }
717                                    CorrectorOutcome::Error(e) => Outcome::failure(e.to_string()),
718                                }
719                            }
720                            Err(e) => Outcome::failure(e.to_string()),
721                        },
722                    )?;
723
724                    // Capture usage from correction into "Final Review" phase (US-005)
725                    // This accumulates with the review usage since both are part of the review loop
726                    state.capture_usage("Final Review", corrector_result.usage.clone());
727
728                    // Print bottom border to close the output frame
729                    print_phase_footer(BannerColor::Yellow);
730
731                    // Print breadcrumb trail after correct phase completion
732                    print_breadcrumb_trail(breadcrumb);
733
734                    // Show progress bar after correct task completion
735                    print_full_progress(
736                        spec.completed_count(),
737                        spec.total_count(),
738                        state.review_iteration,
739                        MAX_REVIEW_ITERATIONS,
740                    );
741                    println!();
742
743                    match corrector_result.outcome {
744                        CorrectorOutcome::Complete => {
745                            // Increment review iteration and loop back to Reviewing
746                            state.review_iteration += 1;
747                            // Save state with captured corrector usage (US-005)
748                            self.state_manager.save(state)?;
749                        }
750                        CorrectorOutcome::Error(e) => {
751                            let iteration = state.iteration;
752                            let results = story_results;
753                            return Err(self.handle_fatal_error(
754                                state,
755                                "Corrector Failed",
756                                &e.message,
757                                e.exit_code,
758                                e.stderr.as_deref(),
759                                Some(|| print_summary_fn(iteration, results)),
760                                Autom8Error::ClaudeError(format!("Corrector failed: {}", e)),
761                            ));
762                        }
763                    }
764                }
765                ReviewOutcome::Error(e) => {
766                    let iteration = state.iteration;
767                    let results = story_results;
768                    return Err(self.handle_fatal_error(
769                        state,
770                        "Review Failed",
771                        &e.message,
772                        e.exit_code,
773                        e.stderr.as_deref(),
774                        Some(|| print_summary_fn(iteration, results)),
775                        Autom8Error::ClaudeError(format!("Review failed: {}", e)),
776                    ));
777                }
778            }
779        }
780    }
781
782    /// Handle commit and PR creation flow after all stories are complete.
783    /// Returns Ok(()) on success, Err on failure.
784    /// Respects config settings: if commit=false, skips commit state entirely.
785    /// If pull_request=false, skips PR creation (ends after commit or immediately if commit=false).
786    fn handle_commit_and_pr(
787        &self,
788        state: &mut RunState,
789        spec: &Spec,
790        breadcrumb: &mut Breadcrumb,
791    ) -> Result<()> {
792        // Get the effective config for this run (US-005)
793        let config = state.effective_config();
794
795        // If commit=false, skip commit state entirely
796        if !config.commit {
797            print_state_transition(state.machine_state, MachineState::Completed);
798            print_info("Skipping commit (commit = false in config)");
799            return Ok(());
800        }
801
802        if !git::is_git_repo() {
803            print_state_transition(state.machine_state, MachineState::Completed);
804            return Ok(());
805        }
806
807        print_state_transition(state.machine_state, MachineState::Committing);
808        state.transition_to(MachineState::Committing);
809        self.state_manager.save(state)?;
810        self.flush_live(MachineState::Committing);
811
812        // Update breadcrumb to enter Commit state
813        breadcrumb.enter_state(BreadcrumbState::Commit);
814
815        print_phase_banner("COMMITTING", BannerColor::Cyan);
816
817        // Run commit with progress display and live output (for heartbeat updates)
818        let commit_result = with_progress_display_and_live(
819            self.verbose,
820            &self.state_manager,
821            MachineState::Committing,
822            VerboseTimer::new_for_commit,
823            ClaudeSpinner::new_for_commit,
824            |callback| run_for_commit(spec, callback),
825            |res| match res {
826                Ok(r) => {
827                    let tokens = r.usage.as_ref().map(|u| u.total_tokens());
828                    match &r.outcome {
829                        CommitOutcome::Success(hash) => {
830                            Outcome::success(hash.clone()).with_optional_tokens(tokens)
831                        }
832                        CommitOutcome::NothingToCommit => {
833                            Outcome::success("Nothing to commit").with_optional_tokens(tokens)
834                        }
835                        CommitOutcome::Error(e) => Outcome::failure(e.to_string()),
836                    }
837                }
838                Err(e) => Outcome::failure(e.to_string()),
839            },
840        )?;
841
842        // Capture usage from commit into "PR & Commit" phase (US-005)
843        state.capture_usage("PR & Commit", commit_result.usage.clone());
844        self.state_manager.save(state)?;
845
846        // Print bottom border to close the output frame
847        print_phase_footer(BannerColor::Cyan);
848
849        // Print breadcrumb trail after commit phase completion
850        print_breadcrumb_trail(breadcrumb);
851
852        // Track whether commits were made for PR creation
853        let commits_were_made = matches!(&commit_result.outcome, CommitOutcome::Success(_));
854
855        match &commit_result.outcome {
856            CommitOutcome::Success(hash) => {
857                print_info(&format!("Changes committed successfully ({})", hash))
858            }
859            CommitOutcome::NothingToCommit => print_info("Nothing to commit"),
860            CommitOutcome::Error(e) => {
861                print_error_panel(
862                    "Commit Failed",
863                    &e.message,
864                    e.exit_code,
865                    e.stderr.as_deref(),
866                );
867            }
868        }
869
870        // Skip PR creation if pull_request=false (US-005)
871        if !config.pull_request {
872            print_state_transition(MachineState::Committing, MachineState::Completed);
873            print_info("Skipping PR creation (pull_request = false in config)");
874            return Ok(());
875        }
876
877        // PR Creation step
878        self.handle_pr_creation(state, spec, commits_were_made, config.pull_request_draft)
879    }
880
881    /// Handle PR creation after committing.
882    fn handle_pr_creation(
883        &self,
884        state: &mut RunState,
885        spec: &Spec,
886        commits_were_made: bool,
887        draft: bool,
888    ) -> Result<()> {
889        print_state_transition(MachineState::Committing, MachineState::CreatingPR);
890        state.transition_to(MachineState::CreatingPR);
891        self.state_manager.save(state)?;
892        self.flush_live(MachineState::CreatingPR);
893
894        match create_pull_request(spec, commits_were_made, draft) {
895            Ok(PRResult::Success(url)) => {
896                print_pr_success(&url);
897                print_state_transition(MachineState::CreatingPR, MachineState::Completed);
898                Ok(())
899            }
900            Ok(PRResult::Skipped(reason)) => {
901                print_pr_skipped(&reason);
902                print_state_transition(MachineState::CreatingPR, MachineState::Completed);
903                Ok(())
904            }
905            Ok(PRResult::AlreadyExists(url)) => {
906                print_pr_already_exists(&url);
907                print_state_transition(MachineState::CreatingPR, MachineState::Completed);
908                Ok(())
909            }
910            Ok(PRResult::Updated(url)) => {
911                print_pr_updated(&url);
912                print_state_transition(MachineState::CreatingPR, MachineState::Completed);
913                Ok(())
914            }
915            Ok(PRResult::Error(msg)) => {
916                print_state_transition(MachineState::CreatingPR, MachineState::Failed);
917                Err(self.handle_fatal_error(
918                    state,
919                    "PR Creation Failed",
920                    &msg,
921                    None,
922                    None,
923                    None::<fn() -> Result<()>>,
924                    Autom8Error::ClaudeError(format!("PR creation failed: {}", msg)),
925                ))
926            }
927            Err(e) => {
928                print_state_transition(MachineState::CreatingPR, MachineState::Failed);
929                Err(self.handle_fatal_error(
930                    state,
931                    "PR Creation Error",
932                    &e.to_string(),
933                    None,
934                    None,
935                    None::<fn() -> Result<()>>,
936                    e,
937                ))
938            }
939        }
940    }
941
942    /// Handle the flow when all stories are complete at iteration start.
943    /// Returns LoopAction::Break on success (run complete).
944    fn handle_all_stories_complete(
945        &self,
946        state: &mut RunState,
947        spec: &Spec,
948        breadcrumb: &mut Breadcrumb,
949        story_results: &[StoryResult],
950        print_summary_fn: &impl Fn(u32, &[StoryResult]) -> Result<()>,
951    ) -> Result<LoopAction> {
952        print_all_complete();
953
954        // Get the effective config for this run (US-005)
955        let config = state.effective_config();
956
957        // Skip review if --skip-review flag is set OR if review=false in config
958        if self.skip_review || !config.review {
959            print_skip_review();
960        } else {
961            // Run review/correct loop
962            self.run_review_correct_loop(state, spec, breadcrumb, story_results, print_summary_fn)?;
963        }
964
965        // Commit changes and create PR (respects commit and pull_request config)
966        self.handle_commit_and_pr(state, spec, breadcrumb)?;
967
968        state.transition_to(MachineState::Completed);
969        // Flush live state to ensure GUI sees the Completed state before cleanup
970        self.flush_live(MachineState::Completed);
971        self.state_manager.save(state)?;
972        print_summary_fn(state.iteration, story_results)?;
973
974        // Print final run completion message with total tokens (US-007)
975        let duration_secs = state.run_duration_secs();
976        let total_tokens = state.total_usage.as_ref().map(|u| u.total_tokens());
977        print_run_completed(duration_secs, total_tokens);
978
979        self.archive_and_cleanup(state)?;
980        Ok(LoopAction::Break)
981    }
982
983    /// Handle an error from Claude story execution.
984    /// Transitions to Failed state and returns the appropriate error.
985    #[allow(clippy::too_many_arguments)]
986    fn handle_story_error(
987        &self,
988        state: &mut RunState,
989        story: &UserStory,
990        story_results: &mut Vec<StoryResult>,
991        story_start: Instant,
992        error_msg: &str,
993        error_panel_title: &str,
994        error_panel_msg: &str,
995        exit_code: Option<i32>,
996        stderr: Option<&str>,
997        print_summary_fn: &impl Fn(u32, &[StoryResult]) -> Result<()>,
998    ) -> Result<LoopAction> {
999        state.finish_iteration(IterationStatus::Failed, error_msg.to_string());
1000        state.transition_to(MachineState::Failed);
1001        // Clear live output when iteration finishes (US-003)
1002        let _ = self.state_manager.clear_live();
1003        self.state_manager.save(state)?;
1004
1005        story_results.push(StoryResult {
1006            id: story.id.clone(),
1007            title: story.title.clone(),
1008            passed: false,
1009            duration_secs: story_start.elapsed().as_secs(),
1010        });
1011
1012        print_error_panel(error_panel_title, error_panel_msg, exit_code, stderr);
1013        print_summary_fn(state.iteration, story_results)?;
1014        Err(Autom8Error::ClaudeError(error_msg.to_string()))
1015    }
1016
1017    /// Handle a single story iteration, processing the Claude result.
1018    /// Returns LoopAction::Continue to continue the loop, LoopAction::Break to finish.
1019    #[allow(clippy::too_many_arguments)]
1020    fn handle_story_iteration(
1021        &self,
1022        state: &mut RunState,
1023        spec: &Spec,
1024        spec_json_path: &Path,
1025        story: &UserStory,
1026        breadcrumb: &mut Breadcrumb,
1027        story_results: &mut Vec<StoryResult>,
1028        story_start: Instant,
1029        claude_runner: &ClaudeRunner,
1030        print_summary_fn: &impl Fn(u32, &[StoryResult]) -> Result<()>,
1031    ) -> Result<LoopAction> {
1032        // Calculate story progress for display: [US-001 2/5]
1033        let story_index = spec
1034            .user_stories
1035            .iter()
1036            .position(|s| s.id == story.id)
1037            .map(|i| i as u32 + 1)
1038            .unwrap_or(state.iteration);
1039        let total_stories = spec.total_count() as u32;
1040        let story_id = story.id.clone();
1041        let iterations = state.iterations.clone();
1042        let knowledge = state.knowledge.clone();
1043
1044        // Run Claude with progress display and live output streaming (US-003)
1045        // Use the provided ClaudeRunner so it can be killed on interrupt
1046        let result = with_progress_display_and_live(
1047            self.verbose,
1048            &self.state_manager,
1049            MachineState::RunningClaude,
1050            || VerboseTimer::new_with_story_progress(&story_id, story_index, total_stories),
1051            || ClaudeSpinner::new_with_story_progress(&story_id, story_index, total_stories),
1052            |callback| {
1053                claude_runner.run(
1054                    spec,
1055                    story,
1056                    spec_json_path,
1057                    &iterations,
1058                    &knowledge,
1059                    callback,
1060                )
1061            },
1062            |res| match res {
1063                Ok(result) => {
1064                    let tokens = result.usage.as_ref().map(|u| u.total_tokens());
1065                    Outcome::success("Implementation done").with_optional_tokens(tokens)
1066                }
1067                Err(e) => Outcome::failure(e.to_string()),
1068            },
1069        );
1070
1071        match result {
1072            Ok(ClaudeStoryResult {
1073                outcome: ClaudeOutcome::AllStoriesComplete,
1074                work_summary,
1075                full_output,
1076                usage,
1077            }) => {
1078                // Capture usage from story implementation (US-005)
1079                state.capture_usage(&story.id, usage.clone());
1080                state.set_iteration_usage(usage);
1081                self.handle_all_stories_complete_from_story(
1082                    state,
1083                    spec,
1084                    spec_json_path,
1085                    story,
1086                    breadcrumb,
1087                    story_results,
1088                    work_summary,
1089                    &full_output,
1090                    print_summary_fn,
1091                )
1092            }
1093            Ok(ClaudeStoryResult {
1094                outcome: ClaudeOutcome::IterationComplete,
1095                work_summary,
1096                full_output,
1097                usage,
1098            }) => {
1099                // Capture usage from story implementation (US-005)
1100                state.capture_usage(&story.id, usage.clone());
1101                state.set_iteration_usage(usage);
1102                self.handle_iteration_complete(
1103                    state,
1104                    spec_json_path,
1105                    story,
1106                    breadcrumb,
1107                    story_results,
1108                    work_summary,
1109                    &full_output,
1110                )
1111            }
1112            Ok(ClaudeStoryResult {
1113                outcome: ClaudeOutcome::Error(error_info),
1114                usage,
1115                ..
1116            }) => {
1117                // Capture usage even on error (partial usage before failure) (US-005)
1118                state.capture_usage(&story.id, usage.clone());
1119                state.set_iteration_usage(usage);
1120                self.handle_story_error(
1121                    state,
1122                    story,
1123                    story_results,
1124                    story_start,
1125                    &error_info.message,
1126                    "Claude Process Failed",
1127                    &error_info.message,
1128                    error_info.exit_code,
1129                    error_info.stderr.as_deref(),
1130                    print_summary_fn,
1131                )
1132            }
1133            Err(e) => self.handle_story_error(
1134                state,
1135                story,
1136                story_results,
1137                story_start,
1138                &e.to_string(),
1139                "Claude Error",
1140                &e.to_string(),
1141                None,
1142                None,
1143                print_summary_fn,
1144            ),
1145        }
1146    }
1147
1148    /// Handle when Claude reports all stories complete during story processing.
1149    #[allow(clippy::too_many_arguments)]
1150    fn handle_all_stories_complete_from_story(
1151        &self,
1152        state: &mut RunState,
1153        spec: &Spec,
1154        spec_json_path: &Path,
1155        story: &UserStory,
1156        breadcrumb: &mut Breadcrumb,
1157        story_results: &mut Vec<StoryResult>,
1158        work_summary: Option<String>,
1159        full_output: &str,
1160        print_summary_fn: &impl Fn(u32, &[StoryResult]) -> Result<()>,
1161    ) -> Result<LoopAction> {
1162        state.finish_iteration(IterationStatus::Success, full_output.to_string());
1163        state.set_work_summary(work_summary.clone());
1164        // Clear live output when iteration finishes (US-003)
1165        let _ = self.state_manager.clear_live();
1166
1167        // Capture story knowledge from git diff and agent output (US-006)
1168        state.capture_story_knowledge(&story.id, full_output, None);
1169        self.state_manager.save(state)?;
1170
1171        let duration = state.current_iteration_duration();
1172        story_results.push(StoryResult {
1173            id: story.id.clone(),
1174            title: story.title.clone(),
1175            passed: true,
1176            duration_secs: duration,
1177        });
1178
1179        // Print bottom border to close the output frame
1180        print_phase_footer(BannerColor::Cyan);
1181
1182        // Print breadcrumb trail after story phase completion
1183        print_breadcrumb_trail(breadcrumb);
1184
1185        // Show progress bar after story task completion
1186        let updated_spec = Spec::load(spec_json_path)?;
1187        print_tasks_progress(updated_spec.completed_count(), updated_spec.total_count());
1188        println!();
1189
1190        if self.verbose {
1191            print_story_complete(&story.id, duration);
1192        }
1193
1194        // Validate that all stories are actually complete
1195        if !updated_spec.all_complete() {
1196            // Spec doesn't match Claude's claim - continue processing stories
1197            return Ok(LoopAction::Continue);
1198        }
1199
1200        print_all_complete();
1201
1202        // Get the effective config for this run (US-005)
1203        let config = state.effective_config();
1204
1205        // Skip review if --skip-review flag is set OR if review=false in config
1206        if self.skip_review || !config.review {
1207            print_skip_review();
1208        } else {
1209            // Run review/correct loop
1210            self.run_review_correct_loop(
1211                state,
1212                &updated_spec,
1213                breadcrumb,
1214                story_results,
1215                print_summary_fn,
1216            )?;
1217        }
1218
1219        // Commit changes and create PR (respects commit and pull_request config)
1220        self.handle_commit_and_pr(state, spec, breadcrumb)?;
1221
1222        state.transition_to(MachineState::Completed);
1223        // Flush live state to ensure GUI sees the Completed state before cleanup
1224        self.flush_live(MachineState::Completed);
1225        self.state_manager.save(state)?;
1226        print_summary_fn(state.iteration, story_results)?;
1227
1228        // Print final run completion message with total tokens (US-007)
1229        let run_duration = state.run_duration_secs();
1230        let total_tokens = state.total_usage.as_ref().map(|u| u.total_tokens());
1231        print_run_completed(run_duration, total_tokens);
1232
1233        self.archive_and_cleanup(state)?;
1234        Ok(LoopAction::Break)
1235    }
1236
1237    /// Handle a normal iteration completion (story done, more to go).
1238    #[allow(clippy::too_many_arguments)]
1239    fn handle_iteration_complete(
1240        &self,
1241        state: &mut RunState,
1242        spec_json_path: &Path,
1243        story: &UserStory,
1244        breadcrumb: &mut Breadcrumb,
1245        story_results: &mut Vec<StoryResult>,
1246        work_summary: Option<String>,
1247        full_output: &str,
1248    ) -> Result<LoopAction> {
1249        state.finish_iteration(IterationStatus::Success, full_output.to_string());
1250        state.set_work_summary(work_summary.clone());
1251        // Clear live output when iteration finishes (US-003)
1252        let _ = self.state_manager.clear_live();
1253
1254        // Capture story knowledge from git diff and agent output (US-006)
1255        state.capture_story_knowledge(&story.id, full_output, None);
1256        self.state_manager.save(state)?;
1257
1258        let duration = state.current_iteration_duration();
1259
1260        // Print bottom border to close the output frame
1261        print_phase_footer(BannerColor::Cyan);
1262
1263        // Print breadcrumb trail after story phase completion
1264        print_breadcrumb_trail(breadcrumb);
1265
1266        print_state_transition(MachineState::RunningClaude, MachineState::PickingStory);
1267        print_iteration_complete(state.iteration);
1268
1269        // Reload spec and check if current story passed
1270        let updated_spec = Spec::load(spec_json_path)?;
1271        let story_passed = updated_spec
1272            .user_stories
1273            .iter()
1274            .find(|s| s.id == story.id)
1275            .is_some_and(|s| s.passes);
1276
1277        if story_passed {
1278            story_results.push(StoryResult {
1279                id: story.id.clone(),
1280                title: story.title.clone(),
1281                passed: true,
1282                duration_secs: duration,
1283            });
1284            if self.verbose {
1285                print_story_complete(&story.id, duration);
1286            }
1287        }
1288
1289        // Show progress bar after story task completion
1290        print_tasks_progress(updated_spec.completed_count(), updated_spec.total_count());
1291        println!();
1292
1293        // Continue to next iteration
1294        Ok(LoopAction::Continue)
1295    }
1296
1297    /// Run from a spec-<feature>.md markdown file - converts to JSON first, then implements
1298    pub fn run_from_spec(&self, spec_path: &Path) -> Result<()> {
1299        // IMPORTANT: State must NOT be persisted until after worktree context is determined.
1300        // Saving state before we know the correct session ID would create phantom sessions
1301        // in the main repo when running in worktree mode. Visual state transitions can be
1302        // displayed, but save() must not be called until the effective StateManager is known.
1303
1304        // Check for existing active run
1305        if self.state_manager.has_active_run()? {
1306            if let Some(state) = self.state_manager.load_current()? {
1307                return Err(Autom8Error::RunInProgress(state.run_id));
1308            }
1309        }
1310
1311        // Load effective config at startup, applying CLI flag override (US-002, US-005)
1312        let config = self.load_config_with_override()?;
1313
1314        // Canonicalize spec path
1315        let spec_path = spec_path
1316            .canonicalize()
1317            .map_err(|_| Autom8Error::SpecNotFound(spec_path.to_path_buf()))?;
1318
1319        // Determine spec JSON output path in config directory
1320        let stem = spec_path
1321            .file_stem()
1322            .and_then(|s| s.to_str())
1323            .unwrap_or("spec");
1324        let spec_dir = self.state_manager.ensure_spec_dir()?;
1325        let spec_json_path = spec_dir.join(format!("{}.json", stem));
1326
1327        // Initialize state with config snapshot for resume support
1328        // Clone config since we need it later for worktree setup
1329        // Note: State is NOT saved here - we defer persistence until after worktree context
1330        // is determined to avoid creating phantom sessions in the main repo
1331        let mut state = RunState::from_spec_with_config(
1332            spec_path.clone(),
1333            spec_json_path.clone(),
1334            config.clone(),
1335        );
1336
1337        // LoadingSpec state
1338        print_state_transition(MachineState::Idle, MachineState::LoadingSpec);
1339
1340        // Load spec content
1341        let spec_content = fs::read_to_string(&spec_path)?;
1342        if spec_content.trim().is_empty() {
1343            return Err(Autom8Error::EmptySpec);
1344        }
1345
1346        let metadata = fs::metadata(&spec_path)?;
1347        print_spec_loaded(&spec_path, metadata.len());
1348        println!();
1349
1350        // Transition to GeneratingSpec
1351        // Note: State is NOT saved here - we defer persistence until after worktree context
1352        // is determined. Visual feedback is still shown via print_state_transition.
1353        state.transition_to(MachineState::GeneratingSpec);
1354        print_state_transition(MachineState::LoadingSpec, MachineState::GeneratingSpec);
1355
1356        print_generating_spec();
1357
1358        // Run Claude to generate spec JSON with progress display
1359        let spec_result = match with_progress_display(
1360            self.verbose,
1361            VerboseTimer::new_for_spec,
1362            ClaudeSpinner::new_for_spec,
1363            |callback| run_for_spec_generation(&spec_content, &spec_json_path, callback),
1364            |res| match res {
1365                Ok(r) => {
1366                    let tokens = r.usage.as_ref().map(|u| u.total_tokens());
1367                    Outcome::success("Spec generated").with_optional_tokens(tokens)
1368                }
1369                Err(e) => Outcome::failure(e.to_string()),
1370            },
1371        ) {
1372            Ok(result) => result,
1373            Err(e) => {
1374                print_error_panel("Spec Generation Failed", &e.to_string(), None, None);
1375                return Err(e);
1376            }
1377        };
1378        let spec = spec_result.spec;
1379
1380        // Capture usage from spec generation into "Planning" phase (US-005)
1381        state.capture_usage("Planning", spec_result.usage);
1382
1383        print_spec_generated(&spec, &spec_json_path);
1384
1385        // Check for branch conflicts with other active sessions (US-006)
1386        // This must happen before any git operations to prevent race conditions
1387        if let Some(conflict) = self
1388            .state_manager
1389            .check_branch_conflict(&spec.branch_name)?
1390        {
1391            return Err(Autom8Error::BranchConflict {
1392                branch: spec.branch_name.clone(),
1393                session_id: conflict.session_id,
1394                worktree_path: conflict.worktree_path,
1395            });
1396        }
1397
1398        // Setup worktree context if enabled (US-007)
1399        // This creates/reuses a worktree and changes the current working directory
1400        let (worktree_context, mut worktree_setup_ctx) =
1401            self.setup_worktree_context(&config, &spec.branch_name)?;
1402
1403        // Create the appropriate StateManager for this context
1404        let effective_state_manager = if let Some((ref session_id, _)) = worktree_context {
1405            // In worktree mode, use the worktree's session ID
1406            StateManager::with_session(session_id.clone())?
1407        } else {
1408            // Not in worktree mode, use auto-detected session
1409            StateManager::new()?
1410        };
1411
1412        // Clear any stale live output from a previous crashed run
1413        let _ = effective_state_manager.clear_live();
1414
1415        // If NOT in worktree mode and in a git repo, ensure we're on the correct branch
1416        if worktree_context.is_none() && git::is_git_repo() {
1417            let current_branch = git::current_branch()?;
1418            if current_branch != spec.branch_name {
1419                print_info(&format!(
1420                    "Switching from '{}' to '{}'",
1421                    current_branch, spec.branch_name
1422                ));
1423                git::ensure_branch(&spec.branch_name)?;
1424            }
1425        }
1426
1427        // Update state with branch from generated spec and session ID
1428        state.branch = spec.branch_name.clone();
1429        if let Some((ref session_id, _)) = &worktree_context {
1430            state.session_id = Some(session_id.clone());
1431        }
1432        state.transition_to(MachineState::Initializing);
1433        effective_state_manager.save(&state)?;
1434        // Flush live.json immediately so GUI sees the state transition
1435        flush_live_state(&effective_state_manager, MachineState::Initializing);
1436
1437        print_state_transition(MachineState::GeneratingSpec, MachineState::Initializing);
1438
1439        print_proceeding_to_implementation();
1440
1441        // Create a new Runner with the effective state manager and continue
1442        let effective_runner = Runner {
1443            state_manager: effective_state_manager,
1444            verbose: self.verbose,
1445            skip_review: self.skip_review,
1446            worktree_override: self.worktree_override,
1447            commit_override: self.commit_override,
1448            pull_request_override: self.pull_request_override,
1449        };
1450
1451        // Mark metadata as saved since the state was just saved above
1452        worktree_setup_ctx.metadata_saved = true;
1453
1454        effective_runner.run_implementation_loop(state, &spec_json_path, worktree_setup_ctx)
1455    }
1456
1457    pub fn run(&self, spec_json_path: &Path) -> Result<()> {
1458        // IMPORTANT: State must NOT be persisted until after worktree context is determined.
1459        // Saving state before we know the correct session ID would create phantom sessions
1460        // in the main repo when running in worktree mode. State is first persisted in
1461        // run_implementation_loop() after the effective StateManager is known.
1462
1463        // Check for existing active run
1464        if self.state_manager.has_active_run()? {
1465            if let Some(state) = self.state_manager.load_current()? {
1466                return Err(Autom8Error::RunInProgress(state.run_id));
1467            }
1468        }
1469
1470        // Load effective config at startup, applying CLI flag override (US-002, US-005)
1471        let config = self.load_config_with_override()?;
1472
1473        // Canonicalize path so resume works from any directory
1474        let spec_json_path = spec_json_path
1475            .canonicalize()
1476            .map_err(|_| Autom8Error::SpecNotFound(spec_json_path.to_path_buf()))?;
1477
1478        // Load and validate spec
1479        let spec = Spec::load(&spec_json_path)?;
1480
1481        // Check for branch conflicts with other active sessions (US-006)
1482        // This must happen before any git operations to prevent race conditions
1483        if let Some(conflict) = self
1484            .state_manager
1485            .check_branch_conflict(&spec.branch_name)?
1486        {
1487            return Err(Autom8Error::BranchConflict {
1488                branch: spec.branch_name.clone(),
1489                session_id: conflict.session_id,
1490                worktree_path: conflict.worktree_path,
1491            });
1492        }
1493
1494        // Setup worktree context if enabled (US-007)
1495        // This creates/reuses a worktree and changes the current working directory
1496        let (worktree_context, worktree_setup_ctx) =
1497            self.setup_worktree_context(&config, &spec.branch_name)?;
1498
1499        // Create the appropriate StateManager for this context
1500        let state_manager = if let Some((ref session_id, _)) = worktree_context {
1501            // In worktree mode, use the worktree's session ID
1502            StateManager::with_session(session_id.clone())?
1503        } else {
1504            // Not in worktree mode, use auto-detected session
1505            StateManager::new()?
1506        };
1507
1508        // Clear any stale live output from a previous crashed run (US-003)
1509        let _ = state_manager.clear_live();
1510
1511        // If NOT in worktree mode and in a git repo, ensure we're on the correct branch
1512        if worktree_context.is_none() && git::is_git_repo() {
1513            let current_branch = git::current_branch()?;
1514            if current_branch != spec.branch_name {
1515                print_info(&format!(
1516                    "Switching from '{}' to '{}'",
1517                    current_branch, spec.branch_name
1518                ));
1519                git::ensure_branch(&spec.branch_name)?;
1520            }
1521        }
1522
1523        // Initialize state with config snapshot for resume support
1524        let state = if let Some((ref session_id, _)) = worktree_context {
1525            RunState::new_with_config_and_session(
1526                spec_json_path.to_path_buf(),
1527                spec.branch_name.clone(),
1528                config,
1529                session_id.clone(),
1530            )
1531        } else {
1532            RunState::new_with_config(
1533                spec_json_path.to_path_buf(),
1534                spec.branch_name.clone(),
1535                config,
1536            )
1537        };
1538
1539        print_state_transition(MachineState::Idle, MachineState::Initializing);
1540        print_project_info(&spec);
1541
1542        // Create a new Runner with the worktree-specific state manager
1543        // and delegate to it for the implementation loop
1544        let worktree_runner = Runner {
1545            state_manager,
1546            verbose: self.verbose,
1547            skip_review: self.skip_review,
1548            worktree_override: self.worktree_override,
1549            commit_override: self.commit_override,
1550            pull_request_override: self.pull_request_override,
1551        };
1552
1553        worktree_runner.run_implementation_loop(state, &spec_json_path, worktree_setup_ctx)
1554    }
1555
1556    fn run_implementation_loop(
1557        &self,
1558        mut state: RunState,
1559        spec_json_path: &Path,
1560        mut worktree_setup_ctx: WorktreeSetupContext,
1561    ) -> Result<()> {
1562        // Create signal handler for graceful shutdown (US-004)
1563        let signal_handler = SignalHandler::new()?;
1564
1565        // Create ClaudeRunner that can be killed on interrupt (US-004)
1566        let claude_runner = ClaudeRunner::new();
1567
1568        // Transition to PickingStory
1569        print_state_transition(state.machine_state, MachineState::PickingStory);
1570        state.transition_to(MachineState::PickingStory);
1571        self.state_manager.save(&state)?;
1572        self.flush_live(MachineState::PickingStory);
1573
1574        // Mark metadata as saved now that state has been persisted
1575        // This ensures cleanup_on_interruption won't remove the worktree
1576        worktree_setup_ctx.metadata_saved = true;
1577
1578        // Track story results for summary
1579        let mut story_results: Vec<StoryResult> = Vec::new();
1580        let run_start = Instant::now();
1581
1582        // Breadcrumb trail for tracking workflow journey
1583        let mut breadcrumb = Breadcrumb::new();
1584
1585        // Helper to print run summary (loads spec and prints)
1586        let print_summary_fn = |iteration: u32, results: &[StoryResult]| -> Result<()> {
1587            let spec = Spec::load(spec_json_path)?;
1588            print_run_summary(
1589                spec.total_count(),
1590                spec.completed_count(),
1591                iteration,
1592                run_start.elapsed().as_secs(),
1593                results,
1594            );
1595            Ok(())
1596        };
1597
1598        // Main loop
1599        loop {
1600            // Check for shutdown request at safe point (between state transitions) (US-004)
1601            if signal_handler.is_shutdown_requested() {
1602                return Err(self.handle_interruption(
1603                    &mut state,
1604                    &claude_runner,
1605                    Some(&worktree_setup_ctx),
1606                ));
1607            }
1608
1609            // Reload spec to get latest passes state
1610            let spec = Spec::load(spec_json_path)?;
1611
1612            // Check if all stories complete at loop start
1613            if spec.all_complete() {
1614                match self.handle_all_stories_complete(
1615                    &mut state,
1616                    &spec,
1617                    &mut breadcrumb,
1618                    &story_results,
1619                    &print_summary_fn,
1620                )? {
1621                    LoopAction::Break => return Ok(()),
1622                    LoopAction::Continue => continue,
1623                }
1624            }
1625
1626            // Pick next story
1627            let story = spec
1628                .next_incomplete_story()
1629                .ok_or(Autom8Error::NoIncompleteStories)?
1630                .clone();
1631
1632            // Reset breadcrumb trail at start of each new story
1633            breadcrumb.reset();
1634
1635            // Capture pre-story state for git diff calculation (US-006)
1636            state.capture_pre_story_state();
1637
1638            // Start iteration
1639            print_state_transition(MachineState::PickingStory, MachineState::RunningClaude);
1640            state.start_iteration(&story.id);
1641            self.state_manager.save(&state)?;
1642            self.flush_live(MachineState::RunningClaude);
1643
1644            // Update breadcrumb to enter Story state
1645            breadcrumb.enter_state(BreadcrumbState::Story);
1646
1647            print_phase_banner("RUNNING", BannerColor::Cyan);
1648            print_iteration_start(state.iteration, &story.id, &story.title);
1649
1650            // Process the story iteration
1651            let story_start = Instant::now();
1652            match self.handle_story_iteration(
1653                &mut state,
1654                &spec,
1655                spec_json_path,
1656                &story,
1657                &mut breadcrumb,
1658                &mut story_results,
1659                story_start,
1660                &claude_runner,
1661                &print_summary_fn,
1662            )? {
1663                LoopAction::Break => return Ok(()),
1664                LoopAction::Continue => {
1665                    // Check for shutdown after story iteration completes (US-004)
1666                    if signal_handler.is_shutdown_requested() {
1667                        return Err(self.handle_interruption(
1668                            &mut state,
1669                            &claude_runner,
1670                            Some(&worktree_setup_ctx),
1671                        ));
1672                    }
1673                    continue;
1674                }
1675            }
1676        }
1677    }
1678
1679    pub fn resume(&self) -> Result<()> {
1680        // First try: load from active state
1681        if let Some(state) = self.state_manager.load_current()? {
1682            if state.status == RunStatus::Running
1683                || state.status == RunStatus::Failed
1684                || state.status == RunStatus::Interrupted
1685            {
1686                // Show interruption message if resuming from an interrupted state
1687                if state.status == RunStatus::Interrupted {
1688                    print_resuming_interrupted(&format!("{:?}", state.machine_state));
1689                }
1690
1691                let spec_json_path = state.spec_json_path.clone();
1692
1693                // Archive the interrupted/failed run before starting fresh
1694                self.state_manager.archive(&state)?;
1695                self.state_manager.clear_current()?;
1696
1697                // Start a new run with the same parameters
1698                return self.run(&spec_json_path);
1699            }
1700        }
1701
1702        // Second try: smart resume - scan for incomplete specs
1703        self.smart_resume()
1704    }
1705
1706    /// Scan spec/ in config directory for incomplete specs and offer to resume one
1707    fn smart_resume(&self) -> Result<()> {
1708        use crate::prompt;
1709
1710        let spec_files = self.state_manager.list_specs()?;
1711        if spec_files.is_empty() {
1712            return Err(Autom8Error::NoSpecsToResume);
1713        }
1714
1715        // Filter to incomplete specs
1716        let incomplete_specs: Vec<(PathBuf, Spec)> = spec_files
1717            .into_iter()
1718            .filter_map(|path| {
1719                Spec::load(&path).ok().and_then(|spec| {
1720                    if spec.is_incomplete() {
1721                        Some((path, spec))
1722                    } else {
1723                        None
1724                    }
1725                })
1726            })
1727            .collect();
1728
1729        if incomplete_specs.is_empty() {
1730            return Err(Autom8Error::NoSpecsToResume);
1731        }
1732
1733        print_header();
1734        println!("{YELLOW}[resume]{RESET} No active run found, scanning for incomplete specs...");
1735        println!();
1736
1737        if incomplete_specs.len() == 1 {
1738            // Auto-resume single incomplete spec
1739            let (spec_path, spec) = &incomplete_specs[0];
1740            let (completed, total) = spec.progress();
1741            println!(
1742                "{CYAN}Found{RESET} {} {GRAY}({}/{}){RESET}",
1743                spec_path.display(),
1744                completed,
1745                total
1746            );
1747            println!();
1748            prompt::print_action(&format!("Resuming {}", spec.project));
1749            println!();
1750            return self.run(spec_path);
1751        }
1752
1753        // Multiple incomplete specs - let user choose
1754        println!(
1755            "{BOLD}Found {} incomplete specs:{RESET}",
1756            incomplete_specs.len()
1757        );
1758        println!();
1759
1760        let options: Vec<String> = incomplete_specs
1761            .iter()
1762            .map(|(path, spec)| {
1763                let (completed, total) = spec.progress();
1764                let filename = path
1765                    .file_name()
1766                    .and_then(|n| n.to_str())
1767                    .unwrap_or("spec.json");
1768                format!("{} - {} ({}/{})", filename, spec.project, completed, total)
1769            })
1770            .chain(std::iter::once("Exit".to_string()))
1771            .collect();
1772
1773        let option_refs: Vec<&str> = options.iter().map(|s| s.as_str()).collect();
1774        let choice = prompt::select("Which spec would you like to resume?", &option_refs, 0);
1775
1776        // Handle Exit option
1777        if choice >= incomplete_specs.len() {
1778            println!();
1779            println!("Exiting.");
1780            return Err(Autom8Error::NoSpecsToResume);
1781        }
1782
1783        let (spec_path, spec) = &incomplete_specs[choice];
1784        println!();
1785        prompt::print_action(&format!("Resuming {}", spec.project));
1786        println!();
1787        self.run(spec_path)
1788    }
1789
1790    fn archive_and_cleanup(&self, state: &RunState) -> Result<()> {
1791        self.state_manager.archive(state)?;
1792
1793        // Check if we should clean up the worktree after successful completion
1794        // Only applies when:
1795        // 1. Run completed successfully (not failed)
1796        // 2. worktree_cleanup is enabled in config
1797        // 3. We're currently in a worktree (not the main repo)
1798        let config = state.effective_config();
1799        if state.status == crate::state::RunStatus::Completed && config.worktree_cleanup {
1800            // Check if we're in a worktree
1801            if let Ok(true) = is_in_worktree() {
1802                // Get the worktree path from session metadata
1803                if let Ok(Some(metadata)) = self.state_manager.load_metadata() {
1804                    let worktree_path = metadata.worktree_path;
1805
1806                    // Clear state before removing worktree (since we're inside it)
1807                    self.state_manager.clear_current()?;
1808
1809                    // Change to the main repo before removing worktree
1810                    // We need to get out of the worktree directory first
1811                    if let Ok(main_repo) = crate::worktree::get_main_repo_root() {
1812                        if std::env::set_current_dir(&main_repo).is_ok() {
1813                            // Now remove the worktree
1814                            match remove_worktree(&worktree_path, false) {
1815                                Ok(()) => {
1816                                    print_info(&format!(
1817                                        "Cleaned up worktree: {}",
1818                                        worktree_path.display()
1819                                    ));
1820                                }
1821                                Err(e) => {
1822                                    // Non-fatal - warn but continue
1823                                    print_info(&format!(
1824                                        "Warning: failed to remove worktree: {}",
1825                                        e
1826                                    ));
1827                                }
1828                            }
1829                        }
1830                    }
1831
1832                    return Ok(());
1833                }
1834            }
1835        }
1836
1837        // Default path: just clear the state
1838        self.state_manager.clear_current()?;
1839        Ok(())
1840    }
1841
1842    pub fn status(&self) -> Result<Option<RunState>> {
1843        self.state_manager.load_current()
1844    }
1845}
1846
1847#[cfg(test)]
1848mod tests {
1849    use super::*;
1850    use crate::config::Config;
1851    use crate::spec::{Spec, UserStory};
1852    use crate::state::RunStatus;
1853    use tempfile::TempDir;
1854
1855    // ========================================================================
1856    // Test helpers
1857    // ========================================================================
1858
1859    fn create_test_spec(passes: bool) -> Spec {
1860        Spec {
1861            project: "TestProject".into(),
1862            branch_name: "test-branch".into(),
1863            description: "A test project".into(),
1864            user_stories: vec![UserStory {
1865                id: "US-001".into(),
1866                title: "Test Story".into(),
1867                description: "A test story".into(),
1868                acceptance_criteria: vec!["Test criterion".into()],
1869                priority: 1,
1870                passes,
1871                notes: String::new(),
1872            }],
1873        }
1874    }
1875
1876    fn create_multi_story_spec(completed_count: usize, total: usize) -> Spec {
1877        let stories = (0..total)
1878            .map(|i| UserStory {
1879                id: format!("US-{:03}", i + 1),
1880                title: format!("Story {}", i + 1),
1881                description: format!("Description for story {}", i + 1),
1882                acceptance_criteria: vec!["Criterion".into()],
1883                priority: (i + 1) as u32,
1884                passes: i < completed_count,
1885                notes: String::new(),
1886            })
1887            .collect();
1888        Spec {
1889            project: "TestProject".into(),
1890            branch_name: "test-branch".into(),
1891            description: "Multi-story test".into(),
1892            user_stories: stories,
1893        }
1894    }
1895
1896    // ========================================================================
1897    // Runner builder pattern
1898    // ========================================================================
1899
1900    #[test]
1901    fn test_runner_builder_pattern() {
1902        let runner = Runner::new()
1903            .unwrap()
1904            .with_verbose(true)
1905            .with_skip_review(true)
1906            .with_worktree(true);
1907
1908        assert!(runner.verbose);
1909        assert!(runner.skip_review);
1910        assert_eq!(runner.worktree_override, Some(true));
1911    }
1912
1913    #[test]
1914    fn test_runner_defaults() {
1915        let runner = Runner::new().unwrap();
1916        assert!(!runner.skip_review);
1917        assert!(!runner.verbose);
1918        assert!(runner.worktree_override.is_none());
1919        assert!(runner.commit_override.is_none());
1920        assert!(runner.pull_request_override.is_none());
1921    }
1922
1923    #[test]
1924    fn test_runner_commit_and_pull_request_overrides() {
1925        let runner = Runner::new()
1926            .unwrap()
1927            .with_commit(false)
1928            .with_pull_request(false);
1929
1930        assert_eq!(runner.commit_override, Some(false));
1931        assert_eq!(runner.pull_request_override, Some(false));
1932    }
1933
1934    // ========================================================================
1935    // Story index calculation (1-indexed display)
1936    // ========================================================================
1937
1938    #[test]
1939    fn test_story_index_is_one_indexed() {
1940        let story_ids = vec!["US-001", "US-002", "US-003"];
1941
1942        // First story should be 1, not 0
1943        let idx = story_ids
1944            .iter()
1945            .position(|&s| s == "US-001")
1946            .map(|i| i as u32 + 1)
1947            .unwrap();
1948        assert_eq!(idx, 1);
1949
1950        // Last story should be 3, not 2
1951        let idx = story_ids
1952            .iter()
1953            .position(|&s| s == "US-003")
1954            .map(|i| i as u32 + 1)
1955            .unwrap();
1956        assert_eq!(idx, 3);
1957    }
1958
1959    // ========================================================================
1960    // Spec loading errors
1961    // ========================================================================
1962
1963    #[test]
1964    fn test_spec_load_errors() {
1965        // Nonexistent path
1966        let result = Spec::load(Path::new("/nonexistent/spec.json"));
1967        assert!(matches!(result.unwrap_err(), Autom8Error::SpecNotFound(_)));
1968
1969        // Invalid JSON
1970        let temp_dir = TempDir::new().unwrap();
1971        let spec_path = temp_dir.path().join("spec.json");
1972        fs::write(&spec_path, "{ invalid }").unwrap();
1973        let result = Spec::load(&spec_path);
1974        assert!(matches!(result.unwrap_err(), Autom8Error::InvalidSpec(_)));
1975
1976        // Empty project
1977        fs::write(
1978            &spec_path,
1979            r#"{"project": "", "branchName": "test", "description": "test", "userStories": [{"id": "US-001", "title": "t", "description": "d", "acceptanceCriteria": [], "priority": 1, "passes": false}]}"#,
1980        )
1981        .unwrap();
1982        let result = Spec::load(&spec_path);
1983        assert!(matches!(result.unwrap_err(), Autom8Error::InvalidSpec(_)));
1984    }
1985
1986    // ========================================================================
1987    // State transitions
1988    // ========================================================================
1989
1990    #[test]
1991    fn test_state_transitions_full_workflow() {
1992        let mut state = RunState::new(PathBuf::from("test.json"), "test-branch".to_string());
1993
1994        // Initial -> PickingStory -> RunningClaude -> PickingStory
1995        assert_eq!(state.machine_state, MachineState::Initializing);
1996
1997        state.transition_to(MachineState::PickingStory);
1998        state.start_iteration("US-001");
1999        assert_eq!(state.machine_state, MachineState::RunningClaude);
2000        assert_eq!(state.iteration, 1);
2001
2002        state.finish_iteration(IterationStatus::Success, String::new());
2003        assert_eq!(state.machine_state, MachineState::PickingStory);
2004
2005        // -> Reviewing -> Correcting -> Reviewing
2006        state.transition_to(MachineState::Reviewing);
2007        state.review_iteration = 1;
2008        state.transition_to(MachineState::Correcting);
2009        state.transition_to(MachineState::Reviewing);
2010        state.review_iteration = 2;
2011
2012        // -> Committing -> CreatingPR -> Completed
2013        state.transition_to(MachineState::Committing);
2014        state.transition_to(MachineState::CreatingPR);
2015        state.transition_to(MachineState::Completed);
2016
2017        assert_eq!(state.status, RunStatus::Completed);
2018        assert!(state.finished_at.is_some());
2019    }
2020
2021    #[test]
2022    fn test_state_transitions_to_failed() {
2023        let mut state = RunState::new(PathBuf::from("test.json"), "test-branch".to_string());
2024        state.transition_to(MachineState::Failed);
2025
2026        assert_eq!(state.status, RunStatus::Failed);
2027        assert!(state.finished_at.is_some());
2028    }
2029
2030    // ========================================================================
2031    // StateManager operations
2032    // ========================================================================
2033
2034    #[test]
2035    fn test_state_manager_save_load_clear() {
2036        let temp_dir = TempDir::new().unwrap();
2037        let sm = StateManager::with_dir(temp_dir.path().to_path_buf());
2038
2039        // Initially empty
2040        assert!(sm.load_current().unwrap().is_none());
2041        assert!(!sm.has_active_run().unwrap());
2042
2043        // Save and load
2044        let state = RunState::new(PathBuf::from("test.json"), "test-branch".to_string());
2045        sm.save(&state).unwrap();
2046        assert!(sm.has_active_run().unwrap());
2047
2048        let loaded = sm.load_current().unwrap().unwrap();
2049        assert_eq!(loaded.run_id, state.run_id);
2050
2051        // Clear
2052        sm.clear_current().unwrap();
2053        assert!(sm.load_current().unwrap().is_none());
2054    }
2055
2056    #[test]
2057    fn test_state_manager_completed_not_active() {
2058        let temp_dir = TempDir::new().unwrap();
2059        let sm = StateManager::with_dir(temp_dir.path().to_path_buf());
2060
2061        let mut state = RunState::new(PathBuf::from("test.json"), "test-branch".to_string());
2062        state.transition_to(MachineState::Completed);
2063        sm.save(&state).unwrap();
2064
2065        assert!(!sm.has_active_run().unwrap());
2066    }
2067
2068    #[test]
2069    fn test_state_manager_archive() {
2070        let temp_dir = TempDir::new().unwrap();
2071        let sm = StateManager::with_dir(temp_dir.path().to_path_buf());
2072
2073        let state = RunState::new(PathBuf::from("test.json"), "test-branch".to_string());
2074        let archive_path = sm.archive(&state).unwrap();
2075
2076        assert!(archive_path.exists());
2077        assert!(archive_path.parent().unwrap().ends_with("runs"));
2078    }
2079
2080    // ========================================================================
2081    // Spec operations
2082    // ========================================================================
2083
2084    #[test]
2085    fn test_spec_completion_detection() {
2086        assert!(create_test_spec(true).all_complete());
2087        assert!(!create_test_spec(false).all_complete());
2088    }
2089
2090    #[test]
2091    fn test_spec_next_incomplete_story() {
2092        let spec = create_multi_story_spec(0, 3);
2093        assert_eq!(spec.next_incomplete_story().unwrap().id, "US-001");
2094
2095        let mut spec = create_multi_story_spec(0, 3);
2096        spec.user_stories[0].passes = true;
2097        assert_eq!(spec.next_incomplete_story().unwrap().id, "US-002");
2098
2099        let spec = create_multi_story_spec(3, 3);
2100        assert!(spec.next_incomplete_story().is_none());
2101    }
2102
2103    #[test]
2104    fn test_list_specs_sorted_by_mtime() {
2105        let temp_dir = TempDir::new().unwrap();
2106        let sm = StateManager::with_dir(temp_dir.path().to_path_buf());
2107        let spec_dir = sm.ensure_spec_dir().unwrap();
2108
2109        create_test_spec(false)
2110            .save(&spec_dir.join("spec1.json"))
2111            .unwrap();
2112        std::thread::sleep(std::time::Duration::from_millis(10));
2113        create_multi_story_spec(1, 3)
2114            .save(&spec_dir.join("spec2.json"))
2115            .unwrap();
2116
2117        let specs = sm.list_specs().unwrap();
2118        assert_eq!(specs.len(), 2);
2119        assert!(specs[0].ends_with("spec2.json")); // Most recent first
2120    }
2121
2122    // ========================================================================
2123    // Config integration
2124    // ========================================================================
2125
2126    #[test]
2127    fn test_effective_config() {
2128        // Default config
2129        let state = RunState::new(PathBuf::from("test.json"), "test".to_string());
2130        let config = state.effective_config();
2131        assert!(config.review && config.commit && config.pull_request);
2132
2133        // Custom config preserved
2134        let custom = Config {
2135            review: false,
2136            commit: true,
2137            pull_request: false,
2138            ..Default::default()
2139        };
2140        let state =
2141            RunState::new_with_config(PathBuf::from("test.json"), "test".to_string(), custom);
2142        let config = state.effective_config();
2143        assert!(!config.review && config.commit && !config.pull_request);
2144    }
2145
2146    #[test]
2147    fn test_config_preserved_on_resume() {
2148        let temp_dir = TempDir::new().unwrap();
2149        let sm = StateManager::with_dir(temp_dir.path().to_path_buf());
2150
2151        let config = Config {
2152            review: false,
2153            commit: true,
2154            pull_request: false,
2155            ..Default::default()
2156        };
2157        let state =
2158            RunState::new_with_config(PathBuf::from("test.json"), "test".to_string(), config);
2159        sm.save(&state).unwrap();
2160
2161        let loaded = sm.load_current().unwrap().unwrap();
2162        assert!(!loaded.effective_config().review);
2163    }
2164
2165    // ========================================================================
2166    // Worktree mode
2167    // ========================================================================
2168
2169    #[test]
2170    fn test_worktree_mode_override() {
2171        let runner = Runner::new().unwrap();
2172        let config_true = Config {
2173            worktree: true,
2174            ..Default::default()
2175        };
2176        let config_false = Config {
2177            worktree: false,
2178            ..Default::default()
2179        };
2180
2181        // No override - uses config
2182        assert!(runner.is_worktree_mode(&config_true));
2183        assert!(!runner.is_worktree_mode(&config_false));
2184
2185        // Override takes precedence
2186        let runner_override = Runner::new().unwrap().with_worktree(true);
2187        assert!(runner_override.is_worktree_mode(&config_false));
2188
2189        let runner_override = Runner::new().unwrap().with_worktree(false);
2190        assert!(!runner_override.is_worktree_mode(&config_true));
2191    }
2192
2193    #[test]
2194    fn test_session_isolation() {
2195        let temp_dir = TempDir::new().unwrap();
2196
2197        let sm1 = StateManager::with_dir_and_session(
2198            temp_dir.path().to_path_buf(),
2199            "session1".to_string(),
2200        );
2201        let sm2 = StateManager::with_dir_and_session(
2202            temp_dir.path().to_path_buf(),
2203            "session2".to_string(),
2204        );
2205
2206        let state = RunState::new(PathBuf::from("test.json"), "test".to_string());
2207        sm1.save(&state).unwrap();
2208
2209        assert!(sm1.has_active_run().unwrap());
2210        assert!(!sm2.has_active_run().unwrap());
2211    }
2212
2213    // ========================================================================
2214    // Iteration tracking
2215    // ========================================================================
2216
2217    #[test]
2218    fn test_iteration_tracking() {
2219        let mut state = RunState::new(PathBuf::from("test.json"), "test".to_string());
2220
2221        state.start_iteration("US-001");
2222        state.set_work_summary(Some("Work 1".to_string()));
2223        state.finish_iteration(IterationStatus::Success, String::new());
2224
2225        state.start_iteration("US-002");
2226        state.set_work_summary(Some("Work 2".to_string()));
2227        state.finish_iteration(IterationStatus::Success, String::new());
2228
2229        assert_eq!(state.iterations.len(), 2);
2230        assert_eq!(state.iterations[0].story_id, "US-001");
2231        assert_eq!(state.iterations[1].story_id, "US-002");
2232    }
2233
2234    // ========================================================================
2235    // Live output flusher
2236    // ========================================================================
2237
2238    #[test]
2239    fn test_live_output_flusher() {
2240        let temp_dir = TempDir::new().unwrap();
2241        let sm = StateManager::with_dir(temp_dir.path().to_path_buf());
2242        sm.ensure_dirs().unwrap();
2243
2244        let mut flusher = LiveOutputFlusher::new(&sm, MachineState::RunningClaude);
2245        assert!(flusher.live_state.output_lines.is_empty());
2246
2247        // Append lines
2248        flusher.append("Line 1");
2249        flusher.append("Line 2");
2250        assert_eq!(flusher.live_state.output_lines.len(), 2);
2251
2252        // Flush resets counter
2253        flusher.flush();
2254        assert_eq!(flusher.line_count_since_flush, 0);
2255
2256        // Auto-flush at 10 lines
2257        for i in 0..10 {
2258            flusher.append(&format!("Line {}", i));
2259        }
2260        assert_eq!(flusher.line_count_since_flush, 0);
2261        assert!(sm.load_live().is_some());
2262    }
2263
2264    #[test]
2265    fn test_live_flush_constants() {
2266        assert_eq!(LIVE_FLUSH_INTERVAL_MS, 200);
2267        assert_eq!(LIVE_FLUSH_LINE_COUNT, 10);
2268        assert_eq!(HEARTBEAT_INTERVAL_MS, 2500);
2269    }
2270
2271    // ========================================================================
2272    // Signal handling / interruption
2273    // ========================================================================
2274
2275    #[test]
2276    fn test_handle_interruption() {
2277        let temp_dir = TempDir::new().unwrap();
2278        let sm = StateManager::with_dir(temp_dir.path().to_path_buf());
2279        sm.ensure_dirs().unwrap();
2280
2281        let runner = Runner {
2282            state_manager: StateManager::with_dir(temp_dir.path().to_path_buf()),
2283            verbose: false,
2284            skip_review: false,
2285            worktree_override: None,
2286            commit_override: None,
2287            pull_request_override: None,
2288        };
2289
2290        let mut state = RunState::new(PathBuf::from("test.json"), "test".to_string());
2291        state.transition_to(MachineState::RunningClaude);
2292        sm.save(&state).unwrap();
2293
2294        // Create live output
2295        sm.save_live(&LiveState::new(MachineState::RunningClaude))
2296            .unwrap();
2297
2298        let claude_runner = ClaudeRunner::new();
2299        let error = runner.handle_interruption(&mut state, &claude_runner, None);
2300
2301        // Verify results
2302        assert!(matches!(error, Autom8Error::Interrupted));
2303        assert_eq!(state.status, RunStatus::Interrupted);
2304        assert_eq!(state.machine_state, MachineState::RunningClaude); // Preserved
2305        assert!(state.finished_at.is_some());
2306        assert!(sm.load_live().is_none()); // Cleared
2307    }
2308
2309    #[test]
2310    fn test_interrupted_is_resumable() {
2311        for (status, resumable) in [
2312            (RunStatus::Interrupted, true),
2313            (RunStatus::Running, true),
2314            (RunStatus::Failed, true),
2315            (RunStatus::Completed, false),
2316        ] {
2317            let is_resumable = status == RunStatus::Running
2318                || status == RunStatus::Failed
2319                || status == RunStatus::Interrupted;
2320            assert_eq!(is_resumable, resumable, "{:?}", status);
2321        }
2322    }
2323
2324    // ========================================================================
2325    // WorktreeSetupContext
2326    // ========================================================================
2327
2328    #[test]
2329    fn test_worktree_setup_context() {
2330        let ctx = WorktreeSetupContext::new().unwrap();
2331        assert_eq!(ctx.original_cwd, std::env::current_dir().unwrap());
2332        assert!(ctx.worktree_path.is_none());
2333        assert!(!ctx.worktree_was_created);
2334        assert!(!ctx.cwd_changed);
2335        assert!(!ctx.metadata_saved);
2336    }
2337
2338    #[test]
2339    fn test_worktree_cleanup_logic() {
2340        // Newly created without metadata - should be removed
2341        let ctx = WorktreeSetupContext {
2342            original_cwd: PathBuf::from("/orig"),
2343            worktree_path: Some(PathBuf::from("/wt")),
2344            worktree_was_created: true,
2345            cwd_changed: false,
2346            metadata_saved: false,
2347        };
2348        assert!(ctx.worktree_was_created && !ctx.metadata_saved);
2349
2350        // Reused - should NOT be removed
2351        let ctx = WorktreeSetupContext {
2352            worktree_was_created: false,
2353            ..ctx.clone()
2354        };
2355        assert!(!ctx.worktree_was_created);
2356
2357        // With metadata - should NOT be removed
2358        let ctx = WorktreeSetupContext {
2359            worktree_was_created: true,
2360            metadata_saved: true,
2361            ..ctx.clone()
2362        };
2363        assert!(ctx.metadata_saved);
2364    }
2365
2366    #[test]
2367    fn test_worktree_cleanup_restores_cwd() {
2368        let original_cwd = std::env::current_dir().unwrap();
2369        let temp_dir = TempDir::new().unwrap();
2370
2371        let mut ctx = WorktreeSetupContext::new().unwrap();
2372        std::env::set_current_dir(temp_dir.path()).unwrap();
2373        ctx.cwd_changed = true;
2374        ctx.worktree_was_created = false;
2375
2376        ctx.cleanup_on_interruption();
2377        assert_eq!(std::env::current_dir().unwrap(), original_cwd);
2378    }
2379
2380    // ========================================================================
2381    // Phantom session prevention
2382    // ========================================================================
2383
2384    #[test]
2385    fn test_state_not_saved_before_worktree_context() {
2386        let temp_dir = TempDir::new().unwrap();
2387        let sm = StateManager::with_dir(temp_dir.path().to_path_buf());
2388
2389        // State can be created without persistence
2390        let state = RunState::from_spec_with_config(
2391            PathBuf::from("spec.md"),
2392            PathBuf::from("spec.json"),
2393            Config::default(),
2394        );
2395        assert_eq!(state.machine_state, MachineState::LoadingSpec);
2396
2397        // No state persisted yet
2398        assert!(sm.load_current().unwrap().is_none());
2399    }
2400
2401    #[test]
2402    fn test_state_saved_to_correct_session() {
2403        let temp_dir = TempDir::new().unwrap();
2404        let main_sm =
2405            StateManager::with_dir_and_session(temp_dir.path().to_path_buf(), "main".to_string());
2406        let wt_sm = StateManager::with_dir_and_session(
2407            temp_dir.path().to_path_buf(),
2408            "abc12345".to_string(),
2409        );
2410
2411        let state = RunState::new_with_config_and_session(
2412            PathBuf::from("spec.json"),
2413            "feature".to_string(),
2414            Config::default(),
2415            "abc12345".to_string(),
2416        );
2417        wt_sm.save(&state).unwrap();
2418
2419        assert!(main_sm.load_current().unwrap().is_none());
2420        assert!(wt_sm.load_current().unwrap().is_some());
2421    }
2422}