continuous-refactoring 0.2.0__tar.gz → 0.3.0__tar.gz

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.
Files changed (83) hide show
  1. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/.gitignore +2 -0
  2. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/PKG-INFO +76 -21
  3. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/README.md +75 -20
  4. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/pyproject.toml +1 -1
  5. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/src/continuous_refactoring/cli.py +139 -30
  6. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/src/continuous_refactoring/commit_messages.py +6 -2
  7. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/src/continuous_refactoring/decisions.py +15 -20
  8. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/src/continuous_refactoring/effort.py +45 -8
  9. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/src/continuous_refactoring/failure_report.py +68 -2
  10. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/src/continuous_refactoring/git.py +1 -1
  11. continuous_refactoring-0.3.0/src/continuous_refactoring/log_mirroring.py +11 -0
  12. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/src/continuous_refactoring/loop.py +351 -77
  13. continuous_refactoring-0.3.0/src/continuous_refactoring/migration_cli.py +701 -0
  14. continuous_refactoring-0.3.0/src/continuous_refactoring/migration_consistency.py +528 -0
  15. continuous_refactoring-0.3.0/src/continuous_refactoring/migration_tick.py +950 -0
  16. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/src/continuous_refactoring/phases.py +13 -5
  17. continuous_refactoring-0.3.0/src/continuous_refactoring/planning.py +1223 -0
  18. continuous_refactoring-0.3.0/src/continuous_refactoring/planning_publish.py +688 -0
  19. continuous_refactoring-0.3.0/src/continuous_refactoring/planning_state.py +1037 -0
  20. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/src/continuous_refactoring/prompts.py +44 -7
  21. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/src/continuous_refactoring/refactor_attempts.py +86 -18
  22. continuous_refactoring-0.3.0/src/continuous_refactoring/review_cli.py +296 -0
  23. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/src/continuous_refactoring/routing.py +40 -21
  24. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/src/continuous_refactoring/routing_pipeline.py +170 -70
  25. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/src/continuous_refactoring/scope_candidates.py +27 -18
  26. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/src/continuous_refactoring/scope_expansion.py +51 -15
  27. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/src/continuous_refactoring/targeting.py +15 -18
  28. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/tests/conftest.py +76 -24
  29. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/tests/test_cli_init_taste.py +48 -37
  30. continuous_refactoring-0.3.0/tests/test_cli_migrations.py +1485 -0
  31. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/tests/test_cli_review.py +113 -10
  32. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/tests/test_cli_taste_warning.py +33 -21
  33. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/tests/test_cli_upgrade.py +56 -62
  34. continuous_refactoring-0.3.0/tests/test_cli_version.py +39 -0
  35. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/tests/test_commit_messages.py +10 -0
  36. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/tests/test_config.py +6 -20
  37. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/tests/test_continuous_refactoring.py +8 -1
  38. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/tests/test_decisions.py +85 -0
  39. continuous_refactoring-0.3.0/tests/test_effort.py +143 -0
  40. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/tests/test_failure_report.py +216 -7
  41. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/tests/test_focus_on_live_migrations.py +448 -30
  42. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/tests/test_git.py +60 -29
  43. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/tests/test_loop_migration_tick.py +872 -38
  44. continuous_refactoring-0.3.0/tests/test_migration_consistency.py +305 -0
  45. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/tests/test_no_driver_branching.py +8 -18
  46. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/tests/test_phases.py +133 -6
  47. continuous_refactoring-0.3.0/tests/test_planning.py +1498 -0
  48. continuous_refactoring-0.3.0/tests/test_planning_publish.py +611 -0
  49. continuous_refactoring-0.3.0/tests/test_planning_state.py +873 -0
  50. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/tests/test_prompts.py +222 -0
  51. continuous_refactoring-0.3.0/tests/test_refactor_attempts.py +166 -0
  52. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/tests/test_routing.py +90 -0
  53. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/tests/test_run.py +829 -128
  54. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/tests/test_run_once.py +128 -18
  55. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/tests/test_scope_candidates.py +29 -10
  56. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/tests/test_scope_expansion.py +136 -0
  57. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/tests/test_scope_loop_integration.py +211 -6
  58. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/tests/test_scope_selection.py +28 -0
  59. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/tests/test_targeting.py +14 -0
  60. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/tests/test_taste_interview.py +7 -6
  61. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/tests/test_taste_refine.py +3 -5
  62. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/tests/test_taste_upgrade.py +16 -10
  63. continuous_refactoring-0.2.0/src/continuous_refactoring/migration_tick.py +0 -468
  64. continuous_refactoring-0.2.0/src/continuous_refactoring/planning.py +0 -588
  65. continuous_refactoring-0.2.0/src/continuous_refactoring/review_cli.py +0 -136
  66. continuous_refactoring-0.2.0/tests/test_effort.py +0 -54
  67. continuous_refactoring-0.2.0/tests/test_planning.py +0 -579
  68. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/LICENSE +0 -0
  69. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/src/continuous_refactoring/__init__.py +0 -0
  70. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/src/continuous_refactoring/__main__.py +0 -0
  71. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/src/continuous_refactoring/agent.py +0 -0
  72. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/src/continuous_refactoring/artifacts.py +0 -0
  73. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/src/continuous_refactoring/config.py +0 -0
  74. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/src/continuous_refactoring/migration_manifest_codec.py +0 -0
  75. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/src/continuous_refactoring/migrations.py +0 -0
  76. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/tests/fixtures/claude_stream_json/selection.stdout.log +0 -0
  77. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/tests/test_claude_stream_json.py +0 -0
  78. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/tests/test_e2e.py +0 -0
  79. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/tests/test_main_entrypoint.py +0 -0
  80. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/tests/test_migrations.py +0 -0
  81. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/tests/test_prompts_scope_selection.py +0 -0
  82. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/tests/test_run_once_regression.py +0 -0
  83. {continuous_refactoring-0.2.0 → continuous_refactoring-0.3.0}/tests/test_wake_up.py +0 -0
