autom8-cli 0.3.0

CLI automation tool for orchestrating Claude-powered development
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
//! Git worktree operations for autom8.
//!
//! This module provides functions for managing git worktrees, enabling
//! parallel execution of autom8 sessions on the same project.

use crate::error::{Autom8Error, Result};
use sha2::{Digest, Sha256};
use std::path::{Path, PathBuf};
use std::process::Command;

/// Information about a git worktree.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorktreeInfo {
    /// Absolute path to the worktree directory
    pub path: PathBuf,
    /// The branch checked out in this worktree (None for detached HEAD)
    pub branch: Option<String>,
    /// The current commit hash
    pub commit: String,
    /// Whether this is the main worktree (the original repo)
    pub is_main: bool,
    /// Whether this worktree is bare (no working directory)
    pub is_bare: bool,
    /// Whether the worktree is currently locked
    pub is_locked: bool,
    /// Whether the worktree is prunable (missing directory)
    pub is_prunable: bool,
}

impl WorktreeInfo {
    /// Parse a single worktree from porcelain output lines.
    ///
    /// The porcelain format outputs one attribute per line, with worktrees
    /// separated by blank lines.
    fn from_porcelain_lines(lines: &[&str]) -> Option<Self> {
        let mut path: Option<PathBuf> = None;
        let mut branch: Option<String> = None;
        let mut commit: Option<String> = None;
        let mut is_bare = false;
        let mut is_locked = false;
        let mut is_prunable = false;

        for line in lines {
            if let Some(rest) = line.strip_prefix("worktree ") {
                path = Some(PathBuf::from(rest));
            } else if let Some(rest) = line.strip_prefix("HEAD ") {
                commit = Some(rest.to_string());
            } else if let Some(rest) = line.strip_prefix("branch ") {
                // Branch is in format "refs/heads/branch-name"
                let branch_name = rest.strip_prefix("refs/heads/").unwrap_or(rest).to_string();
                branch = Some(branch_name);
            } else if *line == "bare" {
                is_bare = true;
            } else if *line == "detached" {
                // Detached HEAD - branch remains None
            } else if line.starts_with("locked") {
                is_locked = true;
            } else if line.starts_with("prunable") {
                is_prunable = true;
            }
        }

        let path = path?;
        let commit = commit?;

        // The first worktree listed is always the main worktree
        // We'll set this properly in list_worktrees()
        Some(WorktreeInfo {
            path,
            branch,
            commit,
            is_main: false,
            is_bare,
            is_locked,
            is_prunable,
        })
    }
}

/// List all worktrees for the current repository.
///
/// Returns information about each worktree including path, branch, and commit.
/// The main repository is always included in the list with `is_main: true`.
///
/// # Returns
/// * `Ok(Vec<WorktreeInfo>)` - List of worktrees (always has at least one - the main repo)
/// * `Err` - If not in a git repository or git command fails
pub fn list_worktrees() -> Result<Vec<WorktreeInfo>> {
    let output = Command::new("git")
        .args(["worktree", "list", "--porcelain"])
        .output()?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        return Err(Autom8Error::WorktreeError(format!(
            "Failed to list worktrees: {}",
            stderr.trim()
        )));
    }

    let stdout = String::from_utf8_lossy(&output.stdout);
    let worktrees = parse_worktree_list_porcelain(&stdout)?;

    Ok(worktrees)
}

/// Parse the output of `git worktree list --porcelain`.
///
/// The porcelain format is machine-readable with one attribute per line,
/// and worktrees separated by blank lines.
fn parse_worktree_list_porcelain(output: &str) -> Result<Vec<WorktreeInfo>> {
    let mut worktrees = Vec::new();
    let mut current_lines: Vec<&str> = Vec::new();
    let mut is_first = true;

    for line in output.lines() {
        if line.is_empty() {
            // End of a worktree block
            if !current_lines.is_empty() {
                if let Some(mut wt) = WorktreeInfo::from_porcelain_lines(&current_lines) {
                    // First worktree in the list is always the main worktree
                    wt.is_main = is_first;
                    is_first = false;
                    worktrees.push(wt);
                }
                current_lines.clear();
            }
        } else {
            current_lines.push(line);
        }
    }

    // Don't forget the last worktree (output may not end with blank line)
    if !current_lines.is_empty() {
        if let Some(mut wt) = WorktreeInfo::from_porcelain_lines(&current_lines) {
            wt.is_main = is_first;
            worktrees.push(wt);
        }
    }

    Ok(worktrees)
}

