# SpecVerse AI

*Configuration reference for the AI layer — providers, models, env vars, CLI flags, and the realize-emit skill.*

**Engines version 6.97.12** · *Last updated: 2026-06-04*

---

## TL;DR — most users should set zero env vars

The AI layer auto-detects what you have available and picks a sensible default. The decision tree:

1. Inside an MCP server (`MCP_SERVER=1` set by the server itself) → **stub**
2. `claude` binary on PATH → **claude-cli** (your Max-plan auth, no marginal cost)
3. `ANTHROPIC_API_KEY` set → **anthropic** (metered)
4. Else → **stub** (emits the prompt verbatim; you run it yourself)

If you have Max + `claude-cli` installed, run `spv realize all spec.specly` and it works. Read on only when the default isn't what you want.

## When the AI runs

| Command | AI usage |
|---|---|
| `spv realize all <spec>` | Per-owner LLM emit for `.ai.ts` behaviour files (the bulk of AI work) |
| `spv ai analyse <repo>` | Reverse-engineer source code → spec (per-action microcalls) |
| `spv ai create [requirements]` | Natural-language requirements → spec |
| `spv ai behaviours <spec-dir>` | Fill action bodies for an existing analysed spec |
| `spv ai regenerate <fn>` | Regenerate one AI-emitted function |
| `spv ai template <op> <spec>` | Render a prompt without calling the LLM (inspection/iteration) |
| `spv validate / infer / gen` | **No AI** — deterministic only |
| `spv init / smoke / skill` | **No AI** — deterministic only |

## The three knobs you actually set

### 1. `SPECVERSE_AI_PROVIDER` — which backend

```bash
SPECVERSE_AI_PROVIDER=claude-cli         # Max user (default if claude on PATH)
SPECVERSE_AI_PROVIDER=anthropic          # Metered API (requires ANTHROPIC_API_KEY)
SPECVERSE_AI_PROVIDER=openai-compatible  # OSS hosts / Ollama / Together / etc.
SPECVERSE_AI_PROVIDER=stub               # No LLM; emit prompt for ambient executor
```

### 2. `SPECVERSE_AI_MODEL` — which model within that backend

```bash
# claude-cli (default: whatever your Max CLI picks — currently Opus 4.7)
SPECVERSE_AI_MODEL=claude-sonnet-4-6     # force sonnet on Max
SPECVERSE_AI_MODEL=claude-opus-4-7       # force opus explicitly

# anthropic (default: claude-sonnet-4-6)
SPECVERSE_AI_MODEL=claude-opus-4-7

# openai-compatible (default: deepseek-chat)
SPECVERSE_AI_MODEL=qwen3-coder:30b
```

**Gotcha**: `SPECVERSE_AI_PROVIDER=claude-cli` without `SPECVERSE_AI_MODEL` on a Max plan defaults to **Opus 4.7**, not sonnet. If you want sonnet, set the model explicitly.

### 3. Backend-specific auth (only if you're not on `claude-cli`)

```bash
# For anthropic
ANTHROPIC_API_KEY=sk-ant-...

# For openai-compatible
SPECVERSE_AI_BASE_URL=https://api.together.xyz/v1   # endpoint
SPECVERSE_AI_API_KEY=your-token                     # optional for Ollama
```

`claude-cli` needs nothing — it uses your authenticated Max session.

## Skills + claude-cli "thin" delivery (engines 6.44.0+; family skills 6.78.0+)