@@ -1,4 +1,6 @@
1
1
  .scratchpad/
2
+ tmpdir/
3
+ .pytest_cache/
2
4
 
3
5
  # Byte-compiled / optimized / DLL files
4
6
  __pycache__/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: continuous-refactoring
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Continuous refactoring loop for AI coding agents
5
5
  Project-URL: Repository, https://github.com/bigH/continuous-refactoring
6
6
  Project-URL: Issues, https://github.com/bigH/continuous-refactoring/issues
@@ -32,6 +32,8 @@ Small, test-gated cleanup commits by an AI coding agent.
32
32
 
33
33
  Think of it as a supervised janitor loop: the agent proposes a cleanup, your tests decide if it stays.
34
34
 
35
+ Here's [an article](https://artisincode.com/essays/how-i-use-unspent-tokens/) I wrote about it.
36
+
35
37
  ## Install
36
38
 
37
39
  Try it without installing:
@@ -92,15 +94,18 @@ continuous-refactoring run \
92
94
  --max-attempts 2
93
95
  ```
94
96
 
95
- That keeps sweeping targets until it runs out, hits your caps, or starts failing.
97
+ That runs up to 10 refactor actions, then stops sooner if the finite target file
98
+ runs out or the loop starts failing. Use `run --focus-on-live-migrations` when
99
+ you want the loop to work only on eligible live migrations; it bypasses target
100
+ selection and `--max-refactors`.
96
101
 
97
102
  ## What it does
98
103
 
99
- - Resolves a target from `--targets`, `--globs`, `--extensions`, or `--paths`, with optional natural-language scoping via `--scope-instruction`.
104
+ - Resolves each source action from `--targets`, `--globs`, `--extensions`, or `--paths`, with optional natural-language scoping via `--scope-instruction`.
100
105
  - Runs the agent with a refactoring prompt + your "taste" guidelines.
101
106
  - Runs your validation command (default: `uv run pytest`).
102
107
  - If green and there's a diff, it commits locally and leaves the branch for you to inspect.
103
- - Repeats until it runs out of targets, hits the retry budget, or stacks too many failures.
108
+ - Repeats until it spends the action budget, exhausts a finite target file, hits the retry budget, or stacks too many failures.
104
109
 
105
110
  ## Requirements
106
111
 
@@ -143,10 +148,15 @@ continuous-refactoring run \
143
148
  | `init` | Registers this directory as a project, creates a default `taste.md`, and can store `--live-migrations-dir` or `--in-repo-taste`. |
144
149
  | `taste` | Prints the active taste file path. Add `--interview` to have an agent author it, `--refine` to iteratively improve an existing taste doc, `--upgrade` to refresh stale taste dimensions, `--global` for the shared file, and `--force` to let `--interview` overwrite custom content after writing a `.bak`. |
145
150
  | `run-once` | Single pass on one resolved target. No retry. If there is a diff and validation passes, it commits locally and prints the diffstat. |
146
- | `run` | The loop. Iterates targets, retries on failure, and commits successful targets locally. |
151
+ | `run` | The loop. Iterates refactor actions, retries on failure, and commits successful changes locally. Add `--focus-on-live-migrations` to bypass targeting and work only on eligible live migrations. |
147
152
  | `upgrade` | Checks that the global config manifest is current, rewrites it idempotently, and warns if the global taste file is stale. |
148
- | `review list` | Lists migrations flagged for human review (`awaiting_human_review`). |
149
- | `review perform <migration>` | Starts an interactive agent session to resolve a flagged migration's review. Requires `--with`, `--model`, and `--effort`. |
153
+ | `migration list` | Lists visible migrations. Add `--status <status>` or `--awaiting-review` to filter. |
154
+ | `migration doctor <slug-or-path>` | Validates one visible migration's consistency. |
155
+ | `migration doctor --all` | Validates every visible migration plus internal transaction state. |
156
+ | `migration review <slug-or-path>` | Starts staged review for a migration awaiting human review. Requires `--with`, `--model`, and `--effort`. |
157
+ | `migration refine <slug-or-path>` | Records feedback for a planning or unexecuted ready migration and runs one staged planning revision. Requires `--message <text>` or `--file <path>`, plus `--with`, `--model`, and `--effort`; add `--show-agent-logs` to mirror the planning agent. |
158
+
159
+ Legacy `review list` and `review perform <migration>` remain compatibility aliases; prefer `migration list --awaiting-review` and `migration review`.
150
160
 
151
161
  ## Targeting / Useful flags
152
162
 
@@ -157,22 +167,46 @@ Target resolution is first-match-wins:
157
167
 
158
168
  These flags are not mutually exclusive, but only the highest-priority populated source is used.
159
169
 
160
- - `--targets path/to/targets.jsonl` — explicit list; one JSON object per line with `description`, `files`, optional `scoping`, `model-override`, `effort-override`. Effort overrides use `low`, `medium`, `high`, or `xhigh`.
161
- - `--globs 'src/**/*.py:tests/**/*.py'` — colon-separated globs; each matched file becomes its own target.
162
- - `--extensions .py,.ts` — shorthand that expands to `**/*.py`, `**/*.ts`; each matched file becomes its own target.
163
- - `--paths a.py:b.py` — literal paths, all treated as one target.
164
- - `--scope-instruction "clean up the auth module"` — extra free-text scoping. If file-based targeting resolves nothing, this becomes the useful fallback context.
170
+ - `--targets path/to/targets.jsonl` — explicit finite list; one JSON object per line with `description`, `files`, optional `scoping`, `model-override`, `effort-override`. Effort overrides use `low`, `medium`, `high`, or `xhigh`. If `--max-refactors` is omitted, `run` processes the file once and stops.
171
+ - `--globs 'src/**/*.py:tests/**/*.py'` — colon-separated globs matched once against tracked files from `git ls-files`; each refactor action samples one matched file, so files can repeat.
172
+ - `--extensions .py,.ts` — shorthand that expands to `**/*.py`, `**/*.ts` against tracked files from `git ls-files`; each refactor action samples one matched file, so files can repeat.
173
+ - `--paths a.py:b.py` — literal user-provided paths, all treated as one grouped target; each refactor action reuses that group.
174
+ - `--scope-instruction "clean up the auth module"` — extra free-text scoping. If selected file patterns resolve nothing, this becomes the useful fallback context.
165
175
 
166
- If you provide none of `--targets`, `--globs`, `--extensions`, or `--paths`, then `run` and `run-once` require `--scope-instruction`.
176
+ If `--globs` or `--extensions` match no tracked files and there is no
177
+ `--scope-instruction`, `run` completes successfully with zero refactor actions.
178
+ `--paths` is literal input and is not filtered through `git ls-files`.
179
+
180
+ If you provide none of `--targets`, `--globs`, `--extensions`, or `--paths`,
181
+ then `run` and `run-once` require `--scope-instruction`; the driver still
182
+ random-samples tracked files from `git ls-files` for each action and uses the
183
+ scope text as context for that target.
167
184
 
168
185
  ### Migrations & taste flags
169
186
 
170
187
  - `init --live-migrations-dir PATH` — enables the larger-refactoring workflow for this project. The path is stored repo-relative in the project registry and created if missing.
171
188
  - `init --in-repo-taste [PATH]` — stores this project's taste file in the repo and remembers the repo-relative path. Defaults to `.continuous-refactoring/taste.md`; re-run `init --in-repo-taste ...` to choose a different path.
189
+ - `migration list` — shows visible migrations; `--awaiting-review` narrows to human-review handoffs.
190
+ - `migration doctor <slug-or-path>` / `migration doctor --all` — read-only consistency checks. Doctor reports problems; it does not repair them.
191
+ - `migration review <slug-or-path> --with ... --model ... --effort ...` — resolves an `awaiting_human_review` migration through a staged workspace.
192
+ - `migration refine <slug-or-path> (--message <text>|--file <path>) --with ... --model ... --effort ... [--show-agent-logs]` — adds user feedback to a planning or unexecuted ready migration and resumes planning through the `revise` step when reopening ready work.
172
193
  - `taste --refine` — opens a collaborative editing session for the taste file. The agent keeps refining until you tell it to write, then the session ends automatically after the settled write.
173
194
  - `taste --upgrade` — re-interviews for taste dimensions added since your last version. No-op when already current; use `taste --refine` if you want to rework the doc anyway.
174
195
  - `taste --force` — only applies to `--interview`; it allows a customized taste file to be overwritten after backing it up to `taste.md.bak`.
175
196
 
197
+ Canonical migration commands:
198
+
199
+ ```bash
200
+ continuous-refactoring migration list
201
+ continuous-refactoring migration list --status planning
202
+ continuous-refactoring migration list --awaiting-review
203
+ continuous-refactoring migration doctor <slug-or-path>
204
+ continuous-refactoring migration doctor --all
205
+ continuous-refactoring migration review <slug-or-path> --with codex --model gpt-5 --effort high
206
+ continuous-refactoring migration refine <slug-or-path> --message "split the risky phase" --with codex --model gpt-5 --effort high
207
+ continuous-refactoring migration refine <slug-or-path> --file feedback.md --with codex --model gpt-5 --effort high
208
+ ```
209
+
176
210
  ### Shared `run` / `run-once` flags
177
211
 
178
212
  - `--with`, `--model` — required agent backend/model selection.
@@ -187,10 +221,11 @@ If you provide none of `--targets`, `--globs`, `--extensions`, or `--paths`, the
187
221
 
188
222
  ### `run`-only flags
189
223
 
190
- - `--max-attempts N` — per-target retry budget. `1` = no retry, `0` = unlimited (which means permanently broken targets will never give up).
191
- - `--max-refactors N` — cap the number of targets per run. Required unless you use `--targets`.
192
- - `--max-consecutive-failures N` — bail after N targets fail in a row. Default 3.
193
- - `--sleep SECONDS` — pause between completed targets. Useful when you want a long batch without hammering the repo or your agent budget.
224
+ - `--max-attempts N` — per-action retry budget. `1` = no retry, `0` = unlimited (which means permanently broken actions will never give up).
225
+ - `--max-refactors N` — cap the number of refactor actions per run. Required unless you use `--targets` or `--focus-on-live-migrations`.
226
+ - `--focus-on-live-migrations` — bypass target selection and `--max-refactors`; iterate eligible live migrations until they are done, deferred, blocked, or the failure budget trips.
227
+ - `--max-consecutive-failures N` — bail after N actions fail in a row. Default 3.
228
+ - `--sleep SECONDS` — pause between completed actions. Useful when you want a long batch without hammering the repo or your agent budget.
194
229
  - `--commit-message-prefix TEXT` — subject prefix for successful refactor or migration-plan commits. Default `continuous refactor`.
195
230
 
196
231
  ## Safety behaviors
@@ -207,13 +242,27 @@ If you provide none of `--targets`, `--globs`, `--extensions`, or `--paths`, the
207
242
  Each run writes to `$TMPDIR/continuous-refactoring/<run-id>/`:
208
243
 
209
244
  - `summary.json` — rolling status, counts, per-attempt stats
210
- - `events.jsonl` — structured event log
245
+ - `events.jsonl` — structured event log with call roles such as `classify`,
246
+ `planning.<step>`, `phase.ready-check`, `phase.execute`, and
247
+ `phase.validation`
211
248
  - `run.log` — human-readable log
212
249
  - `attempt-NNN/[retry-NN/]refactor/` — per-attempt agent + test stdout/stderr
250
+ - `baseline/initial/` — baseline validation stdout/stderr before work starts
251
+ - `classify/` — classifier agent stdout/stderr
252
+ - `scope-expansion/` — scope candidates, selection, and bypass reason
253
+ - `attempt-NNN/[retry-NN/]planning/<step>/` — planning agent stdout/stderr for
254
+ migration planning steps
255
+ - `phase-ready-check/` — phase precondition agent stdout/stderr
256
+ - `attempt-NNN/[retry-NN/]phase-execute/` — phase agent and validation logs
257
+ - `migration-probes/action-NNN/` — migration probe logs during normal `run`
258
+ actions, including planning, phase ready-checks, and phase execution
213
259
 
214
260
  Mixed-effort runs are auditable: summaries and call events record the default effort, max allowed effort, requested effort, effective effort, source, and whether the request was capped.
215
261
 
216
- The path prints at startup. Grep it when something goes sideways.
262
+ The path prints at startup. Grep it when something goes sideways. Failed
263
+ non-commit decisions also write durable XDG snapshots under the project failure
264
+ directory, usually
265
+ `~/.local/share/continuous-refactoring/projects/<uuid>/failures/`.
217
266
 
218
267
  ## Taste files
219
268
 
@@ -241,7 +290,7 @@ This tells the CLI where to store migration artifacts. The path is repo-relative
241
290
  Each `run` / `run-once` tick now checks for eligible migration work before falling back to single-commit cleanups:
242
291
 
243
292
  1. **Classify** — a classifier agent reads the target and decides: `cohesive-cleanup` (one-shot path) or `needs-plan` (migration path).
244
- 2. **Plan** — for `needs-plan` targets, a six-stage planning workflow runs: generate approaches pick best expand into phases → review revise final review. Artifacts land under `<live-migrations-dir>/<migration-name>/`.
293
+ 2. **Plan** — for `needs-plan` targets, each automation action runs exactly one planning step: approaches, pick-best, expand, review, optional revise/review-2, then final-review. Accepted steps update `.planning/state.json`, store stdout under `.planning/stages/`, and publish through a staged transaction. Failed current-step output stays in run artifacts and is not resume input.
245
294
  3. **Execute** — each phase is a self-contained unit of work. The tick picks the oldest eligible migration, checks whether its current phase precondition is satisfied, and executes it on the current branch. Phase completion is judged against the phase file's `## Definition of Done`; commit message identifies the migration as `migration/<name>/<phase-file>.md`.
