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 series. Keeping them coordinated has become a real challenge; when I fix something in one library, dependent packages need updating, and agents working on one repo have no visibility into related code. Ideally, I could have all my code in one place and see what agents can do with a lot of local context.

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 RWOv2 and its monorepo when I built a duniverse tool that turned into the opam-monorepo plugin that MirageOS now uses. Let's see what happens in today's agentic world instead!

I also wanted to explore the small group dynamic around vibecoding tools. For today's tool, I first asked Mark Elvers to spend a few hours sketching out the sort of tool he might want, and then Jon Ludlam has been using Claude to build up complex odocv3 rules. The way we work together with agentic code is quite different from when we've handcrafted a project, with the code itself now being more throwaway as we pass the baton among each other. I'm lightheartedly calling this 'vibrating' amongst each other to reflect the new speed of agentic iterations, and to differentiate from the more thoughtful process of pair programming. Today's tool monopam helps to manage OCaml monorepos for cross-cutting code and documentation.

The Git repo coordination problem

The OCaml libraries I've built are designed to be standalone, but obviously have interdependencies among each other. Requests depends on conpool for connection management and HTTP cookie logic from cookeio. The codec libraries like yamlt, tomlt, and init all have optional dependencies on bytesrw for serialisation. Meanwhile, html5rw depends on langdetect and has optional dependencies on the wasm and JavaScript compiler stack.

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; Thomas Gazagnaire asked me today why my yamlt library depends on webassembly, and I'm sure it shouldn't -- I've clearly got a stray missing dependency somewhere in my metadata.

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 David Allsopp explained back in 2018, you can simply clone dependencies into your project tree and dune will build them together.

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 Mark Elvers initial foray into monorepos to release monopam: a little CLI tool that reads opam metadata from a local repository (like aoah-opam-repo), resolves the dependency graph, materialises the sources as git submodules, and produces a single dune workspace that builds everything together. For now, it depends on an opam local switch to work, but if someone wants to try it with dune package management I'd love to hear how it goes.

Materialising aoah-opam-repo

The aoah-opam-repo contains all the packages I've built during this series, maintained using the opam metadata skill. Let's turn it into a unified source tree using monopam:

$ 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

Jon Ludlam has been doing excellent work on odoc3, the modern documentation generator for OCaml. odoc is a composable documentation generator that has a number of mini-commands that can be called in sequence to build fragments of HTML. Jon's been adding support into dune build rules to build a fully cross-referenced documentation site across an entire dune workspace.

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 interactive notebooks I built earlier could reference any type across the whole codebase.

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.

Docs fixes from the agent across repositories
Docs fixes from the agent across repositories
And similarly, interface fixes work just as well
And similarly, interface fixes work just as well

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.

# 22nd Dec 2025ai, aoah, llms, ocaml, oxcaml

Loading recent items...