Coverage for little_loops / cli / parallel.py: 16%
63 statements
« prev ^ index » next coverage.py v7.12.0, created at 2026-03-18 16:18 -0500
« prev ^ index » next coverage.py v7.12.0, created at 2026-03-18 16:18 -0500
1"""ll-parallel: Process issues concurrently using isolated git worktrees."""
3from __future__ import annotations
5import argparse
6import os
7import subprocess
8from pathlib import Path
10from little_loops.cli.output import configure_output
11from little_loops.cli_args import (
12 add_dry_run_arg,
13 add_handoff_threshold_arg,
14 add_idle_timeout_arg,
15 add_max_issues_arg,
16 add_only_arg,
17 add_quiet_arg,
18 add_resume_arg,
19 add_skip_arg,
20 add_timeout_arg,
21 add_type_arg,
22 parse_issue_ids,
23 parse_issue_types,
24)
25from little_loops.config import BRConfig
26from little_loops.logger import Logger
29def main_parallel() -> int:
30 """Entry point for ll-parallel command.
32 Process issues concurrently using isolated git worktrees.
34 Returns:
35 Exit code (0 = success)
36 """
37 parser = argparse.ArgumentParser(
38 description="Process issues concurrently using isolated git worktrees",
39 formatter_class=argparse.RawDescriptionHelpFormatter,
40 epilog="""
41Examples:
42 %(prog)s # Process with default workers
43 %(prog)s --workers 3 # Use 3 parallel workers
44 %(prog)s --dry-run # Preview what would be processed
45 %(prog)s --priority P1,P2 # Only process P1 and P2 issues
46 %(prog)s --cleanup # Clean up worktrees and exit
47 %(prog)s --stream-output # Stream Claude CLI output in real-time
48 %(prog)s --only BUG-001,BUG-002 # Process only specific issues
49 %(prog)s --skip BUG-003 # Skip specific issues
50 %(prog)s --type BUG # Process only bugs
51 %(prog)s --type BUG,ENH # Process bugs and enhancements
52""",
53 )
55 # Parallel-specific arguments (--workers, not --max-workers)
56 parser.add_argument(
57 "--workers",
58 "-w",
59 type=int,
60 default=None,
61 help="Number of parallel workers (default: from config or 2)",
62 )
63 parser.add_argument(
64 "--priority",
65 "-p",
66 type=str,
67 default=None,
68 help="Comma-separated priorities to process (default: all)",
69 )
70 parser.add_argument(
71 "--worktree-base",
72 type=Path,
73 default=None,
74 help="Base directory for git worktrees",
75 )
76 parser.add_argument(
77 "--cleanup",
78 "-c",
79 action="store_true",
80 help="Clean up all worktrees and exit",
81 )
82 parser.add_argument(
83 "--merge-pending",
84 action="store_true",
85 help="Attempt to merge pending work from previous interrupted runs",
86 )
87 parser.add_argument(
88 "--clean-start",
89 action="store_true",
90 help="Remove all worktrees and start fresh (skip pending work check)",
91 )
92 parser.add_argument(
93 "--ignore-pending",
94 action="store_true",
95 help="Report pending work but continue without merging",
96 )
97 parser.add_argument(
98 "--stream-output",
99 action="store_true",
100 help="Stream Claude CLI subprocess output to console",
101 )
102 parser.add_argument(
103 "--show-model",
104 action="store_true",
105 help="Make API call to verify and display model on worktree setup",
106 )
107 parser.add_argument(
108 "--overlap-detection",
109 action="store_true",
110 help="Enable pre-flight overlap detection to reduce merge conflicts (ENH-143)",
111 )
112 parser.add_argument(
113 "--warn-only",
114 action="store_true",
115 help="With --overlap-detection, warn about overlaps instead of serializing",
116 )
118 # Add common arguments from shared module
119 add_dry_run_arg(parser)
120 add_resume_arg(parser)
121 add_timeout_arg(parser)
122 add_idle_timeout_arg(parser)
123 add_handoff_threshold_arg(parser)
124 add_quiet_arg(parser)
125 add_only_arg(parser)
126 add_skip_arg(parser)
127 add_type_arg(parser)
129 # Add max-issues and config individually (different help text needed)
130 add_max_issues_arg(parser)
131 parser.add_argument(
132 "--config",
133 type=Path,
134 default=None,
135 help="Path to project root",
136 )
138 args = parser.parse_args()
140 project_root = args.config or Path.cwd()
141 config = BRConfig(project_root)
142 configure_output(config.cli)
144 logger = Logger(verbose=not args.quiet)
146 # Handle cleanup mode
147 if args.cleanup:
148 from little_loops.parallel import WorkerPool
150 parallel_config = config.create_parallel_config()
151 pool = WorkerPool(parallel_config, config, logger, project_root)
152 pool.cleanup_all_worktrees()
153 logger.success("Cleanup complete")
154 return 0
156 # Build priority filter
157 priority_filter = (
158 [p.strip().upper() for p in args.priority.split(",")] if args.priority else None
159 )
161 if args.handoff_threshold is not None:
162 if not (1 <= args.handoff_threshold <= 100):
163 parser.error("--handoff-threshold must be between 1 and 100")
164 os.environ["LL_HANDOFF_THRESHOLD"] = str(args.handoff_threshold)
166 # Parse issue ID filters
167 only_ids = parse_issue_ids(args.only)
168 skip_ids = parse_issue_ids(args.skip)
169 type_prefixes = parse_issue_types(args.type)
171 # Detect current branch for rebase/merge operations (BUG-439)
172 _branch_result = subprocess.run(
173 ["git", "rev-parse", "--abbrev-ref", "HEAD"],
174 capture_output=True,
175 text=True,
176 cwd=project_root,
177 )
178 _base_branch = _branch_result.stdout.strip() if _branch_result.returncode == 0 else "main"
180 # Create parallel config with CLI overrides
181 parallel_config = config.create_parallel_config(
182 max_workers=args.workers,
183 priority_filter=priority_filter,
184 max_issues=args.max_issues,
185 dry_run=args.dry_run,
186 timeout_seconds=args.timeout,
187 idle_timeout_per_issue=args.idle_timeout,
188 stream_output=args.stream_output if args.stream_output else None,
189 show_model=args.show_model if args.show_model else None,
190 only_ids=only_ids,
191 skip_ids=skip_ids,
192 type_prefixes=type_prefixes,
193 merge_pending=args.merge_pending,
194 clean_start=args.clean_start,
195 ignore_pending=args.ignore_pending,
196 overlap_detection=args.overlap_detection,
197 serialize_overlapping=not args.warn_only,
198 base_branch=_base_branch,
199 )
201 # Delete state file if not resuming
202 if not args.resume:
203 state_file = config.get_parallel_state_file()
204 if state_file.exists():
205 state_file.unlink()
207 # Create and run orchestrator
208 from little_loops.parallel import ParallelOrchestrator
210 orchestrator = ParallelOrchestrator(
211 parallel_config=parallel_config,
212 br_config=config,
213 repo_path=project_root,
214 verbose=not args.quiet,
215 )
217 return orchestrator.run()