Notaire

Act I · The problem

Case study · Notaire · 2014 → 2026

Fast agents, slow trust.How Notaire learned to ship AI-written code.

A legacy Java desktop app was rebuilt as Spring Boot, Next.js and PostgreSQL, mostly by AI agents and one human Owner. This is the step-by-step story of the process that made that safe: what broke, what each failure taught, and how the lessons became a framework any repository can adopt.

commits
2,187
24 of them from the 2014 desktop app
merged pull requests
954
834 without Dependabot
architecture violations
512 → 0
shrink-only baseline, four days
bug issues per merged PR
0.78 → 0.09
July → October 2026

The arc in one chart

Throughput grew while defects per change fell, but the process itself took up to a third of all merged work while it was being built. Both are part of the story.

Merged PRs, process PRs and bug issues per merged PR, by monthMar 26: 15 merged PRs, 1 about the process, 0 bug issues; Apr 26: 38 merged PRs, 6 about the process, 6 bug issues; May 26: 33 merged PRs, 0 about the process, 3 bug issues; Jun 26: 94 merged PRs, 2 about the process, 17 bug issues; Jul 26: 69 merged PRs, 5 about the process, 54 bug issues; Aug 26: 52 merged PRs, 18 about the process, 17 bug issues; Sep 26: 97 merged PRs, 34 about the process, 32 bug issues; Oct 26: 435 merged PRs, 69 about the process, 40 bug issues01002003004005000.000.250.500.751.00Mar 2615 PRsApr 2638 PRsMay 2633 PRsJun 2694 PRsJul 2669 PRsAug 2652 PRsSep 2697 PRsOct 26435 PRsMar 26: 0.00 bug issues per merged PRApr 26: 0.16 bug issues per merged PRMay 26: 0.09 bug issues per merged PRJun 26: 0.18 bug issues per merged PRJul 26: 0.78 bug issues per merged PRAug 26: 0.33 bug issues per merged PRSep 26: 0.33 bug issues per merged PROct 26: 0.09 bug issues per merged PR
merged PRs (no Dependabot) PRs about the process itself bug issues opened per merged PR (right axis)
Show the data as a table
Monthly delivery data
MonthCommitsMerged PRsProcess PRsBug issuesBugs / PR
2026-038715100.00
2026-0413338660.16
2026-055033030.09
2026-06191942170.18
2026-07149695540.78
2026-081355218170.33
2026-096949734320.33
2026-1067043569400.09
Snapshot generated 2026-10-28 by python3 workspace/ci/generate-ai-sdlc-history.py. Correlation, not proof of causation: bug issues are counted by the month they were opened.

2014 → Dec 2025

Chapter 1: A university desktop app nobody could change safely

Problem
Notaire began in 2014 as a Java Swing monolith with direct database access: 24 commits, no tests, no pipeline. Rebuilding it as Spring Boot, Next.js and PostgreSQL was a multi-year job for one maintainer.
Move
December 2025: the first agent entry file lands, and AI assistants start writing most of the new code.
Result
The bet: agents bring the speed. The open question: who proves the work is right?

Mar → Jul 2026

Chapter 2: Speed without proof

Problem
Merged PRs grew from 15 to 94 a month with rule files and prompts, but the rules disagreed with each other and nothing enforced them. In July, 54 bug issues were opened against 69 merged PRs.
Move
A local preflight began to mirror CI, so a push that passes locally passes on the server.
Result
Lesson: prose rules are wishes. The always-loaded context also grew past 16k tokens of imported rules.
before · AGENTS.md until #1259 — 16,737 bytes, every rule imported up front
@CONSTITUTION.md
@.claude/rules/ai-agent-workflow.md
@.claude/skills/ai-agent-workflow/SKILL.md
…
@.claude/rules/general.md
@.claude/rules/programming.md
@.claude/rules/code-quality.md
@.claude/rules/refactoring.md
@.claude/rules/ui-ux-design.md
after · 3,385 bytes, a 3 KB card, rules on demand, budget checked in CI
**CONSTITUTION.md is highest process authority.**
Always-loaded digest: CONSTITUTION-AGENT-CARD.md.