SpecVerse ships skills that carry the **shared grounding** for each AI operation. On
`claude-cli`, Claude Code auto-loads the relevant skill from `~/.claude/skills/`, so the
engine sends a **thin** system prompt (role + a "read the `<family>` skill's
`reference/<op>.md`" pointer) and lets the CLI load the grounding — no double-send. Other
providers (anthropic / openai-compatible / stub) can't auto-load skills, so the same
grounding is **inlined** from the composed prompt instead. Both bodies are byte-equal by
construction (the compose-prompts parity gate), so output is equivalent either way.

**Two kinds of skill, both installed by `spv skill install`:**

| Skill(s) | Source of truth | Trigger | Audience |
|---|---|---|---|
| `specverse` | hand-authored + schema/guides | "spv", ".specly", "create / analyse / realize my spec" | User-facing (Claude Code interactive) |
| `analyse` · `create` · `verify` · `behavior` · `manifest` | **prompt partials** → `compose-prompts.mjs` → `@specverse/assets/.../_composed/<tag>/skills/` | the matching `spv ai <op>` micro-calls | Engine-internal (one skill per operation family) |
| `realize-emit` | the realize **rule manifest** (`engines/src/realize/realize-rules.ts`) | "emit the entire contents of backend/src/behaviors/<Owner>.ai.ts" | Engine-internal (`spv realize` per-owner emit) |

`realize-emit` keeps its own manifest-driven generator (CONSTRAINTS / GUIDANCE / EXAMPLE)
— it predates and is structurally different from the per-operation family skills, so it is
not compose-generated. Without it the LLM emits noticeably noisier code (empirical: v17 no
skill = 68 TS errors, v18 skill = 7, same spec/model).

**Install:**
```bash
spv skill install --global   # installs `specverse` + the 5 family skills + realize-emit
```
The family skills install as siblings at the skills root from the composed bundle pinned by
`SPECVERSE_PROMPT_TAG` (default `current`). (Multi-skill install landed in self 5.20.0 — no
more manual `cp` of `realize-emit`.)

## Iterating prompts — composition + A/B (the two-channel rule)

Prompts are **composed from one source**: `assets/prompts/core/standard/default/partials/*.md`
→ `scripts/compose-prompts.mjs --tag <tag>` → `_composed/<tag>/`, which holds BOTH the inlined
`*.prompt.yaml` (openai / anthropic / stub) AND the per-family `skills/` (claude-cli). Edit a
partial, recompose, and both regenerate in parity.

**The trap (learned 2026-05-28):** `SPECVERSE_PROMPT_TAG` selects the composed *prompt*, so it
A/Bs openai-family providers — but **claude-cli reads the installed, auto-loaded skill**
(`~/.claude/skills/`), which the tag does NOT touch. So to test a prompt change on claude-cli you
must (re)install its skill. `SPECVERSE_PROMPT_FORCE_FULL_GROUNDING=1` forces the full context
inline, but Claude Code *still* auto-loads the (stale) skill → they conflict; that env is for
openai-side isolation, not a clean claude-cli A/B.

**Clean A/B (one source, both providers):**
```bash
cp -R core/standard/default core/standard/exp            # sibling source — default untouched
# edit core/standard/exp/partials/<…>.md
node scripts/compose-prompts.mjs --from exp --tag exp    # → _composed/exp/{prompts,skills}
# marrbox/openai:  SPECVERSE_PROMPT_TAG=exp spv ai analyse …
# claude-cli:      (re)install the exp skill, then run
```
If it wins on **both** providers (effects diverge — always test both), merge the partial edit
into `default/`, `compose --tag current`, and publish `@specverse/assets` + self (reinstall skill).
Empirical: #92's additive-only analyse-action prompt cut marrbox steps ~50-60% (business intent
intact) and lifted sonnet's business-intent 79→100% (clean) / 3→92% (idle-meta).

## Post-emit verify + feedback (engines 6.51.0+, default-on at 6.52.0)

After per-owner realize emit completes, the framework runs language-specific
verifiers (`tsc` for TypeScript today; future: `mypy`/`go vet`/etc.) on the
output tree and feeds any errors back to the LLM for surgical per-file fix
passes. Each verifier knows whether it `applies` to the realized stack; tsc
skips when there's no `tsconfig.json`.

**Default behaviour (engines 6.52.0+)**: runs automatically after realize when
LLM-emitted owners exist. Reports `✅ Post-emit feedback: N → M errors` and
writes `<outputDir>/post-emit-feedback.json` when feedback actually fires.

Empirical wins on real workloads (from the Phase A validation corpus):
- scoremyclays + qwen3-coder:30b — 9 → 0 tsc errors
- idle-meta + sonnet — 44 → 3 tsc errors
- nestjs-billing — correctly identifies template-generator errors as
  `unmappable` (not LLM-fixable), no spurious re-emits

**Env knobs (engines 6.53.2+ — escape hatch restored):**

| Env var | Default | Effect |
|---|---|---|
| `SPECVERSE_REALIZE_POST_VERIFY_PASSES=N` | `1` | Max feedback passes per realize. Early exit if a pass produces no improvement. |
| `SPECVERSE_REALIZE_POST_VERIFY_MAX_FILES=N` | `20` | Cap distinct files re-emitted per pass — bounds LLM spend on pathological codebases. |
| `SPECVERSE_REALIZE_POST_VERIFY_ENABLE=id1,id2` | unset | Opt in additional verifiers that ship `enabledByDefault: false`. |
| `SPECVERSE_REALIZE_NO_POST_VERIFY=1` | unset | Hard opt-out — skip the entire verify + feedback subsystem. Restored at 6.53.2 after the 6.53.0 retirement, in case a wild regression surfaces. |