/// Create a new worktree at the specified path for the given branch.
///
/// If the branch already exists, it will be checked out in the new worktree.
/// If the branch doesn't exist, it will be created from the current HEAD.
///
/// # Arguments
/// * `path` - The path where the worktree should be created
/// * `branch` - The branch name to checkout or create
///
/// # Returns
/// * `Ok(())` - Worktree created successfully
/// * `Err` - If creation fails (e.g., branch already checked out elsewhere)
pub fn create_worktree<P: AsRef<Path>>(path: P, branch: &str) -> Result<()> {
    let path = path.as_ref();

    // First, check if branch exists
    let branch_exists = Command::new("git")
        .args([
            "show-ref",
            "--verify",
            "--quiet",
            &format!("refs/heads/{}", branch),
        ])
        .output()?
        .status
        .success();

    let output = if branch_exists {
        // Branch exists, just add worktree
        Command::new("git")
            .args(["worktree", "add", path.to_string_lossy().as_ref(), branch])
            .output()?
    } else {
        // Create new branch with -b flag
        Command::new("git")
            .args([
                "worktree",
                "add",
                "-b",
                branch,
                path.to_string_lossy().as_ref(),
            ])
            .output()?
    };

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        return Err(Autom8Error::WorktreeError(format!(
            "Failed to create worktree at '{}' for branch '{}': {}",
            path.display(),
            branch,
            stderr.trim()
        )));
    }

    Ok(())
}

/// Remove a worktree at the specified path.
///
/// By default, this will fail if the worktree has uncommitted changes.
/// Use `force: true` to remove even with uncommitted changes.
///
/// # Arguments
/// * `path` - The path of the worktree to remove
/// * `force` - If true, remove even if the worktree has uncommitted changes
///
/// # Returns
/// * `Ok(())` - Worktree removed successfully
/// * `Err` - If removal fails
pub fn remove_worktree<P: AsRef<Path>>(path: P, force: bool) -> Result<()> {
    let path = path.as_ref();
    let path_str = path.to_string_lossy();

    let mut args = vec!["worktree", "remove"];
    if force {
        args.push("--force");
    }
    args.push(path_str.as_ref());

    let output = Command::new("git").args(&args).output()?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        return Err(Autom8Error::WorktreeError(format!(
            "Failed to remove worktree at '{}': {}",
            path.display(),
            stderr.trim()
        )));
    }

    Ok(())
}

/// Get the worktree root for the current directory.
///
/// If the current directory is inside a linked worktree (not the main repo),
/// returns the root path of that worktree. Returns None if in the main repo.
///
/// # Returns
/// * `Ok(Some(path))` - The worktree root if in a linked worktree
/// * `Ok(None)` - If in the main repository (not a linked worktree)
/// * `Err` - If not in a git repository
pub fn get_worktree_root() -> Result<Option<PathBuf>> {
    // git rev-parse --git-common-dir returns the .git dir of the main repo
    // git rev-parse --git-dir returns the .git dir of the current worktree
    // If they're different, we're in a linked worktree

    let git_dir_output = Command::new("git")
        .args(["rev-parse", "--git-dir"])
        .output()?;

    if !git_dir_output.status.success() {
        let stderr = String::from_utf8_lossy(&git_dir_output.stderr);
        return Err(Autom8Error::WorktreeError(format!(
            "Failed to get git directory: {}",
            stderr.trim()
        )));
    }

    let git_dir = String::from_utf8_lossy(&git_dir_output.stdout)
        .trim()
        .to_string();

    // In a linked worktree, git-dir points to .git/worktrees/<name>
    // The gitdir file inside contains the path we need to check
    if git_dir.contains("/worktrees/") || git_dir.contains("\\worktrees\\") {
        // We're in a worktree - get the toplevel
        let toplevel_output = Command::new("git")
            .args(["rev-parse", "--show-toplevel"])
            .output()?;

        if !toplevel_output.status.success() {
            let stderr = String::from_utf8_lossy(&toplevel_output.stderr);
            return Err(Autom8Error::WorktreeError(format!(
                "Failed to get worktree root: {}",
                stderr.trim()
            )));
        }

        let toplevel = String::from_utf8_lossy(&toplevel_output.stdout)
            .trim()
            .to_string();
        return Ok(Some(PathBuf::from(toplevel)));
    }

    Ok(None)
}

