# Narration

> Produce time-aligned natural-language narration for a Mobius simulation, explaining the physics happening at each visible moment in clear prose. Generates the narration.segments[] sub-tree of SimSpec from physics, primitives, and execution telemetry. Run after primitive-generator and (typically) after simulation execution.

- **Type:** Skill
- **Install:** `agentstack add skill-leventilo-mobius-narration`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [leventilo](https://agentstack.voostack.com/s/leventilo)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [leventilo](https://github.com/leventilo)
- **Source:** https://github.com/leventilo/mobius/tree/main/skills/narration

## Install

```sh
agentstack add skill-leventilo-mobius-narration
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# narration

## 1. Purpose and scope

This skill writes the `narration.segments[]` array of the SimSpec. Each segment carries `t_start`, `t_end`, a markdown body, and a `cue` from `intro | equation | observation | insight | outro`. Segments cover `simulation.time.t0` to `tmax` without gaps or overlaps, per Rule 10 of `simspec-author`.

Two consumers. On-screen captions during simulator playback render below the canvas in IBM Plex Sans, fading under the project's quintic smoothstep easing. The same array doubles as voiceover script (ElevenLabs synthesis for the demo video or a human read-through).

What this skill does NOT do. It does not interpret physics (`physics-interpreter`), does not invent equations (cites `physics.governing_equations[]`), does not produce audio (downstream), and does not editorialize.

## 2. Input

The populated SimSpec. Two modes.

Wet mode: telemetry available. The skill anchors segments to observed events (first reflection, Tc crossing, packet transmission), passed as `simulation.events[]`.

Dry mode: telemetry unavailable. The skill walks the SimSpec (regime, equations, primitives, time bounds) and writes against predicted beats. Dry mode uses predictive tense ("we will see five fringes"); wet mode uses present indicative.

Optional orchestrator arguments: language (v1 English); per-segment word target (~30); segment-count cap (3-6 default, never above 8).

## 3. Output — the 3-paragraph contract (R5)

The skill writes the entire `narration.*` subtree of the SimSpec, conforming to `simspec.schema.json#/$defs/narration`. It has two co-equal outputs:

1. **The structured 3-paragraph card** — three short bodies of prose (`what`, `why`, `try_it`) plus a `claims_referenced[]` bijection anchor and an optional `caveats[]` list. This is the editorial readout shown alongside the simulator. Static, paper-anchored, written once.
2. **The time-aligned `segments[]` array** — same content as before (intro / equation / observation / insight / outro), gap-free coverage from `t0` to `tmax`. This is the voiceover/caption track that plays during simulation. Adjacent endpoints touch (`segment[i].t_end == segment[i+1].t_start`).

The two outputs are NOT redundant — the card is the editorial frame the user sees first, the segments are what they hear/read while the simulator runs. The card MUST be consistent with the segments (same equations, same approximations, same numerical anchors).

### 3.1 The three paragraphs

`narration.what` — 1–2 sentences, 30–400 chars, plain English. **What is on screen.** Names at least one `primitives[].id` (the user-facing label) or one `paper.figures[].id`. No inline LaTeX. No marketing. Answers: "what am I looking at?".

`narration.why` — 2–3 sentences, 60–800 chars, with inline KaTeX between `$...$` for at least one `physics.governing_equations[]` entry. **Why this picture.** Enumerates the active approximations from `physics.approximations[]` (e.g. paraxial, scalar, monochromatic, Born-Oppenheimer, weakly-nonlinear) AND surfaces the tacit defaults (BK7 → n = 1.5, ℏ = m = 1, T/J = 2.3, etc.) that turn the equation into a number. Answers: "why does this picture come out the way it does?".

`narration.try_it` — 1–2 sentences, 40–400 chars. **Which slider tests which claim.** References at least one `viz.sliders[].label` AND at least one `paper.numerical_claims[i]` value (citing the paper figure or section). Answers: "what should I move, and what should I expect?".

### 3.2 Bidirectional binding (R5 critical)

`narration.claims_referenced[]` lists the claim ids (each `paper.numerical_claims[i].id`) that this narration cites. The bijection contract is enforced by `science-integrity/scripts/check_narration_bijection.py`:

- **Forward direction.** Every `paper.numerical_claims[i].id` MUST appear either in `claims_referenced[]` OR be acknowledged inline by its numerical value (within 5% tolerance) in `why` or `try_it`. No orphan claims.
- **Reverse direction.** Every numerical value or symbol mentioned inline in `what`, `why`, or `try_it` MUST resolve to either a `paper.numerical_claims[i]` entry, a `physics.constants[]` entry (with `source` in `{paper, standard}`), or a `viz.sliders[].default`. No ungrounded numbers.

Failure of either direction emits an `integrity.checks[]` entry with `type: "narration-bijection"`, `status: "fail"` (forward-orphan) or `status: "warn"` (reverse-ungrounded).

### 3.3 Segments — same as v0.1, plus claims_referenced consistency

Each `markdown` body is plain prose with optional inline LaTeX between `$...$`. No headings, lists, tables. Backslashes JSON-escaped. KaTeX out of scope; the renderer treats `$...$` as KaTeX when available else raw text.

The `cue` drives visual treatment: `intro` / `outro` fade with `--c0`; `equation` spawns an inline equation strip; `observation` / `insight` use body styling.

Numerical anchors used inside any `segment.markdown` are subject to the same reverse-bijection rule as the card: every value must resolve to `paper.numerical_claims[]`, `physics.constants[]`, or `viz.sliders[].default`. Phase 3 review (§5) runs the lexicon check on segments AND on the card.

### 3.4 Ineligible papers

When `eligibility.tier == "ineligible"` (see `science-integrity/SKILL.md`), this skill does not run. The orchestrator routes the paper to `viz.scene_archetype = "explainer"` (see `viz-mapper/templates/explainer.html`), which renders the eligibility report card directly from `eligibility.reasons[]`. No narration is written for these papers.

When `eligibility.tier == "best_effort"`, this skill runs normally but MUST populate `narration.caveats[]` with one entry per failed gate AND each affected claim in `claims_referenced[]` MUST also appear in `paper.numerical_claims[i].verification` with `status` in `{"partially-verified", "unverified"}`. Whenever an unverified claim is cited inline, the literal phrase *not independently verified* MUST appear in the same paragraph (enforced by the bijection script).

## 3.5 Output format (mandatory)

You MUST emit your final output as a single fenced ```json block at the END
of your reply, with NO prose after the closing fence. The orchestrator parses
that block by regex (`/```(?:json)?\s*\n([\s\S]*?)\n\s*```/`) and ignores
everything else in your text content.

The canonical artifact for `narration` (the 3-paragraph card of §3.1 plus the
time-aligned segments of §3.3, plus a flattened `narration_md` rendition for
the orchestrator's Phase F consumer):

```json
{
  "what":    "1-2 sentences plain English: what is plotted, what the user sees.",
  "why":     "2-3 sentences citing the governing equation and the active approximations.",
  "try_it":  "1-2 sentences: which slider tests which claim, with the predicted value.",
  "claims_referenced": ["lambda", "d", "delta_x"],
  "caveats": [],
  "segments": [
    { "t_start": 0.0, "t_end": 0.5, "cue": "intro", "markdown": "..." },
    { "t_start": 0.5, "t_end": 1.0, "cue": "outro", "markdown": "..." }
  ],
  "narration_md": "# What\n\n...\n\n# Why\n\n...\n\n# Try it\n\n...\n"
}
```

Required keys: `what`, `why`, `try_it` (the 3-paragraph card per `simspec.schema.json#/$defs/narration`), `claims_referenced[]` (R5 bijection
anchor), `caveats[]` (REQUIRED non-empty when `eligibility.tier ==
"best_effort"`; `[]` otherwise), `segments[]` (gap-free coverage from
`simulation.time.t0` to `tmax`), and `narration_md` (the same content
rendered as a single Markdown string the orchestrator's Phase F consumer
reads via `pickArtifact`).

