How I work with coding agents
I enjoy writing code and solving problems myself. I want to get better at it. I’m also skeptical of AI, which might sound strange given how extensively I use coding agents.
I use them as engineering tools. I can delegate repetitive work, ask an agent to investigate something, or let it try an implementation. That doesn’t make understanding the result optional. And when I want to learn something, working through the problem myself matters to me, even if an agent could finish it faster.
Compound engineering gives me a structure for the work I delegate: plan, implement, review, and carry useful lessons into the next task. I want that process to help me make better decisions. I still have to decide what to build, question the implementation, and check whether it works.
Building this portfolio gave me a small example. I wanted to read unfinished articles on localhost without including them in the deployed site. An agent added a development-only filter. Then we opened localhost and the articles were missing.
The server was serving the previous production build. The filter wasn’t the problem. We were checking the wrong thing.
Start with a result you can inspect
“Add a blog” leaves a lot for an agent to guess. A more useful task names the behavior and the boundary:
Let me read selected drafts in development.
Keep them out of the production homepage, writing list, and article routes.
Keep published articles working in both modes.
Use the existing Astro content collection and Bun commands.
Show what you checked before calling it done.
This still leaves room for the agent to make implementation decisions. I don’t need to specify every line. I need to say what must be true when the work is finished.
My defaults belong in the repository instructions: read the nearby code, preserve unrelated changes, use the existing dependencies, and keep the implementation small. I prefer straightforward code. Adding a helper just to name an expression can make me jump between files without making the behavior easier to understand.
Those defaults are preferences, not a reason to refuse an abstraction when it solves an actual problem.
Let the agent investigate before prescribing the fix
I want the agent to find the relevant files and explain what controls the behavior. On this site, hiding a draft from the writing list isn’t enough. Its route could still be generated, or its title could still appear on the homepage.
Astro’s content collection supplies the entries. Each place that uses those entries needs the same visibility rule. In this project, the predicate is:
!data.draft || (import.meta.env.DEV && !!data.preview)
An entry marked as a draft needs an explicit preview setting to appear in development. In a production build, the development branch is false. Astro content collections and environment variables describe the underlying APIs.
But reading that expression only checks the idea. It doesn’t establish that every route uses it, or that the server I opened is running in development mode.
Make verification part of the task
I want the agent to run the checks while it still has enough context to fix a failure. “You can test it now” is a weak handoff if it hasn’t tried the path itself.
For this portfolio, that means checking two different outputs: the generated production files and the running development server. Here is a small check for one draft, run from the project root after a production build:
import assert from 'node:assert/strict';
const slug = 'first-post';
const route = `/blog/${slug}/`;
assert.equal(await Bun.file(`dist${route}index.html`).exists(), false);
for (const path of ['dist/index.html', 'dist/blog/index.html']) {
assert.equal((await Bun.file(path).text()).includes(route), false);
}
const response = await fetch(`http://127.0.0.1:4321${route}`, {
signal: AbortSignal.timeout(10_000),
});
assert.equal(response.status, 200);
const html = await response.text();
assert.ok(html.includes('My first post'));
assert.ok(html.includes('Local draft'));
The first assertion catches an exposed draft route. The loop checks the two listing pages. The HTTP assertions catch a missing local article, including the mistake of serving a production build when I meant to open development.
The repository keeps the check in scripts/verify-writing.mjs, with an additional published-article check so hiding everything cannot count as success. With the development server stopped, run:
bun run check
bun run build
bun run dev
Once development is ready, run this in another terminal:
bun run verify:writing
This is a check for specific fixtures, not a proof about every future post. It also says nothing about the layout. I still need to open the page to judge reading width, spacing, and how it feels on a phone. And hiding an article from a website doesn’t hide its Markdown in a public repository.
Give parallel work separate boundaries
I use planning, implementation, review, and simplification for different questions. A small copy edit doesn’t need a planning ceremony. An unfamiliar change across several parts of an application usually benefits from one.
If I split work between agents, I want each one to own a clear piece. One can investigate an API while another reviews a diff. Two agents editing the same files introduce coordination work that can cancel out the time saved. Separate branches or worktrees help isolate changes, but the combined result still needs checking.
Even tools can collide. During this portfolio work, overlapping Astro commands tried to write the same content cache and one failed. I now run the production checks in sequence, then start the development server. More processes aren’t automatically more progress.
Review the change, then simplify it
For correctness, I ask what input or state could make the change fail. For simplification, I ask whether each new dependency, branch, and function earns its place. I want findings tied to the actual diff and a reproducible consequence.
A reviewer agreeing with the implementation isn’t another test. If a concern matters, reproduce it or identify the evidence that would settle it. If review changes the code, rerun the affected checks against that version.
This is the distinction I find useful between vibe coding and agentic engineering. I can use a loose prompt to explore an idea. Before I rely on the result, I need an explicit requirement and a way to check it. Delegating the implementation doesn’t remove that responsibility.
Leave enough context to continue
When a task needs another session, I want a short record of the decision that wasn’t obvious from the code, what was tested, and what remains unresolved. Copying the whole conversation makes the next agent search through the same noise again.
I also want a stopping point. Once the requested behavior works and the relevant checks pass, another redesign needs a reason. The time I save should be available for my next project, not spent supervising an endless rewrite of the last one.