/// Get the main repository root (works from any worktree).
///
/// Returns the path to the main repository, regardless of whether
/// the current directory is in the main repo or a linked worktree.
///
/// # Returns
/// * `Ok(path)` - The main repository root path
/// * `Err` - If not in a git repository
pub fn get_main_repo_root() -> Result<PathBuf> {
    // git rev-parse --git-common-dir gives us the path to the main .git directory
    let output = Command::new("git")
        .args(["rev-parse", "--git-common-dir"])
        .output()?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        return Err(Autom8Error::WorktreeError(format!(
            "Failed to get main repo root: {}",
            stderr.trim()
        )));
    }

    let git_common_dir = String::from_utf8_lossy(&output.stdout).trim().to_string();

    // The common dir is the .git directory - we want its parent
    let git_path = PathBuf::from(&git_common_dir);

    // Handle both .git file (in worktrees) and .git directory cases
    // Also handle absolute vs relative paths
    let main_repo_path = if git_path.is_absolute() {
        git_path.parent().map(|p| p.to_path_buf())
    } else {
        // Relative path - resolve it
        let current_dir = std::env::current_dir()?;
        let absolute_git = current_dir.join(&git_path);
        absolute_git
            .canonicalize()
            .ok()
            .and_then(|p| p.parent().map(|p| p.to_path_buf()))
    };

    main_repo_path.ok_or_else(|| {
        Autom8Error::WorktreeError("Failed to determine main repository root".to_string())
    })
}

/// Check if the current working directory is inside a linked worktree.
///
/// Returns true if the CWD is inside a linked worktree (not the main repository).
///
/// # Returns
/// * `Ok(true)` - CWD is inside a linked worktree
/// * `Ok(false)` - CWD is inside the main repository
/// * `Err` - If not in a git repository
pub fn is_in_worktree() -> Result<bool> {
    Ok(get_worktree_root()?.is_some())
}

/// Get the git repository name (basename of the main repo root).
///
/// This function returns the name of the git repository, which is the
/// basename of the main repository root directory. This ensures consistent
/// project identification regardless of whether you're in the main repo
/// or a linked worktree.
///
/// # Returns
/// * `Ok(Some(name))` - The repository name if in a git repository
/// * `Ok(None)` - If not in a git repository
/// * `Err` - If there's an error determining the repo name
///
/// # Example
/// ```no_run
/// use autom8::worktree::get_git_repo_name;
///
/// if let Some(name) = get_git_repo_name().expect("git error") {
///     println!("Repository: {}", name);
/// }
/// ```
pub fn get_git_repo_name() -> Result<Option<String>> {
    // First check if we're in a git repo
    let output = Command::new("git")
        .args(["rev-parse", "--git-common-dir"])
        .output()?;

    if !output.status.success() {
        // Not in a git repository - this is not an error, just means no git
        let stderr = String::from_utf8_lossy(&output.stderr);
        if stderr.contains("not a git repository") {
            return Ok(None);
        }
        return Err(Autom8Error::WorktreeError(format!(
            "Failed to check git repository: {}",
            stderr.trim()
        )));
    }

    // Get the main repo root (works from both main repo and worktrees)
    let main_root = get_main_repo_root()?;

    // Extract the basename
    main_root
        .file_name()
        .and_then(|n| n.to_str())
        .map(|s| Some(s.to_string()))
        .ok_or_else(|| {
            Autom8Error::WorktreeError("Could not determine repository name from path".to_string())
        })
}

