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.
Show the data as a table
| Month | Commits | Merged PRs | Process PRs | Bug issues | Bugs / PR |
|---|---|---|---|---|---|
| 2026-03 | 87 | 15 | 1 | 0 | 0.00 |
| 2026-04 | 133 | 38 | 6 | 6 | 0.16 |
| 2026-05 | 50 | 33 | 0 | 3 | 0.09 |
| 2026-06 | 191 | 94 | 2 | 17 | 0.18 |
| 2026-07 | 149 | 69 | 5 | 54 | 0.78 |
| 2026-08 | 135 | 52 | 18 | 17 | 0.33 |
| 2026-09 | 694 | 97 | 34 | 32 | 0.33 |
| 2026-10 | 670 | 435 | 69 | 40 | 0.09 |
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.
@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**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 8000Aug 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.
# 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"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 1Sep 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.
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$ 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: PASSED28 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.
$ 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))
$ 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
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.
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.