`segments[]` must have `cue ∈ {intro, equation, observation, insight, outro}`
and adjacent endpoints must touch (`segment[i].t_end == segment[i+1].t_start`).

If the SimSpec is `eligibility.tier == "ineligible"`, this skill MUST NOT
run. If the orchestrator dispatches it anyway, emit:

```json
{ "error": "eligibility.tier == ineligible; refusing to narrate per SKILL.md §3.4" }
```

If the bijection check (Phase 3 review) fails on a fabricated noun, replace
the body with the safe fallback:

```json
{
  "what": "(narration unavailable for this paper)",
  "why":  "(narration unavailable for this paper)",
  "try_it": "(narration unavailable for this paper)",
  "claims_referenced": [],
  "caveats": ["narration_audit fail: fabricated noun detected in Phase 3 lexicon check"],
  "segments": [{ "t_start": 0, "t_end": 1, "cue": "intro", "markdown": "(narration unavailable for this paper)" }],
  "narration_md": "(narration unavailable for this paper)"
}
```

DO NOT emit prose, headings, or follow-up questions after the closing fence —
they break the orchestrator's downstream consumption and get silently dropped.

## 4. Persona and voice rules

Audience: an undergraduate physics student or curious non-physicist. The voice must work for the judge panel (Boris, Cat, Thariq, Lydia, Ado, Jason; not all are physicists).

