← Back

The System

Four public repos talk over frozen HTTP contracts. An audit found each side's tests asserting against its own copy of the shape.

Case study summary

Both sides report GREEN against their own copy of the shape. The wire between them is BROKEN. PROVIDER own fixture GREEN CONSUMER own stub GREEN THE WIRE BROKEN

Two unit tests that happen to agree are not a contract test. Each side was green. The seam was not.

I direct Claude Code to build it. The operating layer is public: agent-ops — the PreToolUse guards that enforce the security posture mechanically, the working agreements parallel sessions run under, and an incident log held to a deliberate severity bar, failures left in.

The four repos

All four repos are public. The ADRs and contract tests below live in those repos.

The map

Click a box to see what each part does. The same flow is written out below.

Click a box above to see what each part does.

How it flows

  1. A note is created in notes-api (POST /notes), which enqueues a FastAPI BackgroundTask to classify and tag it. The 201 returns immediately; the task runs after the response is sent.
  2. The background task POSTs the note text to the classifier's /classify endpoint.
  3. The classifier classifies the text (one Sonnet call, structured output forced via tool-use) into a category, an operational domain, and a region, and returns them.
  4. The task writes the labels back as namespaced tags (PUT /notes/{id}/tags, replace semantics), preserving the user's own tags. It currently maps the first two; the region field is newer than this consumer. CLASSIFIER_URL unset makes the whole step a no-op.
  5. The kb-agent reads notes (GET /notes) to ground its RAG answers, and can also call the classifier synchronously.

That is the loop in words. Watch one note go through it, with the real schemas

The problem

The four repos should act as one system and stay independently deployable. That lives in the contracts between them, not in shared code.

The decisions

Decision · SYS-004 / SYS-006

Decouple with contracts, not shared code

The repos talk over frozen HTTP contracts with explicit versioning rules, and each side carries tests pinning the shape.

Why: a renamed field breaks the consumer silently at runtime. Tradeoff: freeze the wire shape and write the tests, so drift fails a build instead of production.

I audited my own decision log in July 2026 and found this seam broken. The classifier shipped a third field. The frozen contract required a coordinated consumer update. That update never happened, and both repos' CI stayed green. Each side's “contract test” asserted against its own copy of the shape. The provider's fixture was updated in the same commit that changed the provider. The consumer checked itself against a stub it wrote. Neither could see the other.

A real contract test needs a single shared artifact both sides assert against, so changing one copy fails the other's build. Amended in the open at SYS-004, with the falsified consequences struck through rather than deleted. False Green collects six more checks that reported confidence they had not earned. Context Architecture is the same split one layer up, where the rules an agent reads get promoted from prose into hooks.

Reversed. Decision · SYS-005

Make the enrichment loop async and idempotent by design

Creating a note triggers a FastAPI BackgroundTask in notes-api. The task calls the classifier, then writes the returned labels back as namespaced tags (category: / domain:) with replace semantics — notes-api owns the writeback, the classifier stays a pure provider.

Why: running classification in a BackgroundTask keeps note creation fast and decouples it from classifier latency, while replace semantics ensure reprocessing converges instead of accumulating duplicate tags. Tradeoff: each task needs its own retry path and its own error handling.

Decision · SYS-002

Gate model choice on evidence

Default to Sonnet across the system. Escalate to Opus only where an eval shows the quality gain pays for itself. The same gate ran the July 2026 migration to Sonnet 5: eval before and after on the same gold set.

Why: the escalation has now been built and measured three separate times, and declined all three — the verdicts are on the classifier and the judge pages, where they were measured. Tradeoff: you have to run the eval to justify the tier instead of defaulting to the biggest model.

Decision · SYS-001 / ADR-001

Record decisions, then make the system legible

Cross-repo decisions live as two-tier ADRs (repo-local choices stay local; system-level ones are SYS-NNN in one place). A generated portal pulls every repo's docs into one browsable view that deep-links back to the code. It links to each doc where it already lives instead of copying it, so there's a single source of truth.

Why: a decision that is not written down gets re-argued.

The outcome

Where it goes next

The roadmap is an autonomy ladder: the same classifier, more self-direction one level at a time. The ladder, level by level