Use `PASSES=0` if you need an audit but not a re-emit (verifier still runs;
no LLM calls fire).

**Skip cases (no audit emitted, silent):**
- Zero verifier errors after realize (the happy path; nothing to do)
- `SPECVERSE_AI_PROVIDER=stub` (no LLM available; logs a skip line)
- No LLM-emitted owners (per-owner emit fell through entirely)

## Stub-completeness verifier + diagnostic sidecar (engines 6.55.0+ Phase 1, 6.56.0+ Phase 2, 6.57.0+ Phase 3)

The tsc verifier catches code that *doesn't compile*. It says nothing about
code that compiles but ships no behaviour — the dominant failure mode for
smaller/open LLMs on per-owner emit. Engines 6.54.0 introduced a second
verifier (`stub-completeness`) for this gap; the 6.55.0 → 6.57.0 series
(Phases 1-3 of `docs/proposals/in-progress/2026-05-13-VERIFIER-DIAGNOSTIC-TREATMENT-SPLIT.md`)
refined how it's surfaced and treated.

### Diagnostic sidecar (Phase 1, engines 6.55.0)

Every realize now writes a `<outputDir>/realize-quality.json` sidecar
listing detected stub bodies per owner. Runs always (no LLM calls,
regex-only scan, milliseconds). When stubs are present, `spv realize`
prints:

```
   ⚠ realize-quality: 5 stubs across 3 owners (see realize-quality.json)
```

Sidecar shape:

```json
{
  "schemaVersion": "1.0",
  "ranAt": "2026-05-13T22:00:00.000Z",
  "ctx": { "outputDir", "targetLanguage", "subpath" },
  "totals": { "stubs", "stubsByCode": {STUB001, STUB002, ...}, "ownersWithStubs" },
  "byOwner": { "<Owner>": { "ownerName", "stubs": [{code, file, line, message}] } },
  "specGaps": [{ "ownerName", "file", "line", "code", "message" }],
  "notes": [...]
}
```

Stub codes:
- `STUB001` — engine γ-fallback (LLM declined original emit; placeholder file)
- `STUB002` — LLM-emitted throw stubs (`throw new Error("not implemented" | "requires X" | …)`)
- `STUB003` — trivial returns (`return null | undefined | {} | []`)
- `STUB004` — empty bodies (whitespace/comments only)

Opt-out: `SPECVERSE_REALIZE_NO_DIAGNOSTICS=1`.

### Treatment selector (Phase 2, engines 6.56.0)

The feedback runner now classifies each verifier error by stub kind and
dispatches a per-strategy fix:

| Code | Strategy | Behaviour |
|---|---|---|
| `TSxxxx`, `STUB001`, `STUB003`, `STUB004` | `auto-reemit` | LLM re-emits the file with the error in the prompt |
| `STUB002` | `surface-spec-gap` | NO reemit; entry appended to `realize-quality.json::specGaps` for user action |

Why: STUB002s are the LLM signalling a context gap on the original emit
(missing library, missing manifest declaration, etc.). Empirical at engines
6.54.0: blind re-prompting either no-ops or hallucinates cross-file calls
that introduce +5 / +2 tsc regressions on idle-meta Ollama/MarrBox. Surfacing
the gap to the user — who can adjust the spec, manifest, or provider — is
the correct response.

When spec gaps are routed, `spv realize` prints:

```
   ⚠ realize-quality: 3 spec gaps surfaced; review realize-quality.json::specGaps
```

The default policy can be overridden by passing a custom
`treatmentSelector` to `runPostEmitFeedback()` for advanced integrations.

### Context-complete reemit (Phase 3, engines 6.57.0)

The reemit prompt now carries the same TARGET RUNTIME + [PRE-BAKED] step
bullets + cross-service operations + capabilities the original per-owner
emit had. Mechanism:

1. Per-owner emit writes a per-owner context snapshot to
   `<outputDir>/.realize-context/<Owner>.md`
2. Feedback runner's `buildLlmReemit` reads the snapshot via
   `contextLoader: (ownerName) => loadContextSnapshot(outputDir, ownerName)`
3. `formatFeedbackPrompt` prepends the snapshot before the error block
4. Reemit prompt closes with: `Honour the [PRE-BAKED] step bodies above
   verbatim where present; do not paraphrase.`

