skip to content
Jobs

Refactoring the Build Engine Into Concern-Focused Modules

Turning one oversized file into a directory of per-concern modules, with the test suite as proof nothing moved but the code.

Gantry milestones
Turning one oversized file into a directory of per-concern modules, with the test suite as proof nothing moved but the code.

Gantry's build engine — the code that turns a plan into a sequence of driver runs, dispatches agents, runs the gate, and merges the result — lived in a single Rust file. It had grown large enough that no one person, and no fresh coding agent working from a clean start, could hold the whole thing in view at once; every concern, from job routing to conflict resolution, was tangled into the same file.

This job changed nothing about how that engine behaves. It moved the code, unchanged, out of the one file and into a directory of smaller files, each holding a single concern: the job model, the design logic, each of the build drivers, the gate, agent dispatch, the finish-and-merge path, and the test module. The discipline was strict — relocate each block of code verbatim, edit only what the compiler demands to make it compile, and change no logic. The proof that nothing was broken was the existing test suite: the same tests had to pass, none deleted or weakened, before and after. A structural change that carries that guarantee is worth more than a rewrite, because it makes the engine navigable without risking what already works.

Build

The work split into two milestones, ordered so the extractions needing no visibility changes came first and the cross-module ones came second — leaving each half a compilable, test-green intermediate state.

The decomposition ran on two axes at once. The broad one is concern: each cluster of methods and free functions in the original file — plan source, job model, design, the map, milestone, and sprint drivers, then gate, agent, finish, commit — became a file. The axis that determined the order is dependency. The first milestone extracted the clusters that carry no reliance on the shared build context, plus the driver method blocks; the second extracted the support clusters that the drivers call and split the tests.

That ordering is what let each fresh agent work against a compiler that stayed happy. The briefs state the reason directly: while a called method still lives in the module root, a driver in a child file can reach it with no visibility change, because Rust makes a parent's private items visible to descendant modules. Only once gate, agent, finish, and commit move out to sibling files do those calls cross a module boundary and need `pub(crate)` or `pub(super)`. The plan placed every such visibility bump in the second milestone, so the first could finish as a valid, test-green state with a still-large module root — and the second dissolved the rest.

The run never went red. No gate sent a sprint back, no review forced a re-plan, no sprint was retried. For a move-only refactor that is the expected shape, and it is the point: the difficulty was dissolved in advance by the invariant the whole job was built around. Because no code was rewritten, the unchanged test count was a precise check on every step, and compiling after each extraction gave each agent a local proof before the next block moved. The boundary that could have fought back — the cross-module visibility edits — was named ahead of time and placed where the driver files already existed to exercise it.

Feature

The build engine used to be one file with no internal addresses; the job left a directory where each concern has its own file behind a spine of re-exports that preserved every external path. That directory still stands and has absorbed later growth.

Before this job, anything that touched Gantry's build orchestration meant opening one file and searching within it; the concerns had no addresses of their own. The design this job left gives each one a file — `plan_source.rs`, `job.rs`, `design.rs`, a file per driver, `gate.rs`, `agent.rs`, `finish.rs`, `commit.rs` — with `mod.rs` reduced to shared type definitions, the dispatch entry point, and a set of `pub use` re-exports so that every path the rest of the crate imported still resolves unchanged. The tests moved into their own subdirectory mirroring the modules.

Standing in the tree today, the structure is not only intact but has become where later work landed. The `build/` directory now holds files that did not exist when this job ran — drivers for other run modes, a split-out sprint context, a `driver_map/` subdirectory — all following the one-concern-per-file arrangement this job established rather than collapsing back into a single file. Most of the files the job created are still present and still carry the bulk of the lines it introduced.

One file the job created is gone: `driver_sprint.rs`. Its flat-sprint driver methods — `build_sprint`, `repair`, `review`, `replan` — were folded into a later `context_sprint.rs` when the driver layer was reorganized around a shared context. That is drift in the naming, not a reversal of the split: the sprint concern still has its own file, with a different name and a reworked internal shape. The directory this job carved out of one monolith is the arrangement the build engine is still built on.

24 sessions
peak 147,525 · median execute 108,284 · heaviest 147,525 (02-support-clusters-and-tests / 02) context
186,060 in / 423,000 out tokens
$53.08 cost
2 x 11 milestones x sprints
71 edits
442 commands
2h 5m duration
unavailable roles
0 x 0 fixes x replans
claude harness