Coverage for little_loops / cli_args.py: 30%
111 statements
« prev ^ index » next coverage.py v7.12.0, created at 2026-06-08 15:34 -0500
« prev ^ index » next coverage.py v7.12.0, created at 2026-06-08 15:34 -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: auto-detected from session transcript; 200000 for known Claude 4 models, 1000000 for 1M-context sessions)",
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 add_json_arg(parser: argparse.ArgumentParser, help_text: str = "Output as JSON") -> None:
198 """Add --json/-j argument for machine-readable output.
200 Args:
201 parser: The argument parser to add the argument to
202 help_text: Optional custom help text. If not provided, uses default.
203 """
204 parser.add_argument("-j", "--json", action="store_true", help=help_text)
207def parse_issue_ids(value: str | None) -> set[str] | None:
208 """Parse comma-separated issue IDs into a set.
210 Args:
211 value: Comma-separated string like "BUG-001,FEAT-002" or None
213 Returns:
214 Set of uppercase issue IDs, or None if value is None
216 Example:
217 >>> parse_issue_ids("BUG-001,feat-002")
218 {'BUG-001', 'FEAT-002'}
219 >>> parse_issue_ids(None)
220 None
221 """
222 if value is None:
223 return None
224 return {i.strip().upper() for i in value.split(",")}
227def parse_issue_ids_ordered(value: str | None) -> list[str] | None:
228 """Parse comma-separated issue IDs into an ordered list.
230 Like parse_issue_ids but preserves input order, enabling callers to
231 honor the sequence in which IDs were specified.
233 Args:
234 value: Comma-separated string like "BUG-001,FEAT-002" or None
236 Returns:
237 List of uppercase issue IDs in input order, or None if value is None
239 Example:
240 >>> parse_issue_ids_ordered("BUG-010,FEAT-005,ENH-020")
241 ['BUG-010', 'FEAT-005', 'ENH-020']
242 >>> parse_issue_ids_ordered(None)
243 None
244 """
245 if value is None:
246 return None
247 return [i.strip().upper() for i in value.split(",")]
250_NUMERIC_RE = re.compile(r"^\d+$")
253def _id_matches(candidate: str, pattern: str) -> bool:
254 """Return True if candidate matches pattern, supporting numeric-only patterns.
256 Args:
257 candidate: Full issue ID like 'ENH-732'
258 pattern: Full ID like 'ENH-732' or numeric suffix like '732'
260 Returns:
261 True if candidate matches the pattern
263 Example:
264 >>> _id_matches("ENH-732", "732")
265 True
266 >>> _id_matches("ENH-732", "ENH-732")
267 True
268 >>> _id_matches("ENH-732", "BUG-732")
269 False
270 """
271 if _NUMERIC_RE.match(pattern):
272 return candidate.split("-")[-1] == pattern
273 return candidate == pattern
276VALID_ISSUE_TYPES = {"BUG", "FEAT", "ENH", "EPIC"}
278VALID_PRIORITIES: frozenset[str] = frozenset({"P0", "P1", "P2", "P3", "P4", "P5"})
281def parse_priorities(value: str | None) -> set[str] | None:
282 """Parse comma-separated priority levels into a validated set.
284 Args:
285 value: Comma-separated string like "P1,P2" or None
287 Returns:
288 Set of uppercase priority strings, or None if value is None
290 Raises:
291 SystemExit: If invalid priority levels are provided (exit code 2)
293 Example:
294 >>> parse_priorities("p1,P2")
295 {'P1', 'P2'}
296 >>> parse_priorities(None)
297 None
298 """
299 if value is None:
300 return None
301 priorities = {p.strip().upper() for p in value.split(",")}
302 invalid = priorities - VALID_PRIORITIES
303 if invalid:
304 import sys
306 print(
307 f"error: invalid priority level(s): {', '.join(sorted(invalid))}. "
308 f"Valid priorities: {', '.join(sorted(VALID_PRIORITIES))}",
309 file=sys.stderr,
310 )
311 sys.exit(2)
312 return priorities
315def add_priority_arg(parser: argparse.ArgumentParser) -> None:
316 """Add --priority/-p argument for filtering issues by priority level."""
317 parser.add_argument(
318 "--priority",
319 "-p",
320 type=str,
321 default=None,
322 help="Comma-separated priority levels to process (e.g., P0, P1,P2)",
323 )
326def add_label_arg(parser: argparse.ArgumentParser) -> None:
327 """Add --label argument for filtering issues by label."""
328 parser.add_argument(
329 "--label",
330 type=str,
331 default=None,
332 help="Comma-separated labels to process (e.g., fsm,cli,quick-win)",
333 )
336def parse_labels(value: str | None) -> set[str] | None:
337 """Parse comma-separated labels into a set.
339 Args:
340 value: Comma-separated string like "fsm,cli" or None
342 Returns:
343 Set of lowercase label strings, or None if value is None
344 """
345 if value is None:
346 return None
347 return {lb.strip().lower() for lb in value.split(",") if lb.strip()}
350def add_type_arg(parser: argparse.ArgumentParser) -> None:
351 """Add --type/-T argument for filtering issues by type prefix."""
352 parser.add_argument(
353 "--type",
354 "-T",
355 type=str,
356 default=None,
357 help="Comma-separated issue types to process (e.g., BUG, FEAT, ENH, EPIC)",
358 )
361def parse_issue_types(value: str | None) -> set[str] | None:
362 """Parse comma-separated issue types into a validated set.
364 Args:
365 value: Comma-separated string like "BUG,ENH" or None
367 Returns:
368 Set of uppercase type prefixes, or None if value is None
370 Raises:
371 SystemExit: If invalid issue types are provided (via argparse error)
373 Example:
374 >>> parse_issue_types("bug,enh")
375 {'BUG', 'ENH'}
376 >>> parse_issue_types(None)
377 None
378 """
379 if value is None:
380 return None
381 types = {t.strip().upper() for t in value.split(",")}
382 invalid = types - VALID_ISSUE_TYPES
383 if invalid:
384 import sys
386 print(
387 f"error: invalid issue type(s): {', '.join(sorted(invalid))}. "
388 f"Valid types: {', '.join(sorted(VALID_ISSUE_TYPES))}",
389 file=sys.stderr,
390 )
391 sys.exit(2)
392 return types
395def add_common_auto_args(parser: argparse.ArgumentParser) -> None:
396 """Add arguments common to ll-auto command.
398 Adds: --resume, --dry-run, --max-issues, --quiet, --only, --skip, --type, --priority,
399 --label, --config, --idle-timeout, --handoff-threshold, --context-limit
400 """
401 add_resume_arg(parser)
402 add_dry_run_arg(parser)
403 add_max_issues_arg(parser)
404 add_quiet_arg(parser)
405 add_only_arg(parser)
406 add_skip_arg(parser)
407 add_type_arg(parser)
408 add_priority_arg(parser)
409 add_label_arg(parser)
410 add_config_arg(parser)
411 add_idle_timeout_arg(parser)
412 add_handoff_threshold_arg(parser)
413 add_context_limit_arg(parser)
416def add_common_parallel_args(parser: argparse.ArgumentParser) -> None:
417 """Add arguments common to parallel execution tools.
419 Adds: --dry-run, --resume, --max-workers, --timeout, --idle-timeout, --quiet, --only, --skip, --type, --label,
420 --config, --context-limit
421 """
422 add_dry_run_arg(parser)
423 add_resume_arg(parser)
424 add_max_workers_arg(parser)
425 add_timeout_arg(parser)
426 add_idle_timeout_arg(parser)
427 add_quiet_arg(parser)
428 add_only_arg(parser)
429 add_skip_arg(parser)
430 add_type_arg(parser)
431 add_label_arg(parser)
432 add_config_arg(parser)
433 add_context_limit_arg(parser)
436__all__ = [
437 "add_dry_run_arg",
438 "add_resume_arg",
439 "add_config_arg",
440 "add_only_arg",
441 "add_skip_arg",
442 "add_type_arg",
443 "add_priority_arg",
444 "add_json_arg",
445 "add_label_arg",
446 "add_max_workers_arg",
447 "add_timeout_arg",
448 "add_idle_timeout_arg",
449 "add_handoff_threshold_arg",
450 "add_context_limit_arg",
451 "add_quiet_arg",
452 "add_skip_analysis_arg",
453 "add_max_issues_arg",
454 "parse_issue_ids",
455 "parse_issue_types",
456 "parse_priorities",
457 "parse_labels",
458 "VALID_ISSUE_TYPES",
459 "VALID_PRIORITIES",
460 "add_common_auto_args",
461 "add_common_parallel_args",
462]