// ============================================================================
// Session Identity System (US-002)
// ============================================================================

/// Well-known session ID for the main repository.
pub const MAIN_SESSION_ID: &str = "main";

/// Generate a deterministic session ID from a worktree path.
///
/// The session ID is derived from the SHA-256 hash of the absolute path,
/// taking the first 8 characters. This ensures:
/// - Determinism: same path always produces the same ID
/// - Uniqueness: different paths produce different IDs (with high probability)
/// - Filesystem safety: only alphanumeric characters (hex digits)
/// - Readability: 8 characters is short but sufficient
///
/// # Arguments
/// * `worktree_path` - The absolute path to the worktree directory
///
/// # Returns
/// An 8-character hexadecimal string that uniquely identifies the worktree.
///
/// # Example
/// ```
/// use autom8::worktree::generate_session_id;
/// use std::path::Path;
///
/// let id = generate_session_id(Path::new("/home/user/project-feature"));
/// assert_eq!(id.len(), 8);
/// assert!(id.chars().all(|c| c.is_ascii_hexdigit()));
/// ```
pub fn generate_session_id(worktree_path: &Path) -> String {
    let path_str = worktree_path.to_string_lossy();
    let mut hasher = Sha256::new();
    hasher.update(path_str.as_bytes());
    let result = hasher.finalize();
    // Take first 8 characters of hex representation (4 bytes = 8 hex chars)
    hex::encode(&result[..4])
}

/// Get the session ID for the current working directory.
///
/// This function determines the appropriate session ID based on the current
/// location:
/// - If in the main repository: returns the well-known "main" session ID
/// - If in a linked worktree: returns a hash-based ID from the worktree path
///
/// # Returns
/// * `Ok(String)` - The session ID for the current directory
/// * `Err` - If not in a git repository
///
/// # Example
/// ```no_run
/// use autom8::worktree::get_current_session_id;
///
/// let session_id = get_current_session_id().expect("Not in a git repo");
/// println!("Session ID: {}", session_id);
/// ```
pub fn get_current_session_id() -> Result<String> {
    // Check if we're in a linked worktree
    if let Some(worktree_root) = get_worktree_root()? {
        // In a linked worktree - generate ID from path
        Ok(generate_session_id(&worktree_root))
    } else {
        // In main repository - use well-known ID
        Ok(MAIN_SESSION_ID.to_string())
    }
}

/// Get the session ID for the main repository.
///
/// This function returns the session ID that would be used when running
/// from the main repository (not a linked worktree). This is useful for
/// operations that need to reference the main session regardless of
/// the current working directory.
///
/// # Returns
/// The well-known "main" session ID.
pub fn get_main_session_id() -> String {
    MAIN_SESSION_ID.to_string()
}

/// Get the session ID for a specific worktree path.
///
/// This is a convenience function that combines path resolution with
/// session ID generation. For the main repository path, it returns "main".
/// For linked worktree paths, it generates a hash-based ID.
///
/// # Arguments
/// * `path` - The path to resolve a session ID for
///
/// # Returns
/// * `Ok(String)` - The session ID for the given path
/// * `Err` - If the path is not in a git repository or cannot be resolved
pub fn get_session_id_for_path(path: &Path) -> Result<String> {
    // Get the absolute path
    let abs_path = if path.is_absolute() {
        path.to_path_buf()
    } else {
        std::env::current_dir()?.join(path)
    };

    // Get the main repo root to compare
    let main_root = get_main_repo_root()?;

    // Canonicalize both paths for reliable comparison
    let abs_canonical = abs_path.canonicalize().unwrap_or(abs_path);
    let main_canonical = main_root.canonicalize().unwrap_or(main_root);

    // Check if this is the main repo
    if abs_canonical == main_canonical {
        Ok(MAIN_SESSION_ID.to_string())
    } else {
        Ok(generate_session_id(&abs_canonical))
    }
}

// ============================================================================
// Worktree Path Generation (US-007)
// ============================================================================

