Coverage for little_loops / cli_args.py: 100%
109 statements
« prev ^ index » next coverage.py v7.12.0, created at 2026-05-22 16:19 -0500
« prev ^ index » next coverage.py v7.12.0, created at 2026-05-22 16:19 -0500
1"""Shared CLI argument definitions for little-loops tools.
3Provides reusable functions for adding common command-line arguments
4to argparse parsers, ensuring consistency across ll-auto, ll-parallel,
5and ll-sprint commands.
6"""
8from __future__ import annotations
10import argparse
11import re
12from pathlib import Path
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 )
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 )
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 )
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 )
57def add_skip_arg(parser: argparse.ArgumentParser, help_text: str | None = None) -> None:
58 """Add --skip argument for excluding specific issues.
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 )
75def add_max_workers_arg(parser: argparse.ArgumentParser, default: int | None = None) -> None:
76 """Add --max-workers/-w argument for parallel execution.
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 )
100def add_timeout_arg(parser: argparse.ArgumentParser, default: int | None = None) -> None:
101 """Add --timeout/-t argument for per-issue timeout.
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 )
125def add_idle_timeout_arg(parser: argparse.ArgumentParser) -> None:
126 """Add --idle-timeout argument for idle process termination.
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 )
139def add_handoff_threshold_arg(parser: argparse.ArgumentParser) -> None:
140 """Add --handoff-threshold argument for per-run context handoff override.
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 )
153def add_context_limit_arg(parser: argparse.ArgumentParser) -> None:
154 """Add --context-limit argument for per-run context window size override.
156 Args:
157 parser: The argument parser to add the argument to
158 """
159 parser.add_argument(
160 "--context-limit",
161 type=int,
162 default=None,
163 help="Override context window token estimate (default: from config or 1000000 for Sonnet/Opus, 200000 for Haiku 4.5)",
164 )
167def add_quiet_arg(parser: argparse.ArgumentParser) -> None:
168 """Add --quiet/-q argument to suppress output."""
169 parser.add_argument(
170 "--quiet",
171 "-q",
172 action="store_true",
173 help="Suppress non-essential output",
174 )
177def add_skip_analysis_arg(parser: argparse.ArgumentParser) -> None:
178 """Add --skip-analysis argument to skip dependency discovery."""
179 parser.add_argument(
180 "--skip-analysis",
181 action="store_true",
182 help="Skip dependency analysis (use when dependencies are known to be current)",
183 )
186def add_max_issues_arg(parser: argparse.ArgumentParser) -> None:
187 """Add --max-issues/-m argument for limiting issues processed."""
188 parser.add_argument(
189 "--max-issues",
190 "-m",
191 type=int,
192 default=0,
193 help="Limit number of issues to process (0 = unlimited)",
194 )
197def parse_issue_ids(value: str | None) -> set[str] | None:
198 """Parse comma-separated issue IDs into a set.
200 Args:
201 value: Comma-separated string like "BUG-001,FEAT-002" or None
203 Returns:
204 Set of uppercase issue IDs, or None if value is None
206 Example:
207 >>> parse_issue_ids("BUG-001,feat-002")
208 {'BUG-001', 'FEAT-002'}
209 >>> parse_issue_ids(None)
210 None
211 """
212 if value is None:
213 return None
214 return {i.strip().upper() for i in value.split(",")}
217def parse_issue_ids_ordered(value: str | None) -> list[str] | None:
218 """Parse comma-separated issue IDs into an ordered list.
220 Like parse_issue_ids but preserves input order, enabling callers to
221 honor the sequence in which IDs were specified.
223 Args:
224 value: Comma-separated string like "BUG-001,FEAT-002" or None
226 Returns:
227 List of uppercase issue IDs in input order, or None if value is None
229 Example:
230 >>> parse_issue_ids_ordered("BUG-010,FEAT-005,ENH-020")
231 ['BUG-010', 'FEAT-005', 'ENH-020']
232 >>> parse_issue_ids_ordered(None)
233 None
234 """
235 if value is None:
236 return None
237 return [i.strip().upper() for i in value.split(",")]
240_NUMERIC_RE = re.compile(r"^\d+$")
243def _id_matches(candidate: str, pattern: str) -> bool:
244 """Return True if candidate matches pattern, supporting numeric-only patterns.
246 Args:
247 candidate: Full issue ID like 'ENH-732'
248 pattern: Full ID like 'ENH-732' or numeric suffix like '732'
250 Returns:
251 True if candidate matches the pattern
253 Example:
254 >>> _id_matches("ENH-732", "732")
255 True
256 >>> _id_matches("ENH-732", "ENH-732")
257 True
258 >>> _id_matches("ENH-732", "BUG-732")
259 False
260 """
261 if _NUMERIC_RE.match(pattern):
262 return candidate.split("-")[-1] == pattern
263 return candidate == pattern
266VALID_ISSUE_TYPES = {"BUG", "FEAT", "ENH", "EPIC"}
268VALID_PRIORITIES: frozenset[str] = frozenset({"P0", "P1", "P2", "P3", "P4", "P5"})
271def parse_priorities(value: str | None) -> set[str] | None:
272 """Parse comma-separated priority levels into a validated set.
274 Args:
275 value: Comma-separated string like "P1,P2" or None
277 Returns:
278 Set of uppercase priority strings, or None if value is None
280 Raises:
281 SystemExit: If invalid priority levels are provided (exit code 2)
283 Example:
284 >>> parse_priorities("p1,P2")
285 {'P1', 'P2'}
286 >>> parse_priorities(None)
287 None
288 """
289 if value is None:
290 return None
291 priorities = {p.strip().upper() for p in value.split(",")}
292 invalid = priorities - VALID_PRIORITIES
293 if invalid:
294 import sys
296 print(
297 f"error: invalid priority level(s): {', '.join(sorted(invalid))}. "
298 f"Valid priorities: {', '.join(sorted(VALID_PRIORITIES))}",
299 file=sys.stderr,
300 )
301 sys.exit(2)
302 return priorities
305def add_priority_arg(parser: argparse.ArgumentParser) -> None:
306 """Add --priority/-p argument for filtering issues by priority level."""
307 parser.add_argument(
308 "--priority",
309 "-p",
310 type=str,
311 default=None,
312 help="Comma-separated priority levels to process (e.g., P0, P1,P2)",
313 )
316def add_label_arg(parser: argparse.ArgumentParser) -> None:
317 """Add --label argument for filtering issues by label."""
318 parser.add_argument(
319 "--label",
320 type=str,
321 default=None,
322 help="Comma-separated labels to process (e.g., fsm,cli,quick-win)",
323 )
326def parse_labels(value: str | None) -> set[str] | None:
327 """Parse comma-separated labels into a set.
329 Args:
330 value: Comma-separated string like "fsm,cli" or None
332 Returns:
333 Set of lowercase label strings, or None if value is None
334 """
335 if value is None:
336 return None
337 return {lb.strip().lower() for lb in value.split(",") if lb.strip()}
340def add_type_arg(parser: argparse.ArgumentParser) -> None:
341 """Add --type/-T argument for filtering issues by type prefix."""
342 parser.add_argument(
343 "--type",
344 "-T",
345 type=str,
346 default=None,
347 help="Comma-separated issue types to process (e.g., BUG, FEAT, ENH, EPIC)",
348 )
351def parse_issue_types(value: str | None) -> set[str] | None:
352 """Parse comma-separated issue types into a validated set.
354 Args:
355 value: Comma-separated string like "BUG,ENH" or None
357 Returns:
358 Set of uppercase type prefixes, or None if value is None
360 Raises:
361 SystemExit: If invalid issue types are provided (via argparse error)
363 Example:
364 >>> parse_issue_types("bug,enh")
365 {'BUG', 'ENH'}
366 >>> parse_issue_types(None)
367 None
368 """
369 if value is None:
370 return None
371 types = {t.strip().upper() for t in value.split(",")}
372 invalid = types - VALID_ISSUE_TYPES
373 if invalid:
374 import sys
376 print(
377 f"error: invalid issue type(s): {', '.join(sorted(invalid))}. "
378 f"Valid types: {', '.join(sorted(VALID_ISSUE_TYPES))}",
379 file=sys.stderr,
380 )
381 sys.exit(2)
382 return types
385def add_common_auto_args(parser: argparse.ArgumentParser) -> None:
386 """Add arguments common to ll-auto command.
388 Adds: --resume, --dry-run, --max-issues, --quiet, --only, --skip, --type, --priority,
389 --label, --config, --idle-timeout, --handoff-threshold, --context-limit
390 """
391 add_resume_arg(parser)
392 add_dry_run_arg(parser)
393 add_max_issues_arg(parser)
394 add_quiet_arg(parser)
395 add_only_arg(parser)
396 add_skip_arg(parser)
397 add_type_arg(parser)
398 add_priority_arg(parser)
399 add_label_arg(parser)
400 add_config_arg(parser)
401 add_idle_timeout_arg(parser)
402 add_handoff_threshold_arg(parser)
403 add_context_limit_arg(parser)
406def add_common_parallel_args(parser: argparse.ArgumentParser) -> None:
407 """Add arguments common to parallel execution tools.
409 Adds: --dry-run, --resume, --max-workers, --timeout, --idle-timeout, --quiet, --only, --skip, --type, --label,
410 --config, --context-limit
411 """
412 add_dry_run_arg(parser)
413 add_resume_arg(parser)
414 add_max_workers_arg(parser)
415 add_timeout_arg(parser)
416 add_idle_timeout_arg(parser)
417 add_quiet_arg(parser)
418 add_only_arg(parser)
419 add_skip_arg(parser)
420 add_type_arg(parser)
421 add_label_arg(parser)
422 add_config_arg(parser)
423 add_context_limit_arg(parser)
426__all__ = [
427 "add_dry_run_arg",
428 "add_resume_arg",
429 "add_config_arg",
430 "add_only_arg",
431 "add_skip_arg",
432 "add_type_arg",
433 "add_priority_arg",
434 "add_label_arg",
435 "add_max_workers_arg",
436 "add_timeout_arg",
437 "add_idle_timeout_arg",
438 "add_handoff_threshold_arg",
439 "add_context_limit_arg",
440 "add_quiet_arg",
441 "add_skip_analysis_arg",
442 "add_max_issues_arg",
443 "parse_issue_ids",
444 "parse_issue_types",
445 "parse_priorities",
446 "parse_labels",
447 "VALID_ISSUE_TYPES",
448 "VALID_PRIORITIES",
449 "add_common_auto_args",
450 "add_common_parallel_args",
451]