AoAH Day 22: Assembling monorepos for agentic OCaml development / Dec 2025
Over the past three weeks, I've accumulated dozens of OCaml repositories as
part of this
Today I'm switching tacks to address this with a monorepo workflow built around dune's
excellent vendoring support.
I last visited this when building
I also wanted to explore the small group dynamic around vibecoding tools. For today's tool, I first asked
The Git repo coordination problem
The OCaml libraries I've built are designed to be standalone, but obviously have interdependencies among each other.
Here's the full inventory of what I've built in the last few weeks:
| Day | Library | Description |
|---|---|---|
| 1 | ocaml-crockford | Crockford Base32 encoding |
| 2 | ocaml-jsonfeed | JSONFeed 1.1 implementation |
| 3 | xdge | XDG directories with Eio capabilities |
| 4 | claudeio | Claude OCaml/Eio SDK |
| 5 | ocaml-bytesrw-eio | Bytesrw/Eio adapter |
| 6 | ocaml-yamlrw | Pure OCaml Yaml 1.2 parser |
| 7 | ocaml-yamlt | jsont codecs for Yaml |
| 8 | sortal | Contacts management CLI |
| 11 | ocaml-punycode | Punycode RFC3492 implementation |
| 11 | ocaml-publicsuffix | Public suffix list for cookies |
| 11 | ocaml-cookeio | HTTP cookie handling |
| 12 | ocaml-conpool | TCP/TLS connection pooling |
| 13 | ocaml-requests | HTTP client library |
| 14 | ocaml-karakeep | Karakeep bookmark API |
| 15 | ocaml-html5rw | HTML5 parser and validator |
| 16 | ocaml-json-pointer | JSON Pointer RFC6901 |
| 16 | odoc-xo | odoc extras for notebooks |
| 17 | ocaml-jmap | JMAP email client |
| 18 | ocaml-tomlt | TOML 1.1 codecs |
| 19 | ocaml-zulip | Zulip bot framework |
| 19 | ocaml-init | INI file codecs |
| 20 | ocaml-langdetect | Language detection |
And the Claude skills I've developed along the way:
| Skill | Purpose |
|---|---|
| claude-ocaml-metadata | Automate opam package setup |
| claude-ocaml-internet-rfc | Fetch and integrate IETF RFCs |
| claude-ocaml-tidy-code | Refactor generated OCaml |
| claude-ocaml-to-npm | Publish js_of_ocaml to NPM |
So far, I've been publishing each of these as individual Git repositories, but maintaining an overlay opam repo that a user can add to gain access to consistent metadata that makes the dev packages installable. Unfortunately, incorrect interdependencies are already creeping in;
When an agent works on just one repository, it has no visibility into how changes might benefit (or break) dependent code. It also can't make fixes for documentation across repositories. I noticed quite often in the past month that I was cloning source packages temporarily into my workspace for the agent to access, and then deleting them. All this motivates me to investigate alternatives to having lots of small git repos for my day-to-day agentic development.
Dune's vendoring is amazing for monorepos
Dune has a fantastic but underappreciated feature: it automatically discovers and builds any OCaml code in subdirectories. As
Note that this only works if all the packages contain dune files. Since OCaml is all about choice, there's no hard mandate to use one build tool: it's perfectly fine to use ocamlbuild or Makefiles, as long as your libraries install a findlib META file. Dune will also gain support for opam package installation next year to help make this even easier.
Years ago I built a tool called duniverse to automate this vendoring workflow. It worked, but required a lot of manual repository management. With agents now doing the heavy lifting, though, I thought it might be easier now and so decided to revisit it.
Today's work ended up extending
Materialising aoah-opam-repo
The aoah-opam-repo contains all the packages I've built during this series, maintained using the
$ monopam --opam-overlay aoah-opam-repo -o aoah-vendor --submodules
Scanning opam overlay at aoah-opam-repo
Found 21 repositories to process
Initialized empty Git repository in aoah-vendor/.git/
Using git submodules for vendor dependencies
Cloning into ...
remote: Enumerating objects: 14, done.
remote: Counting objects: 100% (14/14), done.
remote: Compressing objects: 100% (11/11), done.
remote: Total 14 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
# ...etc
Output written to aoah-vendor
opam-repository/ - opam package definitions
vendor/ - source code
setup.sh - run to pin packages and install deps
This solves the opam constraints, finds a cut of dependencies, and then git
submodule adds the lot of them into my target repository. At this point, we run
setup.sh which creates an opam local switch and then dune build just works
using all the locally cloned repos.
.
├── _opam
├── dune
├── dune-project
├── opam-repository
│ ├── packages
│ └── repo
└── vendor
├── dune
├── ocaml-bytesrw-eio
├── ocaml-claudeio
├── ocaml-conpool
├── ocaml-cookeio
├── ocaml-crockford
├── ocaml-html5rw
├── ocaml-init
├── ocaml-json-pointer
├── ocaml-karakeep
├── ocaml-langdetect
├── ocaml-publicsuffix
├── ocaml-punycode
├── ocaml-requests
├── ocaml-tomlt
├── ocaml-yamlrw
├── ocaml-yamlt
├── odoc-xo
└── xdge
The directory structure is straightforward: we have our opam repository, a local switch and the source code all in one place now, and buildable in a single dune invocation.
Cross-cutting fixes with agents
With all the code in one place, agents can now spot opportunities that span multiple packages. The first thing I did was to build a full documentation set across all my packages.

