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