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