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

1"""ll-parallel: Process issues concurrently using isolated git worktrees.""" 

2 

3from __future__ import annotations 

4 

5import argparse 

6import os 

7import subprocess 

8import sys 

9from pathlib import Path 

10 

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 

33 

34 

35def main_parallel() -> int: 

36 """Entry point for ll-parallel command. 

37 

38 Process issues concurrently using isolated git worktrees. 

39 

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 ) 

61 

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 ) 

130 

131 parser.add_argument( 

132 "--verbose", 

133 "-v", 

134 action="store_true", 

135 help="Enable verbose output (default when --quiet is not set)", 

136 ) 

137 

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) 

150 

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 ) 

160 

161 args = parser.parse_args() 

162 

163 project_root = args.config or Path.cwd() 

164 config = BRConfig(project_root) 

165 configure_output(config.cli) 

166 

167 logger = Logger(verbose=args.verbose or not args.quiet, use_color=use_color_enabled()) 

168 

169 # Handle cleanup mode 

170 if args.cleanup: 

171 from little_loops.parallel import WorkerPool 

172 

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 

178 

179 # Build priority filter (validates against VALID_PRIORITIES) 

180 priority_filter = parse_priorities(args.priority) 

181 

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) 

186 

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) 

191 

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) 

197 

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" 

206 

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 ) 

229 

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() 

235 

236 # Create and run orchestrator 

237 from little_loops.events import EventBus 

238 from little_loops.parallel import ParallelOrchestrator 

239 

240 event_bus = EventBus() 

241 from little_loops.extension import wire_extensions 

242 from little_loops.transport import wire_transports 

243 

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 ) 

253 

254 return orchestrator.run()