Building unified documentation with odoc3
This is where the monorepo approach obviously shines, since we could generate a single site for all my code with types linking directly to their definitions across opam packages. The
I first pinned Jon's odoc branch and then built the unified docs with the right rules.
$ opam pin add dune https://github.com/jonludlam/dune.git#odoc-v3-rules
$ dune build @doc
$ open _build/default/_doc/_html/index.html
This generated a working doc page, that also included cross-referenced links across packages. But even more cool is that if a package doesn't exist in the local monorepo, it also does a best-effort link straight to the central doc repository on OCaml.org.
There were a few integration issues that may be bugs in the dune rules. For instance:
> dune build @doc
File "/Users/avsm/src/git/knot/aoah-vendor3/_opam/lib/angstrom/META", line 1, characters 0-0:
Error: Library "angstrom-unix" not found.
-> required by library "angstrom.unix" in
/Users/avsm/src/git/knot/aoah-vendor3/_opam/lib/angstrom
-> required by alias vendor/ocaml-karakeep/doc
This is a package that's present in the local tree, but not installed in opam. After I opam installed it, the doc generation worked. This probably shouldn't break a local docs build, so I commented on the GitHub PR.
After this, there were genuine bugs in my own documentation, as evidenced by
warnings emitted by dune. The agents fixed problems and added cross-references across
the documentation, and I could do a single git status to see all the affected
packages.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
(commit or discard the untracked or modified content in submodules)
modified: vendor/ocaml-claudeio (modified content)
modified: vendor/ocaml-init (modified content)
modified: vendor/ocaml-json-pointer (modified content)
modified: vendor/ocaml-requests (modified content)
modified: vendor/ocaml-tomlt (modified content)
modified: vendor/ocaml-yamlrw (modified content)
modified: vendor/ocaml-yamlt (modified content)
modified: vendor/xdge (modified content)
Code fixing across packages
I then prompted the agents to find opportunities for optimisation across all the packages. Running this in a fixpoint ended up allowing for backwards and forwards cross-references: for example, it could add "related libraries" sections, and also normalise error handling and logging interfaces where there were inconsistencies.


Reflections
Once I had a consistent monorepo, I could commit the changes and distribute a batch easily. For example, I uploaded my test odocv3 monorepo and commented on ocaml/dune#12995.
On the other hand, monopam's git submodule workflow is awkward to use due to how separate submodules are from the main git repository. I had to individually commit and push each of the changes, and I couldn't get a unified git diff or make commits across the vendored repositories. I have a scheme in mind to improve this, which is a topic for tomorrow's post!
Socially speaking, I'm reasonably convinced a monorepo workflow of some sort is the future for agentic coding. They just work so much better with local tool calls that can rapidly scan a lot of data instead of making remote calls (which are awkward from a permissions perspective as well). We'll still need to figure out how the dynamics of 'vibrating' patches across each other goes; it's early days for the dynamics of agentic pair programming.