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
« 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.
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_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 )
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 )
234def add_json_arg(parser: argparse.ArgumentParser, help_text: str = "Output as JSON") -> None:
235 """Add --json/-j argument for machine-readable output.
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)
244def parse_issue_ids(value: str | None) -> set[str] | None:
245 """Parse comma-separated issue IDs into a set.
247 Args:
248 value: Comma-separated string like "BUG-001,FEAT-002" or None
250 Returns:
251 Set of uppercase issue IDs, or None if value is None
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(",")}
264def parse_issue_ids_ordered(value: str | None) -> list[str] | None:
265 """Parse comma-separated issue IDs into an ordered list.
267 Like parse_issue_ids but preserves input order, enabling callers to
268 honor the sequence in which IDs were specified.
270 Args:
271 value: Comma-separated string like "BUG-001,FEAT-002" or None
273 Returns:
274 List of uppercase issue IDs in input order, or None if value is None
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(",")]
287_NUMERIC_RE = re.compile(r"^\d+$")
290def _id_matches(candidate: str, pattern: str) -> bool:
291 """Return True if candidate matches pattern, supporting numeric-only patterns.
293 Args:
294 candidate: Full issue ID like 'ENH-732'
295 pattern: Full ID like 'ENH-732' or numeric suffix like '732'
297 Returns:
298 True if candidate matches the pattern
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
313VALID_ISSUE_TYPES = {"BUG", "FEAT", "ENH", "EPIC"}
315VALID_PRIORITIES: frozenset[str] = frozenset({"P0", "P1", "P2", "P3", "P4", "P5"})
318def parse_priorities(value: str | None) -> set[str] | None:
319 """Parse comma-separated priority levels into a validated set.
321 Args:
322 value: Comma-separated string like "P1,P2" or None
324 Returns:
325 Set of uppercase priority strings, or None if value is None
327 Raises:
328 SystemExit: If invalid priority levels are provided (exit code 2)
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
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
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 )
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 )
373def parse_labels(value: str | None) -> set[str] | None:
374 """Parse comma-separated labels into a set.
376 Args:
377 value: Comma-separated string like "fsm,cli" or None
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()}
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 )
398def parse_issue_types(value: str | None) -> set[str] | None:
399 """Parse comma-separated issue types into a validated set.
401 Args:
402 value: Comma-separated string like "BUG,ENH" or None
404 Returns:
405 Set of uppercase type prefixes, or None if value is None
407 Raises:
408 SystemExit: If invalid issue types are provided (via argparse error)
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
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
432def add_common_auto_args(parser: argparse.ArgumentParser) -> None:
433 """Add arguments common to ll-auto command.
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)
453def add_common_parallel_args(parser: argparse.ArgumentParser) -> None:
454 """Add arguments common to parallel execution tools.
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)
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]