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