Test-gated refactor loop that drives codex and claude CLIs through small, validated commits. Zero runtime deps, stdlib only. Dog-foods itself through live migrations. User-facing docs live in README.md; this file is the mutator's contract.
Every agent working in this repo MUST:
- Update
AGENTS.mdin the same commit as any change that contradicts a statement here. Stale is worse than missing. - Remove entries describing load-bearing subtleties that get refactored away.
- Add entries for new load-bearing subtleties, new migrations, new commands, new gotchas.
Treat AGENTS.md as part of the codebase's invariants, not documentation. A drift between this file and the code is a shipped bug.
- Install:
uv sync && uv pip install -e . - Test all:
uv run pytest - Test one:
uv run pytest tests/test_x.py::test_name - Entry:
continuous-refactoring --help/continuous-refactoring --version(orpython -m continuous_refactoring) - Init:
continuous-refactoring init [--path PATH] [--live-migrations-dir DIR] [--in-repo-taste [PATH]] [--force] - Taste:
continuous-refactoring taste [--global] [--interview|--upgrade|--refine] [--with codex|claude --model <model>] [--force]Active taste agent modes require--withand--model; the taste agent always runs at fixedmediumeffort. - Run once:
continuous-refactoring run-once --with codex|claude --model <model> [common targeting/validation flags] - Run loop:
continuous-refactoring run --with codex|claude --model <model> [--max-attempts N] [--max-refactors N] [--focus-on-live-migrations] [--commit-message-prefix TEXT] [--max-consecutive-failures N] [--sleep N]Requires targeting flags or--scope-instructionunless--focus-on-live-migrations;--max-refactorsis required unless using--targetsor--focus-on-live-migrations. - Upgrade config:
continuous-refactoring upgrade - Inspect migrations:
continuous-refactoring migration list [--status planning|ready|in-progress|skipped|done] [--awaiting-review] [--no-headers]/continuous-refactoring migration doctor <slug-or-path>/continuous-refactoring migration doctor --all - Review migrations:
continuous-refactoring migration review <slug-or-path> --with codex|claude --model <model> - Refine migration planning:
continuous-refactoring migration refine <slug-or-path> (--message <text>|--file <path>) --with codex|claude --model <model> [--show-agent-logs]
No lint, no typecheck, no formatter, no pre-commit. GitHub Actions Test
runs uv run pytest. Pytest is the only code gate. GitHub Actions
PR Title gates pull request title policy.
src/continuous_refactoring/— flat module layout, no subpackagestests/— flat,test_<module>.pyper source module plus behavior bundles (test_e2e.py,test_run.py,test_run_once.py)<live-migrations-dir>/— configurable live multi-phase plans (dog-food output); a checkout may not havemigrations/.scratchpad/— ephemeral agent state, gitignored- Durable user state:
~/.local/share/continuous-refactoring/…(XDG)
- Target — the refactoring unit the driver is working on this iteration: a JSONL target, one matched tracked file, literal path set, random tracked-file bundle, or fallback scoped prompt.
- Taste — project or global prose that shapes every prompt. Project taste is XDG by default, or a repo-relative path stored as
repo_taste_pathafterinit --in-repo-taste [PATH]. - Scope expansion — deciding the set of files edited together with the target (
scope_expansion.py). - Classifier / routing — chooses a target route:
cohesive-cleanupvsneeds-plan(routing.py). - Migration — a multi-phase plan living under
<live-migrations-dir>/<slug>/. - Visible migration directory — direct child migration dir that is not hidden, dotted, symlinked, or internal/transactional; enumerate through
iter_visible_migration_dirs(). - Consistency finding — structured migration integrity result with shared
info | warning | errorseverity andplanning-snapshot | ready-publish | execution-gate | doctormode. - Planning state — durable resume/audit cursor at
<migration>/.planning/state.json; it records accepted planning steps and their repo-relative stage outputs. - Planning stage output — accepted planning stdout stored under
<migration>/.planning/stages/<step>.stdout.md; repeated accepted steps use suffixed refs such as<step>-2.stdout.md. Failed current-step output stays in run artifacts only. - Planning feedback — explicit user refinement feedback recorded in
.planning/state.json; it reuses thereviseplanning step and is published only through staged planning/refine transactions. - Planning workspace — off-live candidate migration snapshot built under project state, then copied to a live-dir transaction before publish.
- Phase — one step of a migration; state transitions in
phases.py. - Precondition — what must already be true before a phase may execute; stored on each manifest phase as
precondition. - Definition of Done — what must be true for a phase to count as completed; written in each phase markdown doc under
## Definition of Done. - Phase cursor —
manifest.current_phasestores the active phasename; human-facing references use the relative phase file path; phase names must be unique within a migration. - Wake-up rule — schedule for when the driver reconsiders an idle target.
- Eligibility cooldown —
manifest.cooldown_untilgates re-checks after a migration was deferred or blocked;last_touchrecords activity only. - Settle protocol —
<file>.done+ sha256 handshake confirming an interactive agent is finished. - Status block — the agent-emitted final-message block parsed by
decisions.py. - Call role — prompt slot recorded in artifacts, including
classify,refactor, dotted planning roles such asplanning.<step>,planning.state,planning.publish, and phase roles such asphase.ready-checkorphase.execute. - Effort budget — shared nominal tiers
low < medium < high < xhigh;--default-effortis the normal call effort,--max-allowed-effortcaps target overrides and phase escalation. - Failure snapshot — per-attempt failure record at
…/projects/<uuid>/failures/<run_id>-attempt-NNN-retry-NN-<role>.md. One file per failed attempt; sort to find the latest.
from __future__ import annotationsat the top of every src file, after an optional module docstring.- Frozen dataclasses for value types;
Literal[…]for state machines. - Explicit
__all__per module. - Full-path imports (
from continuous_refactoring.X import Y). Never relative. - Boundary error type:
ContinuousRefactorError(RuntimeError). - Subprocess:
subprocess.run(..., check=False, capture_output=True); branch on the returnedCompletedProcess. - Atomic writes: write to a temp file, then
os.replace. - Comments only when load-bearing. Clean abstractions beat prose.
- Zero runtime dependencies — stdlib only.
src/continuous_refactoring/__init__.py walks every public submodule's __all__ and asserts no duplicate symbols. A collision breaks package import at boot. Any rename or addition to the package-root surface needs a project-wide check before commit. Internal modules such as migration_manifest_codec.py, review_cli.py, and effort.py stay out of the package-root exports.
pytest>=8.0only. No coverage, no hypothesis, no custom pytest markers;pytest.mark.parametrizeis normal.- Monkeypatching is idiomatic — not a smell.
tests/conftest.pyprovides:write_fake_codex— drops a Python stub forcodexon PATH. Controlled byFAKE_CODEX_STDOUT,FAKE_CODEX_STDERR,FAKE_CODEX_LAST_MESSAGE,FAKE_CODEX_TOUCH_FILE,FAKE_CODEX_TOUCH_CONTENT,FAKE_CODEX_EXIT_CODE._prepare_run_env—git init -b mainintmp_path; redirectsTMPDIRandXDG_DATA_HOMEto the sandbox.make_run_once_args/make_run_loop_args— build argparseNamespaces so tests bypass the CLI layer.
- Claude stream-json parsing is covered with recorded NDJSON at
tests/fixtures/claude_stream_json/selection.stdout.log.
No dedicated loop.py migration is active. Keep broad structural loop.py
edits behind a live migration plan; narrow edits are allowed only when an
active phase explicitly names loop.py in scope.
- Settle protocol (
agent.py:253-290,:413-475) — interactive agents must write<file>.donecontainingsha256:<hex>matching the content's digest. Driver force-stops once digests match forsettle_window_seconds. - Codex terminal reset (
agent.py:342-372) — raw ANSI reset after force-stop. Codex leaves the tty corrupt. Do not remove. - Claude stream-json unwrap (
agent.py:68-102) — NDJSON; prefer the lastresultevent, else join assistant text blocks, else return raw. - Watchdog (
agent.py:549-665) — silent ≥5 min → SIGTERM → SIGKILL →ContinuousRefactorError. - Driver owns commits (
refactor_attempts.py:_finalize_commit(), called fromloop.py) — if an agent commits mid-attempt, driver doesgit reset --soft head_beforeand re-commits with its own message. - Migration scheduling split (
migrations.py,loop.py,phases.py) —last_touchis activity bookkeeping, not the 6-hour retry gate. Deferred/blocked migrations setcooldown_until; successful phase completion clears deferral markers so the next ready phase can run immediately. - Migration tick deferral writes (
migration_tick.py) — ready-check deferrals are queued while scanning candidates and saved only when the tick finds no executable phase or blocks for human review. Do not save a deferred manifest before checking later candidates; that dirties the worktree and can make ready-checks reject runnable phases. - Migration visibility + consistency gate (
migration_consistency.py,migration_tick.py,loop.py,review_cli.py) — candidate scans useiter_visible_migration_dirs()so hidden/dotted/internal/symlink dirs are invisible to tick/list/review commands. Before ready-check,execution-gateconsistency errors block phase execution;info/warningnever block. - Manifest codec boundary (
migration_manifest_codec.py,migrations.py) — codec owns legacyready_when, legacy integercurrent_phase, duplicate phase-name rejection, and saved JSON formatting.load_manifest()/save_manifest()own filesystem and JSON boundary errors. - Planning state codec boundary (
planning_state.py,planning.py) —.planning/state.jsonis valid only when completed steps replay through the branching planning graph tonext_step; recorded outputs must be repo-relative files inside the migration directory. User refinement feedback is durable state, and append-onlyrevision_base_step_countsanchors let refinement reuserevisefrom review cursors or unexecuted ready terminal decisions; legacyrevision_base_step_countdecodes as one anchor. Persist accepted step stdout after the step is validated; do not add durable fields for failed current-step output. - Planning publish transaction (
planning_publish.py) — publish copies the complete workspace snapshot to__transactions__/<token>/staged, validates it, checks same-device andbase_snapshot_id, moves live torollback, moves staged live, validates live, then deletes rollback. On post-rollback failure, move bad live tofailedbefore restoring rollback. Transaction directories are invisible to scheduling/list candidates but visible tomigration doctor --all. Do not bypass the lock or dirty-live check. - One-step planning engine (
planning.py) — product planning entry points callrun_next_planning_step()so one action runs exactlyPlanningState.next_step, records accepted stdout/state in an off-live workspace, and publishes throughplanning_publish.py. Failed current-step output is never durable resume input.run_planningis intentionally not package-exported. - Planning resume scheduling (
migration_tick.py,loop.py,routing_pipeline.py) — normal automation runs one eligiblestatus: planningstep before ready/in-progress phase ticks and before source-target routing. Missing or invalid.planning/state.jsonblocks automation with planning failure evidence;status: planningmust never enter phase ready-check or phase execution. - Focused planning reselection (
loop.py,migration_tick.py) — focused mode tracks planning migrations abandoned withnew-targetonly in memory for the current run, skips them while another planning or phase candidate is eligible, and retries them only when no alternative remains. Do not persist this ascooldown_until; planning step failure is not a durable readiness deferral. - Review CLI boundary (
cli.py,review_cli.py) —cli.pyowns parser wiring; staged migration review internals live inreview_cli.py, publish only throughplanning_publish.py, and stay internal/out of package-root_SUBMODULES. Review mutation is only exposed throughmigration review. - Migration CLI boundary (
cli.py,migration_cli.py) —cli.pyowns parser wiring only;migration_cli.pyowns namespace dispatch, read-only list/doctor behavior, and the contained slug/path resolver used by mutation commands. Mutating subcommands delegate their internals to focused modules such asreview_cli.pyor the planning refine entry point. Resolver targets must stay direct visible children of the configured live migrations root and reject symlink, outside, parent-traversal, and ambiguous paths. - Human-review gating (
planning.py,migration_tick.py,review_cli.py) — migrations withawaiting_human_review=truemust be invisible to automated migration ticks/ready-checks until canonicalmigration reviewclears the flag through staged publish.migration refinemay reopen an unexecuted ready migration to planning, but it is user feedback, not review approval. - Migration terminology split (
migrations.py,planning.py,prompts.py) — manifestpreconditiongates phase start; phase markdown## Definition of Donegoverns completion. - Run-level baseline validation (
loop.py) —run-once,run, and--focus-on-live-migrationsrun the configured validation command after the clean-worktree check and before routing/refactoring. A red baseline stops asbaseline_failed, not migration human review. - Phase execution validation gate (
phases.py,prompts.py,loop.py) — a migration phase is complete only after host-side full validation passes.execute_phase()retries validation-red attempts fromhead_beforeup to the effective--max-attemptsbudget, and the phase prompt must include the literal configured validation command plus the phase file's Definition of Done as the completion contract. - Effort budgeting (
effort.py,loop.py,migration_tick.py,planning.py) —run/run-oncedefault to--default-effort lowand--max-allowed-effort xhigh; there is no--effortalias on those commands. Targeteffort-overridechanges that target's default but is still capped. Migrationrequired_effortabove the cap defers the phase without failing the run. Manualmigration reviewandmigration refineoperations use fixed internalhigheffort. Taste agent actions do not accept--effort; they always use fixedmediumeffort. - Taste injection — every prompt includes a
## Tastesection.tests/test_prompts.pyenforces this via_TASTE_INJECTED_PROMPTS. Do not drop it. - Taste read boundary (
config.py,cli.py,loop.py) —load_taste()translates unreadable project/global taste reads intoContinuousRefactorError; CLI stale-taste checks and loop taste loading must treat that boundary failure as non-fatal and skip/fall back instead of leaking rawOSError/PermissionError. - Repo-local taste routing (
config.py,cli.py) —ProjectEntry.repo_taste_pathis stored repo-relative in the XDG manifest and resolved throughresolve_project_taste_path(). Keepinit,taste, stale warnings, and run prompt loading on that helper so the active taste path does not drift. - Init reconfiguration moves state (
cli.py) — re-runninginitwith--in-repo-tasteor--live-migrations-dirmoves existing taste/live migration state before updating manifest pointers; destination conflicts require--force.
- Targeting is first-match-wins across
--targets > --globs > --extensions > --paths. Multiple flags silently use the highest. runrequires targeting or--scope-instruction, and also requires--max-refactorsunless--targetsor--focus-on-live-migrationsis set.--max-attempts 0means unlimited, not zero. A WARN fires at startup.run-onceandrunboth create local commits only; the driver never publishes branch updates.
- Durable:
~/.local/share/continuous-refactoring/manifest.json,projects/<uuid>/taste.mdunless repo-local taste is configured,…/failures/<snapshot>.md,global/taste.md. - Per-run (ephemeral):
$TMPDIR/continuous-refactoring/<run-id>/summary.json,events.jsonl,run.log.
- All work happens off
main. - Changes land by PR. Do not push directly to
main. - PR titles must match:
<type>(optional-scope)!: Capitalized Title Text, where type isfeat|chore|fix|refactor|migrationand scope/!are optional. - Prefixes and scopes are lowercase. Title text starts capitalized.
- Release Please owns version bumps,
CHANGELOG.md, tags, and GitHub releases. - Release-driving commits follow Conventional Commits:
feat:is minor,fix:is patch, and breaking changes require!or aBREAKING CHANGE:footer. - Non-release cleanup uses
chore:orrefactor:unless it truly changes user behavior. - Commit subject prefix:
continuous refactor: <path>(dominant pattern). - Driver-generated commit bodies include a concise
Why:section and validation context when available.
- No re-export shims on refactor.
- No new runtime dependencies.
- No relative imports.
- Do not drop
## Tastefrom prompts. - Do not simplify the ANSI terminal reset in
agent.py. - Do not structurally refactor
loop.pywithout an active migration plan. - Do not amend commits in the driver path (driver uses
git reset --soft). - Driver never creates, switches, or deletes branches. The user controls branching.
- Do not hand-edit release versions except for Release Please bootstrap or emergency repair.
README.md— feature tour and CLI reference.<live-migrations-dir>/<live>/plan.md— active structural work, when this checkout has a live migrations dir.src/continuous_refactoring/__init__.py— public surface and uniqueness check.tests/conftest.py— test env patterns and fake agents.src/continuous_refactoring/prompts.py— prompt templates and taste injection.