/// Convert a branch name to a filesystem-safe slug.
///
/// Replaces slashes with dashes, removes unsafe characters, and normalizes
/// to produce a valid directory name component.
///
/// # Arguments
/// * `branch_name` - The git branch name to slugify
///
/// # Returns
/// A filesystem-safe version of the branch name.
///
/// # Example
/// ```
/// use autom8::worktree::slugify_branch_name;
///
/// assert_eq!(slugify_branch_name("feature/login"), "feature-login");
/// assert_eq!(slugify_branch_name("feat/add-user-auth"), "feat-add-user-auth");
/// ```
pub fn slugify_branch_name(branch_name: &str) -> String {
    branch_name
        .chars()
        .map(|c| {
            if c == '/' || c == '\\' {
                '-'
            } else if c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.' {
                c
            } else {
                '-'
            }
        })
        .collect::<String>()
        // Collapse multiple dashes into one
        .split('-')
        .filter(|s| !s.is_empty())
        .collect::<Vec<_>>()
        .join("-")
}

/// Generate the worktree path from a pattern and parameters.
///
/// Replaces placeholders in the pattern:
/// - `{repo}` - The repository name
/// - `{branch}` - The branch name (slugified)
///
/// The worktree is created as a sibling directory of the main repository.
///
/// # Arguments
/// * `pattern` - The path pattern (e.g., "{repo}-wt-{branch}")
/// * `repo_name` - The repository name
/// * `branch_name` - The branch name (will be slugified)
///
/// # Returns
/// The generated worktree directory name.
///
/// # Example
/// ```
/// use autom8::worktree::generate_worktree_name;
///
/// let name = generate_worktree_name("{repo}-wt-{branch}", "myproject", "feature/login");
/// assert_eq!(name, "myproject-wt-feature-login");
/// ```
pub fn generate_worktree_name(pattern: &str, repo_name: &str, branch_name: &str) -> String {
    let slugified_branch = slugify_branch_name(branch_name);
    pattern
        .replace("{repo}", repo_name)
        .replace("{branch}", &slugified_branch)
}

/// Generate the full path for a worktree.
///
/// Creates the worktree as a sibling directory of the main repository.
///
/// # Arguments
/// * `pattern` - The path pattern (e.g., "{repo}-wt-{branch}")
/// * `branch_name` - The branch name (will be slugified)
///
/// # Returns
/// * `Ok(PathBuf)` - The full path where the worktree should be created
/// * `Err` - If not in a git repository
///
/// # Example
/// ```no_run
/// use autom8::worktree::generate_worktree_path;
///
/// // If main repo is at /home/user/myproject, returns:
/// // /home/user/myproject-wt-feature-login
/// let path = generate_worktree_path("{repo}-wt-{branch}", "feature/login").unwrap();
/// ```
pub fn generate_worktree_path(pattern: &str, branch_name: &str) -> Result<PathBuf> {
    let main_repo = get_main_repo_root()?;
    let repo_name = main_repo
        .file_name()
        .and_then(|n| n.to_str())
        .ok_or_else(|| {
            Autom8Error::WorktreeError("Could not determine repository name".to_string())
        })?;

    let worktree_name = generate_worktree_name(pattern, repo_name, branch_name);

    // Place worktree as sibling of main repo
    let parent = main_repo.parent().ok_or_else(|| {
        Autom8Error::WorktreeError("Could not determine repository parent directory".to_string())
    })?;

    Ok(parent.join(worktree_name))
}

/// Result of ensuring a worktree exists.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WorktreeResult {
    /// A new worktree was created at this path
    Created(PathBuf),
    /// An existing worktree was found and reused at this path
    Reused(PathBuf),
}

impl WorktreeResult {
    /// Get the path regardless of whether it was created or reused.
    pub fn path(&self) -> &Path {
        match self {
            WorktreeResult::Created(p) | WorktreeResult::Reused(p) => p,
        }
    }

    /// Returns true if the worktree was newly created.
    pub fn was_created(&self) -> bool {
        matches!(self, WorktreeResult::Created(_))
    }
}