| When touching | Load |
| Any Java/backend | .claude/rules/programming.md, … |
| Flyway / schema  | .claude/rules/database-migrations.md |

# guard: agent-context-budget.py --max-tokens 8000

Aug 2026

Chapter 3: One Constitution, and a Gate 1 that cannot be fooled

Problem
Specifications were optional and Gate 1 only checked that a proposal cited something shaped like an issue number. An agent could cite #999999 and pass.
Move
An agent-agnostic Engineering Constitution became the single authority, wired into OpenSpec so every agent receives it, and Gate 1 began resolving the issue live on GitHub.
Result
Bug issues per merged PR fell from 0.78 to 0.33 the next month, while the process itself took a third of all PRs.
before · Gate 1 until #793 — a format check
# A real issue number, not the template placeholder.
if grep -qE '^\|[[:space:]]*GitHub Issue[[:space:]]*\|[[:space:]]*#[0-9]+' "$proposal"; then
  ok "proposal: GitHub Issue referenced"
after · the issue must exist on GitHub
check_issue_live() {
  local number="$1"
  state="$(gh issue view "$number" --json state --jq '.state')"
  if [ -z "$state" ]; then
    bad "proposal: Issue #$number was not found on GitHub"
    return 1

Sep 2026

Chapter 4: Mechanize every rule that can fail

Problem
Agents skipped steps the Constitution required, and security skills were used aspirationally rather than applied.
Move
Harness hooks block pushes to main, CI checks the process itself (commit subjects, tests with code, spec or exception), and a dashboarded pipeline runs the security case catalog before every PR.
Result
Each rule moved from a sentence into a command with an exit code, and therefore into every agent's path.

Sep 2026

Chapter 5: Models that claim success they have not earned

Problem
A local 9B-to-20B model wrote "1,940 tests pass" and "PR created" into its task list, copied commit SHAs from main and reported changes its commits never made. One issue took 16 worker runs, 17 retries and 3 foreman stops.
Move
A foreman/worker harness took the truth away from the worker: one phase per run, frozen test commands, scope and ref guards, a harness-owned ledger, harness-owned pushes, and review notes with CHECK/EXPECTED lines.
Result
Gates caught every observed failure, but not every cheap pass: a duplicate test and an absolute path still needed a reviewer. Review cannot be removed; it can be made checkable.

Oct 2026

Chapter 6: A fleet, and the discipline to serialize it

Problem
Cloud agents worked in parallel and merged 435 PRs in a month. One merged on "all 18 checks success" while integration and E2E suites were still running; parallel heavy PRs queued behind each other; closed issues left specs that blocked every other PR.
Move
A heavy-CI merge gate, one written Foreman merge rule, an atomic claim protocol, archive-in-the-PR, continuous publication, and one heavy PR at a time.
Result
Throughput rose six-fold over July while bug issues per merged PR fell to 0.09. The process share of PRs dropped from 35% to 16% as its toil was automated.

14 → 17 Oct 2026

Chapter 7: Ratchets: 512 architecture violations to zero in four days

Problem
The top-priority goal was a full Ports & Adapters system, but the code had 512 known violations. A big-bang rewrite by parallel agents would have been unreviewable.
Move
ArchUnit and ESLint rules landed on day one with a shrink-only baseline: a new violation fails the build, a fixed one must be removed from the file. One domain per PR.
Result
The hexagon baseline went from 512 entries to 0, and frontend boundary suppressions from 66 to 0.
before · hexagon-baseline.txt at #1695 — 512 entries (133 R1, 377 R2, 2 R6)
R1 | …adapter.in.web.budget.BudgetController -> …business.Budget
R1 | …adapter.in.web.budget.BudgetController -> …business.BudgetStatus
R1 | …adapter.in.web.budget.BudgetController -> …business.Person
R1 | …adapter.in.web.budget.BudgetController -> …repository.PersonRepository
… 508 more
after · #1914 — empty, and now it can only stay empty
$ aisdlc ratchet --base origin/main
  PASS  ArchUnit hexagon baseline (R1–R6) — 0 entries (base 0)
  PASS  frontend hexagon ESLint suppressions — 0 entries (base 0)
ratchets: PASSED

28 Oct 2026

Chapter 8: Extracting a framework any repository can adopt

Problem
The process worked, but it was welded to Notaire: the Constitution named Maven and Playwright, and three agent runtimes kept three configurations. A rule like "never add a line to a baseline by hand" was still only prose.
Move
One policy, one TOML profile, one zero-dependency kit. Notaire became its first adopter: the kit runs Notaire's existing scripts and adds the checks that were missing.
Result
On its first run the kit found four real defects in the skills catalog, and it now fails any hand-added baseline line in preflight and CI.

See it run

Two real runs of the extracted kit: one on Notaire, one on a brand-new repository with nothing but git and Python.

A hand-added baseline line, caught
$ aisdlc --profile .aisdlc/aisdlc.toml ratchet --base origin/main  FAIL  ArchUnit hexagon baseline (R1–R6) — 1 entry added to        backend-api/src/test/resources/architecture/hexagon-baseline.txt since origin/main;        fix the violation instead of listing it:        + R1 | adapter.in.web.FooController -> jpa.FooJpaController (+1)ratchets: FAILED (ArchUnit hexagon baseline (R1–R6))
Real output: a violation appended to Notaire's hexagon baseline, then checked against main.
Code without a test, in a brand-new repository
$ aisdlc gate pre-push --base HEAD~1stage pre-push · G3 verified  PASS  commit subjects (exit 0, 0.1s)  FAIL  tests come with code (exit 1, 0.1s)        |   FAIL  production code changed without any test change:        |         src/app.py  PASS  agent context budget (exit 0, 0.0s)  PASS  ratchets (exit 0, 0.1s)  TODO  test suite — placeholder; configure its command in the profilestage pre-push: FAILED
Real output: `aisdlc init` in an empty repository, then a commit that adds code and no test.

What went wrong while writing this

The tests broke the repository

Pushing from a linked worktree, git exported GIT_DIR to the pre-push hook. Self-tests that build throwaway repositories ran git init against it and set core.bare = true in the shared config. The fix is a gate, not a note: preflight and the kit now drop hook-exported git variables, and tests prove it.

Two agents took the same issue

One session opened the issue and pushed its branch; another claimed it minutes later through the claim protocol. A pushed branch is not a claim. The lesson went into the framework: claim before branching, in every runtime.

Lessons and trade-offs

A rule without an exit code is a wish

Every rule that held in Notaire became a script or a test. The ones that stayed prose were the ones agents broke.

Trade-off · Writing and maintaining checks is real work; budget for it.

Agents are not witnesses

A model's report of its own success is not evidence. The harness, not the worker, owns ledgers, pushes and results.

Trade-off · Harness code grows; it needs its own tests and its own review.

Gates tell pass from fail, not right from cheap

Duplicate tests, absolute paths and invented scope pass gates. An independent reviewer with checkable notes catches them.

Trade-off · Human review time stays in the loop for anything high-risk.

Serialize what is scarce

Parallel agents scale only when heavy CI, shared files and merges are serialized and work is published continuously.

Trade-off · Some agents wait; that is cheaper than conflict storms.

Ratchet, do not big-bang

Freeze today's violations, forbid new ones, and let every PR shrink the list. 512 became 0 in four days.

Trade-off · Baselines need a guard against hand edits — now added.

Size the process to the project

The process took a third of all PRs while it was being built. Adoption levels keep a small project from paying for a fleet.

Trade-off · Lower levels accept more risk; raise the level on evidence.

Take the process, not the stack

The framework keeps everything that worked here and names no language, build tool or agent product. One policy, one profile, one zero-dependency kit.