Coverage for little_loops / cli_args.py: 31%

117 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2026-06-29 00:54 -0500

1"""Shared CLI argument definitions for little-loops tools. 

2 

3Provides reusable functions for adding common command-line arguments 

4to argparse parsers, ensuring consistency across ll-auto, ll-parallel, 

5and ll-sprint commands. 

6""" 

7 

8from __future__ import annotations 

9 

10import argparse 

11import re 

12from pathlib import Path 

13 

14 

15def add_dry_run_arg(parser: argparse.ArgumentParser) -> None: 

16 """Add --dry-run/-n argument to parser.""" 

17 parser.add_argument( 

18 "--dry-run", 

19 "-n", 

20 action="store_true", 

21 help="Show what would be done without making changes", 

22 ) 

23 

24 

25def add_resume_arg(parser: argparse.ArgumentParser) -> None: 

26 """Add --resume/-r argument to parser.""" 

27 parser.add_argument( 

28 "--resume", 

29 "-r", 

30 action="store_true", 

31 help="Resume from previous checkpoint", 

32 ) 

33 

34 

35def add_config_arg(parser: argparse.ArgumentParser) -> None: 

36 """Add --config/-C argument to parser.""" 

37 parser.add_argument( 

38 "--config", 

39 "-C", 

40 type=Path, 

41 default=None, 

42 help="Path to project root (default: current directory)", 

43 ) 

44 

45 

46def add_only_arg(parser: argparse.ArgumentParser) -> None: 

47 """Add --only/-o argument for filtering specific issues.""" 

48 parser.add_argument( 

49 "--only", 

50 "-o", 

51 type=str, 

52 default=None, 

53 help="Comma-separated list of issue IDs to process (e.g., BUG-001,FEAT-002)", 

54 ) 

55 

56 

57def add_skip_arg(parser: argparse.ArgumentParser, help_text: str | None = None) -> None: 

58 """Add --skip argument for excluding specific issues. 

59 

60 Args: 

61 parser: The argument parser to add the argument to 

62 help_text: Optional custom help text. If not provided, uses default. 

63 """ 

64 if help_text is None: 

65 help_text = "Comma-separated list of issue IDs to skip (e.g., BUG-003,FEAT-004)" 

66 parser.add_argument( 

67 "--skip", 

68 "-s", 

69 type=str, 

70 default=None, 

71 help=help_text, 

72 ) 

73 

74 

75def add_max_workers_arg(parser: argparse.ArgumentParser, default: int | None = None) -> None: 

76 """Add --max-workers/-w argument for parallel execution. 

77 

78 Args: 

79 parser: The argument parser to add the argument to 

80 default: Default value. If None, no default is specified. 

81 """ 

82 if default is not None: 

83 parser.add_argument( 

84 "--max-workers", 

85 "-w", 

86 type=int, 

87 default=default, 

88 help=f"Maximum parallel workers (default: {default})", 

89 ) 

90 else: 

91 parser.add_argument( 

92 "--max-workers", 

93 "-w", 

94 type=int, 

95 default=None, 

96 help="Maximum parallel workers", 

97 ) 

98 

99 

100def add_timeout_arg(parser: argparse.ArgumentParser, default: int | None = None) -> None: 

101 """Add --timeout/-t argument for per-issue timeout. 

102 

103 Args: 

104 parser: The argument parser to add the argument to 

105 default: Default value in seconds. If None, no default is specified. 

106 """ 

107 if default is not None: 

108 parser.add_argument( 

109 "--timeout", 

110 "-t", 

111 type=int, 

112 default=default, 

113 help=f"Timeout in seconds (default: {default})", 

114 ) 

115 else: 

116 parser.add_argument( 

117 "--timeout", 

118 "-t", 

119 type=int, 

120 default=None, 

121 help="Timeout in seconds", 

122 ) 

123 

124 

125def add_idle_timeout_arg(parser: argparse.ArgumentParser) -> None: 

126 """Add --idle-timeout argument for idle process termination. 

127 

128 Args: 

129 parser: The argument parser to add the argument to 

130 """ 

131 parser.add_argument( 

132 "--idle-timeout", 

133 type=int, 

134 default=None, 

135 help="Kill worker if no output for N seconds (0 to disable, default: from config)", 

136 ) 

