Innesti Digital
Research note27 October 2026Libretto7 min read

How we built Libretto with AI agents

A CMS written largely by agents, the limits we added along the way, and the times they did not hold.

Libretto is a CMS for design studios and their clients. We built it with AI agents: from the first commit, on 21 April 2026, to today there are 3,665 commits, and 2,642 of them carry an agent as co-author. This note is about how the work changed once the agents were many and fast, which limits we added, and where those limits did not hold.

What we measured

Type errors, application plus tests, 6 to 20 October 20261,185 โ†’ 234knip issues, 6 to 25 October 2026483 โ†’ 35Steps before main moves14Commits since 21 April 20263,665

Sources: .github/workflows/ci.yml, scripts/merge-gate.mjs and git log in Librettoโ€™s repository

Where we started

For the first months Libretto grew the way a prototype grows: quickly, and forwards. There were 662 commits in April, 90 in June, a few hundred a month over the summer. The code worked, the site built, the editor opened. What was missing was a way to tell whether a change made it better or worse.

On 6 October we counted. The type checker found 636 errors in the application and 549 in the tests: 1,185 in all. knip, which looks for files, exports and dependencies nobody uses, reported 483. None of those numbers stopped anything. An agent could add ten type errors and its work reached production like everyone elseโ€™s.

The first limits

The first limit was a ratchet. The idea is simple: todayโ€™s count becomes the maximum allowed. A change that exceeds it does not pass; a change that lowers it has to lower the limit too, in the same commit. The number can only go down, and whoever brings it down fixes it there for everyone who comes after. It does not ask for everything to be fixed in a day. It asks that nothing ever gets worse.

The layer boundaries came the same day. Libretto has four parts, the admin, the shared code, the site themes and the server, and not every part may depend on every other: a theme must not import the admin, and the server must import neither. On 6 October the rules were a snapshot; on the 7th they became errors. Today there are 17, and the list of allowed exceptions is empty.

The third limit is about data, not code. A Libretto page is a tree of nodes, and since 7 October every write to that tree goes through a single check, validateTree. If a node lands in the wrong place, the write is refused with the reason, and the tree stays as it was. That holds for the editor and for the agents, which since 13 October also work on sites through an MCP interface.

More agents, one door

With the limits in place we picked up the pace. On 15 October we decided to run three jobs in parallel, and no more: each in its own copy of the code, with its own database, so that two agents would not get in each otherโ€™s way. The next day there were 216 commits, and thirteen branches were merged into main.

Every merge brought the numbers down: the ratchet comment in CI keeps the tally, line by line. On 16 October, the day of the thirteen merges, type errors went from 491 to 262. By 20 October they were 234, 80 per cent fewer than on the 6th. Over the same period the issues knip reported went from 483 to 37.

The chart below is knipโ€™s ratchet. It never climbs: each step is a change that took something away and lowered the limit with it.

knip issues in Libretto from 6 to 25 October 2026: from 483 to 35.knip issues at each step of the ratchet, 6 to 25 October 2026.
Source the ratchet comment in .github/workflows/ci.yml

Agents as users too

The agents do not only write Librettoโ€™s code: they use it. Through the MCP interface an agent reads a site, looks up the operations it needs and sends them in batches. The same rules apply here as well, because a site built by an agent has to hold as well as one built by a person.

A batch can be tried first: the dry run runs every check and then rolls back, and publishing in a dry run is always refused. When a batch is applied, the answer holds the revisions and the call to undo it. Publishing a site is a separate operation that has to be confirmed explicitly, and the agent asks for it only once a person has given the go. Since 25 October, when a tool does not allow something, the agent does not look for a shortcut: it opens a ticket for whoever maintains the platform, saying what is missing.

What broke

The limits held almost every time. The times they did not taught us more than the rest.

A count taken in the wrong place. On 21 October an agent lowered the applicationโ€™s type-error limit to 107. The number was right on its machine and wrong in CI, which counted 111: it had been measured right after a build, when the generated types differ from the ones CI checks. The change was reverted and the limit went back where it was. No error had been added, but for a moment the limit asked for something the code was not.1

A check that mistook silence for success. The type check counts the error lines the compiler prints. When the compiler runs out of memory and stops, it prints no error lines at all: the count reads zero, and zero is under every limit. We saw it on 23 October. Since then a compiler that stops makes the check fail instead of pass.

A release that did not wait. Until 25 October a push to main started two things at once: the checks and the release. The release waited only for the image build, not for the checks. That day we found a commit in production with part of its end-to-end tests red. Since then a release starts only when CI is green.

A door that could not be shut. The obvious answer would have been to protect main on GitHub. On the free plan, for a private repository, GitHub says no. So the gate became a script. It takes a lock, so that one branch merges at a time; it merges the branch into main in a dedicated copy, with its own database; it runs the same fourteen steps as CI, cheapest first, stopping at the first red one; and only at the end does it push the result, without force. If main has moved in the meantime, the push is refused and the gate starts again from the new point.2

The numbers that remain

On 25 October the ratchet reached complexity too: the functions above complexity 15 are on a list, by name, that can only get shorter. There were 149; today there are 143. On 26 October the applicationโ€™s type-error limit went down to 100. These numbers are still high, and we say so: a ratchet tells you things are not getting worse, not that they are already good.

What it taught us

A limit is only as good as the count beneath it. If the number is measured in one place and checked in another, sooner or later the two drift apart. Now we count where CI counts, and a script reads the limits straight from the CI file, so there are never two versions of the same number.

A check has to tell silence from success. Zero errors and no answer are two different things, and a check that cannot tell them apart lets through exactly the worst case.

Checks have to run on what will really be published. A branch that is green on its own says nothing about what happens when it meets everyone elseโ€™s work: that is why the gate merges first and checks after, and why the release waits.

And the agentsโ€™ speed is useful only inside these bounds. Three jobs in parallel, thirteen merges in a day, 274 commits on 25 October: none of it would have held without one door, the same checks for everyone, and a person who decides when something gets published.

The method that came out of it is written down, in general form, on The method. Librettoโ€™s numbers are on the case page.

Notes and sources

  1. The commit that reverts the drop to 107 gives the cause: the types generated after a build differ from the ones CI uses.
  2. The gate is scripts/merge-gate.mjs; its fourteen steps are the same commands as CI, with the limits read from the CI file.
  3. Every figure comes from Librettoโ€™s repository, read on 27 October 2026: git log, .github/workflows/ci.yml, scripts/merge-gate.mjs, .dependency-cruiser.cjs and docs/metrics/ccn-over-15.json.

More notes

If something here is useful and youโ€™d like a hand making it your own, weโ€™re at info@innestidigital.it. If not, thatโ€™s fine too.

0