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.
- continuous_refactoring/__init__.py +74 -0
- continuous_refactoring/__main__.py +8 -0
- continuous_refactoring/agent.py +733 -0
- continuous_refactoring/artifacts.py +431 -0
- continuous_refactoring/cli.py +687 -0
- continuous_refactoring/commit_messages.py +68 -0
- continuous_refactoring/config.py +377 -0
- continuous_refactoring/decisions.py +197 -0
- continuous_refactoring/effort.py +159 -0
- continuous_refactoring/failure_report.py +329 -0
- continuous_refactoring/git.py +134 -0
- continuous_refactoring/loop.py +1137 -0
- continuous_refactoring/migration_manifest_codec.py +190 -0
- continuous_refactoring/migration_tick.py +468 -0
- continuous_refactoring/migrations.py +251 -0
- continuous_refactoring/phases.py +690 -0
- continuous_refactoring/planning.py +588 -0
- continuous_refactoring/prompts.py +900 -0
- continuous_refactoring/refactor_attempts.py +424 -0
- continuous_refactoring/review_cli.py +136 -0
- continuous_refactoring/routing.py +133 -0
- continuous_refactoring/routing_pipeline.py +313 -0
- continuous_refactoring/scope_candidates.py +421 -0
- continuous_refactoring/scope_expansion.py +219 -0
- continuous_refactoring/targeting.py +274 -0
- continuous_refactoring-0.1.0.dist-info/METADATA +272 -0
- continuous_refactoring-0.1.0.dist-info/RECORD +30 -0
- continuous_refactoring-0.1.0.dist-info/WHEEL +4 -0
- continuous_refactoring-0.1.0.dist-info/entry_points.txt +2 -0
- continuous_refactoring-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -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
|
+
}
|