Software development
-
`updated_at` Is Not a Conflict-Resolution Strategy
In the last post we talked about the problems with a distributed system, and touched on the fact that timestamps are not as reliable as you think they are.
If you have two
updated_atfields and you compare them, how do you decide which side is the correct one?The
updated_atfield only tells you that a write happened. It doesn’t tell you the meaning, or if it was intentional. Conflict resolution is fundamentally a question about causality. Did side A intend for this change to happen? A wall clock timestamp can’t tell you the answer to that.Two independent clocks can drift, and will drift. Yes, it will get corrected by NTP occasionally. But you can’t always rely on their NTP service working. Timestamps are a fine signal that something occurred, and they’re a reasonable way for a human to sort a list and answer roughly when we think a change occurred. But if you use them as a foundation to decide what data to keep, you’re gonna end up destroying and losing data.
Things That Actually Work
The good news is the alternatives are not exotic, and you don’t need all of them.
Content hashes. Hash the meaningful content and compare hashes instead of times. This kills the metadata-edit problem outright: if the hash matches, nothing changed, no matter what the timestamp claims. It’s the highest-value change on this list and usually the easiest, because it’s a pure function of data you already have.
Version counters. A monotonic integer per record, incremented on every meaningful write. Immune to clock skew entirely, because it isn’t a clock. The cost is that somebody has to own the increment, which is straightforward with a single authority and gets harder without one.
Sync checkpoints. Record what was confirmed at the last successful sync, not just when it happened. Then the question becomes “has this changed since the last agreed state,” which is answerable, instead of “is this newer,” which is a guess.
Operation logs. Store what happened rather than only the result. Heavier, but it’s the only option that lets you reconstruct intent after the fact, and it turns “which one wins” into a question you can actually audit.
You can get most of the benefit from the first one. Hash the content, and let the timestamp go back to being a display field.
When Last-Write-Wins Is Fine
I’m not gonna lie, last write wins is often the correct engineering choice, and replacing it with something more complicated can be its own mistake. Sometimes it’s fine. If a write gets lost and the data is recoverable, that’s a trade you can live with.
If it’s a simple tool without a ton of users, adding a lot of complexity is not the way to go.
If the data is just a cache or a projection, then who cares? You can rebuild it from the authoritative source anyway.
What I’d Actually Do
Keep
updated_at. It’s useful. Sort by it, display it, log it.Just stop letting it decide things. Add a content hash and check that first, so a no-op edit stays a no-op. If a field can be written from two sides independently, give it a version counter or an explicit authority rule, and write the rule down somewhere the next person will find it.
I’d appreciate a follow. You can subscribe with your email below. The emails go out once a week, or you can find me on Mastodon at @[email protected].
Databases Software-development Distributed-systems Local-first Data-modeling
-
The Moment You Add Sync, You Have a Distributed System
How do you keep two sets of data in sync? Like, by definition, you now have a distributed system.
It could be something simple, syncing files or talking with a remote service somewhere. Maybe it’s not a lot of code. Initially, it might not feel like a distributed system, because there’s no cluster or consensus protocol. There’s no leader election system. You have multiple leaders that need to stay in sync.
How do you maintain state between two independent systems, when their only connection is an over-the-network connection that’s allowed to fail?
Sync can be a verb that you apply to the data on one side, but it’s also describing the negotiation that happens between two distributed systems.
Here is the set of questions you have to answer if you are trying to build a distributed system that maintains sync.
- What’s new here that isn’t there?
- What’s new there that isn’t here?
- What changed in both places since we last talked?
- What happens if we get halfway through and the connection dies?
- If I retry, do I create a duplicate?
All of these sound like problems from a paper you read about replicated state machines. Congratulations, they’re now your problem too.
Just because the request succeeded doesn’t mean that the two systems now agree.
The problems that you’re going to run into are either caused by or solved by a timestamp.
Recovering from an error state is crucial for building a durable system.
Idempotency Is the Cheapest Insurance You Can Buy
It is a guarantee, or pretty much a guarantee, that if your sync can be interrupted, it will be. Idempotency is how you ensure that the same request can be retried safely. If you run the same request twice, you either need to produce the same result or no result.
Every item on both sides of the system needs its own stable identity that each side agrees on. The create needs to always be create-if-absent, basically an upsert.
How do you decide which copy of the data has authority?
When you start needing to do resolution logic, this is where your subtle data loss can occur. Your point-in-time recovery window is likely 30 days or less, and chances are you aren’t going to go back and check the old copy that is about to expire.
Last write wins is the default because it’s easy, and it’s what everybody assumes. It works when there aren’t a whole lot of writes and when one side is clearly the primary. It breaks down when the data can’t be replayed safely.
Do your timestamps actually mean what you think they mean? Can you trust time? It’s complicated to get correct. And if the difference between two timestamps is very small, and the drift is larger than the difference, problems occur.
Things to look into for later: CRDTs, vector clocks, operational transforms.
Chances are these are not the right answer for a personal tool that you’re building on the weekends. It’s good to have discipline and understand the solutions we’ve come up with for resolving synchronization problems. At the end of the day, you’re just gonna want something that works.
You have a distributed system. It has one user and it runs on a laptop, but it has all the failure modes, and it doesn’t care that you didn’t mean to build one.
I’d appreciate a follow. You can subscribe with your email below. The emails go out once a week, or you can find me on Mastodon at @[email protected].
Software-development Architecture Distributed-systems Local-first Apis
-
Your Local File Should Not Have to Argue With Your Database
Sync bugs usually all start the same way. Two copies of something, both of them mostly right, and no written rule about which one wins.
The problems occur when you don’t notice The bug. When the file says one thing and the database says another. It’s not a problem until it is. And then you have to spend time figuring the why and when’s of the drift.
So let’s talk about authority. Not storage, not sync, not “where does the data live.” Authority. Which copy is allowed to be right when two copies disagree.
The Question Nobody Writes Down
Most systems that hold the same data in two places never actually decide this. The decision gets made accidentally, by whichever code path happened to run last, and then it gets re-made differently by the next feature.
The failure is duplication without a stated rule.
Here’s a concrete version. I run a content pipeline for this blog. Posts are Markdown files with YAML frontmatter sitting in a directory. There’s also a Turso database holding metadata about those same posts. Two copies of what looks like the same information.
Ask the naive question, “which one is the source of truth,” and you get a bad answer, because the honest answer is neither, and both, depending on the field.
Split Authority by Field, Not by Store
You might try to pick an authoritative source based on store. Files win, or the database wins. But the useful granularity is usually the field.
In my pipeline it breaks down like this:
- Post content and tags: the Markdown file wins. The frontmatter is authoritative. If the database has a different tag list, the database is wrong, and it gets rebuilt from the file.
- Scheduling: the database wins. What time a post goes out, what slot it holds, whether it’s been claimed. The file does not get a vote.
Those are different answers for the same post, and that’s fine, because each one is written down and each one has a reason.
The content lives in the file because content is the thing I edit by hand, in an editor, with Git history behind it. I want
git logto be the real record of what changed. Putting that in a database would mean my writing history lives somewhere that is harder to access.The schedule lives in the database because scheduling is a coordination problem. It needs uniqueness constraints, it needs to answer “what’s in the 10am slot on Tuesday,” and it needs to do that without me parsing 241 files. A database is genuinely better at that. It just isn’t better at holding prose.
A Database Can Be Useful Without Being Authoritative
I think there’s a reflex where adding a database feels like promoting the data into it. You put the posts in Postgres and now Postgres is where posts are.
It doesn’t have to work that way. A database can be a query layer over data that lives somewhere else, and that’s a completely respectable job. Indexes, joins, counts, “show me every post tagged local-first published before June.” All of that is worth having, and none of it requires the database to be the authority.
The test I use: if I deleted the database right now, what would I lose forever?
For me, it would be the scheduling state because that’s what I put in the database. The important thing is I wouldn’t lose a single word that I’ve written. Every post would still be in a directory. This choice is deliberate.
It’s easy for the database to become a Cache and not an authoritative source.
What Should Happen When They Disagree
If you have documented your authoritative source, then the disagreements stops becoming a crisis, and it just is a routine. Resolution event
You should be able to rebuild it. There should be nothing to decide. The decision is documented and how you resolve conflicts. Just depends on. Which authoritative source owns Which s segment of your data?
In my case, there’s actually a third authoritative source, and that’s the remote blog system that hands back an ID every time I schedule a new post.
So, this is totally fine if you pick the authority at the field level and Document that decision to prevent trip-ups in the future.
Your files and your database shouldn’t be arguing, all it requires is a bit of planning.
I’d appreciate a follow. You can subscribe with your email below. The emails go out once a week, or you can find me on Mastodon at @[email protected].
Databases Software-development Architecture Local-first Data-modeling
-
The Decision Log: A Lightweight Artifact for Agentic Coding
Coding agents are remarkably good at reopening decisions you already made.
Imagine a content pipeline where posts live as local Markdown files and a database holds the scheduling metadata. You open a fresh session. Which one does the agent think is authoritative?
It has to guess. And the database looks like the better answer, because databases usually are. So it proposes the obvious cleanup: make the database the source of truth and treat the files as an export format.
It’s wrong, for a reason the code never states. You can rebuild the database from the files. You cannot rebuild the files from the database. Drop the database and you re-index from markdown, losing some operational history on the way. Lose the markdown and the posts are gone.
Some questions are still better answered by the database, scheduling among them. But the authoritative copy is whichever one you could regenerate the other from, and that asymmetry is the entire argument. It appears nowhere in the schema, nowhere in the file layout, and nowhere in the diff.
So you explain it. The agent takes the point immediately, drops the idea, and gets on with the actual work. Good outcome.
Then the session ends, and the next one arrives with the same instincts and the same blank slate. Nothing’s wrong with its reasoning. It’s missing the one piece of context the repository doesn’t contain: that road has already been walked.
That’s what a decision log is for.
Issues and Skills Leave a Gap
Agentic projects tend to accumulate two useful artifacts.
Issues describe work. Add a field, fix the retry logic, move the cache, update the API client. They tell an agent what needs to change and, if the issue is any good, what done looks like.
Skills and project instructions describe process. Use this package manager. Run these tests. Never call this destructive command. They tell an agent how work should happen, repeatedly.
Both matter. Neither is a natural home for, “We considered Redis, rejected it because this service has to run without another dependency, and we’ll reconsider if the process moves to multiple instances.”
Artifact The question it answers Issue What needs to change? Skill or instruction How should work be done? Decision log Why this path instead of another one? The code records the result. Git records the diff. A closed issue might contain the discussion, if somebody thinks to go looking for it. The decision log keeps the conclusion and the rejected alternatives somewhere an agent can find them before it starts planning.
The Rejected Option Is the Important Part
I wrote recently about decision debt, the gap created when code gets produced faster than anyone records what it means or why it exists. A decision log is one small way to pay that debt as you go.
The useful part isn’t “We chose SQLite.” The repository already contains a SQLite database. The useful part is why SQLite won, which alternatives lost, and what would have to change before the decision should be reopened.
Without that negative history, an agent sees an absence and treats it as an oversight. No Redis? Add Redis. No abstraction around this HTTP client? Generate one. No Kubernetes deployment? Surely the project just hasn’t matured enough yet.
Sometimes the missing thing is missing on purpose.
Humans do this too, of course. We reopen old arguments when the people who remember them leave, or when the conclusion is buried in a meeting recording. Agents just compress the cycle. Every fresh session is a new developer joining the project with excellent technical instincts and absolutely no institutional memory.
Keep the Entry Small
Architecture Decision Records have been around since Michael Nygard described the pattern in 2011. They preserve the status, context, decision, and consequences of an important architectural choice. Lightweight templates like MADR also capture the options considered, the decision drivers, and why one option won.
That’s the right idea. I just don’t need a formal architecture record for every consequential choice in a personal project.
I’d start with some sort of log file, holding entries like this:
## 2026-07-26: Keep Markdown as the publishing source of truth **Context:** Posts exist as local files and as database records. **Decision:** Frontmatter controls publish state. The database owns scheduling metadata and is not authoritative for publishing. **Rejected:** Making the database authoritative, or newest-write-wins. **Why:** The database can be rebuilt from the files. The files cannot be rebuilt from the database. **Revisit when:** Editing moves to a multi-user hosted application. **Links:** the repository module, the publishing documentation.That’s the whole artifact.
An agent that reads that before it starts planning knows the database-as-source-of-truth idea isn’t a fresh insight. It also knows exactly what would have to change before it becomes one again.
I’ll be honest that I haven’t started doing this yet, but it sounds like a good idea right?
The entry doesn’t need a transcript of the debate. It needs enough context for a future person or agent to understand that the alternative was considered, why it lost, and which changed condition would make it worth discussing again.
What Belongs in the Log
If every choice becomes an entry, the log turns into another file nobody reads. I’d record a decision when at least one of these is true:
- Two or more reasonable approaches existed
- A future agent is likely to propose the rejected option again
- The choice establishes a source of truth, security boundary, schema, dependency, or workflow
- Reversing it later would be expensive or dangerous
- The reason isn’t obvious from the code
Don’t log naming arguments, routine implementation details, or every library function you picked. “Used a dictionary here” isn’t institutional knowledge. “Kept provider integrations on raw HTTP because the SDK doesn’t support the endpoint we need” might be.
Major decisions can still become full ADRs in
docs/decisions/or whatever. The lightweight log is for the big middle ground between an architectural record and a comment somebody vaguely remembers leaving on a pull request.
Make Agents Read It at the Right Time
Creating the file isn’t enough. The agent needs a retrieval rule. One sentence in the project instructions:
Before proposing changes to architecture, dependencies, data ownership, security boundaries, or core workflows, search `docs/decisions.md` for related decisions and revisit conditions.Then the other half:
After a consequential decision is approved, propose a short decision-log entry. Do not record a new project policy without human confirmation.The instruction defines the recurring behavior. The log supplies the project-specific facts. That keeps settled choices out of a giant instruction file while still putting them in the agent’s path when they matter.
When a decision changes, don’t quietly rewrite history. Add a new entry that supersedes the old one and say which revisit condition actually showed up. Version control preserves the edit, but the document should make the change legible without requiring repository archaeology.
Not Another Memory System
A chat transcript contains every false start, tool result, and half-formed idea. It’s too noisy to act as a project constitution. Agent memory can help retrieve past context, but it might be private to one tool, unavailable to a collaborator, or hard to review in a pull request.
A decision log needs to be deliberately boring. Plain text. Searchable. Reviewable. Stored next to the code, or accessible by it. Any human or agent can read the same entry and argue with it in the open.
None of this guarantees an agent will make the right call. It removes one wasteful failure mode: spending another session rediscovering a settled tradeoff and confidently proposing the option the project already rejected.
Issues tell the agent where to go. Skills tell it how to move. The decision log marks the roads we already closed, why we closed them, and when they might be worth opening again.
That feels like context worth keeping.
Sources
- Documenting Architecture Decisions — Michael Nygard’s original 2011 ADR proposal.
- MADR — a lightweight decision-record format covering options, rationale, consequences, and revisit conditions.
- The GDS Way: Documenting architecture decisions — keeping decision rationale in the repository while using issues to track implementation.
- How Claude Code works — fresh session context, compaction, project instructions, and persistent memory.
I’d appreciate a follow. You can subscribe with your email below. The emails go out once a week, or you can find me on Mastodon at @[email protected].
-
The Human Job Is Choosing What Not to Build
Coding agents have made one word much harder to say.
No.
You describe a feature, and the agent can already see the model, the migration, the command, the tests, and the docs it needs to touch. The whole thing sounds like an afternoon instead of a week.
So why not build it?
That question used to contain its own answer. We didn’t have the time or the people, and the feature wasn’t worth interrupting everything else for two weeks. Now the implementation might take twenty minutes. The old constraint is gone, but the need to choose is not. If anything, choosing matters more, because we can say yes faster than we can understand what all those yeses are doing to the product.
The agent’s job is to make the thing we asked for. The human job is deciding whether the thing should exist.
Yes Produces Better Receipts
Building something leaves evidence. There’s a diff. Tests pass. A new command shows up in the help output. You can take a screenshot, close an issue, and point at the feature. Work happened, and the artifacts prove it.
Choosing not to build produces almost nothing. Maybe you leave the idea in a notebook. There’s no demo for the dependency you avoided or the interface you never had to support.
That makes restraint feel less like engineering.
It can look like indecision, or lack of ambition, or an empty afternoon of planning.
A good “no” can preserve more value than a clean implementation.
You protected the shape of the product. You kept it understandable. You left room for the features and the users that actually matter.
Agents Make Local Ideas Look Great
Coding agents are strongest when the task is concrete. Add this flag. Support this file format. Create an adapter for this provider. Cache this response. Put a dashboard on top of these logs.
Given one of those, the agent inspects the local code and produces a reasonable path forward. It sees where the new feature fits.
What it doesn’t automatically carry is the opportunity cost.
You can put that context in the prompt, but somebody still has to decide how much product is enough.
A human has to protect the product.
Cheap Is Not the Same as Free
I recently argued that code is cheap now, but decisions are not. The mistake is pricing a feature only by how long the first implementation takes.
A twenty-minute feature can create a permanent interface, another concept every future agent has to understand, or a second way to do a task when the first way was already fine.
The implementation estimate answers: how quickly can we make this work?
The product decision asks: is this worth changing what the system is?
Four Ways to Say No
Not building something doesn’t always mean rejecting it forever. Here are four versions of no.
No, this is not the product
Some ideas are useful and still don’t belong. A small command-line tool doesn’t automatically need a web dashboard. A personal publishing pipeline doesn’t need multi-tenant permissions because it could theoretically serve a team. A library doesn’t need a plugin system before a second plugin exists.
These features may solve real problems. But they solve somebody else’s version of the product.
This is the cleanest no.
Write down the boundary and move on.
Not yet, we don’t have the evidence
Sometimes the problem is plausible but unproven. The database might need caching. The API might need another abstraction. Users might want a second export format. “Might” is doing all the work.
Wait for the system to produce evidence. Measure the slow query. See a second provider show up. Hear the same request from someone who actually has the problem. The agent will still be there when the need is real.
Deferral is only useful when it has a condition: “Revisit imports when we have enough data to support the decision.”
All the “maybe laters” are not backlogs; they are fossils that deserve to be buried.
Yes, but smaller
Plenty of ideas contain one valuable piece surrounded by a feature-shaped cloud.
Build something that you think is useful. Not everything needs a dashboard.
You don’t need a generic provider framework until you have a reason for the generic provider to exist.
You don’t need a rules engine if you’ve only got three rules.
Ask for the smallest change that provides the most value.
Narrow the solution before it becomes a broad one.
No longer
The hardest no is the one aimed at code that already exists. How do you determine whether it’s actually being used or not? This is how we get features that outlive the reason they were built. You added an experiment that became a supported path, and now everybody’s afraid to remove it.
Prune the branches of the product tree. Before you decide what to build next, make sure you’re not letting the system grow forever.
You should be asking yourself: what choices are you deciding against? Sometimes it’s important to know what already exists before deciding what should exist.
My Filter Before I Say Yes
Product committees suck, especially for small changes. Before I accept an idea as a feature, I ask myself the following questions. Or I should ask myself. Or I hope I ask myself. Whatever version of that makes sense for the day.
- What problem gets easier?
- Why now?
- What new promise does this create?
- What gets harder after this exists?
- Can a smaller change prove the value?
- What would make us remove it?
These help you tell the difference between what’s easy to generate and what’s worth owning. Sometimes they help you solve for a hypothetical problem. It’s a good thing to try to figure out whether the tests will pass before you build them.
Taste Is the Remaining Bottleneck
As implementation gets cheaper, the scarce skill becomes taste.
Not taste as in fonts and rounded corners. Taste is recognizing when a product has enough concepts. It’s choosing the boring interface people can understand. It’s seeing that a flexible abstraction makes the current problem worse. It’s knowing which rough edge gives the tool character and which one just wastes time.
Then decide.
The future of software isn’t a world where we finally build every idea in the backlog.
That sounds exhausting.
The future of software is a world where we get to decide, and be honest about which ideas were actually good to begin with.
Don’t forget you’re allowed to say no.
I’d appreciate a follow. You can subscribe with your email below. The emails go out once a week, or you can find me on Mastodon at @[email protected].
-
When I Use a Subagent and When I Don't
Give a good harness access to subagents and everything starts looking like a team project.
You’ve got one agent working with the database layer. Another can research the API. Another can write the tests. Another can check the work. Another can manage moving the data. As work starts happening in parallel, colored dots appear, and the whole thing starts looking like an org chart.
That sounds like fun to me, but it can also be a mess.
Having more agents doesn’t automatically produce better results.
Sometimes they duplicate work. Sometimes they edit files at the same time, producing unexpected changes or behavior.
I mean, I still use subagents constantly. They are definitely the default.
Sometimes we need, like, a subagent arena. Two agents enter. One survives.
The Subagent Context Boundary
I started using subagents as a way to protect the main context window. I wrote about that back in March: the delegated agent can read files, inspect logs, and grind through intermediate reasoning in its own context.
It’s still one of the best reasons to delegate. When finding the right answer means searching through 20 files, reading five of them, tracing a configuration value, and checking the tests, that sounds like a subagent task to me.
The search is noisy, and the output needs to be compact.
Using subagents keeps the main context window free for the larger task.
I Delegate Bounded, Independent Work
The tasks I delegate have one thing in common: I can define the output before the work begins.
Here are some areas where a subagent makes sense:
- Repository research.
- A separate implementation surface.
- Specialist review.
- Independent verification.
- Real parallel work.
Don’t Delegate an Unclear Problem
Delegation is not a substitute for deciding what the work is.
If a request is vague, spawning more agents isn’t going to help with the vagueness. It’s just more output with no clear direction. The parent still needs to choose the requirement and figure out what we’re building.
Before I delegate, I want to handle the following in the parent context:
- what question the subagent is answering,
- which files or systems it owns,
- whether it may edit anything,
- what constraints it must preserve,
- what evidence it should return,
- when it should stop and ask instead of guessing.
If we can’t figure out answers to those, the next step should be planning, not delegation.
Don’t Split Tightly Coupled Work
Avoid having two agents edit code in neighboring areas. The code may share an interface, a fixture, or a schema, and if you start changing things in multiple places without coordination, you’re going to have problems.
Each agent has its own context window. When it reads a file, block of code, or dependency into that window, the context is only current at that moment. If another agent changes the same thing, how does the first agent know its context needs to be updated?
It’s a complicated problem, for sure.
So your best bet is to avoid parallel edits where agents are working in the same or similar areas of the code. The agent work areas need to be distinct.
If humans can have coordination problems, agents can too.
I Don’t Delegate Five Minutes of Work
Delegation has a cost. The parent needs to describe the task and load enough context for the subagent to do the work and summarize it. Then the parent needs to verify the result. All of that burns a bunch of tokens. Obviously, for one-line changes or small amounts of text, this handoff should never happen in the first place. To decide whether to pass the work to a subagent, ask: will using a subagent substantially improve the output?
Sometimes the answer is just to do it in the parent context window, even if that means you have to compact sooner.
A good subagent task looks a lot like a good software interface. It has a narrow purpose, explicit inputs, clear permissions, and predictable output.
Compare these two assignments:
Look into the tests and fix anything missing.Review the unchanged-record tests for Book. Identify provider-owned fields that are not covered. Add focused tests only in tests/test_books_client.py. Do not change production code. Run the focused test file and report the result.The first transfers uncertainty. The second transfers work.
The difference does matter.
The Parent Still Owns the Result
Subagents report completion. They don’t make completion true.
The parent still has to inspect the changes, reconcile conflicting findings, run the combined quality gate, and decide whether the original requirement was satisfied. If three agents each report that their piece passes, you have three pieces of evidence. You don’t yet know whether the assembled system works.
Delegation changes who gathers the evidence. It doesn’t remove the need to judge it.
The goal isn’t keeping every agent busy. The goal is finishing the work without turning yourself into middle management for robots.
Oh God, I think that’s my job title.
I’d appreciate a follow. You can subscribe with your email below. The emails go out once a week, or you can find me on Mastodon at @[email protected].
-
Not Every Agent Task Needs an Issue
I’ve written before that your AI agent needs a task manager, and I stand by it. Chat history is not project state. Context windows compact. Sessions end. If the work matters tomorrow, it needs to live somewhere tomorrow’s agent can find it.
The part I didn’t mention: not every task matters tomorrow.
Some work should disappear with the session. Turning all of it into durable project state doesn’t make your agent more organized. It gives the next session a bigger pile of bookkeeping to misunderstand.
Tasks Have Different Lifetimes
When we say “task,” we’re lumping together several different things.
“Add filtering to the search API” is a project commitment. It might take several sessions, affect other work, and need a record of why the behavior changed.
“Inspect the existing query code” is not a project commitment. It’s one step the agent needs to finish the larger task.
“Run the focused tests” is even shorter-lived. Once they pass and the result has informed the work, that checklist item has done its whole job.
These all deserve attention. They don’t all deserve permanent storage. I think about them in two buckets:
- Session tasks help the current agent organize execution. They live in a plan or checklist, then disappear when the work is complete.
- Durable issues preserve commitments, decisions, dependencies, and unfinished work across sessions.
The distinction isn’t importance. A database backup check can be a critical session task. The distinction is whether the information needs to outlive the work happening right now.
If it doesn’t, let it die.
Ephemeral Does Not Mean Sloppy
There’s a temptation to treat ephemeral tasks as unstructured work. Just tell the agent,
plz fixand see what happens.A session still benefits from a clear plan. The agent should inspect before editing, break a change into steps, mark progress, run focused tests, run the full quality gate, and verify the result. A visible checklist makes long work easier to supervise, and it reduces the chance the agent quietly skips the boring last step.
The checklist just doesn’t need to become part of the project’s permanent record. Picture something like this:
- inspect the repository method - find every caller - update the shared comparison helper - add focused tests - run the full suiteThose are excellent session tasks. They tell the agent how to move through one change. After the implementation lands, keeping all five around adds nothing. The commit and tests preserve the result. The issue, if one exists, preserves the reason.
An Issue Should Earn Its Permanence
A durable issue is more expensive than it looks. Someone has to write it clearly, connect dependencies, update its state, close it, and eventually decide whether it’s stale. An agent also has to read it. Every open issue becomes part of the project’s apparent reality.
That cost is worth paying when the issue preserves something important. I create one when at least one of these is true:
- The work will survive the current session. If we’re likely to stop before it’s done, the next session needs a reliable handoff.
- Other work depends on it. A dependency belongs in a system that can represent blocked and ready work, not in a paragraph buried in chat history.
- It represents a real commitment. A user-reported bug, an accepted feature, or a promised follow-up shouldn’t vanish because a terminal closed.
- The decision needs a record. If future maintainers will ask why the system behaves this way, the issue preserves context a diff can’t.
- The work crosses boundaries. Changes spanning repos, services, migrations, or people need coordination beyond one agent’s checklist.
- We found valid work but aren’t doing it now. That’s exactly what a backlog is for.
If none of those apply, a session task is probably enough.
Promote Work When It Changes Shape
You don’t have to pick the perfect tracking level before the agent starts.
Begin with a session plan. During inspection, the agent may discover that the “small fix” requires a migration, depends on another repository, or exposes a separate bug. If the work changes shape. Promote it.
Agents are very good at expanding scope. They inspect one path, notice three adjacent problems, and offer to fix everything while the files are open. Sometimes that’s useful…
Your Backlog Is an Agent Prompt
Humans are pretty good at looking at an old issue and thinking, “yeah, we don’t care about that anymore.”
Agents are more literal. If the tracker says the issue is open and ready, the agent has a strong reason to treat it as authorized work. A stale backlog can send a perfectly capable agent down an obsolete implementation plan just as it easily as it can with a valid one.
No issue hygiene needs to be a part of your process. An open issue tells the agent this work is still wanted, this description is still accurate, these constraints still apply, and finishing it would improve the project.
If any of that is false, the issue isn’t harmless clutter. It’s a bad prompt waiting to be executed.
Use the Smallest State That Survives Long Enough
After six months with git-native issue tracking, my workflow has gotten a lot more varied.
I use a session plan when the current agent only needs help organizing the work in front of it. I use an issue tracker when the project needs to remember something after that agent is gone. For local projects i’m very rarely reaching for Beads. For team work it’s always GitHub or GitLab.
Too little tracking and the project forgets real commitments. Too much and it accumulates stale instructions, duplicate tasks, and chores whose only purpose is maintaining the tracker.
Start small. Let the agent make a checklist. Promote the work when it makes sense. Close it when the feature is satisfied. Delete old tasks like you are pulling weeds to make space for the flowers to bloom.
Your agent needs a task manager. It does not need a permanent record of every box it checked along the way.
I’d appreciate a follow. You can subscribe with your email below. The emails go out once a week, or you can find me on Mastodon at @[email protected].
-
Tests Are Evidence, Not a Definition of Done
I’ve watched thousands of tests run over the years.
When they pass it has felt good. A huge screen of green checks in the terminal is incredibly rewarding as a software developer.
But the work is never finished.
The tests are always valuable. They are evidence, but they were never the definition of done.
Tests Prove the Claims We Give Them
I’ve written before that testing is how we show our work, and I still believe it. If software is meant to run more than once, test it. No excuses, just testing.
The mistake is treating a passing suite as proof of more than it covers.
A test makes one specific claim about a system:
given this setup when this action happens then I expect this resultRun enough of those claims and you build confidence. You know the parser handles a missing field. You know the repository preserves an identifier. You know the command returns a failure code when authentication breaks.
What you do not know is whether you wrote the right claims.
If the requirement was misunderstood, the implementation and its tests can agree perfectly. If your fixture contains a field the real API never sends, every parser test can pass while production fails. If your mock accepts a request the provider rejects, you’ve proven compatibility with the mock.
That’s useful, but let’s not get carried away.
The Agent Can Test Its Own Misunderstanding
Coding agents make this distinction more important, because they generate code and tests together.
You ask for a feature. The agent interprets the request, implements that interpretation, then writes tests asserting the implementation behaves exactly as it wrote it. Everything passes.
Sometimes that’s a clean, efficient workflow. Sometimes it’s one misunderstanding with excellent test coverage.
Picture asking an agent to add a
published_atfield from an external service. It sees another timestamp calledcreated_at, assumes they mean roughly the same thing, and uses one as a fallback for the other. Then it writes tests proving the fallback works.The suite is green. The code is wrong.
The issue isn’t that an agent wrote the tests. Humans have spent decades writing tests around their own bad assumptions. The difference is speed. An agent can turn a vague requirement into a thoroughly tested wrong answer before you notice the semantic choice it made.
More tests don’t rescue a bad premise. They preserve it.
Mocks Prove You Understand the Mock
Mocks are one of the best tools we have for keeping tests fast and deterministic. I use them a lot. I don’t want every local run hitting a real provider, burning rate limits, changing remote data, or failing because somebody’s service is having a bad Tuesday.
Still, a mocked integration is a model of reality, not reality.
Your fixture may be stale. The provider may omit fields you marked required. Auth may use a header you never modeled. Pagination may stop differently than the docs imply. Error responses may arrive as HTML because a proxy had opinions.
Unit tests prove your code handles the world you described. A live, read-only check tells you whether the world still resembles that description.
That doesn’t mean turning the whole suite into live integration tests. It means picking a small amount of extra evidence proportional to the risk:
- Fetch one real response and inspect the fields you depend on.
- Exercise authentication without modifying remote state.
- Run a dry run through the production code path.
- Validate a migration against a realistic database copy.
- Confirm the deployed service reports the expected version.
Tests stay fast. Reality gets a vote.
Passing Is Not the Same as Shipped
There’s another gap between tested and done that has nothing to do with correctness.
Code can pass every check and still exist only in a working tree. A migration can be valid but unapplied. A config change can be committed but missing from the deployment environment. A feature can reach production without the logs you’d need to understand its first failure.
This sounds operational because it is. Software isn’t finished when the implementation works in the place where it was written. It’s finished when the intended system has the change, and you can tell whether that change is healthy.
For a small personal script, that might mean committing it and running it once with real input. For a web service, deployment, a health check, logs, metrics, and a rollback path. For a database change, verifying both the schema and the application behavior after migration.
The evidence changes with the risk. The principle doesn’t.
pytestcannot tell you whether you forgot to push the commit.
Done Is a Decision Built From Evidence
A definition of done should answer a broader question than “did the tests pass?” It should answer: what evidence would make us comfortable owning this change?
For most work, I look in a few categories:
- Intent: The behavior matches the actual requirement, not the first interpretation of it.
- Implementation: Automated tests cover the important paths and failure cases.
- Quality: Static analysis, formatting, types, and review caught what they’re designed to catch.
- Integration: Real boundaries behave the way our fixtures and mocks claim.
- Operations: The change is delivered, observable, and recoverable in proportion to its risk.
- Durability: Code, migration, docs, and task state are saved where the next person or agent can find them.
Not every change needs all of them. Fixing a typo doesn’t require a rollback drill. Changing how customer data is stored deserves more than one unit test and a thumbs-up from the agent that wrote it.
Good engineering is choosing the right amount of evidence, not applying the largest checklist to everything.
Green means the evidence looks good. Done means you have enough of it.
I’d appreciate a follow. You can subscribe with your email below. The emails go out once a week, or you can find me on Mastodon at @[email protected].
DevOps AI Testing Software development Engineering practices
-
Every Feature Opens a Maintenance Account
Coding agents have developed a dangerous little habit. You ask for one change, and somewhere near the end they offer to add three more.
Would you like a configuration flag? A JSON output mode? A retry option? Maybe a dashboard while we’re here?
The agent can build it. The tests and relevant files are already in the context. So why not?
Just one more feature, one more prompt. You know what I’m talking about.
Then six months later the flag is still there. Somebody relies on the JSON shape. The retry behavior conflicts with a new timeout. The dashboard has a dependency that needs a security update. The agent that offered to build all of it has moved on with its life, mostly because it never had one.
You still own the feature.
That’s the part we need to consider. Every feature is another record in a maintenance ledger.
A Feature Is a Standing Promise
We tend to think of a feature as the code that implements it. Add the function, connect the interface, write the tests, merge the change. Feature complete.
That’s the construction phase. The feature itself is a promise that begins after the merge.
If you add a CLI flag, you’re promising that scripts can keep using it. If you expose a JSON response, you’re promising something about its shape. If you store a new piece of data, you’re promising to preserve, migrate, export, and eventually delete it correctly. If you connect another service, you’re promising to notice when its API changes.
The code might be thirty lines. The promise has no line count.
This is why tiny features get expensive. The implementation fits in one file, but the behavior crosses documentation, tests, support, deployment, security, and every future change near that boundary. Agents are very good at showing us the thirty lines. They’re much less likely to show us the next three years.
The Opening Balance Looks Great
Before coding agents, implementation cost acted as a filter. Not always a good one, but a filter.
Someone had to estimate the work, find time in a sprint, write the code, and get it reviewed. A small convenience feature might lose simply because nobody wanted to spend two days on it. Frustrating, sure, but it forced the question: is this worth building?
Now the estimate is twenty minutes. The agent has already inspected the codebase. It can update the model, add the command, generate the tests, and fix the type errors without needing another meeting. The old cost-benefit calculation collapses, because almost everything looks worth building when you only count the first implementation.
So we say yes more often.
That’s not automatically bad. Plenty of useful software never existed because construction cost too much. Cheaper implementation lets small teams solve problems that used to require a real budget. Good for us, but the maintenance math didn’t collapse along with it.
The feature still adds another path through the system. It still creates behavior that can regress. It still has users, even if the only user is you on a Sunday afternoon six months from now.
The opening balance is cheap. The account stays open.
What Accumulates
Maintenance is easy to wave away because no single piece sounds overwhelming. It’s just one more test. One more paragraph in the docs. One more migration. Then the interest starts adding up:
- Compatibility: Existing callers depend on behavior you considered an implementation detail.
- Testing: Every supported path needs coverage, fixtures, and updates when neighboring code changes.
- Documentation: The feature needs to be discoverable, accurate, and removed from the docs if it goes away.
- Dependencies: A tiny feature can introduce a library that now participates in every upgrade and security review.
- Operations: New jobs, tables, queues, or API calls need logs, failure handling, and a recovery story.
- Support: Someone has to answer why it behaved differently on another machine.
- Removal: Deleting it later means finding its users, migrating their data, and deciding how long compatibility lasts.
None of these costs are unique to generated code. We’ve always paid them. The difference is volume. Agents let us open maintenance accounts much faster than we close them.
A Ten-Minute Flag Is Still an Interface
Let me give you an example.
You have a command that prints a human-readable table. An agent offers to add
--json. That sounds great. It probably is great. The code serializes the existing records, the tests compare a sample payload, and the whole change lands before lunch.Then someone pipes that output into another script.
Now field names matter. Null behavior matters. Ordering might matter even though you never promised it. A renamed internal property breaks an external workflow. Adding a timestamp creates noisy diffs. Removing a field requires a compatibility decision.
The flag didn’t add another display format. It created an API.
Would you still build it? Probably. I like useful CLI tools, and machine-readable output is usually worth supporting. The point isn’t to reject the feature. The point is to recognize the account you’re opening. Once you see it as an interface instead of a ten-minute patch, you define the schema deliberately, document what’s stable, avoid exposing fields that should stay internal, and decide whether versioning matters before somebody’s automation answers that question for you.
Same code. Better ownership.
Backlogs Hide the Statements
One reason maintenance gets away from us is that backlogs are organized around changes, not promises.
The issue says “add export support.” It rarely says:
Maintain this export format for as long as anyone depends on it, update it whenever the underlying model changes, keep its documentation accurate, and provide a safe way to retire it later.
That would look ridiculous in an issue title. It’s still what the issue means.
Agents make backlogs disappear quickly, which feels fantastic. I’ve watched them knock out work that would have sat around for months. But a closed issue can become an open obligation. A project with fifty completed features isn’t necessarily healthier than one with twenty. It might just have thirty more things that can break.
Price the Account Before You Open It
I don’t want a meeting for every CLI flag. The whole advantage of these tools is that we can move fast.
We can still take thirty seconds to ask better questions before accepting the extra code:
- Who will depend on this? A person clicking a button creates a different promise than a script parsing output.
- What new state or interface does it introduce? Stored data and public schemas are much harder to remove than local calculations.
- What has to stay compatible? Name the stable boundary instead of letting users guess.
- How will we know it broke? Tests help, but logs, validation, and recovery may matter more.
- What ongoing work does it create? Dependencies, docs, migrations, provider changes.
- What would cause us to close the account? Decide now whether it’s experimental, permanent, or removable.
If the answers are cheap too, build it. If the feature creates a permanent public contract for a minor convenience, nope, not going in.
Cheap Construction Needs Better Restraint
I’m not interested in making software expensive again. Faster implementation is good. More people turning an idea into a working tool is good. Small teams getting leverage that used to belong to large companies is very good.
We just need to stop treating features as free.
I’d appreciate a follow. You can subscribe with your email below. The emails go out once a week, or you can find me on Mastodon at @[email protected].
AI Programming Software-development Coding-agents Maintenance
-
Code Is Cheap Now. Decisions Aren't
I’ve spent a lot of time working on software backlogs with coding agents. Pick an issue, inspect the code, make the change, run the tests, close it, move to the next one.
The strange part is how often writing the code is no longer the hard part.
An agent can add a field to a model, update the database, wire it through a client, and write a pile of tests before I check on it again. That would have been hours or days of time before. Now it happens in one focused session.
Great.
But the feature still needs someone to decide what the field means. Is it nullable? Which system owns it? Can it be inferred from another value? What happens when the provider stops sending it? Does an older local value survive, or should it be cleared?
The typing got cheaper. The decisions did not.
The Keyboard Was Never the Whole Job
We’ve spent decades measuring software work by its visible output. Lines of code, commits, pull requests, story points, features shipped. Code was expensive to produce, so counting the artifacts at least felt related to the effort.
That relationship is falling apart.
You can ask an agent for three implementations of the same feature and get all three before lunch. You can generate a REST API, a migration, a test suite, and a deployment manifest in one sitting. If you don’t like the architecture, ask for another one.
Now you have four implementations and a new problem: which one should exist?
Software development was never just converting requirements into syntax. It was deciding which requirements were real, where the boundaries belonged, what failure looked like, and which tradeoffs you were willing to own. Code generation doesn’t remove that work. It removes the part that used to hide it.
One Small Change, Twelve Real Questions
Let me give you an example.
Say an external API adds an optional date. Adding it to your application sounds trivial. Nullable column, property on the model, parse the response, write a migration, done.
Except the provider already has another date with a similar name. One is when the item was published. The other is when the user interacted with it. They’re both dates. They are not interchangeable.
So now the real questions start:
- Do we store only the explicit value, or infer it when it’s missing?
- Is a missing field the same as a field containing
null? - If the remote value disappears, do we delete the local value?
- Does this field participate in unchanged-record detection?
- Will a dry run exercise the same comparison path as a real sync?
- Can older installations run safely before the migration is applied?
- What happens when a second provider represents the same idea differently?
None of those are unique to any language. The agent can explain the options and implement whichever answer you pick, but it can’t discover your intended data contract from the shape of a JSON response.
If you skip those decisions, you still get code. You just get code that quietly invents policy for you.
Cheap Code Creates Expensive Options
This is the part that you might underestimate.
When implementation was slow, the cost naturally limited how many ideas made it into a codebase. You might sketch three approaches, but you probably built one. The friction forced a little restraint.
Agents remove that friction. A feature that would have been rejected as “not worth a week” now looks attractive because the first version only takes an hour. A speculative abstraction feels harmless because the agent can generate it in minutes. A new internal tool seems reasonable because the prototype already works.
It worked. But “it works” and “it’s a good idea” are two very different things.
The generated code still has to be reviewed. Its dependencies still need updates. Its behavior still needs tests. Someone still has to understand it when the surrounding system changes. If it handles credentials or customer data, somebody owns that risk too.
The implementation cost may be close to zero. The ownership cost is not.
This is how you end up with a backlog full of individually reasonable features that collectively make the project worse. Each one was cheap enough to add. Nobody stopped to price the maintenance costs associated with each successive feature.
Decision Debt
We already have a name for shortcuts in implementation: technical debt. You move quickly today and accept that the code will cost more to change later.
Agentic development creates another kind. Call it decision debt.
Decision debt is what happens when code gets produced faster than anyone can answer why it exists, what promise it makes, and who is responsible for it. The implementation is complete, but the boundaries are fuzzy. The tests prove what the code currently does, but nobody has decided whether that’s what it should do.
You can see it in questions like these:
- Are we supporting this behavior or merely tolerating it?
- Is this data authoritative, derived, or cached?
- Is this workflow meant for one person or every user?
- Is this abstraction solving a repeated problem or predicting one?
- What would make us remove this feature?
An agent can help you reason through every one of those. That’s one of the best uses of the technology. Ask it to inspect the repository, find conflicting assumptions, model failure cases, and challenge the proposed design.
Just don’t confuse receiving an answer with making a decision.
The Human Work Moves Upstream
If agents keep getting better at implementation, the valuable human work moves toward choosing and framing the work.
That means writing a clear definition of done before generating code. Deciding which system is the source of truth. Recognizing when two similar concepts need separate names. Looking at a working implementation and saying, “No, this does not belong here.”
It also means treating restraint as engineering work.
Closing a stale issue without implementing it can be more valuable than shipping the feature. Reusing an existing boundary can beat introducing a cleaner new abstraction. Deleting a half-maintained tool can improve a system more than generating its replacement.
None of those choices produce an impressive diff. That’s fine. The diff was never the product.
What Good Looks Like Now
I don’t think the answer is to slow agents down, or to pretend generated code is somehow less legitimate than code typed by hand. The code doesn’t care who wrote it. If it’s correct, understandable, tested, and worth owning, ship it.
The change is in where we spend our attention.
Spend less time admiring how quickly the implementation appeared. Spend more time checking the assumptions it smuggled in. Ask what new state the system owns, what promise the interface makes, what can fail, and whether the feature deserves to survive its prototype.
Agents make it possible to build almost anything you can describe. Our job is deciding what should still exist six months later.
I’d appreciate a follow. You can subscribe with your email below. The emails go out once a week, or you can find me on Mastodon at @[email protected].
AI Software-development Engineering Agentic-coding Technical-debt
-
Build Your Own Skills Repo
If you’ve been working with AI coding agents for a while, you’ve probably started collecting workflows. You might not call them that yet, but they’re there. Another name for workflows is Skills.
Some are tiny: run these checks before shipping. Some are project-specific: when you touch this library, preserve this API contract. Some are operational: never print secrets, always summarize the logs. Right now they probably live in chat history, a README note, a shell alias, or your own memory. That works until it doesn’t.
A skills repo is a better shape for this. It’s one place to collect, version, review, and share the workflows that make agents useful in your projects. I built my own yesterday, so let me walk through how I think about it.
A skill is judgment, not a command list
The first mistake is treating a skill like a list of commands. Commands matter, but they’re the easy part. The real value is judgment: when to run the command, what to inspect first, what not to do, how to validate the result, and what risks are specific to this tool.
A good skill makes an agent more careful. It narrows the space of bad decisions. So before you write a single instruction, figure out who the skill is for.
Separate adoption from maintenance
Most serious projects have two audiences: people using the project, and people maintaining it. Those should usually be two different skills.
An adoption skill helps an outside developer get value from your project. Install the package, configure it correctly, use the right import path, migrate existing code, avoid the common mistakes, run the right validation.
A maintainer skill helps contributors work inside the source repo. Understand the layout, run the local quality gate, preserve compatibility promises, follow the release conventions.
Here’s why you need two. If you only write maintainer skills, your repo becomes a private automation folder. If you write adoption skills too, it becomes onboarding infrastructure.
Keep each skill focused
A skill should have a job. Not “everything about this project.” Not a duplicate README. Good skill names are verbs:
integrate,audit,upgrade,migrate,debug,ship. That keeps the trigger obvious, so when someone asks the agent to do that kind of work, the skill has a clear reason to load.If a project needs multiple workflows, split them. A library might have
integrateanddevelop. A deployment system might havedeployandrollback. Don’t cram them into one file.Put the safety rules near the top
The most important part of many skills is the “do not” section:
- Do not print secret values.
- Do not delete or archive anything without confirmation.
- Do not add failing CI enforcement unless asked.
- Do not do broad rewrites before previewing a diff.
- Do not commit local registry URLs.
Agents are good at momentum. Safety rules are how you make that momentum usable. The more destructive the workflow, the more explicit the guardrails should be.
Include validation, not just execution
Every skill should answer one question: how do we know this worked? That might be
pnpm test && pnpm build, orcargo test && cargo clippy -- -D warnings, orgo test ./.... For non-code workflows it might be “export the review list” or “verify the generated config is ignored by git.”This matters because agents can complete every step without completing the work. Validation closes the loop.
Write for the agent inside the repo
Skills should assume the agent is operating in a real project with real files and existing conventions. So the useful instructions look like:
- Inspect
package.jsonbefore choosing a package manager. - Read the existing test scripts before adding new ones.
- Prefer the local task-runner commands when they exist.
- Check the framework boundary before picking an import path.
That’s the context generic model knowledge won’t reliably infer. And it’s why you shouldn’t just copy the README into the skill. A README is for a human browsing the project. A skill is for an agent doing work. They overlap, but they aren’t the same artifact. Keep the skill short enough that loading it is cheap.
Use a marketplace repo as the index
Your skills repo doesn’t need to be the canonical home for every skill. Some projects should own their own plugin metadata, especially if they already have a CLI, release process, and docs. Your marketplace can just point at them remotely. Other skills can live directly in the marketplace repo. One structure that works:
skills/ .claude-plugin/ marketplace.json plugins/ esm/ .claude-plugin/plugin.json skills/develop/SKILL.md upkeep-rs/ .claude-plugin/plugin.json skills/audit/SKILL.mdThe marketplace becomes the thing people add once. Individual plugins stay free to live locally or point at their canonical upstream.
Scan third-party skills before you import them
The moment your marketplace points at someone else’s plugin, you’ve inherited their security posture. And skills are a soft target. The dangerous payload usually isn’t code, it’s prose: an attacker buries instructions inside a
SKILL.md, gated behind an innocent-sounding trigger, that tell the agent to read your.envand send it somewhere. A normal code scanner walks right past that. There’s no malware signature to match. It’s just English.This isn’t hypothetical. Snyk’s ToxicSkills research found prompt injection in 36% of the skills they tested, across more than a thousand malicious payloads. If you’re pulling skills from a public index, some fraction of them are trying to do something you didn’t ask for.
So run a scanner before you add anything you didn’t write. A few worth knowing:
- Snyk agent-scan inventories your installed agents, MCP servers, and skills, then checks them for prompt injection and data-handling problems.
- NVIDIA SkillSpector scans repos, URLs, or single files against a big catalog of patterns: injection, exfiltration, privilege escalation, tool poisoning.
- claude-skill-antivirus is purpose-built for Claude Code skills and runs several detection engines at once.
One caveat worth internalizing: scanning an MCP config can execute it, because starting a stdio server means running the command in the file. Do that in a sandbox, a container or a throwaway VM, not on your main machine. The tool you run to check for danger shouldn’t be the thing that sets it off.
This cuts both ways. If you publish a plugin others will install, a clear “do not” block and an honest description of what the skill touches is part of being a good citizen of the marketplace.
Start with your serious projects
You don’t need a skill for everything. Start where better agent behavior would matter: public libraries people might adopt, CLIs with safety-sensitive workflows, tools with tricky setup, projects with recurring maintenance, systems where mistakes are expensive.
For each one, ask yourself:
- Who is this for: user, maintainer, operator, contributor?
- What’s the concrete task?
- What should the agent inspect first?
- What commands are preferred, and which are dangerous?
- What should never happen silently?
- What validation proves the work succeeded?
Answer those and you have enough to write a useful first skill.
Why it’s worth doing
A skills repo turns scattered project knowledge into reusable operational guidance. But it also forces a better product question. If this project is meant to help people, what would it look like for an AI agent to help them use it well?
That’s a higher bar than “can the agent run the command?” The point isn’t to automate everything. It’s to package the judgment around your tools so the next agent starts from a better place.
I’d appreciate a follow. You can subscribe with your email below. The emails go out once a week, or you can find me on Mastodon at @[email protected].
-
How Do You Actually Review Code an Agent Wrote?
We’ve all had the magic moment by now. You fire up an agent in Claude Code, hand it a bunch of tasks, then let it do its thing. It feels like the future.
Right up until you have to hit
git commit.That’s when the magic curdles into a very specific kind of anxiety: how do I actually review this? I didn’t write it. I barely watched it happen. And now I’m supposed to vouch for it.
The AI Reviewer Trap
The obvious move is to fight fire with fire. An AI wrote it, so pipe the diff into an AI PR review tool and let the machines sort it out, right?
I wrote about this back in May: AI code reviewers won’t save you. Having one LLM grade another LLM’s homework can potentially lead to disaster. They share the same blind spots. They’re trained on the same patterns, so they tend to nod along at the same plausible-looking mistakes, and they’re notoriously bad at catching the subtle, systemic logic flaws that span an entire architecture. The bug that matters is rarely on one line. It’s the interaction between four files, and that’s exactly the kind of thing a second LLM waves through.
So you swing the other way and read it yourself, line by line. Every variable assignment, every branch. And that’s exhausting. Worse, it defeats the entire point of using an agent. If I have to mentally re-type every line the agent produced, I might as well have typed it for real.
So we’re stuck between a reviewer we can’t trust and a review process that erases the speedup. Neither one is the answer.
The Bottleneck Just Moved
Agentic development didn’t make software engineering easier. It moved the hard part.
Writing the implementation used to be the bottleneck. That’s the part the agent is genuinely good at now. What it can’t do for you is tell you the behavior is correct. Verifying the behavior is the whole job now, and that’s a different skill than writing code.
This is why migrating my test suite to Vitest earlier this year has paid off more than I expected at the time. When you’re driving autonomous agents, automated testing stops being a chore you do to keep a coverage badge green. It becomes the only safety net you actually have.
Trust the Spec, Not the Code
In an agentic workflow, my job as the human isn’t to write the function anymore. It’s to write the tests, or at least to rigorously verify the ones the agent proposes.
Think about what that buys you. If I have a comprehensive, fast test suite, I don’t ALWAYS need to read all 400 lines Claude Code just generated. I need to watch the runner light up green. If the tests pass, and the tests are good, the implementation details matter a lot less than they used to. The tests are the contract. The code is just one way to satisfy it.
That second condition is doing a lot of work, though, so I want to be honest about it. “If the tests are good” is the entire game. A passing suite that doesn’t cover the edge cases is worse than no suite, because it hands you false confidence at the exact moment you’ve stopped reading the code. So the scrutiny doesn’t disappear. It relocates. Instead of reviewing the implementation, you review the spec. Are the right behaviors tested? Are the failure modes tested? Did the agent quietly write a test that asserts its own bug?
That’s a much smaller surface to review than 400 lines of implementation, and it’s a far more durable thing to spend your attention on. The tests outlive any single refactor.
TDD Didn’t Die, It Got Promoted
For years people treated test-driven development as a discipline you adopted if you were virtuous and skipped if you were busy. Agentic coding flipped that. It made testing the load-bearing skill, because tests are now the interface between what you want and what the machine builds.
So the answer to “how do I review code an agent wrote” turns out to be: mostly, you don’t. You review what it’s supposed to do, you encode that in tests you trust, and you let the green checkmark tell you whether the agent got there.
I’m curious whether this matches your experience. I’ve found I write more tests now than I did before the agents showed up, not fewer. The implementation got cheap. Being sure it works did not.
I’d appreciate a follow. You can subscribe with your email below. The emails go out once a week, or you can find me on Mastodon at @[email protected].
-
Day 29: It's All The Same Problem
We’re twenty-nine days into this. I’ve thrown twenty-eight different time problems at you. Sundials, cesium fountains, leap seconds, Unix epochs, DST, lunar time, the Y2038 bug, calendar drift, time zones in Nepal. Twenty-eight posts. Twenty-eight things that should not be problems but are.
Today I want to tell you the thing I’ve been quietly noticing the whole series and never once called out by name.
They’re all the same problem.
Every single one. Same pattern, twenty-eight times, wearing twenty-eight different outfits. I’m going to name it today. Then I’m going to do something annoying with the schedule, which I’ll get to at the end.
Speed recap (skip if you’ve been reading along)
Week 1, what time is. Philosophers spent a century arguing about whether the present is real or whether all moments coexist. Einstein answered them. Your brain disagrees with both. Your “now” is a two-second hallucination your prefrontal cortex glues together from inputs that arrived at different speeds. Most animals don’t even live on the same scale of now you do.
Week 2, how we measure it. Sundials to water clocks to pendulums to cesium fountains to optical lattice clocks. We measure time to a part in 10^18, the most precise measurement humanity makes of anything. And then we strap leap seconds onto it.
Week 3, how computers handle it. Unix time pretends leap seconds don’t exist. NTP synchronizes the internet to within a millisecond of UTC, mostly because one guy at the University of Delaware refused to let it die. ISO 8601 prevented an entire generation of date-format wars. Time zones aren’t 24, they’re 38 and counting, and at least one sits at +5:45 for reasons.
Week 4, the cracks. Leap seconds crashed Reddit and Cloudflare. DST kills people, measurably, twice a year. Calendars are 2,000 years of patches on patches. The World Calendar almost passed the UN in 1955 and got killed by religious objections. The Hanke-Henry proposal solves the religious problem and has no political mechanism.
Week 5, the punch line. Einstein made “now” frame-dependent. Atomic time and astronomical time have been quietly drifting apart since 1972. Clocks on the Moon run 58 microseconds per day faster than clocks on Earth, and the White House gave NASA a 2026 deadline to figure out what to do about it. Mars’s day is the wrong length, and the people running rovers there go nocturnal in shifts.
Twenty-eight different stories. Each one feels like its own little disaster. Each one is its own little disaster.
OK. Now look at all of them at once.
A pattern emerges
Every single one is the same shape.
Too many time systems pretending to be one.
That’s it. That’s the diagnosis. Let me run it back.
- Wall clock time pretends to be solar time. It isn’t. It’s solar time, offset up to an hour by your time zone, then offset another hour by DST. Three systems. Presented as one.
- UTC pretends to be a uniform atomic timescale. It isn’t. It’s atomic seconds with manual leap-second patches jammed in whenever the Earth misbehaves. Two systems. Presented as one.
- Unix time pretends to be UTC. It isn’t. It’s atomic seconds with the leap seconds quietly deleted. Two systems. Presented as one, with the inconvenient one erased.
- The Gregorian calendar pretends to track the seasons. It almost does. The seasons drift against it at 26 seconds a year. Two systems. Presented as one.
- Earth civil time pretends to be the universal frame. It isn’t. The moment people start living on the Moon it won’t even pretend. Two systems. Presented as one. Third one inbound.
Every outage we’ve documented in this series is what happens when the layers diverge.
The 2012 Linux meltdown was the atomic layer screaming at the civil layer through the leap-second seam. DST mortality is the social layer dragging the biological layer somewhere it doesn’t want to go. Y2038 is the storage layer running out of room to lie about the coordination layer. The 2019 Brazilian DST cancellation broke every calendar event saved in local time, because the saved-time layer disagreed with the rule layer.
It’s the same bug everytime, just dressed up in a different costume.
They’re all just one problem we keep meeting, just at different layers.
What the shape of a fix looks like
I’m not telling you the full answer today. But the shape of it appears when you can see the pattern. The key is to stop letting the layers pretend to be each other.
That’s the whole design I laid out yesterday: one coordination layer underneath, atomic, uniform, no leaps, no zones, no calendars baked in, the thing computers and GPS and finance and navigation already run on, with known relativistic offsets if you leave Earth. Then a thin civil layer on top for humans: local, day-night aware, with the calendar and zones living up here as display only. The civil layer is computed from the coordination layer plus your local context. It’s never stored as the source of truth.
The whole point is that the layers don’t pretend to be each other. The coordination layer doesn’t pretend to track the sun. The civil layer doesn’t pretend to be a database timestamp. When two layers disagree, only the display changes. The stored truth is invariant.
Some flags I’m planting
Before someone accuses me of refusing to commit, three things I’ll say flat out.
Time zones are stupid. Not “annoying.” Not “a useful tradeoff.” Stupid. Thirty-eight of them. One sits at +5:45 because someone wanted to be 15 minutes off from India. Half of Australia runs on a different schedule than the other half. Indiana spent decades arguing with itself about which zone to be in. China is one country and one time zone across five solar hours. The whole point of a time zone is supposed to be “the sun is roughly overhead at noon.” We’re not honoring that contract anywhere. Some American zones put solar noon as late as 1:30 PM in summer. We’re paying the entire complexity cost of having zones and not actually getting the thing zones were invented to do. Time zones are a 19th-century can we’ll keep kicking well into the 21st.
DST is dumb. Day 21 was the whole case. Heart attacks, strokes, car crashes, a 1% increase in residential electricity use, no farmers asking for it, no voters asking for it, no science defending it. We do it because two camps in Congress can’t agree on which fake time to settle on. Not relitigating. Just doubling down.
UTC is on “borrowed time” and it knows it. UTC the protocol is fine. UTC the way it shows up in your daily life has time zones bolted on through the “+5” notation, has DST schedules layered on top, and (currently) has leap seconds duct-taped in whenever the Earth misbehaves. UTC itself is a leaky abstraction. It tries to be the coordination layer and the civil layer at the same time, and the cracks have already begun to appear. Abolishing the leap second was UTC quietly admitting it can’t be both, and after 2035 the civil-display half of its job becomes somebody else’s problem.
Now the things I’m not claiming.
I’m not claiming any one proposed solution is right. Decimal time failed. Swatch Internet Time failed. The World Calendar failed. Hanke-Henry probably won’t pass. The path forward, whatever it ends up being, has to be designed with those failures in mind. Top-down reform fails. Branded reform fails. The thing that has actually worked, where anything has worked at all, is open standards adopted gradually by institutions that found them useful. The way ISO 8601 became universal without anyone forcing it on anybody.
And I’m not claiming this is a five-alarm fire. The system mostly works. The bugs are real but tolerable. Civilization will not collapse if we don’t fix this.
But it was built for an Earth-bound, slower, less precise world, and it’s being asked to do things it was never designed for.
Lunar Coordinated Time is due by the end of 2026. The system that mostly works is going to be asked to do more, and we are going to need a better one.
Tomorrow
OK, first, I need to apologize.
Day 30 would normally land tomorrow. That’s where you would typically end a series called “30 Days of Time”. But tomorrow I’m going to break that promise on purpose, and here’s why.
I’ve been working on something while writing this series. The whole reason this synthesis exists is that this pattern is the thing I’ve been trying to design a solution for. I have a draft of an answer. I have a reference implementation. What I don’t have is anything finalized that I can actually point you toward.
I’d rather get it right than ship Day 30 tomorrow with a half baked idea. I want the Day 30 payoff to be a well thought-out draft of a unified time standard, not a rushed sketch. I don’t know how long that takes. Probably a couple of weeks but maybe longer.
So Day 30 is coming, I promise. No specific date. When it lands, it’ll come with something formalized that’s actually worth pointing you at.
Thank you for sticking with me for twenty-nine days. Day 30 won’t be a manifesto. It’ll be informative, but it will also be a call to action.
So, the most important post in the series is NOT the one I’m not going to publish tomorrow.
I’d appreciate a follow. You can subscribe with your email below. The emails go out once a week, or you can find me on Mastodon at @[email protected].
-
Day 16: How the World Agreed on a Date Format (Except the US)
There is a war that has been quietly raging for about a century, and it is fought over six little characters.
05/06/26In the United States, that is May 6, 2026. In most of Europe, it is June 5, 2026. In Japan, which uses a year-month-day order and frequently uses the imperial calendar, the
05might be read as year 5 of the Reiwa era (2023). In Iran, the entire premise is wrong, because Iran’s official calendar is the Solar Hijri and the current year is 1405.So here we are. A date written by one person and parsed by another is, in the general case, an act of faith.
The most consequential standards effort of the late 20th century was an attempt to end this. It succeeded, sort of. The format it produced,
2026-06-08T14:30:00Z, looks unremarkable now, but it represents a multi-decade campaign to drag the world’s date conventions into a single, unambiguous, machine-parseable shape.That standard is ISO 8601, and the story of how it won is the story of why your API logs look the way they do.
What’s actually wrong with
05/06/26Let me give you the tick of the tock (lay of the land). Every culture has a different intuition about which number comes first in a date, and none of them is wrong.
In the United States, the convention is month-day-year. This descends from spoken American English, “May sixth, twenty-twenty-six,” where the month comes first in speech.
In most of Europe, Latin America, and much of Asia, the convention is day-month-year. “The fifth of June, twenty-twenty-six.” The day is the first specific element.
In East Asia, the convention is year-month-day, written largest-unit-first. This reflects a linguistic preference for going general-to-specific that runs the opposite direction of the English phrasing.
Who is to say which is more correct than another? The problem is that a single string of digits separated by slashes can mean three different things depending on who wrote it, and there is no way other way to tell.
When the ambiguity bites
It’s not just an annoyance.
International travel figured this out the hard way. Across global passport documentation, the convention settled on a three-letter month abbreviation:
08 JUN 2026. It’s unambiguous because no month is named06. The international passport standard (ICAO Doc 9303) mandatesJANthroughDECfor exactly this purpose.In healthcare, patient safety organizations have flagged date ambiguity as a documented source of medication error: a chart that says
7/8/09can be read as July 8 by one clinician and August 7 by another.The shape of the problem is the same across medicine, aviation, logistics, contracts, and customs declarations. Different conventions lead to confusion and errors.
The standard
In 1988, ISO published
ISO 8601:1988. Pick one format, make it unambiguous, make it sort lexicographically, make it machine-parseable, and standardize the world on it.The format they picked:
2026-06-08T14:30:00ZThe choice of
YYYY-MM-DDwas deliberate. Year-month-day is the East Asian convention, but it has a technical property that the other two don’t: it sorts correctly as a string.2025-12-31comes before2026-01-01whether you sort by character or by number.12/31/25and01/01/26do not. For the emerging computing industry of the late 1980s, databases, log files, file systems, this was a decisive advantage.The capital
Tseparates the date from the time. Not pretty, but unambiguous. The trailingZ(informally pronounced “Zulu”) means UTC. This timestamp has no timezone offset, it is anchored directly to UTC.What actually use: RFC 3339
ISO 8601 is too permissive for engineering use.
It allows fractional seconds. It allows omitting components. It allows the basic form (
20260608T143000Z) without separators. It allows week dates and ordinal dates. It allows24:00:00as midnight (this was removed in 2019, then reinstated by amendment, in one of those standards-committee compromises that satisfies no one).So in 2002, the IETF published
RFC 3339. RFC 3339 is a profile of ISO 8601, a strict subset that picks one form and forbids the rest. The basic form is disallowed. Week dates are disallowed. The time component is mandatory. The timezone designator is mandatory.This is what every modern internet API actually uses. GitHub, AWS, Stripe, Cloudflare, OpenAI. They accept RFC 3339, not full ISO 8601. They reject
20260608T143000Zeven though it’s legal ISO 8601.What everyone calls “ISO 8601” in casual conversation is, almost always, RFC 3339.
What ISO 8601 isn’t
A few things worth being clear about:
- ISO 8601 is not UTC. UTC is a timescale. ISO 8601 is a format.
- ISO 8601 is not Unix time. Unix time is the integer
1781055000. ISO 8601 is the string2026-06-08T14:30:00Z. They can represent the same instant. They are not the same thing. - ISO 8601 does not solve leap seconds. The format permits
:60in the seconds field, but what to do with such a value is implementation-defined. - ISO 8601 does not include the calendar system. It assumes the Gregorian calendar. No provision for Islamic, Hebrew, or Buddhist calendars.
The civilizational payoff
There is a sense in which
2026-06-08T14:30:00Zis the most consequential string format in modern computing.While legacy systems still cling to their own formats—HTTP headers use RFC 1123, Git and JWTs use integer Unix timestamps, and X.509 certificates use ASN.1—RFC 3339 has conquered the modern web. It is the default serialization for datetime objects in modern programming languages. It appears in the JSON payloads of almost every modern API (GitHub, Stripe, AWS, OpenAI). It is the standard format for XML’s
xs:dateTime. It is written into millions of cloud infrastructure log lines every second.It is the closest thing modern technical infrastructure has to a universal vocabulary for the question “when did this happen?"
It won because it was unambiguous and sortable, and a single committee was willing to pick one of three equally valid cultural conventions and tell the other two cultures to deal with it. Most international standards die in the negotiation. ISO 8601 survived because the technical advantages of
YYYY-MM-DDwere strong enough to overwhelm the political cost.Us Americans haven’t adopted it (yet). We still write
06/08/2026on bank checks, forms and filings, but the machines we all use are on 8601 and they are doing most of the talking.
Sources
- Japanese era name — Wikipedia — Reiwa began 1 May 2019; Reiwa 5 = 2023.
- Solar Hijri calendar — Wikipedia — year 1405 began 21 March 2026, ends 21 March 2027.
- ISO 8601 — Wikipedia — first published 1988; ISO 8601-1:2019 removed
24:00; the 2022 amendment reinstated it. - ISO 8601-1:2019/Amd 1:2022 — the amendment that put
24:00:00back. - RFC 3339 — Date and Time on the Internet: Timestamps — IETF, July 2002. Profile of ISO 8601 used by most modern APIs.
- RFC 3339 vs ISO 8601 — visual map of which forms each standard accepts; basic form (
20260608T143000Z) is valid ISO 8601 but not RFC 3339. - Machine-readable passport (ICAO Doc 9303) — Wikipedia — ICAO standard requiring three-letter month abbreviations (
DD MMM YYYY) in the visual inspection zone of all passports. - ISMP List of Error-Prone Abbreviations — highlights the risk of ambiguous documentation and dates in medical records.
- RFC 1123 — Requirements for Internet Hosts — specifies the required date format for HTTP Date headers.
I’d appreciate a follow. You can subscribe with your email below. The emails go out once a week, or you can find me on Mastodon at @[email protected].
Tomorrow: Unix time, the second-counting system that runs under every timestamp you’ve ever seen, and the rollover problem that hits in 2038.
Programming Software development 30daysoftime Standards Iso8601
-
AI Code Reviewers Won't Save You
Dropping an AI reviewer into your pull request pipeline is just a band-aid. Tools like CodeRabbit or Greptile are great for catching syntax errors or basic anti-patterns, but they can’t assess architectural intent or domain-specific business logic. They’re spell-checkers for code. Useful, sure. But nobody ever said “our codebase is solid because we run spell check.”
AI doesn’t change your engineering baseline. It just accelerates it. If your foundational guardrails are weak, agentic tools will help your team generate technical debt at unprecedented speeds. So the real question isn’t “how do we review AI code?” It’s “how do we build systems that prevent slop from ever reaching production?”
Shift Left, Hard
When engineers use agents to scaffold a new Go service or spin up a SvelteKit frontend, they’re inevitably pulling in generated dependencies or utilizing unfamiliar libraries. Models hallucinate packages. They suggest insecure patterns with total confidence.
Your CI pipeline needs to be ruthless before a human ever looks at the code. Aggressive SAST and SCA should automatically block PRs that introduce vulnerable dependencies or hardcoded secrets. If the agent generates slop, the pipeline rejects it instantly. No discussion.
Make the Agents Write the Tests
Agents are incredibly eager to generate feature code, but humans are historically lazy about writing the tests for it. The influx of AI-generated code means human reviewers can’t possibly step through every logic branch manually.
So flip the script. Use the agentic tools to build the guardrails themselves. Mandate that any generated feature code must be accompanied by generated, human-verified unit tests. If an agent writes a sprawling TypeScript function, the build should fail if the test coverage doesn’t meet a strict threshold. You’re already using AI to write the code. Use it to prove the code works, too.
Context Boundaries Matter
Bloated AI output often happens because the model is given too much context or allowed to generate too much at once. Heavyweight IDEs with aggressive multi-file auto-completion can easily create cascading messes across a codebase.
Define strict architectural boundaries and API contracts upfront. Agents should be tasked with solving small, well-defined, modular problems. “Write a function that parses this specific JSON schema” is a good prompt. “Build the backend” is not. The tighter the scope, the less room for generated nonsense.
Observability Is Your Safety Net
You can’t catch all generated slop at the PR level. Some of it only reveals itself under load. An agent might write a technically correct query that causes an N+1 database issue, or introduce a subtle memory leak that passes all unit tests.
Your ultimate safety net is what happens at runtime. You need an airtight observability stack to trust the velocity AI brings. Logs, distributed tracing, metrics, all feeding into dashboards your team actually watches. When generated code hits staging, you need the immediate telemetry to spot performance regressions before they reach production.
Redefine the Human Review
Because AI makes the “typing” part of coding trivial, the human code review needs to fundamentally shift. Reviewers should no longer be looking for missing semicolons. They should be asking: “Does this component fit our architecture?” and “Did the agent over-engineer this solution?”
Train your senior engineers to review for intent and systemic impact. That’s the stuff AI genuinely can’t do yet. Leave the syntax checking to the robots.
I’d appreciate a follow. You can subscribe with your email below. The emails go out once a week, or you can find me on Mastodon at @[email protected].
-
Your AI Agent Needs a Task Manager
If you’ve spent time working with AI coding tools, you’ve probably hit the compaction wall. Suddenly, your agent knows what it’s currently working on but has completely forgotten the five other things connected to it.
This is the memory problem, and it’s a big one.
The Context Window Isn’t Enough
Your AI agent needs some sort of memory system that lives outside the context window. When you’re working on simple, one-off tasks, the chat-as-workspace approach works fine. You ask a question, you get an answer, you move on. But the moment you’re tackling a complex set of related tasks? It breaks down fast.
I’ve been thinking about this through the lens of a framework I’m calling the Agentic Maturity Model. The short version is that there are distinct levels to how teams and developers use AI agents, and moving between levels isn’t about using “better” tools, but rather it’s a shift in how you approach the work.
Four months ago, there were no real options. The good news? It seems like all the model providers recognize this is the next frontier. Memory and persistence are where I’m looking for the actual progress to happen next.
Claude Code has certainly gotten better in these areas over the last couple of months. They’ve added an auto memory feature in beta. They added a lightweight Tasks system based on a Todo system called Beads built by Steve Yegge. His key idea was that the task state should live outside the context window.
These are meaningful building blocks towards an actual working memory system that persists across sessions and survives compaction.
We’re Almost There
The tooling and harneses we built on top of the LLMs are already changing how software gets built, but where we are headed? Here is what I think:
- auto-improving memory: where the agent learns your patterns, your codebase, your preferences
- persistent task tracking that survives compaction: Tasks, todos, issues, whatever you want to call them. The point is they exist outside the conversation.
When those two pieces come together properly, the workflow for everyone will change again.
Your agent doesn’t just respond to the current prompt. It knows where it is in a larger plan, what’s been done, what’s blocked, and what’s next. That’s the difference between a helpful chatbot and an actual collaborator.
We are so close I can taste the blood in the water, oh wait, that’s mine. ☠️
-
Why Testing Matters
There is a fundamental misunderstanding about testing in software development. The dirty and not-so-secret, secret, in software development is TESTING is more often than not seen as something that we do after the fact, despite the best efforts from the TDD and BDD crowd.
So why is that the case and why does TESTING matter?
All questions about software decisions lead to a maintainability answer.
If you write software that is intended to be used, and I don’t care how you write it, what language, what framework or what your background is; it should be tested or it should be deleted/archived.
That sounds harsh but it’s the truth.
If you intended to run the software beyond the moment you built it, then it needs to be maintained. It could be used by someone else, or even by you at a later date, it doesn’t matter. Test it.
if software.intended_to_run > once: testing = requiredThat’s just the reality of the craft. Here is why.
Testing Is Showing Your Work
Remember proofs in math class? Testing is the software equivalent. It’s how you show your work. It’s how you demonstrate that the thing you built actually does what you say it does, and will keep doing it tomorrow.
Chances are your project has dependencies. What happens to those dependencies a month from now? Five years from now? A decade?
Code gets updated. Libraries evolve. APIs change. Testing makes sure that those future dependency updates aren’t going to cause regression issues in your application.
It’s a bet against future problems. If I write tests now, I reduce the time I spend debugging later. That’s not idealism, it’s just math.
T = Σ(B · D) - CWhere B = probability of bug, D = debug time, C = cost of writing tests and T is time saved.
Protecting Your Team’s Work
If you’re working on a team at the ole' day job, you want to make sure that the code other people are adding isn’t breaking the stuff you’re working on or the stuff you worked on six months ago, add tests.
Tests give you that safety net. They’re the contract that says “this thing works, and if someone changes it in a way that breaks it, we’ll know immediately.”
Without tests, you’re essentially hoping for the best and hope isn’t good bet when it comes to the future of a software based business.
Your Customers Are Not Your QA Team
Auto-deploying to production without any testing or verification process? That’s just crazy. You shouldn’t be implicitly or explicitly asking your customers to test your software. It’s not their responsibility. It’s yours.
Your job is to produce software that’s as bug-free as possible. Software that people can rely on. Reliable, maintainable software, that’s what you owe the people using what you build.
Bringing Testing to the Table
Look, I get it. Writing tests isn’t the most fun part of the job. However, a lot has changed in the past couple of years. You might have heard about this whole AI thing? With the Agents we all have available to us, we can add tests with as little as 5 words.
“Write tests on new code.”
Looking back at that forumla for C, we can now see that the cost of writing tests is quickly approaching zero. It just takes a bit of time for the tests to be written, it just takes a bit of time to verify the tests the Agent added are useful.
Don’t worry about doing everything at the start and setup a full CI pipeline to run the tests. Just start with the 5 words and add the complicated bits later.
No excuses, just testing.
-
Why Data Modeling Matters When Building with AI
If you’ve started building software recently, especially if you’re leaning heavily on AI tools to help you code—here’s something that might not be obvious: data modeling matters more now than ever.
AI is remarkably good at getting the local stuff right. Functions work. Logic flows. Tests pass. But when it comes to understanding the global architecture of your application? That’s where things get shaky.
Without a clear data model guiding the process, you’re essentially letting the AI do whatever it thinks is best. And what the AI thinks is best isn’t always what’s best for your codebase six months from now.
The Flag Problem
When you don’t nail down your data structure upfront, AI tools tend to reach for flags to represent state. You end up with columns like
is_draft,is_published,is_deleted, all stored as separate boolean fields.This seems fine at first. But add a few more flags, and suddenly you’ve got rows where
is_draft = trueANDis_published = trueANDis_deleted = true.That’s an impossible state. Your code can’t handle it because it shouldn’t exist.
Instead of multiple flags, use an enum:
status: draft | published | deleted. One field. Clear states. No contradictions.This is just one example of why data modeling early can save you from drowning in technical debt later.
Representation, Storage, and Retrieval
If data modeling is about the shape of your data, data structures determine how efficiently you represent, store, and retrieve it.
This matters because once you’ve got a lot of data, migrating from one structure to another, or switching database engines—becomes genuinely painful.
When you’re designing a system, think about its lifetime.
- How much data will you store monthly? Yearly?
- How often do you need to retrieve it?
- Does recent data need to be prioritized over historical data?
- Will you use caches or queues for intermediate storage?
Where AI Takes Shortcuts
AI agents inherit our bad habits. Lists and arrays are everywhere in their training data, so they default to using them even when a set, hash map, or dictionary would perform dramatically better.
In TypeScript, I see another pattern constantly: when the AI hits type errors, it makes everything optional.
Problem solved, right? Except now your code is riddled with null checks and edge cases that shouldn’t exist.
Then there’s the object-oriented problems. When building software that should use proper OOP patterns, AI often takes shortcuts in how it represents data. Those shortcuts feel fine in the moment but create maintenance nightmares down the road.
The Prop Drilling Epidemic
LLM providers have optimized their agents to be nimble, managing context windows so they can stay productive. That’s a good thing. But that nimbleness means the agents don’t always understand the full structure of your code.
In TypeScript projects, this leads to prop drilling: passing the entire global application object down through nested components.
Everything becomes tightly coupled. When you need to change the structure of an object, it’s like dropping a pebble in a pond. The ripples spread everywhere.
You change one thing, and suddenly you’re fixing a hundred other places that all expected the old structure.
The Takeaway
If you’re building with AI, invest time in data modeling before you start coding. Define your data structures. Think about how your data will grow and how you’ll access it.
The AI can help you build fast. But you still need to provide the architectural vision. That’s not something you can blindly trust the AI to handle, not yet, anyway.