Jaunt
Guides

Gating Generated Code in CI

Use jaunt check as a deterministic, model-free CI gate that fails when generated code drifts from its spec.

You want CI to fail when someone commits a spec change without rebuilding, or lets committed code drift from its contract. You don't want CI to call a model, and you don't want to store an API key on the runner. jaunt check is that gate for Python, TypeScript, and mixed workspaces. It is also the gate Jaunt runs on itself: every pull request to the framework passes jaunt check over Jaunt's own self-hosted modules, keyless.

jaunt check is the gate

jaunt check verifies two things and never calls the model:

  • Contract batteries — committed pytest or Vitest batteries still pass against the committed code.
  • Magic freshness — Python magic specs and private TypeScript *.jaunt.ts[x] specs have matching, up-to-date artifacts. A spec edited without a rebuild is stale; a spec never built is unbuilt.

It exits 4 on any blocking state and prints which module blocked and why:

$ jaunt check
Contract check: 0 contract function(s).
Magic freshness: 1 unbuilt, 0 stale.
[BLOCK] specs: unbuilt

Because the check is deterministic and model-free, the runner needs no CODEX_API_KEY. reconcile and build call the model; check reads digests, validates generated artifacts, and runs committed tests. A TypeScript job must install its locked npm dependencies first, including the project-local worker, compiler, and Vitest.

Exit codes

jaunt check shares Jaunt's exit-code protocol, so a plain jaunt check step is enough — a non-zero exit fails the job.

CodeMeaning
0Success — nothing stale, all batteries pass
2Config, discovery, or dependency-cycle error
3Code-generation error (not raised by check)
4A battery failed, or a magic spec is stale or unbuilt
5Timeout while waiting for daemon jobs (not raised by check)

Gating one half at a time

By default check gates both contracts and magic. Two mutually exclusive flags narrow it:

jaunt check --contracts-only   # only the committed contract batteries
jaunt check --magic-only       # only generated-artifact freshness

On the same project as above, --contracts-only passes (there are no contracts to break) while --magic-only still catches the unbuilt spec:

$ jaunt check --contracts-only
Contract check: 0 contract function(s).      # exit 0

$ jaunt check --magic-only
Magic freshness: 1 unbuilt, 0 stale.
[BLOCK] specs: unbuilt                        # exit 4

Split them across jobs when you want the failure annotated as "contract drift" versus "stale build" in your CI UI.

Machine-readable output for annotations

--json emits the full state on stdout (errors stay on stderr), which is what you parse to post inline annotations or a PR comment:

$ jaunt check --json
{
  "command": "check",
  "ok": false,
  "blocked": [],
  "checked": [],
  "magic": {
    "fresh": [],
    "stale": {},
    "unbuilt": [
      "specs"
    ]
  }
}

The magic block splits modules into fresh, stale (with the reason), and unbuilt; checked and blocked cover the contract batteries. See the JSON output reference for the full schema.

Also worth gating: the repo map

If you maintain a treedocs.yaml repo map, jaunt tree --check fails when the tree drifts from the source (new files, removed files, edited descriptions). It exits 4 the same way check does:

$ jaunt tree --check
drift: +1 new, -0 removed, ~0 stale description(s). Run `jaunt tree`.

Add it as a second step when the repo map is part of your build context.

GitHub Actions

A complete job. It installs uv, then runs jaunt check with uvx so nothing is added to the project's own dependencies:

name: jaunt

on: [push, pull_request]

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install uv
        uses: astral-sh/setup-uv@v5
        with:
          python-version: "3.12"

      - name: Sync project
        run: uv sync

      - name: Gate generated code
        run: uvx --from jaunt jaunt check

No secret is wired in, because check never reaches the network. If you also gate the repo map, add a uvx --from jaunt jaunt tree --check step.

TypeScript targets

Install the Node dependencies from the package named by [target.ts].tool_owner, then run the same gate. This example uses Node 24; Jaunt's worker supports Node 20 through 24.

name: jaunt-typescript

on: [push, pull_request]

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: "24"
          cache: npm

      - uses: astral-sh/setup-uv@v5
        with:
          python-version: "3.12"

      - run: npm ci
      - run: uvx --from jaunt jaunt check --language ts

@usejaunt/ts, a supported TypeScript compiler, and Vitest must be direct devDependencies of their owning packages. Add fast-check directly when generated property cases use @prop. For a mixed workspace, omit --language ts to gate both targets in one command, or split --language py and --language ts into separate jobs for clearer annotations.

Guarding agents locally: jaunt guard

CI catches drift after the fact. In an agent-driven repo you also want to stop an agent from editing __generated__/ in the first place, since those edits are overwritten on the next build. jaunt guard is a PreToolUse hook: it reads the tool-call payload on stdin and, when the target is under __generated__/, asks the agent to edit the spec instead.

$ echo '{"tool_name":"Edit","tool_input":{"file_path":"__generated__/specs.py"}}' | jaunt guard
{"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "ask", "permissionDecisionReason": "__generated__/specs.py is machine-owned generated code (jaunt). Edit the spec instead: specs.py. Changes here are overwritten on the next build."}}

A tool call against a source path produces no output and is allowed through. Wire it into Claude Code via .claude/settings.json — see the coding-agents guide for the full hook block.

Next: For Coding Agents.

On this page