246
295
 
247
296
  ### Migration directory layout
@@ -250,14 +299,20 @@ Each `run` / `run-once` tick now checks for eligible migration work before falli
250
299
  <live-migrations-dir>/
251
300
  <migration-name>/
252
301
  manifest.json # status, phases, wake-up schedule
302
+ .planning/
303
+ state.json # durable planning cursor and accepted step refs
304
+ stages/ # accepted planning stdout, suffixed on repeats
253
305
  plan.md # the expanded plan
254
306
  approaches/ # candidate approaches considered during planning
255
307
  phase-1-<name>.md # per-phase specification
256
308
  phase-2-<name>.md
257
309
  ...
310
+ __transactions__/ # internal staged publish state
258
311
  __intentional_skips__/ # migrations rejected at final review
259
312
  ```
260
313
 
314
+ Do not edit `.planning/` or `__transactions__/` by hand. Use `migration doctor` when the shape looks wrong.
315
+
261
316
  ### Wake-up rules
262
317
 
263
318
  Migrations don't run on every tick. The scheduler now separates **activity** from
@@ -291,7 +346,7 @@ Before executing a phase, a ready-check agent verifies that the current phase pr
291
346
 
292
347
  - **ready: yes** — phase executes; on green tests, the phase is marked done, any prior deferral markers are cleared, and the migration advances immediately to the next phase.
293
348
  - **ready: no** — manifest activity is bumped, a retry cooldown is started, and a future `wake_up_on` is recorded when needed; the tick moves on.
294
- - **ready: unverifiable** — the migration is flagged `awaiting_human_review` and put on cooldown. Automated migration ticks skip flagged migrations until review clears the flag. Use `review list` to find it and `review perform <migration> --with ... --model ... --effort ...` to resolve it interactively.
349
+ - **ready: unverifiable** — the migration is flagged `awaiting_human_review` and put on cooldown. Automated migration ticks skip flagged migrations until review clears the flag. Use `migration list --awaiting-review` to find it and `migration review <slug-or-path> --with ... --model ... --effort ...` to resolve it interactively.
295
350
 
296
351
  Human-facing migration references use the relative phase spec path, for example `phase-2-failure-report.md`. The manifest cursor stores the phase `name`, not a numeric index.
297
352
 
@@ -9,6 +9,8 @@ Small, test-gated cleanup commits by an AI coding agent.
9
9
 
10
10
  Think of it as a supervised janitor loop: the agent proposes a cleanup, your tests decide if it stays.
11
11
 
12
+ Here's [an article](https://artisincode.com/essays/how-i-use-unspent-tokens/) I wrote about it.
13
+
12
14
  ## Install
13
15
 
14
16
  Try it without installing:
@@ -69,15 +71,18 @@ continuous-refactoring run \
69
71
  --max-attempts 2
70
72
  ```
