# Chiron and Hephaestus — the Embodied Teacher and Explorables

Once Prometheus has forged a gated `V9LessonArtifact`, two subsystems turn that
verified data structure into something a learner actually experiences.
**Chiron** is the teacher who delivers it — a persona, an integrity policy, and
a delivery tier, never a chat box that freestyles. **Hephaestus** is the
workshop that makes the idea manipulable — a real Nyx sky or a real Kalika orbit
the learner can perturb, not an animation. The two are deliberately small,
composable libraries (`libs/v9/chiron/src`, `@oshun/v9-chiron`;
`libs/v9/hephaestus/src`, `@oshun/v9-hephaestus`) that sit on top of the same
verified skeleton: Chiron reads the artifact's `narrative.socraticTurns` and
`assessment.retrievalCheck`; Hephaestus produces the `explorables[]` that the
artifact's definition-of-done requires. Both refuse to operate on an ungated
lesson — `planLessonDelivery` throws `UngatedLessonError` and
`assertComputedExplorableDoD` throws when no real computed explorable reached
its success state — so neither can present something the seven gates did not
clear.

The reason they work the way they do is the V9 thesis applied to delivery: an
LLM proposes, a verifier disposes, and **nothing reaches a learner that was not
demonstrated**. A teacher persona is a re-skin over immutable gated truth (the
`mentor` ref is regenerable surface; `groundTruth` is not), so the same lesson
can be voiced by a warm generalist or a grounded historical figure without
re-verifying a single claim. An explorable's `successState.reachable` is set
`true` only after a kernel actually produced the result — the Sun's right
ascension really did sweep ≈360° across the slider, the symplectic integrator
really did conserve energy below tolerance. This page is the features-side
companion for the delivery half of V9; the hub for the full set is
[../V9_features.md](../V9_features.md), and the upstream forge that hands these
two their input is documented in
[Prometheus — the Lesson Forge](./prometheus-lesson-forge.md).

## What ships, honestly

The **policy and projection logic is real and tested.** Chiron's three modules —
integrity-mode resolution (`integrity-modes.ts`), the avatar→voice→text delivery
fallback (`delivery.ts`), and the persona-fronted delivery plan
(`lesson-delivery.ts`) — are exercised by `chiron.spec.ts` (11 cases, green in
this audit). Hephaestus's two computed explorables and the G4 definition-of-done
(`explorable-runtime.ts`) are exercised by `hephaestus.spec.ts` (7 cases,
green), and they call the **real substrates, not mocks**: the Nyx sky explorable
invokes `calculateSunPosition` from `@nyx/ephemeris`, and the Kalika orbit
explorable integrates a Kepler Hamiltonian through `@kalika/symplectic`'s
velocity-Verlet.

Three honest boundaries deserve naming up front:

- **The live runtimes are injected, not in this code.** `@oshun/v9-chiron`
  depends on `@oshun/contracts` and nothing else — there is no avatar renderer,
  no speech runtime, no Psyche emotion model wired in. The integrity modes and
  the delivery-fallback shape Chiron mirrors are genuinely shipped, but in the
  **Metis Python backend** (`services/metis/src/metis/schemas/tutoring.py`),
  which already defines `TutoringIntegrityMode`, `TutoringDeliveryMode`,
  `TutoringDeliveryFallback*`, and `supports_barge_in` / `supports_turn_taking`.
  Chiron-lite is the TypeScript policy that produces the plan those runtimes
  execute; the embodied face and barge-in voice loop are the boundary, not a
  fabricated capability.
- **`planLessonDelivery` is not yet wired into the player.** It is implemented
  and tested, but the consumer-facing lesson player
  (`libs/v9/experience/src/lesson-player.ts`) projects the artifact
  independently rather than calling it. Both read the same gated artifact; they
  are parallel projections, and the delivery plan is not currently on the path
  the experience layer renders. This page describes what Chiron _computes_, and
  is explicit where that output is not yet consumed.