137 

138 

139def add_handoff_threshold_arg(parser: argparse.ArgumentParser) -> None: 

140 """Add --handoff-threshold argument for per-run context handoff override. 

141 

142 Args: 

143 parser: The argument parser to add the argument to 

144 """ 

145 parser.add_argument( 

146 "--handoff-threshold", 

147 type=int, 

148 default=None, 

149 help="Override auto-handoff context threshold (1-100, default: from config)", 

150 ) 

151 

152 

153def add_intent_arg(parser: argparse.ArgumentParser) -> None: 

154 """Add --intent <query> flag for output filtering. 

155 

156 Args: 

157 parser: The argument parser to add the argument to 

158 """ 

159 parser.add_argument( 

160 "--intent", 

161 type=str, 

162 default=None, 

163 help="Intent query for output filtering (no-op until FTS5 ranking lands)", 

164 ) 

165 

166 

167def add_intent_limit_arg(parser: argparse.ArgumentParser) -> None: 

168 """Add --intent-limit <N> flag for capping intent-filtered output lines. 

169 

170 Args: 

171 parser: The argument parser to add the argument to 

172 """ 

173 parser.add_argument( 

174 "--intent-limit", 

175 type=int, 

176 default=50, 

177 help="Max lines for intent-filtered output (default: 50)", 

178 ) 

179 

180 

181def add_context_limit_arg(parser: argparse.ArgumentParser) -> None: 

182 """Add --context-limit argument for per-run context window size override. 

183 

184 Args: 

185 parser: The argument parser to add the argument to 

186 """ 

187 parser.add_argument( 

188 "--context-limit", 

189 type=int, 

190 default=None, 

191 help="Override context window token estimate (default: auto-detected from session transcript; 200000 for known Claude 4 models, 1000000 for 1M-context sessions)", 

192 ) 

193 

194 

195def add_quiet_arg(parser: argparse.ArgumentParser) -> None: 

196 """Add --quiet/-q argument to suppress output.""" 

197 parser.add_argument( 

198 "--quiet", 

199 "-q", 

200 action="store_true", 

201 help="Suppress non-essential output", 

202 ) 

203 

204 

205def add_skip_analysis_arg(parser: argparse.ArgumentParser) -> None: 

206 """Add --skip-analysis argument to skip dependency discovery.""" 

207 parser.add_argument( 

208 "--skip-analysis", 

209 action="store_true", 

210 help="Skip dependency analysis (use when dependencies are known to be current)", 

211 ) 

212 

213 

214def add_skip_learning_gate_arg(parser: argparse.ArgumentParser) -> None: 

215 """Add --skip-learning-gate argument to bypass the learning-test pre-flight gate.""" 

216 parser.add_argument( 

217 "--skip-learning-gate", 

218 action="store_true", 

219 help="Skip learning-test pre-flight gate (for emergency runs when learning_tests.enabled is true)", 

220 ) 

221 

222 

223def add_max_issues_arg(parser: argparse.ArgumentParser) -> None: 

224 """Add --max-issues/-m argument for limiting issues processed.""" 

225 parser.add_argument( 

226 "--max-issues", 

227 "-m", 

228 type=int, 

229 default=0, 

230 help="Limit number of issues to process (0 = unlimited)", 

231 ) 

232 

233 

234def add_json_arg(parser: argparse.ArgumentParser, help_text: str = "Output as JSON") -> None: 

235 """Add --json/-j argument for machine-readable output. 

236 

237 Args: 

238 parser: The argument parser to add the argument to 

239 help_text: Optional custom help text. If not provided, uses default. 

240 """ 

241 parser.add_argument("-j", "--json", action="store_true", help=help_text) 

242 

243 

244def parse_issue_ids(value: str | None) -> set[str] | None: 

245 """Parse comma-separated issue IDs into a set. 

246 

247 Args: 

248 value: Comma-separated string like "BUG-001,FEAT-002" or None 

249 

250 Returns: 

251 Set of uppercase issue IDs, or None if value is None 

252 

253 Example: 

254 >>> parse_issue_ids("BUG-001,feat-002") 

255 {'BUG-001', 'FEAT-002'} 

256 >>> parse_issue_ids(None) 

257 None 

258 """ 

259 if value is None: 

