Coverage for little_loops / cli_args.py: 30%
115 statements
« prev ^ index » next coverage.py v7.12.0, created at 2026-06-15 17:27 -0500
« prev ^ index » next coverage.py v7.12.0, created at 2026-06-15 17:27 -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_intent_arg(parser: argparse.ArgumentParser) -> None:
154 """Add --intent <query> flag for output filtering.
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 )
167def add_intent_limit_arg(parser: argparse.ArgumentParser) -> None:
168 """Add --intent-limit <N> flag for capping intent-filtered output lines.
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 )
181def add_context_limit_arg(parser: argparse.ArgumentParser) -> None:
182 """Add --context-limit argument for per-run context window size override.
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 )
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 )
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 )
214def add_max_issues_arg(parser: argparse.ArgumentParser) -> None:
215 """Add --max-issues/-m argument for limiting issues processed."""
216 parser.add_argument(
217 "--max-issues",
218 "-m",
219 type=int,
220 default=0,
221 help="Limit number of issues to process (0 = unlimited)",
222 )
225def add_json_arg(parser: argparse.ArgumentParser, help_text: str = "Output as JSON") -> None:
226 """Add --json/-j argument for machine-readable output.
228 Args:
229 parser: The argument parser to add the argument to
230 help_text: Optional custom help text. If not provided, uses default.
231 """
232 parser.add_argument("-j", "--json", action="store_true", help=help_text)
235def parse_issue_ids(value: str | None) -> set[str] | None:
236 """Parse comma-separated issue IDs into a set.
238 Args:
239 value: Comma-separated string like "BUG-001,FEAT-002" or None
241 Returns:
242 Set of uppercase issue IDs, or None if value is None
244 Example:
245 >>> parse_issue_ids("BUG-001,feat-002")
246 {'BUG-001', 'FEAT-002'}
247 >>> parse_issue_ids(None)
248 None
249 """
250 if value is None:
251 return None
252 return {i.strip().upper() for i in value.split(",")}
255def parse_issue_ids_ordered(value: str | None) -> list[str] | None:
256 """Parse comma-separated issue IDs into an ordered list.
258 Like parse_issue_ids but preserves input order, enabling callers to
259 honor the sequence in which IDs were specified.
261 Args:
262 value: Comma-separated string like "BUG-001,FEAT-002" or None
264 Returns:
265 List of uppercase issue IDs in input order, or None if value is None
267 Example:
268 >>> parse_issue_ids_ordered("BUG-010,FEAT-005,ENH-020")
269 ['BUG-010', 'FEAT-005', 'ENH-020']
270 >>> parse_issue_ids_ordered(None)
271 None
272 """
273 if value is None:
274 return None
275 return [i.strip().upper() for i in value.split(",")]
278_NUMERIC_RE = re.compile(r"^\d+$")
281def _id_matches(candidate: str, pattern: str) -> bool:
282 """Return True if candidate matches pattern, supporting numeric-only patterns.
284 Args:
285 candidate: Full issue ID like 'ENH-732'
286 pattern: Full ID like 'ENH-732' or numeric suffix like '732'
288 Returns:
289 True if candidate matches the pattern
291 Example:
292 >>> _id_matches("ENH-732", "732")
293 True
294 >>> _id_matches("ENH-732", "ENH-732")
295 True
296 >>> _id_matches("ENH-732", "BUG-732")
297 False
298 """
299 if _NUMERIC_RE.match(pattern):
300 return candidate.split("-")[-1] == pattern
301 return candidate == pattern
304VALID_ISSUE_TYPES = {"BUG", "FEAT", "ENH", "EPIC"}
306VALID_PRIORITIES: frozenset[str] = frozenset({"P0", "P1", "P2", "P3", "P4", "P5"})
309def parse_priorities(value: str | None) -> set[str] | None:
310 """Parse comma-separated priority levels into a validated set.
312 Args:
313 value: Comma-separated string like "P1,P2" or None
315 Returns:
316 Set of uppercase priority strings, or None if value is None
318 Raises:
319 SystemExit: If invalid priority levels are provided (exit code 2)
321 Example:
322 >>> parse_priorities("p1,P2")
323 {'P1', 'P2'}
324 >>> parse_priorities(None)
325 None
326 """
327 if value is None:
328 return None
329 priorities = {p.strip().upper() for p in value.split(",")}
330 invalid = priorities - VALID_PRIORITIES
331 if invalid:
332 import sys
334 print(
335 f"error: invalid priority level(s): {', '.join(sorted(invalid))}. "
336 f"Valid priorities: {', '.join(sorted(VALID_PRIORITIES))}",
337 file=sys.stderr,
338 )
339 sys.exit(2)
340 return priorities
343def add_priority_arg(parser: argparse.ArgumentParser) -> None:
344 """Add --priority/-p argument for filtering issues by priority level."""
345 parser.add_argument(
346 "--priority",
347 "-p",
348 type=str,
349 default=None,
350 help="Comma-separated priority levels to process (e.g., P0, P1,P2)",
351 )
354def add_label_arg(parser: argparse.ArgumentParser) -> None:
355 """Add --label argument for filtering issues by label."""
356 parser.add_argument(
357 "--label",
358 type=str,
359 default=None,
360 help="Comma-separated labels to process (e.g., fsm,cli,quick-win)",
361 )
364def parse_labels(value: str | None) -> set[str] | None:
365 """Parse comma-separated labels into a set.
367 Args:
368 value: Comma-separated string like "fsm,cli" or None
370 Returns:
371 Set of lowercase label strings, or None if value is None
372 """
373 if value is None:
374 return None
375 return {lb.strip().lower() for lb in value.split(",") if lb.strip()}
378def add_type_arg(parser: argparse.ArgumentParser) -> None:
379 """Add --type/-T argument for filtering issues by type prefix."""
380 parser.add_argument(
381 "--type",
382 "-T",
383 type=str,
384 default=None,
385 help="Comma-separated issue types to process (e.g., BUG, FEAT, ENH, EPIC)",
386 )
389def parse_issue_types(value: str | None) -> set[str] | None:
390 """Parse comma-separated issue types into a validated set.
392 Args:
393 value: Comma-separated string like "BUG,ENH" or None
395 Returns:
396 Set of uppercase type prefixes, or None if value is None
398 Raises:
399 SystemExit: If invalid issue types are provided (via argparse error)
401 Example:
402 >>> parse_issue_types("bug,enh")
403 {'BUG', 'ENH'}
404 >>> parse_issue_types(None)
405 None
406 """
407 if value is None:
408 return None
409 types = {t.strip().upper() for t in value.split(",")}
410 invalid = types - VALID_ISSUE_TYPES
411 if invalid:
412 import sys
414 print(
415 f"error: invalid issue type(s): {', '.join(sorted(invalid))}. "
416 f"Valid types: {', '.join(sorted(VALID_ISSUE_TYPES))}",
417 file=sys.stderr,
418 )
419 sys.exit(2)
420 return types
423def add_common_auto_args(parser: argparse.ArgumentParser) -> None:
424 """Add arguments common to ll-auto command.
426 Adds: --resume, --dry-run, --max-issues, --quiet, --only, --skip, --type, --priority,
427 --label, --config, --idle-timeout, --handoff-threshold, --context-limit
428 """
429 add_resume_arg(parser)
430 add_dry_run_arg(parser)
431 add_max_issues_arg(parser)
432 add_quiet_arg(parser)
433 add_only_arg(parser)
434 add_skip_arg(parser)
435 add_type_arg(parser)
436 add_priority_arg(parser)
437 add_label_arg(parser)
438 add_config_arg(parser)
439 add_idle_timeout_arg(parser)
440 add_handoff_threshold_arg(parser)
441 add_context_limit_arg(parser)
444def add_common_parallel_args(parser: argparse.ArgumentParser) -> None:
445 """Add arguments common to parallel execution tools.
447 Adds: --dry-run, --resume, --max-workers, --timeout, --idle-timeout, --quiet, --only, --skip, --type, --label,
448 --config, --context-limit
449 """
450 add_dry_run_arg(parser)
451 add_resume_arg(parser)
452 add_max_workers_arg(parser)
453 add_timeout_arg(parser)
454 add_idle_timeout_arg(parser)
455 add_quiet_arg(parser)
456 add_only_arg(parser)
457 add_skip_arg(parser)
458 add_type_arg(parser)
459 add_label_arg(parser)
460 add_config_arg(parser)
461 add_context_limit_arg(parser)
464__all__ = [
465 "add_dry_run_arg",
466 "add_resume_arg",
467 "add_config_arg",
468 "add_only_arg",
469 "add_skip_arg",
470 "add_type_arg",
471 "add_priority_arg",
472 "add_json_arg",
473 "add_label_arg",
474 "add_max_workers_arg",
475 "add_timeout_arg",
476 "add_idle_timeout_arg",
477 "add_handoff_threshold_arg",
478 "add_context_limit_arg",
479 "add_intent_arg",
480 "add_intent_limit_arg",
481 "add_quiet_arg",
482 "add_skip_analysis_arg",
483 "add_max_issues_arg",
484 "parse_issue_ids",
485 "parse_issue_types",
486 "parse_priorities",
487 "parse_labels",
488 "VALID_ISSUE_TYPES",
489 "VALID_PRIORITIES",
490 "add_common_auto_args",
491 "add_common_parallel_args",
492]