# 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]