# jaunt, don't code.
> *Tyger Tyger, burning bright, / In the forests of the night*
> \-- William Blake, via Alfred Bester's *The Stars My Destination*
Jaunt is a CLI for spec-driven code generation in Python and TypeScript. Python is
stable; the TypeScript target is an alpha in Jaunt 1.7. In either language, you write
a typed contract and Jaunt generates a validated implementation beside it.
In Python, call `jaunt.magic_module(__name__)` once and every typed stub below it is a
spec; reach for `@jaunt.magic` when you need per-symbol options. In TypeScript, private
`*.jaunt.ts` and `*.jaunt.tsx` files are parsed without execution by the project-local
`@usejaunt/ts` worker. Production code imports a normal committed facade, and emitted
JavaScript has no Jaunt runtime dependency.
The spec is the contract. Jaunt reads your signature and docstring, drives the OpenAI Codex CLI to write the code, checks the result against the signature you declared, and stamps each file with a provenance header. Edit a docstring and rebuild: only the specs whose contract actually changed regenerate, because jaunt hashes each spec with its dependencies.
There is a second mode for code you already trust. Python's
[`@jaunt.contract`](/docs/guides/contract-mode) derives a pytest battery from the
committed implementation and its docstring. TypeScript contract mode adopts an
exported function or concrete class and derives a committed Vitest battery from its
declaration and TSDoc. The name comes from Bester's 1956 novel, where to *jaunt* is to
teleport by thinking about where you want to be.
Python in one file [#python-in-one-file]
You write the stubs. Jaunt writes the rest. One `magic_module` call governs the whole file: the docstring-only class, the function stub, and nothing else. The handwritten constant and helper stay yours.
```python
import re
import jaunt
jaunt.magic_module(__name__, prompt="All parsers are RFC 5322 strict.")
ADDR_RE = re.compile(r"[^@\s]+@[^@\s]+") # handwritten constant
class Email:
"""An email message with from_, to, subject, and body string fields.
Validate on construction: from_ and to must each look like an address.
"""
def parse_email(raw: str) -> Email:
"""Parse a raw RFC 5322 payload into an Email. Read the From, To, and
Subject headers and the body after the first blank line. Raise ValueError
on malformed input."""
...
def _render_debug(email: Email) -> str: # real body → handwritten helper
return f"<{email.from_} -> {email.to}>"
```
The [Python quickstart](/docs/tutorials/quickstart) runs a full module end to end. The
[TypeScript quickstart](/docs/tutorials/typescript-quickstart) starts from an empty
directory, synchronizes the typed facade, builds the implementation, and runs the
generated Vitest battery.
And since 1.5.2, [jaunt builds jaunt](/docs/self-hosting): parts of the framework you install are generated from their own docstring specs, and `jaunt check` gates the framework's own drift in CI.
Find your way around [#find-your-way-around]
Two ways in [#two-ways-in]
**Evaluating Jaunt?** Pick the [Python quickstart](/docs/tutorials/quickstart) or the
[TypeScript quickstart](/docs/tutorials/typescript-quickstart), then read
[design philosophy](/docs/concepts/design-philosophy) and the honest
[limitations](/docs/reference/limitations) before you commit to it.
**Building an agent that drives jaunt?** The [coding agents guide](/docs/guides/coding-agents) covers `jaunt instructions` -- the canonical machine-readable project primer -- and the `--json` contract that every command speaks.
---
# Jaunt builds Jaunt
Since 1.5.2, Jaunt's repository is a Jaunt project. A root `jaunt.toml` with
`source_roots = ["src"]` governs part of the framework's own source, using both
modes on the code you install from PyPI. When you run `jaunt build`, some of
the tool doing the building was itself built by `jaunt build`.
This page is the proof, the honest boundaries of it, and how to inspect it in
a clone.
Seven modules are magic-mode specs [#seven-modules-are-magic-mode-specs]
`jaunt.guard`, `jaunt.heldout`, `jaunt.migrate`, and the four contract helpers
`jaunt.contract.{strength, cases, drift, edits}` are docstring-contract stubs.
Their implementations live in `src/jaunt/__generated__/` and their `.pyi`
stubs sit next to each spec — all committed, all shipped in the wheel. Runtime
resolution is by module name, so an installed Jaunt loads its own generated
code without any `jaunt.toml` present.
This is the committed source of `src/jaunt/contract/drift.py`, trimmed:
```python
import enum
import jaunt
jaunt.magic_module(__name__)
class DriftState(enum.Enum): # real body -> handwritten, untouched
UNBUILT = "unbuilt"
STALE_PROSE = "stale-prose"
...
def compute_drift_state(
*,
has_battery: bool,
prose_match: bool,
signature_match: bool,
body_match: bool,
battery_passed: bool | None,
) -> DriftState:
"""Resolve the drift state of one contract function from five boolean signals.
This is a pure, deterministic classifier (no model, no I/O): given the
already-computed comparison signals for a single contract-tracked function,
it returns exactly one ``DriftState``...
"""
raise NotImplementedError
```
The docstring is the module. The body Jaunt generated from it is what runs
when `jaunt check` classifies your contract drift.
The existing test suite was the acceptance gate for every conversion, and no
test was edited to accommodate generation. Where a generated body diverged
from the docstring's intent, the fix went through the docstring and a rebuild,
never through the generated file — the same [fix-forward
rule](/docs/guides/writing-magic-specs) the docs ask of you.
Fifteen modules run contract batteries [#fifteen-modules-run-contract-batteries]
The build-critical core — `digest`, `deps`, `header`, `module_api`,
`module_contract`, `change_detection`, `validation`, `stub_emitter`, `cost`,
`cache`, `config`, `parse_cache`, `discovery`, `generate.fingerprint`,
`reconcile` — uses [contract mode](/docs/guides/contract-mode): the committed
code stays canonical, and `jaunt reconcile` derives pytest batteries from the
docstrings. Thirty-one batteries are committed under `tests/contract/jaunt/`
and run with the rest of the suite.
All thirty-one derived deterministically from structured `Examples:` and
`Raises:` blocks. The reconcile that produced them made zero model calls.
`jaunt check` runs in Jaunt's CI on every pull request and gates both kinds of
drift — spec-vs-generated for the magic modules, prose-vs-battery for the
contract ones — with no API key.
Converting the framework found bugs in the framework [#converting-the-framework-found-bugs-in-the-framework]
That is the point of dogfooding, and it delivered on day one:
* **The advisories channel flagged real quirks in Jaunt's own code** during
its first self-builds: a stale comment in a contract-editing helper, an
`import jaunt as ...` alias edge the helper mishandles, and an ambiguous
requirement in the mutation-scoring spec that code review then confirmed as
an actual behavior divergence. The spec was tightened and the module
rebuilt.
* **Deriving batteries for `config.find_project_root` hung `jaunt reconcile`.**
Mutation-based strength scoring executes AST mutants of a governed body, and
a mutant of an unbounded loop never terminates. Strength scoring now bounds
each mutant's wall-clock time. Any adopter with a `while True:` in a
governed function was one reconcile away from the same hang.
* **Two discovery bugs blocked self-hosting entirely** (a project whose specs
live inside the running tool's own package made discovery return zero
specs) and are fixed in 1.5.2 for any tool that wants to do the same thing.
What stays handwritten, and why [#what-stays-handwritten-and-why]
Self-hosting has hard boundaries, and pretending otherwise would be worse than
naming them:
* **Eight modules can carry no Jaunt decorator at all**: `errors`, `runtime`,
`module_magic`, `decorator_analysis`, `class_analysis`, `registry`,
`spec_ref`, `paths`. They execute during `import jaunt` itself, before
`magic_module` and `contract` exist to be called.
* **Build-critical modules can never be magic-mode.** The builder imports and
runs them to build anything, so a stub would be circular — the builder would
need the generated body it is trying to generate. They get contract mode
instead.
* **`contract.derive` is handwritten by choice.** Its rendered battery bytes
feed the deterministic `check` gate; regenerating the renderer would restale
every committed battery.
* **One limitation**: `jaunt watch` holds a single interpreter, so an edit to
an already-imported framework spec module takes effect on the next process,
not the current one. One-shot CLI runs and daemon job subprocesses always
start fresh.
See for yourself [#see-for-yourself]
```bash
git clone https://github.com/creatorrr/jaunt && cd jaunt
uv sync --frozen
uv run jaunt specs # the framework's own governed specs + dep graph
uv run jaunt status # freshness of the self-hosted modules
uv run jaunt check # exit 0: specs, generated code, and batteries in sync
```
`jaunt check` is deterministic and free — the same gate Jaunt's CI runs, the
same one the [CI guide](/docs/guides/ci) recommends for your project.
---
# Change Detection
Jaunt decides what to rebuild in two layers. The first is deterministic and
free; the second spends one cheap model call to avoid a full rebuild when an
edit changed the wording of a spec but not its meaning. Between them, cosmetic
edits cost nothing and clarifying edits rarely cost a rebuild.
Layer A: AST-normalized digests [#layer-a-ast-normalized-digests]
Spec freshness comes from an **AST-normalized contract digest**, not the raw
file bytes. Ruff reformatting, comment edits, whitespace, and quote-style
changes leave the digest untouched, so they never mark a module stale. This
layer is deterministic, applies to both build and test freshness, and cannot be
turned off.
What does move the digest is anything that changes the contract:
* the signature — parameters, annotations, return type
* the cleaned docstring, anywhere in it
* decorator kwargs such as `deps=` or `prompt=`
* for a whole-class spec, adding, removing, or re-signing a declared member
* a change to a dependency's exported API
Repo-map **content** does not. Adding or editing a sibling spec's repo-map entry
never restales an unchanged module — `jaunt build` and `jaunt check` ignore
repo-map drift, and `jaunt status` surfaces it as an informational note at most.
Turning the repo map on or off is a different matter; that toggle is a
fingerprint input.
Layer B: the semantic gate [#layer-b-the-semantic-gate]
When a spec's docstring genuinely changed but its signature and structure did
not, a small judge model decides whether the edit is **behaviorally meaningful**:
* **Equivalent** — a typo fix, a reword, a clearer sentence that says the same
thing. Jaunt **re-freezes** the module: it rewrites the header digests over
the validated, unchanged generated body instead of paying for a full rebuild
with the main model. `--json` lists these under `"refrozen"`.
* **Meaningful** — the module rebuilds normally.
Structural changes, validation failures, and any error from the gate itself
**fail safe to a rebuild**. The gate can only save work; it can never skip a
real change.
Skip it for one run with `jaunt build --no-semantic-gate` (or the same flag on
`jaunt test`), and every digest change rebuilds. Layer A still applies. The
`[semantic_gate]` keys — which judge model, what reasoning effort — live in the
[configuration reference](/docs/reference/config).
The gate model has to run through `codex exec` (`gpt-5.6-luna` is the default).
`gpt-5.4-nano` does not: `codex exec` attaches a `tool_search` tool that nano
rejects.
Converting to module style [#converting-to-module-style]
Moving a spec from a `@jaunt.magic` decorator to `magic_module` governance is
digest-neutral **as long as the stub body form does not change**. The decorator
line never entered the digest, and Jaunt renders a module-governed spec's
signature through the same path a decorated one uses, so the digest comes out
byte-for-byte identical and the module stays fresh.
Two conversions do move the digest, both correctly:
* **Rewriting a `raise RuntimeError("spec stub")` body to `...`.** That older
body is not a recognized stub form, so its text is part of the current digest.
In a governed module it would not even classify as a spec. Rewriting it to
`...` restales the module once — a one-time cost you pay when you adopt the
convention.
* **Adding or editing a `magic_module` kwarg.** Module defaults like `prompt=`
are part of every governed spec's contract, so editing one restales every spec
in the file. That is the same rule that restales a spec when you edit its own
`prompt=`.
Does this save tokens? [#does-this-save-tokens]
Yes, potentially a lot — but it depends on when you adopted Jaunt and on the
shape of your codebase.
What is unconditional: an unchanged module skips the model entirely, formatting
and comment churn never trigger a rebuild, and a reworded-but-equivalent
docstring re-freezes through the small `[semantic_gate]` judge instead of paying
for a full generation. Day to day, most edits touch a few modules and leave the
rest cached.
What it depends on:
* **Adoption timing.** The first build after you adopt Jaunt regenerates
everything, so the savings start accruing from the *second* build onward.
* **Dependency shape.** Staleness follows the graph. A change to a module's
exported API restales every dependent, so a hub with wide fan-out rebuilds a
lot when it moves; many small, independent modules amortize best.
Inherited base API [#inherited-base-api]
A whole-class spec that inherits from a spec'd base in **another module** also
tracks that base's generated public API. Jaunt hashes the rendered inherited-API
block — method signatures **and** docstrings, read from the base's built
artifact — into the subclass's digest and records it in the header as
`base_api_digest`.
* Change the base's public API — a signature or a docstring on any generated
public method — and the subclass restales, even if you never touched the
subclass's own spec.
* A body-only rebuild of the base, with the same signatures and docstrings,
leaves the block unchanged, so the subclass stays fresh.
* The header stores the post-dependency value, so a later `jaunt status`
recomputes the same digest from the same artifacts and nothing flaps.
A moved base API can mean the subclass body genuinely needs to change, so the
Layer B re-freeze is **refused** when the base's API digest moved. Jaunt falls
back to a full rebuild rather than re-freeze over a body that may now be wrong.
What else counts as a change [#what-else-counts-as-a-change]
Beyond the spec contract, Jaunt fingerprints the machinery that produced the
code: the generation engine and its model settings, the prompt templates
(including the preamble), and each module's exported dependency API. Change any
of those and the affected modules go stale without `--force`. `jaunt status`
shows the resulting stale set, upstream fallout included, and the
[configuration reference](/docs/reference/config) lists the exact keys.
Next: [Repo Context](/docs/concepts/repo-context).
---
# Codex Engine
Codex is Jaunt's only generation engine. Model-backed implementation, test, contract,
skill, and project-overview work drives the OpenAI **Codex CLI** (`codex exec`). Jaunt
keeps discovery, dependency ordering, freshness, validation, repair retries, and
artifact transactions for itself.
Writing a file does not automatically mean calling Codex. TypeScript `sync`, reviewed
`design --apply`, compatible `migrate --language ts --apply`, `check`, and deterministic
re-stamps use the worker and local toolchain only. Fresh build and test targets skip
generation too.
The older `legacy` (direct provider SDK) and `aider` engines are gone.
`agent.engine` must be `"codex"`; any other value is a config error (exit
code `2`).
Prerequisites [#prerequisites]
Codex is an external CLI, so install and authenticate it once before you run
Jaunt:
```bash
# Install the Codex CLI (see the OpenAI Codex docs for your platform), then:
codex login
```
Jaunt shells out to whatever `codex` is on your `PATH`. Authentication belongs
entirely to Codex — `codex login`, or the `CODEX_API_KEY` env var. Jaunt does
not read `llm.api_key_env` for generation. The `[codex]` keys that pick the
model, reasoning effort, and sandbox policy live in the
[configuration reference](/docs/reference/config).
How a generation call works [#how-a-generation-call-works]
For each module that needs model-backed generation:
1. Jaunt renders the target prompt from your contract, per-spec extras, project
context, and target templates, then seeds builtin and project skills into the
workspace at `.agents/skills/` for Codex to find.
2. It spawns one `codex exec --json` subprocess and passes the prompt on stdin.
3. Codex edits inside the configured sandbox; Jaunt reads the JSONL event stream
for the final result.
4. Python validates the proposed module with AST and type checks. TypeScript asks
Codex for reserved internal bindings, composes the deterministic exports, and
checks the complete candidate in the owning compiler overlay, including affected
project references. Either target can run bounded repair retries before an atomic
write.
Each call is its own subprocess. There are no shared credentials and no
process-wide locks, so generation is task-local and parallel-safe by
construction — Jaunt just runs several `codex exec` processes at once.
Freshness [#freshness]
The Codex runtime is part of what Jaunt fingerprints for freshness: switch the model,
sandbox, reasoning effort, or target prompt templates, and affected modules become
stale without `--force`. A fingerprint-only change can be validated and re-stamped
without asking the implementation model to rewrite the body. The full list is in
[Change Detection](/docs/concepts/change-detection).
Test generation [#test-generation]
Generated pytest and Vitest modules have to satisfy the literal test specs you wrote.
Jaunt tests through the **public module contract**, not generated internals. Spell out
every behavior you want covered rather than trusting the engine to guess at a broad,
unstated matrix: the model writes what you specify, not what you meant.
See also: [Configuration](/docs/reference/config) and
[Limitations](/docs/reference/limitations).
---
# Design Philosophy
Jaunt makes one bet: when generating code is cheap and non-deterministic, the
durable things are the **specification** of what you want and the
**verification** that you got it. The code in between is disposable. Everything
in the tool follows from treating the spec as the asset and the generated body
as a build artifact.
Why not just prompt an LLM directly? [#why-not-just-prompt-an-llm-directly]
You can paste a stub into a chat window and get back a working function. For a
one-off, that is faster than any framework. The problem starts on the second
day, when you want to change it, or the tenth function, or the first time a
teammate asks where a behavior is defined.
Jaunt exists for the difference between those two situations:
* **Specs are versioned.** A Python signature and docstring, or a private
TypeScript declaration and TSDoc, is ordinary source in your repo rather than a
chat transcript you'll lose. The intent moves through review and history like
anything else.
* **Rebuilds are incremental.** Jaunt hashes each spec's contract and
regenerates only what changed. Prompting by hand has no memory of what it
produced last time, so every edit is a fresh, full re-generation you have to
re-read end to end.
* **Dependencies are ordered.** Specs can depend on other specs. Jaunt builds a
DAG, generates in the right order, and propagates staleness downstream. A chat
window has no idea your helper changed.
* **The call site stays plain.** Python imports its spec module and forwards to the
generated body. TypeScript imports a committed facade that resolves through normal
compiler and runtime rules. The API you depend on doesn't move when the
implementation does.
None of that is magic the model provides. It's the bookkeeping around the model
that makes generated code something you can live with.
The prose is the contract [#the-prose-is-the-contract]
The typed declaration is the API Jaunt has to match: names, parameters, return type,
members, and async behavior. The Python docstring or TypeScript TSDoc is the
behavioral spec: rules, edge cases, and errors. Together they are the contract, and
the generated body is judged against them and nothing else.
This has a sharp consequence: if a behavior isn't in the contract prose, the model
shouldn't invent it, and you shouldn't rely on it. Behavior that lives only in
the generated code is behavior that will vanish on the next rebuild. When you
find yourself wanting to edit a generated Python module, TypeScript implementation,
API mirror, or sidecar, that's the signal to edit the spec instead. Otherwise your
change isn't versioned, and it won't survive.
What's deterministic, what isn't [#whats-deterministic-what-isnt]
Generation is the non-deterministic part. Ask for the same spec twice and you
can get two different correct implementations. Jaunt doesn't try to make the
model deterministic; it draws a hard boundary around the part that is, so the
non-determinism can't leak into your workflow.
Deterministic, no model involved:
* **Change detection.** A normalized Python AST or TypeScript contract IR decides
what's stale from the contract, the same way every time.
* **Validation.** Python passes AST and type checks. TypeScript candidates are
composed behind a deterministic public boundary and checked in a compiler overlay.
A candidate that fails its target's checks never lands.
* **`jaunt check`.** The CI gate verifies committed artifacts against their
contracts with no API key and no model call. It gives the same verdict on your
machine and in CI.
Non-deterministic, model involved:
* **Generation itself**, and the optional semantic gate that judges whether
reworded docstring or TSDoc prose changed the contract.
The upshot: the model proposes, and a deterministic layer decides whether to
keep the result. An artifact stays frozen until one of its inputs changes, so
what you review is what ships until you change the spec.
When not to use Jaunt [#when-not-to-use-jaunt]
Jaunt earns its keep on glue and boring-but-correct code: parsers, validators,
formatters, adapters, the utilities every project accumulates. It's the wrong
tool when:
* **The code is smaller than its spec.** For a throwaway script or a three-line
helper, writing the contract costs more than writing the function. Just write
the function.
* **You need to hand-tune performance.** On a hot path where you care about the
exact allocations and the exact loop, you want to be the author, not the
reviewer.
* **The invariants are too tight to write down.** Complex stateful systems whose
correctness lives in cross-cutting invariants are hard to pin in behavioral
prose, and what you can't specify, the model can't reliably hit.
* **You depend on fragile runtime behavior.** Code that leans on an exact
library version or an undocumented quirk is a poor fit for a tool that
regenerates freely.
* **Nobody will read the diffs.** A person owns the merge. If generated code
lands without review, you've automated writing code that no one understands —
which is worse than writing it by hand.
The honest version: Jaunt moves your effort from typing implementations to
writing specs and reading diffs. If that trade doesn't help for a given piece of
code, don't force it. See [Limitations](/docs/reference/limitations) for the
specific sharp edges.
What it costs [#what-it-costs]
Each stale module that needs a new implementation is one `codex exec` call, plus
optional model work for Python package skills and the project overview when enabled.
Fingerprint-only drift re-stamps without generation; equivalent prose needs only the
semantic judge. TypeScript npm skills are rendered locally from installed package
metadata. Incremental builds mean you pay for what changed, not for the whole tree.
`jaunt build` prints a cost summary per run, and `jaunt build --json` reports
`context_stats` — how much context each module's prompt actually pulled in,
broken down by block. That's the number to watch if a build costs more than you
expect; it tells you whether the repo map, skills, or retrieval is inflating your
prompts. The [JSON output reference](/docs/reference/json-output) documents the
shape.
Next: [How It Works](/docs/concepts/how-jaunt-works).
---
# How It Works
Jaunt has one loop: you write specs, Jaunt generates modules, you review, you
tighten the spec, you rebuild. Once you can picture that loop, the rest of the
tool follows from it.
The Flow [#the-flow]
```text
You write specs
-> Jaunt discovers them (source_roots / test_roots)
-> routes them through the Python or TypeScript target
-> builds a dependency graph
-> for each module:
- build prompt = target template + contract + project context
- Jaunt drives `codex exec` to propose an implementation
- the target validates the complete candidate
- Jaunt writes the artifact transaction
-> Python imports forward from the spec module
-> TypeScript consumers import the committed facade
```
Python discovery imports prescreened spec modules and uses Python ASTs, ty, and
pytest. TypeScript discovery does not execute application or spec modules: the
project-local `@usejaunt/ts` worker parses private `*.jaunt.ts[x]` inputs with the
owning TypeScript compiler. It validates candidates in an in-memory compiler overlay
before any implementation, API mirror, or sidecar is replaced.
What the model sees [#what-the-model-sees]
At build time Jaunt sends the model a prompt built from a few pieces:
* A target-specific system template (packaged under `src/jaunt/prompts/`).
* Your spec's **source segment**: the Python signature/docstring/decorator options,
or the TypeScript declaration/TSDoc and marker options.
* Optional per-spec context, such as `prompt="..."`.
* A repo map rendered from `treedocs.yaml`, on by default (see
[Repo Context](/docs/concepts/repo-context)).
Library and tooling **skills** are not prompt text. Jaunt seeds them into the
Codex workspace under `.agents/skills/`, where Codex discovers them on its own.
The job the model is handed is narrow: implement the contract the spec implies,
nothing more.
The Codex engine [#the-codex-engine]
Jaunt has a single generation engine, the OpenAI **Codex CLI**. For each module
Jaunt spawns one `codex exec` subprocess and hands it the rendered prompt. Python
validates the proposed module before writing it. TypeScript composes the proposed
internal bindings with its deterministic public boundary, then checks the complete
overlay through the owning project and affected project references before writing.
Everything around that step — discovery, staleness, validation, file locations,
and runtime integration — belongs to Jaunt, not to Codex. See
[Codex Engine](/docs/concepts/codex-engine).
Incremental rebuilds [#incremental-rebuilds]
Jaunt hashes the spec source for each module and writes the digest into the
generated file's header. On the next run it skips modules whose digest still
matches. Change a spec and only what depends on it regenerates.
The digest covers more than the spec text. Jaunt also fingerprints the
generation runtime and the prompt templates, so switching models or editing the
preamble can mark files stale too. [Change Detection](/docs/concepts/change-detection)
covers exactly what counts as a change and what does not.
Dependency ordering [#dependency-ordering]
Jaunt builds a spec-level DAG, so dependencies generate before the specs that
use them, and a change to a dependency propagates staleness to its dependents.
You declare edges with `deps=...`, and Jaunt infers the rest best-effort. See
[Dependencies](/docs/guides/dependencies).
Parallel builds [#parallel-builds]
Jaunt does not build modules in level-synchronous waves. It runs a single ready
queue over the dependency graph and keeps every worker busy.
When a build starts, Jaunt takes the stale modules, computes each one's indegree
(how many of its dependencies are also stale and still unbuilt), and assigns a
priority equal to its critical-path length — the longest chain of dependents
that has to wait on it. Modules with no unbuilt dependencies seed a ready heap,
ordered longest-critical-path first, so the work that unblocks the most other
work goes first.
From there the loop is continuous. Jaunt starts modules off the ready heap until
`[build] jobs` are in flight, then waits for the *first* one to finish rather
than the whole batch. The moment a module completes, every dependent whose last
dependency just landed drops onto the ready heap and can start immediately —
there is no barrier between "levels." A module that **fails** marks only its
dependents failed (recursively); the rest of the graph keeps building. You get
the partial result plus a precise list of what was skipped and why.
Python runtime forwarding [#python-runtime-forwarding]
TypeScript does not use runtime forwarding or a loader hook. Production code imports
the ordinary facade next to the private spec; normal `tsc`, bundler, and Node
resolution take over from there. See the
[TypeScript target guide](/docs/guides/typescript#author-a-module).
A spec module does not generate code when you call it. It forwards your call
into the generated module under `__generated__/`. If that module exists, your
call delegates to it. If it does not, you get `JauntNotBuiltError` and a nudge
to run `jaunt build`. Either way you import the *spec* module in normal code:
the spec module is the stable API surface, and the generated body behind it is
free to change on every rebuild.
The `@jaunt.magic` decorator forwards per symbol — the decorated function or
class is a wrapper that resolves the generated object on first call.
`magic_module` forwards per module with a lighter touch. On the first attribute
access to any governed spec name, Jaunt imports the generated counterpart,
rebinds every spec name in the module to its generated object (functions and
classes alike), and then swaps the module back to a plain module. After that
first access the module is ordinary: attribute lookups hit the generated objects
directly, at zero overhead. It intercepts once, rebinds, and vanishes.
Two-phase registration [#two-phase-registration]
`magic_module` has to register its specs before the stubs below it exist as
runtime objects, because the call runs while the module is still executing. It
does this in two phases. At call time Jaunt parses the module source, classifies
the top-level stubs, and registers each one from the AST alone — signature and
docstring are enough to compute digests, so the spec object can be `None` for
now. Once the module finishes importing, a finalize step backfills every entry
with its real function or class and runs the same analysis a decorated spec
gets. By the time the builder, tester, or `jaunt specs` looks at the registry, a
module-governed spec is indistinguishable from a decorated one — same signature
rendering, same digest inputs. Converting a decorated spec to module style is
digest-neutral as long as you leave the stub body form alone.
PyPI skills [#pypi-skills]
On `jaunt build`, Jaunt can generate "skills" from PyPI READMEs for the external
libraries you import, best-effort. Those skills, plus Jaunt's bundled builtins,
land in the Codex workspace at `.agents/skills/` for Codex to read. It tends to
improve correctness on unfamiliar libraries without bloating your prompt. See
[Auto-Generated PyPI Skills](/docs/guides/pypi-skills).
Where to go deeper [#where-to-go-deeper]
Internals [#internals]
The design notes behind all of this live in the repository, not on this site.
Two are worth reading if you want to know why Jaunt draws the lines where it
does. A [design-principles doc](https://github.com/creatorrr/jaunt/blob/main/docs/principles/2026-06-29-building-with-coding-agents.md)
writes down the rules Jaunt's own generator is held to. And Jaunt is built with
itself: the [`docs/superpowers/`](https://github.com/creatorrr/jaunt/tree/main/docs/superpowers)
directory holds the written specs and plans behind each Jaunt feature, in the
same spec-first style this page describes.
---
# Repo Context
A spec rarely lives alone. It calls helpers, imports siblings, and fits into a
shape the rest of the repo already defines. Jaunt keeps a lightweight map of
that surrounding code and feeds it to the generator, so the model writing one
module knows what exists around it. The map is on by default and works offline.
The repo map [#the-repo-map]
`treedocs.yaml` at the project root holds a one-line description of every
directory and supported source file (`.py`, `.ts`, or `.tsx`; generated output and
declaration files are excluded). Descriptions start from an offline static baseline:
Python module docstrings and public symbols, or TypeScript documentation and exported
surface. They can be enriched by the model if you opt in.
```bash
jaunt tree # sync the tree incrementally (AST-only by default)
jaunt tree --check # CI gate: exit 4 if the tree is stale
```
`jaunt build` keeps the tree in sync on its own and renders a capped repo-map
block into every build prompt. Source digests are cached in
`.jaunt/tree-cache.json`, so a sync only touches what changed. You can turn the
map off for a single build with `jaunt build --no-repo-map` or for the project
with the `[context]` config; see the
[configuration reference](/docs/reference/config) for those keys.
Semantic retrieval (opt-in) [#semantic-retrieval-opt-in]
The repo map tells the model what exists. For Python generation, semantic retrieval
goes one step further and shows it the actual neighboring code. Turn it on, put the
`colgrep` binary (LightOn next-plaid) on your `PATH`, and Jaunt queries `colgrep` for
code relevant to each Python spec, then seeds the top hits into the workspace as
`_context/relevant_*.py`. If `colgrep` is missing, the build still runs — retrieval
just sits out that round.
The TypeScript alpha receives the shared repo map and optional project overview, but
does not yet add `colgrep` hits to its generation context.
The trade-off is real neighboring implementations against an extra tool
dependency and a bit of build time, which is why it stays off until you ask for
it.
Project overview (opt-in) [#project-overview-opt-in]
The repo map is a list of one-liners; a project overview is prose. Switch it on
and Jaunt asks the model to write an architecture summary of the whole codebase
and adds it to build prompts. That call runs once, keyed to a digest of the spec
sources, the repo map, your project docs (README/AGENTS/CLAUDE), and the
overview prompt. The result is cached to `.jaunt/PROJECT_OVERVIEW.md` and reused
until one of those inputs changes, so you pay for the prose once, not per build.
The call counts against your build cost budget and shows up in the cost summary.
Target prompt preambles [#target-prompt-preambles]
Python build prompts open with `src/jaunt/prompts/codex_preamble.md`. TypeScript uses
its own strict system and module templates under `src/jaunt/typescript/prompts/` so
the model writes only reserved internal bindings while Jaunt owns the public boundary.
Both targets ask for straightforward, maintainable source that remains useful after
ejection. These templates add no model call, and their content participates in the
target's generation fingerprint. Override them through `[prompts.py]` or
`[prompts.ts]` in a version-2 config.
Next: [Change Detection](/docs/concepts/change-detection).
---
# Gating Generated Code in CI
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](/docs/self-hosting): every pull
request to the framework passes `jaunt check` over Jaunt's own self-hosted
modules, keyless.
jaunt check is the gate [#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:
```console
$ 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 [#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.
| Code | Meaning |
| ---- | ------------------------------------------------------------- |
| 0 | Success — nothing stale, all batteries pass |
| 2 | Config, discovery, or dependency-cycle error |
| 3 | Code-generation error (not raised by `check`) |
| 4 | A battery failed, or a magic spec is stale or unbuilt |
| 5 | Timeout while waiting for daemon jobs (not raised by `check`) |
Gating one half at a time [#gating-one-half-at-a-time]
By default `check` gates both contracts and magic. Two mutually exclusive flags
narrow it:
```bash
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:
```console
$ 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 [#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:
```console
$ 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](/docs/reference/json-output) for the full schema.
Also worth gating: the repo map [#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:
```console
$ 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 [#github-actions]
A complete job. It installs `uv`, then runs `jaunt check` with `uvx` so nothing
is added to the project's own dependencies:
```yaml
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 [#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.
```yaml
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 [#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.
```console
$ 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](/docs/guides/coding-agents) for the full hook block.
Next: [For Coding Agents](/docs/guides/coding-agents).
---
# Claude Code Plugin
Version 1.2.0 adds version-2 TypeScript target awareness to the Claude Code
plugin. It packages the same CLI-backed Python and TypeScript authoring loop as
the Codex plugin, while keeping Claude's approval-style PreToolUse decision and
first-build reviewer agent.
Install [#install]
```bash
uvx jaunt install-claude-plugin
# from this clone:
uv run jaunt install-claude-plugin --local --root .
```
The direct GitHub flow is:
```bash
claude plugin marketplace add creatorrr/jaunt
claude plugin install jaunt@jaunt-plugins
```
On a rerun, the installer refreshes the marketplace and calls
`claude plugin update jaunt@jaunt-plugins` for an existing installation.
Start a new Claude Code session after installation. For one-session development,
use `claude --plugin-dir ./jaunt-claude-plugin`.
The bundled command hooks require Bash.
Workspace routing [#workspace-routing]
One root `jaunt.toml` can cover several packages and mixed flat/`src`
layouts. `source_roots` and `test_roots` accept literal paths and globs.
Each module uses its longest containing root and nearest owning
`pyproject.toml`. There is no one-config-per-package rule.
Preview consolidation of older nested configs with:
```bash
uv run jaunt migrate --merge-projects
```
Add `--apply` only after the no-model plan reports no route, digest,
fingerprint, or artifact change.
Skills and reviewer [#skills-and-reviewer]
* `/jaunt:working-with-jaunt`: spec rules, workspace routing, and freshness.
* `/jaunt:build`: preview likely model calls, build, report actual cost, and
run the Python or TypeScript native gates.
* `/jaunt:doctor`: authentication, Node/npm/worker/compiler readiness,
workspace health, current stale or unbuilt state, diagnostics, orphans, and
duplicate Claude/Codex guards.
* `/jaunt:convert`: explicit-only Python or TypeScript conversion, with
characterization tests before the first model call.
* `first-build-reviewer`: read-only contract-silence review after a module's
first successful build.
The plugin does not use fixed dollar estimates. It distinguishes likely model
work before a build and reports the actual cost afterward.
Freshness reasons [#freshness-reasons]
| reason | next action |
| --------------------------- | ------------------------------------------------------------------------- |
| `structural` | implementation-model rebuild |
| `prose` | semantic-gate judgment, then refreeze or rebuild |
| `fingerprint` or `re-stamp` | deterministic re-stamp |
| `stub` | deterministic `.pyi` re-emission when implementation inputs are unchanged |
Hooks [#hooks]
The shared SessionStart script reads payload `cwd` and injects a bounded
freshness/orphan summary for each discovered workspace, including TypeScript
unbuilt, invalid, and diagnostic counts.
Because `jaunt status` imports discovered spec modules, enable SessionStart only
for workspaces whose Python code you trust.
The Claude guard resolves the nearest workspace before calling `jaunt guard`,
so custom generated directories use the correct config. It also asks for
approval before editing an existing provenance-headed generated `.pyi` and
points to the matching `.py` spec.
Both hooks fail open on malformed input, missing configuration, unavailable
tools, or timeouts. `/jaunt:doctor` flags hand-written guards in either Claude
or Codex config once the matching plugin hook is enabled.
There is no MCP server; Jaunt's JSON CLI is the machine interface.
Plugin commands prefer a compatible installed `jaunt`, use `uv run --no-sync jaunt` in a
uv project, and fall back to `uvx jaunt` for a JavaScript-only workspace.
Next: [PyPI Skills](/docs/guides/pypi-skills).
---
# Codex Plugin
Jaunt's Codex plugin packages five CLI-backed skills and two hooks. It does not
run an MCP server or app connector. Version 1.1.0 is distributed from this
repository's `.agents/plugins/marketplace.json`.
Install [#install]
```bash
uvx jaunt install-codex-plugin
# from this clone:
uv run jaunt install-codex-plugin --local --root .
```
The default command runs these steps in order:
```bash
codex plugin marketplace add creatorrr/jaunt
codex plugin add jaunt@jaunt-codex-plugins
```
Local mode verifies `.agents/plugins/marketplace.json`, adds the clone root as
the marketplace, and installs the same plugin reference. On a rerun, Git mode
upgrades the marketplace snapshot. Since Codex has no plugin-update command,
the installer re-adds the plugin to refresh its cache. Current Codex releases
refresh in place; if an older CLI reports an already-installed error, the
installer removes and re-adds it. Local mode performs the same refresh without
a Git upgrade.
Start a new Codex session after installation. Open `/hooks` to inspect and
trust the bundled hook definitions; changed hooks require review again.
The bundled command hooks require Bash (macOS, Linux, or a Windows environment
that provides Bash).
Skills [#skills]
* `$jaunt:working-with-jaunt` loads when work touches Jaunt specs, config, or
generated artifacts.
* `$jaunt:build` resolves the workspace, previews likely model calls, builds,
reports actual cost, and runs the gates.
* `$jaunt:doctor` checks Python and TypeScript workspace health, including the
Node/npm/worker/compiler setup, without building.
* `$jaunt:convert` converts Python or TypeScript only when explicitly invoked.
* `$jaunt:first-build-reviewer` runs when explicitly invoked or delegated by
the build workflow.
On a first build, the build skill delegates one read-only explorer subagent to
the reviewer checklist when delegation is available. Otherwise it checks the
same contract-silence risks in the main thread.
Workspace model [#workspace-model]
Jaunt 1.6.2 lets one `jaunt.toml` cover several packages. Source and test roots
may be literal paths or globs. Each module follows its longest containing root
and nearest `pyproject.toml`; that owner controls generated files, stubs,
dependency validation, tests, and contract batteries.
Use `jaunt migrate --merge-projects` to preview consolidation of older child
configs. The command makes no model call and refuses route, digest,
fingerprint, or artifact changes.
Freshness [#freshness]
The plugin reports Jaunt's current reasons without fixed price estimates:
| reason | next action |
| --------------------------- | ------------------------------------------------------------------------- |
| `structural` | implementation-model rebuild |
| `prose` | semantic-gate judgment, then refreeze or rebuild |
| `fingerprint` or `re-stamp` | deterministic re-stamp |
| `stub` | deterministic `.pyi` re-emission when implementation inputs are unchanged |
The build skill reports the actual cost after the command completes.
Hooks [#hooks]
The SessionStart hook reads `cwd` from its JSON payload and scans for a bounded
set of workspaces, injecting a one-line freshness and orphan summary for each.
TypeScript summaries include unbuilt, invalid, and diagnostic state.
`jaunt status` imports discovered spec modules, so trust the SessionStart hook
only in workspaces whose Python code you trust.
The PreToolUse hook matches `apply_patch`. It extracts Add, Update, Delete, and
Move paths, resolves them against payload `cwd`, and checks each path through
`jaunt guard`. Jaunt's approval result becomes a Codex `deny`, because Codex
does not support `permissionDecision: "ask"` for this event. Existing
provenance-headed generated `.pyi` files are denied too, with a pointer to the
corresponding `.py` spec.
Malformed payloads, missing configuration, missing executables, command
failures, and timeouts fail open. A hook is a useful guardrail, not a complete
security boundary.
Plugin commands select the runner in this order: a compatible installed `jaunt`,
`uv run --no-sync jaunt` inside a uv project, then `uvx jaunt`. The final path
keeps JavaScript-only projects independent of a Python project scaffold.
See the official [plugin](https://learn.chatgpt.com/docs/build-plugins),
[skill](https://learn.chatgpt.com/docs/build-skills), and
[hook](https://learn.chatgpt.com/docs/hooks) documentation for the host formats.
Next: [Claude Code Plugin](/docs/guides/claude-code-plugin).
---
# For Coding Agents
You want an agent — Claude Code, Codex, Cursor, or a CI bot — to drive Jaunt
without a plugin or a server to install. Codex and Claude Code also have
first-party plugins that package the guard hook, freshness summary, and
build/convert/doctor skills: see the [Codex plugin](/docs/guides/codex-plugin)
and [Claude Code plugin](/docs/guides/claude-code-plugin). Everything the agent
needs is on the CLI: a primer to load into context, structured output on every
command it drives, and exit codes it can branch on.
The Primer: jaunt instructions [#the-primer-jaunt-instructions]
Load a project-aware briefing into the agent's context before it operates Jaunt:
```bash
jaunt instructions # framework rules + a live snapshot of this project
jaunt instructions --json # {command, ok, text, project} for tooling
```
The output covers the framework rules — the two authoring modes, the build/test
loop, how to write a good spec, the command and exit-code reference — followed
by a live snapshot of the current project: resolved `[paths]`, engine and model,
or version-2 target routes, semantic-gate and repo-map settings, and which Python or
TypeScript modules are stale. It ships with
the package, so the briefing always matches the installed version. The `--json`
form is what you feed to a harness: `text` holds the primer, `project` holds the
snapshot.
Run outside an initialized project it still succeeds (exit `0`), printing the
framework rules plus a note to run `jaunt init`. That makes it safe to load
unconditionally at the start of an agent session.
For this documentation itself, fetch [`jaunt.ing/llms.txt`](https://jaunt.ing/llms.txt) —
the full docs as one plain-text file, built for loading into an agent's context.
Machine-Readable Output [#machine-readable-output]
The commands agents drive — including `sync`, `design`, `build`, `test`, `check`,
`status`, `specs`, `instructions`, `jobs`, `log`, `tree`, `clean`, and `watch` — accept `--json` (structured output on
stdout, errors on stderr). A few interactive/hook commands (`guard`,
`daemon stop`, `skill show`) do not:
```bash
jaunt specs --json # spec list + dependency graph
jaunt status --json # stale/fresh modules, contract drift, strength
jaunt build --json # {"command": "build", "ok": true, "generated": [...], "refrozen": [...], ...}
```
Progress output is agent-aware too: `--progress {auto,rich,plain,none}`. `auto`
is rich on a TTY and plain lines off-TTY, and an explicit `--progress plain`
composes with `--json` (progress on stderr, JSON on stdout).
Reading The Docs As An Agent [#reading-the-docs-as-an-agent]
The canonical machine-readable briefing is `jaunt instructions`, not this site:
it ships with the installed package, works offline, and always matches the
version the agent is running. Prefer it over scraping documentation pages —
scraped docs can describe a different release than the one on the machine.
These guide and reference pages are authored as plain Markdown (MDX) under
`docs-site/content/docs/` in the repo, so an agent that does want the prose can
read the source files directly from GitHub rather than parsing rendered HTML.
The Agent Loop [#the-agent-loop]
The intended workflow with the [background daemon](/docs/guides/daemon)
running:
```bash
git commit -m "spec: tighten normalize_email contract" \
&& jaunt jobs wait --timeout 1800
```
Exit codes are the protocol: `0` green, `4` a job failed or parked, `5`
timeout. See the full table in the [CLI reference](/docs/reference/cli#exit-codes).
Keeping Agents Out Of __generated__/ [#keeping-agents-out-of-__generated__]
Generated code is overwritten on every build, so an agent must fix specs, not
output. `jaunt guard` is a PreToolUse hook: it reads the tool-call payload on
stdin and, when the target sits under `__generated__/`, returns a decision that
points the agent back at the owning spec. The
[Codex plugin](/docs/guides/codex-plugin) and
[Claude Code plugin](/docs/guides/claude-code-plugin) ship this hook prewired,
including protection for provenance-headed generated `.pyi` files. Or wire the
Claude form yourself via `.claude/settings.json`:
```json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|MultiEdit|Write|Read|NotebookEdit",
"hooks": [{ "type": "command", "command": "jaunt guard" }]
}
]
}
}
```
For harnesses without hook support the barrier is advisory — `jaunt instructions`
states the rule, and CI catches any drift with [`jaunt check`](/docs/guides/ci).
The Principles Behind The Design [#the-principles-behind-the-design]
Jaunt's design follows a written coding-agent principles framework — six core
laws plus named corollaries (value migrates to spec + verification; the model
proposes, a deterministic and independent verdict disposes; treat everything
the model reads as untrusted; …). The full document lives in the repo at
`docs/principles/2026-06-29-building-with-coding-agents.md`, and the build
prompt preamble embeds the laws so the generator operates under the same rules.
Next: [Codex Plugin](/docs/guides/codex-plugin).
---
# Contract Mode
You have code you want to keep hand-written — an adopted legacy function, a hot
path you've tuned, anything the generator should not touch — but you still want
its behavior pinned by a contract. That is contract mode. It is how
[Jaunt pins its own build-critical core](/docs/self-hosting): fifteen framework
modules run committed Python contract batteries derived this way. TypeScript uses
the same lifecycle with exported functions or concrete classes and committed Vitest
batteries.
Jaunt picks a mode per symbol from its marker:
* **Magic mode**: Python uses `@jaunt.magic` / `@jaunt.test`; TypeScript uses
private `*.jaunt.ts[x]` / `*.jaunt-test.ts[x]` inputs. The behavioral prose is
canonical and Jaunt generates the implementation or test battery.
* **Contract mode**: the **committed code is canonical**. Python uses the runtime
no-op `@jaunt.contract` decorator and derives pytest. TypeScript `adopt` adds an
`@jauntContract` TSDoc marker and derives Vitest without moving the source behind
a generated facade.
Python contract mode covers top-level functions (sync or async) and whole classes.
TypeScript covers exported functions and concrete exported classes.
Write a Python contract [#write-a-python-contract]
```python
import re
import jaunt
_NON_ALNUM = re.compile(r"[^a-z0-9]+")
@jaunt.contract
def slugify(title: str) -> str:
"""
Convert a human title into a URL-safe slug.
Examples:
- " Hello, World! " -> "hello-world"
- "C++ > Java" -> "c-java"
- "already-slug" -> "already-slug"
Raises:
- "" raises ValueError
- "!!!" raises ValueError
"""
cleaned = _NON_ALNUM.sub("-", title.strip().lower()).strip("-")
if not cleaned:
raise ValueError("title is empty after cleaning")
return cleaned
```
The `Examples:` and `Raises:` prose is what Jaunt derives test cases from
(configurable via `[contract] derive = ["examples", "errors"]`). The battery is
written to `tests/contract/` (configurable via `[contract] battery_dir`) and is
**committed** — it's plain pytest that runs with or without Jaunt installed.
Python workflow [#python-workflow]
```bash
jaunt adopt my_app.slug:slugify # add @jaunt.contract to existing code + derive its battery
jaunt reconcile # derive/refresh committed batteries (calls the model)
jaunt check # verify batteries deterministically (CI gate, no model, no API key)
jaunt status # per-function drift state + strength score
jaunt eject my_app.slug:slugify # remove contract tracking; leave plain Python + plain pytest
jaunt eject --all
```
For Python, `jaunt reconcile` is the only contract command that can call the model;
`adopt` derives supported structured cases deterministically. `jaunt check` is fully
deterministic and offline, which makes it the CI gate: it exits `4` on any blocking
drift state (unbuilt / stale-prose / signature-drift / behavior-drift).
Structured docstring prose (call-form `Examples:` like the one above) can often
be derived deterministically too, so `reconcile` doesn't always need the model.
TypeScript workflow [#typescript-workflow]
Adopt a committed export by path and symbol:
```bash
jaunt adopt src/slug.ts#slugify --language ts
jaunt reconcile --language ts
jaunt check --language ts --contracts-only
jaunt status --language ts
jaunt eject src/slug.ts#slugify --language ts
```
`adopt` adds one `@jauntContract` tag to the export's TSDoc and derives its first
battery. The worker projects the declaration without executing the module, and the
battery imports the ordinary committed source. `reconcile` refreshes stale batteries;
`adopt` and `reconcile` can call the test-generation model, while `check`, `status`,
and `eject` do not. `check` typechecks and runs the committed batteries. `eject`
removes the marker and its battery while leaving the implementation untouched.
TypeScript batteries live under `[target.ts].contract_battery_dir` and are ordinary
committed Vitest files. The source declaration and TSDoc are the contract. See the
[TypeScript target guide](/docs/guides/typescript#design-and-contract-mode) for
mutation strength, package-output checks, and magic-module ejection.
Python async functions and fixtures [#python-async-functions-and-fixtures]
Contract mode covers `async def` functions. The derived test functions are
`async def` and `await` the contract function (with `pytest-asyncio` auto
mode). See `examples/contract_async/` in the repo.
Derived cases may declare pytest fixtures in the docstring
(`Fixtures: db`), resolved from `tests/contract/conftest.py`. That's the seam
for contracts that need setup — a database handle, a temp dir, a fake clock —
without giving up deterministic derivation.
Whole classes [#whole-classes]
A class-level Python `@jaunt.contract` pins the class's public behavior. TypeScript
accepts a concrete exported class and derives cases from its declaration and TSDoc;
abstract classes and authored `private` or `protected` members are outside the alpha.
Strength scoring [#strength-scoring]
With `[contract] strength = true` (the default), `jaunt reconcile` runs
mutation-based strength scoring: it estimates how well your pinned examples
actually constrain the implementation. A function with one weak example gets a
low score, and `jaunt status` shows the gap so you know which contracts need more
pinned cases. TypeScript runs supported mutants in disposable overlays and blocks the
transaction when a required mutant survives.
Configuration [#configuration]
```toml
[contract]
battery_dir = "tests/contract" # where derived batteries are written
derive = ["examples", "errors"] # case kinds derived from docstring prose
strength = true # mutation-based strength scoring at reconcile
```
Version-2 workspaces move the language-local battery path under each target. Strength
is shared; Python's `derive` list continues to select its Python case parsers:
```toml
[target.ts]
contract_battery_dir = "tests/contract"
[contract]
strength = true
```
Next: [Background Daemon](/docs/guides/daemon).
---
# Background Daemon
You want spec changes to rebuild themselves in the background while you keep
working, and you want to review the result before it lands. `jaunt daemon` does
that. You (or your coding agent) commit spec changes, and the daemon rebuilds
what went stale in an isolated job. By default it **parks the green result as a
reviewable proposal** — you land it explicitly with `jaunt jobs land`. Opt into
auto-committing green jobs with `[daemon] auto_commit = true`.
Start / Stop / Status [#start--stop--status]
```bash
jaunt daemon start # run the daemon (foreground; Ctrl-C to stop)
jaunt daemon status # is it running? what is it doing? which landing mode?
jaunt daemon stop
```
The daemon polls the repository `HEAD` (every `[daemon] poll_interval` seconds).
When a commit changes specs, it launches an isolated worktree job that rebuilds
the stale modules and runs validation. On green, what happens next depends on the
landing mode (see below). Failed or conflicted jobs are **parked** instead of
landed. `jaunt daemon status` reports the active landing mode
(`landing: propose-only` or `landing: auto-commit`).
Landing modes [#landing-modes]
* **Propose-only (default, `auto_commit = false`).** A green job ends in the
`PROPOSED` state: its diff is captured as a patch artifact under
`.jaunt/jobs/.patch` alongside the recorded spec digest and provenance
commit message. No commit is created — you review and land it explicitly. A
newer proposal for the same module automatically supersedes an older pending
one.
* **Auto-commit (`auto_commit = true`).** A green job flows straight into a
provenance commit with no review step. Jaunt's own repo runs its dogfooding
daemon this way.
Landing a proposal [#landing-a-proposal]
```bash
jaunt jobs land # land one parked proposal as a provenance commit
jaunt jobs land --all # land every fresh proposal, in job-creation order
jaunt jobs discard # drop a proposal you don't want
```
`jaunt jobs land` re-validates before committing: it recomputes the module's spec
digest at `HEAD` and refuses (marking the proposal `SUPERSEDED`) if the spec moved
since generation — the daemon will propose a fresh build. It also refuses if the
current branch differs from the job's branch, if the patch's target paths are
dirty in your working tree, or if the patch no longer applies cleanly (a 3-way
conflict supersedes the proposal). There is no `--force`: regeneration is the
honest resolution for any land-time doubt. On success it creates the same
provenance commit auto-commit mode would have, marks the job `LANDED` with the
commit sha, and appends the journal line.
`jaunt jobs land --all` lands every still-fresh `PROPOSED` job in job-creation
order (the daemon queues jobs in dependency order), prints a per-job outcome, and
exits `0` only when every attempted land succeeded (else `4`).
Jobs [#jobs]
```bash
jaunt jobs # job records + would-rebuild preview + heartbeat
jaunt jobs show --full # one record, with the full local detail log
jaunt jobs retry # retry landing a parked job (--force if the spec moved on)
jaunt jobs land # land a parked proposal as a provenance commit
jaunt jobs land --all # land every fresh proposal (job-creation order)
jaunt jobs discard # discard a parked proposal
jaunt jobs wait --timeout 1800
jaunt jobs wait --timeout 1800 --json --progress plain
```
`jaunt jobs wait` blocks until a target job finishes — or, without an id, until
the daemon is idle. `--settle N` controls the idle race-guard window (default:
2 × `poll_interval`). Exit codes make it loopable: `0` green, `4` a job failed
or parked, `5` timeout. A parked proposal (`PROPOSED`) counts as terminal-green,
so `jaunt jobs wait` returns `0` on it — a proposal is a successful build
awaiting your landing decision.
The canonical agent loop under the default propose-only mode is wait-then-land:
```bash
git commit -m "feat: tighten slugify contract" && \
jaunt jobs wait --timeout 1800 && jaunt jobs land --all
```
With `auto_commit = true` the daemon commits green jobs itself, so the loop is
just the wait:
```bash
git commit -m "feat: tighten slugify contract" && jaunt jobs wait --timeout 1800
```
The Change Journal (jaunt log) [#the-change-journal-jaunt-log]
Build, test, and daemon runs append to a `JAUNT_LOG` journal at the project
root (scaffolded by `jaunt init`). Tail it with:
```bash
jaunt log # last 20 entries
jaunt log -n 0 # everything
jaunt log --module my_app.specs
jaunt log --json
```
Guarding Generated Code (jaunt guard) [#guarding-generated-code-jaunt-guard]
`jaunt guard` is a PreToolUse hook for coding agents: it reads the hook payload
on stdin and warns (with a pointer to the owning spec) when a tool call is
about to touch `__generated__/**`. Wire it into Claude Code via
`.claude/settings.json`:
```json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|MultiEdit|Write|Read|NotebookEdit",
"hooks": [{ "type": "command", "command": "jaunt guard" }]
}
]
}
}
```
For harnesses without hook support, the barrier is advisory:
`jaunt instructions` states the rule.
Configuration [#configuration]
```toml
[daemon]
poll_interval = 2.0 # seconds between HEAD polls
max_jobs = 0 # 0 -> build.jobs
notify_command = "" # optional shell command run on landed/parked/failed jobs
auto_commit = false # false (default): park green jobs as proposals; true: auto-commit
```
`auto_commit` defaults to `false`: green jobs are parked as proposals you land
explicitly. Set it to `true` to auto-commit on green. (If you're coming from an
older setup, see [Upgrading](/docs/reference/upgrading) for the default change.)
`notify_command` is the push-notification seam: point it at a script that pings
Slack, `notify-send`, or your agent loop. It fires on proposed, landed, parked,
and failed jobs.
Build, propose, land, discard, and supersede events are all recorded in the
`JAUNT_LOG` journal, so it stays a complete audit trail whether or not
auto-commit is on. The `job-propose` and `job-discard` lines are appended
uncommitted (union-merge-safe); a landing carries the standard build/refreeze
journal line committed alongside its provenance commit.
Next: [Gating Generated Code in CI](/docs/guides/ci).
---
# Dependencies
You want one spec to call another, and you want a change to the first to rebuild the second. Jaunt handles both from a spec-level dependency graph, which it uses for:
* generation order (dependencies before dependents)
* incremental rebuilds (a dependent goes stale when a dependency's exported API changes)
Explicit Deps (deps=...) [#explicit-deps-deps]
Declare deps explicitly when you want guaranteed ordering:
```python
@jaunt.magic(deps=["my_app.specs:normalize_email", "my_app.other.Helper"])
def is_corporate_email(raw: str) -> bool:
...
```
Accepted `deps=` formats:
* string `"pkg.mod:Qualname"` (canonical)
* string `"pkg.mod.Qualname"` (dot shorthand; last `.` becomes `:`)
* an object (function/class) that Jaunt can convert to `module:qualname`
Inference (Best-Effort) [#inference-best-effort]
Jaunt can also infer edges best-effort. It’s useful, but it’s not magic.
Use explicit `deps=` when:
* the dependency is in another module
* you want deterministic ordering
* inference is missing an edge or creating the wrong one
Base-Class Edges (Structural) [#base-class-edges-structural]
When a whole-class `@jaunt.magic` spec inherits from another spec — `class Child(Base)` where both carry `@jaunt.magic` — the base is an **always-on dependency**. This edge is not inference: it is a structural fact read straight from the class header, so it holds even with `infer_deps` turned off (or `--no-infer-deps`). The base's module always builds before the subclass's.
For a **cross-module** base (base and subclass live in different modules), the subclass's build context includes the base's *generated* public API — method signatures and docstrings read from the built artifact on disk — so the model builds on real inherited methods instead of a stale snapshot. A **same-module** base pair is co-generated as one component: both classes are designed together in a single shot, so there is no separate "base artifact first" step.
A base-class cycle (two specs inheriting from each other across modules) is a real cycle and raises `JauntDependencyCycleError` (exit `2`).
What Happens During Generation [#what-happens-during-generation]
* Dependencies are generated first.
* If a dependency's exported API changes, dependents become stale on the next run.
Dependency API changes include:
* signature changes
* edits anywhere in the cleaned docstring contract
* for whole-class specs, adding/removing/changing declared members or method signatures
Current limitation: dependency context plumbing is intentionally minimal. Ordering and staleness propagation work, but the backend does not currently get rich "here is the generated dependency source" context for dependents.
If a cycle exists, Jaunt raises `JauntDependencyCycleError` and exits with code `2`.
Next: [Spec Writing Tips](/docs/guides/spec-tips).
---
# Package Skills
Your specs import a third-party library and the generated code uses its API in a
way that's subtly wrong or out of date. Skills give Codex version-specific package
context. Python projects can generate skills from PyPI metadata; TypeScript projects
materialize deterministic skills from installed direct npm dependencies.
`jaunt build` runs a best-effort pre-build step that generates a **skill** for
each external library your project imports. Skills are not prompt text: Jaunt
seeds them into the Codex workspace under `.agents/skills/`, where `codex exec`
discovers them natively.
There are three related skill workflows in Jaunt:
* automatic PyPI skill generation during `jaunt build`
* automatic npm skill materialization for TypeScript generation
* explicit user-managed skill authoring through `jaunt skill build `
PyPI generation and explicit user-managed authoring run through the Codex engine.
TypeScript npm skills are rendered locally from installed package metadata and README
text, so preparing them adds no model call.
Python packages [#python-packages]
On every `jaunt build`:
* Scans `paths.source_roots` for Python imports (`import ...` and `from ... import ...`).
* Filters out:
* stdlib modules
* project-internal top-level modules
* relative imports (`from .foo import ...`)
* Resolves the remaining imports to installed **PyPI distributions + versions** (from your current environment).
* Ensures there is exactly one skill per distribution at:
* `/.agents/skills//SKILL.md`
* Fetches the README for the exact `==` from PyPI, then uses the Codex "skills generator" to produce `SKILL.md`.
* Seeds the resulting skills (together with Jaunt's builtin skills) into the Codex workspace at `.agents/skills/`, where the builder discovers and reads them as needed.
If any piece fails (resolution, PyPI fetch, generation, write), Jaunt prints a warning and continues building without those missing skills.
TypeScript packages [#typescript-packages]
Before TypeScript generation, Jaunt reads each owning package's direct runtime
`dependencies`, `peerDependencies`, and `optionalDependencies`. For each installed
package it writes a versioned skill such as
`.agents/skills/npm-zod/SKILL.md`, using only the installed manifest and README.
Transitive packages, lockfile-only entries, and `devDependencies` are ignored.
Only files carrying Jaunt's npm provenance fields are updated or removed. A
user-authored `SKILL.md` at the same path is skipped. Missing packages and filesystem
errors are warnings; they do not fail the build. The generated skill tells Codex to
use public package exports and leaves the owning compiler and tests as the verdict.
Where Skills Are Stored [#where-skills-are-stored]
Skills are stored inside your project:
```text
/
.agents/
skills/
requests/
SKILL.md
pydantic/
SKILL.md
npm-zod/
SKILL.md
```
The directory name is the distribution name normalized like PyPI (lowercased; `-`, `_`, `.` treated consistently).
Safe Overwrites (When Jaunt Regenerates) [#safe-overwrites-when-jaunt-regenerates]
Jaunt will only overwrite a skill file if it was previously generated by Jaunt and the installed version changed.
* Generated files start with a header like:
* ``
* If a file has **no header**, it is treated as user-managed and is never overwritten.
How Skills Reach The Builder [#how-skills-reach-the-builder]
Before each generation call, Jaunt copies the enabled builtin skills plus your
project's `.agents/skills/` directories into the Codex workspace under
`.agents/skills/`. Project skills override builtins of the same name. The build
prompt then simply tells Codex that relevant skills are available there — Codex
reads the ones it needs. Note that skill content is not part of module
freshness: editing a `SKILL.md` does not mark already-built modules stale, so
rebuild with `jaunt build --force` (or change the spec) to pick up skill edits.
Builtin Skills [#builtin-skills]
Independent of PyPI auto-generation, Jaunt ships a curated set of builtin
skills and seeds them by default:
```toml
[skills]
auto = true # prepare package skills for the active target
builtin = true # seed Jaunt's bundled builtin skills
builtin_skills = [ # the default set (override to trim/extend)
"asyncpg", "dbos", "descope", "fastmcp", "openai", "pydantic", "pydantic-ai",
"pytest", "ruff", "spacy", "starlette", "ty", "uv",
]
```
Disable package-specific skills per build with `jaunt build --no-auto-skills`, or
disable bundled skills with `--no-builtin-skills`.
Security Note (Prompt Injection Hygiene) [#security-note-prompt-injection-hygiene]
Package READMEs are treated as **untrusted input**. Python's skill generator is
instructed to extract factual usage rather than follow README instructions. npm skill
text is bounded and rendered into a fixed template; the generated implementation is
still checked by the TypeScript compiler and tests.
Troubleshooting [#troubleshooting]
* Want to see it in action quickly? Try a runnable example:
* `uv run python examples/run_example.py pydantic test`
* "It didn't generate a skill": the import might be stdlib or internal, the dist might not be installed, or the README fetch/generation might have failed. Jaunt should emit a warning but still proceed with the build.
* "It didn't create an npm skill": the package must be an installed direct runtime,
peer, or optional dependency of a configured TypeScript package owner.
* "Wrong version": skills are keyed to what's installed in the environment running `jaunt build`. Update your environment, rerun build, and Jaunt should refresh generated skills.
* "I want to edit a checked-in skill directly": use `jaunt skill build ` on a
user-managed skill directory under `.agents/skills//`.
Next: [Limitations](/docs/reference/limitations).
---
# Spec Writing Tips
You don't "use Jaunt" once. You live in a loop: **write spec → build → review → tighten spec → rebuild**.
The examples below decorate a single symbol with `@jaunt.magic` to keep each one in focus. In practice you will usually turn on a whole file with `jaunt.magic_module(__name__)` and drop the decorators; the tips about docstrings, `prompt=`, and iteration apply the same either way. See [Writing Magic Specs](/docs/guides/writing-magic-specs) for both styles.
Iterating On Specs [#iterating-on-specs]
This loop IS the development process:
1. Write a spec with your best guess at the contract.
2. Run `jaunt build`.
3. Read the generated code. Does it match your intent?
4. If not: tighten the docstring, add edge cases, use `prompt=` for constraints.
5. Re-run `jaunt build`. Jaunt only regenerates what changed.
You'll usually get 80% right on the first pass. The second pass — where you name the edge cases you forgot — is where the spec gets good. Don't try to write the perfect spec upfront. Iterate.
Vague vs. Precise Docstrings [#vague-vs-precise-docstrings]
Bad (vague):
```python
@jaunt.magic()
def parse_user_agent(ua: str) -> dict:
"""Parse a user agent string."""
...
```
Better (contract):
```python
@jaunt.magic()
def parse_user_agent(ua: str) -> dict[str, str]:
"""
Parse a user agent string into a small, stable dict.
Contract:
- Return keys: "browser", "os".
- If unknown, use "unknown" (do not raise).
- Input may be empty or junk; treat as unknown.
"""
...
```
The difference: the vague spec leaves every decision to the LLM. The precise spec names the return shape, the failure mode, and the edge case. Better spec → better code → less iteration.
Use prompt= When The Docstring Isn't Enough [#use-prompt-when-the-docstring-isnt-enough]
If a constraint is non-negotiable, say it twice:
```python
@jaunt.magic(prompt="Use only the standard library. Do not import third-party deps.")
def stable_hash(text: str) -> str:
"""Return a stable hex sha256 of UTF-8 text."""
...
```
Good uses of `prompt=`:
* Force a specific library: `prompt="Use the cryptography library, not hashlib."`
* Architectural constraints: `prompt="Must be async-safe. No global state."`
* Style preferences: `prompt="Prefer explicit loops over list comprehensions for readability."`
Don't Edit Generated Files [#dont-edit-generated-files]
Generated output is disposable. Specs are the product.
If you need a behavior change, change the spec and rebuild so your intent is versioned. Editing `__generated__/` files directly means your changes will be overwritten on the next build.
Whole-Class vs Per-Method @magic [#whole-class-vs-per-method-magic]
Jaunt supports two styles of class specs. Pick whichever matches your situation:
**Whole-class** — decorate the class itself:
```python
@jaunt.magic()
class LRUCache:
"""Fixed-capacity LRU cache. get/set/size methods."""
pass
```
Use this when the class is a self-contained data structure or algorithm and you want the LLM to decide internals (`__init__`, data layout, helper methods).
**Per-method** — decorate individual methods:
```python
class TaskBoard:
def __init__(self) -> None:
self._tasks: list[dict[str, object]] = []
@jaunt.magic()
def add(self, title: str, priority: int) -> dict[str, object]:
"""..."""
...
```
Use this when you want to control the class shape — `__init__`, attributes, inheritance, non-generated helper methods — and only generate specific methods. This is natural for service objects, repositories, and classes that mix custom logic with generated glue.
You cannot combine both styles on the same class.
When Not To Use Jaunt [#when-not-to-use-jaunt]
Jaunt is best for glue code and boring-but-correct utilities (parsers, validators, formatters).
Avoid it (for now) for:
* performance-critical hot paths
* complex stateful systems with tight invariants
* code that depends on exact library versions or fragile runtime behavior
Next: [Contract Mode](/docs/guides/contract-mode).
---
# TypeScript target
The TypeScript target is available behind `version = 2` configuration. It keeps
Jaunt's Python CLI and Codex scheduler, but delegates TypeScript parsing and
typechecking to the project-local `@usejaunt/ts` worker. Discovery reads source and
`tsconfig.json`; it does not execute application modules, specs, Vite config, or
package scripts.
Install [#install]
Start a new project with:
```bash
uvx jaunt init --language ts
npm init -y && npm pkg set type=module
npm install -D @usejaunt/ts@next 'typescript@^5.9' vitest fast-check @types/node
```
`jaunt init` does not edit `package.json`. Its human and JSON output includes the
package command you still need to run. For an existing manifest with no `type`, that
command is `npm pkg set type=module`. An explicit `"type": "commonjs"` is preserved;
the generated NodeNext config lowers the scaffold's import/export syntax to CommonJS.
The worker and protected test runner support Node `>=20 <25`. That is a Jaunt
tool-host constraint, not a requirement on the runtime that executes your generated
JavaScript.
For an existing project, add this target to `jaunt.toml`:
```toml
version = 2
[target.ts]
source_roots = ["src"]
test_roots = ["tests"]
projects = ["tsconfig.json"]
test_projects = ["tsconfig.test.json"]
tool_owner = "."
generated_dir = "__generated__"
test_runner = "vitest"
fast_check_runs = 50
[codex]
model = "gpt-5.6-sol"
reasoning_effort = "medium"
sandbox = "workspace-write"
```
If the project already has a version-1 Python config, preview the mechanical rewrite
first, then apply it:
```bash
jaunt migrate --config-v2
jaunt migrate --config-v2 --apply
```
The migration moves Python-only keys under `[target.py]`, preserves the loaded Python
configuration exactly, and makes no model call.
When upgrading an existing TypeScript alpha project, preview its artifact migration
separately:
```bash
jaunt migrate --language ts
jaunt migrate --language ts --apply
```
In a TypeScript-only version-2 project, `jaunt migrate` selects this path without the
language flag. The command asks the project-local worker for current routes and
validated artifact bytes, but never calls Codex. It can repair mirrors, canonical
facades, placeholders, and sidecars, or freely re-stamp compatible fingerprint drift.
Contract changes and incompatible alpha protocol, IR, or route records are listed as
`model-rebuild` with the exact module to rebuild.
Migration is plan-only unless `--apply` is present. The apply checks every analyzed
input again, refuses a dirty worktree unless you pass `--force`, and rolls back the
whole artifact manifest if a replacement fails. Older preview layouts that used an
ambiguous `spec.jaunt.ts`, `__generated__/impl.ts`, or loader hook are reported for
manual intervention; Jaunt does not guess the intended public facade or rename private
specs.
`tool_owner` is the package that directly declares both `@usejaunt/ts` and
`typescript`. Jaunt does not use a global compiler and does not download one during a
command.
The first alpha supports TypeScript 5.8 through 6.x. [TypeScript 7.0 does not ship a
stable programmatic compiler API](https://devblogs.microsoft.com/typescript/announcing-typescript-7-0/#running-side-by-side-with-typescript-60),
so the worker rejects it with install guidance rather than analyzing the project under
different compiler semantics. TypeScript 7 support will follow its stable tooling API.
Author a module [#author-a-module]
A private spec uses the `.jaunt.ts` suffix:
```ts title="src/slug/index.jaunt.ts"
import * as jaunt from "@usejaunt/ts/spec";
jaunt.magicModule();
/** Lowercase a title and join non-empty ASCII word runs with hyphens. */
export function slugify(title: string): string {
return jaunt.magic();
}
```
Consumers import an ordinary committed facade:
```ts title="src/slug/index.ts"
export * from "./__generated__/index.js";
```
When the spec exports standalone interfaces or type aliases, the generated facade
also re-exports those names explicitly from `index.api.js`. It does not use
`export type *`, because a class has both a type and a runtime value and star exports
would create ambiguous collisions.
The full layout is:
```text
src/slug/
index.jaunt.ts private contract input; never emitted or executed
index.context.ts optional handwritten runtime leaf
index.ts public facade
__generated__/
index.api.ts deterministic declaration mirror
index.ts generated implementation
index.jaunt.json committed freshness record
```
Source imports use the owning project's runtime convention. NodeNext projects use
`.js` specifiers; TypeScript resolves them to source `.ts` files and emitted JavaScript
keeps valid runtime paths.
The checked matrix covers NodeNext ESM, NodeNext CommonJS, Bundler/Vite-style
resolution, and governed `.tsx`. The marker package exposes separate ESM and CommonJS
declaration conditions, so a CommonJS spec does not accidentally type against an ESM
entry. JSX uses the owning project's normal factory/runtime and global declarations;
Jaunt does not add a framework runtime.
Sync before the first build [#sync-before-the-first-build]
```bash
uvx jaunt sync --language ts
uvx jaunt status --language ts
```
`sync` is deterministic and does not call Codex or the semantic gate. It writes the
API mirror, creates a canonical facade only when none exists, and supplies an exact
typed throwing placeholder for a missing implementation. The editor can then typecheck
the module, while `status` and `check` still report it as unbuilt.
Build, test, and check [#build-test-and-check]
```bash
uvx jaunt build --language ts
uvx jaunt test --language ts
uvx jaunt check --language ts
```
The model writes reserved internal bindings. The worker rejects model-authored
exports, `any`, TypeScript suppressions, and unsafe boundary casts, then composes the
public exports itself. Candidate files are typechecked in an in-memory overlay. Jaunt
writes the implementation, API mirror, sidecar, and any new facade only after the
complete validation succeeds.
Authored test intent lives in `*.jaunt-test.ts`. Generated batteries use Vitest and
import the public facade. Example-tier failures retain normal detail. Derived-tier
repair feedback contains only an opaque case ID and a normalized failure category.
Every package that owns a configured test project must declare `vitest` directly in
`devDependencies`; add `fast-check` there too when that package uses `@prop`. A
hoisted installation alone is not authorization, and Jaunt checks the supported peer
version before spending a model call.
Property intent uses one deterministic line:
```ts
/** @prop given bytes: fc.uint8Array() :: decode(encode(bytes)) equals bytes */
```
The grammar accepts `equals` and `does not equal`. The input may be `string`,
`number`, `boolean`, `bigint`, `Uint8Array`, a supported array type, or a
compositional `fc` expression such as `fc.integer({ min: 0, max: 100 })`,
`fc.tuple(...)`, or `fc.record(...)`. Strategy expressions are restricted to
fast-check calls and data literals, then typechecked by the owning test project.
Jaunt writes the predicate, semantic case ID, seed, and run count itself. Malformed
prose, `any`, unknown invariant identifiers, and executable strategy escapes fail
before generation. An async invariant must write `await` explicitly, uses
`fc.asyncProperty`, and may consume typed fixtures normally.
Design and contract mode [#design-and-contract-mode]
`@jauntDesign` asks Codex for a declaration patch before implementation. Review the
dry-run diff, then apply the same source-digest-guarded proposal:
```bash
jaunt design --target ts:src/store/index#TokenStore
jaunt design --target ts:src/store/index#TokenStore --apply
```
The second command applies the recorded patch exactly; it does not call Codex again.
Existing committed TypeScript can enter contract mode without moving behind a
generated facade:
```bash
jaunt adopt src/tokens/b64url.ts#encode --language ts
jaunt reconcile --language ts
jaunt check --language ts --contracts-only
jaunt eject src/tokens/b64url.ts#encode --language ts
```
Reconcile derives committed Vitest batteries from the declaration and TSDoc. With
`[contract].strength = true`, disposable mutation runs must show that the battery
kills supported behavioral mutants; surviving mutants block the transaction. Magic
eject turns a fresh generated module into ordinary TypeScript, retargets its tests,
and validates normal JavaScript emit, declaration emit, tests, package-output safety,
and runtime independence before removing private Jaunt artifacts.
Context and import rules [#context-and-import-rules]
`index.context.ts` is an optional runtime dependency for generated code. It is a leaf:
it cannot value-import its own facade, generated implementation, or a private spec.
Handwritten code that calls the generated API belongs in another module that imports
the facade.
Production code cannot import `*.jaunt.ts` or `*.jaunt-test.ts`, even with a loader.
Specs must be excluded from emitting production and test projects. Jaunt checks this
before any model call.
Concrete classes and preserved methods [#concrete-classes-and-preserved-methods]
Class contracts may declare constructors, overloads, explicit `this` parameters,
generic constraints, methods, accessors, static/readonly fields, optional members,
and concrete inheritance. Jaunt validates each boundary through strict synthetic
adapters instead of relying on TypeScript's bivariant class assignment.
Use `@jauntPreserve` in a method or accessor's TSDoc when its handwritten body must be
copied into the generated class:
```ts
import * as jaunt from "@usejaunt/ts/spec";
import { normalize } from "./formatter.context.js";
jaunt.magicModule();
export class Formatter {
constructor() {
jaunt.magic();
}
/** Normalize one value. @jauntPreserve */
format(value: string): string {
return normalize(value);
}
}
```
Preserved code may use parameters, `this`, local bindings, standard globals, and
runtime imports from the paired context module. It cannot pull arbitrary runtime
dependencies or private spec modules into generated code.
Mixed Python and TypeScript workspaces [#mixed-python-and-typescript-workspaces]
A version-2 root may contain both `[target.py]` and `[target.ts]`. Commands operate on
both by default, and `--language py|ts` narrows a run. TypeScript IDs use the form
`ts:src/slug/index#slugify`; unprefixed dotted IDs keep their Python meaning.
Version-1 Python projects keep their existing output shape. Version-2 JSON adds a
`targets.py` / `targets.ts` partition and qualifies top-level IDs.
Project references and package workspaces [#project-references-and-package-workspaces]
Point `projects` at a solution `tsconfig.json` when the workspace uses project
references. Jaunt loads the full reference DAG, keeps solution-only configs as graph
nodes, and assigns each spec to one unambiguous production project. Candidate
validation runs against the owner and every affected downstream project before any
file is replaced.
Cross-project `deps` should use the same package or path alias the authored spec uses.
Jaunt removes the private `.jaunt` segment for generated runtime imports instead of
reaching across another project's `rootDir` with a relative source path. Every config
in one reference graph must resolve the same project-local TypeScript compiler.
Dependency provenance follows the nearest `package.json`. Production imports must be
declared by that package; `devDependencies` are accepted only in configured test
roots. npm and pnpm layouts are supported, including pnpm's workspace-visible
compiler symlink into its external store. Keep production `compilerOptions.types`
explicit (the starter uses `["node"]`) so a package manager's incidental hoisting of
transitive `@types` packages cannot change the ambient compilation contract.
Security boundary [#security-boundary]
Static discovery never evaluates application code or executable config. The installed
worker and compiler do execute, and `jaunt test` executes trusted project tests in a
disposable child process. Jaunt is a build tool, not a sandbox for generated code or
tests.
Fresh generated batteries are still typechecked and run, but they do not call the
model again. A runner- or Vitest-only fingerprint change reheaders the unchanged
battery after overlay validation and reports it as `refrozen`; contract, API, prompt,
property, or body drift regenerates it. `jaunt test --force` always regenerates.
Current alpha limits [#current-alpha-limits]
The alpha supports referenced project graphs, cross-spec dependencies, concrete class
inheritance, and `@jauntPreserve`. Put a preserve tag on the single concrete
implementation of a non-overloaded method or accessor. Its runtime closure is limited
to parameters, `this`, local bindings, standard globals, and imports from the paired
context module.
The analyzer still rejects `.mts`, `.cts`, JavaScript specs, abstract governed
classes, authored `private`/`protected` members, parameter properties, computed member
names, mixin or `implements` heritage, and preserve tags on overload groups. These
cases fail during discovery instead of receiving a weaker conformance check.
The repository JWT example under `examples/typescript-jwt` is generated by this worker.
It covers classes, contract batteries, held-out tests, mutation strength, and a packed
consumer. The smaller slugify and project-reference fixtures keep the basic authoring
and workspace-routing paths independently testable.
`jaunt watch` follows TypeScript source, project, package, and lockfile changes while
excluding generated output. Daemon jobs use qualified `ts:` artifact keys and park
only the exact implementation, API mirror, sidecar, and newly created canonical
facade paths returned by the validated build. Landing rechecks the target before it
commits a proposal; mixed workspaces keep Python and TypeScript job identities apart.
Runnable examples:
* [Single-project slugify](https://github.com/creatorrr/jaunt/tree/main/examples/typescript_slugify)
* [npm/pnpm project references](https://github.com/creatorrr/jaunt/tree/main/examples/typescript_project_references)
* [JWT, classes, contracts, and ejection](https://github.com/creatorrr/jaunt/tree/main/examples/typescript-jwt)
The complete design and staged compatibility matrix are in
[`docs/ts-port/IMPLEMENTATION_PLAN.md`](https://github.com/creatorrr/jaunt/blob/main/docs/ts-port/IMPLEMENTATION_PLAN.md).
---
# Writing Magic Specs
You can describe what a function should do far more precisely than you want to hand-write it. That is the case magic specs are for: you write the signature and the docstring, Jaunt generates the implementation into `__generated__/`, and at runtime your calls forward there.
There are two ways to mark a spec. Turning on a whole module is the primary one; the `@jaunt.magic` decorator is the precision layer for a single symbol.
Turn on a whole module [#turn-on-a-whole-module]
Call `jaunt.magic_module(__name__)` at the top of a file and every top-level stub below it becomes a spec. No per-function decorator, no repetition.
```python
import re
import jaunt
jaunt.magic_module(__name__, prompt="All parsers are RFC 5322 strict.")
ADDR_RE = re.compile(r"[^@\s]+@[^@\s]+") # handwritten constant — untouched
class Email:
"""An email message with from_, to, subject, and body string fields.
Validate on construction: from_ and to must each look like an address.
"""
def parse_email(raw: str) -> Email:
"""Parse a raw RFC 5322 payload into an Email. Raise ValueError on
malformed input."""
...
def _render_debug(email: Email) -> str: # real body → handwritten helper
return f"<{email.from_} -> {email.to}>"
```
Jaunt generates `Email` and `parse_email`. It never touches `ADDR_RE` or `_render_debug` — those have real bodies, so they are context the generator can read, not code it regenerates.
What counts as a spec [#what-counts-as-a-spec]
Jaunt scans the top-level `def`, `async def`, and `class` statements and classifies each one:
| Shape | Classification |
| ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| Function whose body is a stub: docstring-only, `...`, `pass`, or `raise NotImplementedError` | **spec** |
| Class with a docstring-only body, or with at least one stub method | **whole-class spec** — members keep the three-tier semantics below |
| Function with a real body, or a class whose methods are all real | **handwritten** — read as context, never regenerated |
| Carries a jaunt decorator (`@magic`, `@preserve`, `@sig`, `@test`, `@contract`) | **skipped by the scan** — the decorator governs it |
| Carries a non-jaunt decorator (`@property`, `@functools.cache`, `@typing.overload`, `@dataclass`, …) | **never governed** — handwritten |
Two rules keep the scan from surprising you. A jaunt decorator always wins, so a `@jaunt.magic()` or `@jaunt.preserve` symbol in a governed module is handled by its decorator, not the scan (this recognizes aliased imports like `from jaunt import magic as m`). And any non-jaunt decorator opts a symbol out entirely — an `@typing.overload` stub or a `@property` is left alone, because those ellipsis bodies are not specs. To generate a symbol that carries another decorator, mark it explicitly with `@jaunt.magic`.
Only direct top-level statements are scanned. Constants, `if TYPE_CHECKING:` blocks, and imports are ignored, and a `def` nested inside an `if`/`try` branch or another function is out of scope — neither governed nor guaranteed as context.
Stub bodies [#stub-bodies]
A stub body is a placeholder Jaunt never runs. Reach for a bare `...`:
```python
def parse_email(raw: str) -> Email:
"""..."""
...
```
`pass`, a bare docstring, and `raise NotImplementedError` read as stubs too, and all four forms digest identically — switching between them never restales a module. The older `raise RuntimeError("spec stub")` convention does **not** classify — in a governed module that body looks like real code, so the function is handwritten.
A spec body never runs — Jaunt replaces it, and an unbuilt spec raises a clear error the first time you call it, so `...` carries no runtime risk. Some type checkers (ty, Pyright) flag `...` under a concrete return annotation outside `.pyi` files. Two remedies work equally well: relax that rule on your spec roots, or write `raise NotImplementedError` in place of `...` (a raise never returns, so it satisfies the checker). Since the forms digest identically, switching between them never restales. Jaunt does not scaffold type-checker config — the choice is yours.
An intentionally empty function is the one case where you want an empty body left alone. Mark it `@jaunt.preserve` and the scan skips it:
```python
@jaunt.preserve
def on_startup() -> None:
"""Deliberately does nothing yet."""
...
```
The capture rule [#the-capture-rule]
`magic_module` rebinds each spec name to its generated object the first time something outside the module reads it. Code at module level that runs *before* that — a top-level call, a class that subclasses a governed spec, an assignment that instantiates one — captures the pre-rebind stub and keeps it. So **module-level consumption of a spec belongs inside a function**, where it runs after the module has finished importing.
Jaunt warns you when the scan sees the trap:
```text
mail: module-level code calls governed spec 'parse_email' before rebinding;
it will see the pre-rebind stub. Move the call into a function.
```
```text
mail: class 'Signed' subclasses governed spec 'Email' at module level; it will
see the pre-rebind stub. Move the subclass into a function or mark 'Email' with
an explicit @jaunt.magic.
```
The escape hatch for a base class is an explicit `@jaunt.magic` on it: the decorator substitutes the generated class at decoration time, so a subclass declared right below it binds to the real thing.
The docstring is the contract [#the-docstring-is-the-contract]
However you mark a spec, its contract lives in three places:
* **Signature.** Type hints are leverage. If your spec returns `str`, say `str`, not `Any`.
* **Docstring.** This is the specification. Write it like a sharp ticket: what the function must do, which inputs fail and how (a `ValueError`? a `None`? a silent fallback?), the exact return shape, and a concrete example when the behavior is subtle. Jaunt treats the whole cleaned docstring as contract text, not just the summary line.
* **Body.** A `...` placeholder. Never run.
Think: if you handed this docstring to a careful developer, would they implement it the same way every time? If not, add constraints.
Module defaults [#module-defaults]
The kwargs on `magic_module` are defaults for every spec in the file. They match the decorator's: `prompt=`, `deps=`, `infer_deps=`, and `test=`.
```python
jaunt.magic_module(__name__, prompt="Prefer the standard library. No third-party deps.")
```
That `prompt=` reaches both scan-governed specs and any decorated symbol in the same file. A per-symbol decorator wins key by key — `@jaunt.magic(prompt="…")` on one function overrides the module `prompt=` for that function, while a `@jaunt.magic(deps=[…])` symbol still inherits the module `prompt=`. Editing a module default changes the contract of every governed spec, so it restales them on the next build, exactly as a docstring edit would.
The precision layer: @jaunt.magic [#the-precision-layer-jauntmagic]
Decorate a single function or class with `@jaunt.magic` when you want to govern just that symbol, or pass options that differ from the module. It works with or without a `magic_module` call, so it is also how you adopt one function in a file you do not want to govern wholesale.
```python
@jaunt.magic(prompt="Use the cryptography library, not hashlib.")
def stable_hash(text: str) -> str:
"""Return a stable hex sha256 of UTF-8 text."""
...
```
The decorator options:
* `deps=...`: explicit dependencies (objects or `"pkg.mod:Qualname"` strings).
* `prompt="..."`: extra per-symbol context appended to what the model sees.
* `infer_deps=True|False`: per-spec override of dependency inference.
Dependencies [#dependencies]
If a spec depends on another, declare it. Jaunt gets a stable build order and correct staleness: change the upstream spec's public shape and the dependent restales.
```python
@jaunt.magic(deps=[normalize_email])
def is_corporate_email(raw: str, *, domain: str = "example.com") -> bool:
"""
Return True iff `normalize_email(raw)` belongs to `domain`.
- If normalize_email raises ValueError, propagate it unchanged.
- Comparison should be case-insensitive.
"""
...
```
See [Dependencies](/docs/guides/dependencies).
Classes [#classes]
A class spec — whether governed by `magic_module` or decorated — can be docstring-only, in which case Jaunt designs the whole surface:
```python
@jaunt.magic()
class LRUCache:
"""
A small fixed-capacity LRU cache.
- get(key): return value or None, and mark as most-recently used
- put(key, value): insert/update and evict least-recently used if needed
"""
```
This hands full control to the model: it decides `__init__`, the internal data structures, and the method signatures.
Method tiers in a whole-class spec [#method-tiers-in-a-whole-class-spec]
Inside a whole-class spec you steer how much freedom the generator has per method. Every method falls into exactly one tier:
| Tier | Marker | Body | The generator may |
| ------------- | -------------------------------------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------- |
| **Preserved** | `@jaunt.preserve` | hand-written | nothing — the method is emitted verbatim |
| **Sealed** | `@jaunt.sig` (or inner `@jaunt.magic`) | stub | implement the body only; the signature is enforced **exactly** |
| **Guidepost** | none (default) | stub | adapt the signature, rename or add parameters, split into several methods, or add public methods — the docstring is the contract |
```python
@jaunt.magic()
class TokenBucket:
"""A token-bucket rate limiter. Design the storage and refill internals."""
@jaunt.sig
def allow(self, cost: int = 1) -> bool: ... # sealed: signature is the contract
def describe(self) -> str: ... # guidepost: adapt freely
@jaunt.preserve
def reset(self) -> None: # preserved: kept verbatim
self._tokens = self.capacity
```
Sealed-signature drift is a hard build error; guidepost drift is only a warning. `@jaunt.sig` (and the inner `@jaunt.magic` alias) takes no kwargs and cannot sit under `@property` (v1) — use a guidepost stub or `@jaunt.preserve` instead.
The exported API of a whole-class spec is the class signature plus its declared members and method signatures. Adding, removing, or changing those is a downstream contract change. Custom metaclasses are not supported for magic classes.
Method-level specs [#method-level-specs]
Instead of governing the whole class, decorate **individual methods**. You keep the class shape — `__init__`, attributes, inheritance — and generate only the methods you mark.
```python
class TaskBoard:
"""A simple in-memory task board."""
def __init__(self) -> None:
self._tasks: list[dict[str, object]] = []
@jaunt.magic()
def add(self, title: str, priority: int) -> dict[str, object]:
"""
Add a task and return it.
- Assign an auto-incrementing integer id (starting at 1).
- priority must be 1-5 (raise ValueError otherwise).
- Return {"id": int, "title": str, "priority": int}.
"""
...
```
Non-decorated members are preserved as-is; only the `@magic`-decorated methods are generated.
@classmethod and @staticmethod [#classmethod-and-staticmethod]
Both work. The rule: **`@magic()` must be the innermost decorator**, closest to `def`.
```python
class TaskBoard:
@classmethod
@jaunt.magic()
def from_dict(cls, data: dict[str, object]) -> TaskBoard:
"""
Reconstruct a TaskBoard from {"tasks": [{"id", "title", "priority"}, ...]}.
Raise ValueError if `data` is missing the "tasks" key.
"""
...
```
Whole-class vs method-level [#whole-class-vs-method-level]
You cannot mix whole-class `@magic` with standalone method-level `@magic` on the *same* class. Choose one: whole-class when you want the model to decide internals, method-level when you own the structure and generate only some methods. (A `@jaunt.magic` method *inside* a whole-class spec is not the method-level style — it is the [sealed tier](#method-tiers-in-a-whole-class-spec), and it is fully supported.)
Async functions [#async-functions]
Async stubs work the same way. Jaunt returns an async wrapper so call sites still `await`, and the generated implementation is `async def`.
```python
@jaunt.magic()
async def fetch_user_profile(user_id: str) -> dict[str, object]:
"""
Return a normalized user profile by id. Raise ValueError if `user_id` is empty.
"""
...
```
Cross-referencing siblings: inline the shared rule [#cross-referencing-siblings-inline-the-shared-rule]
A spec is generated with its own source and its declared dependencies in
context — not the docstrings of its neighbors in the same file. So if one
spec's docstring leans on another's ("does step 1 of `parse_temporal_reference`
first"), the model never sees that referenced text and has to guess. Two fixes:
declare the neighbor as a dep so its generated API comes along, or, for a rule
that spans several specs, lift it into the module default:
```python
jaunt.magic_module(
__name__,
prompt="All timestamps are UTC. Reject naive datetimes with ValueError.",
)
```
The `prompt=` reaches every governed spec in the file, so a rule that would
otherwise live in one docstring and be invisible to the rest now applies
everywhere. This is the pattern to prefer over cross-referencing prose.
Workspaces: one config, several packages [#workspaces-one-config-several-packages]
A root config can cover flat packages, `src` packages, or both:
```toml
[paths]
source_roots = ["packages/*/src", "apps/*"]
test_roots = ["packages/*/tests", "apps/*/tests"]
```
Jaunt derives module names relative to each concrete import root. Overlapping
roots use longest containment, so `source_roots = [".", "src"]` still names a
file under `src/pkg/` as `pkg.*`. Two files that derive the same dotted module
name are rejected before either file is imported.
The nearest `pyproject.toml` owns each module. Generated code and `.pyi` files
stay beside that package, ty runs from the owner directory, and dependency
validation reads only the owner's declared dependencies. Tests and contract
batteries also run per owner, which keeps package-local pytest settings and
identical `tests.*` names separate.
Existing split configs can be consolidated without regeneration:
```bash
jaunt migrate --merge-projects
jaunt migrate --merge-projects --apply
```
The dry run compares policy, routes, fingerprints, and artifact locations. The
apply step changes only the root `[paths]` table and removes redundant child
configs. It refuses any merge that would rename a module, move an artifact, or
adopt stale output.
If the output is garbage [#if-the-output-is-garbage]
Don't fix the generated file. Fix the spec.
1. Make the docstring more specific.
2. Name the edge cases and the exact error behavior.
3. Use `prompt=` for any crucial constraint.
4. Re-run `jaunt build` — only stale modules regenerate.
Next: [@jaunt.test Specs](/docs/guides/writing-test-specs).
---
# @jaunt.test Specs
You want pytest coverage for a magic spec, but you'd rather state what the tests should check than write the assertions by hand. `@jaunt.test` specs let you do that. They are not tests themselves; they are descriptions of tests. Jaunt generates the real pytest files under `tests/__generated__/`.
Rules [#rules]
* Test specs must be **top-level** functions.
* Name them like pytest tests (`test_*`) so the generated tests are collected.
* The stub itself is marked `__test__ = False`, so pytest won't collect the stub.
Annotated Example [#annotated-example]
```python
from __future__ import annotations
import jaunt
@jaunt.test()
def test_normalize_email__lowercases_and_strips() -> None:
"""
Assert normalize_email:
- strips surrounding whitespace
- lowercases
- rejects invalid inputs like "no-at-sign" (ValueError)
Concrete assertions (write these out, don't imply them):
- normalize_email(" A@B.COM ") == "a@b.com"
"""
raise AssertionError("spec stub (generated at test time)")
```
Tips That Make Generated Tests Better [#tips-that-make-generated-tests-better]
* **Be explicit about assertions**: write `X == Y` in the docstring.
* **Name edge cases**: empty strings, invalid formats, boundary values.
* **Show the import path**: mention it in the docstring or stub body. The generator does better when it sees where the thing under test lives.
Test Tiers And Held-Out Feedback [#test-tiers-and-held-out-feedback]
Generated tests are tagged with a tier marker automatically — you don't write it yourself:
* `@pytest.mark.jaunt_tier("example")` — a case anchored to a **canonical example** in the docstring (shared, public).
* `@pytest.mark.jaunt_tier("derived")` — a case the generator **derived itself** (adversarial, edge cases). Held out.
This is how Jaunt keeps the implementer and the tester independent. When a generated test fails and Jaunt regenerates the implementation, the implementation generator sees *derived* failures only as an opaque id plus an exception class — never the expected value — so it can't be written to the held-out answer key. An oracle the implementer can see is one it games. *Example* failures keep full detail, since they are part of the shared contract.
For your specs, that means: put your **canonical, behavior-defining assertions in the docstring as examples**. They ground both the implementation and the tests. Trust the generator to derive the adversarial cases. Derived cases get **opaque names** (`test_derived_01`) on purpose — even a descriptive name like `test_empty_list_returns_zero` leaks intent.
`jaunt test --no-redact-derived` turns off the redaction and gives full detail for all tiers. It is a debugging escape hatch: it defeats the barrier and logs a warning.
Async Test Specs [#async-test-specs]
`@jaunt.test()` also accepts `async def` stubs.
Jaunt uses `build.async_runner` to choose the generated marker:
* `asyncio` -> `@pytest.mark.asyncio`
* `anyio` -> `@pytest.mark.anyio`
```python
from __future__ import annotations
import jaunt
@jaunt.test()
async def test_fetch_user_profile__returns_payload() -> None:
"""
Assert fetch_user_profile returns a normalized payload.
Concrete assertions:
- "abc" is the user id in the returned payload
"""
raise AssertionError("spec stub (generated at test time)")
```
Running [#running]
```bash
jaunt test # build if needed, generate tests, run pytest
jaunt test --no-run # generate the tests without running them
```
See: [Output Locations](/docs/reference/output-layout).
Next: [Dependencies](/docs/guides/dependencies).
---
# CLI
The `jaunt` entry point is `jaunt.cli:main`. Commands are grouped below by the
job they do: authoring specs, building, testing, contract mode, the daemon, and
project maintenance. New to Jaunt? Start with [Quickstart](/docs/tutorials/quickstart)
instead.
Every generation command runs through the Codex engine and needs the `codex`
CLI installed and authenticated (`codex login`). See [Codex Engine](/docs/concepts/codex-engine).
Common flags [#common-flags]
These apply to every generation command (`build`, `test`, `status`, `check`,
`watch`, `tree`, `specs`, `adopt`, `reconcile`, `eject`). Per-command sections
below list only the flags unique to that command.
| Flag | Effect |
| ----------------------------------- | ------------------------------------------------------------------------------------------- |
| `--root PATH` | Project root. Default: search upward from cwd for `jaunt.toml`. |
| `--config PATH` | Path to `jaunt.toml`. Default: `/jaunt.toml`. |
| `--jobs N` | Concurrency override. |
| `--target MODULE[:QUALNAME]` | Restrict work to one or more modules (repeatable; filtering is module-level). |
| `--language {py,ts}` | Restrict a version-2 workspace to one language. TypeScript targets use `ts:path#symbol`. |
| `--no-infer-deps` | Disable AST dependency inference; explicit `deps=` still applies. |
| `--progress {auto,rich,plain,none}` | Progress mode. `auto` is rich on a TTY, plain lines off it. |
| `--no-progress` | Disable progress output. |
| `--no-cache` | Bypass the LLM response cache. |
| `--json` | Emit a machine-readable envelope on stdout. See [JSON Output](/docs/reference/json-output). |
Authoring [#authoring]
jaunt init [#jaunt-init]
Scaffold a `jaunt.toml`, source/test directories, and a starter spec. The TypeScript
starter intentionally leaves package metadata to the package manager; when no
`package.json` exists, follow its printed `npm init` command before installing tools.
```bash
jaunt init
jaunt init --root /tmp/myproj
jaunt init --force
jaunt init --language ts
```
| Flag | Effect |
| -------------------- | --------------------------------------------------- |
| `--root PATH` | Directory to create `jaunt.toml` in (default: cwd). |
| `--force` | Overwrite an existing `jaunt.toml`. |
| `--language {py,ts}` | Choose the starter target (default: `py`). |
| `--json` | Machine-readable output. |
`init` takes no other common flags.
jaunt specs [#jaunt-specs]
List magic specs and the dependency graph between them.
```bash
jaunt specs
jaunt specs --module my_app.specs
```
Specs registered by a `magic_module(__name__)` call are marked `[module]`;
decorated specs carry no marker. When a spec's effective kwargs are non-empty
(a per-symbol option, or a module default it inherited), they print after the
marker:
```text
- mail:parse_email (mail.py) [module] kwargs={'prompt': 'All parsers are RFC 5322 strict.'}
```
| Flag | Effect |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--module MODULE` | Restrict output to one module. |
| `--no-infer-deps` | Explicit deps only. |
| `--json` | Emits `specs` and `dependency_graph`. Each spec carries `"origin"` (`"module"` or `"decorator"`), `"kwargs"` (the merged effective kwargs), and `"newly_governed"` (a module-origin spec with no artifact yet). |
Module-origin specs that have never been built print with a
`[newly governed — first build]` marker, so the first build a `magic_module`
scan commissions is never a surprise.
For a TypeScript target, `specs` is static: the Node worker parses private
`*.jaunt.ts[x]` inputs without executing them and reports `ts:` module and symbol
IDs.
jaunt instructions [#jaunt-instructions]
Print a project-aware agent primer: the authoring modes, the build/test loop,
the command and exit-code reference, then a live snapshot of the current project
(resolved `[paths]`, engine/model, semantic-gate and repo-map settings, which
modules are stale). Run before an initialized project exists, it prints the
framework rules plus the full `jaunt.toml` schema and a note to run `jaunt init`.
```bash
jaunt instructions
jaunt instructions --json
```
`--json` emits `{command, ok, text, project}`, where `project` is the structured
snapshot (or `null` with no `jaunt.toml`).
`jaunt instructions` is the single source of truth for the agent workflow. The
bundled Claude Code / Codex skills are thin stubs that point here, so they
cannot drift from the installed CLI.
Building [#building]
jaunt migrate --config-v2 [#jaunt-migrate---config-v2]
Preview or apply the deterministic version-1 to version-2 configuration rewrite:
```bash
jaunt migrate --config-v2
jaunt migrate --config-v2 --apply
```
The action moves Python-specific settings under `[target.py]`, verifies that the
loaded Python compatibility view is unchanged, and makes no model call. It is separate
from `--merge-projects`.
jaunt sync [#jaunt-sync]
Render TypeScript API mirrors, missing canonical facades, and typed unbuilt
placeholders without calling a model:
```bash
jaunt sync
jaunt sync --target ts:src/tokens/index
```
The owner project must typecheck before the artifacts are committed. Existing custom
facades are never overwritten. Sync may refresh the deterministic API mirror and its
sidecar hash bookkeeping, but it never rewrites a built implementation's body or
advances its provenance to a changed contract. Semantic and fingerprint re-stamps
belong to `build`; compatible alpha artifact repairs belong to `migrate --language
ts`. A synchronized placeholder keeps the editor green but remains `unbuilt` in
`status` and `check`.
jaunt design [#jaunt-design]
Ask Codex to propose the declaration and TSDoc for one `@jauntDesign` contract:
```bash
jaunt design --target ts:src/store/index#TokenStore
jaunt design --target ts:src/store/index#TokenStore --apply
```
The default prints a patch, leaves the spec unchanged, and records the exact proposal
under `.jaunt/`. `--apply` requires that proposal, makes no model call, checks the
source digest and dirty-tree policy, confines the patch to the marked declaration,
and validates it before writing.
jaunt build [#jaunt-build]
Generate implementation modules for `@jaunt.magic` specs. Stale dependents of a
changed upstream API are rebuilt too.
```bash
jaunt build
jaunt build --force
jaunt build --target my_app.specs
jaunt build --jobs 16
jaunt build --language ts --target ts:src/tokens/index
```
| Flag | Effect |
| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
| `--force` | Regenerate everything, ignoring digests. |
| `--instruction "..."` | Extra build instruction appended to the prompt (repeatable). |
| `--include-target-tests` / `--no-include-target-tests` | Include targeted test-spec source in build prompts (default from `[build] include_target_tests`). |
| `--no-auto-skills` | Skip automatic PyPI or npm package skills this run. |
| `--no-builtin-skills` | Don't seed the bundled builtin skills into the Codex workspace. |
| `--no-repo-map` | Disable repo-map injection this run. See [Repo Context](/docs/concepts/repo-context). |
| `--no-semantic-gate` | Rebuild on every real change; skip the Layer B gate (Layer A still applies). See [Change Detection](/docs/concepts/change-detection). |
`--json` reports `generated`, `skipped`, `refrozen`, `failed`, `cost`, and
`context_stats`, plus `needs_deps`, `emitted_stubs`, and `advisories` when
present — see [JSON Output](/docs/reference/json-output#build). The
`context_stats` seeded-skills block is keyed `skills_workspace_seeded` (with the
legacy `skills_workspace` alias for this release). The build plan flags any
module-origin spec with no artifact yet as `newly governed by module scan`
before generation starts.
jaunt watch [#jaunt-watch]
Watch source and test roots and rebuild on relevant Python or TypeScript changes.
For `[target.ts]`, Jaunt also watches the configured projects, package manifests,
lockfiles, and Vitest config while excluding generated output.
```bash
jaunt watch
jaunt watch --test
```
Accepts the `build`-shaping flags (`--instruction`, `--include-target-tests`,
`--no-auto-skills`, `--no-builtin-skills`) plus `--test` (run tests after each
successful build). With `--json`, watch emits one envelope per rebuild cycle,
not nested build/test documents.
jaunt status [#jaunt-status]
Show which modules are stale or fresh, using the same dependency-driven
freshness model as `build` (including modules invalidated by an upstream API
change).
```bash
jaunt status
jaunt status --magic-only
```
| Flag | Effect |
| -------------- | --------------------------------------------------------------------------- |
| `--magic-only` | Probe only generated-artifact freshness; skip contract and repo-map checks. |
A module the next build resolves through the free re-stamp path reads
`stale (re-stamp: free)` rather than `stale (structural)`; when that module also
carries legacy `raise RuntimeError("spec stub")` bodies, status hints
`jaunt migrate`. Orphaned artifacts (a generated file whose spec is gone) list
in their own section, reported under `"orphans"` in `--json`.
jaunt clean [#jaunt-clean]
Remove target-owned generated artifacts under the configured roots. Python removes
`__generated__` directories and header-marked `.pyi` stubs. TypeScript removes owned
implementations, API mirrors, sidecars, and generated magic-test batteries while
preserving private specs, public facades, and contract batteries.
```bash
jaunt clean
jaunt clean --dry-run
jaunt clean --orphans
```
| Flag | Effect |
| ----------- | ------------------------------------------------------------- |
| `--dry-run` | List what would be removed without deleting. |
| `--orphans` | Remove only orphaned artifacts, not the whole generated tree. |
| `--json` | Emits `would_remove`. |
Plain `jaunt clean` wipes every generated directory and stub. `--orphans`
narrows the sweep to artifacts whose spec no longer exists: the generated
module, its `.contract.json` sidecar, its `.pyi`, and its generated tests or
contract battery. It leaves fresh, still-governed output alone, and writes one
`orphan removed` line to the journal per deletion. Combine with `--dry-run` to
preview without deleting or journaling.
Testing [#testing]
jaunt test [#jaunt-test]
Generate held-out batteries, then run the target's protected test runner on only the
generated files. Python uses pytest; TypeScript uses project-local Vitest.
```bash
jaunt test
jaunt test --no-build
jaunt test --no-run
jaunt test --pytest-args=-k --pytest-args email
jaunt test --language ts --target ts:src/tokens/index
```
| Flag | Effect |
| --------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `--no-build` | Skip the `build` step. |
| `--no-run` | Generate tests without running the target's pytest or Vitest runner. |
| `--pytest-args ARG` | Extra arg appended to pytest for Python targets (repeatable). TypeScript runner args are intentionally not accepted. |
| `--no-semantic-gate` | Rebuild on every real change; skip the Layer B gate. |
| `--no-redact-derived` | Debug only — feed full derived-tier failure detail into repair. |
Generated tests split into a held-out set: Python cases use
`@pytest.mark.jaunt_tier("example" | "derived")`; TypeScript batteries carry the
same tier in protected reporter metadata.
When a test fails and Jaunt regenerates the implementation, derived failures are
redacted to a semantic case id and category so the implementation generator never
sees the value, file, timing, or diagnostic location it could otherwise game;
example failures keep authored evidence.
`--no-redact-derived` disables that barrier and logs a warning — a debugging
escape hatch only.
Fresh TypeScript batteries skip generation but still typecheck and run. Changes only
to the protected runner or Vitest fingerprint deterministically reheader the unchanged
battery and appear under `refrozen`; content-affecting drift and `--force` regenerate it.
Contract mode [#contract-mode]
Adopt committed code under docstring/TSDoc contracts and keep a derived pytest or
Vitest battery in sync. See the [Contract Mode guide](/docs/guides/contract-mode)
and [TypeScript target guide](/docs/guides/typescript#design-and-contract-mode).
jaunt adopt [#jaunt-adopt]
Add the target-language contract marker and derive its battery.
```bash
jaunt adopt my_app.slug:slugify
jaunt adopt src/slug.ts#slugify --language ts
```
Takes a positional Python `module:func` or TypeScript `path.ts#symbol` ref plus the
common flags. Python adopts whole classes; TypeScript supports committed exported
functions and concrete classes described in the TypeScript guide.
jaunt reconcile [#jaunt-reconcile]
Derive or refresh committed contract batteries. This is the only contract
command that calls the model.
```bash
jaunt reconcile
jaunt reconcile --language ts
```
jaunt check [#jaunt-check]
The single deterministic CI gate for both modes — no model, no API key. It
verifies committed contract batteries and Python/TypeScript magic freshness, exiting `4`
on any blocking drift: a contract that is unbuilt, stale-prose, signature-drift,
or behavior-drift; or a magic module that is unbuilt or stale (including a
missing or stale Python `.pyi` stub). TypeScript invalid-artifact diagnostics and
unbuilt private spec modules block the same gate.
```bash
jaunt check
jaunt check --contracts-only
jaunt check --magic-only
```
| Flag | Effect |
| ------------------ | -------------------------------------------------------------- |
| `--contracts-only` | Gate only contract batteries. |
| `--magic-only` | Gate only generated-artifact freshness for configured targets. |
`check` also blocks on orphaned artifacts. A generated module, `.pyi`, or
contract battery whose spec no longer exists exits `4` with a message naming the
fix — `jaunt clean --orphans`, or restore the spec. A mid-refactor rename shows
one orphan and one newly-governed spec and holds CI red until you rebuild, which
is intentional.
`--contracts-only` and `--magic-only` are mutually exclusive. `--json` reports a
`magic` block (`fresh` / `stale` / `unbuilt`, plus generated/stub/sidecar
`orphans`) and a top-level `orphans` key for contract-battery orphans; see
[JSON Output](/docs/reference/json-output#check). A project with no magic specs
and no contract drift exits `0`.
jaunt eject [#jaunt-eject]
Remove contract tracking; leave ordinary Python/pytest or TypeScript/Vitest code.
```bash
jaunt eject my_app.slug:slugify
jaunt eject src/slug.ts#slugify --language ts
jaunt eject ts:src/slug/index --language ts
jaunt eject --all
```
| Flag | Effect |
| ------- | ------------------------------ |
| `--all` | Eject every contract function. |
Daemon [#daemon]
Background codegen from committed spec changes. See the
[Background Daemon guide](/docs/guides/daemon).
jaunt daemon [#jaunt-daemon]
```bash
jaunt daemon start # foreground; Ctrl-C to stop
jaunt daemon status
jaunt daemon stop
```
`start` and `status` accept `--root` and `--json`; `stop` accepts `--root`.
jaunt jobs [#jaunt-jobs]
Inspect job records and the pending-rebuild preview, and land or discard parked
proposals.
```bash
jaunt jobs
jaunt jobs show --full
jaunt jobs retry
jaunt jobs land
jaunt jobs land --all
jaunt jobs discard
jaunt jobs wait --timeout 1800
```
| Subcommand | Purpose | Flags |
| ------------- | ------------------------------------------------------ | ------------------------------------------------------- |
| (bare) `jobs` | Job records, pending rebuilds, latest heartbeat phase. | `--root`, `--json` |
| `show ID` | One job record. | `--full` (local detail log), `--json` |
| `retry ID` | Retry landing a parked job. | `--force` (land even if the spec changed since parking) |
| `land [ID]` | Land a parked proposal as a provenance commit. | `--all`, `--json` |
| `discard ID` | Mark a proposal `DISCARDED` and drop its patch. | `--json` |
| `wait [ID]` | Block until a job (or the daemon) is idle. | `--timeout N`, `--settle N`, `--progress`, `--json` |
Under the default propose-only mode (`[daemon] auto_commit = false`), green jobs
are parked as proposals rather than committed:
* `jaunt jobs land ` lands one proposal. It re-validates first: a stale spec
digest, a mismatched branch, dirty target paths, or a 3-way apply conflict all
refuse the land (exit `4`); a stale digest or apply conflict also marks the
proposal `SUPERSEDED`. `jaunt jobs land` has no `--force`; use
`jaunt jobs retry --force` to land despite a changed spec.
* `jaunt jobs land --all` lands every fresh proposal in job-creation order,
prints per-job outcomes, and exits `0` only when every attempted land
succeeded, else `4`.
* `jaunt jobs discard ` drops a proposal and its patch artifact.
`jaunt jobs wait` blocks until the target job finishes, or (without an id) until
the daemon is idle. A parked proposal (`PROPOSED`) counts as terminal-green, so
`wait` returns `0` on it. `--settle N` sets the idle race-guard window;
`--timeout N` exits `5` on timeout. Bad ids or wrong-state jobs exit `2`.
**Automation**
| Pattern | Command |
| ------------------------- | ------------------------------------------------------------------------------------------- |
| Agent loop (propose-only) | `git commit … && jaunt jobs wait --timeout 1800 && jaunt jobs land --all` |
| Agent loop (auto-commit) | `git commit … && jaunt jobs wait --timeout 1800` |
| Notifications | Set `[daemon] notify_command` to run your hook on proposed, landed, parked, or failed jobs. |
jaunt log [#jaunt-log]
Tail the `JAUNT_LOG` change journal that build, test, and daemon runs append to.
```bash
jaunt log
jaunt log -n 50 # 0 = all lines
jaunt log --module my_app.specs
```
| Flag | Effect |
| ----------------------- | --------------------------------------- |
| `-n, --lines N` | Number of lines (`0` for all). |
| `--module MODULE` | Filter by module name. |
| `--root PATH`, `--json` | Root override; machine-readable output. |
jaunt guard [#jaunt-guard]
A PreToolUse hook for coding agents that reads a hook payload on stdin and warns
when a tool call is about to touch files under the generated dir. See
`docs/hooks.md` in the repo for Claude Code wiring.
```bash
jaunt guard
jaunt guard --generated-dir __generated__
```
| Flag | Effect |
| --------------------- | ------------------------------------------------------------------------ |
| `--generated-dir DIR` | Override the guarded dir (default from `jaunt.toml` or `__generated__`). |
jaunt install-codex-plugin [#jaunt-install-codex-plugin]
Install Jaunt's first-party Codex plugin from the GitHub marketplace, or from a
local clone for development.
```bash
jaunt install-codex-plugin
jaunt install-codex-plugin --local --root /path/to/jaunt
jaunt install-codex-plugin --json
```
jaunt install-claude-plugin [#jaunt-install-claude-plugin]
Install Jaunt's first-party Claude Code plugin with the matching GitHub or local
marketplace flow.
```bash
jaunt install-claude-plugin
jaunt install-claude-plugin --local --root /path/to/jaunt
jaunt install-claude-plugin --json
```
Both installers are idempotent and use a 120-second timeout per CLI command.
Maintenance [#maintenance]
jaunt tree [#jaunt-tree]
Maintain `treedocs.yaml`, the one-line-per-path repo map injected into build
prompts. See [Repo Context](/docs/concepts/repo-context).
```bash
jaunt tree # sync (AST-only descriptions by default)
jaunt tree --check # CI gate: exit 4 if the tree is stale
jaunt tree --enrich # force LLM enrichment this run
```
| Flag | Effect |
| -------------------------- | -------------------------------------------------------------------------- |
| `--check` | Exit `4` if the tree is stale. |
| `--enrich` / `--no-enrich` | Force or forbid LLM enrichment this run (default from `[context] enrich`). |
jaunt cache [#jaunt-cache]
Inspect or clear the LLM response cache under `.jaunt/`.
```bash
jaunt cache info
jaunt cache clear
```
Both accept `--root`, `--config`, and `--json`.
jaunt skill [#jaunt-skill]
Manage project-local skills under `.agents/skills/`.
```bash
jaunt skill list
jaunt skill show rich
jaunt skill add rich --description "Rich usage notes" --lib rich
jaunt skill remove rich -f
jaunt skill import --from /path/to/skills-dir
jaunt skill refresh
jaunt skill build rich
```
Two subcommands generate content through the Codex engine: `skill build `
expands a user-managed checked-in skill (it must already exist with `--lib`
metadata, usually from `skill add --lib `), and `skill refresh`
regenerates Jaunt-managed auto skills. Both drive `codex exec` under the hood.
jaunt migrate [#jaunt-migrate]
Plan or apply mechanical migrations. The default is always a dry run:
`jaunt migrate` prints what it would change and exits `0`, while
`jaunt migrate --apply` writes the validated plan. Neither mode calls the model.
```bash
jaunt migrate
jaunt migrate --apply
jaunt migrate --apply --allow-newly-governed
jaunt migrate --language ts
jaunt migrate --language ts --apply
```
For Python targets, the command handles two source and stub migrations:
* **Legacy stub bodies.** A `raise RuntimeError("spec stub")` body — the form the
old scaffold emitted — is rewritten to `...`, and the generated header is
re-stamped over the untouched body, so the conversion is free. For a symbol
that is already a spec this is a `re-stamp`. For a currently ungoverned
function in a `magic_module` file, rewriting its body would newly govern it and
commission a first build, so `migrate` lists it as `would newly govern` and
skips it unless you pass `--allow-newly-governed`.
* **Stub re-emission.** When a committed `.pyi` is stale only because
`_STUB_FORMAT_VERSION` bumped in a past release, `migrate --apply` re-renders
it without a build. This retires the 1.4.2 wart where `jaunt check` reported
stubs stale until you ran `jaunt build` once.
For a TypeScript-only version-2 root, plain `jaunt migrate` selects TypeScript.
Use `--language ts` explicitly in a mixed Python/TypeScript root. The worker
analyzes the current project and proposes only deterministic artifact work:
* create or repair the API mirror, canonical facade, typed unbuilt placeholder,
and sidecar;
* re-stamp a compatible built implementation when only its tool fingerprint or
deterministic artifacts are stale;
* report `model-rebuild` when the contract changed or an alpha protocol, IR, or
route is incompatible.
The TypeScript migration records exact input hashes and commits its complete
write set through the recoverable artifact transaction. An apply refuses a stale
plan or dirty worktree unless `--force` is present. Preview-era private specs,
facades, or loader layouts that do not map unambiguously to the current route are
reported as `manual-intervention`; Jaunt does not rename or rewrite them.
| Flag | Effect |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------- |
| `--apply` | Write the planned changes. Refuses on a dirty git tree unless `--force`. |
| `--force` | Apply despite uncommitted changes. |
| `--language {py,ts}` | Select one target. Plain migrate selects TypeScript only when `[target.ts]` is the sole configured target. |
| `--allow-newly-governed` | Also rewrite legacy bodies that would newly govern a symbol. |
| `--json` | Emit `{applied, actions: [...]}`. TypeScript plans also include `plan_digest`, `diagnostics`, `blocked`, and `requires_rebuild`. |
Every applied change is printed file by file. After an apply on a legacy-body
project, `jaunt status` reports the module fresh with no model call.
jaunt eval [#jaunt-eval]
`jaunt eval` is **not currently supported** under the Codex engine (a rework
is pending). The command exits `2`. The provider/compare benchmarking suite
predates the Codex cutover and will return in a future release.
Exit codes [#exit-codes]
| Code | Meaning |
| ---- | --------------------------------------------------------------------------------------------------------------------------------- |
| `0` | Success |
| `2` | Config, discovery, or dependency-cycle error; bad or wrong-state job id |
| `3` | Generation error (LLM / backend / validation / import) |
| `4` | Pytest/Vitest failure, contract `check`/`reconcile` block, stale `tree --check`, or a daemon job failed/parked during `jobs wait` |
| `5` | Timeout while waiting for daemon jobs |
Next: [Configuration](/docs/reference/config).
---
# Configuration (jaunt.toml)
Jaunt finds the project root by walking upward from the current directory until
it hits a `jaunt.toml`. `jaunt init` scaffolds a small, opinionated starter. Version
1 keeps Python paths under `[paths]`; version 2 moves language-local settings under
`[target.py]` and `[target.ts]`. Both accepted shapes are documented below.
A minimal config sets only paths:
```toml
version = 1
[paths]
source_roots = ["src"]
test_roots = ["tests"]
generated_dir = "__generated__"
```
Version 2 language targets [#version-2-language-targets]
Version 2 keeps shared Codex, daemon, context, and semantic-gate settings at the
top level and moves language-specific paths under targets. A root may configure
Python, TypeScript, or both:
```toml
version = 2
[target.py]
source_roots = ["services/api/src"]
test_roots = ["services/api/tests"]
generated_dir = "__generated__"
infer_deps = true
test_infer_deps = true
emit_stubs = true
ty_retry_attempts = 1
async_runner = "asyncio"
check_generated_imports = true
generated_import_allowlist = []
pytest_args = ["-q"]
auto_class_tests = false
contract_battery_dir = "tests/contract"
[target.ts]
source_roots = ["packages/*/src"]
test_roots = ["packages/*/tests"]
projects = ["tsconfig.json"]
test_projects = ["tsconfig.test.json"]
tool_owner = "."
generated_dir = "__generated__"
test_runner = "vitest"
vitest_config = ""
vitest_args = [] # reserved; the protected runner requires this to remain empty
auto_class_tests = false
fast_check_runs = 50
contract_battery_dir = "tests/contract"
[build]
jobs = 8
include_target_tests = false
instructions = []
[test]
jobs = 4
[prompts.py]
build_system = ""
build_preamble = ""
build_module = ""
test_system = ""
test_module = ""
project_overview_system = ""
project_overview_user = ""
[prompts.ts]
build_system = ""
build_module = ""
test_system = ""
test_module = ""
design_system = ""
design_user = ""
[contract]
derive = ["examples", "errors"]
strength = true
property_max_examples = 50
```
`[target.ts].projects` and `test_projects` are explicit; Jaunt does not guess the
nearest `tsconfig.json`. `tool_owner` must directly declare `@usejaunt/ts` and a
supported project-local TypeScript compiler. Version 2 cannot also contain `[paths]`.
The other shared sections (`llm`, `agent`, `codex`, `daemon`, `skills`,
`semantic_gate`, and `context`) use the same keys and defaults shown in the version-1
schema below. In version 2, `contract.battery_dir` moves to each target as
`contract_battery_dir`; the remaining `[contract]` keys stay shared. Version-1 files
keep their existing behavior and JSON output.
Use `jaunt migrate --config-v2` to preview a deterministic version-1 rewrite and
`jaunt migrate --config-v2 --apply` to commit it. The migration is model-free and
refuses a dirty worktree unless `--force` is supplied.
Version 1 full schema [#version-1-full-schema]
Every key outside `version` is optional and shown with its default.
```toml
version = 1
[paths]
source_roots = ["src"] # dirs scanned for specs (package parent, e.g. "src")
test_roots = ["tests"] # dirs scanned for @jaunt.test specs
generated_dir = "__generated__" # output dir for generated code
[llm]
# Retained for back-compat and ignored by the Codex engine. The model is set in
# [codex], and Codex authenticates via `codex login` / CODEX_API_KEY, not
# api_key_env. The provider and anthropic_* keys below are pre-Codex leftovers
# with no effect today; they stay in the schema so old configs still validate.
provider = "openai"
model = "gpt-5.2"
api_key_env = "OPENAI_API_KEY"
max_cost_per_build = 5.0
reasoning_effort = "high" # informational under Codex
anthropic_thinking_budget_tokens = 2048
prompt_cache = false
prompt_cache_key = ""
[build]
jobs = 8 # parallel build workers
infer_deps = true # AST-infer dependency edges in addition to deps=
ty_retry_attempts = 1 # ty-driven regeneration retries
async_runner = "asyncio" # asyncio | anyio
include_target_tests = false # keep target test source out of build prompts
check_generated_imports = true # reject undeclared imports in generated code
generated_import_allowlist = [] # extra top-level imports to permit in generated code
instructions = [] # persistent extra build-generation instructions
emit_stubs = true # emit provenance-headed .pyi stubs (opt-out)
[test]
jobs = 4
infer_deps = true
pytest_args = ["-q"]
auto_class_tests = false # auto-generate pytest coverage for class specs
[prompts]
# Override packaged prompt templates with project-local files (empty = default).
build_system = ""
build_preamble = ""
build_module = ""
test_system = ""
test_module = ""
project_overview_system = ""
project_overview_user = ""
[agent]
engine = "codex" # the only supported engine
[codex]
model = "gpt-5.6-sol"
reasoning_effort = "medium" # low | medium | high
sandbox = "workspace-write"
# Opt-in: embed `codex --version` in freshness fingerprints. Couples `jaunt
# check` to environments that have the codex binary installed.
fingerprint_cli_version = false
features = []
# Raw passthrough to the `codex` CLI (advanced); keys here are not validated.
[codex.config]
[daemon]
poll_interval = 2.0 # seconds between HEAD polls
max_jobs = 0 # 0 -> build.jobs
notify_command = "" # optional shell command run on job completion
auto_commit = false # false parks green jobs as proposals (land later)
[skills]
auto = true # auto-generate PyPI helper skills for imports
max_chars_per_skill = 8000 # retained for back-compat (unused by Codex builder)
inject_user_skills = [] # retained for back-compat (unused by Codex builder)
builtin = true # seed Jaunt's bundled builtin skills
builtin_skills = ["pytest", "ruff", "ty", "uv"] # override to trim/extend the default set
[contract]
battery_dir = "tests/contract" # where derived contract batteries are written
derive = ["examples", "errors"] # case kinds derived from docstring prose
strength = true # run mutation-based strength scoring at reconcile
[semantic_gate]
enabled = true # gate behaviorally-equivalent edits before a rebuild
model = "gpt-5.6-luna" # small model that judges contract equivalence
reasoning_effort = "medium" # low | medium | high
[context]
repo_map = true # maintain treedocs.yaml + inject a repo map
repo_map_file = "treedocs.yaml"
enrich = false # LLM-enrich descriptions (else AST-only, offline)
max_chars = 6000 # cap the injected repo-map block
overview = false # inject a model-written architecture overview
[context.search] # colgrep semantic retrieval (opt-in)
enabled = false # requires the `colgrep` binary on PATH
internal_retrieval = true # seed _context/relevant_*.py from colgrep hits
max_hits = 8
```
Notes on a few keys:
* `paths.source_roots` accepts literal directories and globs. Jaunt expands
globs in lexical order and routes each module through the most-specific
matching import root. An unmatched glob is a config error; a missing literal
is tolerated when another source root exists.
* `paths.test_roots` uses the same glob rules. Test roots are grouped by their
nearest `pyproject.toml`, and generated tests go to the first configured root
for that owner.
* `prompts.*` are treated as *file paths* read at runtime, not inline text. Copy
a default from `src/jaunt/prompts/` and point at your edited copy.
* `[llm]` is informational under the Codex engine. Codex handles its own
authentication; `llm.api_key_env` does not configure generation.
* `build.async_runner` selects the pytest marker for generated async tests.
* `[codex.config]` is raw `-c key=value` passthrough to `codex exec`; keys under
it are not validated by Jaunt.
Deeper coverage lives with each feature: [`[context]`](/docs/concepts/repo-context),
[`[semantic_gate]`](/docs/concepts/change-detection),
[`[contract]`](/docs/guides/contract-mode), and
[`[daemon]`](/docs/guides/daemon). Version-to-version defaults changes are
collected in [Upgrading](/docs/reference/upgrading).
The generated-import allowlist [#the-generated-import-allowlist]
`[build] check_generated_imports` (on by default) rejects any top-level import
in generated code that is not the standard library, a first-party module, or a
declared project dependency. It catches the model reaching for a package you
never installed. `generated_import_allowlist` is the escape hatch: distribution
names on this list are permitted even when no pyproject declares them.
```toml
[build]
generated_import_allowlist = ["whenever"]
```
Before 1.5.1 you needed this more often than you should have. In a uv workspace
with `jaunt.toml` at the repo root, the validator resolved declared dependencies
only from the root pyproject. A dependency declared in the *owning package's*
pyproject — the normal place to declare it — was invisible, so a correct
`import whenever` was rejected as undeclared and the allowlist was the only way
through. 1.5.1 fixes the resolution: Jaunt now reads the pyproject that owns the
spec's own source file as well as the config-root pyproject, so a dependency
declared at either level resolves on its own.
So you rarely need the allowlist now. Reach for it only when a real import
belongs to a distribution that no pyproject declares — a namespace package, a
vendored module, an implicit transitive dep you import on purpose. The error
message names it as one of the fixes when generation trips the check.
Strict validation [#strict-validation]
Jaunt validates `jaunt.toml` against the schema above before doing any work. An
unknown section or key is an error, not a silent no-op, and Jaunt suggests the
closest real name. This catches typos that would otherwise leave a setting
quietly at its default.
A misspelled key:
```toml
[build]
infer_dep = true
```
```text
error: unknown key 'infer_dep' in [build] — did you mean 'infer_deps'?
```
A misspelled section:
```text
error: unknown key 'buld' in jaunt.toml — did you mean 'build'?
```
Both exit `2`. Under `--json` the same message goes to stderr and stdout carries
`{"command": "...", "ok": false, "error": "..."}`.
Before a `jaunt.toml` exists, `jaunt instructions` prints this whole schema, so
you can see the accepted keys without guessing.
Next: [JSON Output](/docs/reference/json-output).
---
# JSON Output
Pass `--json` to a command and it prints a single JSON object on stdout instead
of human progress. This is the interface for CI annotations, agents, and any
tooling that shells out to Jaunt. The payloads below are trimmed from real runs.
Conventions [#conventions]
* Every envelope carries a `"command"` field naming the command and an `"ok"`
boolean.
* Errors go to **stderr** as a human-readable line. In `--json` mode Jaunt also
writes a machine envelope to stdout: `{"command": ..., "ok": false, "error":
"..."}`. So a non-zero exit can still leave parseable JSON on stdout.
* Progress bars are suppressed. To keep a progress display while parsing JSON,
use `--progress plain` (progress on stderr, JSON on stdout).
* `--json` is available on `build`, `test`, `status`, `check`, `specs`, `jobs`,
`sync`, `design`, `clean`, `migrate`, `watch`, `instructions`, `init`, `log`,
`tree`, `cache`, and `skill`.
Version-1 Python projects keep these envelopes byte-for-byte. Version-2 workspaces
add `"schema_version": 2`, qualify top-level IDs (`py:demo.specs`,
`ts:src/slug/index`), and repeat language-local values under `targets.py` and
`targets.ts`. TypeScript errors use a structured `{code, message, diagnostics}`
record; version-1 errors remain strings.
```json
{
"schema_version": 2,
"command": "build",
"ok": true,
"generated": ["ts:src/slug/index"],
"skipped": ["py:demo.models"],
"refrozen": [],
"failed": {},
"targets": {
"py": {"generated": [], "skipped": ["demo.models"], "refrozen": [], "failed": {}},
"ts": {"generated": ["src/slug/index"], "skipped": [], "refrozen": [], "failed": {}}
}
}
```
build [#build]
```json
{
"command": "build",
"ok": true,
"generated": ["demo.specs"],
"skipped": [],
"refrozen": [],
"failed": {},
"cost": {
"api_calls": 1,
"cache_hits": 0,
"prompt_tokens": 79687,
"cached_prompt_tokens": 61952,
"completion_tokens": 557,
"total_tokens": 80244,
"estimated_cost_usd": 0.16383
},
"cache": { "hits": 0, "misses": 1 },
"context_stats": {
"demo.specs": {
"preamble": { "chars": 2790, "est_tokens": 697 },
"module_contract": { "chars": 7, "est_tokens": 1 },
"repo_map": { "chars": 131, "est_tokens": 32 }
}
},
"emitted_stubs": {
"demo.specs": "/path/to/src/demo/specs.pyi"
}
}
```
* `generated`, `skipped`, `refrozen` — modules built, left fresh, and
re-frozen by the [semantic gate](/docs/concepts/change-detection) without a
rebuild.
* `failed` — `{module: reason}`; a non-empty map sets `ok: false` and exits `3`.
* `context_stats` — per-module char/token estimates for each prompt block
(`preamble`, `system`, `module_contract`, `deps`, `package_context`,
`repo_map`, `blueprint`, `skills_workspace_seeded`). Trimmed above. The
seeded-skills block also carries the legacy `skills_workspace` alias for this
release.
* `emitted_stubs` — `{module: pyi_path}`, present only when `.pyi` stubs were
written. `stub_warnings` appears only when a hand-authored stub was skipped.
* `needs_deps` — present only when a module inlined logic for an **undeclared**
dependency: `{module: [marker, ...]}`. Declare those deps to reuse the code.
* `advisories` — present only when Codex flagged a logical issue while
implementing: `{module: [note, ...]}`. Advisories never touch digests or exit
codes.
* `newly_governed` — present only when a `magic_module` scan governed a symbol
that has no artifact yet: `{module: [symbol, ...]}`. Those modules build on the
next run.
status [#status]
```json
{
"command": "status",
"ok": true,
"stale": ["demo.specs"],
"stale_changes": { "demo.specs": "structural" },
"fresh": [],
"digests": { "demo.specs": "9c39cc26…f656d76a" },
"orphans": [],
"contracts": [],
"contract_review": [],
"tree": { "added": 1, "removed": 0, "restaled": 0 }
}
```
`stale_changes` classifies why each stale module changed — `"structural"`,
`"prose"`, `"fingerprint"`, or `"re-stamp"` when the next build resolves the
module through the free re-stamp path (text output renders this value as
`re-stamp: free`). `orphans` lists generated artifacts whose spec no longer exists.
`tree` reports repo-map drift, or is `null` when the tree is in sync.
A TypeScript status also reports unbuilt and invalid artifacts plus structured
diagnostics:
```json
{
"schema_version": 2,
"command": "status",
"ok": true,
"fresh": [],
"stale": ["ts:src/slug/index"],
"stale_changes": {"ts:src/slug/index": "structural"},
"unbuilt": ["ts:src/new/index"],
"invalid": {
"ts:src/bad/index": [
{"code": "JAUNT_TS_INVALID", "message": "artifact hash does not match", "severity": "error"}
]
},
"diagnostics": [],
"targets": {
"ts": {
"fresh": [],
"stale": {"src/slug/index": "structural"},
"unbuilt": ["src/new/index"],
"invalid": {
"src/bad/index": [
{"code": "JAUNT_TS_INVALID", "message": "artifact hash does not match", "severity": "error"}
]
},
"orphans": []
}
}
}
```
check [#check]
```json
{
"command": "check",
"ok": true,
"blocked": [],
"checked": [],
"orphans": [],
"magic": {
"fresh": ["demo.specs"],
"stale": {},
"unbuilt": [],
"orphans": []
}
}
```
`blocked`/`checked` cover contract batteries. Orphaned artifacts are split by
kind: contract-battery orphans are listed at the top-level `orphans` key
(present whenever contracts are in scope), while generated-module, stub, and
sidecar orphans are listed under `magic.orphans` (present whenever magic is in
scope). A combined `jaunt check` emits both keys; `--contracts-only` emits only
the top-level `orphans`, and `--magic-only` emits only `magic.orphans`. Any
blocking drift — including an orphan of either kind — sets `ok: false` and exits
`4`. Before the first build the same project reports `"unbuilt": ["demo.specs"]`
and exits `4`.
In a version-2 TypeScript payload, `magic` is partitioned as `magic.ts` and includes
`invalid`; `targets.ts.magic` repeats the same language-local state. Structured
diagnostics remain available at the top level.
specs [#specs]
```json
{
"command": "specs",
"ok": true,
"specs": [
{
"ref": "demo.specs:add",
"module": "demo.specs",
"qualname": "add",
"source_file": "/path/to/src/demo/specs.py"
}
],
"dependency_graph": { "demo.specs:add": [] }
}
```
TypeScript `specs` adds the resolved `projects`, package `owners`, and static routes.
The same spec list, routes, and dependency graph are repeated under `targets.ts`.
test [#test]
```json
{
"command": "test",
"ok": true,
"exit_code": 0,
"refrozen": [],
"generation_failed": {}
}
```
For Python, `exit_code` is pytest's exit code. A TypeScript payload instead carries
the protected runner report under `vitest` at both the top level and `targets.ts`.
`ok` is true only when the target runner passed and no generation failed. A generation
failure exits `3`; a pytest or Vitest failure exits `4`.
sync [#sync]
TypeScript `sync` is model-free. It lists each deterministic boundary artifact it
wrote and repeats the same paths under `targets.ts`:
```json
{
"schema_version": 2,
"command": "sync",
"ok": true,
"mirrors": ["src/slug/__generated__/index.api.ts"],
"placeholders": ["src/slug/__generated__/index.ts"],
"created_facades": ["src/slug/index.ts"],
"failed": {},
"targets": {
"ts": {
"mirrors": ["src/slug/__generated__/index.api.ts"],
"placeholders": ["src/slug/__generated__/index.ts"],
"created_facades": ["src/slug/index.ts"],
"failed": {}
}
}
}
```
design [#design]
`design` returns a declaration-only patch. The default records but does not apply the
proposal; a later `--apply` returns the same target with `applied: true` and makes no
second model call.
```json
{
"schema_version": 2,
"command": "design",
"ok": true,
"target": "ts:src/store/index#TokenStore",
"patch": "*** Begin Patch\n…\n*** End Patch",
"applied": false,
"diagnostics": [],
"usage": {"api_calls": 1},
"targets": {
"ts": {"target": "src/store/index#TokenStore", "applied": false}
}
}
```
jobs [#jobs]
```json
{
"command": "jobs",
"ok": true,
"jobs": [],
"would_rebuild": { "demo.specs": "structural" }
}
```
`jobs` lists daemon job records; `would_rebuild` previews the modules the next
build would regenerate and why.
clean [#clean]
```json
{
"command": "clean",
"ok": true,
"dry_run": true,
"would_remove": [
"/path/to/src/demo/__generated__",
"/path/to/src/demo/specs.pyi"
]
}
```
Without `--dry-run`, `would_remove` becomes `removed`.
migrate [#migrate]
```json
{
"command": "migrate",
"ok": true,
"applied": false,
"actions": [
{
"migration": "legacy-stub-body",
"path": "src/demo/specs.py",
"module": "demo.specs",
"symbol": "parse_email",
"kind": "rewrite-stub-body",
"classification": "re-stamp",
"description": "demo.specs.parse_email: raise RuntimeError('spec stub') -> ... [re-stamp (free)]"
}
]
}
```
`applied` is `false` in the default plan-only mode and `true` under `--apply`.
Each entry in `actions` describes one planned or applied change; `classification`
is `re-stamp` for an already-governed spec or `newly-governs` for a legacy body
that would newly govern a symbol. Neither mode calls the model.
For `jaunt migrate --language ts`, the plan is worker-validated and includes its
input-bound digest and any work that cannot be completed mechanically:
```json
{
"schema_version": 2,
"command": "migrate",
"ok": true,
"language": "ts",
"applied": false,
"plan_digest": "sha256:…",
"blocked": false,
"actions": [
{
"migration": "typescript-artifacts-v1",
"module_id": "ts:src/slug/index",
"path": "src/slug/__generated__/index.api.ts",
"kind": "api-mirror",
"classification": "deterministic-rewrite",
"description": "[deterministic-rewrite] repair API mirror: src/slug/__generated__/index.api.ts"
}
],
"diagnostics": [],
"requires_rebuild": []
}
```
Possible TypeScript classifications are `deterministic-rewrite`,
`free-restamp`, `model-rebuild`, and `manual-intervention`. A dry-run with a
manual item still has `ok: true` because the analysis succeeded; an attempted
apply returns `ok: false`, writes nothing, and includes an `error`. Successful
applies also include `applied_paths`.
instructions [#instructions]
```json
{
"command": "instructions",
"ok": true,
"text": "# Jaunt — agent primer\n…",
"project": {
"root": "/path/to/project",
"paths": { "source_roots": ["src"], "test_roots": ["tests"], "generated_dir": "__generated__" },
"engine": "codex",
"model": "gpt-5.6-sol",
"reasoning_effort": "low",
"semantic_gate": { "enabled": true, "model": "gpt-5.6-luna" },
"repo_map": true,
"freshness": { "total": 1, "fresh": 0, "stale": 1, "stale_modules": ["demo.specs"] }
}
}
```
`text` is the full primer as one string. `project` is the live snapshot, or
`null` when no `jaunt.toml` exists.
watch [#watch]
`watch` emits one envelope per rebuild cycle (a watch envelope) rather than
nested build or test documents.
Exit codes [#exit-codes]
| Code | Meaning |
| ---- | --------------------------------------------------------------------------------------------------------------------------------- |
| `0` | Success |
| `2` | Config, discovery, or dependency-cycle error; bad or wrong-state job id |
| `3` | Generation error |
| `4` | Pytest/Vitest failure, contract `check`/`reconcile` block, stale `tree --check`, or a daemon job failed/parked during `jobs wait` |
| `5` | Timeout while waiting for daemon jobs |
Next: [Output Locations](/docs/reference/output-layout).
---
# Limitations / Gotchas
These are the sharp edges you should know about.
TypeScript alpha [#typescript-alpha]
The TypeScript target does not accept `.mts`, `.cts`, JavaScript specs, abstract
governed classes, authored `private`/`protected` class members, or computed member
names. Parameter properties and mixin or `implements` heritage are also rejected.
`@jauntPreserve` is limited to the concrete implementation of a non-overloaded method
or accessor; preserve tags on overload groups are rejected.
TypeScript 7 is also rejected until its native compiler exposes a stable programmatic
API; install a supported project-local compiler such as `typescript@^5.9`.
These are explicit diagnostics rather than silently weakened validation. See the
[TypeScript target guide](/docs/guides/typescript) for the supported module layout and
workflow.
Code-Generation Engine [#code-generation-engine]
Jaunt uses the OpenAI Codex CLI (`codex exec`) as its sole code-generation
engine. Install the `codex` CLI and authenticate with `codex login` before
running generation commands.
Jaunt is batteries-included: runtime helpers (`rich`, `watchfiles`) and the test
stack (`pytest`, `pytest-asyncio`, `anyio`) are core dependencies. There are no
optional extras.
Hardcoded __generated__ Dir Name [#hardcoded-__generated__-dir-name]
Runtime forwarding for `@jaunt.magic` reads `JAUNT_GENERATED_DIR` (default: `__generated__`). `jaunt build`/`jaunt test` set this env var in-process, but that does not persist across separate shell sessions or app runtimes.
If you use a custom `paths.generated_dir`, set `JAUNT_GENERATED_DIR` in the environment where your app runs. Otherwise forwarding will still look under `__generated__`.
Prompt Overrides [#prompt-overrides]
The `[prompts]` config keys (`build_system`, `build_module`, `test_system`, `test_module`) are treated as **file paths** by the backend. If you set them, those files must exist on disk and contain the full prompt text — they're not inline strings.
If you want to tweak prompts, copy the defaults from `src/jaunt/prompts/`, modify them, and version the prompt files alongside your repo.
Dependency Context Plumbing [#dependency-context-plumbing]
The dependency DAG, ordering, and staleness propagation all work correctly. However, the backend does not currently pass rich "here is the generated dependency source" context to dependents. The LLM generating a dependent spec doesn't see what its dependencies actually implemented.
In practice, this means you may need to be more explicit in docstrings (or add `prompt=` context) when a spec depends on non-trivial behavior from another generated spec. Ordering and digest-based staleness still work — it's only the prompt context that's limited.
Auto-Generated PyPI Skills [#auto-generated-pypi-skills]
The skills system adds extra network calls (to PyPI for README fetches) and extra OpenAI calls (to generate skill docs) during `jaunt build`. These are best-effort: failures emit a warning and the build continues without the missing skills.
This means build output can vary based on your environment, network connectivity, and what's installed in the active venv. If reproducibility matters, commit the generated `.agents/skills/` directory.
Discovery Prescreen and Aliased jaunt [#discovery-prescreen-and-aliased-jaunt]
To avoid importing side-effectful modules in large repos, discovery imports
**only** modules that show evidence of jaunt specs. During the directory scan
(not the `--target` fast-path), each candidate file must contain the substring
`jaunt` and then pass an AST check for an `import jaunt` / `from jaunt import …`
statement or a `magic` / `test` / `contract` / `preserve` decorator. Files
without the textual `jaunt` are never parsed; files that fail to parse are skipped
quietly.
Documented limitation: a module that reaches jaunt only through an alias with no
textual `jaunt` anywhere in the file (for example
`import jaunt as j; @j.magic(...)`) is **not** discovered by a scan. This is
contrived and accepted. Work around it by keeping a plain `import jaunt` (or a
`from jaunt import …`) somewhere in the file, or by naming the module explicitly
with `--target`, which bypasses the prescreen and imports it unconditionally.
Module Magic (magic_module) [#module-magic-magic_module]
`jaunt.magic_module(__name__)` governs a module by rebinding each spec name to
its generated object on first access. That timing is the source of its sharp
edges.
* **Import-time capture.** Module-level code that runs *before* the rebind — a
top-level call of a governed spec, a class that subclasses one, an assignment
that instantiates one — captures the pre-rebind stub and keeps it. Rebinding
cannot fix a reference that was already stored (a base class, a dataclass
field default, a registry entry, a closure). Unlike decorator mode, a
pre-rebind stub *class* is silently constructible, since its `__init__` is
just a docstring, so the failure can be quiet. The scan emits a build-time
warning when it sees the trap; the rule is that module-level consumption of a
spec belongs inside a function. The escape hatch for a base class is an
explicit `@jaunt.magic` on it.
* **Pickle.** An instance built from a pre-rebind stub class does not round-trip
through pickle: the module attribute now resolves to the generated class, and
the two identities disagree.
* **Circular imports** that reach into a half-executed governed module bypass
resolution the same way they bypass everything else in Python. The names they
see are the pre-rebind stubs.
* **Conditional and nested defs are out of scope.** A `def` or `class` under a
top-level `if`/`try` branch, or nested inside another function, is neither
governed nor guaranteed to appear as context. Only direct top-level statements
are scanned.
* **`magic_module` in `__init__.py`** governing a whole package is not supported
yet; call it in the module that holds the stubs.
Codex Authentication [#codex-authentication]
Jaunt delegates generation to the `codex` CLI. Authenticate that CLI with
`codex login`, or provide `CODEX_API_KEY` in the environment that runs Jaunt.
The `[llm]` config section is retained for compatibility, but under Codex
`llm.api_key_env` is informational and does not configure generation
authentication.
Next: [Upgrading](/docs/reference/upgrading).
---
# Output Locations
Generated paths [#generated-paths]
TypeScript target [#typescript-target]
A TypeScript spec is a private static input. Jaunt writes the declaration mirror,
implementation, and sidecar under the configured generated directory; production
code imports the committed facade next to the spec:
```text
src/slug/
index.jaunt.ts
index.context.ts
index.ts
__generated__/
index.api.ts
index.ts
index.jaunt.json
```
The generated implementation and mirror carry `jaunt:module`, structural, prose, and
API digests. The sidecar records the same contract identity plus exact artifact
hashes. `jaunt sync` re-renders the deterministic mirror and its sidecar bookkeeping,
but it does not rewrite a built body, replace an existing facade, or advance the
built implementation's provenance to a changed contract. Semantic and fingerprint
re-stamps happen during `jaunt build`; compatible alpha-format repairs happen through
`jaunt migrate --language ts`. Do not edit any of the three machine-owned files by
hand.
Python target [#python-target]
For a spec module inside a package, the generated code lands in a
`__generated__/` dir beside the source, keeping the package structure:
| Spec module | Generated file | Import path |
| ----------------------------- | ------------------------------------ | --------------------------------- |
| `my_app.specs` (under `src/`) | `src/my_app/__generated__/specs.py` | `my_app.__generated__.specs` |
| `tests.specs_email` | `tests/__generated__/specs_email.py` | `tests.__generated__.specs_email` |
A **top-level** spec module — a bare module directly under a source root, not
inside a package — maps to a single top-level `__generated__/` dir:
| Spec module | Generated file | Import path |
| ------------------------------ | ------------------------- | ---------------------- |
| `timing` (under a source root) | `__generated__/timing.py` | `__generated__.timing` |
Package-nested `__generated__/` dirs get an `__init__.py`. The single top-level
`__generated__/` dir is deliberately left without one, so it resolves as a
[PEP 420 namespace package](https://peps.python.org/pep-0420/): two installed
distributions that each ship a top-level `__generated__` merge instead of
shadowing each other.
If you have a pre-1.3 project whose top-level modules generated into
`timing/__generated__/__init__.py`, migrate with `jaunt clean && jaunt build`.
See [Upgrading](/docs/reference/upgrading).
Provenance header [#provenance-header]
Every generated file opens with a comment header. A build module looks like:
```python
# This file was generated by jaunt. DO NOT EDIT.
# jaunt:tool_version=1.3.0
# jaunt:kind=build
# jaunt:source_module=demo.specs
# jaunt:module_digest=sha256:9c39cc26…f656d76a
# jaunt:digest_scheme=2
# jaunt:spec_digests={"demo.specs:add": {"p": "fd2f90c7…", "s": "bb83f44e…"}}
# jaunt:generation_fingerprint=sha256:1572e3c8…c33b42692
# jaunt:module_api_digest=sha256:4fe71801…afbb1b681
# jaunt:module_context_digest=sha256:1f77c98f…54b861dc
# jaunt:spec_refs=["demo.specs:add"]
```
Jaunt reads these fields to decide whether a module is stale:
* `module_digest` covers the module's own spec inputs (signatures, docstring
contracts, decorator kwargs, transitive deps).
* `module_api_digest` covers the **exported** dependency contract a downstream
module sees: callable signatures, the full cleaned docstring, and for
whole-class specs the declared members and method signatures. An upstream API
change can therefore stale a downstream module whose own source never changed.
* `module_context_digest` and `generation_fingerprint` cover the prompt context
and engine/prompt fingerprint that produced the file.
`kind` is `build` or `test`. For test modules the header is otherwise the same
shape.
.pyi stubs [#pyi-stubs]
With `[build] emit_stubs = true` (the default), a successful build also writes a
provenance-headed `.pyi` beside each spec module, so type checkers and editors
see real signatures. Docstring-only `@jaunt.magic` classes expose their designed
`__init__`/methods; decorated spec functions appear undecorated with their exact
signatures.
```python
# This .pyi stub was generated by jaunt. DO NOT EDIT.
# jaunt:tool_version=1.3.0
# jaunt:kind=stub
# jaunt:source_module=demo.specs
# jaunt:generated_digest=sha256:46ae3973…c4a9331b
# jaunt:inputs_digest=sha256:4a7f40a9…ddb7fa69
from __future__ import annotations
import jaunt
def add(a: int, b: int) -> int:
...
```
A missing or stale stub marks its module stale for `jaunt status` and
`jaunt check`. A hand-authored `.pyi` (one without the jaunt header) is never
overwritten — it is skipped with a warning.
What clean removes [#what-clean-removes]
For Python, `jaunt clean` removes `__generated__/` directories under the configured source
and test roots, plus header-marked `.pyi` stubs. It never touches a
hand-authored `.pyi`. Preview with `jaunt clean --dry-run`, which lists exactly
what would go:
```json
"would_remove": [
"/path/to/src/demo/__generated__",
"/path/to/src/demo/specs.pyi"
]
```
For TypeScript, clean works from the worker's artifact manifest instead of deleting
an arbitrary directory. It removes Jaunt-owned implementations, API mirrors,
freshness sidecars, and generated magic-test batteries. It preserves `*.jaunt.ts[x]`
inputs, handwritten context, public facades, and committed contract batteries.
Next: [Limitations](/docs/reference/limitations).
---
# Upgrading
The rest of the reference is versionless. This page collects what changes
between releases and what you have to do about it. For the complete log, see the
[GitHub releases](https://github.com/creatorrr/jaunt/releases).
**Compatibility promise:** patch releases (1.3.0 → 1.3.1) do not normally
invalidate built modules. The exception is a correctness repair for artifacts
whose provenance was previously marked fresh incorrectly; that safety migration
is called out explicitly below.
1.7.0 [#170]
Jaunt 1.7 adds the first alpha TypeScript target. The Python package is now
1.7.0; the project-local worker and marker package use their own prerelease
version under `@usejaunt/ts`.
* **Existing Python projects do not need a migration.** Version-1
`jaunt.toml` files and generated Python artifacts keep their current layout.
The release does not intentionally stale an otherwise fresh Python module;
upgrade and run `jaunt check` as usual.
* **A TypeScript target uses version-2 configuration.** Install the Python CLI,
then add the worker, a supported compiler, and the protected test runtime to
the package named by `tool_owner`:
```bash
npm install -D @usejaunt/ts@next 'typescript@^5.9' vitest fast-check @types/node
```
The worker supports Node 20 through 24 and TypeScript 5.8 through 6.x. Start
with the [TypeScript target guide](/docs/guides/typescript), then run
`jaunt sync --language ts` before the first model-backed build.
* **Adding TypeScript to a version-1 Python project is mechanical.** Preview
`jaunt migrate --config-v2`, review the version-2 target layout, and apply it
with `jaunt migrate --config-v2 --apply`. The migration moves Python-specific
settings under `[target.py]` without changing their loaded values or calling
a model.
* **Alpha artifact formats can still change.** Preview upgrades with
`jaunt migrate --language ts`. Compatible mirrors, facades, placeholders,
sidecars, and fingerprints are repaired deterministically; an incompatible
protocol, IR, route, or contract change is reported as `model-rebuild` rather
than guessed.
* **Mixed workspaces operate on both targets by default.** Use
`--language py` or `--language ts` to narrow a command. TypeScript target IDs
are qualified as `ts:path/to/spec#symbol`; existing dotted Python IDs keep
their meaning.
1.6.3 [#163]
* Removing a magic or generated-test spec now rebuilds its module instead of
re-stamping the old body. This prevents deleted implementations and pytest
functions from surviving as dead generated text.
* Generated artifacts last stamped by 1.0.0rc7 through 1.6.2 rebuild once on
upgrade because those releases could overwrite the evidence needed to detect
an earlier removal-only re-stamp. This is an intentional safety exception to
the usual patch-release freshness promise.
* Context-only rebuilds stay local, while removal and public-API changes still
propagate to affected dependents. Pure fingerprint changes and semantically
equivalent prose edits retain the free re-stamp path.
1.6.2 [#162]
* A single `jaunt.toml` can now route modules across several packages and mixed
flat/`src` layouts. `source_roots` and `test_roots` accept globs; unmatched
globs fail during config loading.
* Generated implementations, stubs, tests, contract batteries, ty checks, and
dependency validation follow each module's nearest owning `pyproject.toml`.
Root-level `status`, `check`, `clean`, `watch`, and daemon jobs cover the full
workspace.
* Run `jaunt migrate --merge-projects` to prove that existing child projects can
be consolidated without changing module names, digests, fingerprints, or
artifact paths. Add `--apply` after reviewing the plan.
* The semantic gate now defaults to `gpt-5.6-luna` at medium reasoning. Semantic
gate settings remain outside generation fingerprints, so this does not stale
generated modules.
1.5.1 [#151]
Patch release from the sixth adoption-feedback wave, focused on uv workspaces.
No generated body changes, and the one restale it causes costs nothing to clear.
* **Modules read `stale (fingerprint)` after the upgrade — the next build
re-stamps them all for free.** The build prompt's import rule changed, and the
prompt is a fingerprint input, so every built module reports stale once. Their
spec sources are unchanged, so `jaunt build` resolves each through the
re-stamp path: it writes the new fingerprint over the existing body with no
semantic-gate call and no `gpt-5.6-sol` call. `jaunt check` counts fingerprint
staleness as drift and exits `4` until you run that one build and commit the
re-stamped headers. `--json` lists the modules under `refrozen`.
* **Specs that span source roots now stop at a config error instead of building
into the wrong package.** Under 1.5.0, a multi-root project (`source_roots`
with more than one entry) routed every generated file to the first existing
root, which quietly broke packages under the others — `status` and `check`
read through the same wrong path, so the tree looked fresh while runtime was
broken. 1.5.1 makes that a hard config error (exit `2`) on `build`, `check`,
`status`, and `migrate`: it fires when governed specs span more than one root,
or when they all live under a root that is not the first existing one (the
message tells you to reorder). The adopter-verified fix is one jaunt project
per package: put a `jaunt.toml` with `source_roots = ["."]` at each package's
import root and run jaunt from there. Nested default roots (`["src", "."]`)
with specs under `src/` resolve by longest path and pass untouched.
* **Generated code may import third-party packages plainly.** The build prompt
now sanctions the standard library and any installed distribution that the
spec module imports or the owning package declares — imported from its real
module, not smuggled in through a duck-typed stand-in or a dynamic import.
Paired with it, the undeclared-import validator resolves declared dependencies
from the pyproject that owns the spec's source file, not just the config-root
one. A workspace dependency declared in its own package's pyproject now passes
without an entry in `build.generated_import_allowlist`; see
[the allowlist](/docs/reference/config#the-generated-import-allowlist).
1.5 [#15]
Minor release that closes the adoption-feedback backlog: a codex advisories
channel, `jaunt migrate`, an orphaned-artifact gate, and clearer status labels.
No built module restales on the upgrade.
* **Nothing restales when you upgrade.** The advisories prompt instruction lives
in backend code, outside the generation fingerprint, so it cannot invalidate a
built module. `_STUB_FORMAT_VERSION` is unchanged, so committed `.pyi` stubs
stay fresh. Upgrade, run `jaunt status`, and every module reports fresh.
* **`jaunt check` now blocks on orphaned artifacts.** An orphan is a generated
module, `.pyi` stub, or contract battery whose spec no longer exists. `check`
exits `4` and names the fix. On upgrade this fires only if you already have
orphans on disk from a deleted or renamed spec. Clear them once with
`jaunt clean --orphans` (preview with `--dry-run`), or restore the spec.
Hand-authored files are never candidates: only files carrying a jaunt
provenance header, plus the `.contract.json` sidecar next to an orphaned
module, can be classified.
* **`jaunt migrate` retires two upgrade chores.** It rewrites legacy
`raise RuntimeError("spec stub")` bodies to `...` and re-stamps the header so
the change costs no model call, and it re-emits `.pyi` stubs left stale by a
past `_STUB_FORMAT_VERSION` bump. It plans by default and prints what it would
change; pass `--apply` to write. This closes the 1.4.2 caveat where
`jaunt check` stayed red until you ran a full `jaunt build`.
* **Advisories are additive.** Codex ends its final message with any logical
issues it noticed while implementing: a spec ambiguity, a spec that
contradicts a dependency's documented API, or a suspected bug in code it read.
Fresh builds and test runs print them, `--json` carries an `advisories` map,
and `jaunt log` replays them. They never touch digests, staleness, or exit
codes.
* **The `context_stats` skills key is renamed.** `skills_workspace` becomes
`skills_workspace_seeded` to say what it measures: the bytes of skill files
seeded on disk and *available* to the agent, not bytes the agent read. The old
`skills_workspace` key stays as a `--json` alias for this release.
* **Two new status labels.** A module the next build resolves through the free
re-stamp path now reads `stale (re-stamp: free)` instead of
`stale (structural)`, and a module-origin spec with no artifact yet is flagged
`newly governed by module scan` before you spend anything.
* **Docs and the `jaunt init` scaffold lead with `...` again.** 1.4.2 had
switched the example stub body to `raise NotImplementedError` to satisfy strict
type checkers; `...` reads plainer, never runs, and carries no runtime risk, so
it is the default once more. All stub forms still digest identically. If ty or
Pyright flags `...` under a concrete return annotation, `raise NotImplementedError`
works just as well and switching between the two never restales a module.
1.4.2 [#142]
Patch release from the fourth adoption-feedback wave. No built module is
invalidated and re-emitting stubs makes no model calls — but note the CI
caveat: the stub format version changed, so `jaunt check` reports committed
stubs stale (exit 4) until you run `jaunt build` once and commit the
re-emitted `.pyi` files.
* **`importlib.reload()` works on governed modules.** Re-executing the same
`magic_module(__name__)` call (same file, same line) is now treated as a
reload: the prior registration is replaced instead of raising "already
called". Reload-dependent test idioms (`monkeypatch.setenv` + `reload`) no
longer force a module back to decorator style. A second call at a different
line in the same module is still an error.
* **Scaffold and docs lead with `raise NotImplementedError`.** Strict type
checkers (ty, Pyright) reject a `...` body under a concrete return
annotation outside stub files. All stub forms digest identically, so
switching your existing `...` bodies over is free. (1.5 reverted the scaffold
default back to `...`; the type-checker note still holds, and either form works.)
* **Emitted `.pyi` stubs are lint-clean.** The unused `import jaunt` is no
longer copied from the spec; names inside string annotations
(`"Chunker | None"`) resolve like plain ones (from the generated module's
imports, including `if TYPE_CHECKING:` blocks, with an `Any` fallback for
optional-dependency guards); and when `ruff` is on PATH the emitted stub is
formatted with it. You can drop per-file lint exemptions for jaunt stubs.
* **Refreeze no longer blanks `tool_version`.** Re-stamped generated headers
(the free path that resolves digest-scheme migrations and gate-equivalent
edits) now record the jaunt version that wrote them, preserving provenance.
1.4.1 [#141]
Patch release: three post-merge review fixes for `magic_module`. No built
module is invalidated.
* **A handwritten helper reached first no longer runs raw stubs.** In 1.4.0,
if the first external access to a governed module was a handwritten helper
(not a spec name), the helper's calls into sibling specs hit the pre-rebind
stubs and silently returned stub results (`None`). Resolution now fires on
the first access to *any* non-dunder attribute.
* **Circular imports are detected exactly.** The mid-execution guard now keys
on importlib's `__spec__._initializing` flag, so a circular importer probing
the module — even after the last stub is defined — sees plain stubs instead
of triggering eager resolution. The 1.4.0 known-limitation window is closed.
* **A stub shadowed by a later real definition is handwritten.** The scan now
classifies the last top-level `def`/`class` per name, matching Python's
last-binding-wins semantics. If you relied on an early stub being governed
despite a later real redefinition (unlikely), the real definition now wins.
* **Finalize errors exit structurally.** A governed module that fails
post-import finalize (for example a spec class with a custom metaclass) now
reports as a discovery error (exit 2) instead of an uncaught traceback.
1.4.0 [#140]
`jaunt.magic_module(__name__)` — module-level magic — becomes the primary
authoring style. Nothing about decorator mode changes; the new call is additive.
* **Converting a decorated spec to `magic_module` is digest-neutral, as long as
the stub body form is unchanged.** The `@jaunt.magic` line never entered the
digest, and a module-governed spec renders its signature through the same path
a decorated one does, so dropping the decorator does not restale the module.
* **Rewriting `raise RuntimeError("spec stub")` to `...` restales once.** That
older body is not a recognized stub form — in a governed module it would
classify as handwritten, not a spec — so its text is part of the current
digest. Standardizing on `...` during conversion costs one rebuild per module.
* **Module-level kwargs restale their governed specs.** A `magic_module(prompt=…)`
default is part of every governed spec's contract, so adding or editing one
restales the file, as any contract change should.
* **Eject, don't hand-strip, in a governed module.** Removing a `@jaunt.contract`
or `@jaunt.magic` marker by hand from a still-stub-shaped function inside a
`magic_module` file silently flips that function to module-magic governance —
the scan now picks it up. Use `jaunt eject`, which leaves committed real code
that classifies as handwritten, instead of deleting the marker yourself.
1.3.1 [#131]
Patch release from the second adoption-feedback wave.
* **Migrating inner `@jaunt.magic` to `@jaunt.sig` is free.** (Corrected in
1.4.2 — this note originally said to budget one rebuild; adopter reports
showed the rename resolves through the re-stamp path with zero model calls.
`jaunt status` may label the module stale until the next `jaunt build`
re-stamps it.)
* **`[codex] fingerprint_cli_version` now defaults to `false`.** Embedding
`codex --version` in the generation fingerprint coupled `jaunt check` to
environments with the codex binary installed: a CI runner without it restaled
a byte-identical tree. If you relied on CLI-version churn triggering rebuilds,
set it back to `true` explicitly. Projects that built with the old default
will see a one-time `stale (fingerprint)` on the next build after upgrading.
* **Fingerprint-only staleness is now labeled.** `status` and `check` report
`stale (fingerprint)` instead of `stale (structural)` when specs are
byte-identical but the generation fingerprint differs, and print a hint when
the cause is a missing codex binary with `fingerprint_cli_version = true`.
* **`.pyi` stubs no longer contain `from __future__ import annotations`.**
The emitter could place a future import mid-file (a syntax error for ruff and
ty). Stale stubs re-emit on the next build; no action needed.
1.3.0 [#130]
Adoption-feedback release: strict config, `.pyi` stubs, a CI-grade `check`, and
the `@jaunt.sig` vocabulary.
* **Generated layout for top-level modules changed.** A bare module `timing`
under a source root now generates to `__generated__/timing.py` instead of the
old `timing/__generated__/__init__.py`, which shadowed the module name.
Package members (`pkg.mod` → `pkg/__generated__/mod.py`) are unchanged. If you
have a top-level-module project, run `jaunt clean && jaunt build` once to
migrate. See [Output Locations](/docs/reference/output-layout).
* **Config validation is strict.** Unknown sections and keys are now errors
(exit `2`) with a did-you-mean suggestion. Upgrading can surface a latent typo
that was previously ignored and silently left a setting at its default. Fix
the reported key. See [Strict validation](/docs/reference/config#strict-validation).
* **`emit_stubs` defaults on.** Builds now write a `.pyi` beside each spec
module, and a missing or stale stub marks the module stale. Set
`[build] emit_stubs = false` to opt out. See
[`.pyi` stubs](/docs/reference/output-layout#pyi-stubs).
* **`jaunt check` gates magic drift.** `check` now fails on an unbuilt or stale
`@jaunt.magic` module, not just contract drift, so it is the single CI gate
for both modes. Scope it with `--contracts-only` or `--magic-only`. See
[the CI guide](/docs/guides/ci).
* **`@jaunt.sig` for sealed methods.** In a whole-class spec, mark a method
`@jaunt.sig` to seal its signature (exact params, defaults, annotations, and
return type enforced). See [writing magic specs](/docs/guides/writing-magic-specs).
* **Repo-map drift is decoupled from magic freshness.** `status` and `check`
track `treedocs.yaml` drift separately; `--magic-only` skips it entirely.
1.2.0 [#120]
* **The daemon parks green jobs by default.** `[daemon] auto_commit` now
defaults to `false`: a green job becomes a reviewable proposal you land with
`jaunt jobs land`, rather than an automatic commit. This is the one behavior
change existing daemon users feel. To restore the pre-1.2 auto-commit-on-green
loop, set `auto_commit = true`. See the
[Background Daemon guide](/docs/guides/daemon).
Next: back to [CLI](/docs/reference/cli).
---
# Adopt jaunt in an existing project
You have a real codebase with real tests. You do not want a rewrite. You want to try jaunt on exactly one function, keep your existing tests as the safety net, and gate the result in CI. That is what this tutorial does, using a small token-bucket rate limiter as the host project. (It is also exactly how [jaunt adopted jaunt](/docs/self-hosting) — the framework converted its own modules with this protocol, existing tests as the gate.)
Every command and its output ran against jaunt 1.3.0.
The starting point [#the-starting-point]
The project is an ordinary `src`-layout package with hand-written code and passing pytest tests. The interesting part is one pure function, `refill_tokens`, buried in `src/ratelimiter/bucket.py`:
```python
def refill_tokens(
current: float,
capacity: float,
refill_per_second: float,
elapsed: float,
) -> float:
"""Return the token count after `elapsed` seconds of refilling."""
if elapsed < 0:
raise ValueError("elapsed must not be negative")
replenished = current + refill_per_second * elapsed
return min(capacity, replenished)
@dataclass
class TokenBucket:
capacity: float
refill_per_second: float
# ... __post_init__ and a lock ...
def allow(self, cost: float = 1.0) -> bool:
with self._lock:
now = time.monotonic()
self._tokens = refill_tokens(
self._tokens, self.capacity, self.refill_per_second, now - self._last
)
self._last = now
if self._tokens >= cost:
self._tokens -= cost
return True
return False
```
`refill_tokens` is a good first target: pure, well-specified, correctness-heavy, and covered by tests. The tests pass today:
```bash
PYTHONPATH=src uv run pytest -q
```
```text
.... [100%]
4 passed in 0.01s
```
Add jaunt.toml [#add-jaunttoml]
Add jaunt and drop a config at the repo root. Point `source_roots` at the parent of your package, not the package itself.
```bash
uv add jaunt
```
```toml
version = 1
[paths]
source_roots = ["src"]
test_roots = ["tests"]
generated_dir = "__generated__"
[build]
jobs = 4
emit_stubs = true
```
jaunt 1.3.0 validates this file strictly. A typo is a hard error with a suggestion, not a silently ignored key. Write `job = 4` instead of `jobs` and any command stops:
```text
error: unknown key 'job' in [build] — did you mean 'jobs'?
```
That is exit code 2. If you are unsure of a key, `uv run jaunt instructions` prints the full annotated schema. Fix the typo and move on.
Convert one function [#convert-one-function]
Add the decorator, keep the signature, and move the body's behavior into the docstring. Delete the implementation:
```python
import jaunt
@jaunt.magic()
def refill_tokens(
current: float,
capacity: float,
refill_per_second: float,
elapsed: float,
) -> float:
"""Return the token count after `elapsed` seconds of refilling.
Tokens accrue at `refill_per_second` and never exceed `capacity`.
Raise ValueError if `elapsed` is negative.
"""
...
```
`TokenBucket.allow` still calls `refill_tokens(...)` exactly as before. Nothing else in the module changes.
Build [#build]
```bash
uv run jaunt build --progress plain
```
```text
[build] ratelimiter.bucket: generating
[build] ratelimiter.bucket: done (attempt 1)
[build] 1/1 ok=1 fail=0 ratelimiter.bucket
Cost: 1 API call(s), 0 cache hit(s)
Estimated cost: $0.2205
Emitted 1 .pyi stub(s).
Built 1 module(s), skipped 0.
```
The generated body lands in `src/ratelimiter/__generated__/bucket.py`:
```python
def refill_tokens(
current: float,
capacity: float,
refill_per_second: float,
elapsed: float,
) -> float:
"""Return the token count after `elapsed` seconds of refilling."""
if elapsed < 0:
raise ValueError("elapsed must be non-negative")
return min(capacity, current + (refill_per_second * elapsed))
```
Run the existing tests again. They never referenced `__generated__`; they call `refill_tokens` through the spec module, and it still behaves:
```text
.... [100%]
4 passed in 0.01s
```
Your own tests are the acceptance gate for the generated code, and they stay that way after the conversion — keep them in CI alongside `jaunt check`. Jaunt's validation proves the generated module is structurally sound (signatures, imports, no fallback ladders); it cannot prove semantics. A docstring that under-specifies an edge case produces generated code that honestly implements the under-specified contract — your characterization tests are what catch it, and the fix is a docstring edit plus a rebuild, not a hand-patch. That is the point of converting a covered function first.
The .pyi stub [#the-pyi-stub]
`emit_stubs` wrote `src/ratelimiter/bucket.pyi` beside the source. Your editor and type checker read the real signatures from it, and its header records the digest of the inputs it was built from:
```python
# This .pyi stub was generated by jaunt. DO NOT EDIT.
# jaunt:kind=stub
# jaunt:source_module=ratelimiter.bucket
def refill_tokens(current: float, capacity: float, refill_per_second: float, elapsed: float) -> float: ...
@dataclass
class TokenBucket:
def allow(self, cost: float = 1.0) -> bool: ...
def tokens(self) -> float: ...
```
Coverage [#coverage]
A spec body is unreachable: the runtime forwards every call into `__generated__/`, so the `...` in the stub never runs. Coverage.py does not count a bare `...` as a statement, so the default `@jaunt.magic` stub costs you nothing on a coverage report.
If you prefer a louder sentinel body, that line will read as missing:
```python
raise NotImplementedError("spec stub")
```
Exclude it once, and your `--cov-fail-under` gate stays honest:
```toml
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
'raise NotImplementedError\("spec stub"\)',
]
```
Gate it in CI [#gate-it-in-ci]
`jaunt check` is the deterministic gate. It calls no model and needs no API key. It confirms that every built module still matches its spec:
```bash
uv run jaunt check
```
```text
Contract check: 0 contract function(s).
Magic freshness: all modules fresh.
```
Change a docstring without rebuilding and the same command blocks with exit code 4:
```text
Magic freshness: 0 unbuilt, 1 stale.
[BLOCK] ratelimiter.bucket: stale (prose)
```
Because you committed the generated code (see below), CI runs `jaunt check` with no Codex access at all. A minimal GitHub Actions job:
```yaml
name: jaunt
on: [push, pull_request]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
- run: uvx --from jaunt jaunt check
- run: uv run pytest -q
```
The `--json` flag turns `check` into machine-readable output for annotations; see the [JSON output reference](/docs/reference/json-output).
Committing generated code [#committing-generated-code]
`jaunt init` does not gitignore `__generated__/`. The default posture is to commit it, which is what the CI job above assumes: deterministic installs, generated diffs visible in review, no model access in automation. Track the generated files and the `.pyi` stubs alongside your source. The alternative, regenerating in CI, means giving automation a Codex login; pick one and hold to it.
Where to next [#where-to-next]
---
# Python quickstart
You will write a JWT helper as a spec module: one `magic_module` call and two function signatures with their docstrings, no bodies. Jaunt turns that into real Python, you call it like any other module, and then you let Jaunt write the test too.
The generated snippets and representative output below come from a real run. Token use
and cost vary with the contract, repository context, cache state, and configured model.
`jaunt build` drives the OpenAI Codex CLI (`codex exec`) and spends tokens. Install the `codex` CLI and run `codex login` before you start. Jaunt previews likely model work before a build and reports actual usage afterward.
1. Create a project [#1-create-a-project]
Start from an empty `uv` project and add jaunt. The base install already carries pytest, anyio, rich, and watchfiles, so there is nothing else to pull in.
```bash
uv init jwt-quickstart
cd jwt-quickstart
uv add jaunt
codex login # one-time Codex authentication
```
2. Scaffold with jaunt init [#2-scaffold-with-jaunt-init]
```bash
uv run jaunt init
```
It prints nothing on success. It writes a `jaunt.toml`, a `JAUNT_LOG` journal, and a starter `src/specs.py`. The starter is already in module-magic style:
```python
# Starter spec: `jaunt build` implements this module into `__generated__/`.
import jaunt
jaunt.magic_module(__name__)
def greet(name: str) -> str:
"""Return a friendly greeting for `name`.
Includes the name verbatim and ends with an exclamation mark.
"""
...
```
The `jaunt.magic_module(__name__)` call at the top is the whole trick: every top-level stub in the file becomes a spec, no per-function decorator needed. The default config points at a `src` layout:
```toml
version = 1
[paths]
source_roots = ["src"]
test_roots = ["tests"]
generated_dir = "__generated__"
```
`init` writes a fuller annotated config than this; the rest is covered in the [config reference](/docs/reference/config). Replace the starter `greet` spec with the two below.
3. Write the spec [#3-write-the-spec]
A spec is a normal function with a real signature and a docstring that states the contract. The body is a placeholder — a bare `...`, which never runs. Because `magic_module` governs the file, `create_token` is a spec on its own. `verify_token` carries a `@jaunt.magic` decorator only to declare a dependency; the decorator is the precision layer for a single symbol, and it still inherits whatever the module call set. Put this in `src/specs.py`:
A spec body never runs — Jaunt replaces it, and an unbuilt spec raises a clear error the first time you call it. So `...` is all you need. Some type checkers (ty, Pyright) flag `...` under a concrete return annotation; two remedies work equally well and jaunt leaves the choice to you: relax that rule on your spec roots, or write `raise NotImplementedError` in place of `...`. The two forms digest identically, so switching between them never restales a module. Jaunt does not scaffold type-checker config.
```python
import jaunt
jaunt.magic_module(__name__)
def create_token(user_id: str, secret: str, ttl_seconds: int = 3600) -> str:
"""
Create an HS256-signed JWT for a user.
Structure: base64url(header).base64url(payload).base64url(signature)
Header: {"alg": "HS256", "typ": "JWT"}
Payload: {"sub": user_id, "iat": , "exp": }
- Sign with HMAC-SHA256 using `secret` as the key.
- base64url segments must omit "=" padding.
- Raise ValueError if user_id is empty.
"""
...
@jaunt.magic(deps=[create_token])
def verify_token(token: str, secret: str) -> dict:
"""
Verify an HS256-signed JWT and return its decoded payload.
- Split on "." into exactly three parts, else raise ValueError("malformed").
- Recompute the HMAC over header.payload and compare in constant time;
raise ValueError("invalid signature") on mismatch.
- Raise ValueError("expired") if the "exp" claim is at or before now.
- Otherwise return the payload dict with "sub", "iat", and "exp".
"""
...
```
The type hints and the docstring are the contract. `deps=[create_token]` tells Jaunt that `verify_token` depends on `create_token`, so the two build together and `verify_token` restales if `create_token`'s public shape changes.
4. Build [#4-build]
```bash
uv run jaunt build --progress plain
```
```text
[build] specs: generating
[build] specs: attempt (1/3)
[build] specs: done (attempt 1)
[build] specs: validating
[build] 1/1 ok=1 fail=0 specs
[build] done 1/1 ok=1 fail=0
Cost: 1 API call(s), 0 cache hit(s)
Tokens: 134,368 prompt + 4,950 completion = 139,318 total
Cached prompt tokens: 118,016
Emitted 1 .pyi stub(s).
specs context: 3k chars (~765 tok) — preamble 91%, blueprint 5%, repo_map 3%, package_context 1%
Built 1 module(s), skipped 0.
```
Jaunt read the module, drove `codex exec`, checked the result against your signatures, and wrote a real module to `src/__generated__/specs.py`. Two docstrings became roughly a hundred lines that do the tedious part right: canonical JSON, base64url without padding, a constant-time signature compare.
```python
def create_token(user_id: str, secret: str, ttl_seconds: int = 3600) -> str:
if user_id == "":
raise ValueError("user_id is empty")
now = int(time.time())
header_segment = _json_segment({"alg": "HS256", "typ": "JWT"})
payload_segment = _json_segment({"sub": user_id, "iat": now, "exp": now + ttl_seconds})
signature_segment = _base64url_encode(_signed_bytes(header_segment, payload_segment, secret))
return f"{header_segment}.{payload_segment}.{signature_segment}"
```
Jaunt also emitted `src/specs.pyi` next to the spec, so your editor sees the real signatures.
5. Call it [#5-call-it]
Import the spec module, not the generated one. The `magic_module` call rebinds each spec name to its generated implementation the first time you touch it.
```bash
PYTHONPATH=src uv run python - <<'PY'
from specs import create_token, verify_token
token = create_token("user-42", "s3cr3t")
print("token:", token[:32], "...")
print("claims:", verify_token(token, "s3cr3t"))
try:
verify_token(token, "wrong-secret")
except ValueError as e:
print("rejected:", e)
PY
```
```text
token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpX ...
claims: {'sub': 'user-42', 'iat': 1783199036, 'exp': 1783202636}
rejected: invalid signature
```
Call a spec before building it and you get a clear error instead of a mysterious `None`:
```text
JauntNotBuiltError: Spec specs:create_token has not been built yet. Run `jaunt build` and try again.
```
6. Let Jaunt write the test [#6-let-jaunt-write-the-test]
A `@jaunt.test` spec describes what to check; Jaunt writes the pytest. Test specs are always explicit decorators, whichever style your source uses. Put this in `tests/specs_jwt.py`:
```python
from __future__ import annotations
import jaunt
from specs import create_token, verify_token
@jaunt.test(targets=[create_token, verify_token])
def test_roundtrip() -> None:
"""
Cover the token lifecycle:
- A token from create_token verifies and returns a payload whose "sub"
matches the user id.
- verify_token raises ValueError on a tampered signature.
- verify_token raises ValueError("expired") for a token whose ttl has passed.
"""
...
```
Generate the test and run pytest in one step:
```bash
PYTHONPATH=src uv run jaunt test --progress plain
```
```text
[test] tests.specs_jwt: generating
[test] tests.specs_jwt: attempt (1/2)
[test] tests.specs_jwt: done (attempt 1)
[test] tests.specs_jwt: validating
[test] 1/1 ok=1 fail=0 tests.specs_jwt
[test] done 1/1 ok=1 fail=0
Emitted 1 .pyi stub(s).
Built 0 module(s), skipped 1.
........ [100%]
8 passed in 0.02s
Generated 1 test module(s), skipped 0.
```
One test spec became eight cases. The generated file lives at `tests/__generated__/specs_jwt.py`; the example anchored to your docstring reads:
```python
@pytest.mark.jaunt_tier("example")
def test_roundtrip() -> None:
token = create_token("user-123", "shared-secret", ttl_seconds=60)
payload = verify_token(token, "shared-secret")
assert payload["sub"] == "user-123"
header_segment, payload_segment, signature_segment = token.split(".")
tampered_signature = signature_segment[:-1] + _different_char(signature_segment[-1])
tampered_token = ".".join((header_segment, payload_segment, tampered_signature))
with pytest.raises(ValueError, match="^invalid signature$"):
verify_token(tampered_token, "shared-secret")
expired_token = create_token("user-123", "shared-secret", ttl_seconds=-1)
with pytest.raises(ValueError, match="^expired$"):
verify_token(expired_token, "shared-secret")
```
Jaunt derived five more cases beside it — header shape, signature recomputation, the empty-`user_id` guard, the wrong-secret path, and malformed tokens — each tagged `@pytest.mark.jaunt_tier("derived")`.
That is the whole loop: write intent, build, call, test. From here you keep editing docstrings and signatures; `jaunt build` regenerates only what changed.
Where to next [#where-to-next]
---
# TypeScript quickstart
This tutorial builds the starter `greet` function that `jaunt init` creates. The
private spec is parsed without being executed. Application code imports an ordinary
TypeScript facade, and the emitted JavaScript has no Jaunt runtime dependency.
The TypeScript target is alpha. It requires Jaunt 1.7 or later and installs its
project-local worker separately as `@usejaunt/ts@next`. Alpha artifact formats can
change; the [upgrade notes](/docs/reference/upgrading#170) explain the deterministic
migration path.
1. Check the prerequisites [#1-check-the-prerequisites]
You need:
* Python 3.12 or later and [`uv`](https://docs.astral.sh/uv/)
* Node 20 through 24
* the OpenAI Codex CLI, authenticated with `codex login`
Jaunt uses `codex exec` for `build` and `test`. `sync`, `status`, and `check` do not
call a model.
2. Scaffold the project [#2-scaffold-the-project]
```bash
mkdir jaunt-ts-quickstart
cd jaunt-ts-quickstart
uvx jaunt init --language ts
npm init -y
npm pkg set type=module
npm install -D @usejaunt/ts@next 'typescript@^5.9' vitest fast-check @types/node
codex login
```
`jaunt init` does not edit `package.json`; it prints the package commands you still
need to run. It creates a version-2 `jaunt.toml` with one TypeScript target:
```toml
version = 2
[target.ts]
source_roots = ["src"]
test_roots = ["tests"]
projects = ["tsconfig.json"]
test_projects = ["tsconfig.test.json"]
tool_owner = "."
generated_dir = "__generated__"
test_runner = "vitest"
fast_check_runs = 50
```
`tool_owner` names the package that directly owns `@usejaunt/ts` and TypeScript.
Jaunt uses that compiler rather than a global installation. The scaffold also writes
separate production and test `tsconfig` files and excludes private Jaunt inputs from
both emit paths.
3. Read the starter spec [#3-read-the-starter-spec]
The generated `src/index.jaunt.ts` is a private static input:
```ts title="src/index.jaunt.ts"
import * as jaunt from "@usejaunt/ts/spec";
jaunt.magicModule();
/** Return a friendly greeting for `name`, including the name verbatim. */
export function greet(name: string): string {
return jaunt.magic();
}
```
Production code never imports this file. It imports the committed facade instead:
```ts title="src/index.ts"
export * from "./index.context.js";
export * from "./__generated__/index.js";
```
The `.js` specifiers are intentional for NodeNext projects. TypeScript resolves them
to source `.ts` files, and the emitted JavaScript keeps valid runtime paths. Jaunt
also supports private `.jaunt.tsx` specs when the owning project supplies its normal
JSX configuration and runtime.
4. Sync the typed boundary [#4-sync-the-typed-boundary]
Run the model-free synchronization step before the first build:
```bash
uvx jaunt sync --language ts
npx tsc -p tsconfig.json --noEmit
uvx jaunt status --language ts
```
`sync` renders the API mirror and a typed throwing placeholder. Your editor and
compiler can resolve `greet`, while `status` still reports the implementation as
unbuilt.
```text
src/
index.jaunt.ts
index.context.ts
index.ts
__generated__/
index.api.ts
index.ts
index.jaunt.json
```
`index.api.ts`, the implementation, and `index.jaunt.json` are machine-owned. Commit
them, but change the spec and regenerate instead of editing them.
5. Build and run [#5-build-and-run]
```bash
uvx jaunt build --language ts
npx tsc -p tsconfig.json
node --input-type=module -e \
'import("./dist/index.js").then(({ greet }) => console.log(greet("Ada")))'
```
The build asks Codex for an implementation, checks it against the project-local
compiler in an in-memory overlay, then writes the validated files as one transaction.
The last command runs ordinary emitted JavaScript.
6. Generate and run the test [#6-generate-and-run-the-test]
The scaffold includes test intent in `tests/index.jaunt-test.ts`:
```ts title="tests/index.jaunt-test.ts"
import * as jaunt from "@usejaunt/ts/spec";
import { greet } from "../src/index.jaunt.js";
jaunt.magicModule();
/** `greet("Ada")` includes "Ada" and reads as a friendly greeting. */
export function greetExample(): void {
jaunt.testSpec({ targets: [greet] });
}
```
Generate the Vitest battery, run it, and finish with the offline drift gate:
```bash
uvx jaunt test --language ts
uvx jaunt check --language ts
```
`test` calls the model only when the battery needs generation. A fresh battery is
still typechecked and run on later calls. `check` never calls the model; it verifies
the committed implementation, mirrors, sidecars, and Vitest batteries.
7. Commit the result [#7-commit-the-result]
Commit the authored spec and test intent, `jaunt.toml`, both `tsconfig` files, the
package manifest and lockfile, the public facade, `JAUNT_LOG`, and the generated
artifacts. Keep `.jaunt/` ignored.
In a mixed Python and TypeScript workspace, use version 2 with both `[target.py]` and
`[target.ts]`. Plain commands cover both targets; `--language py` and `--language ts`
narrow a run. See the [TypeScript target guide](/docs/guides/typescript#mixed-python-and-typescript-workspaces)
for project references, pnpm workspaces, classes, contract mode, and current alpha
limits.
Where to next [#where-to-next]
---
# Whole-class specs and the daemon
A `@jaunt.magic` on a class lets you describe a type in prose and have Jaunt design the API. You can then take back as much control as you want, method by method. Once the specs settle, the daemon rebuilds them in the background whenever you commit, and parks the result for you to review.
This tutorial builds a small in-memory order book, then wires up the propose-only daemon around it. Everything ran against jaunt 1.3.0.
Each `jaunt build` here drives Codex and spends tokens. The three builds on this page cost about a dollar in total.
1. A class from a docstring [#1-a-class-from-a-docstring]
Start with intent only. No methods, no attributes, just the contract in the class docstring. Put this in `src/book.py`:
```python
from __future__ import annotations
import jaunt
@jaunt.magic()
class OrderBook:
"""An in-memory limit order book for one instrument.
Track resting buy and sell limit orders. The book supports:
- adding a limit order with a side ("buy" or "sell"), a positive price,
and a positive integer quantity;
- reading the best bid (highest resting buy price) and best ask (lowest
resting sell price), each None when that side is empty;
- filling a market order for a given side and quantity, consuming the
best-priced resting orders on the opposite side and returning the list
of (price, quantity) fills.
Reject a non-positive price or quantity with ValueError.
"""
```
Build it:
```bash
uv run jaunt build --progress plain
```
```text
[build] book: generating
[build] book: done (attempt 1)
[build] 1/1 ok=1 fail=0 book
Estimated cost: $0.3165
Emitted 1 .pyi stub(s).
Built 1 module(s), skipped 0.
```
Jaunt designed the whole surface. The `.pyi` stub at `src/book.pyi` shows the public shape it chose:
```python
class OrderBook:
def __init__(self) -> None: ...
def add_limit_order(self, side: str, price: float, quantity: int) -> None: ...
def best_bid(self) -> float | None: ...
def best_ask(self) -> float | None: ...
def fill_market_order(self, side: str, quantity: int) -> list[tuple[float, int]]: ...
```
It works like any class:
```python
b = OrderBook()
b.add_limit_order("sell", 101.0, 5)
b.add_limit_order("sell", 100.0, 3)
b.add_limit_order("buy", 99.0, 2)
b.best_bid(), b.best_ask() # (99.0, 100.0)
b.fill_market_order("buy", 4) # [(100.0, 3), (101.0, 1)]
```
2. Take control, method by method [#2-take-control-method-by-method]
Designing the API is fine for a sketch. For code you will maintain, you want to fix some of it. Each method in a whole-class spec sits in one of three tiers:
* **preserved** with `@jaunt.preserve`: your code, emitted verbatim.
* **sealed** with `@jaunt.sig`: Jaunt writes the body, but the signature you declared is enforced exactly.
* **guidepost**: a plain unmarked stub. Jaunt may adapt the signature or split it, as long as the documented behavior arrives.
Rewrite `src/book.py` to use all three:
```python
@jaunt.magic()
class OrderBook:
"""... same class docstring as before ..."""
@jaunt.preserve
def __init__(self) -> None:
# Resting orders as {price: total_quantity} per side.
self._bids: dict[float, int] = {}
self._asks: dict[float, int] = {}
@jaunt.sig
def add_limit_order(self, side: str, price: float, quantity: int) -> None:
"""Rest a limit order on the given side.
`side` is "buy" or "sell". Reject a non-positive price or quantity
with ValueError. Accumulate quantity when a price level already exists.
"""
...
def best_quotes(self) -> tuple[float | None, float | None]:
"""Return (best_bid, best_ask): the highest resting buy price and the
lowest resting sell price, each None when that side is empty."""
...
```
Rebuild, then read `src/__generated__/book.py`. Each tier did what it promised.
Your `__init__` came through untouched:
```python
def __init__(self) -> None:
self._bids: dict[float, int] = {}
self._asks: dict[float, int] = {}
```
`add_limit_order` kept the exact signature you sealed, and Jaunt filled the body:
```python
def add_limit_order(self, side: str, price: float, quantity: int) -> None:
book = self._book_for_side(side)
clean_price = self._validate_price(price)
clean_quantity = self._validate_quantity(quantity)
book[clean_price] = book.get(clean_price, 0) + clean_quantity
```
`best_quotes` is a guidepost, so Jaunt was free to shape it. It also pulled `fill_market_order` and a handful of private helpers back out of the class docstring. A sealed method that drifts from its declared signature is a hard build error; a guidepost that drifts only warns. For the full vocabulary, see [writing magic specs](/docs/guides/writing-magic-specs).
3. The daemon: rebuild on commit [#3-the-daemon-rebuild-on-commit]
The daemon watches your git HEAD. On each commit it builds the specs that changed, in an isolated worktree, and parks the result as a proposal for you to land. It does not touch your working tree until you say so.
The daemon only lands changes under `__generated__/`. Because `.pyi` stubs are written beside your source, turn them off in a daemon-driven repo with `emit_stubs = false` under `[build]`; otherwise a rebuild's stub write falls outside what the daemon will land.
Set `emit_stubs = false`, then commit a clean, built baseline:
```bash
git add -A && git commit -m "orderbook baseline"
```
Start the daemon. It runs in the foreground, so give it its own terminal:
```bash
uv run jaunt daemon start
```
```bash
uv run jaunt daemon status
```
```text
Daemon: running (pid 867872)
landing: propose-only
```
Now edit the spec. Add one guidepost method to `OrderBook`:
```python
def resting_volume(self, side: str) -> int:
"""Return the total resting quantity on the given side ("buy" or
"sell"). Reject any other side with ValueError."""
...
```
Commit it. That commit is the trigger:
```bash
git commit -am "add resting_volume to OrderBook spec"
```
Within a couple of seconds the daemon picks up the new HEAD, builds `book` in its worktree, and parks a proposal:
```bash
uv run jaunt jobs
```
```text
- 7de8e8fc book: proposed
battery -
would rebuild: book (structural)
```
Inspect it before landing. The proposal only touches generated paths:
```bash
uv run jaunt jobs show 7de8e8fc
```
```text
id: 7de8e8fc
module: book
state: proposed
patch_paths: ["src/__generated__/book.py", "src/__generated__/book.py.contract.json"]
cause: spec change
```
Land it. Jaunt re-validates the patch and commits it as a provenance commit; there is no `--force`:
```bash
uv run jaunt jobs land 7de8e8fc
```
```text
da593e815d1dd78c0bd5f2ef4e6d81fb25c49ca1
```
The landed commit records which job and spec produced it:
```text
regen(book): spec change
Jaunt-Job: 7de8e8fc
Jaunt-Spec: 19213577
```
Your generated `OrderBook` now has `resting_volume`, and you approved the diff instead of trusting a background process to commit to your branch. Stop the daemon when you are done:
```bash
uv run jaunt daemon stop
```
Propose-only is the default. If you would rather have the daemon commit green rebuilds itself, set `auto_commit = true` under `[daemon]`.
Where to next [#where-to-next]