Skip to content

The Drift Log · Governing agents that write code

Enforce schema contracts for agent‑generated code at merge time

13 September 2026 · 3 min read · 701 words · established

A crystal shard held at a red-lit gateway in a black wall

A deterministic, model‑independent validator can block non‑conforming agent code at merge time, keeping the main branch clean.

The failure: unchecked agent code reaches the main branch

Agents can generate TypeScript files, push them, and the CI system will merge them without a human eye. The result is code that does not respect the project's public API, violates naming conventions, or introduces subtle type mismatches. When the breakage surfaces weeks later, the cost of a hot‑fix dwarfs the time saved by the agent. The root cause is the absence of a code gate that validates the agent's output against the project's schema contracts before the merge is permitted.

What a code gate must enforce

A code gate is a deterministic check that runs at merge time. It takes the set of files an agent proposes, runs them through a TypeScript schema validator, and either returns a PASS or a detailed FAIL response. The validator must be model‑independent: it does not rely on any AI model at runtime, only on the static type system and the project's declared contracts. The contract is expressed as a set of .d.ts files or a zod schema that the repository already uses. The gate’s job is to guarantee that every new piece of agent code conforms to those contracts before it becomes part of the codebase.

Worked example

Consider a repository that defines a User interface in src/types.ts:

export interface User {
  id: string;
  email: string;
  role: 'admin' | 'member';
}

An agent proposes a new file src/generated/userService.ts that implements a helper:

export function createUser(data: any): User {
  // naive implementation
  return {
    id: data.id,
    email: data.email,
    role: data.isAdmin ? 'admin' : 'member',
  };
}

The merge‑time validator loads the project's User schema and type‑checks the new file. It discovers that the function accepts any, which bypasses the contract that callers must supply a User‑shaped payload. The validator emits:

ERROR: src/generated/userService.ts:2
  Parameter 'data' is of type 'any' but should be 'Partial<User>'.
  Refactor to accept a typed argument.

Because the validator returns a FAIL, the pull request is blocked. The developer (or the supervising system) can correct the signature to createUser(data: Partial<User>) and push again. The next validation passes, and the merge proceeds.

Embedding the validator in the merge workflow

The validator can be run as a GitHub Action, a GitLab CI job, or any CI system that supports a pre‑merge hook. The steps are:

  1. Checkout the PR head.
  2. Install the project's TypeScript compiler and any schema libraries (e.g., zod).
  3. Run the custom validation script, which imports the project's schema definitions and type‑checks the new files.
  4. If the script exits with a non‑zero status, the CI job fails and the PR shows the error messages.

Because the validator is a pure TypeScript program, its output is repeatable: the same input files always produce the same pass/fail result. This property aligns with the broader principle of treating the agent write as a Build Intent gate, as described in the pillar post on governing code agents at the build‑intent gate. The merge validation is simply the final enforcement point before the repository mutation occurs.

The validator can be packaged as a small npm module and reused across projects. The SHPBL TypeScript client (@shpbl/sdk) demonstrates how a typed client can be built around a repeatable backend; the same pattern applies to a local validator that consumes the same schema contracts.

Aligning the gate with licensing and provenance

When an agent proposes code, the Build Intent gate first records the proposal, resolves licensing invariants, and returns a terminal state. The merge‑time validator sits downstream of that gate: it does not replace the licensing check, but it ensures that the code that passes the licensing gate also respects the technical contracts. If the validator fails, the proposal remains PROVISIONAL; the agent can iterate without consuming entitlement credits. This separation of concerns prevents the repository from being polluted by code that is legally sound but technically broken.

What to do on Monday

  1. Identify a single public TypeScript interface that your repository treats as a contract (e.g., User).
  2. Add a small CI job that runs tsc --noEmit on any new files introduced by a PR.
  3. Configure the job to fail the PR when type errors appear, and surface the compiler output as feedback.

With those three steps in place, you have a functional merge‑time validator that blocks non‑conforming agent code. The next time an agent writes a file, the CI will either let the merge happen or return a clear error, keeping the main branch clean without sacrificing automation.

This post supports the longer argument in Governing Code Agents at the Build Intent Gate.

Keep reading

Next in the log

The Strategic Master Library · written and reviewed under the house's own epistemic rules: nothing claimed that we cannot show.