The Accidental Framework. The thing I built was a Go binary that… | by Ian Johnson | Jun, 2026 | MediumSitemapOpen in appSign up<br>Sign in
Medium Logo
Get app<br>Write
Search
Sign up<br>Sign in
The Accidental Framework
Ian Johnson
16 min read·<br>Just now
Listen
Share
The thing I built was a Go binary that copied markdown files into a repository. I called it an installer. The README called it an installer. The CLI verb was init. For the several months in early 2026, that framing held. Then I sat down to plan the 1.0 release, started typing, and could not write the next paragraph without using the word framework. Not in the marketing-blurb sense. In the load-bearing sense: a small runtime, a set of named extension points, conventional file layouts, a resolver that picked a winner among competing sources, a lockfile, a migration runner, per-backend adapters. The thing I had been calling an installer for four months had been doing framework work the whole time, and I just had not noticed.<br>This post is about that moment of recognition and what came after. It is about the refactor that turned an installer-shaped framework into a framework-shaped framework. The most surprising part was not the rewrite. It was how little of the concepts had to change. The names were already right. The directories needed to move. The runtime needed a physical line down the middle between framework code and content. But the abstractions I had been using (guides, corpus, sensors, actions, playbooks, adapters) held up under the new framing without revision. The work I had done naming things over the previous months turned out to be the work that made the refactor cheap.<br>I want to walk through how that happened, what tipped me off, and the one wrong turn the refactor almost made before backing out of it. The story has a tidy ending; the lesson does not, and I will get to that.<br>The shape was there before the word<br>Keystone started as keystone init — a binary you ran in a fresh repo and it laid down a harness/ directory full of markdown. Rules for the agent. A small lifecycle. A few sensors that ran your existing lint and test commands. Nothing especially framework-ish. The 0.1 README said installer four times.<br>Then I started adding things. Each addition felt like a feature, not an architectural shift. The commit log reads like a feature log:<br>0.2: install-time options. Pick your agent. Add a target.<br>0.3: restructured the harness into corpus, guides, sensors, flywheels. Named taxonomy for the first time.<br>0.4: a kind taxonomy on guides and sensors. Different kinds, different load behavior.<br>0.5: a migrate command for forward-compatible upgrades. The harness had a schema now, and the schema had a version.<br>0.10: policy plugins. Org-level rules that projects pulled in.<br>0.11: the policy cascade. Org → Team → Project. Strict and non-strict layers.<br>0.12: playbooks. Ordered chains of actions.<br>0.13: sensors as a tier-aware policy kind.<br>Read that list once and it looks like product growth. Read it twice and the shape gets harder to miss. By 0.13 there was a runtime with conventions, plugins, a cascade, a lockfile, migrations, and per-agent rendering. The README still said installer.<br>The thing about a shape forming under a name is that the name is the last thing to change. Each individual commit looks like a small feature; the architectural mass piles up underneath and nobody is watching the mass. I was not, anyway. I was writing the next feature.<br>When I opened a fresh document called PLAN-10.md and tried to summarize where things stood, the first sentence I wrote was: Convert Keystone from a harness installer with org policy plugins into a harness framework. I stared at that for a while. The conversion was not a conversion. It was an admission.<br>What an installer was pretending not to be<br>Here is what makes the installer label dishonest in retrospect. An installer drops files. It is done. The user takes it from there. The framework that Keystone had become did all of this on top:<br>It defined named extension points. Guides, corpus, sensors, actions, playbooks, adapters per agent. Each one had a path convention, a frontmatter contract, and a load behavior. That is an API, not a directory listing.<br>It resolved conflicts across sources. The same /.md could exist in the project, in a team plugin, in an org plugin. Exactly one won. The resolution order mattered, and the ordering rules were stable across versions. That is a runtime, not a copy step.<br>It tracked drift. A lockfile pinned per-source SHAs and per-file hashes. The binary would notice when plugin files had been edited under it. That is integrity enforcement, not file-laying.<br>It migrated old layouts forward. keystone migrate would walk an installed harness, apply numbered transforms, and bring it to the current schema. That is a schema migrator, with all the implications of having a schema.<br>It rendered the same harness into multiple targets. Claude Code’s CLAUDE.md shape, Codex’s AGENTS.md, Cursor’s...