continuous-refactoring 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,687 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import sys
5
+ from collections.abc import Callable
6
+ from pathlib import Path
7
+
8
+ __all__ = [
9
+ "build_parser",
10
+ "cli_main",
11
+ "parse_max_attempts",
12
+ "parse_sleep_seconds",
13
+ ]
14
+
15
+ from continuous_refactoring.agent import run_agent_interactive_until_settled
16
+ from continuous_refactoring.artifacts import ContinuousRefactorError
17
+ from continuous_refactoring.effort import (
18
+ DEFAULT_EFFORT,
19
+ DEFAULT_MAX_ALLOWED_EFFORT,
20
+ EFFORT_TIERS,
21
+ parse_effort_arg,
22
+ resolve_effort_budget,
23
+ )
24
+ from continuous_refactoring.loop import (
25
+ run_loop,
26
+ run_migrations_focused_loop,
27
+ run_once,
28
+ )
29
+ from continuous_refactoring.review_cli import handle_review
30
+
31
+ _TASTE_WARNING = "warning: taste out of date — run `continuous-refactoring taste --upgrade`"
32
+ _GLOBAL_TASTE_WARNING = (
33
+ "warning: global taste is out of date — "
34
+ "run 'continuous-refactoring taste --upgrade' to update."
35
+ )
36
+
37
+
38
+ def parse_max_attempts(value: str) -> int:
39
+ try:
40
+ attempts = int(value)
41
+ except ValueError as error:
42
+ raise argparse.ArgumentTypeError(str(error)) from error
43
+ if attempts < 0:
44
+ raise argparse.ArgumentTypeError("--max-attempts must be >= 0")
45
+ return attempts
46
+
47
+
48
+ def parse_sleep_seconds(value: str) -> float:
49
+ try:
50
+ seconds = float(value)
51
+ except ValueError as error:
52
+ raise argparse.ArgumentTypeError(str(error)) from error
53
+ if seconds < 0:
54
+ raise argparse.ArgumentTypeError("--sleep must be >= 0")
55
+ return seconds
56
+
57
+
58
+ def _add_common_args(parser: argparse.ArgumentParser) -> None:
59
+ parser.add_argument(
60
+ "--with",
61
+ dest="agent",
62
+ choices=("codex", "claude"),
63
+ required=True,
64
+ help="Which agent backend to use.",
65
+ )
66
+ parser.add_argument("--model", required=True, help="Model name.")
67
+ parser.add_argument(
68
+ "--default-effort",
69
+ default=DEFAULT_EFFORT,
70
+ type=parse_effort_arg,
71
+ metavar="{" + ",".join(EFFORT_TIERS) + "}",
72
+ help=f"Default effort level. Defaults to {DEFAULT_EFFORT}.",
73
+ )
74
+ parser.add_argument(
75
+ "--max-allowed-effort",
76
+ type=parse_effort_arg,
77
+ default=DEFAULT_MAX_ALLOWED_EFFORT,
78
+ metavar="{" + ",".join(EFFORT_TIERS) + "}",
79
+ help=f"Highest effort this run may use. Defaults to {DEFAULT_MAX_ALLOWED_EFFORT}.",
80
+ )
81
+ parser.add_argument(
82
+ "--validation-command",
83
+ default="uv run pytest",
84
+ help="Command to validate the repo.",
85
+ )
86
+ parser.add_argument("--extensions", default=None, help="File extensions, e.g. .py,.ts")
87
+ parser.add_argument("--globs", default=None, help="Colon-separated glob patterns.")
88
+ parser.add_argument("--targets", type=Path, default=None, help="JSONL targets file.")
89
+ parser.add_argument("--paths", default=None, help="Colon-separated literal paths.")
90
+ parser.add_argument("--scope-instruction", default=None, help="Natural language scope.")
91
+ parser.add_argument("--timeout", type=int, default=None, help="Timeout per agent call (seconds).")
92
+ parser.add_argument("--refactoring-prompt", type=Path, default=None, help="Override default refactoring prompt.")
93
+ parser.add_argument("--fix-prompt", type=Path, default=None, help="Override default fix prompt.")
94
+ parser.add_argument("--show-agent-logs", action="store_true", help="Mirror agent output to terminal.")
95
+ parser.add_argument("--show-command-logs", action="store_true", help="Mirror validation output to terminal.")
96
+ parser.add_argument(
97
+ "--repo-root",
98
+ type=Path,
99
+ default=Path.cwd(),
100
+ help="Repository root.",
101
+ )
102
+
103
+
104
+ def _add_init_parser(subparsers: argparse._SubParsersAction) -> None:
105
+ init_parser = subparsers.add_parser(
106
+ "init",
107
+ help="Register a project for continuous refactoring.",
108
+ )
109
+ init_parser.set_defaults(handler=_handle_init)
110
+ init_parser.add_argument(
111
+ "--path",
112
+ type=Path,
113
+ default=None,
114
+ help="Project path (default: current directory).",
115
+ )
116
+ init_parser.add_argument(
117
+ "--live-migrations-dir",
118
+ type=Path,
119
+ default=None,
120
+ help="Directory for live migration artifacts (repo-relative path).",
121
+ )
122
+
123
+
124
+ def _add_taste_parser(subparsers: argparse._SubParsersAction) -> None:
125
+ taste_parser = subparsers.add_parser(
126
+ "taste",
127
+ help="Manage refactoring taste files.",
128
+ )
129
+ taste_parser.set_defaults(handler=_handle_taste)
130
+ taste_parser.add_argument(
131
+ "--global",
132
+ dest="global_",
133
+ action="store_true",
134
+ help="Use global taste file instead of project-level.",
135
+ )
136
+ taste_mode = taste_parser.add_mutually_exclusive_group()
137
+ taste_mode.add_argument(
138
+ "--interview",
139
+ action="store_true",
140
+ help="Interview the user with an agent and write answers to the taste file.",
141
+ )
142
+ taste_mode.add_argument(
143
+ "--upgrade",
144
+ action="store_true",
145
+ help="Upgrade taste to current version (interview about new dimensions only).",
146
+ )
147
+ taste_mode.add_argument(
148
+ "--refine",
149
+ action="store_true",
150
+ help="Open an editing session to refine the taste file in place.",
151
+ )
152
+ taste_parser.add_argument(
153
+ "--with",
154
+ dest="agent",
155
+ choices=("codex", "claude"),
156
+ default=None,
157
+ help="Agent backend for --interview, --upgrade, or --refine.",
158
+ )
159
+ taste_parser.add_argument(
160
+ "--model",
161
+ default=None,
162
+ help="Model name for --interview, --upgrade, or --refine.",
163
+ )
164
+ taste_parser.add_argument(
165
+ "--effort",
166
+ default=None,
167
+ help="Effort level for --interview, --upgrade, or --refine.",
168
+ )
169
+ taste_parser.add_argument(
170
+ "--force",
171
+ action="store_true",
172
+ help="Allow --interview to overwrite a taste file with custom content (backup at .bak).",
173
+ )
174
+
175
+
176
+ def _add_run_once_parser(subparsers: argparse._SubParsersAction) -> None:
177
+ run_once_parser = subparsers.add_parser(
178
+ "run-once",
179
+ help="Single refactoring attempt (one agent call, no fix retry).",
180
+ )
181
+ run_once_parser.set_defaults(handler=_handle_run_once)
182
+ _add_common_args(run_once_parser)
183
+
184
+
185
+ def _add_run_parser(subparsers: argparse._SubParsersAction) -> None:
186
+ run_parser = subparsers.add_parser(
187
+ "run",
188
+ help="Continuous refactoring loop with fix-prompt retry.",
189
+ )
190
+ run_parser.set_defaults(handler=_handle_run)
191
+ _add_common_args(run_parser)
192
+ run_parser.add_argument(
193
+ "--max-attempts",
194
+ type=parse_max_attempts,
195
+ default=None,
196
+ help="Total attempts per target (1=no retry, 0=unlimited).",
197
+ )
198
+ run_parser.add_argument(
199
+ "--max-refactors",
200
+ type=int,
201
+ default=None,
202
+ help="Distinct targets to process.",
203
+ )
204
+ run_parser.add_argument(
205
+ "--focus-on-live-migrations",
206
+ action="store_true",
207
+ help=(
208
+ "Iterate only on live migrations until every one is done or blocked. "
209
+ "Bypasses targeting and --max-refactors requirements."
210
+ ),
211
+ )
212
+ run_parser.add_argument(
213
+ "--commit-message-prefix",
214
+ default="continuous refactor",
215
+ help="Prefix for commit messages.",
216
+ )
217
+ run_parser.add_argument(
218
+ "--max-consecutive-failures",
219
+ type=int,
220
+ default=3,
221
+ help="Stop after N consecutive failures.",
222
+ )
223
+ run_parser.add_argument(
224
+ "--sleep",
225
+ type=parse_sleep_seconds,
226
+ default=0.0,
227
+ help="Seconds to sleep between completed targets.",
228
+ )
229
+
230
+
231
+ def _add_review_parser(subparsers: argparse._SubParsersAction) -> None:
232
+ review_parser = subparsers.add_parser(
233
+ "review",
234
+ help="Review migrations awaiting human review.",
235
+ )
236
+ review_parser.set_defaults(handler=handle_review)
237
+ review_sub = review_parser.add_subparsers(dest="review_command")
238
+ review_sub.add_parser("list", help="List migrations flagged for review.")
239
+ perform_parser = review_sub.add_parser(
240
+ "perform",
241
+ help="Perform review on a flagged migration.",
242
+ )
243
+ perform_parser.add_argument("migration", help="Migration name to review.")
244
+ perform_parser.add_argument(
245
+ "--with", dest="agent", choices=("codex", "claude"), required=True,
246
+ help="Agent backend.",
247
+ )
248
+ perform_parser.add_argument("--model", required=True, help="Model name.")
249
+ perform_parser.add_argument("--effort", required=True, help="Effort level.")
250
+
251
+
252
+ def build_parser() -> argparse.ArgumentParser:
253
+ parser = argparse.ArgumentParser(
254
+ description="Continuous refactoring CLI for AI coding agents.",
255
+ )
256
+ subparsers = parser.add_subparsers(dest="command")
257
+
258
+ _add_init_parser(subparsers)
259
+ _add_taste_parser(subparsers)
260
+ _add_run_once_parser(subparsers)
261
+ _add_run_parser(subparsers)
262
+ upgrade_parser = subparsers.add_parser(
263
+ "upgrade",
264
+ help="Verify and upgrade global configuration.",
265
+ )
266
+ upgrade_parser.set_defaults(handler=_handle_upgrade)
267
+ _add_review_parser(subparsers)
268
+
269
+ return parser
270
+
271
+
272
+ def _handle_init(args: argparse.Namespace) -> None:
273
+ from continuous_refactoring.config import (
274
+ ensure_taste_file,
275
+ register_project,
276
+ set_live_migrations_dir,
277
+ )
278
+
279
+ path = (args.path or Path.cwd()).resolve()
280
+ try:
281
+ project = register_project(path)
282
+ taste_path = project.project_dir / "taste.md"
283
+ ensure_taste_file(taste_path)
284
+
285
+ live_dir_arg: Path | None = getattr(args, "live_migrations_dir", None)
286
+ if live_dir_arg is not None:
287
+ resolved_live = (path / live_dir_arg).resolve()
288
+ if not resolved_live.is_relative_to(path):
289
+ print(
290
+ f"Error: --live-migrations-dir must be inside the repo: {live_dir_arg}",
291
+ file=sys.stderr,
292
+ )
293
+ raise SystemExit(2)
294
+ relative = str(resolved_live.relative_to(path))
295
+ resolved_live.mkdir(parents=True, exist_ok=True)
296
+ set_live_migrations_dir(project.entry.uuid, relative)
297
+ except ContinuousRefactorError as error:
298
+ print(f"Error: {error}", file=sys.stderr)
299
+ raise SystemExit(1) from error
300
+
301
+ print(f"Project registered: {project.entry.uuid}")
302
+ print(f"Data directory: {project.project_dir}")
303
+ print(f"Taste file: {taste_path}")
304
+ if live_dir_arg is not None:
305
+ print(f"Live migrations dir: {resolved_live}")
306
+
307
+
308
+ def _resolve_taste_path(global_: bool) -> Path:
309
+ from continuous_refactoring.config import global_dir, resolve_project
310
+
311
+ if global_:
312
+ path = global_dir() / "taste.md"
313
+ path.parent.mkdir(parents=True, exist_ok=True)
314
+ return path
315
+
316
+ try:
317
+ project = resolve_project(Path.cwd().resolve())
318
+ except ContinuousRefactorError as error:
319
+ if not str(error).startswith("Project not registered:"):
320
+ raise
321
+ print(
322
+ "Error: project not initialized. Run 'continuous-refactoring init' first.",
323
+ file=sys.stderr,
324
+ )
325
+ raise SystemExit(1) from error
326
+ return project.project_dir / "taste.md"
327
+
328
+
329
+ def _taste_settle_path(path: Path) -> Path:
330
+ return path.with_name(path.name + ".done")
331
+
332
+
333
+ def _active_taste_mode(args: argparse.Namespace) -> str | None:
334
+ for mode in _TASTE_MODE_HANDLERS:
335
+ if getattr(args, mode, False):
336
+ return mode
337
+ return None
338
+
339
+
340
+ def _taste_agent_flags_set(args: argparse.Namespace) -> bool:
341
+ return any(
342
+ getattr(args, name, None) is not None for name in ("agent", "model", "effort")
343
+ )
344
+
345
+
346
+ def _require_taste_action_flags(
347
+ *,
348
+ action: str,
349
+ agent: str | None,
350
+ model: str | None,
351
+ effort: str | None,
352
+ ) -> None:
353
+ missing = [
354
+ flag
355
+ for flag, value in (
356
+ ("--with", agent),
357
+ ("--model", model),
358
+ ("--effort", effort),
359
+ )
360
+ if not value
361
+ ]
362
+ if missing:
363
+ print(
364
+ f"Error: --{action} requires " + ", ".join(missing) + ".",
365
+ file=sys.stderr,
366
+ )
367
+ raise SystemExit(2)
368
+
369
+
370
+ def _run_taste_agent(
371
+ *,
372
+ action: str,
373
+ args: argparse.Namespace,
374
+ prompt: str,
375
+ path: Path,
376
+ ) -> None:
377
+ settle_path = _taste_settle_path(path)
378
+ try:
379
+ returncode = run_agent_interactive_until_settled(
380
+ args.agent,
381
+ args.model,
382
+ args.effort,
383
+ prompt,
384
+ Path.cwd().resolve(),
385
+ content_path=path,
386
+ settle_path=settle_path,
387
+ )
388
+ except ContinuousRefactorError as error:
389
+ print(f"Error: {action} agent did not settle: {error}.", file=sys.stderr)
390
+ raise SystemExit(1) from error
391
+ if returncode != 0:
392
+ print(
393
+ f"Error: {action} agent exited with code {returncode}.",
394
+ file=sys.stderr,
395
+ )
396
+ raise SystemExit(returncode)
397
+
398
+ if not path.exists() or path.stat().st_size == 0:
399
+ print(
400
+ f"Error: {action} agent did not write to {path}.",
401
+ file=sys.stderr,
402
+ )
403
+ raise SystemExit(1)
404
+ if settle_path.exists():
405
+ settle_path.unlink()
406
+
407
+
408
+ def _handle_plain_taste(args: argparse.Namespace) -> None:
409
+ from continuous_refactoring.config import ensure_taste_file
410
+
411
+ try:
412
+ path = _resolve_taste_path(args.global_)
413
+ ensure_taste_file(path)
414
+ except ContinuousRefactorError as error:
415
+ print(f"Error: {error}", file=sys.stderr)
416
+ raise SystemExit(1) from error
417
+ print(str(path))
418
+
419
+
420
+ def _handle_current_taste_upgrade_if_noop(args: argparse.Namespace) -> bool:
421
+ from continuous_refactoring.config import (
422
+ TASTE_CURRENT_VERSION,
423
+ parse_taste_version,
424
+ )
425
+
426
+ try:
427
+ path = _resolve_taste_path(args.global_)
428
+ stored_version = None
429
+ if path.exists():
430
+ stored_version = parse_taste_version(path.read_text(encoding="utf-8"))
431
+ except (ContinuousRefactorError, OSError) as error:
432
+ print(f"Error: {error}", file=sys.stderr)
433
+ raise SystemExit(1) from error
434
+ if stored_version != TASTE_CURRENT_VERSION:
435
+ return False
436
+ print("taste already current; use `taste --refine` to re-interview or refine it.")
437
+ return True
438
+
439
+
440
+ def _handle_taste(args: argparse.Namespace) -> None:
441
+ mode = _active_taste_mode(args)
442
+
443
+ if getattr(args, "force", False) and mode != "interview":
444
+ print("Error: --force requires --interview.", file=sys.stderr)
445
+ raise SystemExit(2)
446
+
447
+ if mode is None:
448
+ if _taste_agent_flags_set(args):
449
+ print(
450
+ "Error: --with/--model/--effort require --interview, --upgrade, or --refine.",
451
+ file=sys.stderr,
452
+ )
453
+ raise SystemExit(2)
454
+ return _handle_plain_taste(args)
455
+
456
+ if mode == "upgrade" and _handle_current_taste_upgrade_if_noop(args):
457
+ return
458
+ _require_taste_action_flags(
459
+ action=mode,
460
+ agent=getattr(args, "agent", None),
461
+ model=getattr(args, "model", None),
462
+ effort=getattr(args, "effort", None),
463
+ )
464
+ return _TASTE_MODE_HANDLERS[mode](args)
465
+
466
+
467
+ def _handle_taste_interview(args: argparse.Namespace) -> None:
468
+ from continuous_refactoring.config import default_taste_text
469
+ from continuous_refactoring.prompts import compose_interview_prompt
470
+
471
+ try:
472
+ path = _resolve_taste_path(args.global_)
473
+ default_text = default_taste_text()
474
+ existing: str | None = None
475
+ file_exists = path.exists()
476
+ if file_exists:
477
+ current = path.read_text(encoding="utf-8")
478
+ if current != default_text:
479
+ if not args.force:
480
+ print(
481
+ "Error: taste file already has custom content; "
482
+ "pass --force to overwrite (backup at taste.md.bak).",
483
+ file=sys.stderr,
484
+ )
485
+ raise SystemExit(1)
486
+ backup = path.with_name(path.name + ".bak")
487
+ backup.write_text(current, encoding="utf-8")
488
+ existing = current
489
+
490
+ path.parent.mkdir(parents=True, exist_ok=True)
491
+ prompt = compose_interview_prompt(path, _taste_settle_path(path), existing)
492
+ _run_taste_agent(
493
+ action="interview",
494
+ args=args,
495
+ prompt=prompt,
496
+ path=path,
497
+ )
498
+ except ContinuousRefactorError as error:
499
+ print(f"Error: {error}", file=sys.stderr)
500
+ raise SystemExit(1) from error
501
+ print(str(path))
502
+
503
+
504
+ def _handle_taste_refine(args: argparse.Namespace) -> None:
505
+ from continuous_refactoring.config import default_taste_text
506
+ from continuous_refactoring.prompts import compose_taste_refine_prompt
507
+
508
+ try:
509
+ path = _resolve_taste_path(args.global_)
510
+ starting_taste = (
511
+ path.read_text(encoding="utf-8") if path.exists() else default_taste_text()
512
+ )
513
+
514
+ path.parent.mkdir(parents=True, exist_ok=True)
515
+ prompt = compose_taste_refine_prompt(
516
+ path,
517
+ _taste_settle_path(path),
518
+ starting_taste,
519
+ )
520
+ _run_taste_agent(
521
+ action="refine",
522
+ args=args,
523
+ prompt=prompt,
524
+ path=path,
525
+ )
526
+ except ContinuousRefactorError as error:
527
+ print(f"Error: {error}", file=sys.stderr)
528
+ raise SystemExit(1) from error
529
+ print(str(path))
530
+
531
+
532
+ def _handle_taste_upgrade(args: argparse.Namespace) -> None:
533
+ from continuous_refactoring.config import (
534
+ TASTE_CURRENT_VERSION,
535
+ parse_taste_version,
536
+ )
537
+ from continuous_refactoring.prompts import compose_taste_upgrade_prompt
538
+
539
+ try:
540
+ path = _resolve_taste_path(args.global_)
541
+
542
+ existing: str | None = None
543
+ stored_version: int | None = None
544
+ if path.exists():
545
+ existing = path.read_text(encoding="utf-8")
546
+ stored_version = parse_taste_version(existing)
547
+
548
+ prompt = compose_taste_upgrade_prompt(
549
+ path,
550
+ _taste_settle_path(path),
551
+ existing,
552
+ stored_version,
553
+ TASTE_CURRENT_VERSION,
554
+ )
555
+ _run_taste_agent(
556
+ action="upgrade",
557
+ args=args,
558
+ prompt=prompt,
559
+ path=path,
560
+ )
561
+ except ContinuousRefactorError as error:
562
+ print(f"Error: {error}", file=sys.stderr)
563
+ raise SystemExit(1) from error
564
+ print(str(path))
565
+
566
+
567
+ def _handle_upgrade(args: argparse.Namespace) -> None:
568
+ from continuous_refactoring.config import (
569
+ config_is_current,
570
+ global_dir,
571
+ load_manifest,
572
+ save_manifest,
573
+ taste_is_stale,
574
+ )
575
+
576
+ try:
577
+ if not config_is_current():
578
+ print(
579
+ "Error: config version is absent or out of date.",
580
+ file=sys.stderr,
581
+ )
582
+ raise SystemExit(1)
583
+
584
+ manifest = load_manifest()
585
+ save_manifest(manifest)
586
+
587
+ global_taste_path = global_dir() / "taste.md"
588
+ if global_taste_path.exists():
589
+ taste_text = global_taste_path.read_text(encoding="utf-8")
590
+ if taste_is_stale(taste_text):
591
+ print(_GLOBAL_TASTE_WARNING, file=sys.stderr)
592
+ except (ContinuousRefactorError, OSError) as error:
593
+ print(f"Error: {error}", file=sys.stderr)
594
+ raise SystemExit(1) from error
595
+
596
+
597
+ def _require_targeting_or_scope(args: argparse.Namespace) -> None:
598
+ has_targeting = args.targets or args.extensions or args.globs or args.paths
599
+ if not has_targeting and not args.scope_instruction:
600
+ print(
601
+ "Error: --scope-instruction required when no "
602
+ "--targets/--extensions/--globs/--paths",
603
+ file=sys.stderr,
604
+ )
605
+ raise SystemExit(2)
606
+
607
+
608
+ def _exit_with_loop_result(
609
+ command: Callable[[argparse.Namespace], int],
610
+ args: argparse.Namespace,
611
+ ) -> None:
612
+ try:
613
+ raise SystemExit(command(args))
614
+ except ContinuousRefactorError as error:
615
+ print(error, file=sys.stderr)
616
+ raise SystemExit(1) from error
617
+
618
+
619
+ def _normalize_run_effort_args(args: argparse.Namespace) -> None:
620
+ default_effort = getattr(args, "default_effort", getattr(args, "effort", None))
621
+ max_allowed_effort = getattr(args, "max_allowed_effort", None)
622
+ try:
623
+ budget = resolve_effort_budget(default_effort, max_allowed_effort)
624
+ except ContinuousRefactorError as error:
625
+ print(f"Error: {error}", file=sys.stderr)
626
+ raise SystemExit(2) from error
627
+ args.default_effort = budget.default_effort
628
+ args.effort = budget.default_effort
629
+ args.max_allowed_effort = budget.max_allowed_effort
630
+
631
+
632
+ def _handle_run_once(args: argparse.Namespace) -> None:
633
+ _normalize_run_effort_args(args)
634
+ _require_targeting_or_scope(args)
635
+ _exit_with_loop_result(run_once, args)
636
+
637
+
638
+ def _handle_run(args: argparse.Namespace) -> None:
639
+ _normalize_run_effort_args(args)
640
+ if getattr(args, "focus_on_live_migrations", False):
641
+ _exit_with_loop_result(run_migrations_focused_loop, args)
642
+ return
643
+ _require_targeting_or_scope(args)
644
+ if args.max_refactors is None and not args.targets:
645
+ print(
646
+ "Error: --max-refactors required when no --targets",
647
+ file=sys.stderr,
648
+ )
649
+ raise SystemExit(2)
650
+ _exit_with_loop_result(run_loop, args)
651
+
652
+
653
+ def _maybe_warn_stale_taste() -> None:
654
+ from continuous_refactoring.config import load_taste, resolve_project, taste_is_stale
655
+
656
+ try:
657
+ project = resolve_project(Path.cwd().resolve())
658
+ except ContinuousRefactorError:
659
+ project = None
660
+
661
+ try:
662
+ taste_text = load_taste(project)
663
+ except ContinuousRefactorError:
664
+ return
665
+ if taste_is_stale(taste_text):
666
+ print(_TASTE_WARNING, file=sys.stderr)
667
+
668
+
669
+ def cli_main() -> None:
670
+ parser = build_parser()
671
+ args = parser.parse_args()
672
+
673
+ if args.command is not None:
674
+ _maybe_warn_stale_taste()
675
+
676
+ handler = getattr(args, "handler", None)
677
+ if handler is None:
678
+ parser.print_help()
679
+ raise SystemExit(1)
680
+
681
+ return handler(args)
682
+
683
+ _TASTE_MODE_HANDLERS: dict[str, Callable[[argparse.Namespace], None]] = {
684
+ "interview": _handle_taste_interview,
685
+ "refine": _handle_taste_refine,
686
+ "upgrade": _handle_taste_upgrade,
687
+ }