71
73
 
72
- That keeps sweeping targets until it runs out, hits your caps, or starts failing.
74
+ That runs up to 10 refactor actions, then stops sooner if the finite target file
75
+ runs out or the loop starts failing. Use `run --focus-on-live-migrations` when
76
+ you want the loop to work only on eligible live migrations; it bypasses target
77
+ selection and `--max-refactors`.
73
78
 
74
79
  ## What it does
75
80
 
76
- - Resolves a target from `--targets`, `--globs`, `--extensions`, or `--paths`, with optional natural-language scoping via `--scope-instruction`.
81
+ - Resolves each source action from `--targets`, `--globs`, `--extensions`, or `--paths`, with optional natural-language scoping via `--scope-instruction`.
77
82
  - Runs the agent with a refactoring prompt + your "taste" guidelines.
78
83
  - Runs your validation command (default: `uv run pytest`).
79
84
  - If green and there's a diff, it commits locally and leaves the branch for you to inspect.
80
- - Repeats until it runs out of targets, hits the retry budget, or stacks too many failures.
85
+ - Repeats until it spends the action budget, exhausts a finite target file, hits the retry budget, or stacks too many failures.
81
86
 
82
87
  ## Requirements
83
88
 
@@ -120,10 +125,15 @@ continuous-refactoring run \
120
125
  | `init` | Registers this directory as a project, creates a default `taste.md`, and can store `--live-migrations-dir` or `--in-repo-taste`. |
