DevOps
-
Deploying Hermes Agent With Ansible Without Creating a Snowflake
I have a home server with Plenty of RAM and no useful GPU, so running a local model was never the interesting part of deploying Hermes Agent. The interesting part was making the agent setup repeatable.
I could have pasted a
docker runcommand over SSH and called it finished. It would have worked. But “it works” and “I can rebuild this server six months from now” are two very different things.So I built an Ansible role around the official Hermes container, authenticated with a ChatGPT subscription through the
openai-codexprovider, and left the one interactive step, OAuth, outside Ansible. That split is the whole idea. Ansible owns the infrastructure. Hermes owns its refresh token. I own the browser login.
Start With the Deployment Contract
Before writing a single task, decide what the role is promising. Mine had five rules:
- Pin the image by version and digest. A mutable
latesttag is not a deployment plan. - Persist
/opt/data. Hermes keeps configuration, sessions, skills, memories, and authentication state there. - Publish the API on
127.0.0.1only. A private Docker network handles future service-to-service access. - Bound the process. 4 GiB of RAM, 2 CPUs, 256 PIDs, bounded logs, and agent loop limits.
- Keep OAuth manual. Ansible creates the service, then the operator completes the device-code login over SSH.
The role layout is conventional:
defaults/main.yml,handlers/main.yml,tasks/main.yml, and templates for the Compose file, the Hermes config, the env file, and a host wrapper script.Put every value you expect to tune in
defaults/main.yml:hermes_agent_home: /home/youruser/Web/hermes-agent hermes_agent_data_dir: "{{ hermes_agent_home }}/data" hermes_agent_image: >- nousresearch/hermes-agent:v2026.8.3@sha256:<tag> hermes_agent_api_port: 8642 hermes_agent_publish_host: "127.0.0.1" hermes_agent_model_provider: openai-codex hermes_agent_model_name: gpt-5.6-terra hermes_agent_memory_limit: 4g hermes_agent_cpu_limit: "2.0" hermes_agent_pids_limit: 256Use a release and digest you have reviewed. The version above is what I deployed, not a promise that it is still the right one when you read this.
The Compose template turns those defaults into an enforceable boundary:
services: hermes: image: {{ hermes_agent_image }} command: ["gateway", "run"] restart: unless-stopped env_file: - ./hermes.env volumes: - {{ hermes_agent_data_dir }}:/opt/data ports: - "127.0.0.1:8642:8642" mem_limit: 4g cpus: "2.0" pids_limit: 256 security_opt: - no-new-privileges:true healthcheck: test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8642/health"] interval: 10s retries: 12 logging: options: max-size: "10m" max-file: "5"The loopback bind matters. The official Hermes Docker docs recommend authenticated access for exposed services, and specifically call out SSH tunnels or private networking as the safer way to reach a loopback-bound dashboard. I don’t need the API listening on every interface just because Docker makes that easy.
Make Ansible Own Setup and Proof
The role should do more than render YAML. It should reject unsafe input before it mutates anything, create persistent directories with deliberate ownership, reconcile Compose, and prove the service came back.
- name: Validate Hermes configuration ansible.builtin.assert: that: - hermes_agent_api_key | length >= 32 - hermes_agent_publish_host in ['127.0.0.1', '::1'] - hermes_agent_model_provider in ['openai-codex', 'openai-api', 'openrouter'] no_log: true - name: Render protected environment ansible.builtin.template: src: hermes.env.j2 dest: "{{ hermes_agent_home }}/hermes.env" mode: "0600" no_log: true - name: Reconcile Hermes Compose project community.docker.docker_compose_v2: project_src: "{{ hermes_agent_home }}" state: present pull: missingNotice the two uses of
no_log. An encrypted variable is protected at rest, but Ansible will happily reveal the decrypted value in task output or--diff. Secret-bearing template and validation tasks should not print their inputs.I encrypted only the API bearer key, not the whole variables file:
ansible-vault encrypt_string \ --vault-password-file vault_password_file \ --stdin-name vault_hermes_agent_api_keyType the value, press Ctrl-D, paste the resulting
!vaultblock into your group vars, and let ano_logassertion check its length. You don’t need to print the plaintext back into your terminal to prove Ansible can decrypt it.I also wanted keyless web search, so the role installs a pinned DDGS package into persistent storage with
uv, notpip:- name: Install pinned DDGS community.docker.docker_container_exec: container: hermes-agent user: hermes argv: - /usr/local/bin/uv - pip - install - --python - /opt/hermes/.venv/bin/python - --target - /opt/data/lazy-packages - --reinstall - ddgs==9.14.4The follow-up check imports
DDGS, verifies the version, and confirms/opt/data/lazy-packagesis onsys.path. Checking for a metadata directory is not enough.One gotcha from the release I deployed: do not render
HERMES_YOLO_MODE=0. Its mere presence still triggered the YOLO banner. If you want manual approvals, omit the variable entirely and set the approval mode in the Hermes config instead.OAuth stays manual, because browser authentication is an operator action, not configuration management:
ssh user@homelab docker exec -it hermes-agent hermes auth add openai-codex --no-browser docker exec hermes-agent hermes auth status openai-codexThe refreshable credential lands under the persisted
/opt/datadirectory. Do not copy your laptop’s~/.codexdirectory into the container just to skip one login.
Make Daily Use Boring Too
The last piece was a host command. I wanted to SSH into the server, type
hermes, and resume the last session without remembering a Docker incantation.#!/bin/sh set -eu if [ "$#" -eq 0 ]; then set -- --continue fi if [ -t 0 ] && [ -t 1 ]; then exec docker exec -it -w /opt/data/workspace hermes-agent hermes "$@" fi exec docker exec -i -w /opt/data/workspace hermes-agent hermes "$@"Install that template as
/usr/local/bin/hermeswith mode0755. Arguments pass through, sohermes --helpandhermes auth status openai-codexwork from the host too.Then run the play three times: once with
--check --diffto preview, once to deploy, and once more to confirm it reportschanged=0. Idempotence you haven’t observed is idempotence you’re guessing at. Finish by checking the container is healthy and the port is where you think it is:docker inspect --format '{{.State.Health.Status}}' hermes-agent docker port hermes-agentThat is the difference between a container I happen to have running and a service I know how to rebuild. The manual OAuth step isn’t a failure of automation, it’s a clean boundary around a human credential flow.
Exactly what I want from Ansible.
Sources & References
- Hermes Agent Docker guide — official container deployment docs
- Hermes Agent provider documentation — including
openai-codex - Ansible Vault: encrypting individual variables
- community.docker.docker_compose_v2
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].
- Pin the image by version and digest. A mutable
-
Boring Is a Feature
What does boring look like in the age of AI? And I’m not talking about uninteresting. I’m talking about highly maintainable.
JavaScript?
I mean I guess models are good at it. Everybody knows it. It runs everywhere. The biggest problem with JavaScript is that TypeScript is better.
Certainly it’s better than picking a novelty language that you haven’t built anything with before. Does anybody on the team actually know Haskell? And how long ago did they know Haskell? You need to evaluate the cost of adopting it just as you would evaluate how long it would take to learn it and train the team on it.
The upfront cost can be easier to measure. But the recurring ones are much harder to predict. What happens when a maintainer moves on from a package that everyone uses, and the speed it takes to find a new maintainer is not as fast as you need it to be?
Boring tools are ones where the recurring cost of maintenance is as close to zero as you can get it. If you come back to it in eight months, it should work the way you remember it. This is a fairy tale that we tell ourselves, that nothing ever changes and we can control that change.
Is boring even possible in the age of AI, when it feels like everyone has their own particle beam cannon that they can point at your codebase?
I think it’s worth talking about what I mean by boring, because it can be used as a synonym for old, but that’s not what I mean. Boring means predictability.
Take all your npm packages. Can you answer these questions about all of them? Probably not.
- How often do the release notes contain the word “breaking”? Skim a year of them. This is the single best signal available and it takes ten minutes.
- How many people can merge? One is a risk regardless of how good that one person is. People change jobs, burn out, and lose interest.
- What happens to old versions? A project that supports the previous major for a while is telling you something about how it thinks about your time.
- Can you read the source? Not all of it. Enough to fix something yourself when you’re blocked and nobody’s answering.
Learn the new tool. Experiment. Try new things. Stay passionate about software. Just because you can use the new thing doesn’t mean you should.
Don’t always pick the boring option, just like you don’t always pick the new option. It takes wisdom to know what the right answer is.
You have to understand your failure modes, and when it’s an appropriate time to take a risk, and the scale of the risk.
Pick boring for the parts you don’t want to think about. Save the interesting decisions for the places where being interesting is the point.
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 Developer-tools Software-development Engineering Tooling
-
The Best Automation Has a Manual Escape Hatch
Automation earns trust by being easy to override, not by being impossible to question.
That sounds backwards. The pitch for automating something is usually that it removes the human, and a system you keep reaching into feels like a system that didn’t finish the job. But the automation you actually trust, over years, is the one you know you can stop.
Most automation that you set up is enforcing some sort of policy, and that’s right most of the time, but not always.
The mistake isn’t automating a default way of working. It’s building a system where the default is ingrained so deeply that there’s no way out of it.
The automation must be flexible. You must be able to adapt the automation as the requirements change.
Do you have contingency plans on what to do if the automation fails?
Now I’m not talking about how to get around the automation, or always forcing an outcome that disables the automation. Instead, I’m talking about what a real escape hatch looks like.
It’s one operation. You run a command. You don’t perform a sequence of five steps where forgetting the third leaves things inconsistent.
It maintains the invariants. This is the big one. When I override a post’s date, the file and the database both get updated. If the override only touched one of them, I’d have created a split-brain problem in the name of fixing a scheduling problem.
It’s discoverable. It shows up in the help output next to everything else. An escape hatch nobody knows about is not a feature, it’s trivia.
It’s supported, not tolerated. It has tests. It survives refactors. Nobody has to feel clever for using it.
If your answer to “what if the automation is wrong” is “go around it manually,” you don’t have a hatch. You have a hazard with a tradition attached.
If you design the escape hatch first, it forces a question that’s worth thinking about. At least what happens when the automation is wrong. What are your plans to do something about it?
Log When the Hatch Gets Used
Don’t forget about the log. It’s not one that you should skip over. You should be logging when your escape hatch gets used, even if it only happens once a quarter.
You probably don’t need to update your policy every time. But your escape hatch log is a good indication of when you might consider updating the policy.
Building an escape hatch changes the risk. The worst case is not that the tool did something irreversible, but rather that the tool did something I fixed in one command.
So build the hatch. Make it one command, make it maintain your invariants, put it in the help text, and count how often it gets pulled.
The automation you trust isn’t the one that’s always right. It’s the one you know you can overrule.
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].
-
Run Your Whole Agent Stack on a $5 Box
I SSH’d into my home server this afternoon and ran
docker statson the memory layer that every one of my coding agent sessions talks to. Here’s what came back:mem0-qdrant 28.09MiB / 60.75GiB 2.13% mem0-neo4j 612.7MiB / 60.75GiB 0.77%640 megabytes. Vector store and graph store, both up for three weeks straight, serving every
rememberandrecallcall my agents make. The entire persistent memory for my AI tooling uses less RAM than one Chrome tab with Figma open.So let’s talk about why you’re paying a monthly subscription for this.
Local-First, Not Local-Only
I want to be precise here, because “self-hosted AI” has become a phrase people use to mean nine different things.
My setup is local-first, not local-only. The state lives on my hardware. The memories, the embeddings, the graph relationships, everything my agents have learned about my projects, all of it sits on a box I own, in a Docker volume I can
tarand carry away. Nobody can deprecate it, price-hike it, or sunset it.The inference does not. My embedder points at Mistral’s managed API. I’ll get to why, and how to swap it, but I’m not going to pretend otherwise in a post about self-hosting.
That embedder is the only thing that leaves my network, and only when something actually gets embedded, so when writing a memory and searching for one. Listing, deleting, and every graph operation are local with zero API calls.
State is what you can’t get back. Compute is a commodity you rent by the token. Losing access to an API means switching providers. Losing two years of accumulated project context means starting over.
The Four Pieces
Qdrant is semantic search. When I ask what it remembers about my package manager preferences, Qdrant turns that into a similarity query and hands back the relevant memories. It’s Rust, it’s fast, and at 28MB resident it’s essentially free to run. One gotcha: vector dimensions are fixed when the collection is created. Swap embedding models and you need a new collection, not a migration. I learned that the annoying way.
Neo4j is the graph store. Vectors are great at “find me things that sound like this” and bad at “what depends on what.” The graph holds explicit subject-predicate-object facts, so
project Xbuilt_withPython 3.13is a traversable edge instead of a fuzzy match. It’s the heavy one at 613MB, but it’s a JVM, so that’s mostly heap floor rather than working set. If you’re squeezing onto the smallest possible VPS, interrogate this one first.mem0 is the orchestration on top: what gets extracted from a conversation, what gets deduped against existing memories, what gets written where. That’s the difference between a database and a memory system.
The MCP server is what makes any of it useful. A small Go binary that speaks Model Context Protocol over stdio to Claude Code, exposing eight tools:
remember,recall,list_memories,forget,memory_stats,add_relation,recall_related,forget_relation.The topology is deliberately boring:
Mac Home server ┌──────────────┐ ┌──────────┐ ┌─────────────────┐ │ Claude Code │◄─►│ mem0-mcp │ LAN │ Qdrant + Neo4j │ │ │ │ (Go) │───────►│ │ └──────────────┘ └──────────┘ └─────────────────┘ stdio HTTP + boltClient binary on my laptop, containers on a box. No cloud in the middle, no account, no dashboard, no seat license.
The Ansible Role Is the Whole Argument
Anyone can
docker compose upa stack once. That’s a weekend, not infrastructure. What makes this real is that it’s a role in a repo, and rebuilding it on a fresh box is one command:ansible-playbook -i common_hosts home.yml --tags mem0That role does the unglamorous work:
- Installs a read-only deploy key scoped to exactly one repo, with an SSH
Hostalias so it can’t collide with my personal GitHub key - Clones and updates the source at a pinned branch
- Templates a
.envwith secrets pulled from Ansible Vault,no_log: trueso nothing leaks into terminal output on a--diffrun - Brings up the compose stack with
remove_orphans: true, so when I dropped a service upstream, the stale container went with it instead of lingering forever
Be careful with your secrets and how you are creating your .env files!
The Honest Part About the API Key
I self-host the state and rent the inference. Two reasons.
The first reason is speed. I ran embeddings locally before this, on CPU, and it was miserable: roughly 87 seconds to embed 32 memories, against about 2 seconds through a hosted API. That is a 45x difference on an operation sitting directly in the path of every
rememberandrecall. A good model on a CPU is still a slow model, and this was never a quality problem.The second is that embeddings have gotten cheap enough that not worth the time to setup your own embedding service. Mistral charges $0.10 per million tokens for
mistral-embed. Google’sgemini-embedding-001is $0.15 per million, halved on their batch API. Both are good models. Both bill you.Cloudflare is the worth knowing about if you’d rather not pay at all. Workers AI includes 10,000 neurons per day free, on the free plan as well as the paid one. Neurons are their normalized compute unit, and
bge-m3costs 1,075 of them per million input tokens — so that daily allowance is roughly nine million tokens a day, at no cost. Past it you’re at $0.012 per million, which is an order of magnitude under the paid competition. For a personal memory layer, nine million tokens a day is not a trial. It’s just free.One detail if you’re swapping:
bge-m3emits 1024-dimension vectors, the same asmistral-embed. Go back to that Qdrant gotcha — matching dimensions means your existing collection still works. Mismatched ones mean starting over.And the escape hatch is already built. The env vars in my role are
TEI_BASE_URL,TEI_MODEL,TEI_DIMENSIONS. Generic OpenAI-compatible embedder knobs, named after Text Embeddings Inference for historical reasons and pointed at Mistral today. Aim them at a self-hosted TEI container, at Ollama, at anything speaking that shape, and the rest of the stack doesn’t notice.That’s what local-first buys you. Not purity. Optionality.
So, the $5 Box
My server has 60GB of RAM, which is absurd overkill and exists because it does a dozen other things. The stack itself measured 640MB with three weeks of uptime, essentially zero CPU at idle.
That fits comfortably on a small cloud VPS in the few-dollars-a-month range. Check current pricing yourself rather than trusting a number in a blog post, but the shape is: a 2 vCPU / 4GB instance from Hetzner or similar costs less per month than one seat of most AI memory SaaS products, and you get to run everything else on it too.
Your real constraint is RAM, specifically Neo4j’s JVM floor. On a 1GB instance you’d be fighting it. At 2GB you’re fine. At 4GB you’ll forget it’s running.
Why I Care
The indie web ethos is about noticing that renting your identity from a platform means the platform decides what happens to it.
We’re about to make the same mistake with agent memory, except worse, because the thing being accumulated this time is a working model of how you think and what you’re building. Every “our AI remembers you across sessions” product is a proposal that you deposit that into someone else’s database and hope the pricing page stays reasonable.
Qdrant is Apache 2.0. Neo4j Community is GPL. Docker Compose is a YAML file. Ansible is idempotent YAML. Nothing in this stack is exotic. The barrier to owning your agent memory is an afternoon and 640 megabytes.
Not everyone needs this, and I’m not going to pretend a solo dev with three side projects is being exploited by a $20 subscription. But if you’re accumulating context you’d be genuinely sad to lose, the math changes. Own the state, rent the compute, and keep the role in version control so the whole thing is reproducible on a box you haven’t bought yet.
Moving the embedder onto Cloudflare’s free tier is next on my list, what’s on yours?
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].
- Installs a read-only deploy key scoped to exactly one repo, with an SSH
-
Your Agent Needs a Dollar Limit, Not a Token Budget
If you let an autonomous coding agent run in an unbounded loop, I have bad news for you, or rather, your wallet.
It happens easily. An agent gets handed a task, runs into an unhandled error or a failing test, and gets stuck in a retry loop. It re-reads the same files, attempts the same broken patch, and streams tokens the whole time while nobody is watching.
We solved this problem in cloud infrastructure ten years ago. Nobody deploys a Lambda function without an execution timeout. Nobody configures a Horizontal Pod Autoscaler without setting
maxReplicas. Let a container run wild without bounds and your infra team will revoke your deployment credentials before breakfast.Yet here we are, handing autonomous agents full access to our terminal and our API keys with a carrot “go do the task, good luck!”
If we want agents to be production-grade tools, we need to treat token spend like compute spend. The most important feature an agent harness can ship is a first-class, per-task spend ceiling.
Claude Code Shows the Path
Anthropic is paving the way here, and it’s worth talking about.
In Claude Code, you can set a USD ceiling on a single invocation using the
--max-budget-usdflag:claude -p --max-budget-usd 2.00 "refactor auth module"This flag only works in print mode — that’s the
-pabove, which is short for--print.This cap is apparently aware of any fan-out that might occur from subagents, so spend on subagents counts against the same ceiling. Claude Code will then kill the background subagents that are still running if it hits the budget limit. For this feature to work, you need to be running Claude Code v2.1.217 or later.
So if you’re building CLI harnesses on top of Claude Code, this will be a huge quality-of-life feature for you to implement. This way you can kick off a background task and rest assured that the harness will not result in a big surprise on your API bill.
The other one is not a cap at all
Anthropic also has something on the raw API side called the task budget.
However, it is not the same and it will not protect you. Task budgets are in beta, and they hand the model a token allowance for it to run its full agentic loop. It tries to wrap things up gracefully rather than being cut off in the middle of a tool call.
The task budget on the API side is in tokens, not dollars. So it’s fine for getting an idea of whether something is possible within a given token budget, but it’s not going to save you from any surprises on the API bill side of things.
The Gap in Codex and OpenCode
The rest of the CLI agent ecosystem hasn’t caught up.
OpenAI Codex CLI (
codex exec) has no native--max-budget-usdflag or budget setting. You can pin a cheaper model profile, but you cannot set a hard dollar limit on a per-task basis.OpenCode (
opencode run) is in a similar spot, which is strange, given that OpenCode has done a great job of adding features to their CLI harness. Unfortunately, there’s no way to pass a pre-execution cap to OpenCode before you launch a task. It kind of feels like a missed opportunity, or one that they will add soon, given that OpenCode already tracks consumption inside the CLI if you’re using it directly.
How We Hack Around It Today
So how do you enforce a dollar cap on non-Anthropic models right now? You push the problem down a layer and let a gateway handle it — which is one more reason your AI stack probably wants a gateway anyway.
The infrastructure side of things has solved this already with the proxies that are available. They expose the functionality that you need in order to set hard per-key budgets. LiteLLM Proxy will start rejecting calls after that budget has been exceeded, with a
400and abudget_exceedederror type. If you’re using Cloudflare AI Gateway, they shipped a dollar-denominated spend limit in June of this year, which returns a429once you cross the line. You can scope it by model, provider, or other custom metadata, and you can configure it to fail over to a cheaper model instead of blocking, which is a nice to have.
Shift-Left till you get to FinOps
FinOps is what happens when you keep shifting left.
Eventually, we’re going to get tired of the bill.
The gateway vendors have come prepared, and the CLI harnesses have yet to fully adopt a decent token/thinking/dollar budget flag system.
We’ll get this figured out one of these days.
Sources
- Claude Code CLI reference —
--max-budget-usd, its print-mode constraint, subagent spend counting toward the cap, and the v2.1.217 enforcement requirement; verified locally againstclaude --helpon v2.1.220, 2026-07-26. - Anthropic: Task budgets — the advisory, token-denominated API feature; source of the “soft hint, not a hard cap” language and the note that task budgets are unsupported on Claude Code.
- OpenAI Codex CLI: local inspection of
codex exec --help, 2026-07-26 — no budget or spend-cap flag. - OpenCode CLI: local inspection of
opencode run --helpandopencode stats, 2026-07-26 — post-run cost reporting, no pre-run cap. - LiteLLM: Budgets, Rate Limits — virtual key
max_budget,duration, and thebudget_exceededrejection. - Cloudflare AI Gateway: Spend limits — dollar budgets scoped by model, provider, or metadata,
429on block, optional cheaper-model fallback. - Your AI bill is out of control. Cloudflare can fix it now. — the June 2026 launch announcement.
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].
- Claude Code CLI reference —
-
Your Justfile Is Your Repo's API
Ask your agent to run the tests in a repo it has never seen before and watch what happens. It’s gonna load so many things into the context to try to figure it out, digging through the repo to find the right command and what framework you’re using.
What if you could standardize on a way to run the tests no matter the language or the framework?
just testIt’s a statement. It’s an interface. Sure, it’s a shortcut.
I wrote about Just as a command runner back in March.
After months of using it on a daily basis with agent-driven work, I’m here to tell you that it’s an incredibly useful cog in the wheels of complexity.
Your justfile can become what is essentially an operational API for your repository. You use it, your agents use it, the CLI calls it. Everything underneath it you can change as much as you want, as long as the command keeps its promise.
The Command Is the Contract
An API lets a caller ask a system to do something without understanding every detail inside it. A justfile does exactly that for application operations.
When I run
just test, I’m not asking Just to test the software. I’m asking the repository to perform its official test operation. Today that recipe might be:test: lsm exec -- uv run pytestThose few words hide several decisions. The project uses
uv, tests run throughpytest, secrets come throughlsm.Whoever calls that doesn’t need to rebuild or reconstitute any past decisions that went into that command. And the recipe can change as long as what happens when I run the command doesn’t. This is what makes adding Just to your application a proper interface and not just an alias.
Well, actually, if I were in charge of making a motto, I think the tongue-in-cheek version should be
just an alias.The Justfile Connects the Tooling Layers
Modern repos can have several tools doing different jobs, and that’s fine.
- Mise can pin your Python version, and it can operate as your task runner.
- uv manages the Python environment and runs commands inside it.
- LSM is more of a me thing, but it provides secrets when an operation needs them.
- The application CLI holds the actual business behavior.
- Just If you’re already using Mise as your task runner, you may not need Just at all.
Modules Turn Commands Into Namespaces
Once a project has more than a handful of operations, one flat list becomes a junk drawer. Just supports modules, so the root justfile can declare:
mod blog mod links mod db mod booksEach module owns its related recipes, and the result reads like a small command-line app:
just blog status just db backup just books unresolvedThe manual describes module recipes as subcommands. The hierarchy helps humans discover the interface, and it gives agents a predictable way to narrow down the operation they want. It also kills the naming nonsense of flattening every domain and action into one giant alphabetical list. The namespace carries the context.
Hide Plumbing, Not Consequences
An API should make a system easier to use. It should not disguise what the system does.
A recipe can absolutely hide the auth wrapper, the environment setup, and the CLI invocation. It should never make a destructive production operation look like an innocent local check. Good recipe names describe intent, so the caller knows which commands read, write, publish, restore, or preview. Dangerous workflows still need real validation, permissions, confirmations, and backups.
A justfile is an interface. It is not a place to dump everything.
A Few Rules to Follow
Sometimes it’s obvious.
Name recipes after intent.
test,backup,publish,status. Nottest-pytest-with-lsm— it’s hard to tell what that actually does.Keep one canonical path. If
just test, Mise, a shell script, and the README all run different tests, you have four contracts and no interface.Pass through useful arguments.
just books fetch --dry-runpreserves application options without a new recipe for every flag.Make discovery useful. Comments plus modules turn
just --listinto documentation.Keep recipes thin. Branching rules, error handling, and database logic belong in tested application code.
The Boring Boundary Wins
Just still does the simple thing I liked back in March. It saves project-specific commands and runs them as recipes. The manual calls it a command runner, not a build system, and that’s correct.
The bigger value shows up when everyone agrees to call the same recipes. Humans stop memorizing setup. Agents stop reconstructing commands. CI and local development share an entry point. The tools underneath can churn without dragging things down.
Congratulations, that’s your application as an API.
Sources
- Just Programmer’s Manual — defines Just as a project-specific command runner and documents recipes, arguments, listing, and multi-file organization.
- Just modules documentation — documents
modstatements and invoking module recipes as subcommands.
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
-
Graphify Turns Your Repos Into a Map You Can Query
Navigating code dependencies inside a single repository is already hard enough. But if you’re on a microservice setup, or a split frontend and backend, tracking what depends on what across multiple repos is a special kind of misery. A backend API route changes. Which frontend components just broke? Good luck. You’re grepping three workspaces and hoping you didn’t miss one.
So when I ran across Graphify, an open-source project from Graphify Labs (YC S26), it caught my attention. It maps your code directories into queryable knowledge graphs. Not fuzzy text search. Not an expensive vector RAG lookup that burns tokens every time you ask it a question. A deterministic index of your codebase.
Let me walk through how it works, why it’s useful for AI coding agents, and the part I wanted to figure out: how to stitch multiple repos into one unified map.
What Graphify Does
Instead of guessing at relationships, Graphify parses your source and builds a real graph out of it. Three pieces make it tick:
- Deterministic AST parsing. It uses
tree-sittergrammars locally to parse roughly 40 languages, pulling out classes, functions, calls, and imports. No LLM tokens, no API rate limits. Just parsing. - Explicit vs. inferred edges. Every relationship gets a confidence tag.
EXTRACTEDmeans it’s right there in the syntax, like an import or a direct function call.INFERREDmeans it deduced the connection from context. You always know how much to trust an edge. - Leiden community clustering. It automatically segments your code into logical domain boundaries, which makes it easy to spot the “god nodes”, the files with way too many dependencies hanging off them. Those are usually the first thing you want to refactor.
Merging Multiple Repos Into One Graph
This is the part I cared about. Graphify supports it natively through the CLI, and here’s the flow straight from the docs (I haven’t run it on my own repos yet). Say you’ve got a frontend repo and a backend repo. Three steps.
Step 1: Scan each repo on its own. Run the scan inside each folder. Results land in a
graphify-out/directory.# In your frontend repo cd ~/Work/frontend graphify . # In your backend repo cd ~/Work/backend graphify .Step 2: Merge the graphs. The
merge-graphssubcommand joins the JSON outputs into one combined map of nodes and relationships.graphify merge-graphs \ ~/Work/frontend/graphify-out/graph.json \ ~/Work/backend/graphify-out/graph.json \ --out ~/Work/combined_graph.jsonStep 3: Traverse it, or hand it to your agent. Now you can trace a call path straight across the service boundary, or serve the combined graph to a coding agent over MCP.
# Trace a path across the frontend/backend boundary graphify path "login_component.ts" "auth_controller.py" --graph ~/Work/combined_graph.json # Or expose the combined graph to your coding agent over MCP python -m graphify.serve --graph ~/Work/combined_graph.jsonThat
pathcommand is the whole pitch, honestly. You point it at a frontend file and a backend file and it tells you how they’re connected. No manual grep archaeology.Why This Matters for AI Coding Agents
If you use Claude Code, Cursor, or Antigravity, you already know the problem. Feed the agent raw files and you torch the context window in about four prompts. Point it at Graphify’s output instead, the
GRAPH_REPORT.mdor thegraph.jsonover MCP, and the agent can do a few things it otherwise can’t:- Figure out exactly which files a refactor will touch before it edits anything.
- Trace dependency lineage across code boundaries deterministically, not by vibes.
- Describe your architecture based on the actual shape of the code, not a hallucinated version of it.
That last one is underrated. Half of “the AI got confused” moments happen because the AI never saw the whole picture.
Two Gotchas Before You Install
A couple of things will trip you up, so here they are up front.
The package name has a typo built in.
graphifywas already taken on PyPI, so the official package is registered asgraphifyy. Two y’s. You install it like this:pip install graphifyyWatch your Python version. The Leiden community detection library has C-extension limits, so Graphify currently runs best on Python under 3.13. Worth checking or switching to a compatible version (like 3.12) using mise.
The honest appeal here isn’t the visualization, pretty as the HTML map is. It’s that cross-repo dependency tracing has been a manual, error-prone chore for as long as I’ve worked on split codebases, and this makes it a single command.
Sources
- Graphify Labs on GitHub: setup requirements, supported parsers, and CLI options.
- Auriga IT’s Graphify introduction: explains the three-pass architecture and Leiden clustering optimization.
- Graphify on PyPI: package installation details and version compatibility.
- Aider’s Repository Map: on using tree-sitter to parse AST-based codebase maps for token-efficient coding context.
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].
- Deterministic AST parsing. It uses
-
Why Your AI Stack Needs a Gateway
Picture an autonomous agent loop dying at step 45 of a plan it’s been grinding through for the better part of an hour. Not because the plan was wrong. Because OpenAI handed back an
HTTP 429at exactly the wrong moment, and the whole thing fell over. An hour of work, gone to a transient rate limit.That’s the moment most people start thinking about an AI gateway, whether they know the term or not.
Hardcoding API keys and endpoints straight into your application code feels fine right up until it doesn’t. You start with a simple chat wrapper. Then you’re running agents like OpenClaw or Hermes that chew through hours of command-line work. Then you’re wiring up a real backend that talks to OpenAI, Anthropic, Google, and a couple of self-hosted models. Now every one of those providers is a single point of failure. One of them rate-limits you or goes dark, and your workload crashes.
So people are dropping a new piece into the stack to deal with it: the AI gateway.
What It Is
An AI gateway is a specialized reverse proxy that sits between your application and the model providers upstream. Instead of importing a different SDK and juggling a different set of environment variables for every vendor, your app talks to one OpenAI-compatible endpoint. The gateway handles routing, retries, load balancing, security, and caching behind the scenes.
The request flow is straightforward. Your app makes a normal OpenAI-style call. The gateway checks its cache first, and if it’s seen a semantically similar prompt it serves the answer in milliseconds. On a miss, it runs the request through whatever security layer you’ve configured, then routes to a provider, with a failover path ready if the primary one is down. Your code never has to know any of that happened.
Who Needs One
The value splits cleanly across two kinds of people.
If you’re building agents, the pitch is survival. An agent that runs for hours is going to hit a
429or a500eventually. A gateway catches those, does exponential-backoff retries, and can swap providers mid-task, falling back to Claude if OpenAI is having a bad day. Your long-running loop stays alive instead of dying at step 45. You also get to keep your real vendor credentials locked in one vault and hand your agent scripts restricted local keys instead.If you’re the tech lead shipping customer-facing AI, the gateway becomes your governance layer:
- Observability. One console showing latency, time to first token, cost, and raw prompts across every team, instead of five fragmented dashboards.
- Spend management. Hard dollar budgets per team or per key, so a runaway recursive loop can’t quietly drain the corporate card.
- AI firewalls. Automatic PII masking for emails, phone numbers, and stray API keys, plus prompt-injection blocking at the edge before anything leaves your network.
- Semantic caching. Vector similarity checks catch prompts that mean the same thing and serve a cached answer, cutting both the token bill and the latency to near zero.
The Landscape
This space is filling up fast, and the options sort themselves by how you want to deploy. A quick tour of the ones worth knowing:
- OpenRouter is the managed broker. Hundreds of models under one credit balance, with dynamic pricing, fallbacks, and bring-your-own-key support. The easiest place to start.
- LiteLLM is the self-hosting standard. Python, wildly popular for building a private gateway inside your own VPC, with database-backed key budgets.
- Bifrost is the same idea written in Go for teams that care about throughput. It adds almost no latency overhead and benchmarks its P99 routing well ahead of the Python options.
- Portkey leans into prompt management. Versioned prompt templates live in a central playground and get called by API, which is handy if your prompts change more than your code.
- Cloudflare AI Gateway is the zero-devops edge play, built on Cloudflare’s CDN with fast caching, Logpush exports, and native edge firewalls.
- Vercel AI Gateway plugs straight into the Vercel AI SDK, so you route serverless traffic through it with a simple string change.
There’s no single right answer here… and i’m pretty sure I’m leaving a few out. If you just want to stop thinking about it, OpenRouter or Cloudflare. If you want control and a VPC, LiteLLM or Bifrost. If prompts are your headache, Portkey.
The real takeaway is smaller than the tooling makes it look. The moment your app depends on more than one model, or on any single model staying up, you’ve got an infrastructure problem, not an application problem. A gateway is just where you put the solution so your code doesn’t have to carry it.
An agent like that crashes at step 45 today. Put a gateway in front of it, and it doesn’t.
Sources
- OpenRouter
- Vercel AI Gateway documentation
- LiteLLM Proxy
- Bifrost (Maxim AI)
- Cloudflare AI Gateway docs
- Portkey Gateway docs
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].
-
Autopilot for Your Docs: A Look at LangChain's OpenWiki
Writing documentation for a repo is the chore nobody volunteers for. Keeping it current is somehow worse. You refactor one service or change a database schema, and the architecture overview you wrote three months ago is instantly a lie.
So when the LangChain team dropped OpenWiki, I had to take a look. It’s an open-source CLI tool and agent framework that writes and maintains documentation for your codebase automatically. I read through the source rather than running it, so here’s an honest read on the idea, how it works under the hood, and what you’ll run into if you point it at a real repo today.
What Is OpenWiki?
The premise is simple: AI coding assistants are only as good as the context they can reach. Instead of stuffing your prompts full of raw source files, OpenWiki generates a structured, interlinked Markdown wiki inside an
openwiki/directory right in your repo. It’s built to be read by coding assistants like Cursor, Claude Code, or Codex CLI.graph TD Diff[Git Workspace Diff] -->|CLI/CI| CLI[OpenWiki CLI] CLI -->|LangGraph| Agent[Documentation Agent] Agent --> Analyzer{Code Analyzer} Analyzer -->|Incremental Update| Docs[openwiki/ Markdown] Docs -->|Appends Pointer| Config[CLAUDE.md / AGENTS.md]Three things it does that I like:
- Autopilot docs. You don’t hand-write or hand-format anything. The agent inspects your layout and produces overview, architecture, workflow, and API reference pages.
- Incremental git-diff sync. It doesn’t re-read your whole workspace on every commit. It looks at the diff since the last scan and only rewrites the pages those changes touched.
- Prompt hooking. It appends a reference pointer to your
CLAUDE.mdandAGENTS.mdso downstream assistants check the wiki first.
That last one is interesting and is starting to show up in other harness adjacent tools. For this project, it turns the docs into a context layer your agents are told to read.
The LangChain Footprint
This is built by the LangChain team, and it shows in the dependency list.
- Orchestration runs on
@langchain/coreanddeepagents, with a stateful LangGraph engine backed by@langchain/langgraph-checkpoint-sqlitethat stores checkpoints and agent state locally. - The CLI uses
ink, so it renders a clean React-based interface right in the terminal. - Tracing ships with LangSmith support out of the box, which is worth more than it sounds. When you want to know why a particular page got rewritten, or what a run cost you, that audit trail is right there.
If you’re already living in the LangChain ecosystem, none of this will surprise you. If you’re not, it’s a lot of framework to pull in for a docs tool. Fair tradeoff or not depends on how much you value the tracing.
The Catch: Tokens and Rate Limits
It’s still early but here is what I would wathc out for. The first run is expensive.
When you bootstrap OpenWiki on a medium-to-large repo, the agent has to read, analyze, and index everything. Two things happen:
- You’ll hit rate limits. That initial pass will saturate your provider’s API limits fast. Expect a parade of
429 Too Many Requestson any large source tree. - You’ll spend real money. Bootstrapping a large repo can burn through a huge number of tokens in a single run.
If you’re going to try it, configure your
.openwikisettings to exclude the folders that don’t need documenting.node_modules,dist, generated assets, all of it. There’s no reason to spend tokens teaching an agent about your vendored dependencies. If you have access to a high-throughput endpoint or a fast local model through Ollama or LiteLLM, the bootstrap is a lot less painful.The steady state is fine, since the git-diff sync keeps ongoing runs cheap. It’s that first index that hurts.
Should You Use It?
OpenWiki is new and moving fast, which means you should expect config keys and command arguments to shift under you for a while. This is not a set-and-forget tool yet.
But the core idea is interesting. An agent running quietly in a pre-commit hook or CI, keeping your repo’s context layer in sync so your other agents have something accurate to read, is a real quality-of-life upgrade. Docs that maintain themselves have been a fantasy for as long as I’ve been writing code. On a read of the source, this is the most serious attempt I’ve seen at pulling it off.
I haven’t run it yet, just read through the code, and I’m not putting it anywhere just yet. But the idea is right, and I’m watching where this one goes.
Sources
- LangChain OpenWiki repository for setup, commands, and configuration.
- LangChain blog for the launch announcement and design philosophy.
- LangGraph JS docs for the local SQLite checkpointing and state details.
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].