kova-ops
the automation layer
Four systems that run Kova without me watching: the safety hooks that keep an AI agent off the live store, the books, the delivery routing and the content pipeline. Every line of code is Claude Code’s, written to my spec; the decisions, the tests and the running of it are mine. A revenue operations or GTM engineering seat does this work for a company; I did it for a business of one.
I specced and tested everything in amber. The rest are systems Kova already runs on.
What the 97 tests check
Two suites, run together, built against real throwaway repositories — one dirty, one clean — rather than mocks, because half of what these hooks decide depends on whether there is anything to lose.
- 20theme-push-guard · must ask. Anything that reaches production: a push to
mainin seven spellings, a merge onmain,shopify theme push, andgh pr mergein four forms — including when a compound commandcds into the theme first. - 14theme-push-guard · must pass silently. Ordinary work: branch pushes,
git status,git commit,shopify theme dev, a push in the docs repo, andgrep -rn 'push'. A guard that cries wolf gets routed around. - 5theme-push-guard · permission mode.
askin every mode a human can answer;denyunderbypassPermissions, where a prompt reaches nobody and the run would hang on a question no one sees. - 13theme-push-guard · the second deploy-connected repo. The guard used to key on one directory name. A repo can be a directory called
siteand only the origin remote says which site, so it keys on the basename or the remote slug — matching on basename alone would guard a repo nobody asked for. Six assertmainstill asks; seven assert the preview branch stays silent, because shipping there is the intended workflow rather than a risk. - 7theme-push-guard · heredoc tokenising.
shlexdoes not know what a heredoc is, sopython3 - <<'PY'handed every word of the body to the tokeniser as an ordinary argument — which blocked a legitimate push whose only sin was naming a guarded repo in prose inside a doc-writing heredoc. Bodies are stripped before tokenising; the push itself lives outside the body and is still parsed normally. - 7git-destructive-guard · history. Always ask, even on a clean repo:
reset --hard <ref>,branch -D,stash drop,stash clear, force push. - 8git-destructive-guard · working tree. Ask only when something is at risk. The same command is a question on a dirty repo and silence on a clean one, which is why the suite needs real repos.
- 9git-destructive-guard · ordinary work. Must pass silently — including
git clean -n, a dry run, andgit branch -d, the safe delete. - 5git-destructive-guard · the
cdblind spot. The hook payload'scwdis where the shell was BEFORE the command runs, socd <other repo> && git clean -fdwas judged against the wrong repo and waved through. Found by probing, not by reading. - 5git-destructive-guard · permission mode. The same ask/deny split.
- 4both guards · the exit contract. The one that matters most. A deny is
exit 2on stderr, not advisory JSON on stdout — so it stops the call instead of being printed and ignored. Two cases per guard assert the deny path writes nothing to stdout and the ask path staysexit 0.
- Shopifyorders
- Squarepayments
- Xerobooks
- Notioncanon
- Gmailcomms
- Cloudflaredeploy
Two hooks stand between the agent and anything it can break: a deploy reaching the live storefront, and a destructive git command.
What it taught me: I found three production bugs in these hooks by probing them with crafted payloads. Reading the code said they were fine.
What one sitting does
The mechanical tier only, and I sit with it. Everything below is a rule. The moment it takes judgment, the gate hands it back.
- 1Read. Square card takings, Shopify payouts, and six Xero bank accounts.
- 2Match on rules and dates. Internal transfers, Shopify invoice matches, rule-driven deposits.
- 3Stop at expense coding. Business-versus-personal is a tax position rather than a rule, so a human answers it. This is the boundary, not a gap.
- 4Result. 95 unreconciled items to zero, all six accounts green, receivables $656.65 to $0.00.
- Xero6 accounts green
- receivables$656.65 → $0
- expense codingstays mine
One sitting: 95 unreconciled items to zero, all six bank accounts green, receivables $656.65 → $0.00. Expense coding is deliberately NOT automated — business-vs-personal is a tax position, so the gate stops there and a human answers it.
What it taught me: where to stop. Expense coding is not automated, because business-versus-personal is a tax position and not a rule, so the gate hands it back to a human.
How a delivery run gets built
Live orders in, one map link out. Two of these four steps exist only because a plausible shortcut was measured and failed.
- 1Pull. Live unfulfilled Shopify orders, addresses as written.
- 2Find the departure time by binary search. The latest you can leave and still make every stop inside the hard 9pm cap.
- 3Check the freezer against capacity before anything is loaded, and make the defer-or-deliver call on a late order.
- 4Emit one Google Maps link with every stop in order. A ZIP-centroid fallback was tested for the geocoding and rejected: four miles off.
- 5Hand it to an agent. The same solver runs as an MCP server with two schema-declared tools,
solve_routeandlatest_departure, so an agent can plan the run instead of a person reading a terminal. 23 tests drive it over the protocol itself; six of them are malformed calls the server has to survive.
- Mapsone-tap run
- freezer checkcapacity
- defer calllate orders
Orders in, an ordered run and a single map link out. The departure time is found by binary search — the latest you can leave and still make every stop. A ZIP-centroid fallback was tested and rejected: four miles off.
What it taught me: the ZIP-centroid fallback looked reasonable and was four miles wrong. It only failed once I measured it against real addresses.
The three stages
A library of 46 raw shoots and 46 finals, each with a textless master, so a cut can be re-subtitled or re-hooked without going back to the footage.
- 1Transcribe. The spoken take becomes an SRT with timings.
- 2Derive the cut list. Offsets differ from month to month, so the timing is measured per shoot rather than assumed — that assumption is what used to put every cut a beat late.
- 3Assemble. One keystroke, and the result is a rough cut plus a textless master.
- textless masters46 finals
- Notioncalendar
A three-stage pipeline: transcribe, build the cut list from the transcript, assemble. It runs against a library of 46 finished pieces with clean textless masters, which is what makes re-cutting old footage cheaper than shooting new.
What it taught me: re-cutting the library is cheaper than shooting new, but only because I kept the masters textless months earlier.