121
126
  | `taste` | Prints the active taste file path. Add `--interview` to have an agent author it, `--refine` to iteratively improve an existing taste doc, `--upgrade` to refresh stale taste dimensions, `--global` for the shared file, and `--force` to let `--interview` overwrite custom content after writing a `.bak`. |
122
127
  | `run-once` | Single pass on one resolved target. No retry. If there is a diff and validation passes, it commits locally and prints the diffstat. |
123
- | `run` | The loop. Iterates targets, retries on failure, and commits successful targets locally. |
128
+ | `run` | The loop. Iterates refactor actions, retries on failure, and commits successful changes locally. Add `--focus-on-live-migrations` to bypass targeting and work only on eligible live migrations. |
124
129
  | `upgrade` | Checks that the global config manifest is current, rewrites it idempotently, and warns if the global taste file is stale. |
125
- | `review list` | Lists migrations flagged for human review (`awaiting_human_review`). |
126
- | `review perform <migration>` | Starts an interactive agent session to resolve a flagged migration's review. Requires `--with`, `--model`, and `--effort`. |
130
+ | `migration list` | Lists visible migrations. Add `--status <status>` or `--awaiting-review` to filter. |
131
+ | `migration doctor <slug-or-path>` | Validates one visible migration's consistency. |
132
+ | `migration doctor --all` | Validates every visible migration plus internal transaction state. |
133
+ | `migration review <slug-or-path>` | Starts staged review for a migration awaiting human review. Requires `--with`, `--model`, and `--effort`. |
134
+ | `migration refine <slug-or-path>` | Records feedback for a planning or unexecuted ready migration and runs one staged planning revision. Requires `--message <text>` or `--file <path>`, plus `--with`, `--model`, and `--effort`; add `--show-agent-logs` to mirror the planning agent. |
135
+
136
+ Legacy `review list` and `review perform <migration>` remain compatibility aliases; prefer `migration list --awaiting-review` and `migration review`.
127
137
 
128
138
  ## Targeting / Useful flags
129
139
 
@@ -134,22 +144,46 @@ Target resolution is first-match-wins:
134
144
 
135
145
  These flags are not mutually exclusive, but only the highest-priority populated source is used.
136
146
 
137
- - `--targets path/to/targets.jsonl` — explicit list; one JSON object per line with `description`, `files`, optional `scoping`, `model-override`, `effort-override`. Effort overrides use `low`, `medium`, `high`, or `xhigh`.
138
- - `--globs 'src/**/*.py:tests/**/*.py'` — colon-separated globs; each matched file becomes its own target.
139
- - `--extensions .py,.ts` — shorthand that expands to `**/*.py`, `**/*.ts`; each matched file becomes its own target.
140
- - `--paths a.py:b.py` — literal paths, all treated as one target.
141
- - `--scope-instruction "clean up the auth module"` — extra free-text scoping. If file-based targeting resolves nothing, this becomes the useful fallback context.
147
+ - `--targets path/to/targets.jsonl` — explicit finite list; one JSON object per line with `description`, `files`, optional `scoping`, `model-override`, `effort-override`. Effort overrides use `low`, `medium`, `high`, or `xhigh`. If `--max-refactors` is omitted, `run` processes the file once and stops.
148
+ - `--globs 'src/**/*.py:tests/**/*.py'` — colon-separated globs matched once against tracked files from `git ls-files`; each refactor action samples one matched file, so files can repeat.
149
+ - `--extensions .py,.ts` — shorthand that expands to `**/*.py`, `**/*.ts` against tracked files from `git ls-files`; each refactor action samples one matched file, so files can repeat.
150
+ - `--paths a.py:b.py` — literal user-provided paths, all treated as one grouped target; each refactor action reuses that group.
151
+ - `--scope-instruction "clean up the auth module"` — extra free-text scoping. If selected file patterns resolve nothing, this becomes the useful fallback context.
142
152
 
143
- If you provide none of `--targets`, `--globs`, `--extensions`, or `--paths`, then `run` and `run-once` require `--scope-instruction`.
153
+ If `--globs` or `--extensions` match no tracked files and there is no
154
+ `--scope-instruction`, `run` completes successfully with zero refactor actions.
155
+ `--paths` is literal input and is not filtered through `git ls-files`.
156
+
157
+ If you provide none of `--targets`, `--globs`, `--extensions`, or `--paths`,
158
+ then `run` and `run-once` require `--scope-instruction`; the driver still
159
+ random-samples tracked files from `git ls-files` for each action and uses the
160
+ scope text as context for that target.
144
161
 
145
162
  ### Migrations & taste flags
146
163
 
147
164
  - `init --live-migrations-dir PATH` — enables the larger-refactoring workflow for this project. The path is stored repo-relative in the project registry and created if missing.
148
165
  - `init --in-repo-taste [PATH]` — stores this project's taste file in the repo and remembers the repo-relative path. Defaults to `.continuous-refactoring/taste.md`; re-run `init --in-repo-taste ...` to choose a different path.
166
+ - `migration list` — shows visible migrations; `--awaiting-review` narrows to human-review handoffs.
167
+ - `migration doctor <slug-or-path>` / `migration doctor --all` — read-only consistency checks. Doctor reports problems; it does not repair them.
168
+ - `migration review <slug-or-path> --with ... --model ... --effort ...` — resolves an `awaiting_human_review` migration through a staged workspace.
169
+ - `migration refine <slug-or-path> (--message <text>|--file <path>) --with ... --model ... --effort ... [--show-agent-logs]` — adds user feedback to a planning or unexecuted ready migration and resumes planning through the `revise` step when reopening ready work.
149
170
  - `taste --refine` — opens a collaborative editing session for the taste file. The agent keeps refining until you tell it to write, then the session ends automatically after the settled write.