260 return None 

261 return {i.strip().upper() for i in value.split(",")} 

262 

263 

264def parse_issue_ids_ordered(value: str | None) -> list[str] | None: 

265 """Parse comma-separated issue IDs into an ordered list. 

266 

267 Like parse_issue_ids but preserves input order, enabling callers to 

268 honor the sequence in which IDs were specified. 

269 

270 Args: 

271 value: Comma-separated string like "BUG-001,FEAT-002" or None 

272 

273 Returns: 

274 List of uppercase issue IDs in input order, or None if value is None 

275 

276 Example: 

277 >>> parse_issue_ids_ordered("BUG-010,FEAT-005,ENH-020") 

278 ['BUG-010', 'FEAT-005', 'ENH-020'] 

279 >>> parse_issue_ids_ordered(None) 

280 None 

281 """ 

282 if value is None: 

283 return None 

284 return [i.strip().upper() for i in value.split(",")] 

285 

286 

287_NUMERIC_RE = re.compile(r"^\d+$") 

288 

289 

290def _id_matches(candidate: str, pattern: str) -> bool: 

291 """Return True if candidate matches pattern, supporting numeric-only patterns. 

292 

293 Args: 

294 candidate: Full issue ID like 'ENH-732' 

295 pattern: Full ID like 'ENH-732' or numeric suffix like '732' 

296 

297 Returns: 

298 True if candidate matches the pattern 

299 

300 Example: 

301 >>> _id_matches("ENH-732", "732") 

302 True 

303 >>> _id_matches("ENH-732", "ENH-732") 

304 True 

305 >>> _id_matches("ENH-732", "BUG-732") 

306 False 

307 """ 

308 if _NUMERIC_RE.match(pattern): 

309 return candidate.split("-")[-1] == pattern 

310 return candidate == pattern 

311 

312 

313VALID_ISSUE_TYPES = {"BUG", "FEAT", "ENH", "EPIC"} 

314 

315VALID_PRIORITIES: frozenset[str] = frozenset({"P0", "P1", "P2", "P3", "P4", "P5"}) 

316 

317 

318def parse_priorities(value: str | None) -> set[str] | None: 

319 """Parse comma-separated priority levels into a validated set. 

320 

321 Args: 

322 value: Comma-separated string like "P1,P2" or None 

323 

324 Returns: 

325 Set of uppercase priority strings, or None if value is None 

326 

327 Raises: 

328 SystemExit: If invalid priority levels are provided (exit code 2) 

329 

330 Example: 

331 >>> parse_priorities("p1,P2") 

332 {'P1', 'P2'} 

333 >>> parse_priorities(None) 

334 None 

335 """ 

336 if value is None: 

337 return None 

338 priorities = {p.strip().upper() for p in value.split(",")} 

339 invalid = priorities - VALID_PRIORITIES 

340 if invalid: 

341 import sys 

342 

343 print( 

344 f"error: invalid priority level(s): {', '.join(sorted(invalid))}. " 

345 f"Valid priorities: {', '.join(sorted(VALID_PRIORITIES))}", 

346 file=sys.stderr, 

347 ) 

348 sys.exit(2) 

349 return priorities 

350 

351 

352def add_priority_arg(parser: argparse.ArgumentParser) -> None: 

353 """Add --priority/-p argument for filtering issues by priority level.""" 

354 parser.add_argument( 

355 "--priority", 

356 "-p", 

357 type=str, 

358 default=None, 

359 help="Comma-separated priority levels to process (e.g., P0, P1,P2)", 

360 ) 

361 

362 

363def add_label_arg(parser: argparse.ArgumentParser) -> None: 

364 """Add --label argument for filtering issues by label.""" 

365 parser.add_argument( 

366 "--label", 

367 type=str, 

368 default=None, 

369 help="Comma-separated labels to process (e.g., fsm,cli,quick-win)", 

370 ) 

371 

372 

373def parse_labels(value: str | None) -> set[str] | None: 

374 """Parse comma-separated labels into a set. 

375 

376 Args: 

377 value: Comma-separated string like "fsm,cli" or None 

378 

379 Returns: 

380 Set of lowercase label strings, or None if value is None 

381 """ 

382 if value is None: 

383 return None 