- **Generative and game-bridge explorables are spec/seam, not shipped kernels.**
  Hephaestus ships exactly two computed-kernel builders (Nyx sky, Kalika orbit).
  The `generative-widget` path lives in `@oshun/v9-theia`
  (`generative-widgets.ts`) and **fails loud** without a `HeadlessWidgetRunner`;
  the `game-bridge` kind exists in the contract enum but **no V9 builder
  constructs one** today. Where the source monolith promises "learn-by-playing,"
  the explorable runtime backs the _contract_ for it, not an implementation.

## The embodied teacher (Chiron)

### Integrity modes — the academic-integrity floor

`integrity-modes.ts` encodes the rule that keeps a tutor honest: it may
_support_ the learner's work but never _substitute_ for it. The four modes are
the same ones the Metis live-voice tutor exposes:

```ts
type IntegrityMode = 'teach' | 'hint' | 'practice' | 'do-not-complete-for-me';
```

`resolveTutorAction(mode, request)` is the gate. A `TutorRequest` carries a
`kind` (`explain` / `full-answer` / `hint` / `check-my-work`) and an
`isGradedWork` flag, and the function returns a `TutorActionVerdict`
(`{ allowed, action, reason }`) where `action` is one of
`explain | hint | socratic | check | refuse`. The hard rule sits before the
per-mode switch: a request for a **full answer to the learner's graded work**
under `do-not-complete-for-me` _or_ `practice` is refused outright and
redirected to a hint (`integrity-modes.ts:48`). The rest of the policy:

| Mode                     | `explain`  | `full-answer` (graded)       | `full-answer` (ungraded) | `check-my-work` |
| ------------------------ | ---------- | ---------------------------- | ------------------------ | --------------- |
| `teach`                  | `explain`  | `explain`                    | `explain`                | `check`         |
| `hint`                   | `hint`     | `hint`                       | `hint`                   | `check`         |
| `practice`               | `socratic` | **`refuse`** (→ hint)        | `socratic`               | `socratic`      |
| `do-not-complete-for-me` | `hint`     | **`refuse`** (not `allowed`) | `hint`                   | `check`         |

The defaults matter: `practice` is Socratic for everything (the learner does the
work), and `do-not-complete-for-me` will still `check` work or `hint`, but
`allowed` is `false` the moment a full answer is requested. The spec asserts the
floor directly —
`resolveTutorAction('do-not-complete-for-me', { kind: 'full-answer', isGradedWork: true })`
returns `allowed: false, action: 'refuse'`, and the same holds in `practice`
mode.

### Delivery fallback — richest embodiment, disclosed downgrade

A lesson is delivered through the richest embodiment available and degrades
gracefully, but **never silently**. `delivery.ts` defines the order
richest-first
(`DELIVERY_FALLBACK_ORDER = ['live_avatar', 'live_voice', 'text']`), with text
as the guaranteed floor (`DeliveryCapabilities.text?: true`). `resolveDelivery`
walks the order from the requested tier, picks the first available mode, and —
critically — pushes a disclosure string for every tier it had to skip:

```mermaid
flowchart TD
  R["requested: live_avatar"] --> A{avatar available<br/>& in budget?}
  A -- yes --> AM["active: live_avatar<br/>state: native"]
  A -- no --> D1["disclose: 'live_avatar unavailable; falling back'"]
  D1 --> V{voice available?}
  V -- yes --> VM["active: live_voice<br/>state: fallback"]
  V -- no --> D2["disclose: 'live_voice unavailable; falling back'"]
  D2 --> TM["active: text<br/>state: fallback (the floor)"]
```

