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

2

3 Provides reusable functions for adding common command-line arguments

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

5 and ll-sprint commands.

6"""

7

8 from __future__ import annotations

9

10 import argparse

11 import re

12 from pathlib import Path

13

14

15 def 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

25 def 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

35 def 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

46 def 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

57 def 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

75 def 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

100 def 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

125 def 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

139 def 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

153 def 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

167 def 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

181 def 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

195 def 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

205 def 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

214 def 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

223 def 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

234 def 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

244 def 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

264 def 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

290 def _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

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

314

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

316

317

318 def 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

352 def 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

363 def 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

373 def 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

387 def 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

398 def 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

432 def 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

453 def 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]