15% off - September See services
Gabe Giro

Zero Billable Hours, and the Case for a Single Writer

My dashboard showed zero billable hours for a client I'd worked all day. The bug wasn't the dashboard. Five code paths all marked tasks done and only one wrote the ledger. This is why state needs exactly one writer.

EngineeringSoftware ArchitectureAutomation
Book a code review

My weekly dashboard showed 0 billable hours for a client I had been working on all day. The bug was not the dashboard.

Five different paths in my stack marked tasks done. An end-of-session skill closing out the day. An agent-merge flow after a PR ships. A manual "Done" button in my local IDE's task modal. An "Archive" button in the IDE's research tab. A swarm finishing a piece of work autonomously. Five paths, all of them writing the same conceptual event, and only one of them wrote a row to the per-client time ledger.

Five ways a task got marked done

  • End-of-session skill

    Closes out the day. Wrote the ledger.

  • Agent-merge flow

    Fires after a PR ships. Did not.

  • IDE 'Done' button

    The one I trusted most. The worst offender.

  • IDE 'Archive' button

    Research tab. Did not.

  • Autonomous swarm

    Finishes work on its own. Did not.

The button I trusted most was the worst offender

The IDE I had built, the one I trusted because it had nice buttons, was the offender. Its "Done" handler bypassed the canonical archive script entirely. It did an inline file write, then a partial kanban shortcut that removed the card from one board but never added it to the Done column on the project board. Seven billable cards stranded in PR Review. Zero rows in the time log.

This is the shape of every convenience path. Each one shipped in an afternoon and solved a real problem locally. Each one reimplemented a slice of what the canonical writer already did, minus the parts that were not front of mind that day. The time-log row was not front of mind when I wrote a "Done" button. So the button worked, the dashboard lied, and the drift stayed invisible until a number I trusted came back wrong.

The fix was three lines of design

Before

Five paths each mark tasks done their own way. One writes the ledger, the others silently do not.

N writers, N minus 1 ways to drift

After

One canonical archive script owns the write. Every button and automation routes through it.

One writer, one path

The change was structural, not a new feature.

Three moves. First, extend the canonical archive script: on a done task with an hourly rate above zero, auto-append a row to the client's time log with the estimate, the date, and the slug, idempotent on the pair of done-date and slug. Second, route every done-path through that script, including the IDE buttons. That replaced 30 lines of inline file mutation with a single subprocess call. Third, delete the parallel time-log code in the wrap-up skill. One writer, one path.

There was a bonus. When I ran the new script on the six already-done tasks to backfill the missing rows, it also caught the three cards still stuck in PR Review and moved them to Done. One pass repaired both the ledger and the kanban, because both had drifted from the same missing writer.

When state lives in N places

When state lives in N places, you have N minus 1 bugs waiting for a dashboard to surface them.
Gabe Giro

The rule is older than the code. Every convenience path that bypasses the canonical writer is a future silent-drift incident. The trap is that the bypass always looks reasonable at the point you write it, because you are solving the visible problem and the invisible invariant is somebody else's code. The discipline is boring and it holds: build features that compose through the canonical writer, never around it. When you find yourself reimplementing a slice of what a shared writer already does, that is the signal to call the writer, not to copy it.


Same lens, different symptom: Spot and Fix Messy Code is about the drift you can see in the code. This one is about the drift you cannot, until a number comes back wrong.

If you suspect your own stack has convenience paths quietly diverging from the canonical one, that is exactly what a review surfaces. Book a code review and I will find the writers that should have been one.

Gabe Giro

Stay in the loop

Practical thoughts on engineering leadership, Android, and AI. No spam, unsubscribe anytime.