The returned `DeliveryResolution` carries `requestedMode`, the resolved
`activeMode`, a `state` of `native` or `fallback`, the `fallbackChain` that was
considered, a headline `reason` (`avatar-runtime-unavailable` /
`voice-runtime-unavailable` / `native`), and the per-tier `disclosures[]`. So a
learner who asked for a face and got a voice is _told_ the avatar was
unavailable; one who fell all the way to text sees both downgrades. The spec
covers all three landings: native when the avatar is up, `live_voice` (with a
non-empty disclosure list) when only voice is up, and `text` when neither is.
This is the contract-level expression of the source's promise that "a learner is
never silently given a lesser experience."

### One disclosed synthetic teacher

`persona.ts` ships exactly one P1 persona, and discloses that it is synthetic.
`ChironPersona` binds a stable `id` (the value stamped into
`V9LessonArtifact.mentor`), a `ChironPersonality` of five Hathor facets in
`[0,1]` (`warmth`, `curiosity`, `patience`, `rigor`, `humor`), a Psyche
`voicePackRef`, a literal `synthetic: true`, a `disclosureLabel`, and an
`integrityDefault`. The shipped `WARM_GENERALIST_PERSONA` is:

| Field              | Value                                                          |
| ------------------ | -------------------------------------------------------------- |
| `id`               | `chiron:warm-generalist`                                       |
| `personality`      | warmth 0.9, curiosity 0.95, patience 0.9, rigor 0.8, humor 0.5 |
| `voicePackRef`     | `psyche:voice/chiron-warm-en`                                  |
| `synthetic`        | `true`                                                         |
| `disclosureLabel`  | "AI teacher (synthetic voice). Reconstructions are labeled."   |
| `integrityDefault` | `teach`                                                        |

`CHIRON_PERSONAS` holds exactly this one entry, and `getChironPersona(id)`
resolves it. The disclosure label is not decoration — it is the Sekhmet/Metis
representation rule made structural: a synthetic teacher must announce that it
is synthetic. Multiple faces, a domain specialist, and a grounded historical
figure are P2 (see below); P1 ships one honest voice.

### Binding a gated lesson to a session

`lesson-delivery.ts` is where the persona, the integrity mode, and the delivery
tier combine over a forged artifact. `planLessonDelivery(input)` does five
things in order:

1. **Refuse the ungated.** If `isV9LessonPublishable(artifact)` is false (any of
   the seven gates failed), it throws `UngatedLessonError` — nothing ungated
   reaches a learner. The spec proves this by flipping G2 to `pass: false` and
   asserting the throw.
2. **Resolve the persona.**
   `input.persona ?? getChironPersona(artifact.mentor) ?? WARM_GENERALIST_PERSONA`
   — the artifact's stamped mentor wins, with the warm generalist as the floor.
3. **Resolve the integrity mode.**
   `input.integrityMode ?? persona.integrityDefault`.
4. **Resolve delivery.**
   `resolveDelivery(input.requestedDelivery, input.capabilities)`.
5. **Project the turns.** Each `narrative.socraticTurns` entry becomes a
   `DeliveryTurn { role, text, claimRefs }`, **preserving `claimRefs`** so every
   spoken line still traces back to a grounded claim in `groundTruth`.

A worked call from the spec: deliver the gated Olbers'-paradox lesson,
requesting `live_voice` with `{ avatar: false, voice: true }`. The plan comes
back with `persona.id = 'chiron:warm-generalist'`, `integrityMode = 'teach'`
(the persona default), `delivery.activeMode = 'live_voice'`, a single turn whose
`claimRefs` is `[0]`, a `retrievalPrompt` of "Why is the sky dark?" pulled
straight from `assessment.retrievalCheck.prompt`, and the synthetic-teacher
`disclosure`. The plan is the thing the live tutoring/avatar runtime would then
execute.