150
171
  - `taste --upgrade` — re-interviews for taste dimensions added since your last version. No-op when already current; use `taste --refine` if you want to rework the doc anyway.
151
172
  - `taste --force` — only applies to `--interview`; it allows a customized taste file to be overwritten after backing it up to `taste.md.bak`.
152
173
 
174
+ Canonical migration commands:
175
+
176
+ ```bash
177
+ continuous-refactoring migration list
178
+ continuous-refactoring migration list --status planning
179
+ continuous-refactoring migration list --awaiting-review
180
+ continuous-refactoring migration doctor <slug-or-path>
181
+ continuous-refactoring migration doctor --all
182
+ continuous-refactoring migration review <slug-or-path> --with codex --model gpt-5 --effort high
183
+ continuous-refactoring migration refine <slug-or-path> --message "split the risky phase" --with codex --model gpt-5 --effort high
184
+ continuous-refactoring migration refine <slug-or-path> --file feedback.md --with codex --model gpt-5 --effort high
185
+ ```
186
+
153
187
  ### Shared `run` / `run-once` flags
154
188
 
155
189
  - `--with`, `--model` — required agent backend/model selection.
@@ -164,10 +198,11 @@ If you provide none of `--targets`, `--globs`, `--extensions`, or `--paths`, the
164
198
 
165
199
  ### `run`-only flags
166
200
 
167
- - `--max-attempts N` — per-target retry budget. `1` = no retry, `0` = unlimited (which means permanently broken targets will never give up).
168
- - `--max-refactors N` — cap the number of targets per run. Required unless you use `--targets`.
169
- - `--max-consecutive-failures N` — bail after N targets fail in a row. Default 3.
170
- - `--sleep SECONDS` — pause between completed targets. Useful when you want a long batch without hammering the repo or your agent budget.
201
+ - `--max-attempts N` — per-action retry budget. `1` = no retry, `0` = unlimited (which means permanently broken actions will never give up).
202
+ - `--max-refactors N` — cap the number of refactor actions per run. Required unless you use `--targets` or `--focus-on-live-migrations`.
203
+ - `--focus-on-live-migrations` — bypass target selection and `--max-refactors`; iterate eligible live migrations until they are done, deferred, blocked, or the failure budget trips.
204
+ - `--max-consecutive-failures N` — bail after N actions fail in a row. Default 3.
205
+ - `--sleep SECONDS` — pause between completed actions. Useful when you want a long batch without hammering the repo or your agent budget.
171
206
  - `--commit-message-prefix TEXT` — subject prefix for successful refactor or migration-plan commits. Default `continuous refactor`.
172
207
 
173
208
  ## Safety behaviors
@@ -184,13 +219,27 @@ If you provide none of `--targets`, `--globs`, `--extensions`, or `--paths`, the
184
219
  Each run writes to `$TMPDIR/continuous-refactoring/<run-id>/`:
185
220
 
186
221
  - `summary.json` — rolling status, counts, per-attempt stats
187
- - `events.jsonl` — structured event log
222
+ - `events.jsonl` — structured event log with call roles such as `classify`,
223
+ `planning.<step>`, `phase.ready-check`, `phase.execute`, and
224
+ `phase.validation`
188
225
  - `run.log` — human-readable log
189
226
  - `attempt-NNN/[retry-NN/]refactor/` — per-attempt agent + test stdout/stderr
227
+ - `baseline/initial/` — baseline validation stdout/stderr before work starts
228
+ - `classify/` — classifier agent stdout/stderr
229
+ - `scope-expansion/` — scope candidates, selection, and bypass reason
230
+ - `attempt-NNN/[retry-NN/]planning/<step>/` — planning agent stdout/stderr for
231
+ migration planning steps
232
+ - `phase-ready-check/` — phase precondition agent stdout/stderr
233
+ - `attempt-NNN/[retry-NN/]phase-execute/` — phase agent and validation logs
234
+ - `migration-probes/action-NNN/` — migration probe logs during normal `run`
235
+ actions, including planning, phase ready-checks, and phase execution
190
236
 
191
237
  Mixed-effort runs are auditable: summaries and call events record the default effort, max allowed effort, requested effort, effective effort, source, and whether the request was capped.
192
238
 
193
- The path prints at startup. Grep it when something goes sideways.
239
+ The path prints at startup. Grep it when something goes sideways. Failed
240
+ non-commit decisions also write durable XDG snapshots under the project failure
241
+ directory, usually
242
+ `~/.local/share/continuous-refactoring/projects/<uuid>/failures/`.
194
243
 
195
244
  ## Taste files
196
245
 
@@ -218,7 +267,7 @@ This tells the CLI where to store migration artifacts. The path is repo-relative
218
267
  Each `run` / `run-once` tick now checks for eligible migration work before falling back to single-commit cleanups:
219
268
 
220
269
  1. **Classify** — a classifier agent reads the target and decides: `cohesive-cleanup` (one-shot path) or `needs-plan` (migration path).
221
- 2. **Plan** — for `needs-plan` targets, a six-stage planning workflow runs: generate approaches pick best expand into phases → review revise final review. Artifacts land under `<live-migrations-dir>/<migration-name>/`.
270
+ 2. **Plan** — for `needs-plan` targets, each automation action runs exactly one planning step: approaches, pick-best, expand, review, optional revise/review-2, then final-review. Accepted steps update `.planning/state.json`, store stdout under `.planning/stages/`, and publish through a staged transaction. Failed current-step output stays in run artifacts and is not resume input.
222
271
  3. **Execute** — each phase is a self-contained unit of work. The tick picks the oldest eligible migration, checks whether its current phase precondition is satisfied, and executes it on the current branch. Phase completion is judged against the phase file's `## Definition of Done`; commit message identifies the migration as `migration/<name>/<phase-file>.md`.
223
272
 
224
273
  ### Migration directory layout
@@ -227,14 +276,20 @@ Each `run` / `run-once` tick now checks for eligible migration work before falli
227
276
  <live-migrations-dir>/
228
277
  <migration-name>/
229
278
  manifest.json # status, phases, wake-up schedule