Why: the realize-emit skill is a contract sonnet can hold from action
signatures alone. Smaller models (Ollama qwen3-coder:30b, MarrBox) need
more explicit context — Phase 3 gives them the same input the original
emit had so they don't drift further from the skill on re-emit.

## Scenarios

```bash
# Default: Max + claude-cli + realize-emit skill installed
spv realize all spec.specly

# Force sonnet via Max
SPECVERSE_AI_MODEL=claude-sonnet-4-6 spv realize all spec.specly

# CI without Max auth
SPECVERSE_AI_PROVIDER=anthropic ANTHROPIC_API_KEY=sk-... \
  spv realize all spec.specly

# Local Ollama (free, runs on your laptop)
SPECVERSE_AI_PROVIDER=openai-compatible \
  SPECVERSE_AI_BASE_URL=http://localhost:11434/v1 \
  SPECVERSE_AI_MODEL=qwen3-coder:30b \
  spv realize all spec.specly

# Inspect prompts without spending tokens
SPECVERSE_AI_PROVIDER=stub spv realize all spec.specly
```

## Debugging

`SPECVERSE_VERBOSE` has two levels:

```bash
SPECVERSE_VERBOSE=1 spv realize all spec.specly 2>&1 | grep "✗"
# Prints `✗ per-owner LLM emit [<rule-id>] for <Owner>: <head>` for
# every validator fire, timeout, or γ-stub fallback. Operational
# diagnostics for production-style runs.

SPECVERSE_VERBOSE=2 spv realize all spec.specly
# Adds engine-internal logging: instance-factory loader, typescript
# engine generator-loading, generated-CLI exception traces. Useful for
# engine-developer-level debugging.
```

Production runs stay quiet by default.

## CLI flags that intersect with AI

`spv realize all` flags (the main consumer):

```
-o, --output <dir>        output root (default: generated/code)
-m, --manifest <file>     implementation manifest
--static                  full static frontend (no @specverse/runtime dep)
--estimate                print L1/L2/L3 breakdown without running emit
```

`spv ai analyse` adds run-folder management flags (`--label`, `--facts`, `--verify`, `--realize`) — see `spv ai analyse --help` for the current set. Most are workflow-tuning knobs; the env vars above govern the AI itself.

## Programmatic access

```typescript
import { resolveModel } from '@specverse/engines/ai';
import { generateText } from 'ai';

const model = resolveModel();  // honours SPECVERSE_AI_PROVIDER + SPECVERSE_AI_MODEL
const { text } = await generateText({
  model,
  system: 'You are a helpful assistant.',
  prompt: 'Explain CURVED operations in one sentence.',
});
```

For session reuse across multiple calls (claude-cli's `--resume`, saves ~75% input tokens on multi-call runs):

```typescript
import { randomUUID } from 'crypto';
const sessionId = randomUUID();
const model = resolveModel({ sessionId });   // pass into every generateText call in this run
```

## Reference: internal / non-user env vars

These exist but you shouldn't normally set them — most are set by the framework itself or used only for tests.

| Env var | Purpose |
|---|---|
| `MCP_SERVER=1` | Set by the generated MCP server to switch the resolver to `stub` |
| `SPECVERSE_USER_CWD` | Captured by the `spv` binary entry point so subprocesses can resolve user-relative paths |
| `SPECVERSE_AI_SESSION_ID` | Cross-process claude-cli session sharing (used by the harness) |
| `SPECVERSE_OFFLINE=1` | Force V2 import resolver to fail-loud at npm step — for air-gapped CI and tests |
| `SPECVERSE_REGISTRY_URL` | Override the SpecVerse community registry endpoint (default `https://specverse-lang-registry-api.vercel.app`) |
| `PLAYWRIGHT_BASE_URL` | Test-runtime override for the contract-test base URL in the generated `playwright.contract.config.ts`. Generation-time default comes from the implementation manifest's `frontend.baseUrl` or `frontend.devUrl` field (falling back to `http://localhost:5173`). NOT a framework env var — exposed to YOU when you run `playwright test`. |

## See also

- [SPECVERSE-AI-ARCHITECTURE.md](./SPECVERSE-AI-ARCHITECTURE.md) — internals: prompt flow, session-resume mechanics, the two-stage verify pattern
- [SPECVERSE-TOOLING.md](./SPECVERSE-TOOLING.md) — full CLI reference
- [Vercel AI SDK docs](https://ai-sdk.dev/docs) — upstream reference for `generateText` / `generateObject` / `streamText`
