Extracting a Project From Its Parent: A Practical Refactoring Story

Planclave started as a feature inside Karl Code. The idea was simple: before the coding agent writes a single line, run a multi-role planning session — architect, critic, security reviewer — and produce a structured plan the agent can execute against.

That feature grew. It developed its own data models, its own API surface, its own database schema, its own review pipeline. At some point I stopped calling it "the planning feature" and started calling it "Planclave," and that naming shift was the signal. When a subsystem gets its own name, it is telling you it wants its own repository.

Extracting it was not hard in the way that distributed systems are hard. It was hard in the way that untangling Christmas lights are hard — tedious, fiddly, and requiring more patience than cleverness. Here is what the process looked like.

Building planning-first AI systems, one pipeline at a time. Get weekly deep dives into local AI, Rust, and agent orchestration — no fluff, no hype.

Step One: Map the Dependency Surface

Before moving anything, I needed to know what Planclave actually depended on from Karl Code, and what Karl Code depended on from Planclave.

I ran a simple grep across the Planclave source tree for every import that referenced the parent package:

grep -rn "from karl_code" planclave/
grep -rn "import karl_code" planclave/

The results were better than I feared and worse than I hoped. There were twelve import sites. Eight of them pulled in shared utilities — logging configuration, the retry decorator, the SQLite connection helper. Four of them imported domain types that had leaked across the boundary: a Task struct that Planclave used but did not own, and a UserProfile that it only needed for the user ID field.

The hidden cost of shared utilities is that they feel free. A shared logging module seems harmless until you try to extract the project that uses it, and suddenly "free" becomes "a dependency I have to replicate or cut."

Twelve imports is manageable. I have seen codebases where this number is in the hundreds, at which point extraction becomes a months-long project rather than an afternoon. The lesson: the earlier you extract, the easier it is.

Step Two: Cut or Copy Each Dependency

For each import, the decision was binary: cut the dependency by removing the need for it, or copy the needed code into Planclave.

Shared logging — cut. Planclave did not need Karl Code's elaborate logging configuration. A standard Python logging.getLogger(__name__) call in each module was enough. The parent's logging setup was designed for a CLI agent with verbose tool-call tracing. Planclave is a library with an API. Different needs, different logging.

Retry decorator — copied. It was thirty lines. Writing a shared library for thirty lines of retry logic would have been overengineering. I copied it into planclave/utils/retry.py with a comment noting its origin.

SQLite connection helper — cut and replaced. Planclave needed its own database anyway (it had been sharing Karl Code's database file, which was one of the main reasons to extract it). The connection helper for Planclave's database was simpler because Planclave's schema was smaller.

Leaked domain types — the interesting one. The Task struct that Planclave imported from Karl Code was used in three places, and in all three places Planclave only accessed two fields: id and title. I defined a minimal PlanInput dataclass in Planclave with exactly those fields and updated the call sites. The UserProfile import was even simpler — Planclave only needed the user ID, so I replaced it with a user_id: str parameter.

When extracting a subsystem, every leaked domain type is a coupling point. If your subsystem imports a type from the parent, ask: "Do I need the whole type, or just one or two fields?" Most of the time, the answer is one or two fields. Define a local type with exactly those.

After this step, Planclave had zero imports from Karl Code. That is the critical milestone. The code compiles in isolation. It does not run yet, but it compiles.

Step Three: The Clean Git History

I could have copied the Planclave source directory into a new repository and called it done. The history would have been lost. For a subsystem that had been under active development for months, with dozens of commits documenting design decisions, bug fixes, and architectural shifts, losing that history was not acceptable.

The tool for this is git filter-repo. It takes a source repository and a path filter, and produces a new repository containing only the history of files matching the filter:

git clone karl-code planclave-extract
cd planclave-extract
git filter-repo --subdirectory-filter planclave/

This rewrites every commit in history, keeping only changes to files inside planclave/. The result is a repository where the commit log tells the story of Planclave's development, without the noise of unrelated Karl Code commits.

There was one complication. Some commits touched both planclave/ and other parts of Karl Code in the same commit — a refactoring that updated the planning system and the agent loop together, for instance. git filter-repo handles this by keeping the commit but only applying the changes within the filtered path. The commit messages still make sense, but some commits become empty (no changes within the filtered path). I removed those with a second pass:

git filter-repo --prune-empty=always

The resulting history was clean, readable, and preserved every meaningful commit.

Step Four: Verify the Cut

After extraction, I ran two checks.

The import check. From inside the new Planclave repository, I verified that no file imported anything from Karl Code:

grep -rn "karl_code" .

Zero results. Clean.

The build check. I ran Planclave's test suite from the new repository. All tests passed. This was the moment I knew the extraction was complete — not the grep, but the tests. The grep tells you the source has no references. The tests tell you the behavior is intact.

A clean extraction is not defined by the absence of imports. It is defined by passing tests. Imports are the symptom. Behavioral independence is the property you actually want.

The reverse direction matters too. Karl Code still references Planclave — it calls Planclave's API to run planning sessions. But that is now an API dependency, not an internal coupling. Karl Code imports Planclave as a package, the same way it imports any third-party library. If Planclave disappears, Karl Code fails loudly at the import site, not silently in some deeply nested utility function.

What I Would Do Differently

The extraction took about six hours end to end. It could have been faster.

I should have extracted sooner. By the time I did it, Planclave had accumulated twelve coupling points with Karl Code. If I had extracted at the four-coupling-point stage — about two months earlier — it would have been a one-hour job. Every month of shared development added coupling points, and every coupling point added extraction time.

I should have used a boundary package earlier. A simple planclave/interface.py module that re-exported only the public API would have prevented the leaked domain types. Instead, Planclave modules imported directly from Karl Code's internals, which meant any internal refactoring in Karl Code could break Planclave.

I should have had a separate database from the start. Sharing a SQLite file was the single biggest coupling point. It meant migrations in one system could break the other, and it meant Planclave could not run on a different machine without Karl Code's database schema. Getting Planclave its own database file was the highest-value part of the extraction.

The Principle

Extracting a subsystem is not a technical challenge. It is a discipline challenge. The technical steps are straightforward: map imports, cut or copy dependencies, filter the git history, verify. What is hard is the willingness to stop adding features for a day and do the tedious, unglamorous work of cutting ties.

Subsystems rarely need to be extracted on day one. But when a subsystem gets its own name, its own mental model, and its own direction of growth, that is the signal. Extract it then. Not earlier, when the boundaries are still fuzzy. Not later, when the coupling points have multiplied. Right at the moment it becomes a thing unto itself.

Planclave is now its own project with its own repository, its own database, and its own deployment story. Karl Code is better for it — the codebase is smaller, the agent's behavior is unchanged, and the planning system can evolve without risk of breaking the coding agent. Planclave is better for it — it has its own test suite, its own version history, and the freedom to define its own interfaces.

The extraction was six hours of tedious work. It paid for itself within a week.