Bachkator: Terraform for Your Agent Loops
TL;DR: Bachkator is Terraform for your agent loops.
Its name comes from Bulgarian: бачкатор (pronounced BAHCH-kah-tor) means “the hard worker” — the one who gets the job done.
We have been using the tool internally for about a month, and it has been useful enough that we decided to clean it up and share it.
History
The funny part is that this project started back in 2017, while I was learning Go.
At the time I was looking for projects to build, and we were constantly fighting Make. The syntax made my eyes bleed, and we had hundreds of Makefiles scattered across repositories. Around the same time, Terraform by HashiCorp was taking off, and its configuration language, HCL (HashiCorp Configuration Language), caught my eye. It was declarative, readable in a diff, and structured enough that configuration did not immediately collapse into another shell script.
By accident, I ended up reading Mike Shal’s Build System Rules and Algorithms, the paper behind Tup’s build-system model. The part that stuck with me was more specific than “make builds faster”: a build system should start from what changed, update the smallest correct part of the graph, and keep enough state to remove stale outputs when targets change or files move. If you need clean to trust the result, the build system is leaking its job back to you.
The paper frames build systems around three rules that still feel painfully relevant:
- Scalability: small edits should not require work proportional to the whole project.
- Correctness: if you undo a change, the build output should return to the previous state too, stale artifacts included.
- Usability: developers should not need a mental decision tree for “do I run make here, make at the root, or make clean just in case?”
That last one is the part that stuck with me most. If the tool makes you keep the build graph in your head, it is not doing enough work.
So I tried building a Make replacement in Go with developer ergonomics as the primary goal: keep the graph, keep the artifacts, but make the file something a human could actually stand to read and edit.
It was not great. I spent months on a pretty mediocre version.
Fast-forward to today, and I found myself dealing with a very similar problem, except now it is AI agents. They run random commands, ignore project conventions, burn tokens rediscovering the repo, and generally have no clear control plane for what they are supposed to do.
Worse, they keep their own little version of the build graph in context. One agent reads the README, another reads CI YAML, another guesses from package scripts, and another just runs the full test suite because that feels safe. It works, but it is wasteful and hard to trust.
At some point I realized: wait, I built something for this problem years ago.
So I installed it again and gave it a try.
Why It Became Useful Again
Bachkator was originally about builds. The useful part now is that agents spend most of their time doing the same repo grind we do: inspect, test, lint, build, read logs, fix, repeat.
If those operations are implicit, every agent has to rediscover them. If they are explicit, the agent can just follow the contract.
Agents need a repo contract:
- What executable things exist?
- Which tests are affected?
- What changed?
- Is this target safe, remote, destructive, or expensive?
- Where are the latest logs and artifacts?
- Did the code pass quality checks?
- What does “done” mean?
That information should be cheap to retrieve, checked into the repo, and available without feeding the agent enormous markdowns every turn.
The important part is the contract: commands, flags, Bachfile syntax, reference docs, and predictable output. That is what a human, CI runner, or agent can rely on.
Bachkator gives humans, CI, and agents the same executable contract:
bach list
bach affected
bach --dry-run run shell/test
bach run shell/test
bach quality summary
bach runs
bach reference
Docs are shipped in the binary, so an agent can ask the tool how it works instead of relying on outdated docs elsewhere.
That matters from the first install. You can run bach reference before a project even has a Bachfile, give that output to an agent, and ask it to generate the first Bachfile for the repo using the built-in reference instead of guessing the syntax.
What A Bachfile Gives You
Think Terraform, but for repository operations:
| Terraform | Bachkator |
|---|---|
| configuration declares infrastructure | Bachfile declares project operations |
| plan before apply | dry-run before run |
| state tracks what exists | State Store tracks runs, fingerprints, reports, gates |
| providers expose capabilities | Targets, Inputs, Resources, Plugins, Quality Handlers |
| apply changes intentionally | run named Targets intentionally |
A Bachfile declares project operations as targets with inputs, outputs, dependencies, risk metadata, tools, preflights, logs, and quality reports.
The important abstraction is the Target: a named operation that Bachkator can inspect, plan, run, cache, and record. Shell commands, image builds, and pipelines are different kinds of targets, but they share the same metadata, cache, risk, and run history.
Today there are three target types:
- Shell: run the command you already trust.
- Pipeline: run existing targets in a declared order when order matters.
- Image: generate an OCI image build command for Docker or Apple’s
containerCLI.
The Image target is intentionally just syntax sugar. It is not required. You can always use a Shell target for docker build, container build, gh release, go test, bun test, or whatever else your repo already uses.
Internally, target execution goes through an interface so we can swap in SDK-based implementations later if that ever becomes useful. In practice, most major CLIs are rock solid now, so I do not expect that to be necessary for a while. Shelling out is simple, inspectable, and works.
Here is the shape in miniature:
shell "lint" {
description = "Run lint and publish a Checkstyle report"
command = [
"golangci-lint",
"run",
"--issues-exit-code=0",
"--output.checkstyle.path=$(RUN_DIRECTORY)/checkstyle.xml",
]
inputs = [input.file.go_sources]
outputs = {
checkstyle = "$(RUN_DIRECTORY)/checkstyle.xml"
}
}
quality "lint" {
lint {
format = "checkstyle-xml"
path = shell.lint.outputs.checkstyle
}
quality_gate {
metric = "issues.total.count"
max = 0
}
}
The linter can exit zero and still publish findings. Bachkator owns the quality decision, records the parsed report, and makes the answer available later with bach quality summary, bach quality findings, or bach quality gates.
That means the agent can stop guessing and start asking the repo:
bach list: what can I run?bach affected: what should I run after these changes?bach --dry-run run <target>: what would happen before side effects?bach runs: where did the last run fail?bach quality summary: did the code pass quality gates?bach reference: how does this tool work?
The point is not to replace your language tooling. It wraps the commands you already have in a shared control plane.
Under the hood, each run has a few parts that matter for agents:
- Run Plan: which targets are involved, which edges are dependencies, which edges are ordered pipeline steps, and what risk/tool/preflight checks apply.
- Run Session: the in-memory coordinator that schedules targets, streams output, writes logs, tracks fingerprints, and owns locks for that invocation.
- State Store: a local SQLite-backed record of target fingerprints, runs, artifacts, reports, and quality gates.
The cache fingerprint is not just “hash some files”. It includes inputs, operation shape, environment, dependency fingerprints, outputs, and Git context. When a target is stale, Bachkator can explain why: changed input, changed env var, changed operation, dependency fingerprint change, dirty Git state, a declared output file that no longer exists, or forced run.
That is the piece I wanted for agents. Not just “run tests”, but “this target is stale because these inputs changed” or “skip it, the fingerprints and outputs are fresh”.
Agent Loops, Deterministically-ish
The part that surprised us is how useful Bachkator became for orchestrating agent loops.
Here is the shape of the agent delivery graph from the repo’s examples/plan-agents/Bachfile:
bach graph -f examples/plan-agents/Bachfile
Delivery loop:
flowchart LR contract["Bachfile<br/>contract"] --> agents["parallel<br/>agents"] agents --> merge["serialized<br/>merges"] merge --> gates["quality<br/>gates"] gates --> evidence["logs, artifacts<br/>handoff"]
Long-running loop:
flowchart LR agent["agent<br/>open-ended work"] --> checkpoint["checkpoint<br/>resume state"] checkpoint --> gates["gates<br/>lint + test"] gates --> evidence["evidence<br/>logs + artifacts"] evidence --> feedback["feedback engine"] feedback --> agent
The actual machine-generated graph is more detailed and includes every target edge, risk label, and lock. You can generate it with bach graph -f examples/plan-agents/Bachfile.
Feature agents can run in parallel. Merge agents are serialized behind a lock. Expensive or destructive targets require confirmation. The final regression target is a deterministic verification gate.
There are a few details in that example that matter in practice:
- The delivery program is a Pipeline Target, so the foundation lane, core merge lane, extension merge lane, and final regression run in a declared sequence.
- The feature swarms use normal dependency edges, so independent feature agents can fan out in parallel.
- The merge agents all share
lock = "git-merge", so even if the scheduler has parallelism available, only one merge lane touches the integration branch at a time. - Risk metadata propagates upward. If a child target is remote, destructive, or requires confirmation, the aggregate plan shows that before anything runs.
- The long-running maintenance loop has a separate
agent-looplock, an eight-hour timeout, anopencodetool check, and a clean-worktree preflight.
This is what I mean by deterministically-ish. The agent is still an agent. It can reason, investigate, and make edits. But the outer loop is explicit: run this target, respect this lock, write logs here, stop at this gate, show me the plan before you do anything expensive, and conform to the quality gates I declared. The gates are deterministic checks around probabilistic work. Kind of a tiny, practical, neurosymbolic-ish rule engine for repo operations.
It is not magic. It is just a dependency graph with a CLI contract. But that is exactly what agents need.
If you look closely, this is very similar to Claude Code’s dynamic workflows: a planned, long-running program can fan out work to agents, collect evidence, and stop at gates. The difference is where the contract lives. Dynamic workflows are great for Claude-native orchestration. Bachkator puts the executable truth in the repo, so Claude, OpenCode, Codex, Cursor, CI, or a human terminal can all call the same targets.
What We Borrowed From Build Systems
Bachkator is not Tup, and I do not want to pretend it implements Tup’s full Beta build algorithm. Tup is much deeper on file-level dependency tracking and minimal partial DAG updates.
But the mental model from the paper is all over the tool:
- Do not make the user or agent remember the graph.
- Track produced artifacts instead of pretending stale files do not matter.
- Prefer explicit changed-file evidence over scanning the world when possible.
- Keep enough state to explain why work is fresh or stale.
- Make the fast path the normal path, not a special incantation.
For Bachkator today, bach affected matches Git changed files or explicit paths against resolved target inputs, including plugin-provided graph evidence. It is deliberately read-only. It does not run anything; it tells the agent the smallest configured targets that look relevant.
Plugins are where this gets more interesting. The repo has examples for Bun package graphs and TypeScript import graphs. That means a team can teach Bachkator about its own dependency model without asking every agent to rediscover it from source code.
Why It Saves Tokens
The practical agent win is not philosophical. It is token economics.
Long-running commands are noisy. Test suites, linters, build tools, and release commands can dump thousands of lines into the terminal, most of which the agent does not need unless something fails.
Bachkator supports quiet targets and log-only runs, so the heavy output goes to .bach/runs/.../*.log instead of flooding the conversation. The agent still gets the status, stale/fresh reason, run ID, and artifact index, then can inspect only the specific log or report it needs.
Instead of pasting the whole README, CI config, release notes, terminal scrollback, and tribal knowledge into every session, the agent can ask narrow questions:
bach list --verbose
bach explain shell/lint
bach affected
bach runs --target shell/test
bach artifacts --target shell/build
bach artifacts --runs-limit 1
bach reference quality-reports
This is where the local State Store matters. Bachkator records runs, target fingerprints, logs, artifacts, quality reports, and gates in SQLite. The next agent, or the same agent after a context reset, can ask “what happened last time?” without replaying the whole terminal session.
The answer comes from the repo’s executable contract and local run history, not from the agent’s memory of a previous terminal session.
The last run’s artifacts are quickly available: logs, run directories, generated reports, release archives, manifests, image tags, and named outputs. Quality reports are normalized. Release targets can pin GitHub tags to the exact commit that was built.
None of that is glamorous, but it is exactly the stuff that makes an agent loop feel less like vibes.
Still Beta
Very little polishing has gone into the public workflow so far, but the core idea has been battle-tested internally.
The public API may change. If you end up using Bachkator, expect some breaking changes while the interface settles. The upside is that the documentation is agent-first and shipped with the binary, so your agents should be able to help migrate your configuration when that happens.
One thing we already know we want next is Makefile-like expansion for target families, so you can describe repeated target patterns without copy-pasting a pile of nearly identical blocks.
Try It
Bachkator is available on GitHub: github.com/ApplauseLab/bachkator
The agent orchestration example is here: examples/plan-agents
The Tup paper that influenced the original idea is here: Build System Rules and Algorithms
Would love to hear your thoughts on the thingy.

