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

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

2 

3from __future__ import annotations 

4 

5import argparse 

6import os 

7import subprocess 

8from pathlib import Path 

9 

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 

27 

28 

29def main_parallel() -> int: 

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

31 

32 Process issues concurrently using isolated git worktrees. 

33 

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 ) 

54 

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 ) 

117 

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) 

128 

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 ) 

137 

138 args = parser.parse_args() 

139 

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

141 config = BRConfig(project_root) 

142 configure_output(config.cli) 

143 

144 logger = Logger(verbose=not args.quiet) 

145 

146 # Handle cleanup mode 

147 if args.cleanup: 

148 from little_loops.parallel import WorkerPool 

149 

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 

155 

156 # Build priority filter 

157 priority_filter = ( 

158 [p.strip().upper() for p in args.priority.split(",")] if args.priority else None 

159 ) 

160 

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) 

165 

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) 

170 

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" 

179 

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 ) 

200 

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

206 

207 # Create and run orchestrator 

208 from little_loops.parallel import ParallelOrchestrator 

209 

210 orchestrator = ParallelOrchestrator( 

211 parallel_config=parallel_config, 

212 br_config=config, 

213 repo_path=project_root, 

214 verbose=not args.quiet, 

215 ) 

216 

217 return orchestrator.run()