Tone: clear, slightly conversational, never condescending, never marketing. Active voice, short sentences. Concepts named by their proper terms once introduced. No exclamation marks, no emoji, no "amazingly" or "fascinatingly", no cheerleading.

Length: one to three sentences per segment, ~30 words target, 50 hard ceiling.

Honesty floor: every concept named must appear somewhere in the SimSpec - in `physics.governing_equations[].variables`, `physics.constants[]`, `physics.approximations[]`, `primitives[].name`, or a paper claim.

Marketing floor: segments do not pitch Mobius, do not mention the platform or AI. They describe the physics.

## 5. Segment-generation algorithm

Three phases.

**Phase 1: outline.** Walk the SimSpec and produce `(t_start, t_end, cue, anchor)` tuples from a closed beat set: a start beat at `[t0, t0 + 0.05 dT]` always tagged `intro`; an equation beat anchored to one `physics.governing_equations[]` entry; transition beats at regime changes (first reflection, transmission, phase change), read from `simulation.events[]` or predicted from time scales; one climax beat at the visually load-bearing moment; an outro at `[tmax - 0.1 dT, tmax]`. Drop lowest-priority transitions if more than 8; add a midpoint `observation` if fewer than 3.

**Phase 2: content.** For each beat, call Opus 4.7 (`claude-opus-4-7`, temperature 0.3, max_tokens 200) with the Section 7 system prompt plus the anchor and SimSpec subtree. `equation` cues include the anchored equation as inline `$...$`. `observation` cues describe what is visible using `primitives[].name`. `insight` cues explain why, anchored to the regime or approximations.

**Phase 3: review.** The full array is re-read in one Opus 4.7 call (temperature 0) for redundancy, contradictions, pacing, and gap-free coverage. The reviewer trims, reorders, or merges; it cannot introduce new claims. Honesty-floor violators are rewritten or dropped. Gaps extend the earlier segment's `t_end`.

## 6. Cue types

`intro`. First segment. Sets scene: what the user looks at, what equation governs, what regime. No motion yet.

`equation`. The moment an equation becomes visible or active. Always includes inline LaTeX from `physics.governing_equations[]`.

`observation`. Describes a phenomenon visible on screen. Pure description. Anchored to a primitive name or event.

`insight`. Explains why. Causal. Anchored to regime, approximations, or a constant. Always follows an `observation`.

`outro`. Final segment. Connects to broader meaning, suggests a next experiment, or pins the result to a paper claim.

## 7. LLM prompt (verbatim)

The exact system prompt used in Phase 2:

> You are a science narrator for a physics simulation, in the register of Bartosz Ciechanowski's interactive essays: clear, calm, slightly conversational, never marketing. Audience: an undergraduate physics student or a curious non-physicist. You write in English. Short sentences, active verbs. Name concepts by their proper terms.
>
> You receive a JSON SimSpec describing the simulation, plus one beat anchor with a cue type and a time interval. Write the markdown body for exactly that beat. One to three sentences, target around thirty words, hard ceiling fifty. Plain prose only: no headings, no lists, no tables. Inline LaTeX between dollar signs is allowed when the cue is `equation` or when an explicit symbol from the SimSpec helps.
>
> Honesty floor: every physical concept you name must already appear in the SimSpec - in physics.governing_equations.variables, physics.constants, physics.approximations, primitives.name, or a paper claim. If a concept is not there, you cannot use it.
>
> Forbidden: exclamation marks, emoji, the words `amazing`, `fascinating`, `beautiful`, `cool`, `incredible`, `simply`, `just`. Forbidden: pitching the platform, mentioning AI or the simulator. Forbidden: speculation beyond what the SimSpec asserts. Forbidden: rhetorical questions. Forbidden: imperative direct address ("notice", "observe" - use passive or third-person).
>
> Output: only the markdown body. No JSON wrapper, no preamble, no postscript.

