Skip to content

ctx v0.8.0: The Architecture Release

ctx

  • 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.go files. And every user-facing string was a hardcoded English literal buried in a .go file.

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:

cmd := &cobra.Command{
    Use:   "pad",
    Short: "Encrypted scratchpad",

Now it looks like this:

cmd := &cobra.Command{
    Use:   cmdUse.UsePad,
    Short: desc.Command(cmdUse.DescKeyPad),

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 TextDescKey resolves 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 sync mirrors MEMORY.md into .context/memory/
  • ctx memory diff shows what's diverged
  • ctx memory import promotes auto-memory entries into proper decisions, learnings, or conventions *A check-memory-drift hook 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/cobra and gopkg.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.go files: the broken-window pattern: An agent sees err.go in a package, adds another error constructor. Now err.go has 30 constructors and nobody knows which are used. Consolidated into 22 domain files in internal/err/.
  • nolint:errcheck directives: 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/ or write/ 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:

  1. The Attention Budget: why context windows are a scarce resource
  2. Before Context Windows, We Had Bouncers: the IRC lineage of context engineering
  3. Context as Infrastructure: treating context as persistent files, not ephemeral prompts
  4. When a System Starts Explaining Itself: the journal as a first-class artifact
  5. The Homework Problem: what happens when AI writes code but humans own the outcome
  6. Agent Memory Is Infrastructure: L2 memory vs L3 project knowledge
  7. The Architecture Release (this post): what it looks like when you redesign the internals
  8. 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