<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
  xmlns:atom="http://www.w3.org/2005/Atom"
  xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Frankie Ottomanelli — Writings</title>
    <link>https://ottomanel.li/writings/</link>
    <description>Articles on AI agents, developer experience, and full-stack engineering.</description>
    <language>en-us</language>
    <lastBuildDate>Fri, 01 May 2026 20:29:11 GMT</lastBuildDate>
    <atom:link href="https://ottomanel.li/rss.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>AI as a Dev Tool: Engineering Around the Model</title>
      <link>https://ottomanel.li/writings/engineering-around-the-model/</link>
      <guid isPermaLink="true">https://ottomanel.li/writings/engineering-around-the-model/</guid>
      <pubDate>Sun, 26 Apr 2026 00:00:00 GMT</pubDate>
      <description>The model isn&apos;t the lever; the loop isn&apos;t either. The leverage in working with coding agents is in what you engineer around the model: context, deterministic procedures, and the surface area an agent can actually reach.</description>
      <content:encoded><![CDATA[<p>ChatGPT made AI feel like Google on steroids. I picked up new languages faster than docs, used it for everything I used to ask Stack Overflow, watched my troubleshooting and architecture speed up. It felt like the new ceiling. Then coding agents arrived, and that ceiling was a floor. Like going from physical mail to email, except compressed into a couple of years instead of a century.</p>
<p>The pattern is simpler than the marketing implies:</p>
<pre><code>SESSION SETUP (once):
  system prompt + tools + CLAUDE.md + skills + memory loaded

LOOP (per turn, until model stops calling tools):

  HARNESS pre-work
    └─ assemble context window
    └─ inject any newly-triggered skills/memory
    └─ compact older turns if too long
       │
       ▼
  MODEL call (stateless)
    └─ reads context, produces text + tool calls
       │
       ▼
  HARNESS post-work
    └─ append model output to history
    └─ if tool calls: execute, append results, loop back
    └─ else: done, wait for next user message
</code></pre>
<p>That's the whole thing. The model itself is stateless. Every turn, the harness assembles a fresh context window and asks "what next?" Everything that gives an agent state-like behavior (memory, skills, conventions) happens in the harness, around the model call. The model just reads what it's given.</p>
<p>A year in, the model isn't the bottleneck anymore. Shiny demos are easy. Making coding agents work as a <em>daily extension of yourself</em> (across projects, across weeks, across the accumulated friction) is a different problem. The model isn't the lever. The loop isn't either. The leverage is in three things: managing the context the agent operates inside, codifying the procedures that shouldn't be left to improvisation, and shaping the surface area the agent acts against.</p>
<p>Until AI infrastructure changes, you're working with stateless probability machines. Context engineering is the lever you have. Here's what I've learned about pulling it.</p>
<h2>Context</h2>
<p>Look at where context goes in the diagram. The harness loads the static stuff at session start: system prompt, tools, your <code>CLAUDE.md</code>, any skills, any memory files. After that, every turn, it assembles a context window from that base plus the running conversation. Whatever the model knows about your project, your conventions, your past corrections, it knows because the harness put it there. Nothing else.</p>
<p>That gives "context" two distinct faces, and they want different treatment.</p>
<p><strong>Static context</strong> is the upfront investment. <code>CLAUDE.md</code>, skills, project doc files, the conventions you write down once and expect the agent to follow. It's the layer that decides whether a fresh session opens at <em>"explain what this codebase is"</em> or <em>"you already know the stack, the file conventions, and the deploy flow. Go."</em> You build it slowly, edit it deliberately, and reuse it across every session in the project. Done well, it makes every session start hot.</p>
<p><strong>Dynamic context</strong> is what accumulates over time: within a session as the conversation grows and tool calls add their results, and across sessions as memories of corrections and learned patterns persist. It changes turn to turn and session to session. This is the layer most "agent memory" writing focuses on, and it's also the layer where most setups go wrong.</p>
<p>The default failure mode is treating dynamic context like a journal: save everything, accumulate forever, hope the model figures out what's relevant. Within a session, modern harnesses auto-compact when the window fills, but compaction kicks in late (you've already paid for the bloat in tokens and latency) and is lossy (summaries lose nuance the originals had). Cross-session memories aren't compacted at all by default; if you save every observation and never prune, you're slowly lining your starting context with stale and contradictory entries before the next session even begins.</p>
<p>The skill is the opposite of accumulation. <strong>The skill is contraction.</strong> Pruning, summarizing, deciding what's worth carrying. Memory done well is selective: durable rules that earn their place, indexed pointers to where the deep history lives if it's ever needed, summaries replacing raw transcripts. Memory done badly is just storage with no quality bar.</p>
<p>Tool results are the biggest source of within-session bloat. A <code>Read</code> on a long file, a <code>Bash</code> command that dumps a directory tree, a <code>Grep</code> over a large codebase: every result lands in the running conversation and ships with every subsequent turn. Mature harnesses do some management automatically: truncating long outputs, compacting older turns when the window fills, isolating sub-agents so only the final report returns to the parent. The truncation is just code: keep the first N bytes (or first-and-last N lines for log-shaped output), append a marker noting what was dropped. Compaction is different: when the window itself starts to fill, the harness calls a cheaper model to summarize older turns and replace the raw transcripts. The rest is on you: prefer focused queries to broad dumps, dispatch sub-agents for high-volume exploration, and treat every tool call as a context cost.</p>
<p>That's where feedback comes in, and where the usual framing trips up.</p>
<p>For stateless models, <strong>feedback isn't training the model</strong>. The model is fixed. It doesn't learn from your thumbs-up. What feedback actually does is <em>curate which context survives into the next session.</em> That's a different problem from how AI feedback usually gets discussed, and reframing it changes how you design for it.</p>
<p>For local, user-driven agents, the feedback signal is mostly your own behavior. You accept a suggestion, you reject one, you correct an output, you revert a change. Each of those is information about what the agent should remember and what it shouldn't. The discipline isn't building elaborate feedback systems. It's <em>converting your behavior into durable context.</em> When a correction recurs, lift it into <code>CLAUDE.md</code>. When a one-off observation is genuinely valuable, save it as a memory. When a memory's advice has stopped helping, prune it.</p>
<p>In practice this looks like an index of one-line memory entries pointing at deeper files (short, fast, always-loaded), with the deeper content pulled in only when relevant. Instead of dragging your entire memory store into every session, you keep a curated table of contents at the top and let the harness fetch the rest on demand.</p>
<p>Static context done well, dynamic context kept selective, feedback understood as curation rather than training. That's the discipline of context management.</p>
<p>But context only helps if the agent is doing the right <em>kind</em> of work in the first place. Some things shouldn't be agent decisions at all.</p>
<h2>Procedure</h2>
<p>Agents are for judgment. Code is for procedure. Most "let the agent do everything" failures are category errors: letting the model improvise something that doesn't need improvising, paying tokens to re-derive a sequence that hasn't varied in months.</p>
<p>The cleanest version of this principle is: <strong>separate gathering from judgment.</strong> When you ask an agent to analyze something (a CI failure, a flaky test, a perf regression, a code review), the analysis is the part that needs the model. The data-gathering around it is usually deterministic. You know what to fetch: the error message, the relevant files, the git log, the test logs, the metrics. Pre-fetch all of it in code, hand the agent a packaged input, and let it reason over what's already there.</p>
<p>This is genuinely different from letting the agent gather data itself. An agent doing its own gathering will run <code>gh</code> calls one at a time, decide it needs another file, run another command, decide it needs to grep, run another command. Each round trip is tokens you're paying for; each result swells the context; the gathering itself becomes the slowest, lossiest part of the workflow. A local clone, or a script that prepares the inputs once, replaces a dozen unpredictable tool calls with one deterministic step. Pre-fetch over fetch-as-you-go.</p>
<p>Pre-fetching everything isn't always possible. Sometimes <em>what</em> to gather is conditional on what the agent finds, and you can't predict it in advance. So the actual design has three layers you compose, not three options to pick between. Pre-fetch (<em>code decides what and how</em>) handles the standard data every run needs. Gathering scripts (<em>agent decides what; code handles how</em>) are small commands the agent invokes when it decides it needs data, but that handle the fetching itself in a known, deterministic way. Free improvisation (<em>agent decides what and how</em>) is necessary for the genuinely novel cases, but expensive in tokens, latency, and unpredictable context bloat when overused. A well-designed workflow uses all three: pre-fetch the predictable, expose scripts for the conditional, leave room for improvisation only where the first two layers can't reach. The failure mode isn't using the agent's flexibility. It's using <em>only</em> its flexibility, when most of the gathering could have been handled deterministically a layer or two up.</p>
<p>But the boundary between "code does it" and "agent does it" isn't fixed. It moves over time. <strong>Repetition is the codification trigger.</strong> When you watch an agent improvising the same step across multiple runs of the same workflow (the same git query, the same log fetch, the same test isolation), that's the signal it should graduate into the script. Don't pre-codify what you imagine the agent will need; wait until you see the pattern. The script grows from observation, not anticipation.</p>
<p>That doesn't mean locking the agent out of all gathering. The whole point is that the agent gets to handle the <em>unknown</em>: the parts you couldn't have anticipated, the cases where its judgment matters. The script handles what you know; the agent handles what you don't; and you graduate things from the second category to the first as patterns emerge.</p>
<p>In practice this looks ordinary. A <code>prebuild</code> script generates assets that don't need an agent's opinion. A <code>postbuild</code> step writes a feed that's the same every run. A workflow harness wraps a CI analysis with all the standard fetches before the model ever sees the prompt. Nothing exotic. The agent shows up with the data it needs, not the obligation to discover it.</p>
<p>The same discipline (narrow the agent's surface to defined, deliberate interfaces) applies to permissions and safety, not just procedures. That's the third pillar.</p>
<h2>Surface</h2>
<p>Most AI-safety guidance defaults to approval gates: the user must confirm each risky tool call. It works for a week, sometimes a day. Then prompt fatigue sets in, the user starts auto-approving, and the safety theater becomes worse than no safety at all. You've trained yourself to dismiss the warnings without reading them.</p>
<p>The real strategy isn't approving more things faster. It's <em>reducing the number of things that need approval.</em> That's interface design.</p>
<p>What pillar 2 was teaching, applied to safety: <strong>scripts, safe wrappers, and MCP tools are the same conceptual move.</strong> All three narrow the agent's surface to a defined, auditable interface. The agent gets unconstrained freedom <em>within</em> the interface; what's constrained is the interface itself. A build script narrows the build procedure to a known sequence. A <code>safe-git</code> wrapper narrows git access to a read-only subset within a single org. An MCP tool narrows a backend operation to its declared signature. Different implementations (shell command, CLI wrapper, JSON-RPC server), same pattern. MCP is the most formal version (typed args, schema-discoverable, swappable across clients); for most things, a CLI wrapper is fine.</p>
<p>This opens two complementary surfaces of constraint, both worth using:</p>
<ul>
<li><strong>Credentials</strong>: what identity does the agent operate as? Scope it down. A read-only AWS role rather than your full admin profile. A token with no destructive permissions on the DB. The blast radius shrinks at the auth layer. Risk is bounded by what the credential <em>can</em> do.</li>
<li><strong>Interface</strong>: what tool surface does the agent see? Wrap it. A <code>safe-aws</code> that exposes only describe/list operations. A preapproved deploy script that calls a destructive CLI with vetted args. Risk is bounded by what the <em>wrapper</em> permits.</li>
</ul>
<p>Pair them. Scoped credential as the floor (what's possible), wrapper as the ceiling (what's allowed). When you do both, approval gates become the rare exception for genuinely novel actions, not the default friction layer.</p>
<p>The other axis is the <em>environment</em> the agent runs in. And it's worth naming the threat model honestly: the realistic risk for personal AI dev isn't that the model writes malicious code on purpose. It's <strong>prompt injection</strong>: the model ingests adversarial content (a poisoned tool output, a malicious file, a hijacked dependency, a web result returning attacker-controlled instructions) and decides to act on it. You're not trying to stop the agent from running useful commands; the whole point of an autonomous agent is that it <em>runs</em> commands. You're shaping the environment so that even a hijacked agent can't exfiltrate credentials, reach <code>evil.com</code>, or destroy work you actually care about. The principle isn't "always lock down"; it's <em>match the leash to what the agent could break.</em> That's a spectrum:</p>
<ul>
<li><strong>Same user, no isolation</strong>: full inherited access. Approval gates as the friction layer. Default on most work laptops, and where the fatigue problem hits hardest.</li>
<li><strong>Same user + devcontainer</strong>: Anthropic's officially recommended approach. Real isolation of filesystem, processes, and network from the host. Honest tradeoff: real monorepo friction (slow terminals from large mounts, Git scoping issues when <code>devcontainer.json</code> sits in a subdirectory, mount confusion that takes hours to debug).</li>
<li><strong>Different user + firewall (dedicated env)</strong>: Pi, spare VM, secondary machine. Unix uid/gid bounds filesystem reach; firewall bounds network egress. Practical safety is comparable to a devcontainer for the realistic AI agent threat surface. Neither truly sandboxes the kernel, but both shrink the blast radius. The win is sidestepping the monorepo friction; the cost is needing hardware you can dedicate.</li>
<li><strong>Heavier isolation</strong>: dedicated VMs per task, microVMs (Firecracker, gVisor, Kata), anything where the agent doesn't share a host kernel with anything else. The territory of cloud and autonomous agent systems, where the threat model includes other people's data, untrusted code, or blast radius reaching beyond your own machine. Different problem, separate piece.</li>
</ul>
<p>Match the level of isolation to the actual risk, not to the maximum-paranoia case.</p>
<p>The reframe that ties this all together: <strong>approval gates aren't the default. They're what's left over after you've narrowed credentials and interfaces.</strong> The dev-approval-fatigue problem doesn't get solved by better approval UX. It gets solved by reducing the number of things that need approval in the first place. A <code>safe-git</code> on the allowlist is one fewer prompt. A scoped credential is a whole class of destructive actions you no longer have to think about. Each layer of constraint compounds; what's left after them is the genuinely novel surface where a human's judgment actually adds value.</p>
<p>That's the third pillar. When all three are in place, something interesting happens.</p>
<p>Every wrapper, script, skill, or local MCP you build once <em>stays</em>. Your starting point goes up every project, not because of the tools themselves, but because of the library that grows under you.</p>
<p>The discipline scales further than your laptop. Devcontainers shape the environment so broad permissions can't reach the host (with real monorepo friction you should know going in). A dedicated machine with a separate user and firewall (Pi, spare VM, whatever you can dedicate) buys similar bounds with less workflow cost. Different mechanics, same principle: shape the environment to bound the blast radius.</p>
<p>Autonomous agents running as themselves are the next step on that spectrum, and a separate piece. But the foundation is here. Get the local case right; the rest is evolution.</p>
<p>Until AI infrastructure changes, you're working with stateless probability machines. Context engineering is the lever. Get good at it.</p>]]></content:encoded>
      <category>AI</category>
      <category>Coding Agents</category>
      <category>Developer Experience</category>
    </item>
  </channel>
</rss>
