We Broke the 3:1 Rule¶

The best time to consolidate was after every third session. The second best time is now.
Jose Alekhinne / March 23, 2026
The rule was simple: three feature sessions, then one consolidation session.
The Architecture Release shows the result: This post shows the cost.
The Rule We Wrote¶
In The 3:1 Ratio, I documented a rhythm that worked
during ctx's first month: three feature sessions, then one
consolidation session. The evidence was clear. The rule was simple.
The math checked out.
And then we ignored it for five weeks.
What Happened¶
After v0.6.0 shipped on February 16, the feature pipeline was
irresistible. The MCP server spec was ready. The memory bridge
design was done. Webhook notifications had been deferred twice.
The VS Code extension needed 15 new commands. The sysinfo package
was overdue...
Each feature was important. Each feature was "just one more session." Each feature pushed the consolidation session one day further out.
The git history tells the story in two numbers:
| Phase | Dates | Commits | Duration |
|---|---|---|---|
| Feature run | Feb 16 - Mar 5 | 198 | 17 days |
| Consolidation run | Mar 5 - Mar 23 | 181 | 18 days |
198 feature commits before a single consolidation commit. If the 3:1 rule says consolidate every 4th session, we consolidated after the 66th.
The Actual Ratio
The ratio wasn't 3:1. It was 1:1.
We spent as much time cleaning up as we did building.
The consolidation run took 18 days: longer than the feature run itself.
What Compounded¶
The 3:1 post warned about compounding. Here is what compounding actually looked like at scale.
The String Problem¶
By March 5, there were 879 user-facing strings scattered across 1,500 Go files. Not because anyone decided to put them there. Because each feature session added 10-15 strings, and nobody stopped to ask "should these be in YAML?"
Finding them all took longer than externalizing them. The archaeology was the cost, not the migration.
The Taxonomy Problem¶
24 CLI packages had accumulated their own conventions. Some put
cobra wiring in cmd.go. Some put it in root.go. Some mixed
business logic with command registration. Some had helpers at the
bottom of run.go. Some had separate util.go files.
At peak drift, adding a feature meant first figuring out which of three competing patterns this package was using.
Restructuring one package into cmd/root/ + core/ took 15
minutes. Restructuring 24 of them took days, because each one
had slightly different conventions to untangle.
If we had restructured every 4th package as it was built, the taxonomy would have emerged naturally.
The Type Problem¶
Cross-cutting types like SessionInfo, ExportParams, and
ParserResult were defined in whichever package first needed
them. By March 5, the same types were imported through 3-4
layers of indirection, causing import cycles that required
internal/entity to break.
The entity package extracted 30+ types from 12 packages. Each extraction risked breaking imports in packages we hadn't touched in weeks.
The Error Problem¶
Per-package err.go files had grown into a broken-window
pattern:
An agent sees err.go in a package, adds another error
constructor. By March 5, there were error constructors scattered
across 22 packages with no central inventory. The consolidation
into internal/err/ domain files required tracing every error
through every caller.
The Output Problem¶
Output functions (cmd.Println, fmt.Fprintf) were mixed into
business logic. When we decided output belongs in write/
packages, we had to extract functions from every CLI package.
The Phase WC baseline commit (4ec5999) marks the starting
point of this migration. 181 commits later, it was done.
The Compound Interest Math¶
The 3:1 rule assumes consolidation sessions of roughly equal size to feature sessions. Here is what happens when you skip:
| Consolidation cadence | Feature sessions | Consolidation sessions | Total |
|---|---|---|---|
| Every 4th (3:1) | 48 | 16 | 64 |
| Every 10th | 48 | ~8 | ~56 |
| Never (what we did) | 198 commits | 181 commits | 379 |
The Takeaway
You don't save consolidation work by skipping it:
You increase its cost.
Skipping consolidation doesn't save time: It borrows it.
The interest rate is nonlinear: The longer you wait, the more each individual fix costs, because fixes interact with other unfixed drift.
Renaming a constant in week 2 touches 3 files. Renaming it in week 6 touches 15, because five features built on the original name.
What Consolidation Actually Looked Like¶
The 18-day consolidation run wasn't one sweep. It was a sequence of targeted campaigns, each revealing the next:
Week 1 (Mar 5-11): Error consolidation and write/ migration.
Move output functions out of core/. Split monolithic errors.go
into 22 domain files. Remove fatih/color. This exposed the scope
of the string problem.
Week 2 (Mar 12-18): String externalization. Create
commands.yaml, flags.yaml, split text.yaml into 6 domain
files. Add 879 DescKey/TextDescKey constants. Build exhaustive
test. Normalize all import aliases to camelCase. This exposed the
taxonomy problem.
Week 3 (Mar 19-23): Taxonomy enforcement. Singularize command
directories. Add doc.go to all 75 packages. Standardize import
aliases project-wide. Fix lint-drift false positives. This was
the "polish" phase, except it took 5 days because the
inconsistencies had compounded across 461 packages.
Each week's work would have been a single session if done incrementally.
Lessons (Again)¶
The 3:1 post listed the symptoms of drift. This post adds the consequences of ignoring them:
Consolidation is not optional; it is deferred or paid: We didn't avoid 16 consolidation sessions by skipping them. We compressed them into 18 days of uninterrupted cleanup. The work was the same; the experience was worse.
Feature velocity creates an illusion of progress: 198 commits felt productive. But the codebase on March 5 was harder to modify than the codebase on February 16, despite having more features.
Speed Without Structure
Speed without structure is negative progress.
Agents amplify both building and debt: The same AI that can restructure 24 packages in a day can also create 24 slightly different conventions in a day. The 3:1 rule matters more with AI-assisted development, not less.
The consolidation baseline is the most important commit to
record: We tracked ours in TASKS.md (4ec5999). Without that
marker, knowing where to start the cleanup would have been its
own archaeological expedition.
The Updated Rule¶
The 3:1 ratio still works. We just didn't follow it. The updated practice:
-
After every 3rd feature session, schedule consolidation. Not "when it feels right." Not "when things get bad." After the 3rd session.
-
Record the baseline commit. When you start a consolidation phase, write down the commit hash. It marks where the debt starts.
-
Run
make auditbefore feature work. If it doesn't pass, you are already in debt. Consolidate before building. -
Treat consolidation as a feature. It gets a branch. It gets commits. It gets a blog post. It is not overhead; it is the work that makes the next three features possible.
The Rule
The 3:1 ratio is not aspirational: It is structural.
Ignore consolidation, and the system will schedule it for you.
The Arc¶
This is the eighth post in the ctx blog series:
- The Attention Budget: why context windows are a scarce resource
- Before Context Windows, We Had Bouncers: the IRC lineage of context engineering
- Context as Infrastructure: treating context as persistent files, not ephemeral prompts
- When a System Starts Explaining Itself: the journal as a first-class artifact
- The Homework Problem: what happens when AI writes code but humans own the outcome
- Agent Memory Is Infrastructure: L2 memory vs L3 project knowledge
- The Architecture Release: what v0.8.0 looks like from the inside
- We Broke the 3:1 Rule (this post): what happens when you don't consolidate
See also: The 3:1 Ratio: the original observation. This post is the empirical follow-up, five weeks and 379 commits later.
Key commits marking the consolidation arc:
| Commit | Milestone |
|---|---|
4ec5999 |
Phase WC baseline (consolidation starts) |
ff6cf19e |
All CLI packages restructured into cmd/ + core/ |
d295e49c |
All command descriptions externalized to YAML |
3a0bae86 |
Error package split into 22 domain files |
0fcbd11c |
fatih/color removed; 2 dependencies remain |
5b32e435 |
doc.go added to all 75 packages |
a82af4bc |
Import aliases standardized project-wide |
692f86cd |
lint-drift false positives fixed; make audit green |