My aggregated monorepo of OCaml code, automaintained
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add monopam feature command for parallel worktree development

Adds 'monopam feature add/remove/list' commands that create git worktrees
in root/work/<name> on branch <name>, enabling multiple Claude instances
to work on different features simultaneously.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+504 -1
+37
CLAUDE.md
··· 14 14 | Sync all repos | `monopam sync` | 15 15 | Sync and push upstream | `monopam sync --remote` | 16 16 | Sync one repo | `monopam sync <repo-name>` | 17 + | Create feature worktree | `monopam feature add <name>` | 18 + | Remove feature worktree | `monopam feature remove <name>` | 19 + | List feature worktrees | `monopam feature list` | 17 20 | Build | `opam exec -- dune build` | 18 21 | Test | `opam exec -- dune test` | 19 22 ··· 60 63 - **Always commit before sync**: `monopam sync` only exports committed changes 61 64 - **Check status first**: Run `monopam status` to see what needs attention 62 65 - **One repo per directory**: Each subdirectory maps to exactly one git remote 66 + 67 + ## Parallel Development with Feature Worktrees 68 + 69 + Feature worktrees allow multiple Claudes (or developers) to work on different 70 + features simultaneously in separate branches: 71 + 72 + ```bash 73 + # Create worktrees for parallel work 74 + monopam feature add auth-system 75 + monopam feature add api-refactor 76 + 77 + # Each worktree is at work/<name> on branch <name> 78 + cd work/auth-system # On branch 'auth-system' 79 + cd work/api-refactor # On branch 'api-refactor' 80 + 81 + # When done, merge back to main 82 + cd mono 83 + git merge auth-system 84 + git merge api-refactor 85 + 86 + # Clean up 87 + monopam feature remove auth-system 88 + monopam feature remove api-refactor 89 + ``` 90 + 91 + Workspace structure with features: 92 + ``` 93 + root/ 94 + ├── mono/ # Main monorepo (main branch) 95 + ├── work/ 96 + │ ├── auth-system/ # Worktree on 'auth-system' branch 97 + │ └── api-refactor/ # Worktree on 'api-refactor' branch 98 + └── ... 99 + ``` 63 100 64 101 ## Troubleshooting 65 102
+163 -1
monopam/bin/main.ml
··· 876 876 in 877 877 Cmd.v info Term.(ret (const run $ package_arg $ json_arg $ no_sync_arg $ logging_term)) 878 878 879 + (* Feature commands *) 880 + 881 + let feature_name_arg = 882 + let doc = "Feature name (used for both worktree directory and branch)" in 883 + Arg.(required & pos 0 (some string) None & info [] ~docv:"NAME" ~doc) 884 + 885 + let feature_add_cmd = 886 + let doc = "Create a new feature worktree for parallel development" in 887 + let man = 888 + [ 889 + `S Manpage.s_description; 890 + `P 891 + "Creates a git worktree at $(b,root/work/<name>) with a new branch named \ 892 + $(b,<name>). This allows parallel development on separate branches, \ 893 + useful for having multiple Claude instances working on different features."; 894 + `S "HOW IT WORKS"; 895 + `P "The command:"; 896 + `I ("1.", "Creates the $(b,work/) directory if it doesn't exist"); 897 + `I ("2.", "Creates a git worktree at $(b,work/<name>)"); 898 + `I ("3.", "Checks out a new branch named $(b,<name>)"); 899 + `S Manpage.s_examples; 900 + `P "Create a feature worktree:"; 901 + `Pre "monopam feature add my-feature\n\ 902 + cd work/my-feature\n\ 903 + # Now you can work here independently"; 904 + `P "Have multiple Claudes work in parallel:"; 905 + `Pre "# Terminal 1\n\ 906 + monopam feature add auth-system\n\ 907 + cd work/auth-system && claude\n\n\ 908 + # Terminal 2\n\ 909 + monopam feature add api-refactor\n\ 910 + cd work/api-refactor && claude"; 911 + ] 912 + in 913 + let info = Cmd.info "add" ~doc ~man in 914 + let run name () = 915 + Eio_main.run @@ fun env -> 916 + with_verse_config env @@ fun verse_config -> 917 + let fs = Eio.Stdenv.fs env in 918 + let proc = Eio.Stdenv.process_mgr env in 919 + match Monopam.Feature.add ~proc ~fs ~config:verse_config ~name () with 920 + | Ok entry -> 921 + Fmt.pr "Created feature worktree '%s' at %a@." entry.name Fpath.pp entry.path; 922 + Fmt.pr "@.To start working:@."; 923 + Fmt.pr " cd %a@." Fpath.pp entry.path; 924 + `Ok () 925 + | Error e -> 926 + Fmt.epr "Error: %a@." Monopam.Feature.pp_error_with_hint e; 927 + `Error (false, "feature add failed") 928 + in 929 + Cmd.v info Term.(ret (const run $ feature_name_arg $ logging_term)) 930 + 931 + let feature_remove_cmd = 932 + let doc = "Remove a feature worktree" in 933 + let man = 934 + [ 935 + `S Manpage.s_description; 936 + `P 937 + "Removes the git worktree at $(b,root/work/<name>). The branch is not \ 938 + deleted, so you can recreate the worktree later if needed."; 939 + `S "OPTIONS"; 940 + `I ("--force", "Remove even if there are uncommitted changes"); 941 + `S Manpage.s_examples; 942 + `P "Remove a completed feature worktree:"; 943 + `Pre "monopam feature remove my-feature"; 944 + `P "Force remove with uncommitted changes:"; 945 + `Pre "monopam feature remove my-feature --force"; 946 + ] 947 + in 948 + let info = Cmd.info "remove" ~doc ~man in 949 + let force_arg = 950 + let doc = "Remove even if there are uncommitted changes." in 951 + Arg.(value & flag & info [ "force"; "f" ] ~doc) 952 + in 953 + let run name force () = 954 + Eio_main.run @@ fun env -> 955 + with_verse_config env @@ fun verse_config -> 956 + let fs = Eio.Stdenv.fs env in 957 + let proc = Eio.Stdenv.process_mgr env in 958 + match Monopam.Feature.remove ~proc ~fs ~config:verse_config ~name ~force () with 959 + | Ok () -> 960 + Fmt.pr "Removed feature worktree '%s'.@." name; 961 + `Ok () 962 + | Error e -> 963 + Fmt.epr "Error: %a@." Monopam.Feature.pp_error_with_hint e; 964 + `Error (false, "feature remove failed") 965 + in 966 + Cmd.v info Term.(ret (const run $ feature_name_arg $ force_arg $ logging_term)) 967 + 968 + let feature_list_cmd = 969 + let doc = "List all feature worktrees" in 970 + let man = 971 + [ 972 + `S Manpage.s_description; 973 + `P "Lists all git worktrees in the $(b,root/work/) directory."; 974 + `S Manpage.s_examples; 975 + `Pre "monopam feature list"; 976 + ] 977 + in 978 + let info = Cmd.info "list" ~doc ~man in 979 + let run () = 980 + Eio_main.run @@ fun env -> 981 + with_verse_config env @@ fun verse_config -> 982 + let fs = Eio.Stdenv.fs env in 983 + let proc = Eio.Stdenv.process_mgr env in 984 + let entries = Monopam.Feature.list ~proc ~fs ~config:verse_config () in 985 + if entries = [] then 986 + Fmt.pr "No feature worktrees found.@." 987 + else begin 988 + Fmt.pr "Feature worktrees:@."; 989 + List.iter (fun entry -> 990 + Fmt.pr " %s -> %a (branch: %s)@." 991 + entry.Monopam.Feature.name 992 + Fpath.pp entry.Monopam.Feature.path 993 + entry.Monopam.Feature.branch 994 + ) entries 995 + end; 996 + `Ok () 997 + in 998 + Cmd.v info Term.(ret (const run $ logging_term)) 999 + 1000 + let feature_cmd = 1001 + let doc = "Manage feature worktrees for parallel development" in 1002 + let man = 1003 + [ 1004 + `S Manpage.s_description; 1005 + `P 1006 + "Feature worktrees allow parallel development on separate branches of \ 1007 + the monorepo. This is useful for having multiple Claude instances \ 1008 + working on different features simultaneously."; 1009 + `S "WORKSPACE STRUCTURE"; 1010 + `P "Feature worktrees are created in the $(b,work/) directory:"; 1011 + `Pre "root/\n\ 1012 + ├── mono/ # Main monorepo\n\ 1013 + ├── work/\n\ 1014 + │ ├── feature-a/ # Worktree on branch 'feature-a'\n\ 1015 + │ └── feature-b/ # Worktree on branch 'feature-b'\n\ 1016 + └── ..."; 1017 + `S "COMMANDS"; 1018 + `I ("add <name>", "Create a new feature worktree"); 1019 + `I ("remove <name>", "Remove a feature worktree"); 1020 + `I ("list", "List all feature worktrees"); 1021 + `S "WORKFLOW"; 1022 + `P "Typical workflow for parallel development:"; 1023 + `Pre "# Create feature worktrees\n\ 1024 + monopam feature add auth-system\n\ 1025 + monopam feature add api-cleanup\n\n\ 1026 + # Work in each worktree independently\n\ 1027 + cd work/auth-system && claude\n\ 1028 + cd work/api-cleanup && claude\n\n\ 1029 + # When done, merge branches back to main\n\ 1030 + cd mono\n\ 1031 + git merge auth-system\n\ 1032 + git merge api-cleanup\n\n\ 1033 + # Clean up worktrees\n\ 1034 + monopam feature remove auth-system\n\ 1035 + monopam feature remove api-cleanup"; 1036 + ] 1037 + in 1038 + let info = Cmd.info "feature" ~doc ~man in 1039 + Cmd.group info [ feature_add_cmd; feature_remove_cmd; feature_list_cmd ] 1040 + 879 1041 (* Main command group *) 880 1042 881 1043 let main_cmd = ··· 971 1133 in 972 1134 let info = Cmd.info "monopam" ~version:"%%VERSION%%" ~doc ~man in 973 1135 Cmd.group info 974 - [ status_cmd; sync_cmd; changes_cmd; opam_cmd; doctor_cmd; verse_cmd ] 1136 + [ status_cmd; sync_cmd; changes_cmd; opam_cmd; doctor_cmd; verse_cmd; feature_cmd ] 975 1137 976 1138 let () = exit (Cmd.eval main_cmd)
+85
monopam/lib/feature.ml
··· 1 + type error = 2 + | Git_error of Git.error 3 + | Feature_exists of string 4 + | Feature_not_found of string 5 + | Config_error of string 6 + 7 + let pp_error ppf = function 8 + | Git_error e -> Fmt.pf ppf "Git error: %a" Git.pp_error e 9 + | Feature_exists name -> Fmt.pf ppf "Feature '%s' already exists" name 10 + | Feature_not_found name -> Fmt.pf ppf "Feature '%s' not found" name 11 + | Config_error msg -> Fmt.pf ppf "Configuration error: %s" msg 12 + 13 + let error_hint = function 14 + | Git_error _ -> Some "Check that the monorepo is properly initialized" 15 + | Feature_exists name -> 16 + Some (Printf.sprintf "Run 'monopam feature remove %s' first if you want to recreate it" name) 17 + | Feature_not_found name -> 18 + Some (Printf.sprintf "Run 'monopam feature list' to see available features, or 'monopam feature add %s' to create it" name) 19 + | Config_error _ -> Some "Run 'monopam verse init' to create a workspace configuration" 20 + 21 + let pp_error_with_hint ppf e = 22 + pp_error ppf e; 23 + match error_hint e with 24 + | Some hint -> Fmt.pf ppf "@.Hint: %s" hint 25 + | None -> () 26 + 27 + type entry = { 28 + name : string; 29 + path : Fpath.t; 30 + branch : string; 31 + } 32 + 33 + let pp_entry ppf e = 34 + Fmt.pf ppf "%s -> %a (branch: %s)" e.name Fpath.pp e.path e.branch 35 + 36 + (* Get the work directory path: root/work *) 37 + let work_path config = Fpath.(Verse_config.root config / "work") 38 + 39 + (* Get the feature worktree path: root/work/<name> *) 40 + let feature_path config name = Fpath.(work_path config / name) 41 + 42 + let add ~proc ~fs ~config ~name () = 43 + let mono = Verse_config.mono_path config in 44 + let work_dir = work_path config in 45 + let wt_path = feature_path config name in 46 + (* Check if feature already exists *) 47 + if Git.Worktree.exists ~proc ~fs ~repo:mono ~path:wt_path then 48 + Error (Feature_exists name) 49 + else begin 50 + (* Ensure work directory exists *) 51 + let work_eio = Eio.Path.(fs / Fpath.to_string work_dir) in 52 + (try Eio.Path.mkdirs ~perm:0o755 work_eio with Eio.Io _ -> ()); 53 + (* Create the worktree with a new branch *) 54 + match Git.Worktree.add ~proc ~fs ~repo:mono ~path:wt_path ~branch:name () with 55 + | Error e -> Error (Git_error e) 56 + | Ok () -> Ok { name; path = wt_path; branch = name } 57 + end 58 + 59 + let remove ~proc ~fs ~config ~name ~force () = 60 + let mono = Verse_config.mono_path config in 61 + let wt_path = feature_path config name in 62 + (* Check if feature exists *) 63 + if not (Git.Worktree.exists ~proc ~fs ~repo:mono ~path:wt_path) then 64 + Error (Feature_not_found name) 65 + else 66 + match Git.Worktree.remove ~proc ~fs ~repo:mono ~path:wt_path ~force () with 67 + | Error e -> Error (Git_error e) 68 + | Ok () -> Ok () 69 + 70 + let list ~proc ~fs ~config () = 71 + let mono = Verse_config.mono_path config in 72 + let work_dir = work_path config in 73 + let all_worktrees = Git.Worktree.list ~proc ~fs mono in 74 + (* Filter to only worktrees under work/ directory *) 75 + List.filter_map (fun (wt : Git.Worktree.entry) -> 76 + (* Check if this worktree is under the work directory *) 77 + let wt_str = Fpath.to_string wt.path in 78 + let work_str = Fpath.to_string work_dir in 79 + if String.starts_with ~prefix:work_str wt_str then 80 + let name = Fpath.basename wt.path in 81 + let branch = Option.value ~default:name wt.branch in 82 + Some { name; path = wt.path; branch } 83 + else 84 + None 85 + ) all_worktrees
+79
monopam/lib/feature.mli
··· 1 + (** Feature worktree management. 2 + 3 + This module provides operations for managing feature worktrees that allow 4 + parallel development on separate branches of the monorepo. *) 5 + 6 + (** {1 Types} *) 7 + 8 + (** Errors from feature operations. *) 9 + type error = 10 + | Git_error of Git.error (** Git operation error *) 11 + | Feature_exists of string (** Feature worktree already exists *) 12 + | Feature_not_found of string (** Feature worktree does not exist *) 13 + | Config_error of string (** Configuration error *) 14 + 15 + val pp_error : error Fmt.t 16 + (** [pp_error] formats errors. *) 17 + 18 + val pp_error_with_hint : error Fmt.t 19 + (** [pp_error_with_hint] formats errors with a helpful hint. *) 20 + 21 + (** A feature worktree entry. *) 22 + type entry = { 23 + name : string; (** Feature name *) 24 + path : Fpath.t; (** Path to the worktree *) 25 + branch : string; (** Branch name *) 26 + } 27 + 28 + val pp_entry : entry Fmt.t 29 + (** [pp_entry] formats a feature entry. *) 30 + 31 + (** {1 Operations} *) 32 + 33 + val add : 34 + proc:_ Eio.Process.mgr -> 35 + fs:Eio.Fs.dir_ty Eio.Path.t -> 36 + config:Verse_config.t -> 37 + name:string -> 38 + unit -> 39 + (entry, error) result 40 + (** [add ~proc ~fs ~config ~name ()] creates a new feature worktree. 41 + 42 + Creates a git worktree at [root/work/<name>] on a new branch named [<name>]. 43 + 44 + @param proc Eio process manager 45 + @param fs Eio filesystem 46 + @param config Verse configuration 47 + @param name Feature name (used for both directory and branch) *) 48 + 49 + val remove : 50 + proc:_ Eio.Process.mgr -> 51 + fs:Eio.Fs.dir_ty Eio.Path.t -> 52 + config:Verse_config.t -> 53 + name:string -> 54 + force:bool -> 55 + unit -> 56 + (unit, error) result 57 + (** [remove ~proc ~fs ~config ~name ~force ()] removes a feature worktree. 58 + 59 + Removes the worktree at [root/work/<name>]. The branch is not deleted. 60 + 61 + @param proc Eio process manager 62 + @param fs Eio filesystem 63 + @param config Verse configuration 64 + @param name Feature name 65 + @param force If true, remove even with uncommitted changes *) 66 + 67 + val list : 68 + proc:_ Eio.Process.mgr -> 69 + fs:Eio.Fs.dir_ty Eio.Path.t -> 70 + config:Verse_config.t -> 71 + unit -> 72 + entry list 73 + (** [list ~proc ~fs ~config ()] returns all feature worktrees. 74 + 75 + Only returns worktrees in the [root/work/] directory. 76 + 77 + @param proc Eio process manager 78 + @param fs Eio filesystem 79 + @param config Verse configuration *)
+82
monopam/lib/git.ml
··· 406 406 [ "rev-list"; "--count"; base ^ ".." ^ head ] with 407 407 | Error _ -> 0 408 408 | Ok s -> try int_of_string (String.trim s) with _ -> 0 409 + 410 + module Worktree = struct 411 + type entry = { 412 + path : Fpath.t; 413 + head : string; 414 + branch : string option; 415 + } 416 + 417 + let add ~proc ~fs ~repo ~path ~branch () = 418 + let cwd = path_to_eio ~fs repo in 419 + let path_str = Fpath.to_string path in 420 + run_git_ok ~proc ~cwd 421 + [ "worktree"; "add"; "-b"; branch; path_str ] 422 + |> Result.map ignore 423 + 424 + let remove ~proc ~fs ~repo ~path ~force () = 425 + let cwd = path_to_eio ~fs repo in 426 + let path_str = Fpath.to_string path in 427 + let args = 428 + if force then [ "worktree"; "remove"; "--force"; path_str ] 429 + else [ "worktree"; "remove"; path_str ] 430 + in 431 + run_git_ok ~proc ~cwd args |> Result.map ignore 432 + 433 + let list ~proc ~fs repo = 434 + let cwd = path_to_eio ~fs repo in 435 + match run_git_ok ~proc ~cwd [ "worktree"; "list"; "--porcelain" ] with 436 + | Error _ -> [] 437 + | Ok output -> 438 + if String.trim output = "" then [] 439 + else 440 + (* Parse porcelain output: blocks separated by blank lines 441 + Each block has: 442 + worktree /path/to/worktree 443 + HEAD abc123... 444 + branch refs/heads/branchname (or detached) *) 445 + let lines = String.split_on_char '\n' output in 446 + let rec parse_entries acc current_path current_head current_branch = function 447 + | [] -> 448 + (* Finalize last entry if we have one *) 449 + (match current_path, current_head with 450 + | Some p, Some h -> 451 + let entry = { path = p; head = h; branch = current_branch } in 452 + List.rev (entry :: acc) 453 + | _ -> List.rev acc) 454 + | "" :: rest -> 455 + (* End of entry block *) 456 + (match current_path, current_head with 457 + | Some p, Some h -> 458 + let entry = { path = p; head = h; branch = current_branch } in 459 + parse_entries (entry :: acc) None None None rest 460 + | _ -> parse_entries acc None None None rest) 461 + | line :: rest -> 462 + if String.starts_with ~prefix:"worktree " line then 463 + let path_str = String.sub line 9 (String.length line - 9) in 464 + (match Fpath.of_string path_str with 465 + | Ok p -> parse_entries acc (Some p) current_head current_branch rest 466 + | Error _ -> parse_entries acc current_path current_head current_branch rest) 467 + else if String.starts_with ~prefix:"HEAD " line then 468 + let head = String.sub line 5 (String.length line - 5) in 469 + parse_entries acc current_path (Some head) current_branch rest 470 + else if String.starts_with ~prefix:"branch " line then 471 + let branch_ref = String.sub line 7 (String.length line - 7) in 472 + (* Extract branch name from refs/heads/... *) 473 + let branch = 474 + if String.starts_with ~prefix:"refs/heads/" branch_ref then 475 + Some (String.sub branch_ref 11 (String.length branch_ref - 11)) 476 + else 477 + Some branch_ref 478 + in 479 + parse_entries acc current_path current_head branch rest 480 + else if line = "detached" then 481 + parse_entries acc current_path current_head None rest 482 + else 483 + parse_entries acc current_path current_head current_branch rest 484 + in 485 + parse_entries [] None None None lines 486 + 487 + let exists ~proc ~fs ~repo ~path = 488 + let worktrees = list ~proc ~fs repo in 489 + List.exists (fun e -> Fpath.equal e.path path) worktrees 490 + end
+56
monopam/lib/git.mli
··· 458 458 int 459 459 (** [count_commits_between ~proc ~fs ~repo ~base ~head ()] counts the number of 460 460 commits between base and head (exclusive of base, inclusive of head). *) 461 + 462 + (** {1 Worktree Operations} *) 463 + 464 + (** Operations for git worktree management. *) 465 + module Worktree : sig 466 + (** A git worktree entry. *) 467 + type entry = { 468 + path : Fpath.t; (** Absolute path to the worktree *) 469 + head : string; (** HEAD commit hash *) 470 + branch : string option; (** Branch name if not detached *) 471 + } 472 + 473 + val add : 474 + proc:_ Eio.Process.mgr -> 475 + fs:Eio.Fs.dir_ty Eio.Path.t -> 476 + repo:Fpath.t -> 477 + path:Fpath.t -> 478 + branch:string -> 479 + unit -> 480 + (unit, error) result 481 + (** [add ~proc ~fs ~repo ~path ~branch ()] creates a new worktree at [path] 482 + with a new branch [branch]. 483 + 484 + @param repo Path to the main repository 485 + @param path Path where the worktree will be created 486 + @param branch Name of the new branch to create *) 487 + 488 + val remove : 489 + proc:_ Eio.Process.mgr -> 490 + fs:Eio.Fs.dir_ty Eio.Path.t -> 491 + repo:Fpath.t -> 492 + path:Fpath.t -> 493 + force:bool -> 494 + unit -> 495 + (unit, error) result 496 + (** [remove ~proc ~fs ~repo ~path ~force ()] removes a worktree. 497 + 498 + @param repo Path to the main repository 499 + @param path Path to the worktree to remove 500 + @param force If true, remove even if there are uncommitted changes *) 501 + 502 + val list : 503 + proc:_ Eio.Process.mgr -> 504 + fs:Eio.Fs.dir_ty Eio.Path.t -> 505 + Fpath.t -> 506 + entry list 507 + (** [list ~proc ~fs repo] returns all worktrees for the repository. *) 508 + 509 + val exists : 510 + proc:_ Eio.Process.mgr -> 511 + fs:Eio.Fs.dir_ty Eio.Path.t -> 512 + repo:Fpath.t -> 513 + path:Fpath.t -> 514 + bool 515 + (** [exists ~proc ~fs ~repo ~path] returns true if a worktree exists at [path]. *) 516 + end
+1
monopam/lib/monopam.ml
··· 10 10 module Cross_status = Cross_status 11 11 module Forks = Forks 12 12 module Doctor = Doctor 13 + module Feature = Feature 13 14 14 15 let src = Logs.Src.create "monopam" ~doc:"Monopam operations" 15 16
+1
monopam/lib/monopam.mli
··· 34 34 module Cross_status = Cross_status 35 35 module Forks = Forks 36 36 module Doctor = Doctor 37 + module Feature = Feature 37 38 38 39 (** {1 High-Level Operations} *) 39 40