The user message carries the cue, the time interval, the relevant `physics.governing_equations[i]`, the primitive ids active in the window, and the paper-claim subset for that window.

## 8. Worked examples — the 3-paragraph card pattern

Three narrations across canonical paper classes. Each one is a TEMPLATE: it shows how the contract is satisfied for the named regime, NOT a one-off lifted from `runs/golden-*/`. The actual `runs/golden-*/narration.md` predates this contract and will be re-emitted by `narration` v0.2 against this spec.

Every example below is structured the same way:
- `what` names a primitive id and what is on screen.
- `why` cites at least one `physics.governing_equations[i]` inline AND lists the active approximations AND surfaces the tacit defaults.
- `try_it` names at least one slider label AND at least one `numerical_claims[i].id`.
- `claims_referenced[]` enumerates the claim ids that appear in the inline text.

### 8.1 Wave-optics archetype — Young's double-slit

Source: `runs/golden-young/{simspec,claims,figures}.json`. Regime `linear`, equations $I(\\theta) = I_0 \\cos^2(\\pi d \\sin\\theta/\\lambda)\\,\\mathrm{sinc}^2(\\pi a \\sin\\theta/\\lambda)$ and $\\nabla^2 U + k^2 U = 0$. Approximations: `paraxial`, `Fraunhofer`, `scalar-field`, `monochromatic`. Tacit defaults: $\\lambda$ derived from SPDC degenerate pair.

```json
{
  "what": "A monochromatic plane wave passes through the double-slit-aperture and lands on the screen-intensity detector as a regular pattern of bright and dark vertical bands inside a slowly-decaying envelope (paper Fig. 3).",
  "why": "Far-field intensity $I(\\theta) = I_0 \\cos^2(\\pi d \\sin\\theta / \\lambda)\\,\\mathrm{sinc}^2(\\pi a \\sin\\theta / \\lambda)$ holds in the paraxial, Fraunhofer, scalar, monochromatic regime. Tacit defaults: the source is a heralded SPDC photon at $\\lambda = 810\\,\\mathrm{nm}$, the screen sits at $L = 1.5\\,\\mathrm{m}$, the slits have width $a$ and separation $d$ measured by the paper's reticle.",
  "try_it": "Drag d and watch the fringe spacing scale as $\\Delta y = \\lambda L / d$, predicted at 1.96 mm for the paper's parameters and measured at 2.0 mm in Fig. 3 (within 5 %). Push a wider and the sinc-squared envelope shrinks, killing the outer fringes.",
  "claims_referenced": ["lambda", "d", "b", "L", "delta_x", "V"],
  "caveats": []
}
```

Bijection check: every `paper.numerical_claims[i].id` (`lambda`, `d`, `b`, `L`, `delta_x`, `V`) appears in `claims_referenced[]`. Every inline number resolves: `810 nm` → `lambda`; `1.5 m` → `L`; `1.96 mm` → derived from `lambda`, `L`, `d`; `2.0 mm` → `delta_x`. The slider labels `d`, `a` resolve to `viz.sliders[]` entries. Forward and reverse pass.

### 8.2 Chaos-and-ODE archetype — Lorenz attractor

Source: `runs/golden-lorenz/{simspec,claims,figures}.json`. Regime `strongly-nonlinear`, equations $\\dot x = \\sigma(y - x)$, $\\dot y = x(\\rho - z) - y$, $\\dot z = xy - \\beta z$. Approximations: `Galerkin-3-mode-truncation`, `Boussinesq`. Tacit defaults: dimensionle

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [leventilo](https://github.com/leventilo)
- **Source:** [leventilo/mobius](https://github.com/leventilo/mobius)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-leventilo-mobius-narration
- Seller: https://agentstack.voostack.com/s/leventilo
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