384 return {lb.strip().lower() for lb in value.split(",") if lb.strip()} 

385 

386 

387def add_type_arg(parser: argparse.ArgumentParser) -> None: 

388 """Add --type/-T argument for filtering issues by type prefix.""" 

389 parser.add_argument( 

390 "--type", 

391 "-T", 

392 type=str, 

393 default=None, 

394 help="Comma-separated issue types to process (e.g., BUG, FEAT, ENH, EPIC)", 

395 ) 

396 

397 

398def parse_issue_types(value: str | None) -> set[str] | None: 

399 """Parse comma-separated issue types into a validated set. 

400 

401 Args: 

402 value: Comma-separated string like "BUG,ENH" or None 

403 

404 Returns: 

405 Set of uppercase type prefixes, or None if value is None 

406 

407 Raises: 

408 SystemExit: If invalid issue types are provided (via argparse error) 

409 

410 Example: 

411 >>> parse_issue_types("bug,enh") 

412 {'BUG', 'ENH'} 

413 >>> parse_issue_types(None) 

414 None 

415 """ 

416 if value is None: 

417 return None 

418 types = {t.strip().upper() for t in value.split(",")} 

419 invalid = types - VALID_ISSUE_TYPES 

420 if invalid: 

421 import sys 

422 

423 print( 

424 f"error: invalid issue type(s): {', '.join(sorted(invalid))}. " 

425 f"Valid types: {', '.join(sorted(VALID_ISSUE_TYPES))}", 

426 file=sys.stderr, 

427 ) 

428 sys.exit(2) 

429 return types 

430 

431 

432def add_common_auto_args(parser: argparse.ArgumentParser) -> None: 

433 """Add arguments common to ll-auto command. 

434 

435 Adds: --resume, --dry-run, --max-issues, --quiet, --only, --skip, --type, --priority, 

436 --label, --config, --idle-timeout, --handoff-threshold, --context-limit 

437 """ 

438 add_resume_arg(parser) 

439 add_dry_run_arg(parser) 

440 add_max_issues_arg(parser) 

441 add_quiet_arg(parser) 

442 add_only_arg(parser) 

443 add_skip_arg(parser) 

444 add_type_arg(parser) 

445 add_priority_arg(parser) 

446 add_label_arg(parser) 

447 add_config_arg(parser) 

448 add_idle_timeout_arg(parser) 

449 add_handoff_threshold_arg(parser) 

450 add_context_limit_arg(parser) 

451 

452 

453def add_common_parallel_args(parser: argparse.ArgumentParser) -> None: 

454 """Add arguments common to parallel execution tools. 

455 

456 Adds: --dry-run, --resume, --max-workers, --timeout, --idle-timeout, --quiet, --only, --skip, --type, --label, 

457 --config, --context-limit 

458 """ 

459 add_dry_run_arg(parser) 

460 add_resume_arg(parser) 

461 add_max_workers_arg(parser) 

462 add_timeout_arg(parser) 

463 add_idle_timeout_arg(parser) 

464 add_quiet_arg(parser) 

465 add_only_arg(parser) 

466 add_skip_arg(parser) 

467 add_type_arg(parser) 

468 add_label_arg(parser) 

469 add_config_arg(parser) 

470 add_context_limit_arg(parser) 

471 

472 

473__all__ = [ 

474 "add_dry_run_arg", 

475 "add_resume_arg", 

476 "add_config_arg", 

477 "add_only_arg", 

478 "add_skip_arg", 

479 "add_type_arg", 

480 "add_priority_arg", 

481 "add_json_arg", 

482 "add_label_arg", 

483 "add_max_workers_arg", 

484 "add_timeout_arg", 

485 "add_idle_timeout_arg", 

486 "add_handoff_threshold_arg", 

487 "add_context_limit_arg", 

488 "add_intent_arg", 

489 "add_intent_limit_arg", 

490 "add_quiet_arg", 

491 "add_skip_analysis_arg", 

492 "add_max_issues_arg", 

493 "parse_issue_ids", 

494 "parse_issue_types", 

495 "parse_priorities", 

496 "parse_labels", 

497 "VALID_ISSUE_TYPES", 

498 "VALID_PRIORITIES", 

499 "add_common_auto_args", 

500 "add_common_parallel_args", 

501]