Ports and Adapters for Prose. I rewrote the same skill file three… | by Ian Johnson | Jun, 2026 | MediumSitemapOpen in appSign up<br>Sign in
Medium Logo
Get app<br>Write
Search
Sign up<br>Sign in
Ports and Adapters for Prose
Ian Johnson
9 min read·<br>Just now
Listen
Share
I rewrote the same skill file three times last month. Each time I changed the explanation. The trigger conditions never changed. I’d baked them into the body so deeply that touching the explanation meant re-typing the trigger from memory, which meant the trigger drifted, which meant the skill fired in places it shouldn’t, which meant a fourth rewrite.<br>The trigger was right the first time. The body wasn’t. I rewrote the wrong half of the file three times because the two halves weren’t separable.<br>That afternoon I went back through every skill file, slash command, and harness rule I’d written in the last quarter and looked for the same shape. It was everywhere. The contract — what the artifact must accomplish — was tangled together with the prose explaining how to do it. I couldn’t change one without rewriting the other. The seam wasn’t there.<br>This is the ports-and-adapters pattern, applied to prose.<br>The pattern, translated<br>Ports and adapters comes from software architecture. You define an interface — the port — and write multiple implementations behind it — the adapters. The interface is stable. The implementations swap freely. The point is the seam: code on one side of it doesn’t know or care about the code on the other side.<br>The same shape applies to the prose we write for LLMs. A prompt, a skill file, an agent definition, a harness rule — each one has two parts authors usually conflate.<br>The port is what the prose must accomplish. The preconditions, the inputs, the outputs, the failure modes, what counts as satisfied, what counts as out of scope. The contract the prose has to honor, stated independently of how you’d explain it.<br>The adapter is the actual wording. The metaphors, the worked examples, the tone, the level of detail. One specific way to satisfy the port.<br>Most prose-for-LLMs collapses these. You sit down to write a skill or a system prompt and what comes out is a tangle of “what this does” with “how I’m explaining it.” Six months later you want to change the explanation without changing the contract, or vice versa. The seam isn’t there, so you rewrite the whole thing.<br>The pattern is to separate them at the point of authoring. Write the port explicitly first — a short list of inputs, outputs, invariants, failure modes. Then write the adapter as one specific way to satisfy the port. Multiple adapters can satisfy the same port: terse, verbose, different audience, different model.<br>What a port looks like, written down<br>Here is a port for a skill file that decides whether a code review should run. I write the port as a small block before I write the body.<br>port: review-decision<br>triggers:<br>- user asks "review this", "look at the diff", "/review"<br>- PR is open and unreviewed<br>preconditions:<br>- a diff exists against a base ref<br>- the diff is not empty<br>outputs:<br>- a list of findings, each with a file path and line<br>- a verdict -> ok | suggest-changes | block<br>invariants:<br>- never edits the diff<br>- never claims pass without evidence<br>out-of-scope:<br>- running tests<br>- refactoring adjacent codeThat’s the port. It’s twelve lines and it says nothing about tone, examples, metaphors, or the model on the other end. It is, deliberately, boring.<br>The first time I wrote a port this way I felt like I was missing something. The contract is so spare. There’s nothing in it to chew on. Then I tried writing two adapters against it and the spareness paid for itself.<br>Two adapters, same port<br>Here’s adapter A — terse, for a model that infers well from short instructions:<br>Review the diff against base. For each file, list findings as path:line — issue.<br>Verdict: ok / suggest-changes / block. Don’t edit. Don’t run tests. Show evidence for any “block.”
Here’s adapter B — verbose, for a model that benefits from worked examples:<br>You’re reviewing a diff against a base ref. Walk through each changed file. For<br>every issue you find, write one line: the file path, the line number, then a<br>short description of the problem.<br>End with a verdict: “ok” if nothing meaningful, “suggest-changes” for findings<br>that aren’t blockers, “block” for findings that should stop the merge.<br>You won’t edit the diff. You won’t run tests. If you call something a blocker,<br>quote the line that proves it.<br>Example finding:<br>src/auth.ts:42 — token compared with ==, allows type coercion
Both adapters honor the same port. The findings have a path and a line. The verdict is one of the three values. The invariants — no editing, no test-running, evidence for blocks — survive in both. The difference between them is texture, not contract.<br>Now: which one do I edit when I want to change the tone? Adapter A or B. Which one do I edit when I want to change what “blocker” means? The port, and then both adapters fall out of date in...