/// Ensure a worktree exists for the specified branch.
///
/// If a worktree already exists for this branch, it is reused.
/// Otherwise, a new worktree is created.
///
/// # Arguments
/// * `pattern` - The path pattern for the worktree name
/// * `branch_name` - The branch to use for the worktree
///
/// # Returns
/// * `Ok(WorktreeResult::Created(path))` - A new worktree was created
/// * `Ok(WorktreeResult::Reused(path))` - An existing worktree was found
/// * `Err` - If worktree creation fails
pub fn ensure_worktree(pattern: &str, branch_name: &str) -> Result<WorktreeResult> {
    let target_path = generate_worktree_path(pattern, branch_name)?;

    // Check if a worktree already exists at this path or for this branch
    let worktrees = list_worktrees()?;
    for wt in &worktrees {
        // Check if there's a worktree at our target path
        if wt.path == target_path {
            // Verify it has the right branch
            if let Some(ref wt_branch) = wt.branch {
                if wt_branch == branch_name {
                    return Ok(WorktreeResult::Reused(target_path));
                }
            }
            // Path exists but with wrong branch - this is a conflict
            return Err(Autom8Error::WorktreeError(format!(
                "Worktree at '{}' exists but uses branch '{}', not '{}'",
                target_path.display(),
                wt.branch.as_deref().unwrap_or("(detached)"),
                branch_name
            )));
        }

        // Check if there's already a worktree for this branch at a different path
        if let Some(ref wt_branch) = wt.branch {
            if wt_branch == branch_name && !wt.is_main {
                // Branch is already checked out in a worktree - reuse it
                return Ok(WorktreeResult::Reused(wt.path.clone()));
            }
        }
    }

    // No existing worktree found - create a new one
    create_worktree(&target_path, branch_name)?;
    Ok(WorktreeResult::Created(target_path))
}

