ctx v0.8.0: The Architecture Release¶

- You can't localize what you haven't externalized.
- You can't integrate what you haven't separated.
- You can't scale what you haven't structured.
Jose Alekhinne / March 23, 2026
The Starting Point¶
This release matters if:
- you build tools that AI agents modify daily;
- you care about long-lived project memory that survives sessions;
- you've felt codebases drift faster than you can reason about them.
v0.6.0 shipped the plugin architecture: hooks and skills as a
Claude Code plugin, shell scripts replaced by Go subcommands.
The binary worked. The tests passed. The docs were comprehensive.
But inside, the codebase was held together by convention and goodwill:
- Command packages mixed Cobra wiring with business logic.
- Output functions lived next to the code that computed what to output.
- Error constructors were scattered across per-package
err.gofiles. And every user-facing string was a hardcoded English literal buried in a.gofile.
v0.8.0 is what happens when you stop adding features and start
asking: "What would this codebase look like if we designed it
today?"
374 commits. 1,708 Go files touched. 80,281 lines added, 21,723 removed. Five weeks of restructuring.
The Three Pillars¶
1. Every Package Gets a Taxonomy¶
Before v0.8.0, a CLI package like internal/cli/pad/ was a flat
directory. cmd.go created the cobra command, run.go executed
it, and helper functions accumulated at the bottom of whichever
file seemed closest.
Now every CLI package follows the same structure:
internal/cli/pad/
parent.go # cobra command wiring, nothing else
cmd/root/
cmd.go # subcommand registration
run.go # execution logic
core/
types.go # all structs in one file
store.go # domain logic
encrypt.go # domain logic
The rule is simple: cmd/ directories contain only cmd.go and
run.go. Helpers belong in core/. Output belongs in
internal/write/pad/. Types shared across packages belong in
internal/entity/.
24 CLI packages were restructured this way.
- Not incrementally;
- not "as we touch them."
- All of them, in one sustained push.
2. Every String Gets a Key¶
The second pillar was string externalization.
Before v0.8.0, a command description looked like this:
Now it looks like this:
Every command description, flag description, and user-facing text string is now a YAML lookup.
- 105 command descriptions in
commands.yaml. - All flag descriptions in
flags.yaml. - 879 text constants verified by an exhaustive test that checks every single
TextDescKeyresolves to a non-empty YAML value.
Why?
Not because we're shipping a French translation tomorrow.
Because externalization forces you to find every string. And finding them is the hard part. The translation is mechanical; the archaeology is not.
Along the way, we eliminated hardcoded pluralization (replacing
format.Pluralize() with explicit singular/plural key pairs),
replaced Unicode escape sequences with named config/token
constants, and normalized every import alias to camelCase.
3. Everything Gets a Protocol¶
The third pillar was the MCP server. Model Context Protocol allows
any MCP-compatible AI tool (not just Claude Code) to
read and write .context/ files through a standard JSON-RPC 2.0
interface.
v0.2 of the server ships with:
- 8 tools: add entries, recall sessions, check status, detect drift, compact context, subscribe to changes
- 4 prompts: agent context packet, constitution review, tasks review, and a getting-started guide
- Resource subscriptions: clients get notified when context files change
- Session state: the server tracks which client is connected and what they've accessed
In practice, this means an agent in Cursor can add a decision to
.context/DECISIONS.md and an agent in Claude Code can
immediately consume it; no glue code, no copy-paste, no
tool-specific integration.
The server was also the first package to go through the full
taxonomy treatment: mcp/server/ for protocol dispatch,
mcp/handler/ for domain logic, mcp/entity/ for shared types,
mcp/config/ split into 9 sub-packages.
The Memory Bridge¶
While the architecture was being restructured, a quieter feature
landed: ctx memory sync.
Claude Code has its own auto-memory system. It writes observations
to MEMORY.md in ~/.claude/projects/. These observations are
useful but ephemeral: tied to a single tool, invisible to the
codebase, lost when you switch machines.
The memory bridge connects these two worlds:
ctx memory syncmirrors MEMORY.md into.context/memory/ctx memory diffshows what's divergedctx memory importpromotes auto-memory entries into proper decisions, learnings, or conventions *Acheck-memory-drifthook nudges when MEMORY.md changes
Memory Requires ctx
Claude Code's auto-memory validates the need for persistent context.
ctx doesn't compete with it; ctx absorbs it as an input source and
promotes the valuable parts into structured, version-controlled
project knowledge.
What Got Deleted¶
The best measure of a refactoring isn't what you added. It's what you removed.
fatih/color: the sole third-party UI dependency. Replaced by Unicode symbols. ctx now has exactly two direct dependencies:spf13/cobraandgopkg.in/yaml.v3.format.Pluralize(): a function that tried to pluralize English words at runtime. Replaced by explicit singular/plural YAML key pairs. No more guessing whether "entry" becomes "entries" or "entrys."- Legacy key migration:
MigrateKeyFile()had 5 callers, full test coverage, and zero users. It existed because we once moved the encryption key path. Nobody was migrating from that era anymore. Deleted. - Per-package
err.gofiles: the broken-window pattern: An agent seeserr.goin a package, adds another error constructor. Nowerr.gohas 30 constructors and nobody knows which are used. Consolidated into 22 domain files ininternal/err/. nolint:errcheckdirectives: every single one, replaced by explicit error handling. In tests:t.Fatal(err)for setup,_ = os.Chdir(orig)for cleanup. In production:defer func() { _ = f.Close() }()for best-effort close.
Before and After¶
| Aspect | v0.6.0 | v0.8.0 |
|---|---|---|
| CLI package structure | Flat files | cmd/ + core/ taxonomy |
| Command descriptions | Hardcoded Go strings | YAML with DescKey lookup |
| Output functions | Mixed into core logic | Isolated in write/ packages |
| Cross-cutting types | Duplicated per-package | Consolidated in entity/ |
| Error constructors | Per-package err.go |
22 domain files in internal/err/ |
| Direct dependencies | 3 (cobra, yaml, color) |
2 (cobra, yaml) |
| AI tool integration | Claude Code only | Any MCP client |
| Agent memory | Manual copy-paste | ctx memory sync/import/diff |
| Package documentation | 75 packages missing doc.go |
All packages documented |
| Import aliases | Inconsistent (cflag, cFlag) |
Standardized camelCase |
Making AI-Assisted Development Easier¶
This restructuring wasn't just for humans. It makes the codebase legible to the machines that modify it.
Named constants are searchable landmarks: When an agent sees
cmdUse.DescKeyPad, it can grep for the definition, follow the
chain to the YAML file, and understand the full lookup path. When
it sees "Encrypted scratchpad" hardcoded in a .go file, it
has no way to know that same string also lives in a YAML file, a
test, and a help screen. Constants give the LLM a graph to
traverse; literals give it a guess to make.
Small, domain-scoped packages reduce hallucination: An agent
loading internal/cli/pad/core/store.go gets 50 lines of focused
logic with a clear responsibility boundary. Loading a 500-line
monolith means the agent has to infer which parts are relevant,
and it guesses wrong more often than you'd expect. Smaller files
with descriptive names act as a natural retrieval system: the
agent finds the right code by finding the right file, not by
scanning everything and hoping.
Taxonomy prevents duplication: When there's a write/pad/
package, the agent knows where output functions belong. When
there's an internal/err/pad.go, it knows where error
constructors go. Without these conventions, agents reliably
create new helpers in whatever file they happen to be editing,
producing the exact drift that prompted this consolidation in
the first place.
The difference is concrete:
Before: an agent adds a helper function in whatever file it's editing. Next session, a different agent adds the same helper in a different file.
After: the agent finds
core/orwrite/and places it correctly. The next agent finds it there.
doc.go files are agent onboarding: Each package's doc.go
is a one-paragraph explanation of what the package does and why
it exists. An agent loading a package reads this first. 75
packages were missing this context; now none are. The difference
is measurable: fewer "I'll create a helper function here" moments
when the agent understands that the helper already exists two
packages over.
The irony is that AI agents were both the cause and the beneficiary of this restructuring. They created the drift by building fast without consolidating. Now the structure they work within makes it harder to drift again. The taxonomy is self-reinforcing: the more consistent the codebase, the more consistently agents modify it.
Key Commits¶
| Commit | Change |
|---|---|
| ff6cf19e | Restructure all CLI packages into cmd/root + core taxonomy |
| d295e49c | Externalize command descriptions to embedded YAML |
| 0fcbd11c | Remove fatih/color, centralize constants |
| cb12a85a | MCP v0.2: tools, prompts, session state, subscriptions |
| ea196d00 | Memory bridge: sync, import, diff, journal enrichment |
| 3bcf077d | Split text.yaml into 6 domain files |
| 3a0bae86 | Split internal/err into 22 domain files |
| 8bd793b1 | Extract internal/entry for shared domain API |
| 5b32e435 | Add doc.go to all 75 packages |
| a82af4bc | Standardize import aliases: camelCase, Yoda-style |
Lessons Learned¶
Agents are surprisingly good at mechanical refactoring; they are
surprisingly bad at knowing when to stop: The cmd/ + core/
restructuring was largely agent-driven. But agents reliably
introduce gofmt issues during bulk renames, rename functions
beyond their scope, and create new files without deleting old ones.
Every agent-driven refactoring session needed a human audit pass.
Externalization is archaeology: The hard part of moving strings
to YAML wasn't writing YAML. It was finding 879 strings scattered
across 1,500 Go files. Each one required a judgment call: is this
user-facing? Is this a format pattern? Is this a constant that
belongs in config/ instead?
Delete legacy code instead of maintaining it: MigrateKeyFile
had test coverage. It had callers. It had documentation. It had
zero users. We maintained it for weeks before realizing that the
migration window had closed months ago.
Convention enforcement needs mechanical verification: Writing
"use camelCase aliases" in CONVENTIONS.md doesn't prevent cflag
from appearing in the next commit. The lint-drift script catches
what humans forget; the planned AST-based audit tests will catch
what the lint-drift script can't express.
What's Next¶
v0.8.0 wasn't about features. It was about making future features inevitable. The next cycle focuses on what the foundation enables:
- AST-based audit tests: replace shell grep with Go tests that
understand types, call sites, and import graphs
(spec:
specs/ast-audit-tests.md) - Localization: with every string in YAML, the path to multi-language support is mechanical
- MCP v0.3: expand tool coverage, add prompt templates for common workflows
- Memory publish: bidirectional sync that pushes curated
.context/knowledge back into Claude Code's MEMORY.md
The architecture is ready. The strings are externalized. The protocol is standard. Now it's about what you build on top.
The Arc¶
This is the seventh post in the ctx blog series. The arc so far:
- 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 (this post): what it looks like when you redesign the internals
- We Broke the 3:1 Rule: the consolidation debt behind this release
See also: Agent Memory Is Infrastructure: the memory bridge feature in this release is the first implementation of the L2-to-L3 promotion pipeline described in that post.
See also: We Broke the 3:1 Rule: the companion post explaining why this release needed 181 consolidation commits and 18 days of cleanup.
Systems don't scale because they grow. They scale because they stop drifting.
Full changelog: v0.6.0...v0.8.0