One honest subtlety worth stating: `planLessonDelivery` computes a per-turn
`resolveTutorAction` verdict (a mentor turn is treated as an `explain` request,
which under `practice` resolves to `socratic`), but in P1 the **turn text is
passed through unchanged** either way — the "Socratic mentor keeps the question
form" because the forged narrative is _already_ Socratic, not because this
function rewrites it. The integrity enforcement with teeth is the live-Q&A path
(`resolveTutorAction` applied to a learner's real request) and the
ungated-lesson refusal — not a post-hoc rewrite of forged turns. The code is
honest about this; the comment says exactly what the line does.

### Where the live runtime is — and Chiron-full (P2)

The embodied face, the barge-in/turn-taking voice loop, and pacing adapted to
detected engagement are the **injected boundary**. The Metis backend already
implements the voice-runtime surface (`supports_barge_in`,
`supports_turn_taking`, VAD config in `tutoring.py`), and Psyche ships emotion
models (`libs/psyche/emotion-recognition`, `libs/psyche/emotion-engine`) — but
neither is wired into `@oshun/v9-chiron`, so engagement-adaptive pacing is
spec/planned at the V9 layer.

The two richer persona promises live in `@oshun/v9-theia` (`chiron-full.ts`,
P2), with real logic and the runtime as the gated remainder:

- **Cross-session memory.** `buildSessionMemory(traces)` projects a learner's
  Mnemosyne `MASTERY_LEVELS` concept traces into `struggledConcepts` (≤
  beginner) and `masteredConcepts` (≥ advanced), and `chironOpeningLine(memory)`
  opens a returning session on what was tricky last time — "Last time, X was
  tricky — want to revisit it?" See
  [Threads and the Mastery Loop](./threads-and-mastery-loop.md).
- **Grounded historical personas.**
  `buildHistoricalPersona({ figure, sources, isLivingVoice?, consentRecordId? })`
  requires ≥1 Nisaba source pin or throws `UngroundedPersonaError` (the figure
  may say nothing the sources don't ground), stamps a `reconstructionLabel`, and
  **refuses a living voice without a consent record** (the V3 voice-clone
  registry / Sekhmet gate). This is the "Stoic from Epictetus' actual words"
  promise as enforceable code, not an invitation to invent personal claims. See
  [Governance and Boundaries](./governance-and-boundaries.md).

## The workshop (Hephaestus) — explorables you can manipulate

### The Explorable contract

Every explorable is a manipulable surface bound to a concept node, defined by
`V9ExplorableSchema` (`libs/contracts/src/v9/explorable.ts`). Three kinds in
increasing generative risk:

| Kind                | Trust    | Grounding required           | Status in V9                        |
| ------------------- | -------- | ---------------------------- | ----------------------------------- |
| `computed-kernel`   | highest  | a `kernelRef` (enforced)     | **shipped** (Nyx sky, Kalika orbit) |
| `generative-widget` | verified | a Sophia/grounding pin       | seam (`theia`, fails loud)          |
| `game-bridge`       | bridged  | a pin into a V2–V8 substrate | contract only (no builder)          |

Two pieces of the schema carry the weight. **`params[]`** are the canvas knobs
(`name`, optional `label`, `min`, `max`, `default`, optional `step`/`unit`); a
`superRefine` rejects `min > max` or a `default` outside the range, so a slider
cannot be born outside its verified regime. **`successState`** is the reachable
target: a `description`, a machine-checkable `predicate`, a boolean `reachable`
set true _only after the state was demonstrated_, and an optional `evidence`
string. The top-level `superRefine` enforces that a `computed-kernel` explorable
must be grounded by a `kernelRef`, not a pin — a computed surface cannot pretend
its truth came from a citation. `isV9ExplorableShippable(ex)` is simply
`ex.successState.reachable`, and that boolean is the entire basis of the G4
gate.

### The Nyx sky explorable — real ephemeris, not animation

`nyx-sky-explorable.ts` turns a cosmology concept into a "time-travel" sky.
`buildNyxSkyExplorable({ conceptId, startUnixMs, days?, samples? })` steps a
date slider across `days` (default 365) in `samples` points (default 366) and,
at each step, computes the Sun's true apparent position with the **real**
kernel: `dateToJd(...)` then `calculateSunPosition(jd)` from `@nyx/ephemeris`,
which places the Sun at the antipode of Earth's VSOP-style heliocentric ecliptic
position and converts ecliptic→equatorial with an obliquity correction
(`generator.ts:842`). The explorable's success state is a _measured_ fact, not
an assertion: the total forward sweep of the Sun's right ascension should be
≈`360 · days / 365.24219` (one tropical year). `reachable` is true only when
every sample is finite **and** the measured sweep lands within 10% of that
expectation (`nyx-sky-explorable.ts:81`). The `evidence` string records the
actual numbers ("swept 359.x° (expected 360.0°)"), and the
`provenance.contentHash` is a sha256 of the rounded `(dayOffset, raDeg, decDeg)`
series — a determinism binding. The spec runs this from J2000 and asserts the
sweep lands between 350° and 370° over a year, and that a 30-day window sweeps
≈30° and still reaches its _scaled_ success state — the predicate scales with
the span, so a short window is honest about being a short window.

### The Kalika orbit explorable — real symplectic integrator

`kalika-orbit-explorable.ts` turns a physics concept into "a planet you can
perturb." It wraps `buildOrbitExplorable` from `@oshun/v9-lesson-explorables`,
which builds the 2-D Kepler separable Hamiltonian `H = p²/2 − GM/|q|`, hands it
to `integrateCanonical(..., { method: 'velocity-verlet' })` from
`@kalika/symplectic`, and reads back the trajectory plus
`analyzeEnergyBehavior`'s backward-error energy diagnostic. The initial state is
perihelion `(r₀, 0)` with the vis-viva tangential speed `v₀ = √(GM(1+e)/r₀)`,
and the period comes from the semi-major axis `a = r₀/(1−e)` via Kepler's third
law. The success state is the measured fact that a symplectic integrator
**conserves energy**: `reachable` is true only when the orbit was actually
computed (`orbit.computed`), the relative energy drift is finite, and
`|drift| < maxEnergyDrift` (default `1e-3`). The explorable binds two real knobs
— an `eccentricity` slider (`0`–`0.89`) and a `GM` slider (`0.1`–`10`) — so
changing a parameter recomputes the actual solution rather than scrubbing an
animation. The spec confirms a circular orbit conserves energy below `1e-3` and
that an `eccentricity: 0.5` orbit binds its slider default to `0.5`.

### The definition-of-done and the G4 gate

`explorable-runtime.ts` is the rule that every P1 lesson ships **≥1 computed
explorable whose success state was actually reached**.
`evaluateExplorableDoD(explorables)` filters to `computed-kernel`, counts how
many pass `isV9ExplorableShippable`, and returns an `ExplorableDoDReport`
(`{ ok, total, computedKernel, reachableComputedKernel, reasons }`) where `ok`
is true iff at least one reachable computed-kernel explorable exists. The
`reasons[]` are specific and human: "no computed-kernel explorable (a P1 lesson
needs ≥1 Nyx/Kalika computed explorable)" or
`explorable "<title>" never reached its success state: <description>`.
`assertComputedExplorableDoD` throws on failure (a build-time block);
`explorableDoDToGateVerdict` maps the report to a `V9GateVerdict`.

This report **is** the lesson's G4 verdict. In the Prometheus forge,
`composeGates` (`libs/v9/prometheus/src/gates.ts:133`) calls
`evaluateExplorableDoD(input.explorables)` and folds the result into the
seven-gate struct as `G4`, alongside Aletheia's G1/G2/G6, the pedagogy G3, the
quality G5, and the provenance G7. So Hephaestus does not merely _describe_
completeness — it _decides_ it, and a lesson with a generated-but-unverified
explorable cannot clear the gate. The mechanics of the full gate suite are in
[Subsystem Map and Gates](./subsystem-map-and-gates.md), and the kernels that
back the values these explorables compute are catalogued in
[Atlas — Wonder Resolution](./atlas-wonder-resolution.md).

### Generative and game-bridge explorables — the honest seam

For ideas without a pre-built kernel, the design calls for a generated,
verified-not-just-plausible widget. That verifier exists —
`verifyGenerativeWidget(spec, runner?)` in `@oshun/v9-theia`'s
`generative-widgets.ts` — and it is a **fail-loud seam**: with no
`HeadlessWidgetRunner` wired it throws `WidgetRunnerNotConfiguredError`, and
even with a runner it returns `{ shipped: false, reason }` unless the widget
both compiled _and_ reached its success state under the headless check. Only
then does it mint a `generative-widget` `V9Explorable`. This is the correct
shape for a real-but-absent integration: refuse to fabricate rather than ship a
plausible widget. The `game-bridge` kind — the V2 racing mode as an
applied-physics lab — is present in `V9ExplorableKindSchema` but has **no
builder in `libs/v9`**; treat "learn-by-playing" as a
contracted-but-unimplemented promise.

## How the two halves connect

Chiron and Hephaestus bracket the lesson: Hephaestus's output is gated _into_
the artifact upstream, and Chiron reads that gated artifact _out_ to the
learner.

```mermaid
sequenceDiagram
    participant Forge as Prometheus forge
    participant Heph as Hephaestus
    participant Art as V9LessonArtifact
    participant Chiron as Chiron (planLessonDelivery)
    participant Player as Lesson player / live runtime

    Forge->>Heph: buildNyxSky / buildKalikaOrbit (real kernels)
    Heph-->>Forge: V9Explorable (successState.reachable computed)
    Forge->>Heph: evaluateExplorableDoD(explorables) → G4
    Note over Forge: composeGates folds G4 into G1–G7
    Forge->>Art: emit gated artifact (mentor ref stamped)
    Chiron->>Art: isV9LessonPublishable? (else UngatedLessonError)
    Chiron->>Chiron: resolve persona + integrity + delivery tier
    Chiron-->>Player: LessonDeliveryPlan (turns w/ claimRefs, retrievalPrompt, disclosure)
```

The seam is the `V9LessonArtifact` itself, and the skeleton/surface split is
what lets the two stay decoupled: Hephaestus contributes G4-verified
`explorables[]` to the **gated, immutable-post-gate artifact**, while Chiron's
`mentor` ref is **regenerable surface** (grouped with `narrative`/`media` in the
contract) — re-skinning the teacher never touches the gated explorable or the
grounded claims in `groundTruth`. That is the whole point of the two-hash design
(`skeletonHash` vs `surfaceHash`) described in [Overview](./overview.md): a
popular lesson is gated once, then delivered by any persona.

## Related

- [Overview](./overview.md) — the product, the artifact, and the
  skeleton/surface split
- [Subsystem Map and Gates](./subsystem-map-and-gates.md) — where G4 sits in the
  seven-gate contract and how `composeGates` folds it in
- [Prometheus — the Lesson Forge](./prometheus-lesson-forge.md) — the upstream
  pipeline that builds and gates the artifact Chiron delivers
- [Atlas — Wonder Resolution](./atlas-wonder-resolution.md) — the concept graph
  and the kernels that compute the values an explorable manipulates
- [Threads and the Mastery Loop](./threads-and-mastery-loop.md) — Mnemosyne
  traces, cross-session memory, and the retrieval checkpoint Chiron ends on
- [The Braid, the Commons, and Films](./braid-commons-and-films.md) — the Agora
  creator surface and the Manim explainer-film path (sibling Theia subsystems)
- [Governance and Boundaries](./governance-and-boundaries.md) —
  synthetic-teacher disclosure, grounded-persona consent gating, and the
  injected-runtime seams
- The hub: [../V9_features.md](../V9_features.md)