279
+ .planning/
280
+ state.json # durable planning cursor and accepted step refs
281
+ stages/ # accepted planning stdout, suffixed on repeats
230
282
  plan.md # the expanded plan
231
283
  approaches/ # candidate approaches considered during planning
232
284
  phase-1-<name>.md # per-phase specification
233
285
  phase-2-<name>.md
234
286
  ...
287
+ __transactions__/ # internal staged publish state
235
288
  __intentional_skips__/ # migrations rejected at final review
236
289
  ```
237
290
 
291
+ Do not edit `.planning/` or `__transactions__/` by hand. Use `migration doctor` when the shape looks wrong.
292
+
238
293
  ### Wake-up rules
239
294
 
240
295
  Migrations don't run on every tick. The scheduler now separates **activity** from
@@ -268,7 +323,7 @@ Before executing a phase, a ready-check agent verifies that the current phase pr
268
323
 
269
324
  - **ready: yes** — phase executes; on green tests, the phase is marked done, any prior deferral markers are cleared, and the migration advances immediately to the next phase.
270
325
  - **ready: no** — manifest activity is bumped, a retry cooldown is started, and a future `wake_up_on` is recorded when needed; the tick moves on.
271
- - **ready: unverifiable** — the migration is flagged `awaiting_human_review` and put on cooldown. Automated migration ticks skip flagged migrations until review clears the flag. Use `review list` to find it and `review perform <migration> --with ... --model ... --effort ...` to resolve it interactively.
326
+ - **ready: unverifiable** — the migration is flagged `awaiting_human_review` and put on cooldown. Automated migration ticks skip flagged migrations until review clears the flag. Use `migration list --awaiting-review` to find it and `migration review <slug-or-path> --with ... --model ... --effort ...` to resolve it interactively.
272
327
 
273
328
  Human-facing migration references use the relative phase spec path, for example `phase-2-failure-report.md`. The manifest cursor stores the phase `name`, not a numeric index.
274
329
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "continuous-refactoring"
3
- version = "0.2.0"
3
+ version = "0.3.0"
4
4
  description = "Continuous refactoring loop for AI coding agents"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -5,7 +5,9 @@ import shutil
5
5
  import sys
6
6
  import uuid
7
7
  from collections.abc import Callable
8
+ from importlib.metadata import version as metadata_version
8
9
  from pathlib import Path
10
+ from typing import Literal
9
11
 
10
12
  __all__ = [
11
13
  "build_parser",
@@ -28,8 +30,11 @@ from continuous_refactoring.loop import (
28
30
  run_migrations_focused_loop,
29
31
  run_once,
30
32
  )
33
+ from continuous_refactoring.migration_cli import handle_migration
34
+ from continuous_refactoring.migrations import MIGRATION_STATUSES
31
35
  from continuous_refactoring.review_cli import handle_review
32
36
 
37
+ _PACKAGE_DISTRIBUTION = "continuous-refactoring"
33
38
  _TASTE_WARNING = "warning: taste out of date — run `continuous-refactoring taste --upgrade`"
34
39
  _GLOBAL_TASTE_WARNING = (
35
40
  "warning: global taste is out of date — "
@@ -37,6 +42,10 @@ _GLOBAL_TASTE_WARNING = (
37
42
  )
38
43
 
39
44
 
45
+ def _version_banner() -> str:
46
+ return f"{_PACKAGE_DISTRIBUTION} {metadata_version(_PACKAGE_DISTRIBUTION)}"
47
+
48
+
40
49
  def parse_max_attempts(value: str) -> int:
41
50
  try:
42
51
  attempts = int(value)
@@ -220,7 +229,7 @@ def _add_run_parser(subparsers: argparse._SubParsersAction) -> None:
220
229
  "--max-refactors",
221
230
  type=int,
222
231
  default=None,
223
- help="Distinct targets to process.",
232
+ help="Refactor actions to run.",
224
233
  )
225
234
  run_parser.add_argument(
226
235
  "--focus-on-live-migrations",
@@ -245,7 +254,7 @@ def _add_run_parser(subparsers: argparse._SubParsersAction) -> None:
245
254
  "--sleep",
246
255
  type=parse_sleep_seconds,
247
256
  default=0.0,
248
- help="Seconds to sleep between completed targets.",
257
+ help="Seconds to sleep between completed actions.",
249
258
  )
250
259
 
251
260
 
@@ -270,10 +279,95 @@ def _add_review_parser(subparsers: argparse._SubParsersAction) -> None:
270
279
  perform_parser.add_argument("--effort", required=True, help="Effort level.")
271
280
 
272
281
 
282
+ def _add_migration_parser(subparsers: argparse._SubParsersAction) -> None:
283
+ migration_parser = subparsers.add_parser(
284
+ "migration",
285
+ help="Inspect live migrations.",
286
+ )
287
+ migration_parser.set_defaults(handler=handle_migration)
288
+ migration_sub = migration_parser.add_subparsers(dest="migration_command")
289
+
290
+ list_parser = migration_sub.add_parser(
291
+ "list",
292
+ help="List visible migrations.",
293
+ )
294
+ list_parser.add_argument(
295
+ "--status",
296
+ choices=MIGRATION_STATUSES,
297
+ default=None,
298
+ help="Only show migrations with this status.",
299
+ )
300
+ list_parser.add_argument(
301
+ "--awaiting-review",
302
+ action="store_true",
303
+ help="Only show migrations awaiting human review.",
304
+ )
305
+
306
+ doctor_parser = migration_sub.add_parser(
307
+ "doctor",
308
+ help="Validate migration consistency.",
309
+ )
310
+ doctor_parser.add_argument(
311
+ "target",
312
+ nargs="?",
313
+ help="Migration slug or contained path.",
314
+ )
315
+ doctor_parser.add_argument(
316
+ "--all",
317
+ action="store_true",
318
+ help="Validate every visible migration and transaction state.",
319
+ )
320
+
321
+ review_parser = migration_sub.add_parser(
322
+ "review",
323
+ help="Perform staged review on a flagged migration.",
324
+ )
325
+ review_parser.add_argument("target", help="Migration slug or contained path.")
326
+ review_parser.add_argument(
327
+ "--with", dest="agent", choices=("codex", "claude"), required=True,
328
+ help="Agent backend.",
329
+ )
330
+ review_parser.add_argument("--model", required=True, help="Model name.")
331
+ review_parser.add_argument(
332
+ "--effort", choices=EFFORT_TIERS, required=True, help="Effort level."
333
+ )
334
+
335
+ refine_parser = migration_sub.add_parser(
336
+ "refine",
337
+ help="Refine a planning migration with user feedback.",
338
+ )
339
+ refine_parser.add_argument("target", help="Migration slug or contained path.")
340
+ feedback_group = refine_parser.add_mutually_exclusive_group(required=True)
341
+ feedback_group.add_argument("--message", help="Refinement feedback text.")
342
+ feedback_group.add_argument(
343
+ "--file",
344
+ type=Path,
345
+ help="Path to a UTF-8 file containing refinement feedback.",
346
+ )
347
+ refine_parser.add_argument(
348
+ "--with", dest="agent", choices=("codex", "claude"), required=True,
349
+ help="Agent backend.",
350
+ )
351
+ refine_parser.add_argument("--model", required=True, help="Model name.")
352
+ refine_parser.add_argument(
353
+ "--effort", choices=EFFORT_TIERS, required=True, help="Effort level."
354
+ )
355
+ refine_parser.add_argument(
356
+ "--show-agent-logs",
357
+ action="store_true",
358
+ help="Mirror planning agent output to terminal.",
359
+ )
360
+
361
+
273
362
  def build_parser() -> argparse.ArgumentParser:
274
363
  parser = argparse.ArgumentParser(
275
364
  description="Continuous refactoring CLI for AI coding agents.",
276
365
  )
366
+ parser.add_argument(
367
+ "--version",
368
+ action="version",
369
+ version=_version_banner(),
370
+ )
277
371
  subparsers = parser.add_subparsers(dest="command")
278
372
 
279
373
  _add_init_parser(subparsers)
@@ -285,6 +379,7 @@ def build_parser() -> argparse.ArgumentParser:
285
379
  help="Verify and upgrade global configuration.",
286
380
  )
287
381
  upgrade_parser.set_defaults(handler=_handle_upgrade)
382
+ _add_migration_parser(subparsers)
288
383
  _add_review_parser(subparsers)
289
384
 
290
385
  return parser
@@ -312,36 +407,20 @@ def _handle_init(args: argparse.Namespace) -> None:
312
407
 
313
408
  try:
314
409
  if in_repo_taste_arg is not None:
315
- repo_taste_resolved = (path / in_repo_taste_arg).resolve()
316
- if not repo_taste_resolved.is_relative_to(path):
317
- print(
318
- f"Error: --in-repo-taste must be inside the repo: {in_repo_taste_arg}",
319
- file=sys.stderr,
320
- )
321
- raise SystemExit(2)
322
- if repo_taste_resolved.exists() and not repo_taste_resolved.is_file():
323
- print(
324
- f"Error: --in-repo-taste must point to a file: {in_repo_taste_arg}",
325
- file=sys.stderr,
326
- )
327
- raise SystemExit(2)
328
- repo_taste_relative = str(repo_taste_resolved.relative_to(path))
410
+ repo_taste_resolved, repo_taste_relative = _resolve_repo_relative_arg(
411
+ repo_root=path,
412
+ value=in_repo_taste_arg,
413
+ flag="--in-repo-taste",
414
+ expected_kind="file",
415
+ )
329
416
 
330
417
  if live_dir_arg is not None:
331
- resolved_live = (path / live_dir_arg).resolve()
332
- if not resolved_live.is_relative_to(path):
333
- print(
334
- f"Error: --live-migrations-dir must be inside the repo: {live_dir_arg}",
335
- file=sys.stderr,
336
- )
337
- raise SystemExit(2)
338
- if resolved_live.exists() and not resolved_live.is_dir():
339
- print(
340
- f"Error: --live-migrations-dir must point to a directory: {live_dir_arg}",
341
- file=sys.stderr,
342
- )
343
- raise SystemExit(2)
344
- live_dir_relative = str(resolved_live.relative_to(path))
418
+ resolved_live, live_dir_relative = _resolve_repo_relative_arg(
419
+ repo_root=path,
420
+ value=live_dir_arg,
421
+ flag="--live-migrations-dir",
422
+ expected_kind="directory",
423
+ )
345
424
 
346
425
  project = register_project(path)
347
426
  if repo_taste_relative is not None:
@@ -380,6 +459,36 @@ def _handle_init(args: argparse.Namespace) -> None:
380
459
  print(f"Live migrations dir: {resolved_live}")
381
460
 
382
461
 
462
+ def _resolve_repo_relative_arg(
463
+ *,
464
+ repo_root: Path,
465
+ value: Path,
466
+ flag: str,
467
+ expected_kind: Literal["file", "directory"],
468
+ ) -> tuple[Path, str]:
469
+ resolved = (repo_root / value).resolve()
470
+ if not resolved.is_relative_to(repo_root):
471
+ print(
472
+ f"Error: {flag} must be inside the repo: {value}",
473
+ file=sys.stderr,
474
+ )
475
+ raise SystemExit(2)
476
+ if resolved.exists():
477
+ if expected_kind == "file" and not resolved.is_file():
478
+ print(
479
+ f"Error: {flag} must point to a file: {value}",
480
+ file=sys.stderr,
481
+ )
482
+ raise SystemExit(2)
483
+ if expected_kind == "directory" and not resolved.is_dir():
484
+ print(
485
+ f"Error: {flag} must point to a directory: {value}",
486
+ file=sys.stderr,
487
+ )
488
+ raise SystemExit(2)
489
+ return resolved, str(resolved.relative_to(repo_root))
490
+
491
+
383
492
  def _configure_repo_taste(
384
493
  *,
385
494
  current: Path,