/// Get detailed information about worktree creation failure.
///
/// Provides suggestions for how to resolve common worktree creation issues.
/// Error messages follow the pattern: what happened → why → how to fix.
///
/// # Arguments
/// * `error` - The error message from the failed git command
/// * `branch_name` - The branch that was being created
/// * `worktree_path` - The path where the worktree was being created
///
/// # Returns
/// A user-friendly error message with suggestions.
pub fn format_worktree_error(error: &str, branch_name: &str, worktree_path: &Path) -> String {
    let mut message = format!(
        "Failed to create worktree for branch '{}' at '{}'.\n\n",
        branch_name,
        worktree_path.display()
    );

    // Analyze the error and provide specific suggestions
    if error.contains("already checked out") {
        message.push_str("Reason: Branch is already checked out in another worktree.\n\n");
        message.push_str("To resolve this, try one of the following:\n");
        message.push_str("  1. Use a different branch name in your spec\n");
        message.push_str("  2. Run `git worktree list` to see existing worktrees\n");
        message
            .push_str("  3. Remove the conflicting worktree with `git worktree remove <path>`\n");
        message.push_str("\nManual worktree creation steps:\n");
        message.push_str(&format!(
            "  git worktree add -b {} '{}'\n",
            branch_name,
            worktree_path.display()
        ));
    } else if error.contains("already exists") {
        message.push_str("Reason: A directory or worktree already exists at this path.\n\n");
        message.push_str("To resolve this, try one of the following:\n");
        message.push_str(&format!(
            "  1. Remove the existing directory: rm -rf '{}'\n",
            worktree_path.display()
        ));
        message.push_str("  2. Use a different branch name in your spec\n");
        message.push_str("  3. Configure a different worktree_path_pattern in config\n");
        message.push_str("\nManual worktree creation steps (after removing existing):\n");
        message.push_str(&format!(
            "  git worktree add '{}' {}\n",
            worktree_path.display(),
            branch_name
        ));
    } else if error.contains("permission denied") || error.contains("Permission denied") {
        message.push_str("Reason: Insufficient permissions to create the worktree directory.\n\n");
        message.push_str("To resolve this, try one of the following:\n");
        message.push_str(&format!(
            "  1. Check write permissions on: {}\n",
            worktree_path
                .parent()
                .map(|p| p.display().to_string())
                .unwrap_or_else(|| "parent directory".to_string())
        ));
        message.push_str("  2. Run with appropriate permissions (e.g., sudo if needed)\n");
        message
            .push_str("  3. Choose a different location in your config's worktree_path_pattern\n");
    } else {
        message.push_str(&format!("Error: {}\n\n", error));
        message.push_str("To resolve this, try one of the following:\n");
        message.push_str("  1. Ensure you're in a git repository\n");
        message.push_str("  2. Run `git worktree list` to check current worktrees\n");
        message.push_str("  3. Check git configuration and permissions\n");
        message.push_str("\nManual worktree creation steps:\n");
        message.push_str(&format!(
            "  git worktree add '{}' {}\n",
            worktree_path.display(),
            branch_name
        ));
    }

    message
}

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

    // ========================================================================
    // Porcelain parsing tests - these test actual parsing logic
    // ========================================================================

    #[test]
    fn test_parse_porcelain_single_worktree() {
        let output = "worktree /home/user/project\nHEAD abc1234567890abcdef1234567890abcdef12345678\nbranch refs/heads/main\n\n";

        let worktrees = parse_worktree_list_porcelain(output).unwrap();
        assert_eq!(worktrees.len(), 1);

        let wt = &worktrees[0];
        assert_eq!(wt.path, PathBuf::from("/home/user/project"));
        assert_eq!(wt.branch, Some("main".to_string()));
        assert_eq!(wt.commit, "abc1234567890abcdef1234567890abcdef12345678");
        assert!(wt.is_main);
        assert!(!wt.is_bare);
    }

    #[test]
    fn test_parse_porcelain_multiple_worktrees() {
        let output = concat!(
            "worktree /home/user/project\n",
            "HEAD abc1234567890abcdef1234567890abcdef12345678\n",
            "branch refs/heads/main\n",
            "\n",
            "worktree /home/user/project-feature\n",
            "HEAD def5678901234abcdef5678901234abcdef56789012\n",
            "branch refs/heads/feature/test\n",
            "\n"
        );

        let worktrees = parse_worktree_list_porcelain(output).unwrap();
        assert_eq!(worktrees.len(), 2);

        assert!(worktrees[0].is_main);
        assert_eq!(worktrees[0].branch, Some("main".to_string()));
        assert!(!worktrees[1].is_main);
        assert_eq!(worktrees[1].branch, Some("feature/test".to_string()));
    }

    #[test]
    fn test_parse_porcelain_special_states() {
        // Detached HEAD
        let output = "worktree /path\nHEAD abc123\ndetached\n\n";
        let wt = &parse_worktree_list_porcelain(output).unwrap()[0];
        assert!(wt.branch.is_none());

        // Bare repo
        let output = "worktree /path.git\nHEAD abc123\nbare\n\n";
        let wt = &parse_worktree_list_porcelain(output).unwrap()[0];
        assert!(wt.is_bare);

        // Locked
        let output = "worktree /path\nHEAD abc123\nbranch refs/heads/main\nlocked\n\n";
        let wt = &parse_worktree_list_porcelain(output).unwrap()[0];
        assert!(wt.is_locked);

        // Prunable
        let output = "worktree /path\nHEAD abc123\nbranch refs/heads/main\nprunable\n\n";
        let wt = &parse_worktree_list_porcelain(output).unwrap()[0];
        assert!(wt.is_prunable);
    }

    #[test]
    fn test_parse_porcelain_edge_cases() {
        // No trailing newline
        let output = "worktree /path\nHEAD abc123\nbranch refs/heads/main";
        assert_eq!(parse_worktree_list_porcelain(output).unwrap().len(), 1);

        // Empty output
        assert!(parse_worktree_list_porcelain("").unwrap().is_empty());

        // Path with spaces
        let output = "worktree /home/user/my project/repo\nHEAD abc123\nbranch refs/heads/main\n\n";
        assert_eq!(
            parse_worktree_list_porcelain(output).unwrap()[0].path,
            PathBuf::from("/home/user/my project/repo")
        );
    }

    #[test]
    fn test_from_porcelain_lines_missing_required_fields() {
        // Missing path
        assert!(
            WorktreeInfo::from_porcelain_lines(&["HEAD abc123", "branch refs/heads/main"])
                .is_none()
        );
        // Missing commit
        assert!(
            WorktreeInfo::from_porcelain_lines(&["worktree /path", "branch refs/heads/main"])
                .is_none()
        );
    }

    // ========================================================================
    // Session ID tests - test determinism and uniqueness
    // ========================================================================

    #[test]
    fn test_generate_session_id_properties() {
        let path = Path::new("/home/user/project-feature");
        let id = generate_session_id(path);

        // 8 hex characters
        assert_eq!(id.len(), 8);
        assert!(id.chars().all(|c| c.is_ascii_hexdigit()));

        // Deterministic
        assert_eq!(id, generate_session_id(path));

        // Different paths produce different IDs
        let id2 = generate_session_id(Path::new("/home/user/other-project"));
        assert_ne!(id, id2);
    }

    #[test]
    fn test_generate_session_id_uniqueness() {
        let paths = [
            "/home/user/project1",
            "/home/user/project2",
            "/tmp/worktree-a",
            "/tmp/worktree-b",
        ];

        let ids: Vec<String> = paths
            .iter()
            .map(|p| generate_session_id(Path::new(p)))
            .collect();

        let unique_ids: std::collections::HashSet<_> = ids.iter().collect();
        assert_eq!(ids.len(), unique_ids.len());
    }

    #[test]
    fn test_main_session_id() {
        assert_eq!(MAIN_SESSION_ID, "main");
        assert_eq!(get_main_session_id(), "main");
    }

    // ========================================================================
    // Branch name slugification tests
    // ========================================================================

    #[test]
    fn test_slugify_branch_name() {
        assert_eq!(slugify_branch_name("feature/login"), "feature-login");
        assert_eq!(
            slugify_branch_name("feature/user/auth"),
            "feature-user-auth"
        );
        assert_eq!(slugify_branch_name("main"), "main");
        assert_eq!(slugify_branch_name("v1.0.0"), "v1.0.0");
        assert_eq!(slugify_branch_name("feature//login"), "feature-login"); // collapses multiple
        assert_eq!(slugify_branch_name("feature@login"), "feature-login"); // removes special chars
    }

    #[test]
    fn test_generate_worktree_name() {
        assert_eq!(
            generate_worktree_name("{repo}-wt-{branch}", "myproject", "feature/login"),
            "myproject-wt-feature-login"
        );
        assert_eq!(
            generate_worktree_name("{repo}_worktree_{branch}", "myproject", "main"),
            "myproject_worktree_main"
        );
    }

    // ========================================================================
    // WorktreeResult tests
    // ========================================================================

    #[test]
    fn test_worktree_result() {
        let path = PathBuf::from("/test/path");
        let created = WorktreeResult::Created(path.clone());
        let reused = WorktreeResult::Reused(path.clone());

        assert_eq!(created.path(), &path);
        assert_eq!(reused.path(), &path);
        assert!(created.was_created());
        assert!(!reused.was_created());
    }

    // ========================================================================
    // Error formatting tests
    // ========================================================================

    #[test]
    fn test_format_worktree_error_messages() {
        // Already checked out
        let msg = format_worktree_error(
            "fatal: branch 'main' is already checked out",
            "main",
            Path::new("/new/worktree"),
        );
        assert!(msg.contains("already checked out"));
        assert!(msg.contains("To resolve"));
        assert!(msg.contains("git worktree"));

        // Already exists
        let msg = format_worktree_error(
            "fatal: already exists",
            "feature",
            Path::new("/new/worktree"),
        );
        assert!(msg.contains("already exists"));
        assert!(msg.contains("after removing existing"));

        // Permission denied
        let msg = format_worktree_error(
            "error: permission denied",
            "feature",
            Path::new("/restricted"),
        );
        assert!(msg.contains("permissions"));

        // Generic error includes manual steps
        let msg = format_worktree_error("unknown error", "feature/login", Path::new("/path/to/wt"));
        assert!(msg.contains("Manual worktree creation"));
        assert!(msg.contains("feature/login"));
    }
}